docs: 全面校对全部章节文档与示例代码

通过多智能体工作流对 19 章笔记(README.md)与 96 个 .c 示例代码做深度
审查与对抗性验证,修复 317 处确认问题,涵盖:

技术正确性:
- 修复缓冲区溢出:echo_mpserv.c / echo_storeserv.c 等的 read(buf, BUFSIZ)
  改为 BUF_SIZE(buf 仅 30 字节,BUFSIZ 远大于此)
- 修复 open() 缺少 mode 参数:low_open.c / fd_seri.c / desto.c 等
  O_CREAT 调用补 0644(原导致 low_read 链路失败)
- 修复 feof 循环 off-by-one:news_sender.c / echo_stdserv.c 改用 fgets
  返回值判断
- 修复线程竞态:chat_server.c / webserv_linux.c 的 &clnt_sock 栈地址
  传子线程改为 malloc 分配 + free
- 修复索引混淆:char_EPLTserv.c 错用 clnt_sock 查找改为 ep_events[i].data.fd
- 修复格式化符:thread4.c 的 sizeof 用 %d 改为 %zu
- 修正习题答案:ch01 fd 序号、ch13 MSG_OOB 加粗项、ch09 Nagle 等

文档规范:
- 统一术语:IPv4/IPv6、接收(receive)/连接(connection)
- 修正错别字:occured→occurred、cooffee→coffee、Usgae→Usage、
  eerror→error、proess→process 等
- 修复病句、补全习题答案解释
- GitHub 绝对 URL 改为相对路径,统一项目引用规范
- 同步根 README.md(前言 + 19 章合并)

另:重命名 ch10/remove_zomebie.c → remove_zombie.c(修正拼写)

所有 .c 文件经 gcc 编译验证通过(ch17 epoll 文件因 macOS 无 sys/epoll.h
跳过,已人工复核)。
This commit is contained in:
riba2534
2026-06-28 12:47:46 +08:00
parent a9ef4b6dc4
commit 5625eea472
76 changed files with 707 additions and 629 deletions

View File

