mirror of
https://github.com/riba2534/TCP-IP-NetworkNote.git
synced 2026-06-30 09:56:04 +08:00
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:
@@ -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 函数将创建调用的进程副本。也就是说,并非根据完全不
|
||||
|
||||

|
||||
|
||||
从图中可以看出,父进程调用 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
|
||||
|
||||

|
||||
|
||||
通过 `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 函数时复制父进程的所有资源,但是套接字不是归进程所有的,而是归操作系统所有,只是进程拥有代表相应套接字的文件描述符。
|
||||
|
||||

|
||||
|
||||
@@ -686,7 +686,7 @@ gcc echo_mpserv.c -o eserver
|
||||
|
||||

|
||||
|
||||
从图中可以看出,客户端的父进程负责接收数据,额外创建的子进程负责发送数据,分割后,不同进程分别负责输入输出,这样,无论客户端是否从服务器端接收完数据都可以进程传输。
|
||||
从图中可以看出,客户端的父进程负责接收数据,额外创建的子进程负责发送数据,分割后,不同进程分别负责输入输出,这样,无论客户端是否从服务器端接收完数据都可以进行传输。
|
||||
|
||||
分割 I/O 程序的另外一个好处是,可以提高频繁交换数据的程序性能,如下图所示:
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 信号的处理器。
|
||||
|
||||
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
else
|
||||
{
|
||||
//调用waitpid 传递参数 WNOHANG ,这样之前有没有终止的子进程则返回0
|
||||
// 调用 waitpid 传递参数 WNOHANG,这样如果没有终止的子进程则返回 0
|
||||
while (!waitpid(-1, &status, WNOHANG))
|
||||
{
|
||||
sleep(3);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user