3.1 进程
每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象是线程,而不是进程。
程序本身不是进程,进程是处于执行期的程序以及相关的资源的总称。
3.2 进程描述符及任务结构
内核把进程的列表存放在叫做任务队列的双向循环链表中。链表中每一项的类型都是task_struct
,称为进程描述符的结构。进程描述符中包含一个具体进程的所有信息,包括:它打开的文件、进程的地址空间、挂起的信号、进程的状态等。
3.2.1 分配进程描述符
现在用slab分配器动态生成task_struct,所以只需要在栈顶(对向上增长的栈)创建一个新的结构:struct thread_indo
。
该结构中,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的偏移量。
movl $-8192, %eax
and %esp, %eax
此处假定栈大小为8KB。
3.2.3 进程状态
进程描述符的state域标识进程的状态。必然处于以下五种之一:
- TASK_RUNNING(运行):进程是可执行的。它可能正在执行,也可能在运行队列中等待执行。
- TASK_INTERRUPTIBLE(可中断):进程正在睡面,等待某些条件达成,内核就会把进程状态设置为运行。
- TASK_UNINTERRUPTIBLE(不可中断):就算是接到信号也不会有动作。
- __TASK_TRACED:被其他进程跟踪的进程
- __TASK_STOPPED(停止):进程停止执行。通常发生在接受到SIGSTOP SIGTST SIGTTIN SIGTTOU等信号。
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()
。
3.4 线程在Linux中的实现
从内核角度来说,没有线程这个概念。Linux将所有线程都当做进程来实现。线程仅被视为一个与其他进程共享某些资源的进程。每个线程有自己的task_struct
。所以在内核中,线程看起来像一个普通的进程。
3.4.1 创建线程
线程创建和普通进程的创建类似,只需要在调用clone()
时传递一些参数指明需要共享的资源:
clone(CLONE_VM | CLONE_FILES | CLONE_SIGHAND, 0);
这些参数表明,父子进程共享地址空间、文件系统资源、文件描述符和信号处理程序。
普通的进程实现是:
fork(SIGCHLD,0);
3.5 进程终结
进程的终结依靠do_exit()
完成:
3.5.1 删除进程描述符
wait()
这一组函数都是通过唯一一个系统调用wait4()
实现的。它挂起调用它的进程,直到其中一个子进程退出,此时函数会返回该子进程的PID。