进程与线程是全部的程序员都熟知的概念,简单来讲进程是一个执行中的程序,而线程是进程中的一条执行路径。进程是操做系统中基本的抽象概念,本文介绍 Linux 中进程和线程的用法以及原理,包括建立、消亡等。程序员
Linux 中进程的建立与执行分为两个函数,分别是 fork
和 exec
,以下代码所示:编程
int main() { pid_t pid; if ((pid = fork() < 0) { printf("fork error\n"); } else if (pid == 0) { // child if (execle("/home/work/bin/test1", "test1", NULL) < 0) { printf("exec error\n"); } } // parent if (waitpid(pid, NULL) < 0) { printf("wait error\n"); } }
fork
从当前进程建立一个子进程,此函数返回两次,对于父进程而言,返回的是子进程的进程号,对于子进程而言返回 0。子进程是父进程的副本,拥有与父进程同样的数据空间、堆和栈的副本,而且共享代码段。数据结构
因为子进程一般是为了调用 exec
装载其它程序执行,因此 Linux 采用了写时拷贝技术,即数据段、堆和栈的副本并不会在 fork
以后就真的拷贝,只是将这些内存区域的访问权限变为只读,若是父子进程中有任一个要修改这些区域,才会修改对应的内存页生成新的副本,这样子是为了提升性能。函数
fork
以后父进程先执行仍是子进程先执行是不肯定的,因此若是要求父子进程进行同步,每每须要使用进程间通讯。fork
以后子进程会继承父进程的不少东西,如:性能
父子进程的区别在于:spa
fork
以后,子进程能够执行不一样的代码段,也可使用 exec
函数执行其它的程序。操作系统
进程在运行的时候,除了加载程序,还会打开文件、占用一些资源,而且会进入睡眠等其它状态。操做系统为了支持进程的运行,必然有一个数据结构保存着这些东西。在 Linux 中,一个名为 task_struct
的结构保存了进程运行时的全部信息,称为进程描述符:线程
struct task_struct { unsigned long state; int prio; pid_t pid; ... }
进程描述符完整描述了一个进程:打开的文件、进程的地址空间、挂起的信号以及进程的信号等。系统将全部的进程描述符放在一个双端循环列表中:设计
进程描述符具体存放在内存的哪里呢?在内核栈的末尾。众所周知,进程中占用的内存一部分是栈,主要用于函数调用,不过这里说的栈通常指的是用户空间的栈,其实进程还有内核栈。当进程调用系统调用的时候,进程陷入内核,此时内核表明进程执行某个操做,此时使用的是内核空间的栈。3d
进程描述符中的 state
描述了进程当前的状态,有以下 5 种:
在使用了写时拷贝后,fork
的实际开销就是复制父进程的页表以及给子进程建立惟一的进程描述符。fork
为了建立一个进程到底作了什么呢?fork
其实调用了 clone
,这是一个系统调用,经过给 clone
传递参数,代表父子进程须要共享的资源,clone
内部会调用 do_fork
,而 do_fork
的主要逻辑在 copy_process
中,大体有如下几步:
clone
的参数,拷贝或者共享打开的文件、文件系统信息、信号处理函数以及进程的地址空间等。除了 fork
以外,Linux 还有一个相似的函数 vfork
。它的功能与 vfork
相同,子进程在父进程的地址空间运行。不过,父进程会阻塞,直到子进程退出或者执行 exec
。须要注意的是,子进程不能向地址空间写入数据。若是子进程修改数据、进行函数调用或者没有调用 exec
那么会带来未知的结果。vfork
在 fork
没有写时拷贝的技术时是有着性能优点,如今已经没有太大的意义。
进程的运行终有退出的时候,有 8 种方式使进程终止,其中 5 中为正常终止:
异常终止方式有 3 种:
exit
函数会执行标准 I/O 库的清理关闭操做:对全部打开的流调用 fclose
函数,全部缓冲中的数据会被冲洗,而 _exit
会直接陷入内核。看下面的代码:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { printf("line 1\n"); printf("line 2"); // 没有换行符 // exit(0) _exit(0); }
其中第二行输出没有 \n
,若是末尾调用的是 _exit
,则只会输出 line 1
,若是替换为 exit
,则第二行 line 2
也会输出。
进程退出最终会执行到系统的 do_exit
函数,主要有如下步骤:
此时,进程的大部分资源都被释放了,而且不会进入运行状态。不过还有些资源保持着,主要是 task_struct 结构。之因此要留着是给父进程提供信息,让父进程知道子进程的一些信息,如退出码等。
须要注意的是,若是父进程不进行任何操做,那么这些信息会一直保留在内存中,成为僵尸进程,占用系统资源,以下面的代码:
int main() { pid_t pid = fork(); if (pid == 0) { exit(0); } else { sleep(10); } }
父进程 fork 出子进程后,子进程马上退出,而父进程则进入睡眠。运行程序,观察进程状态:
能够看到,第一行进程为父进程,状态为 S
,表示其正在睡眠,而第二为子进程,状态为 Z
,表示僵尸状态(zombie
),由于此时子进程已经退出,然而 task_struct 还保存着,等待父进程来处理。
父进程如何处理?调用 wait
函数,正如本文第一段代码中所示。当父进程调用 wait
后,子进程的 task_struct 才被释放。
若是父进程先结束了呢?在父进程结束的时候,会为其子进程找新的父进程,一直往上找,最终成为 init
进程的子进程。init
子进程会负责调用 wait
释放子进程的遗留信息。
上面介绍了 Linux 中的进程,那么线程又是怎么的?网上一些说法是,Linux 中并无真正的内核线程,线程是以进程的方式实现的,只不过它们之间会共享内存。这种说法有必定道理,但并不彻底准确。
Linux 中刚开始是不支持线程的,后来出现了线程库 LinuxThreads,不过它有不少问题,主要是与 POXIS 标准不兼容。自 Linux 2.6 以来,Linux 中使用的就是新的线程库,NPTL(Native POSIX Thread Library)。
NPTL 中线程的建立也是经过 clone
实现的,而且经过如下的参数代表了线程的特征:
CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND | CLONE_THREAD | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | CLONE_SYSVSEM
部分参数的含义以下:
NPTL 所实现的线程库是 1:1 的从用户线程映射到内核线程,而且内核为了实现 POSIX 的线程标准也作了一些改动,好比对于信号的处理等。因此说 Linux 内核彻底不区分进程和线程,甚至不知道线程的存在这种说法如今是不许确的。
线程间共享代码段、堆以及打开的文件等,线程私有的部分有如下内容:
Linux 中进程与线程的使用是程序员必备的技能,而若是能了解一些实现的原理,则可使用的更加驾轻就熟。本文介绍了 Linux 中进程的建立、执行以及消亡等,对于线程的实现及其与进程的关系也进行了简单的说明。进程和线程还有更多的内容能够研究,如进程调度、进程以及线程间的通讯等。
参考