@@ -24,7 +24,7 @@
#### 10.1.3 进程 ID
在介绍进程创建方法之前,先要简要说明进程 ID。无论进程是如何创建的所有的进程都会被操作系统分配一个 ID。此 ID 被称为「进程 ID」其值为大于 2 的整数。1 要分配给操作系统启动后的(用于协助操作系统)首个进程,因此用户无法得到 ID 值为 1。接下来观察在 Linux 中运行的进程。
在介绍进程创建方法之前,先要简要说明进程 ID。无论进程是如何创建的所有的进程都会被操作系统分配一个 ID。此 ID 被称为「进程 ID」其值为大于 1 的整数。1 要分配给操作系统启动后的(用于协助操作系统)首个进程,因此用户无法得到 ID 值为 1。接下来观察在 Linux 中运行的进程。
```shell
ps au
@@ -53,7 +53,7 @@ fork 函数将创建调用的进程副本。也就是说,并非根据完全不
![](images/5c43da5412b90.png)
从图中可以看出,父进程调用 fork 函数的同时复制出子进程,并分别得到 fork 函数的返回值。但复制前,父进程将全局变量 gval 增加到 11将局部变量 lval 的值增加到 25因此在这种状态下完成进程复制。复制完成后根据 fork 函数的返回值区分父子进程。父进程的 lval 的值增加 1,但这不会影响子进程的 lval 值。同样子进程将 gval 的值增加 1 也不会影响到父进程的 gval。因为 fork 函数调用后分成了完全不同的进程,只是二者共享同一段代码而已。接下来给出一个例子:
从图中可以看出,父进程调用 fork 函数的同时复制出子进程,并分别得到 fork 函数的返回值。但复制前,父进程将全局变量 gval 增加到 11将局部变量 lval 的值增加到 25因此在这种状态下完成进程复制。复制完成后根据 fork 函数的返回值区分父子进程。父进程的 lval 的值减少 2,但这不会影响子进程的 lval 值。同样子进程将 gval 的值增加 2 也不会影响到父进程的 gval。因为 fork 函数调用后分成了完全不同的进程,只是二者共享同一段代码而已。接下来给出一个例子:
- [fork.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/fork.c)
@@ -100,7 +100,7 @@ gcc fork.c -o fork
进程的工作完成后(执行完 main 函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源。这种状态下的进程称作「僵尸进程」,这也是给系统带来负担的原因之一。
> 僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 子进程被init接管子进程退出后init会回收其占用的相关资源
> 僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出,子进程被 init 接管,子进程退出后 init 会回收其占用的相关资源
**维基百科**
@@ -169,7 +169,7 @@ gcc zombie.c -o zombie
![](images/5c4439a751b11.png)
通过 `ps au` 命令可以看出,子进程仍然存在,并没有被销毁,僵尸进程在这里显示为 `Z+`.30秒后红框里面的两个进程会同时被销毁。
通过 `ps au` 命令可以看出,子进程仍然存在,并没有被销毁,僵尸进程在这里显示为 `Z+`30 秒后,红框里面的两个进程会同时被销毁。
> 利用 `./zombie &`可以使程序在后台运行,不用打开新的命令行窗口。
@@ -188,7 +188,7 @@ pid_t wait(int *statloc);
调用此函数时如果已有子进程终止那么子进程终止时传递的返回值exit 函数的参数返回值main 函数的 return 返回值)将保存到该函数的参数所指的内存空间。但函数参数指向的单元中还包含其他信息,因此需要用下列宏进行分离:
- WIFEXITED 子进程正常终止时返回「真」
- WEXITSTATUS 返回子进程时的返回值
- WEXITSTATUS 返回子进程正常终止时的返回值
也就是说,向 wait 函数传递变量 status 的地址时,调用 wait 函数后应编写如下代码:
@@ -230,7 +230,7 @@ int main(int argc, char *argv[])
else
{
printf("Child PID: %d \n", pid);
wait(&status); //之终止的子进程相关信息将被保存到 status 中,同时相关子进程被完全销毁
wait(&status); //之终止的子进程相关信息将被保存到 status 中,同时相关子进程被完全销毁
if (WIFEXITED(status)) //通过 WIFEXITED 来验证子进程是否正常终止。如果正常终止,则调用 WEXITSTATUS 宏输出子进程返回值
printf("Child send one: %d \n", WEXITSTATUS(status));
@@ -270,7 +270,7 @@ pid_t waitpid(pid_t pid, int *statloc, int options);
成功时返回终止的子进程ID 或 0 ,失败时返回 -1
pid: 等待终止的目标子进程的ID,若传 -1则与 wait 函数相同,可以等待任意子进程终止
statloc: 与 wait 函数的 statloc 参数具有相同含义
options: 传递头文件 sys/wait.h 声明的常量 WNOHANG ,即使没有终止的子进程也不会进入阻塞状态,而是返回 0 退出函数。
options: 传递头文件 sys/wait.h 声明的常量 WNOHANG即使没有终止的子进程也不会进入阻塞状态,而是返回 0 退出函数。
*/
```
@@ -327,9 +327,9 @@ gcc waitpid.c -o waitpid
#### 10.3.1 向操作系统求助
子进程终止的识别主是操作系统,因此,若操作系统能把子进程结束的信息告诉正忙于工作的父进程,将有助于构建更高效的程序
子进程终止的识别主是操作系统,因此,若操作系统能把子进程结束的信息告诉正忙于工作的父进程,将有助于构建更高效的程序
为了实现上述的功能引入信号处理机制Signal Handing。此处「信号」是在特定事件发生时由操作系统向进程发送的消息。另外为了响应该消息执行与消息相关的自定义操作的过程被称为「处理」或「信号处理」。
为了实现上述的功能引入信号处理机制Signal Handling。此处「信号」是在特定事件发生时由操作系统向进程发送的消息。另外为了响应该消息执行与消息相关的自定义操作的过程被称为「处理」或「信号处理」。
#### 10.3.2 信号与 signal 函数
@@ -388,7 +388,7 @@ unsigned int alarm(unsigned int seconds);
// 返回0或以秒为单位的距 SIGALRM 信号发生所剩时间
```
如果调用该函数的同时向它传递一个正整型参数,相应时间后(以秒为单位)将产生 SIGALRM 信号。若向该函数传递 0则之前对 SIGALRM 信号的预约将取消。如果通过该函数预约信号后未指定该信号对应的处理函数,则(通过调用 signal 函数)终止进程,不做任何处理
如果调用该函数的同时向它传递一个正整型参数,相应时间后(以秒为单位)将产生 SIGALRM 信号。若向该函数传递 0则之前对 SIGALRM 信号的预约将取消。如果通过该函数预约信号后未指定该信号对应的处理函数,则 SIGALRM 信号按默认处理方式终止进程
- [signal.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/signal.c)
@@ -412,7 +412,7 @@ int main(int argc, char *argv[])
int i;
signal(SIGALRM, timeout); //注册信号及相应处理器
signal(SIGINT, keycontrol);
alarm(2); //预约 2 秒发生 SIGALRM 信号
alarm(2); //预约 2 秒发生 SIGALRM 信号
for (i = 0; i < 3; i++)
{
@@ -539,7 +539,7 @@ Time out!
下面利用子进程终止时产生 SIGCHLD 信号这一点,来用信号处理来消灭僵尸进程。看以下代码:
- [remove_zomebie.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/remove_zomebie.c)
- [remove_zombie.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/remove_zombie.c)
```c
#include <stdio.h>
@@ -603,7 +603,7 @@ int main(int argc, char *argv[])
编译运行:
```shell
gcc remove_zomebie.c -o zombie
gcc remove_zombie.c -o zombie
./zombie
```
@@ -627,9 +627,9 @@ Child send: 24
wait
```
请仔细观察结果,结果中的每一个空行代表间隔了 5 秒。程序先创建了两个子进程,子进程在 10 秒之后会返回值。第一个 wait 由于子进程在执行,所以直接被唤醒,然后这两个子进程在睡眠 10 秒,所以 5 秒之后第二个 wait 开始执行,又过了 5 秒,两个子进程同时被唤醒。所以剩下的 wait 也被唤醒
请仔细观察结果,结果中的每一个空行代表间隔了 5 秒(父进程每轮循环 sleep 5 秒)。程序先创建了两个子进程,二者各自睡眠 10 秒后终止。父进程进入循环,每轮打印一次 wait 并 sleep 5 秒:前两轮期间子进程在睡眠;约第 10 秒时子进程终止,操作系统向父进程发送 SIGCHLD 信号,中断父进程的 sleep转而执行信号处理函数在其中通过 waitpid 回收子进程并获取其返回值,随后父进程继续后续轮次的循环
所以在本程序的过程中,当子进程终止时候,会向系统发送一个信号,然后调用我们提前写好的处理函数,在处理函数中使用 waitpid 来处理僵尸进程,获取子进程返回值。
所以在本程序的过程中,当子进程终止时,操作系统会向父进程发送 SIGCHLD 信号,进而调用我们提前注册的处理函数,在处理函数中使用 waitpid 来回收僵尸进程,获取子进程返回值。
### 10.4 基于多任务的并发服务器
@@ -642,8 +642,8 @@ wait
从图中可以看出,每当有客户端请求时(连接请求),回声服务器都创建子进程以提供服务。如果请求的客户端有 5 个,则将创建 5 个子进程来提供服务,为了完成这些任务,需要经过如下过程:
- 第一阶段:回声服务器端(父进程)通过调用 accept 函数受理连接请求
- 第二阶段:此时获取的套接字文件描述符创建并传递给子进程
- 第三阶段:进程利用传递来的文件描述符提供服务
- 第二阶段:通过 fork 创建子进程,并将此时获取的套接字文件描述符传递给子进程
- 第三阶段:进程利用传递来的文件描述符提供服务
#### 10.4.2 实现并发服务器
@@ -655,7 +655,7 @@ wait
```shell
gcc echo_mpserv.c -o eserver
./eserver
./eserver 9190
```
结果:
@@ -666,7 +666,7 @@ gcc echo_mpserv.c -o eserver
示例中给出了通过 fork 函数复制文件描述符的过程。父进程将 2 个套接字(一个是服务端套接字另一个是客户端套接字)文件描述符复制给了子进程。
调用 fork 函数时赋值父进程的所有资源,但是套接字不是归进程所有的,而是归操作系统所有,只是进程拥有代表相应套接字的文件描述符。
调用 fork 函数时复制父进程的所有资源,但是套接字不是归进程所有的,而是归操作系统所有,只是进程拥有代表相应套接字的文件描述符。
![](images/kP7Rjx.png)
@@ -686,7 +686,7 @@ gcc echo_mpserv.c -o eserver
![](images/kPbhkD.png)
从图中可以看出,客户端的父进程负责接收数据,额外创建的子进程负责发送数据,分割后,不同进程分别负责输入输出,这样,无论客户端是否从服务器端接收完数据都可以进传输。
从图中可以看出,客户端的父进程负责接收数据,额外创建的子进程负责发送数据,分割后,不同进程分别负责输入输出,这样,无论客户端是否从服务器端接收完数据都可以进传输。
分割 I/O 程序的另外一个好处是,可以提高频繁交换数据的程序性能,如下图所示:

View File

@@ -44,8 +44,8 @@ void read_routine(int sock, char *buf)
{
while (1)
{
int str_len = read(sock, buf, BUF_SIZE);
if (str_len == 0)
int str_len = read(sock, buf, BUF_SIZE - 1);
if (str_len <= 0)
return;
buf[str_len] = 0;
@@ -59,7 +59,7 @@ void write_routine(int sock, char *buf)
fgets(buf, BUF_SIZE, stdin);
if (!strcmp(buf, "q\n") || !strcmp(buf, "Q\n"))
{
shutdown(sock, SHUT_WR); //向服务器端传递 EOF,因为fork函数复制了文件描述,所以通过1次close调用不够
shutdown(sock, SHUT_WR); //向服务器端传递 EOF因为 fork 函数复制了文件描述,所以通过 1 次 close 调用不够
return;
}
write(sock, buf, strlen(buf));

View File

@@ -23,7 +23,7 @@ int main(int argc, char *argv[])
char buf[BUF_SIZE];
if (argc != 2)
{
printf("Usgae : %s <port>\n", argv[0]);
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
act.sa_handler = read_childproc; //防止僵尸进程
@@ -58,7 +58,7 @@ int main(int argc, char *argv[])
if (pid == 0) //子进程运行区域,此部分向客户端提供回声服务
{
close(serv_sock); //关闭服务器套接字,因为从父进程传递到了子进程
while ((str_len = read(clnt_sock, buf, BUFSIZ)) != 0)
while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0)
write(clnt_sock, buf, str_len);
close(clnt_sock);

View File

@@ -11,10 +11,9 @@ void timeout(int sig)
int main(int argc, char *argv[])
{
int i;
struct sigaction act;
act.sa_handler = timeout; //保存函数指针
sigemptyset(&act.sa_mask); //将 sa_mask 函数的所有位初始化成0
sigemptyset(&act.sa_mask); //将 sa_mask 成员的所有位初始化成0
act.sa_flags = 0; //sa_flags 同样初始化成 0
sigaction(SIGALRM, &act, 0); //注册 SIGALRM 信号的处理器。

View File

@@ -18,7 +18,7 @@ int main(int argc, char *argv[])
int i;
signal(SIGALRM, timeout); //注册信号及相应处理器
signal(SIGINT, keycontrol);
alarm(2); //预约 2 秒发生 SIGALRM 信号
alarm(2); //预约 2 秒发生 SIGALRM 信号
for (i = 0; i < 3; i++)
{

View File

@@ -23,7 +23,7 @@ int main(int argc, char *argv[])
char buf[BUF_SIZE];
if (argc != 2)
{
printf("Usgae : %s <port>\n", argv[0]);
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
act.sa_handler = read_childproc; //防止僵尸进程
@@ -60,7 +60,7 @@ int main(int argc, char *argv[])
{
printf("子进程的 serv_sock%d,clnt_sock:%d\n", serv_sock, clnt_sock);
close(serv_sock); //关闭服务器套接字,因为从父进程传递到了子进程
while ((str_len = read(clnt_sock, buf, BUFSIZ)) != 0)
while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0)
write(clnt_sock, buf, str_len);
close(clnt_sock);

View File

@@ -23,7 +23,7 @@ int main(int argc, char *argv[])
else
{
printf("Child PID: %d \n", pid);
wait(&status); //之终止的子进程相关信息将被保存到 status 中,同时相关子进程被完全销毁
wait(&status); //之终止的子进程相关信息将被保存到 status 中,同时相关子进程被完全销毁
if (WIFEXITED(status)) //通过 WIFEXITED 来验证子进程是否正常终止。如果正常终止,则调用 WEXITSTATUS 宏输出子进程返回值
printf("Child send one: %d \n", WEXITSTATUS(status));

View File

@@ -14,7 +14,7 @@ int main(int argc, char *argv[])
}
else
{
//调用waitpid 传递参数 WNOHANG ,这样之前有没有终止的子进程则返回0
// 调用 waitpid 传递参数 WNOHANG这样如果没有终止的子进程则返回 0
while (!waitpid(-1, &status, WNOHANG))
{
sleep(3);

View File

@@ -16,7 +16,7 @@ int main(int argc, char *argv[])
}
if (pid == 0)
puts("End child proess");
puts("End child process");
else
puts("End parent process");
return 0;