进程和线程
进程相当于执行期间的程序,除了代码以外,通常还需要包括其他一些资源,例如:打开的文件、挂起的信号、内核数据、处理器状态、地址空间、一个或多个执行线程、存放全局变量的数据段等等。
线程是在进程中活动的对象,每个线程都有一个独立的程序计数器、进程栈(在 Linux 中不区分进程和线程)、一组寄存器等等。
在 Linux 中不区分进程和线程,线程就是特殊的进程;同一个进程的线程之间可以共享虚拟内存,但是每个线程都有各自的虚拟处理器。对于 Win 或者 Solaris 来说,这些系统专门支持线程。
进程
进程的创建通常是通过 fork 来复制一个现有线程来创建一个全新的进程。调用 fork 的是父进程,生成的进程是子进程,在调用结束的时候,子进程和父进程都返回同一位置开始执行,所以 fork 返回两次,一次回到父进程、一次回到子进程。fork 完成之后,子进程调用 exec 函数创建新的地址空间,并把新的程序载入其中。最终进程通过 exit 推出执行。父进程可以通过 wait 系统调用来查询子进程是否终结,进程退出之后被设置为僵死状态,直到父进程调用 wait 为止。
特殊进程
Linux 中有pid 0, pid 1 和 pid 2 三个特殊的进程。
pid 0,即 “swapper” 进程,是 pid 1 和 pid 2 的父进程。 pid 1,即 “init” 进程,所有用户空间的进程均派生自该进程。 pid 2,即 “kthreadd” 进程,是内核空间所有进程的父进程。
进程描述符
内核把进程列表存放到任务队列中,其是一个双向循环链表。每一项都是一个 task_struct 结构,代表一个进程描述符。
进程描述符包含一个具体进程的所有信息,其内包含了一个程序打开的文件、自身的地址空间、挂起的信号、进程的状态等等。
struct task_struct {
// 进程状态
long state;
// 虚拟内存结构体
struct mm_struct *mm;
// 进程号
pid_t pid;
// 指向父进程的指针
struct task_struct __rcu *parent;
// 子进程列表
struct list_head children;
// 存放文件系统信息的指针
struct fs_struct *fs;
// 一个数组,包含该进程打开的文件指针
struct files_struct *files;
// ....
};
由于现在 x86体系结构寄存器较少,一般在进程内核栈的最低地址(进程内核栈是向低地址增长的)有一个 thread_info 的结构,在 thread_info 中有一个 task_struct 的指针。可以通过与运算(与上一个page 的大小。一般4 or 8k)得到 thread_info的地址,而后就可以得到实际的task_struct数据。
进程的状态
五种:
- TASK_RUNNING:运行状态。要么在执行,要么等待执行。
- TASK_INTERRUPTIBLE:可中断睡眠。进程等待一些条件达成之后就可以转换成运行状态,可以随着信号的到来随时准备投入运行。
- TASK_UNITERRUPTIBLE:不可中断睡眠。接收对信号也不会投入运行。此阶段不对信号进行响应。典型状态例如 fork 操作的时候,子进程在未初始化完成的时候需要设置为此状态以防止其被投入运行。
- __TASK_TRACED:被跟踪的状态,例如调试状态
- __TASK_STOPPED:停止运行状态。
陷入内核
一般程序是在用户空间运行的,但是当程序发生了系统调用、触发异常的情况下,会陷入内核中。
parent
Linux 中所有进程都是 PID=1的进程的后代,内核在启动的最后阶段启动 init 进程。该进程执行初始化脚本并执行相关程序,最终完成系统的整个启动过程。 每个进程一定有一个父进程,也可能会有多个子进程和兄弟进程。task_struct 中包含了指向父进程的指针,已经children 链表的指针。
进程创建
进程创建分成了两个步骤:fork 和 exec。
fork 通过拷贝当前进程创建一个子进程。二者区别仅仅在于PID、PPID(parent pid)和一些统计量以及一点资源。
exec 则负责读取可执行文件并将其载入地址空间开始运行。
fork
fork 通过 COW 实现,内核并不复制整个进程的地址空间,而是让父进程和子进程共享父进程的地址空间,只有在写入数据的时候,实际的数据才会被复制,在此之前,数据都是以只读当时进行的。 fork 的开销就是复制父进程的页表和给子进程创建进程描述符。
fork 的主要步骤如下:
- 为新进程创建内核栈、thread_info、task_struct,这些结构的值和父进程完全一样
- 区分父子进程。将进程描述符的一些成员恢复默认值,主要是统计信息;大部分数据并未修改。
- 将子进程设置为 TASK_UNITERRUPTIBLE,保证其不会投入运行。
- 分配一个有效的 pid 给新进程。
- 拷贝或共享:打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等等。一般情况下这些资源是当前进程的所有线程被共享的;如果不在一个进程则进行拷贝。
- 返回一个指向子进程的指针。
而后唤醒子进程并投入运行。内核尽量让子进程首先运行,目的是避免 COW 的额外开销,防止父进程首先执行的时候向地址空间写入数据。
进程终结
进程可能主动调用 exit 来结束自身,也可能隐士 main 函数返回(main 的最后都有隐式的 exit),也可能收到特殊信号或者异常被动终结。
在进程终结的时候,如果自身有子进程,需要给子进程寻找养父。否则孤儿进程在退出时永远处于僵尸状态,耗费内存。养父可以为同一进程组中的其他进程也可以是 init 进程。
进程终结的时候也需要将自身设置为 EXIT_ZOMBIE状态,即僵尸进程。此时和进程相关的所有资源已经被释放,除了内核栈、thread_info、task_struct。这些结构依然存在的目的就是向其父进程提供信息,父进程检索到信息后,进行释放。wait 函数用来挂起本进程,直到自身的一个子进程推出,此时函数返回已终结的子进程的PID,父进程进行后续处理。
内核线程
内核线程是独立运行在内核空间的标准进程。内核线程和普通进程的区别在于内核线程没有独立的地址空间。只在内核运行,不切换到用户空间中。
内核线程只能有内核线程来创建,pid=2的“kthreadd” 进程,是内核空间所有进程的父进程。