附录 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-4:dup(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-2:stat() 不更新任何 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-1:ls -li 看编译出新文件 i-node 不同——compiler 先 unlink 同名旧 + open(O_CREAT);可 unlink 运行中可执行文件。
18-2:symlink("../test/myfile", "../mylink") 是相对符号链接;相对父目录解析,因此指向父目录中不存在的文件,chmod 失败 ENOENT。
18-9:fchdir() 比 chdir() 高效——fchdir 仅用 fd(一次性解析 inode),chdir 每次传 path 字符串 + 内核解析。循环迭代应用 fchdir 大量节省。
F.4 Chapter 20 / 22 答案(signal)
20-2 / 20-4:TLPI source signals/ignore_pending_sig.c 与 siginterrupt.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-1:execvp("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-5:printf 不 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-1:killpg(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+ 各自独立。
三、关键图表
|
重要答案速查
|
四、思维导图
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 半关
五、重点与易错点
-
umask 不是 thread-safe——多线程程序应用 fd create mode 直接 chmod,不能用临时 umask。
-
setuid() 不能永久降权——除非 saved == effective;用 setresuid(0, ruid, ruid)。
-
fd 共享 file description——dup/dup2 不复制 file offset;用 O_APPEND 时仍有 race。
-
fork / exec 是 race 源头——每个 step 间都可能有 time window;client fopen 看是否 stale data。
-
exec 后 printf buffer 丢失——printf 后必须 fflush,再 exec。
-
pthread_join 自己可能死锁——Linux 上返 EDEADLK;先 pthread_equal check 再 join。
-
SIGCHLD directed 投递——线程池应用中要小心 choose 收信号;最佳实践是主线程统一 wait,handler 仅 set flag。
-
FIFO close race——server close reader fd 时 client 可能 open writer fd 而 SIGPIPE。
-
flock 不可靠 vs fcntl——Linux flock 是 BSD 行为不 deadlock-detect;fcntl 锁更严格。
-
connected UNIX domain datagram——Linux 强制拒绝非 peer(EPERM);BSD 可选;勿假设跨 UNIX 行为一致。
-
第 3 次 fork 计数 7 个新进程——很多初学者错算为 8(含原进程),实际 "3 次 fork = 2^3 - 1 = 7 new"。
-
sendmail 风格的 race——多个进程同时 lseek + write 文件 → 丢失写入;改 O_APPEND atomic 写或 fcntl。
-
TLPI 全部 source 在 book 配套光盘——多数答案直接指向源文件;自学可对照。
-
面试常考:fork / exec / signal / thread / race;了解 TLPI 答案能大部分问题。
-
跨章衔接:第 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。