第 34 章 进程组、会话与作业控制 (Process Groups, Sessions, and Job Control)

      +

      核心结论

      • 进程组 = 共享 PGID 的进程集合;会话 = 进程组集合;会话和进程组都是抽象概念,支持 shell 作业控制;登录 shell 是 session leader(控制进程)。

      • 会话共享一个控制终端(/dev/tty);session leader 是 controlling process;终端断开 → kernel 发 SIGHUP 给 controlling process。

      • 前台进程组独占终端 I/O:终端信号(SIGINT/SIGQUIT/SIGTSTP)发到前台进程组所有成员;后台进程组读终端 → SIGTTIN,写终端(TOSTOP)→ SIGTTOU。

      • setpgid(pid, pgid) 改进程组:pid=0 → 调用者;pgid=0 → 同 PID;限制——pid 必须是自身或子;不能改已 exec 子;不能改 session leader;同 session 内才能改。

      • setsid() 创建新会话:调用进程成为 session leader + process group leader;失去控制终端;进程组 leader 调 setsid 失败 EPERM。

      • tcgetpgrp/tcsetpgrp 管理前台进程组:shell 用它们切换 job 前后台;要求 fd 指向控制终端。

      • SIGHUP 链反应:终端断开 → kernel 给 controlling process 发 SIGHUP;shell 收到后给所有 jobs 发 SIGHUP;controlling process 终止 → kernel 给前台进程组发 SIGHUP + SIGCONT。

      • 孤儿进程组(orphaned):组内无成员的父进程在同会话的另一个进程组中;变成孤儿且有 stopped 成员时 → kernel 发 SIGHUP + SIGCONT 给所有成员。

      本章主旨

      本章深入 UNIX 进程的两级层次结构——进程组与会话——以及它们如何支持 shell 作业控制。读者应掌握:进程组与会话的定义与关系、PGID/SID 的获取与设置(getpgrp/setpgid/getsid/setsid)、控制终端的建立与去除、session leader 与 controlling process 的角色、前台/后台进程组与终端 I/O 仲裁、SIGHUP 链反应机制(terminal disconnect → controlling process → 所有 jobs → 前台进程组)、orphan process group 与 SIGHUP+SIGCONT、作业控制信号(SIGTSTP/SIGTTIN/SIGTTOU/SIGCONT)、正确处理 SIGTSTP 的 5 步模式(reset DFL → raise SIGTSTP → unblock → reblock → reestablish handler)。这是写 shell、终端应用(vi/less)、daemon 化的基础。

      一、核心概念

      本章围绕 6 个核心概念展开:先讲进程组与会话的两级层次,再到控制终端与前台/后台进程组、SIGHUP 链反应、作业控制信号、孤儿进程组。

      概念 定义 + 重要性 实现提示

      进程组与会话的两级层次

      进程组:共享 PGID 的进程集合(job);会话:进程组集合;session leader 通过 setsid 创建会话并成为首个进程组;进程组 leader 的 PID = PGID;fork 继承父的 PGID/SID;新会话无控制终端。

      §34.1;图 34-1 展示 shell + find + wc + sort + uniq 的层次;查询 /proc/PID/stat 看 PGID/SID。

      setpgid(pid, pgid) 改进程组

      pid=0 → 调用者;pgid=0 → 同 PID;限制:pid 必须是自身或子(ESRCH);不能改已 exec 子(EACCES);不能改 session leader(EPERM);同 session 内才能改(EPERM)。

      §34.2;shell 同时 fork 父与子调 setpgid——避免 race(任意一端先 exec 都失败);父忽略 EACCES。

      setsid() 创建新会话

      调用者成为 session leader + process group leader;失去控制终端;进程组 leader 调 setsid 失败 EPERM;典型模式——fork 后父 _exit,子 setsid(避免是进程组 leader)。

      §34.3;daemon 化第 37 章用 setsid;/dev/tty open 验证无控制终端(ENXIO)。

      控制终端与前台/后台进程组

      session leader 打开未控制的终端 → 成为 controlling terminal + controlling process;tcgetpgrp/tcsetpgrp 查询/设置前台进程组;只前台能读终端;后台读 → SIGTTIN(默认 stop);后台写(TOSTOP)→ SIGTTOU。

      §34.4-§34.5;TIOCNOTTY ioctl 去除控制终端;TIOCSCTTY 显式建立(BSD 风格);ctermid 返回 "/dev/tty"。

      SIGHUP 链反应

      终端断开 → kernel SIGHUP 给 controlling process;shell 收 SIGHUP → 给所有 jobs 发 SIGHUP;controlling process 终止 → kernel 给前台进程组发 SIGHUP(+SIGCONT 在 Linux);nohup 命令 → 免疫 SIGHUP。

      §34.6;catch_SIGHUP.c 演示;disc_SIGHUP.c 验证 controlling process 终止后前台进程组也收 SIGHUP;controlling process 不终止 → 不发。

      作业控制信号与孤儿进程组

      作业控制信号:SIGTSTP(suspend)/SIGCONT(resume)/SIGTTIN(后台读)/SIGTTOU(后台写);正确处理 SIGTSTP 5 步(DFL → raise → unblock → reblock → reestablish);孤儿进程组有 stopped 成员 → SIGHUP+SIGCONT。

      §34.7;job_mon.c 演示管道作业;handling_SIGTSTP.c 正确 handler;orphaned_pgrp_SIGHUP.c 验证 SIGHUP+SIGCONT;orphan 进程组 SIGTTIN/SIGTTOU → read/write EIO。

      二、详细笔记

      34.1 进程组与会话概述

      What:进程组 = 共享 PGID 的进程集合(job 概念同义);会话 = 进程组集合;两级层次支持 shell 作业控制。

      Why:理解进程组与会话是写 shell、终端应用、daemon 化的基础;它们定义了进程间的「作业」关系和信号/终端 I/O 仲裁。

      How

      进程组(§34.1):

      • 每个进程有 PGID;进程组 leader PID = PGID。

      • 生命周期:leader 创建 → 最后成员离开(终止或加入其他组)。

      • 新进程继承父的 PGID。

      • 可用 killpg(pgid, sig)kill(-pgid, sig) 发信号到整个进程组。

      会话(§34.1):

      • 每个进程有 SID;session leader PID = SID。

      • 所有 session 内进程共享一个控制终端。

      • session leader 首次打开未控制终端 → 建立控制终端 + 成为 controlling process。

      • 一个终端最多是一个会话的控制终端。

      • 新进程继承父的 SID。

      shell 作业控制流程(§34.1):

      • 登录 shell = session leader + controlling process + 自己进程组的唯一成员。

      • shell 启动每个命令/pipeline → 创建新进程组(job)。

      • & 后缀 → 后台进程组;否则 → 前台进程组。

      • 终端信号(SIGINT/SIGQUIT/SIGTSTP)→ 前台进程组所有成员。

      前台 vs 后台进程组(§34.5):

      • 前台进程组:可自由读写终端;可接收终端信号。

      • 后台进程组:默认可写(除非 TOSTOP);读 → SIGTTIN;写(TOSTOP)→ SIGTTOU。

      • 只有一个前台进程组——tcsetpgrp 切换。

      When:写 shell、终端应用、daemon、监控脚本;理解为何后台进程不能读终端。

      Example

      # 摘自《The Linux Programming Interface》第 34 章
      $ echo $$                    # PID of shell (SID, PGID)
      400
      $ find / 2>/dev/null | wc -l &    # background job (PGID 658)
      $ sort < longlist | uniq -c       # foreground job (PGID 660)
      # Session 400: shell (PGID 400), find/wc (PGID 658), sort/uniq (PGID 660)

      34.2 进程组 setpgid/getpgrp

      Whatsetpgid(pid, pgid) 改进程组;getpgrp() 取调用者 PGID。

      Why:shell 用 setpgid 创建 job;fork 后父与子都要调——避免 race。

      How

      // 摘自《The Linux Programming Interface》第 34 章
      #include <unistd.h>
      pid_t getpgrp(void);                    /* 调用者 PGID */
      int setpgid(pid_t pid, pid_t pgid);     /* 返回 0 或 -1 */
      pid_t getpgid(pid_t pid);               /* SUSv3 BSD 风格(Linux/glibc 支持) */

      setpgid 参数:

      • pid=0 → 调用者;pgid=0 → 与 pid 相同(创建新进程组)。

      • setpgid(0, 0)setpgid(getpid(), getpid())setpgid(getpid(), 0)

      setpgid 限制(违反返回错误):

      • pid 必须是自身或子(ESRCH)。

      • 调用进程、pid 指定进程、目标进程组必须在同 session(EPERM)。

      • pid 不能是 session leader(EPERM)。

      • pid 已 exec 的子不能改(EACCES)。

      shell fork 后双调模式(§34.2):

      // 摘自《The Linux Programming Interface》第 34 章 — Listing 34-1
      childPid = fork();
      switch (childPid) {
      case 0: /* Child */
          if (setpgid(0, pipelinePgid) == -1) /* Handle error */
              /* exec */
      default: /* Parent (shell) */
          if (setpgid(childPid, pipelinePgid) == -1 && errno != EACCES)
              /* Handle error */
          /* other things */
      }

      要点:

      • 双调——父与子都 setpgid 同一值。

      • 父忽略 EACCES(可能子已 exec 改了 PGID;正常情况下成功)。

      • race:父可能先调(子未 fork 完)→ ESRCH;子可能先 exec → EACCES。

      When:写 job-control shell;fork 后立即调整子进程组。

      Example:见上述 Listing 34-1。

      34.3 setsid() 创建新会话

      Whatsetsid() 创建新会话;调用者成为 session leader + process group leader;失去控制终端。

      Why:daemon 化(脱离控制终端);隔离进程组与会话。

      How

      // 摘自《The Linux Programming Interface》第 34 章
      #include <unistd.h>
      pid_t setsid(void);    /* 返回新 SID 或 -1 */

      行为:

      • 调用者 PID = PGID = SID(同时成为两个 leader)。

      • 失去控制终端(断开任何现有连接)。

      • 失败条件:调用者是进程组 leader → EPERM。

      • PID 不被重用——kernel 保证新 PID 不匹配任何现有 PGID/SID(即使旧 leader 已退出)。

      典型模式(§34.3):

      // 摘自《The Linux Programming Interface》第 34 章 — Listing 34-2
      // 摘自 pgsjc/t_setsid.c
      if (fork() != 0)
          _exit(EXIT_SUCCESS);    /* 父退出,确保子不是进程组 leader */
      if (setsid() == -1)
          errExit("setsid");
      /* 子现在是新 session leader + 进程组 leader */
      if (open("/dev/tty", O_RDWR) == -1)   /* 验证无控制终端 */
          errExit("open /dev/tty");          /* ENXIO */

      getsid(§34.3):

      // 摘自《The Linux Programming Interface》第 34 章
      pid_t getsid(pid_t pid);  /* pid=0 → 调用者;HP-UX 限制同 session */

      When:daemon 化(详见第 37 章);创建完全独立的进程组。

      Example:见上述 Listing 34-2。

      34.4 控制终端与 controlling process

      What:每个会话可有 1 个控制终端;session leader 首次打开终端(非 O_NOCTTY)→ 建立控制终端 + 成为 controlling process。

      Why:理解控制终端是理解 SIGHUP、终端信号、TIOCNOTTY 的基础。

      How

      建立与去除(§34.4):

      • session leader open 终端(无 O_NOCTTY)→ 控制终端 + controlling process。

      • TIOCNOTTY ioctl(Linux)→ 去除进程关联控制终端。

      • TIOCSCTTY ioctl(BSD 风格,Linux 也支持)→ 显式建立控制终端。

      • /dev/tty open → 获取控制终端 fd(无控制终端时 ENXIO)。

      • tcgetsid(fd) → 返回控制终端关联的 SID。

      继承与 exec(§34.4):

      • 控制终端跨 fork 继承。

      • 控制终端跨 exec 保留。

      • 一旦失去 → 不能再次获得(除非新 session leader)。

      ctermid(§34.4):

      // 摘自《The Linux Programming Interface》第 34 章
      #include <stdio.h>
      char *ctermid(char *ttyname);  /* 返回 "/dev/tty" */
      • L_ctermid 常量定义 buffer 大小。

      • ttyname 非 NULL → 写入 buffer;NULL → 返回静态 buffer(非 reentrant)。

      When:写终端应用;daemon 化前去除控制终端。

      Example:见上述 Listing 34-2。

      34.5 前台/后台进程组与 tcgetpgrp/tcsetpgrp

      What:每个终端有前台进程组;tcgetpgrp/tcsetpgrp 查询/设置;只前台能读终端。

      Why:shell 用这些 API 实现 job 前后台切换;后台 job 不会抢终端输入。

      How

      // 摘自《The Linux Programming Interface》第 34 章
      #include <unistd.h>
      pid_t tcgetpgrp(int fd);          /* 返回前台 PGID 或 -1 */
      int tcsetpgrp(int fd, pid_t pgid); /* 设置前台 PGID */

      要点:

      • fd 必须指向调用进程的控制终端。

      • tcsetpgrp pgid 必须是调用进程同 session 内某进程组的 PGID。

      • 无前台进程组时 tcgetpgrp 返回 > 1 且不匹配任何 PGID(SUSv3 规定)。

      • 实现为 ioctl TIOCGPGRP/TIOCSPGRP。

      信号生成(§34.5):

      • 后台进程组读终端 → SIGTTIN(默认 stop)。

      • 后台进程组写终端(TOSTOP)→ SIGTTOU(默认 stop)。

      • SIGTTIN 被 block/ignore → read 返回 EIO。

      • SIGTTOU 被 block/ignore → write 允许(TOSTOP 被忽略)。

      SIGTTOU 特殊函数(§34.7.2):

      • tcsetpgrp/tcsetattr/tcflush/tcflow/tcsendbreak/tcdrain → 即使没设 TOSTOP 也可能 SIGTTOU。

      • SIGTTOU block/ignore → 这些函数成功。

      When:写 job-control shell;理解为何后台 cat 读终端会被 stop。

      Example

      // 摘自《The Linux Programming Interface》第 34 章
      /* Shell 在 fg %1 时调 */
      tcsetpgrp(STDIN_FILENO, job_pgid);   /* 设置 job 为前台 */
      /* fg 命令结束,job 终止或 stop 时,shell 调 */
      tcsetpgrp(STDIN_FILENO, getpgrp());  /* 恢复 shell 自己为前台 */

      34.6 SIGHUP 信号与链反应

      What:终端断开 → kernel 发 SIGHUP 给 controlling process;shell 收 SIGHUP 后给 jobs 发 SIGHUP;controlling process 终止 → kernel 给前台进程组发 SIGHUP(+SIGCONT)。

      Why:SIGHUP 是「终端断开通知」+「daemon 重读配置的通用信号」。

      How

      触发场景(§34.6):

      • 终端驱动检测 disconnect(modem 信号丢失)。

      • 终端窗口关闭(master 侧 fd 关闭)。

      链反应(§34.6):

      1. 终端断开 → kernel SIGHUP 给 controlling process。

      2. shell 收 SIGHUP → 在退出前给所有 jobs 发 SIGHUP(可捕获)。

      3. controlling process 终止 → kernel 给前台进程组发 SIGHUP + SIGCONT(Linux)。

      默认行为:

      • SIGHUP 默认动作:终止进程。

      • 进程 catch/ignore SIGHUP → read 终端返回 EOF。

      nohup 命令(§34.6.1):

      • 启动命令前设 SIGHUP = SIG_IGN → 免疫 SIGHUP。

      • bash disown:从 job 列表移除(shell 不再发 SIGHUP)。

      SIGHUP 其他用途:

      • daemon 重读配置(手动 kill -HUP pid)。

      • 孤儿进程组通知(§34.7.4)。

      When:写 daemon → 处理 SIGHUP 重读配置;写 shell → 给 jobs 发 SIGHUP;理解终端断开的传播。

      Example

      // 摘自《The Linux Programming Interface》第 34 章 — catch_SIGHUP.c
      struct sigaction sa;
      sigemptyset(&sa.sa_mask);
      sa.sa_flags = 0;
      sa.sa_handler = handler;
      sigaction(SIGHUP, &sa, NULL);
      /* 测试:两个 catch_SIGHUP 在同 PG;shell 退出只发给 shell 创建的 PG */

      34.7 作业控制信号

      What:SIGTSTP/SIGCONT/SIGTTIN/SIGTTOU 是作业控制 4 大信号;终端特定字符触发。

      Why:理解作业控制信号的语义是写 shell、终端应用的前提。

      How

      信号含义(§34.7):

      • SIGTSTP(终端 suspend 字符通常 Ctrl-Z)→ stop 前台进程组。

      • SIGCONT → resume stopped 进程(默认行为)。

      • SIGTTIN → 后台进程组 read 终端(默认 stop)。

      • SIGTTOU → 后台进程组 write 终端(TOSTOP)或后台调用特定 ioctl(默认 stop)。

      SIGTSTP 处理(§34.7.3):

      • 屏显程序(vi/less)需 catch SIGTSTP → reset terminal → suspend → restore。

      • 关键陷阱:catch SIGTSTP 不真正 stop(已非默认)——handler 必须自己安排 stop。

      • 错误做法:handler raise(SIGSTOP) → 父 wait 看到 SIGSTOP 而非 SIGTSTP。

      • 正确 5 步(Listing 34-6):

      // 摘自《The Linux Programming Interface》第 34 章 — Listing 34-6
      // 摘自 pgsjc/handling_SIGTSTP.c
      static void tstpHandler(int sig) {
          sigset_t tstpMask, prevMask;
          int savedErrno = errno;
          printf("Caught SIGTSTP\n");        /* UNSAFE */
          signal(SIGTSTP, SIG_DFL);          /* 1. reset DFL */
          raise(SIGTSTP);                    /* 2. raise another SIGTSTP */
          sigemptyset(&tstpMask);            /* 3. unblock */
          sigaddset(&tstpMask, SIGTSTP);
          sigprocmask(SIG_UNBLOCK, &tstpMask, &prevMask);
          /* 此处 process 被 SIGTSTP 真正 suspend;SIGCONT 后恢复 */
          sigprocmask(SIG_SETMASK, &prevMask, NULL);  /* 4. reblock */
          /* 5. reestablish handler */
          struct sigaction sa;
          sigemptyset(&sa.sa_mask);
          sa.sa_flags = SA_RESTART;
          sa.sa_handler = tstpHandler;
          sigaction(SIGTSTP, &sa, NULL);
          errno = savedErrno;
      }

      避免处理被 ignore 的信号(§34.7.3):

      • 应用应只在 SIGTSTP/SIGTTIN/SIGTTOU 非 SIG_IGN 时才设置 handler(非 job-control shell)。

      • 同样适用 SIGINT/SIGQUIT/SIGHUP——避免破坏 shell 的 ignore 设置。

      SIGCONT 例外(§34.7.2):

      • kernel 允许同 session 任意进程互相发 SIGCONT(不考虑 credentials)。

      • 原因:set-UID 程序改 credentials 后 shell 仍能 resume 它。

      When:写终端应用(vi/less/top)→ catch SIGTSTP + reset terminal;理解后台进程不能读终端。

      Example:见上述 Listing 34-6。

      34.7.4 孤儿进程组与 SIGHUP+SIGCONT

      What:进程组变成孤儿(无成员有同 session 内另一进程组的父)+ 有 stopped 成员 → kernel 发 SIGHUP + SIGCONT 给所有成员。

      Why:避免 stopped 进程永远无人唤醒的「僵尸」场景。

      How

      孤儿定义(§34.7.4):

      • 进程组孤儿 = 「每个成员的父进程要么是组内成员,要么不在该 session 内」。

      • 即:组内任何成员都没有「同 session 但不同进程组」的父进程。

      • session leader 永远在孤儿进程组(其父不在新 session 内)。

      触发场景(§34.7.4):

      • 进程组中 stopped 成员 + 父进程(同 session 不同组)终止 → 进程组变孤儿 → kernel 发 SIGHUP + SIGCONT。

      • 已 orphan 进程组被特权进程 stop → 仍无法 resume → 永久停在那。

      • kernel 阻止 SIGTSTP/SIGTTIN/SIGTTOU 停 orphan 进程组成员(被 SIG_DFL 时直接丢弃;handler 仍正常递送)。

      orphan 进程组的特殊语义(§34.7.4):

      • read 终端(SIGTTIN 应被发)→ 返回 EIO(而非 SIGTTIN)。

      • write 终端(SIGTTOU 应被发)→ 返回 EIO。

      • tcsetpgrp → ENOTTY;tcsetattr/tcflush/tcflow/tcsendbreak/tcdrain → EIO。

      orphan 进程组 + 停止成员 + 父死 → kernel 行为(Listing 34-7):

      • 给所有成员发 SIGHUP(通知已 disconnect)+ SIGCONT(resume)。

      When:理解为何后台 stopped 进程父死时会被 SIGCONT;写 daemon 化程序处理 SIGHUP。

      Example

      // 摘自《The Linux Programming Interface》第 34 章 — Listing 34-7
      // 摘自 pgsjc/orphaned_pgrp_SIGHUP.c
      /* 父创建子,子 raise(SIGSTOP) 或 pause;父 exit → 子变 orphan */
      /* 父退出后 kernel 给所有成员发 SIGHUP + SIGCONT */

      三、关键图表

      进程组与会话 API 速查
      API 功能

      getpgrp()

      取调用者 PGID

      setpgid(pid, pgid)

      改进程组(pid/调用者;pgid/同 pid)

      getpgid(pid)

      取指定进程 PGID(SUSv3 BSD 风格)

      getsid(pid)

      取指定进程 SID

      setsid()

      创建新会话(成为 leader;进程组 leader 失败)

      tcgetpgrp(fd)

      取终端前台 PGID

      tcsetpgrp(fd, pgid)

      设置终端前台 PGID

      ctermid(buf)

      返回 "/dev/tty"

      TIOCNOTTY ioctl

      去除控制终端

      TIOCSCTTY ioctl

      显式建立控制终端(BSD)

      killpg(pgid, sig)

      发信号到进程组(= kill(-pgid, sig))

      SIGHUP 链反应场景
      触发 后果

      终端断开 / 关闭终端窗口

      kernel SIGHUP → controlling process

      shell 收 SIGHUP

      shell 给所有自创进程组发 SIGHUP

      controlling process 终止

      kernel 给前台进程组发 SIGHUP(+SIGCONT Linux)

      进程组变 orphan 且有 stopped 成员

      kernel 给所有成员发 SIGHUP + SIGCONT

      nohup cmd

      cmd 的 SIGHUP 被设为 SIG_IGN,免疫链反应

      kill -HUP pid

      手动触发 SIGHUP(daemon 用作重读配置)

      作业控制信号对照
      信号 触发 默认动作

      SIGTSTP

      终端 suspend 字符(Ctrl-Z)

      Stop

      SIGTTIN

      后台进程组 read 终端

      Stop(被 block → EIO)

      SIGTTOU

      后台进程组 write 终端(TOSTOP)/后台 ioctl

      Stop(被 block → 允许)

      SIGCONT

      fg/bg 命令 / 进程恢复

      Continue(即使 SIG_DFL)

      SIGSTOP

      kill -STOP

      Stop(不可 catch/block/ignore)

      四、思维导图

      mindmap
        root((第 34 章 进程组会话))
          两级层次
            进程组 job PGID
            会话 SID
            session leader setsid
            进程组 leader setpgid
            fork 继承 PGID SID
            控制终端 /dev tty
          setpgid
            pid 0 调用者
            pgid 0 同 pid
            子必须未 exec
            父忽略 EACCES
            fork 后双调
            shell 模式
          setsid
            session leader
            进程组 leader
            失去控制终端
            fork 后父 _exit
            daemon 化基础
            进程组 leader 失败
          控制终端
            session leader open
            O NOCTTY 不建立
            TIOCNOTTY 去除
            TIOCSCTTY BSD
            跨 fork exec 保留
          前台后台
            tcgetpgrp tcsetpgrp
            后台读 SIGTTIN
            后台写 SIGTTOU
            TOSTOP 标志
            孤儿 read write EIO
          SIGHUP 链反应
            终端断开
            shell 给 jobs 发
            controlling process 终止
            前台进程组 SIGCONT
            nohup 免疫
            daemon 重读配置
          作业控制信号
            SIGTSTP SIGCONT
            SIGTTIN SIGTTOU
            SIGTSTP handler 5 步
            DFL raise unblock reblock
            reestablish handler
            屏显程序 vi less
          孤儿进程组
            定义 无同 session 父
            stopped 成员 SIGHUP CONT
            SIGTSTP 丢弃
            SIGTTIN EIO
            SIGTTOU EIO

      五、重点与易错点

      1. 进程组与会话是两级层次——进程组 = job(共享 PGID),会话 = 进程组集合(共享 SID);用于 shell 作业控制。

      2. session leader 通过 setsid 创建会话——同时是进程组 leader;失去控制终端;进程组 leader 调 setsid 失败 EPERM。

      3. setpgid 必须 fork 后立即调——子 exec 后不能再改(EACCES);shell 双调(父与子)避免 race。

      4. fork 后双调 setpgid 模式——父与子都 setpgid(pid, pgid);父忽略 EACCES(可能子已 exec);这是 job-control shell 的标准模式。

      5. 控制终端由 session leader 首次打开建立——O_NOCTTY 标志可阻止;TIOCNOTTY 去除;TIOCSCTTY(BSD 风格)显式建立。

      6. tcsetpgrp/tcgetpgrp 用于前台进程组切换——shell fg %1 时调;要求 fd 指向控制终端;pgid 必须是同 session 内进程组。

      7. 后台进程读终端 → SIGTTIN(默认 stop)——被 block/ignore 时 read 返回 EIO;后台写(TOSTOP)→ SIGTTOU;TOSTOP 未设但后台调 tcsetpgrp 等也发 SIGTTOU。

      8. SIGHUP 链反应:终端断开 → kernel SIGHUP 给 controlling process;shell 收 → 给所有 jobs 发 SIGHUP;controlling process 终止 → kernel 给前台进程组 SIGHUP(+SIGCONT Linux)。

      9. nohup 命令通过设 SIGHUP = SIG_IGN 免疫链反应——bash disown 从 job 列表移除(shell 不再发)。

      10. 正确处理 SIGTSTP 的 5 步:handler 内 signal(SIGTSTP, SIG_DFL) → raise(SIGTSTP) → unblock SIGTSTP → reblock → reestablish handler;handler raise(SIGSTOP) 错误(父 wait 看到 SIGSTOP 而非 SIGTSTP)。

      11. SIGCONT 特殊:同 session 任意进程可互相发(不考虑 credentials)——set-UID 程序仍能被 shell resume。

      12. 应用应只在 job-control 信号非 SIG_IGN 时设 handler——非 job-control shell 设 SIG_IGN;避免破坏 shell 的 ignore 设置。

      13. 孤儿进程组定义:组内无成员有「同 session 但不同进程组」的父进程;session leader 永远在孤儿进程组。

      14. orphan 进程组 + stopped 成员 + 父死 → kernel 发 SIGHUP + SIGCONT——避免 stopped 进程永远无人唤醒。

      15. orphan 进程组的 SIGTTIN/SIGTTOU 不发——直接 read/write 返回 EIO;SIGTSTP 被 SIG_DFL 时丢弃;handler 仍正常递送。

      16. orphan 进程组的 ioctl 调用失败——tcsetpgrp → ENOTTY;tcsetattr/tcflush/tcflow/tcsendbreak/tcdrain → EIO。

      17. 进程组 leader 不能 setsid——典型模式:fork 后父 _exit,子 setsid;确保子不是进程组 leader。

      18. PID 不重用——kernel 保证新进程 PID 不匹配任何现有 PGID/SID(即使旧 leader 已退出);避免新进程意外成为 leader。

        • 跨章衔接:第 24-25 章 fork/exit 基础 → 第 26 章 wait → 第 27 章 exec + 控制终端保留 → 第 28 章 clone 标志 → 第 34 章进程组与会话;第 37 章 daemon 化(fork+setsid+chdir 等);第 62 章终端 I/O 与 TIOCNOTTY/TIOCSCTTY;第 20-22 章信号(SIGHUP/SIGTSTP/SIGCONT/SIGTTIN/SIGTTOU)。