mirror of
https://github.com/riba2534/TCP-IP-NetworkNote.git
synced 2026-05-07 22:12:54 +08:00
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:
@@ -15,7 +15,7 @@
|
||||
|
||||
- 每秒少则 10 次,多则千次的「上下文切换」是创建进程的最大开销
|
||||
|
||||
只有一个 CPU 的系统是将时间分成多个微小的块后分配给了多个进程。为了分时使用 CPU ,需要「上下文切换」的过程。「上下文切换」是指运行程序前需要将相应进程信息读入内存,如果运行进程 A 后紧接着需要运行进程 B ,就应该将进程 A 相关信息移出内存,并读入进程 B 相关信息。这就是上下文切换。但是此时进程 A 的数据将被移动到硬盘,所以上下文切换要很长时间,即使通过优化加快速度,也会存在一定的局限。
|
||||
只有一个 CPU 的系统是将时间分成多个微小的块后分配给了多个进程。为了分时使用 CPU ,需要「上下文切换」的过程。「上下文切换」是指运行程序前需要将相应进程信息读入内存,如果运行进程 A 后紧接着需要运行进程 B ,就应该将进程 A 相关信息移出内存(或保存到寄存器),并读入进程 B 相关信息。这就是上下文切换。上下文切换需要保存和恢复进程的上下文信息(寄存器、程序计数器、栈指针等),这个过程会带来一定的开销,即使通过优化加快速度,也会存在一定的局限。
|
||||
|
||||
为了保持多进程的优点,同时在一定程度上克服其缺点,人们引入的线程(Thread)的概念。这是为了将进程的各种劣势降至最低程度(不是直接消除)而设立的一种「轻量级进程」。线程比进程具有如下优点:
|
||||
|
||||
@@ -70,7 +70,7 @@ int pthread_create(pthread_t *restrict thread,
|
||||
void *(*start_routine)(void *),
|
||||
void *restrict arg);
|
||||
/*
|
||||
成功时返回 0 ,失败时返回 -1
|
||||
成功时返回 0 ,失败时返回错误码(正数)
|
||||
thread : 保存新创建线程 ID 的变量地址值。线程与进程相同,也需要用于区分不同线程的 ID
|
||||
attr : 用于传递线程属性的参数,传递 NULL 时,创建默认属性的线程
|
||||
start_routine : 相当于线程 main 函数的、在单独执行流中执行的函数地址值(函数指针)
|
||||
@@ -138,13 +138,13 @@ gcc thread1.c -o tr1 -lpthread # 线程相关代码编译时需要添加 -lpthre
|
||||
#include <pthread.h>
|
||||
int pthread_join(pthread_t thread, void **status);
|
||||
/*
|
||||
成功时返回 0 ,失败时返回 -1
|
||||
成功时返回 0 ,失败时返回错误码(正数)
|
||||
thread : 该参数值 ID 的线程终止后才会从该函数返回
|
||||
status : 保存线程的 main 函数返回值的指针的变量地址值
|
||||
*/
|
||||
```
|
||||
|
||||
作用就是调用该函数的进程(或线程)将进入等待状态,知道第一个参数为 ID 的线程终止为止。而且可以得到线程的 main 函数的返回值。下面是该函数的用法代码:
|
||||
作用就是调用该函数的进程(或线程)将进入等待状态,直到第一个参数为 ID 的线程终止为止。而且可以得到线程的 main 函数的返回值。下面是该函数的用法代码:
|
||||
|
||||
- [thread2.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch18/thread2.c)
|
||||
|
||||
@@ -237,7 +237,7 @@ struct hostent *gethostbyname(const char *hostname);
|
||||
struct hostent *gethostbyname_r(const char *name,
|
||||
struct hostent *result,
|
||||
char *buffer,
|
||||
int intbuflen,
|
||||
size_t buflen,
|
||||
int *h_errnop);
|
||||
```
|
||||
|
||||
@@ -245,7 +245,7 @@ struct hostent *gethostbyname_r(const char *name,
|
||||
|
||||
> 声明头文件前定义 `_REENTRANT` 宏。
|
||||
|
||||
无需特意更改源代码加,可以在编译的时候指定编译参数定义宏。
|
||||
无需特意更改源代码,可以在编译的时候指定编译参数定义宏。
|
||||
|
||||
```shell
|
||||
gcc -D_REENTRANT mythread.c -o mthread -lpthread
|
||||
@@ -384,7 +384,7 @@ gcc thread4.c -D_REENTRANT -o tr4 -lpthread
|
||||
|
||||
任何内存空间,只要被同时访问,都有可能发生问题。
|
||||
|
||||
因此,线程访问变量 num 时应该阻止其他线程访问,直到线程 1 运算完成。这就是同步(Synchronization)
|
||||
因此,线程访问变量 num 时应该阻止其他线程访问,直到线程 1 运算完成。这就是同步(Synchronization)。
|
||||
|
||||
#### 18.3.2 临界区位置
|
||||
|
||||
@@ -392,7 +392,7 @@ gcc thread4.c -D_REENTRANT -o tr4 -lpthread
|
||||
|
||||
> 函数内同时运行多个线程时引发问题的多条语句构成的代码块
|
||||
|
||||
全局变量 num 不能视为临界区,因为他不是引起问题的语句,只是一个内存区域的声明。下面是刚才代码的两个 main 函数
|
||||
全局变量 num 不能视为临界区,因为它不是引起问题的语句,只是一个内存区域的声明。下面是刚才代码的两个线程函数
|
||||
|
||||
```c
|
||||
void *thread_inc(void *arg)
|
||||
@@ -438,9 +438,9 @@ void *thread_des(void *arg)
|
||||
|
||||
#### 18.4.2 互斥量
|
||||
|
||||
互斥锁(英语:英语:Mutual exclusion,缩写 Mutex)是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全域变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区域(critical section)达成。临界区域指的是一块对公共资源进行访问的代码,并非一种机制或是算法。一个程序、进程、线程可以拥有多个临界区域,但是并不一定会应用互斥锁。
|
||||
互斥锁(英语:Mutual exclusion,缩写 Mutex)是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全域变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区域(critical section)达成。临界区域指的是一块对公共资源进行访问的代码,并非一种机制或是算法。一个程序、进程、线程可以拥有多个临界区域,但是并不一定会应用互斥锁。
|
||||
|
||||
通俗的说就互斥量就是一把优秀的锁,当临界区被占据的时候就上锁,等占用完毕然后再放开。
|
||||
通俗地说,互斥量就是一把锁,当临界区被占据的时候就上锁,等占用完毕然后再放开。
|
||||
|
||||
下面是互斥量的创建及销毁函数。
|
||||
|
||||
@@ -450,7 +450,7 @@ int pthread_mutex_init(pthread_mutex_t *mutex,
|
||||
const pthread_mutexattr_t *attr);
|
||||
int pthread_mutex_destroy(pthread_mutex_t *mutex);
|
||||
/*
|
||||
成功时返回 0,失败时返回其他值
|
||||
成功时返回 0,失败时返回错误码
|
||||
mutex : 创建互斥量时传递保存互斥量的变量地址值,销毁时传递需要销毁的互斥量地址
|
||||
attr : 传递即将创建的互斥量属性,没有特别需要指定的属性时传递 NULL
|
||||
*/
|
||||
@@ -477,11 +477,11 @@ pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
int pthread_mutex_lock(pthread_mutex_t *mutex);
|
||||
int pthread_mutex_unlock(pthread_mutex_t *mutex);
|
||||
/*
|
||||
成功时返回 0 ,失败时返回其他值
|
||||
成功时返回 0 ,失败时返回错误码
|
||||
*/
|
||||
```
|
||||
|
||||
函数本身含有 lock unlock 等词汇,很容易理解其含义。进入临界区前调用的函数就是 pthread_mutex_lock 。调用该函数时,发现有其他线程已经进入临界区,则 pthread_mutex_lock 函数不会返回,直到里面的线程调用 pthread_mutex_unlock 函数退出临界区位置。也就是说,其他线程让出临界区之前,当前线程一直处于阻塞状态。接下来整理一下代码的编写方式:
|
||||
函数本身含有 lock unlock 等词汇,很容易理解其含义。进入临界区前调用的函数就是 pthread_mutex_lock。调用该函数时,发现有其他线程已经进入临界区,则 pthread_mutex_lock 函数不会返回,直到里面的线程调用 pthread_mutex_unlock 函数退出临界区位置。也就是说,其他线程让出临界区之前,当前线程一直处于阻塞状态。接下来整理一下代码的编写方式:
|
||||
|
||||
```c
|
||||
pthread_mutex_lock(&mutex);
|
||||
@@ -596,38 +596,38 @@ semaphore对象适用于控制一个仅支持有限个用户的共享资源,
|
||||
int sem_init(sem_t *sem, int pshared, unsigned int value);
|
||||
int sem_destroy(sem_t *sem);
|
||||
/*
|
||||
成功时返回 0 ,失败时返回其他值
|
||||
成功时返回 0 ,失败时返回错误码
|
||||
sem : 创建信号量时保存信号量的变量地址值,销毁时传递需要销毁的信号量变量地址值
|
||||
pshared : 传递其他值时,创建可由多个继承共享的信号量;传递 0 时,创建只允许 1 个进程内部使用的信号量。需要完成同一进程的线程同步,故为0
|
||||
pshared : 传递非 0 值时,创建可由多个进程共享的信号量;传递 0 时,创建只允许 1 个进程内部使用的信号量。需要完成同一进程的线程同步,故为 0
|
||||
value : 指定创建信号量的初始值
|
||||
*/
|
||||
```
|
||||
|
||||
上述的 shared 参数超出了我们的关注范围,故默认向其传递为 0 。下面是信号量中相当于互斥量 lock unlock 的函数。
|
||||
上述的 pshared 参数超出我们的关注范围,故默认向其传递 0。下面是信号量中相当于互斥量 lock unlock 的函数。
|
||||
|
||||
```c
|
||||
#include <semaphore.h>
|
||||
int sem_post(sem_t *sem);
|
||||
int sem_wait(sem_t *sem);
|
||||
/*
|
||||
成功时返回 0 ,失败时返回其他值
|
||||
sem : 传递保存信号量读取值的变量地址值,传递给 sem_post 的信号量增1,传递给 sem_wait 时信号量减一
|
||||
成功时返回 0 ,失败时返回错误码
|
||||
sem : 传递保存信号量读取值的变量地址值,传递给 sem_post 的信号量增 1,传递给 sem_wait 时信号量减 1
|
||||
*/
|
||||
```
|
||||
|
||||
调用 sem_init 函数时,操作系统将创建信号量对象,此对象中记录这「信号量值」(Semaphore Value)整数。该值在调用 sem_post 函数时增加 1 ,调用 wait_wait 函数时减一。但信号量的值不能小于 0 ,因此,在信号量为 0 的情况下调用 sem_wait 函数时,调用的线程将进入阻塞状态(因为函数未返回)。当然,此时如果有其他线程调用 sem_post 函数,信号量的值将变为 1 ,而原本阻塞的线程可以将该信号重新减为 0 并跳出阻塞状态。实际上就是通过这种特性完成临界区的同步操作,可以通过如下形式同步临界区(假设信号量的初始值为 1)
|
||||
调用 sem_init 函数时,操作系统将创建信号量对象,此对象中记录着「信号量值」(Semaphore Value)整数。该值在调用 sem_post 函数时增加 1,调用 sem_wait 函数时减 1。但信号量的值不能小于 0,因此,在信号量为 0 的情况下调用 sem_wait 函数时,调用的线程将进入阻塞状态(因为函数未返回)。当然,此时如果有其他线程调用 sem_post 函数,信号量的值将变为 1,而原本阻塞的线程可以将该信号重新减为 0 并跳出阻塞状态。实际上就是通过这种特性完成临界区的同步操作,可以通过如下形式同步临界区(假设信号量的初始值为 1):
|
||||
|
||||
```c
|
||||
sem_wait(&sem);//信号量变为0...
|
||||
sem_wait(&sem);//信号量变为 0...
|
||||
// 临界区的开始
|
||||
//...
|
||||
//临界区的结束
|
||||
sem_post(&sem);//信号量变为1...
|
||||
sem_post(&sem);//信号量变为 1...
|
||||
```
|
||||
|
||||
上述代码结构中,调用 sem_wait 函数进入临界区的线程在调用 sem_post 函数前不允许其他线程进入临界区。信号量的值在 0 和 1 之间跳转,因此,具有这种特性的机制称为「二进制信号量」。接下来的代码是信号量机制的代码。下面代码并非是同时访问的同步,而是关于控制访问顺序的同步,该场景为:
|
||||
上述代码结构中,调用 sem_wait 函数进入临界区的线程在调用 sem_post 函数前不允许其他线程进入临界区。信号量的值在 0 和 1 之间跳转,因此,具有这种特性的机制称为「二进制信号量」。接下来的代码是信号量机制的代码。下面代码并非是同时访问的同步,而是关于控制访问顺序的同步,该场景为:
|
||||
|
||||
> 线程 A 从用户输入得到值后存入全局变量 num ,此时线程 B 将取走该值并累加。该过程一共进行 5 次,完成后输出总和并退出程序。
|
||||
> 线程 A 从用户输入得到值后存入全局变量 num,此时线程 B 将取走该值并累加。该过程一共进行 5 次,完成后输出总和并退出程序。
|
||||
|
||||
下面是代码:
|
||||
|
||||
@@ -699,32 +699,34 @@ gcc semaphore.c -D_REENTRANT -o sema -lpthread
|
||||
|
||||

|
||||
|
||||
从上述代码可以看出,设置了两个信号量 one 的初始值为 0 ,two 的初始值为 1,然后在调用函数的时候,「读」的前提是 two 可以减一,如果不能减一就会阻塞在这里,一直等到「计算」操作完毕后,给 two 加一,然后就可以继续执行下一句输入。对于「计算」函数,也一样。
|
||||
从上述代码可以看出,设置了两个信号量:sem_one 的初始值为 0,sem_two 的初始值为 1,然后在调用函数的时候,「读」的前提是 sem_two 可以减 1,如果不能减 1 就会阻塞在这里,一直等到「计算」操作完毕后,给 sem_two 加 1,然后就可以继续执行下一句输入。对于「计算」函数,也一样。
|
||||
|
||||
### 18.5 线程的销毁和多线程并发服务器端的实现
|
||||
|
||||
先介绍线程的销毁,然后再介绍多线程服务端
|
||||
|
||||
#### 18.5.1 销毁线程的 3 种方法
|
||||
#### 18.5.1 销毁线程的方法
|
||||
|
||||
Linux 的线程并不是在首次调用的线程 main 函数返回时自动销毁,所以利用如下方法之一加以明确。否则由线程创建的内存空间将一直存在。
|
||||
Linux 的线程并不是在首次调用的线程 main 函数返回时自动销毁,所以需要利用如下方法之一加以明确。否则由线程创建的内存空间将一直存在。
|
||||
|
||||
- 调用 pthread_join 函数
|
||||
- 调用 pthread_detach 函数
|
||||
|
||||
之前调用过 pthread_join 函数。调用该函数时,不仅会等待线程终止,还会引导线程销毁。但该函数的问题是,线程终止前,调用该函数的线程将进入阻塞状态。因此,通过如下函数调用引导线程销毁。
|
||||
之前调用过 pthread_join 函数。调用该函数时,不仅会等待线程终止,还会引导线程销毁。但该函数的问题是,线程终止前,调用该函数的线程将进入阻塞状态。因此,可通过如下函数调用引导线程销毁。
|
||||
|
||||
```c
|
||||
#include <pthread.h>
|
||||
int pthread_detach(pthread_t th);
|
||||
int pthread_detach(pthread_t thread);
|
||||
/*
|
||||
成功时返回 0 ,失败时返回其他值
|
||||
成功时返回 0,失败时返回错误码
|
||||
thread : 终止的同时需要销毁的线程 ID
|
||||
*/
|
||||
```
|
||||
|
||||
调用上述函数不会引起线程终止或进入阻塞状态,可以通过该函数引导销毁线程创建的内存空间。调用该函数后不能再针对相应线程调用 pthread_join 函数。
|
||||
|
||||
> 补充说明:Linux 线程有两种状态——「可结合」(joinable)和「分离」(detached)。默认创建的线程是可结合状态,必须被 pthread_join 或 pthread_detach 才能释放其资源。如果线程被设置为分离状态,则线程结束时系统会自动回收其资源,无需其他线程调用 pthread_join。
|
||||
|
||||
#### 18.5.2 多线程并发服务器端的实现
|
||||
|
||||
下面是多个客户端之间可以交换信息的简单聊天程序。
|
||||
@@ -769,22 +771,27 @@ gcc chat_clnt.c -D_REENTRANT -o cclnt -lpthread
|
||||
|
||||
4. **下面关于临界区的说法错误的是**?
|
||||
|
||||
答:下面加粗的选项为说法正确。(全错)
|
||||
答:这四个选项的说法都是错误的。正确理解如下:
|
||||
|
||||
1. 临界区是多个线程同时访问时发生问题的区域
|
||||
2. 线程安全的函数中不存在临界区,即便多个线程同时调用也不会发生问题
|
||||
3. 1 个临界区只能由 1 个代码块,而非多个代码块构成。换言之,线程 A 执行的代码块 A 和线程 B 执行的代码块 B 之间绝对不会构成临界区。
|
||||
4. 临界区由访问全局变量的代码构成。其他变量中不会发生问题。
|
||||
1. (错误)临界区不是指「区域」,而是指访问共享资源的代码片段。即使多个线程同时访问某些区域,如果通过同步机制保护,也不会发生问题。
|
||||
2. (错误)线程安全的函数中同样可能存在临界区,只是通过锁等机制确保了多线程同时调用时的安全性。
|
||||
3. (错误)临界区可以由多个代码块构成。线程 A 执行的代码块 A 和线程 B 执行的代码块 B,如果它们访问同一共享资源,就可能构成临界区。
|
||||
4. (错误)临界区不仅由访问全局变量的代码构成,任何访问共享资源(如静态变量、堆内存、文件等)的代码都可能构成临界区。
|
||||
|
||||
5. **下列关于线程同步的说法错误的是**?
|
||||
|
||||
答:下面加粗的选项为说法正确。
|
||||
答:第 1 和第 4 个说法是错误的,第 2 和第 3 个说法是正确的。
|
||||
|
||||
1. 线程同步就是限制访问临界区
|
||||
2. **线程同步也具有控制线程执行顺序的含义**
|
||||
3. **互斥量和信号量是典型的同步技术**
|
||||
4. 线程同步是代替进程 IPC 的技术。
|
||||
1. (错误)线程同步不仅是限制访问临界区,还包括控制线程执行顺序。
|
||||
2. (正确)线程同步也具有控制线程执行顺序的含义。
|
||||
3. (正确)互斥量和信号量是典型的同步技术。
|
||||
4. (错误)线程同步是解决多线程并发问题的技术,而 IPC 是解决进程间通信问题的技术,两者并不互相代替。线程间数据交换无需特殊技术是因为线程共享同一进程的内存空间,而非线程同步代替了 IPC。
|
||||
|
||||
6. **请说明完全销毁 Linux 线程的 2 种办法**
|
||||
|
||||
答:①调用 pthread_join 函数②调用 pthread_detach 函数。第一个会阻塞调用的线程,而第二个不阻塞。都可以引导线程销毁。
|
||||
答:
|
||||
|
||||
- **调用 pthread_join 函数**:该函数会阻塞调用线程,直到目标线程终止,然后回收其资源。适用于需要获取线程返回值的场景。
|
||||
- **调用 pthread_detach 函数**:该函数将线程设置为分离状态,不会阻塞调用线程。分离状态的线程终止时,系统会自动回收其资源。适用于不关心线程返回值的场景。
|
||||
|
||||
注意:一个线程要么被 pthread_join,要么被 pthread_detach,不能两者都调用。未被 join 或 detach 的线程终止后其资源不会被释放,会造成资源泄漏。
|
||||
|
||||
Reference in New Issue
Block a user