layout | title | category | description | tags |
---|---|---|---|---|
post |
进程之间的关系 |
进程 |
进程之间的关系... |
父进程 子进程 写时复制 fork vfork pidhash |
进程之间除了ID连接的关系之外,内核还负责管理建立在UNIX进程创建模型之上的『家族关系』。
如果进程A分支形成进程B,进程A称之为父进程,而进程B称之为子进程。如果进程A分支若干次形成几个子进程B、C、D,则这几个子进程之间称为兄弟关系。
下面简单的列举了进程描述符中相关亲属关系的描述,进程描述符结构可以看《进程描述符》,进程数据结构可以看《进程链表》。
字段名 | 说明 |
---|---|
real_parent | 指向创建了P的进程描述符,如果P的父进程不存在,就指向进程init的描述符 |
parent | 指向P的当前父进程,当此进程终止时,必须向父进程发信号 |
children | 链表的头部,链表中的所有元素都是P创建的子进程 |
sibling | 指向兄弟进程链表中的下一个元素或前一个元素的指针,这些兄弟进程的父进程都是P |
建立非亲属关系的进程描述符字段有:
字段名 | 说明 |
---|---|
group_leader | P所在进程组的组长的描述符指针 |
signal->pgrp | P所在进程组的组长的PID |
tgid | P所在线程组的组长的PID |
signal->session | P的登录会话组长的PID |
ptrace_children | 链表的头,该聊表包含所有被debugger程序跟踪的P的子进程 |
ptrace_list | 指向锁跟踪进程的实际父进程链表的前一个和下一个元素 |
系统调用提供服务的时候会发生如下情况,当进程P1希望向另一个进程P2发送一个信号时,P1调用kill()系统调用,其参数为P2的PID,内核从这个PID导出其对应的进程描述符,然后从P2的进程描述符中取出记录挂起信号的数据结构指针。顺序扫描进程描述符的pid字段是相当低效的,为了加速查找,引入了4个散列表,分别为:
Hash表的类型 | 说明 |
---|---|
PIDTYPE_PID | 进程的PID |
PIDYTPE_TGID | 进程组的组长的PID |
PIDTYPE_PGID | 进程组的组长的PID |
PIDTYPE_SID | 会话组长的PID |
内核初始化期间动态地为4个散列表分配空间,并把它们的地址存入pid_hash数据组,一个散列表的长度依赖于可用的RAM的容量。用pid_hashfn把PID转换为表索引。pid_hash函数并不一定能保证与表的索引一一对应,不同的PID可能会存在相同的key,就会发生冲突。Linux利用链表来处理冲突。
传统的UNIX中用于复制进程的系统调用是fork,但它并不是Linux为此实现的唯一调用,Linux实现了3个。
(1) fork是重量级调用,因为它建立了父进程的一个完整副本,然后作为子进程。
(2) vfork1类似于fork,但并不创建父进程数据的副本,相反,父子进程共享数据,节省了大量的CPU。vfork设计用于子进程形成后立即执行execve系统调用,在子进程退出或开始新程序之前,父进程处于堵塞状态。
(3) clone用于产生线程,可以堆父子进程之间的共享、复制进行精确控制。
所有的3个fork机制最终都调用了kernel/fork.c中的do_fork函数,在do_fork中,大多数工作都是由copy_process函数完成的。
内核使用了写时复制(Copy-On-Write, COW)技术,为防止在fork执行时将父进程的所有数据复制到子进程。因为这样的操作使用的很长的时间,并且耗费了大量的内存。如果应用程序在进程复制之后使用exec立即加载新程序,那么就表明之前的操作完全时没有必要的,因为拷贝了父进程的数据,但立即刷出内存用于新程序。所以内核不复制进程的整个地址空间,而是只复制其页表,这样就建立了虚拟地址空间和物理内存页之间的联系。
当父子进程不允许修改彼此的页,只能够共享的读取数据,则可以共享内存区域。如果任意一个进程试图向复制的内存页中写入数据,那么处理器会向内核报告访问错误,这类错误通常会被视作缺页异常。内核然后查看额外的内存管理数据结构,检查该页是否可以用读写模式访问。如果该页只能以只读模式访问,则向应用程序报告段错误,如果是可写的,内核会创建该页专用于当前进程的副本,并与父进程独立开。
Footnotes
-
由于写时复制技术,现在不推荐使用vfork。 ↩