docs: 全面审查并修正所有章节文档内容

- 修正各章节中的错别字和术语错误(如 IPv4 大写规范、接收/接受区分等)
- 补充和完善部分习题答案
- 优化技术描述的准确性和专业性
- 合并所有章节内容到根 README.md

新增文件:
- CLAUDE.md: 项目开发指南
- .claude/agents/content-reviewer.md: 内容审查 subagent
- .claude/agents/merger.md: 文档合并 subagent

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
riba2534
2026-01-05 15:28:29 +08:00
parent f163bca3a9
commit d44ecdf807
23 changed files with 1933 additions and 825 deletions

View File

@@ -12,7 +12,7 @@
- 多路复用服务器:通过捆绑并统一管理 I/O 对象提供服务
- 多线程服务器:通过生成与客户端等量的线程提供服务
第一种方法:多进程服务器
介绍第一种方法:多进程服务器
#### 10.1.2 理解进程
@@ -20,31 +20,31 @@
> 占用内存空间的正在运行的程序
假如你下载了一个游戏到电脑上,此时的游戏不是进程,而是程序。只有当游戏被加载到主内存并进入运行状态,这才可称为进程。
假如你下载了一个游戏到电脑上,此时的游戏不是进程,而是程序。只有当游戏被加载到主内存并进入运行状态,这才可称为进程。
#### 10.1.3 进程 ID
#### 10.1.3 进程 ID
进程创建方法之前,先要简要说明进程 ID。无论进程是如何创建的所有的进程都会被操作系统分配一个 ID。此 ID 被称为「进程ID」其值为大于 2 的整数。1 要分配给操作系统启动后的(用于协助操作系统)首个进程,因此用户无法得到 ID 值为 1 。接下来观察在 Linux 中运行的进程。
介绍进程创建方法之前,先要简要说明进程 ID。无论进程是如何创建的所有的进程都会被操作系统分配一个 ID。此 ID 被称为「进程 ID」其值为大于 2 的整数。1 要分配给操作系统启动后的(用于协助操作系统)首个进程,因此用户无法得到 ID 值为 1。接下来观察在 Linux 中运行的进程。
```shell
ps au
```
通过上面的命令可查看当前运行的所有进程。需要注意的是,该命令同时列出了 PID进程ID。参数 a 和 u列出了所有进程的详细信息。
通过上面的命令可查看当前运行的所有进程。需要注意的是,该命令同时列出了 PID进程 ID。参数 a 和 u 列出了所有进程的详细信息。
![](https://i.loli.net/2019/01/20/5c43d7c1f2a8b.png)
#### 10.1.4 通过调用 fork 函数创建进程
创建进程的方式很多,此处只介绍用于创建多进程服务的 fork 函数。
创建进程的方式很多,此处只介绍用于创建多进程服务的 fork 函数。
```c
#include <unistd.h>
pid_t fork(void);
// 成功时返回进程ID,失败时返回 -1
// 成功时返回进程ID失败时返回 -1
```
fork 函数将创建调用的进程副本。也就是说,并非根据完全不同的程序创建进程,而是复制正在运行的、调用 fork 函数的进程。另外,两个进程都执行 fork 函数调用后的语句(准确说是在 fork 函数返回后)。但因为是通过同一个进程、复制相同的内存空间,之后的程序流要根据 fork 函数的返回值加以区分。即利用 fork 函数的如下特点区分程序执行流程。
fork 函数将创建调用的进程副本。也就是说,并非根据完全不同的程序创建进程,而是复制正在运行的、调用 fork 函数的进程。另外,两个进程都执行 fork 函数调用后的语句(准确说是在 fork 函数返回后)。但因为是通过同一个进程、复制相同的内存空间,之后的程序流要根据 fork 函数的返回值加以区分。即利用 fork 函数的如下特点区分程序执行流程。
- 父进程fork 函数返回子进程 ID
- 子进程fork 函数返回 0
@@ -53,7 +53,7 @@ fork 函数将创建调用的进程副本。也就是说,并非根据完全不
![](https://i.loli.net/2019/01/20/5c43da5412b90.png)
从图中可以看出,父进程调用 fork 函数的同时复制出子进程,并分别得到 fork 函数的返回值。但复制前,父进程将全局变量 gval 增加到 11,将局部变量 lval 的值增加到 25因此在这种状态下完成进程复制。复制完成后根据 fork 函数的返回类型区分父子进程。父进程的 lval 的值增加 1 ,但这不会影响子进程的 lval 值。同样子进程将 gval 的值增加 1 也不会影响到父进程的 gval 。因为 fork 函数调用后分成了完全不同的进程,只是二者共享同一段代码而已。接下来给出一个例子:
从图中可以看出,父进程调用 fork 函数的同时复制出子进程,并分别得到 fork 函数的返回值。但复制前,父进程将全局变量 gval 增加到 11将局部变量 lval 的值增加到 25因此在这种状态下完成进程复制。复制完成后根据 fork 函数的返回区分父子进程。父进程的 lval 的值增加 1但这不会影响子进程的 lval 值。同样子进程将 gval 的值增加 1 也不会影响到父进程的 gval。因为 fork 函数调用后分成了完全不同的进程,只是二者共享同一段代码而已。接下来给出一个例子:
- [fork.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/fork.c)
@@ -90,7 +90,7 @@ gcc fork.c -o fork
![](https://i.loli.net/2019/01/20/5c43e054e7f6f.png)
可以看出,当执行了 fork 函数之后,此后就相当于有了两个程序在执行代码对于父进程来说fork 函数返回的是子进程的ID对于子进程来说fork 函数返回 0。所以这两个变量,父进程进行了 +2 操作 ,而子进程进行了 -2 操作,所以结果是这样。
可以看出,当执行了 fork 函数之后,此后就相当于有了两个程序在执行代码对于父进程来说fork 函数返回的是子进程的 ID对于子进程来说fork 函数返回 0。在 fork 之后,父进程对两个变量进行了 -2 操作,而子进程对两个变量进行了 +2 操作,所以结果是这样。
### 10.2 进程和僵尸进程
@@ -147,7 +147,7 @@ int main(int argc, char *argv[])
sleep(30);
}
if (pid == 0)
puts("End child proess");
puts("End child process");
else
puts("End parent process");
return 0;
@@ -293,7 +293,7 @@ int main(int argc, char *argv[])
}
else
{
//调用waitpid 传递参数 WNOHANG ,这样之前有没有终止的子进程则返回0
// 调用 waitpid 传递参数 WNOHANG这样如果没有终止的子进程则返回 0
while (!waitpid(-1, &status, WNOHANG))
{
sleep(3);
@@ -388,7 +388,7 @@ unsigned int alarm(unsigned int seconds);
// 返回0或以秒为单位的距 SIGALRM 信号发生所剩时间
```
如果调用该函数的同时向它传递一个正整型参数,相应时间后(以秒为单位)将产生 SIGALRM 信号。若向该函数传递 0 ,则之前对 SIGALRM 信号的预约将取消。如果通过函数预约信号后未指定该信号对应的处理函数,则(通过调用 signal 函数)终止进程,不做任何处理。
如果调用该函数的同时向它传递一个正整型参数,相应时间后(以秒为单位)将产生 SIGALRM 信号。若向该函数传递 0则之前对 SIGALRM 信号的预约将取消。如果通过函数预约信号后未指定该信号对应的处理函数,则(通过调用 signal 函数)终止进程,不做任何处理。
- [signal.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/signal.c)
@@ -448,11 +448,11 @@ gcc signal.c -o signal
#### 10.3.3 利用 sigaction 函数进行信号处理
前面所学的内容可以防止僵尸进程,还有一个函数叫做 sigaction 函数,类似于 signal 函数,而且可以完全代替后者,也更稳定。之所以稳定,是因为:
前面所学的内容可以防止僵尸进程,还有一个函数叫做 sigaction 函数,类似于 signal 函数,而且可以完全代替后者,也更稳定。之所以稳定,是因为:
> signal 函数在 Unix 系列的不同操作系统可能存在区别,但 sigaction 函数完全相同
实际上现在很少用 signal 函数编写程序,只是为了保持对旧程序的兼容下面介绍 sigaction 函数,只讲解可以替换 signal 函数的功能。
实际上现在很少用 signal 函数编写程序,只是为了保持对旧程序的兼容下面介绍 sigaction 函数,只讲解可以替换 signal 函数的功能。
```c
#include <signal.h>
@@ -627,7 +627,7 @@ Child send: 24
wait
```
自习观察结果结果中的每一个空行代表间隔了5 秒程序先创建了两个子进程,然后子进程 10 秒之后会返回值第一个 wait 由于子进程在执行,所以直接被唤醒,然后这两个子进程正在睡 10 秒,所以 5 秒之后第二个 wait 开始执行,又过了 5 秒,两个子进程同时被唤醒。所以剩下的 wait 也被唤醒。
仔细观察结果,结果中的每一个空行代表间隔了 5 秒程序先创建了两个子进程,子进程 10 秒之后会返回值第一个 wait 由于子进程在执行,所以直接被唤醒,然后这两个子进程正在睡 10 秒,所以 5 秒之后第二个 wait 开始执行,又过了 5 秒,两个子进程同时被唤醒。所以剩下的 wait 也被唤醒。
所以在本程序的过程中,当子进程终止时候,会向系统发送一个信号,然后调用我们提前写好的处理函数,在处理函数中使用 waitpid 来处理僵尸进程,获取子进程返回值。
@@ -635,7 +635,7 @@ wait
#### 10.4.1 基于进程的并发服务器模型
之前的回声服务器每次只能同向 1 个客户端提供服务。因此,需要扩展回声服务器,使其可以同时向多个客户端提供服务。下图是基于多进程的回声服务器的模型。
之前的回声服务器每次只能同向 1 个客户端提供服务。因此,需要扩展回声服务器,使其可以同时向多个客户端提供服务。下图是基于多进程的回声服务器的模型。
![](https://i.loli.net/2019/01/21/5c453664cde26.png)
@@ -719,27 +719,27 @@ gcc echo_mpclient.c -o eclient
### 10.6 习题
> 以下答案仅代表本人个人观点,可能不是正确答案。
> 以下答案仅代表本人个人观点,可能不是标准答案。
1. **下列关于进程的说法错误的是?**
答:以下加粗的内容为正确的
答:说法 3 和说法 4 是错误的。
1. **从操作系统的角度上说,进程是程序运行的单位**
2. 进程根据创建方式建立父子关系
3. **进程可以包含其他进程,即一个进程的内存空间可以包含其他进程**
4. **子进程可以创建其他子进程,而创建出来的子进程还可以创建其他子进程,但所有这些进程只与一个父进程建立父子关系。**
- 说法 1从操作系统的角度上说,进程是程序运行的单位。(正确)
- 说法 2进程根据创建方式建立父子关系。(正确)
- 说法 3**进程可以包含其他进程,即一个进程的内存空间可以包含其他进程。**(错误)进程拥有独立的内存空间,一个进程不能直接包含另一个进程的内存空间。
- 说法 4**子进程可以创建其他子进程,而创建出来的子进程还可以创建其他子进程,但所有这些进程只与一个父进程建立父子关系。**(错误)一个进程可以有多个子进程,但每个子进程只有一个父进程。子进程创建的孙进程与原父进程没有直接的父子关系。
2. **调用 fork 函数将创建子进程,下关于子进程错误的是?**
2. **调用 fork 函数将创建子进程,下关于子进程错误的是?**
答:以下加粗的内容为正确的
答:说法 1、说法 3 和说法 4 是错误的。
1. **父进程销毁时也会同时销毁子进程**
2. **子进程是复制父进程所有资源创建出的进程**
3. 父子进程共享全局变量
4. 通过 fork 函数创建的子进程将执行从开始到 fork 函数调用为止的代码。
- 说法 1**父进程销毁时也会同时销毁子进程。**(错误)父进程销毁时,子进程不会自动销毁,而是成为孤儿进程,被 init 进程PID 为 1收养。
- 说法 2子进程是复制父进程所有资源创建出的进程。(正确)
- 说法 3**父子进程共享全局变量。**错误fork 之后,父子进程拥有各自独立的全局变量副本,修改其中一个不会影响另一个。
- 说法 4**通过 fork 函数创建的子进程将执行从开始到 fork 函数调用为止的代码。**(错误)子进程从 fork 函数调用后的语句开始执行,不会重新执行之前的代码。
3. **创建子进程时复制父进程所有内容,此时复制对象也包含套接字文件描述符。编写程序验证赋值的文件描述符整数值是否与原文件描述符数值相同。**
3. **创建子进程时复制父进程所有内容,此时复制对象也包含套接字文件描述符。编写程序验证复制的文件描述符整数值是否与原文件描述符数值相同。**
答:代码为多进程服务器修改而来,代码:[test_server.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/test_server.c)
@@ -747,8 +747,17 @@ gcc echo_mpclient.c -o eclient
![](https://s2.ax1x.com/2019/01/21/kPj3Md.png)
从图上可以看出,数值相同。
从图上可以看出,数值相同。fork 复制文件描述符时,子进程获得的文件描述符整数值与父进程的相同。
4. **请说明进程变为僵尸进程的过程以及预防措施。**
答:当一个父进程fork()系统调用建一个新的子进程后,核心进程就会在进程表中给这个子进程分配一个进入点然后将相关信息存储在该进入点所对应的进程表内。这些信息中有一项是其父进程的识别码。而当这个子进程结束的时候比如调用exit命令结束其实他并没有真正的被销毁而是留下一个称为僵尸进程Zombie的数据结构系统调用exit的作用是使进程退出但是也仅仅限于一个正常的进程变成了一个僵尸进程并不能完全将其销毁。**预防措施**:通过 wait 和 waitpid 函数加上信号函数写代码来预防。
答:当一个父进程通过 fork() 系统调用建一个新的子进程后,操作系统会在进程表中给这个子进程分配一个表项,并将相关信息(包括父进程的 PID存储在该表项中。
当子进程结束时(比如调用 exit() 函数或 main 函数 return它并不会立即被完全销毁而是保留一个称为「僵尸进程」的数据结构在进程表中。这个僵尸进程包含着子进程的退出状态信息等待父进程读取。系统调用 exit 的作用是使进程退出,但这只是将进程转变为僵尸状态,并不能完全将其销毁。
**僵尸进程的产生条件**:子进程先于父进程终止,而父进程没有调用 wait() 或 waitpid() 来回收子进程的退出状态。
**预防措施**
- 在父进程中调用 wait() 或 waitpid() 函数来回收子进程的退出状态
- 使用信号处理机制:注册 SIGCHLD 信号处理函数,当子进程终止时,操作系统会向父进程发送 SIGCHLD 信号,在信号处理函数中调用 waitpid() 来回收僵尸进程
- 如果父进程不需要关心子进程的退出状态,可以显式忽略 SIGCHLD 信号(通过 signal(SIGCHLD, SIG_IGN)),这样子进程终止后会被系统自动回收