进程与线程的关系
在Linux中,没有线程这个概念。内核将线程与进程一视同仁,也就是说线程相当于一个标准的进程,在调度时不使用特殊策略。同时,内核也不为线程维护多余的数据结构。换句话说,在Linux中,线程仅仅是一个与其他进程分享某些相同资源,地址空间的进程。相比于Windows有很大的不同。
在Windows中,线程显示地被视为轻量进程(lightweight processes)。换句话说,在类似于Windows这样明显区分线程与进程的系统中,我们必须在进程描述符里维护特别的数据结构,指向它的不同线程,并且标记出如打开文件表,地址空间这类的共享资源。这是一种累赘。但是在Linux中,由于理念的不同,我们简单地为每个线程维护一个task_struct即可。
但是,进程和线程的创建是不一样的。都调用了clone(),而参数不同。
创建进程
在Unix和Linux系统中,我们用fork()和exec*()来创建新的进程。如果进程的数据太大,那么在fork()复制进程地址空间这个动作时,将会耗费大量的时间。特别是当fork()之后立即调用exec*()函数,加载一个新的可执行程序,这样的时间开销就显得更加没有意义。所以采用写时复制(Copy-on-Write)机制。就是说,父子进程以只读的方式共享地址空间段,直到其中某一方进行写操作,改变了既有数据。采用COW机制,那么当一个进程fork()之后立即调用exec*()时,开销就只剩下 1.为子进程复制父进程的页表。 2.为子进程创建进程描述符。
fork()实现是通过调用clone()这个系统调用,clone()接受一系列的标志作为参数,这些标志决定了在父子进程中共享哪些信息。与fork()相同,vfork()和__clone()都使用了该系统调用,区别在与传递的参数。所以重点在与clone()如何工作。
首先clone()调用do_fork(),在<kernel/fork.c>中,下图为其参数列表。
在检测了是否进行ptrace之后,do_fork()调用了copy_process()拷贝一份当前进程的样本,copy_process()函数接受传递给clone()的clone_flags标志,当前进程的起始地址,regs参数(未知用途),进程栈大小等参数,并返回一个task_struct类型的指针。
在copy_process()中检测传递的clone_flags是否符合标准,检测子进程数目是否超过系统限制的数目,当前用户进程数是否大于系统限制,更新相关数据。并完成其他工作,其中包括调用dup_task_struct()创建子进程的内核栈,以及为子进程创建新的task_struct和thread_info结构。
然后copy_process()为新进程初始化task_struct中的各项数据,包括分配PID,设置状态为TASK_UNINTERRUPTIBLE, 根据clone_flags设置相关共享信息等等。
当copy_process()执行成功后,返回do_fork()中,最终返回子进程的PID。子进程创建工作完毕。
创建线程
线程的创建就如同普通任务的创建一样,都是调用clone()函数,只不过传递相应的flags参数。
对于普通的fork()相当于使用:
clone(CLONE_VM | CONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
clone(SIGCHLD, 0);
而对于vfork():
clone(CLONE_VFORK | CLONE_VM | SIGCHLD, 0);
可用的clone()的flags值在<linux/sched.h>中。
内核线程
内核线程只能由另一内核线程产生。调用基于clone()的kernel_thread()函数。类似于应用进程创建线程函数,fn指向期望新内核线程一直执行的函数,arg用来为该函数传递参数。
int Kernel_thread(int (*fn)(void *), void *arg, unsigned long flags);