附录 F 部分习题答案 (Solutions to Selected Exercises)

      +

      核心结论

      • 附录 F 收录 TLPI 各章部分习题答案——通常为已编号的 1-2 题精选;许多答案引用 book source code(tlpi-dist/ 子目录)。

      • 核心常考的题型:(1) API 行为辨析(fork / exec / signal / fd 重复);(2) umask / 时序 race / deadlock / 性能 trade-off;(3) select/poll 行为差异;(4) Linux 行为 vs SUSv3 差异(如 flock starvability);(5) 套接字与终端特定行为。

      • 关键哲学:通过做习题熟悉 SUSv3 行为与 Linux 实测差异;参考答案是「答案参考」不是唯一正确答案。

      • 适合回顾场景:(1) 自学学习时核对答案;(2) 面试准备:(3) 调试生产问题对应知识回顾。

      附录主旨

      本附录不同于其他附录——它是「习题答案」。读者用例:(1) 自学后对照;(2) 复习特定概念时(看某个章节的答案);(3) 教学时作为参考。问题主要考「细节是否注意到」——如 dup2 与 dup3、umask race、setuid 永久销毁、fork 计数、signal 交付给哪个线程。下文按章节列举部分答案要点(详细代码见 book distribution)。

      一、核心概念

      本附录围绕 5 个核心概念:fd / dup 类、umask / 时序、setuid 销毁、fork / exec 行为、signal 与线程。

      概念 定义 + 重要性 典型习题

      fd 行为与文件描述符表

      dup/dup2/dup3 是否共享 file offset;多 fd 独立 vs 共享;close-on-exec 设置。

      5-3 (atomic_append.c 多进程 append race);5-4 (dup/dup2 重写);5-6 (fd 共享 offset)

      umask 时序与 race

      umask() 是 process-wide;多线程时 umask 不是 atomic,临时改原值不可重入。

      15-5 (umask 临时改值);15-7 (chiflag.c)

      setuid 系列

      setuid() / setreuid() / seteuid() / setresuid() 永久销毁 vs 临时销毁特权;保存调用前 euid 必须用辅助变量。

      9-1 (分析各种 set* 调用结果);9-4 (销毁特权);9-5 (销毁与恢复)

      fork / exec 与信号

      多次 fork 计数;grandchild 被 init 接管;execve 丢弃 stdio buffer 内容;SIGCHLD 送与谁。

      24-1 (fork 数 7);24-5 (双向 kill/sigsuspend);27-1/27-3 (exec path 解析);27-5 (printf 未 flush)

      signal 与线程

      signal directed by process 还是 thread;多个线程中 SIGCHLD 投递;pthread_join 自己。

      33-2 (SIGCHLD 投递);29-1 (pthread_join self deadlock)

      二、详细笔记

      F.1 Chapter 5 答案(fd 与 dup)

      5-3:TLPI source fileio/atomic_append.c 展示两个进程同时 append 2000000 bytes;不 atomic 时文件少 bytes(lseek + write 非原子)。

      5-4dup(oldfd) 重写为 fcntl(oldfd, F_DUPFD, 0)dup2(oldfd, newfd) 重写为「oldfd == newfd 时检查有效性」否则 close(newfd) + F_DUPFD newfd

      5-6:fd1/fd2 共享同一 open file description(共 file offset);fd3 独立。第一次 write "Hello," → 文件 "Hello,";fd2 共享 offset 接着写 " world" → "Hello, world";lseek(fd1, 0, SEEK_SET) 把 fd1/fd2 共 offset 拨回开头,第三 write "HELLO," → "HELLO, world";fd3 独立 offset 仍在 0,写 "Gidday world" → 头覆盖。

      F.2 Chapter 9 答案(setuid 系列)

      9-1:以 set*uid 调用后 cred 变化(记住 euid 改变时 fsuid 也变):

      a) real=2000, effective=2000, saved=2000, fs=2000   (启动)
      b) real=1000, effective=2000, saved=2000, fs=2000   (setreuid(2000, 2000))
      c) real=1000, effective=2000, saved=0, fs=2000      (seteuid(0) + saved 改)
      d) real=1000, effective=0, saved=0, fs=2000        (永久降权)
      e) real=1000, effective=2000, saved=3000, fs=2000

      9-4:用 setuid() / seteuid() 不能永久降权(必须 euid == saved 才能成功切回);要永久降权用 setresuid(ruid, ruid, ruid) 或 setreuid(ruid, ruid):

      uid_t e = geteuid();
      setuid(getuid());     setuid(e);               /* setuid() can't永久降权 */
      seteuid(getuid());    seteuid(e);               /* seteuid() 同上 */
      setreuid(-1, getuid()); setreuid(-1, e);        /* 临时降权 */
      setreuid(getuid(), getuid());                  /* 永久降权 */
      setresuid(-1, getuid(), -1);  setresuid(-1, e, -1);
      setresuid(getuid(), getuid(), getuid());       /* 永久 */

      9-5:除 setuid() 外其余等价(uid=0);setuid() 在 effective 不是 root 时不能更改 saved。

      F.3 Chapter 15 / 18 答案(fs / dir)

      15-2stat() 不更新任何 i-node 时间戳,因为它只是 fetch i-node 信息。

      15-4:glibc 实现在 sysdeps/posix/euidaccess.c,使用 access 抽象但实际看 euid。

      15-5:两阶段 umask:

      mode_t currUmask;
      currUmask = umask(0);    /* fetch current, set to 0 */
      umask(currUmask);        /* restore */

      注意:多线程下 umask 不是 atomic,应避免。

      18-1ls -li 看编译出新文件 i-node 不同——compiler 先 unlink 同名旧 + open(O_CREAT);可 unlink 运行中可执行文件。

      18-2symlink("../test/myfile", "../mylink") 是相对符号链接;相对父目录解析,因此指向父目录中不存在的文件,chmod 失败 ENOENT。

      18-9fchdir()chdir() 高效——fchdir 仅用 fd(一次性解析 inode),chdir 每次传 path 字符串 + 内核解析。循环迭代应用 fchdir 大量节省。

      F.4 Chapter 20 / 22 答案(signal)

      20-2 / 20-4:TLPI source signals/ignore_pending_sig.csiginterrupt.c

      22-2:Linux 先交付 standard signals,再 realtime signals;SUSv3 不要求此顺序但 Linux 历史上这么做。

      22-3:用 sigtimedwait 替代 sigsuspend 速度提升 25-40%。

      F.5 Chapter 24 答案(fork)

      24-1

      fork() → 2 processes
      2nd fork() 在每个中执行 → 4
      3rd fork() 在每个执行 → 7 (新创建的 + 原有 4 = 7)

      24-5:补充双向通信——父子各发信号收信号,互相 sigsuspend。

      F.6 Chapter 27 答案(exec)

      27-1execvp("xyz", argv) 先查 dir1/xyz 失败(EACCES),然后 dir2/xyz 成功。

      27-3:kernel 把 interpreter 当 cat 调用:/bin/cat -n 执行脚本显示行号。

      27-4:fork + fork + 父 waitpid;child 立即退出被 init 收养(grandchild)。目的:避免 zombie 而不依赖 SIGCHLD 设置。

      27-5printf 不 flush stdio buffer(无 newline + 不 tty 模式);exec 覆盖进程数据段 → buffer 丢 → 输出丢失。

      27-6:若设置了 SIGCHLD handler 但 handler 调 wait()——会在 system() 退同时抢 race,可能 fail ECHILD

      F.7 Chapter 34 / 35 答案(job control)

      34-1killpg(0, sig) 把整个 process group 信号——若 ourprog 在 pipe (ourprog | grep),grep 在同组也被杀。setpgid 创建独立 child group。

      34-5:unblock + raise 期间若再 ^Z,handler 重入第二次 SIGTSTP,resume 需 SIGCONT 两次。

      F.8 Chapter 44 答案(FIFO race)

      44-1:TLPI pipes/change_case.c

      44-5:server 见 EOF 后到 close(reader) 之间存在 race——client 可 open + write 数据,但 server 已 close 读端。client 写会 SIGPIPE。

      44-6:server 用 alarm + O_NONBLOCK 防止 client 卡住;或 fork child 处理 client。

      其它遗留缺陷:sequence number 溢出风险;client 请求负数 length;恶意 client 提前 fill reply FIFO 导致 server 阻塞。方案:concurrent server + O_NONBLOCK + 信号化超时。

      F.9 Chapter 47 / 53 答案(IPC)

      47-5 / 47-6:TLPI svsem/event_flags.c;reserve = read 1 byte,release = write 1 byte。

      53-2:TLPI psem/psem_timedwait.c

      F.10 Chapter 48 答案(共享内存 race)

      48-2:保留 for 循环中 shmp→cnt++ 在 semaphore 之外,remove semaphore 后 writer / reader 间 race——reader 读 cnt 中可能 writer 已更新。

      48-4:TLPI svshm/svshm_mon.c

      F.11 Chapter 55 答案(flock)

      55-1:Linux flock 行为——共享锁可能饿死排他请求;谁能加锁取决于调度,并非 FIFO;共享锁因内核并发而全部成功。

      55-2:flock() 不检测 deadlock(除非实现为 fcntl 包装);大多数实现直接 slept。

      55-4:内核 1.2 之前两种锁协同;1.2+ 各自独立。

      F.12 Chapter 57 / 59 / 60 答案(socket)

      57-4:非 peer sendto 到 connected UNIX domain datagram socket 在 Linux 上返 EPERM;某些 UNIX 不强制。

      59-1 ~ 59-5:TLPI sockets/ 目录。

      60-1:进程数上限技巧。

      60-2:dual-mode server(命令行 + inetd)实现:检查 -i 选项,若有则用 STDIN_FILENO 处理 single client,否则按标准 server 运行。

      F.13 Chapter 61 答案(advanced sockets)

      61-1:单进程 client(不 fork)顺序做 write then read——若 send buffer 已满,write 永久阻塞。

      61-3:TLPI sockets/sendfile.c

      三、关键图表

      重要答案速查
      题号 主题 要点

      5-3

      atomic_append race

      lseek + write 非原子

      5-4

      dup dup2

      F_DUPFD 重写

      9-1

      setuid 分析

      fsuid 跟随 euid

      9-4

      销毁特权

      setresuid 永久

      15-5

      umask race

      两步读 / 写

      18-1

      ls -li i-node 不同

      unlink + create

      22-3

      sigsuspend vs sigwaitinfo

      性能 25-40%

      24-1

      fork 计数

      第 3 fork 后 +7

      27-4

      double-fork

      grandchild init 收养

      27-5

      printf not flushed

      exec 丢 buffer

      34-1

      killpg race

      setpgid 独立 group

      44-5

      server close race

      SIGPIPE

      44-6

      server stuck

      alarm + O_NONBLOCK

      55-1

      flock starvation

      Linux 调度 dep

      四、思维导图

      mindmap
        root((附录 F 答案))
          fd dup
            atomic append
            dup vs F_DUPFD
            多 fd offset
          umask race
            两步 umask
            多线程不安全
          setuid 系列
            seteuid 临时
            setresuid 永久
            fsuid 跟随 euid
          fork 计数
            第 N 次 fork
            7 3 阶 fork
            init 收养 grandchild
          exec 行为
            execve 丢弃 buffer
            printf 未 flush
            execvp PATH 搜索
          signal 投递
            SIGCHLD 任意线程
            realtime 在 standard 后
            sigwait 加速
          job control
            killpg 与 pipe
            setpgid 隔离 group
            handler 期间 ^Z race
          IPC race
            shm cnt race
            mq 多线程 notification
            FIFO server stuck
          flock 行为
            共享锁饿死
            deadlock 不检查
            POSIX vs BSD
          socket
            connected 拒绝 peer
            sendto 静默丢
            shutdown 半关

      五、重点与易错点

      1. umask 不是 thread-safe——多线程程序应用 fd create mode 直接 chmod,不能用临时 umask。

      2. setuid() 不能永久降权——除非 saved == effective;用 setresuid(0, ruid, ruid)。

      3. fd 共享 file description——dup/dup2 不复制 file offset;用 O_APPEND 时仍有 race。

      4. fork / exec 是 race 源头——每个 step 间都可能有 time window;client fopen 看是否 stale data。

      5. exec 后 printf buffer 丢失——printf 后必须 fflush,再 exec。

      6. pthread_join 自己可能死锁——Linux 上返 EDEADLK;先 pthread_equal check 再 join。

      7. SIGCHLD directed 投递——线程池应用中要小心 choose 收信号;最佳实践是主线程统一 wait,handler 仅 set flag。

      8. FIFO close race——server close reader fd 时 client 可能 open writer fd 而 SIGPIPE。

      9. flock 不可靠 vs fcntl——Linux flock 是 BSD 行为不 deadlock-detect;fcntl 锁更严格。

      10. connected UNIX domain datagram——Linux 强制拒绝非 peer(EPERM);BSD 可选;勿假设跨 UNIX 行为一致。

      11. 第 3 次 fork 计数 7 个新进程——很多初学者错算为 8(含原进程),实际 "3 次 fork = 2^3 - 1 = 7 new"。

      12. sendmail 风格的 race——多个进程同时 lseek + write 文件 → 丢失写入;改 O_APPEND atomic 写或 fcntl。

      13. TLPI 全部 source 在 book 配套光盘——多数答案直接指向源文件;自学可对照。

      14. 面试常考:fork / exec / signal / thread / race;了解 TLPI 答案能大部分问题。

      15. 跨章衔接:第 5 章 fd / dup / O_APPEND;第 9 章 setuid 系列;第 24-27 章 fork+exec 模型;第 33 章 thread + signal;第 44 章 pipe race;第 53 章 POSIX semaphore;第 55 章 lock;第 57 章 UNIX domain socket。