进程组是信号投递的基本单位,会话定义终端生命周期边界;前台进程组独占终端i/o与信号,ctrl+c等作用于整个组而非单个进程。

Linux 中的进程组(Process Group)和会话(Session)是终端控制、信号传递与作业管理的核心抽象,它们共同构成 shell 作业控制(job control)的基础。理解它们的关键不在于背定义,而在于看清“谁发信号给谁”“终端挂起时发生了什么”“为什么 Ctrl+C 杀的是前台进程组而不是单个进程”。
进程组:信号投递的基本单位
每个进程属于且仅属于一个进程组,由进程组 ID(PGID)标识,通常等于该组中组长进程(group leader)的 PID。进程组不是静态容器——新进程默认继承父进程的 PGID,但可通过 setpgid(0, 0) 主动创建新组或加入已有组(需满足条件)。
- 信号如 SIGINT(Ctrl+C)、SIGQUIT(Ctrl+\)默认发送给整个前台进程组,而非单个进程
- ps -o pid,ppid,pgid,sid,tty,comm 可直观查看各进程所属的 PGID 和 SID
- shell 启动管道命令(如 ls | grep txt)时,整条管道中的所有进程会被置于同一进程组,确保中断时一并响应
会话:终端生命周期的边界
会话由一次登录(login)或调用 setsid() 创建,拥有唯一会话 ID(SID)。一个会话可包含多个进程组,但有且仅有一个**前台进程组**(foreground process group),它独占控制终端的输入输出和信号接收权。
- 当用户在终端中运行 vim 或 top,shell 会将其设为前台进程组;此时 Ctrl+C、Ctrl+Z 均作用于该组
- 后台任务(如 sleep 100 &)属于同一会话下的不同进程组,但被标记为后台,无法读取终端输入,也不会收到终端生成的信号
- 终端关闭(如 SSH 断连、xterm 关闭)时,内核向该会话的前台进程组发送 SIGHUP;若进程未忽略或捕获,将直接退出
作业控制如何依赖这两层结构
交互式 shell(如 bash)通过系统调用(tcsetpgrp、ioctl(TIOCGPGRP) 等)动态切换前台进程组,并维护内部作业表(jobs)。每次执行命令,shell 实际完成三件事:
- fork 子进程,再在子进程中调用 setpgid(0, 0) 创建独立进程组(除非禁用作业控制)
- 用 tcsetpgrp() 将该组设为终端前台,使其能读写 tty 并接收 Ctrl+C
- 父 shell 暂停自身,等待子进程组状态变化(如停止、退出),期间可响应 Ctrl+Z 或 fg/bg 命令
常见误区与调试技巧
很多“程序意外退出”或“信号收不到”问题,根源在于对进程组/会话归属判断错误:
- nohup command & 的本质:先调用 setsid() 脱离原会话,再重定向 stdin/stdout/stderr,使进程不受 SIGHUP 影响
- disown 不是“删除作业”,而是从 shell 作业表中移除记录,避免 shell 在退出时向其发送 SIGHUP
- 用 strace -e trace=setsid,setpgid,tcsetpgrp -p $PID 可跟踪某进程是否主动脱离会话或改变组归属
- 若某守护进程仍持有终端(tty 列显示 ? 或 /dev/tty),说明它未正确调用 setsid() 或未关闭所有文件描述符
不复杂但容易忽略:进程组和会话不是调度概念,也不影响 CPU 分配,它们纯粹服务于 I/O 控制与信号路由。真正决定一个进程“听不听键盘”“关不关终端就死”的,正是它所处的进程组是否为前台、是否属于当前会话。











