3、进程创建
fork() 通过拷贝当前进程创建一个子进程,子进程与父进程的区别仅仅在于PID、PPID和某些资源和统计量。exec()函数(指所有的exec()一族的函数)负责读取可执行文件并将其载入地址空间开始运行。
3.1 写时拷贝
Linux的fork()使用写时拷贝(copy-on-write)页实现,创建子进程时,内核并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会复制,从而使各个进程拥有各自的拷贝,在此之前,资源只是以只读的方式共享。例如,fork()后立即调用exec(),就无须复制了。
fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。
3.2 fork()
Linux通过clone()系统调用实现fork(),这个调用通过一系列的参数标志来指明父、子进程需要共享的资源。
fork(),vfork(),__clone()库函数都根据各自需要的参数标志去调用clone(),由clone()去调用do_fork(),由do_fork()调用copy_process()函数,然后让进程开始运行。copy_process()函数完成的工作如下:
图摘自《Linux内核设计与实现(第三版)》
回到do_fork()时,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行,因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能会开始向地址空间写入。
3.3 vfork()
除了不拷贝父进程的页表项外,vfork()系统调用和fork()的功能相同。子进程作为父进程的一个单独的线程在它的地址空间里运行,父进程被阻塞,知道子进程退出或执行exec()。子进程不能向地址空间写入。
4、线程在Linux中的实现
从Linux内核的角度,并没有线程这个概念。Linux把所有的线程都当做进程来实现,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一里属于自己的task_struct。
4.1 创建线程
线程的创建和普通进程的创建类似,只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源,例:
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
传递给clone()的参数标志决定了新创建进程的行为方式和父子进程之间共享的资源种类,下表列举了这些clone()用到的参数标志以及它们的作用,这些参数标志是在<linux/sched.h>中定义的。
参数标志 | 含义 |
---|---|
CLONE_FILES | 共享文件描述符 |
CLONE_FS | 共享文件系统信息 |
CLONE_IDLETASK | 将PID设置为0(只供idle进程使用) |
CLONE_NEWNS | 为子进程创建新的命名空间 |
CLONE_PARENT | 将子进程的父进程置为调用者的父进程 |
CLONE_PTRACE | 继续对子进程进行跟踪 |
CLONE_UNTRACED | 防止跟踪进程在子进程上强制执行CLONE_PTRACE |
CLONE_SETTID | 将TID回写至用户空间 |
CLONE_SETTLS | 为子进程创建新的TLS |
CLONE_SIGHAND | 共享信号处理函数及被阻断的信号 |
CLONE_SYSVSEM | 共享System V信号量的撤销值 |
CLONE_THREAD | 将子进程置于父进程的线程组中 |
CLONE_VFORK | 调用vfork(),挂起父进程直至子进程退出或者调用exec() |
CLONE_STOP | 以TASK_STOPPED状态开始进程 |
CLONE_SETTLS | 为子进程创建新的TLS(thread-local storage) |
CLONE_PARENT_SETTID | 设置父进程的TID |
CLONE_CHILD_SETTID | 设置子进程的TID |
CLONE_CHILD_CLEARTID | 清除子进程的TID |
CLONE_VM | 共享地址空间(虚拟内存) |
4.2 内核线程
内核经常需要在后台执行一些操作,这种任务可以通过内核线程来完成。内核线程是独立运行在内核空间的标准进程,其与普通的进程之间的区别在于内核线程没有独立的地址空间(实际上指向地址空间的mm指针被设置为NULL),它们只在内核空间运行,从来不切换到用户空间去。内核进程和普通的进程一样,可以被调度,也可以被抢占。
内核线程启动后就一直运行,直到带哦用do_exit()退出,或者内核的其他部分调用kthread_stop()退出。
5、进程终结
一般来说,进程的析构是自身引起的,它发生在进程调用exit()系统调用时(显式或隐式地)。当进程接收到它既不能处理也不能忽略的信号或异常时,它哈有可能被动地终结。其大部分任务都要靠do_exit()(定义于kernel/exit.c)来完成。
在调用了do_exit() 之后,与进程相关联的所有资源被释放掉,进程不可运行,处于EXIT_ZOMBIE退出状态。它占用的所有内存就是内核栈、thread_info和task_struct结构。此时进程存在的唯一目的就是向它的父进程提供信息。
5.1 删除进程描述符
进程终结时所需的清理工作和进程描述符的删除被分开执行。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct结构才被释放。
wait() 这一族函数都是通过唯一的一个系统调用wait4() 来实现的,它的标准动作是挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回该子进程的PID。
当最终需要释放进程描述符时,release_tast() 会被调用,完成一系列工作,最终将进程描述符和所有进程独享的资源全部释放掉。
5.2 孤儿进程的处理
如果父进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父亲,否则这些成为孤儿的进程就会在退出时永远处于僵死状态,白白耗费内存。解决办法是给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init做它们的父进程。
给子进程找到合适的养父进程之后,只需要遍历所有的子进程并为它们设置新的父进程。
同样需要给ptraced的子进程寻找父亲,通过遍历两个单独的链表:子进程链表和ptrace子进程链表,搜索相关的兄弟进程,用两个相对较小的链表减轻遍历带来的损耗。
init进程会例行调用wait()来检查其子进程,清除所有与其相关的僵死进程。