3.1 进程

每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程

程序本身不是进程,进程是处于执行期的程序以及相关的资源的总称

3.2 进程描述符及任务结构

内核把进程的列表存放在叫做任务队列的双向循环链表中。链表中每一项的类型都是task_struct,称为进程描述符的结构。进程描述符中包含一个具体进程的所有信息,包括:它打开的文件、进程的地址空间、挂起的信号、进程的状态等。
image.png

3.2.1 分配进程描述符

现在用slab分配器动态生成task_struct,所以只需要在栈顶(对向上增长的栈)创建一个新的结构:struct thread_indo
image.png
image.png
该结构中,task域存放的事指向该任务实际task_struct的指针。

3.2.2 进程描述符的存放

PID的类型是pid_t实际上就是个int。内核把每个进程的PID存入各自的进程描述符中。

为了与老版本UNIX兼容,PID最大值默认设置为32768(short int的最大值)。可以通过修改/proc/sys/kernel/pid_max来提高上限。

在内核中访问任务要通过取得指向其task_struct的指针。
通过将汇编的and操作将栈指针的后13位取出来,这样就得到了thread_info的偏移量。

  1. movl $-8192, %eax
  2. and %esp, %eax

此处假定栈大小为8KB。

3.2.3 进程状态

进程描述符的state域标识进程的状态。必然处于以下五种之一:

  • TASK_RUNNING(运行):进程是可执行的。它可能正在执行,也可能在运行队列中等待执行。
  • TASK_INTERRUPTIBLE(可中断):进程正在睡面,等待某些条件达成,内核就会把进程状态设置为运行。
  • TASK_UNINTERRUPTIBLE(不可中断):就算是接到信号也不会有动作。
  • __TASK_TRACED:被其他进程跟踪的进程
  • __TASK_STOPPED(停止):进程停止执行。通常发生在接受到SIGSTOP SIGTST SIGTTIN SIGTTOU等信号。

image.png

3.2.6 进程家族树

所有的进程都是PID为1的init进程的后代。每个进程必有一个父进程。每个task_struct都包含一个parent指针指向其父进程的task_struct;还包含一个children的子进程链表。

3.3 进程创建

3.3.1 写时拷贝(copy-on-write)

fork()的实际开销:复制父进程的页表以及给子进程创建唯一的进程描述符。此时不复制整个进程地址空间,而是让子进程以只读方式共享父进程的地址空间,直到需要写入时,数据才会复制,从而各进程有各自的拷贝。

通常fork()之后会调用exec()运行一个可执行文件,所以这种优化可以避免拷贝大量根本就不会被使用的数据。

3.3.2 fork()

Linux通过系统调用clone()实现fork()fork()vfork()__clone()库函数都通过各自需要的参数标志调用clone(),然后由clone()调用do_fork()

image.png
image.png

3.4 线程在Linux中的实现

从内核角度来说,没有线程这个概念。Linux将所有线程都当做进程来实现。线程仅被视为一个与其他进程共享某些资源的进程。每个线程有自己的task_struct。所以在内核中,线程看起来像一个普通的进程。

3.4.1 创建线程

线程创建和普通进程的创建类似,只需要在调用clone()时传递一些参数指明需要共享的资源

  1. clone(CLONE_VM | CLONE_FILES | CLONE_SIGHAND, 0);

这些参数表明,父子进程共享地址空间、文件系统资源、文件描述符和信号处理程序

普通的进程实现是:

  1. fork(SIGCHLD,0);

3.5 进程终结

进程的终结依靠do_exit()完成:
image.png

3.5.1 删除进程描述符

wait()这一组函数都是通过唯一一个系统调用wait4()实现的。它挂起调用它的进程,直到其中一个子进程退出,此时函数会返回该子进程的PID。