linux-progress-group
February 23, 2019
The linux programming interface
进城组、会话和作业控制
进程组和会话在进程之间形成了一种 两层的层次关系。 进程组: 是 相关进程的集合, 会话: 是 相关进程组的集合。其目的是为了支持shell的作业控制,而定义的抽象概念,使用户通过shell能够交互的在前台或后台运行命令
- 进程组: 一个或多个共享同一进程组标识PGID的进程组成。PGID的数值为 进程组首进程的进程ID,该进程为进程组的第一个进程 或创建该进程组的进程,子进程会继承父进程 的PGID。进程组的结束时间为,最后一个进程成员退出组的时刻 (进程可能终止或加入另一个进程组而退出进程组)
- 会话: 一组进程组的集合,由 SID 标志 (同进程ID 一样为pid_t类型)其数值为 会话首进程 的进程ID,新进程会继承 父进程的 SID。 一个会话中的所有进程共享单个控制终端,控制终端在回话首进程 首次打开一个终端时候建立,该进程成为 该终端的控制进程,在连接断开时,内核会发送信号给该进程。一个终端最多成为一个会话的控制终端。在一个会话中只有一个 进程组会成为 前台进程组,其他的成为 后台进程组,只有 前台进程组能够从控制终端中读取输入,在控制终端中输入一个信号(即 按键会产生信号, liru c-\ c-z c-c)时,该信号会被发送到 前台 进程组的所有成员。
- 通过linux特有的/proc/PID/stat 文件,能够确定任意的进程组ID和会话ID, 此外还能确定进程的控制终端的设备ID 和对应的控制进程ID
- 一个具体的例子为: 登录shell是 会话首进程和终端的控制进程, 从shell中发出的每个命令 都会导致一个或多个进程的创建(通过管道会创建多个进程)并将所有进程放到共一个进程组中,以&结束的命令 会在后台进程组中运行。所有在shell中创建的子进程都会成为该会话的一部分。
$ echo $$ $ find / 2> /dev/null | wc -l & | sort < longlist | uniq -c
进程组
- 每个进程都有一个 进程组ID, 表示该进程的所属进程组。子进程会继承 父进程的 进程组ID。
- pid getpgrp(void): 获取进程的进程组ID。进程组首进程 的进程组ID同进程ID相同
- int setpgid(pid_t pid, pid_t pgid): 将 进程ID为pid的进程 的进程组修改为pgid, pid 0 i表示设定进程自身, pgid为0 表示 设定 pid进程的进程组ID为pid。 使用该调用可以创建新的进程组,或者移动 进程 到另一个进程组, 其中存在如下限制:
- pid 参数 仅可以指定为 调用进程或 其子进程, 违反会导致 ESRCH 错误
- 在进程组之间移动, 调用进程、pid进程, 以及移动目标组必须同属于一个会话,违法导致 EPERM 错误
- 一个进程在其子进程已经执行exec后,就无法修改该子进程的进程组ID了,违法导致EACCES错误, 这个规则导致: 一个 任务, 中的所有进程 必须放置在同一个进程组中, 这一步允许shell通过killpg 来同事想进程组中的所有成员进程发送作业控制信号,2)每个子进程在exec执行程序之前,需要分配到进程组中,因为程序本身是不会修改进程组ID的。 这其中引伸出来的问题, 在于 fork 之后, 并确定 子进程,父进程的执行顺序, 因此需要在 父进程和子进程中都需要调用setpgid()来修改子进程的进程组ID, 父进程需要忽略EACCES错误
会话
- 会话是一组进程组的集合, 一个进程的会话成员关系是由其会话ID 来确定的恩,新进程会进程其父进程的会话ID。
- getsid(pid_t pid): pid 为0,那么返回调用进程的会话ID
- setsid(void): 调用进程不是 进程组 首进程,那么调用会创建一个新会话。 (为什么不能 是 进程组首进程呢?): 创建新会话的步骤如下:
- 调用几次会成为新会话的首进程 和该会话中的新进程组的首进程, 会话ID 为 进程ID, 包会新的进程组ID
- 调用进程没有控制终端,所有之前的控制终端的链接都会断开
- 如果是进程组首进程来 调用setsid, 会返回 EPERM错误,避免的方式很简单, 执行fork, 由 子进程来调用即可。 约束 调用者的身份是重要的, 因为:如果进程组 首进程调用就能够将自身迁移到一另一个会话中,而该进程组中的其他成员仍然位于原来的会话中(这里并不会创建新的进程组,因为根据定义, 进程组首进程的 进程组ID 等于 其 进程ID,如果创建的话, 那么存在进程组ID相同的两个进程组) 这回破坏进程组和会话的层级关系, 音带要求一个进程组的所有成员必须属于同一个会话
控制终端 和 控制进程
- 一个会话中可以拥有一个控制终端, 会话在创建的时候是没有控制终端的, 会话首进程 首次打开一个 没有成为其他会话的控制终端的终端时 会建立 控制终端, 会话首进程 打开 控制终端之后,会成为 该终端的控制进程, 在中断断开之后,内核会发送SIGHUP信号到 控制进程,进程 可以通过打开特殊文件 /dev/tty 来获取 终端的文件描述符 没有控制终端则返回 ENXIO 错误(可以在 进程 的标准输入输出 被重定向之后 确保自己确实在于控制终端进程通信是非常有用的 )控制终端 会被 由 fork 创建的子进程 进程并且在exec中保持
- 终端 以及 控制终端 的概念到底是怎样的? 如何对应到现实中的 键盘输入 现实生活中。
删除 进程与 控制终端之间的关联关系: ioctl(fd, TIOCNOTTY): 能够删除进程与文件描述符fd指定的 控制终端之间的关联关系, 在调用函数之后,再次打开 /dev/tty 文件的话就会失败, 调用者为 终端的控制进程,在控制基础终止时, (?为什么是终止时候,而不是调用函数之后) 会发生如下事情; 1)会话中的所有进程将会失去 与控制终端之间的关联关系, 2) 控制终端失去了与该会话 之间的关联关系, 另一个会话首进程能够获取该终端已成为控制基础, 3) 内核会想前台进程组的所有成员 发送一个SIGHUP信号,来通知他们控制终端的丢失 - ctermid(char * ttyname): 返回表示控制终端的路径名, 通常返回结果为 /dev/ttyname
前台、后台进程组:
一个会话中,在同一时刻只有一个进程能够成为前台进程,会话中的其他所有进程都是后台进程组,前台进程组 是唯一能够自由的读取和写入控制终端的进程组, 在控制终端中输入 产生信号的字符时,终端驱动器 会相应的 发送信号给前台进程组的成员
- tcgetpgrp(int fd): 函数返回文件描述符fd 所指定的中断的前台进程组的进程组ID, fd为 控制终端
- tcsetpgrp(int fd, pid_t pgid): 修改一个终端的前台进程组
SIGHUP信号:
一个控制基础时区与终端链接之后,内核会发送一个SIGHUP信号,还会发送一个SIGCONT 信号,以确保 当该进程之前被一个喜好停止时,重新开始该进程。 出现该场景的情况有:
- 中断驱动器检测到连接断开后, 表明 调制解调器(网络ssh?)或终端行上信号的丢失
- 当 工作站上的终端窗口被关闭时,发生这种清库激昂是因为最近打开的与终端窗口哦关联的伪终端主侧 的文件描述符被关闭了 ?????
控制基础接收到SIGHUP信号,会引起炼石反应, 从而导致将SIGHUP信号发送给其他的进程, 这个过程以如下两种方式发生; - 控制基础通常是一个shell, shell建立了一个 SIGHUP 信号的处理器, 这样在进程终止之前,能够将SIGHUP 信号 发送给由他 创建的各个任务。默认情况下, 这个信号会终止那些任务, 但是如果他们呢不活了这个信号,就能够知道 shell进程已经终止了
- 控制基础终止时,内核会解除会话中的所有进程与该控制终端之间的关联关系以及控制终端与会话的关联关系,并向前台进程组 发送SIGHUP信号来通知他们控制终端的丢失