629 lines
18 KiB
Markdown
629 lines
18 KiB
Markdown
|
|
# 实践02:Web服务器的多进程与多线程模型实现
|
|||
|
|
|
|||
|
|
> [!info] 实验信息
|
|||
|
|
> **课程**:操作系统实践
|
|||
|
|
> **实验名称**:实验二 — Web服务器的多进程与多线程模型实现
|
|||
|
|
> **前置实验**:[[实践01_Web服务器初步实现]]
|
|||
|
|
|
|||
|
|
## 实验目的
|
|||
|
|
|
|||
|
|
1. 将 Web 服务器改造为多进程、多线程、预线程、线程池四种版本
|
|||
|
|
2. 掌握并行网络服务器的设计方法
|
|||
|
|
3. 使用 `http_load` 进行性能测试与对比分析
|
|||
|
|
4. 理解不同并发模型的优劣及适用场景
|
|||
|
|
|
|||
|
|
> [!tip] 关联理论课程
|
|||
|
|
> - [[06_进程控制]] — `fork`、信号处理、僵尸进程回收
|
|||
|
|
> - [[07_多线程编程]] — `pthread_create`、线程同步
|
|||
|
|
> - [[10_并发服务器]] — 并发模型概述与对比
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 源代码文件清单
|
|||
|
|
|
|||
|
|
> [!note] 本次实验新增文件(在实验一基础上)
|
|||
|
|
> | 文件 | 说明 |
|
|||
|
|
> |------|------|
|
|||
|
|
> | `togglesp.c` | 多进程 toggle 服务器 |
|
|||
|
|
> | `togglest.c` | 多线程 toggle 服务器 |
|
|||
|
|
> | `togglest_pre.c` | 预线程 toggle 服务器 |
|
|||
|
|
> | `togglest_pool.c` | 线程池 toggle 服务器 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、四种并发模型总览
|
|||
|
|
|
|||
|
|
### 模型架构图
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph TD
|
|||
|
|
subgraph "模型一:多进程 Process-per-Connection"
|
|||
|
|
C1[客户端 1] -->|accept| S1[主进程]
|
|||
|
|
S1 -->|fork| P1[子进程 1]
|
|||
|
|
S1 -->|fork| P2[子进程 2]
|
|||
|
|
S1 -->|fork| P3[子进程 3]
|
|||
|
|
P1 --> R1[处理请求]
|
|||
|
|
P2 --> R2[处理请求]
|
|||
|
|
P3 --> R3[处理请求]
|
|||
|
|
end
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph TD
|
|||
|
|
subgraph "模型二:多线程 Thread-per-Connection"
|
|||
|
|
|
|||
|
|
C2[客户端] -->|accept| SM[主线程]
|
|||
|
|
SM -->|pthread_create| T1[线程 1]
|
|||
|
|
SM -->|pthread_create| T2[线程 2]
|
|||
|
|
SM -->|pthread_create| T3[线程 3]
|
|||
|
|
T1 --> H1[处理请求]
|
|||
|
|
T2 --> H2[处理请求]
|
|||
|
|
T3 --> H3[处理请求]
|
|||
|
|
|
|||
|
|
end
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph TD
|
|||
|
|
subgraph "模型三:预线程 Pre-threaded"
|
|||
|
|
C3[客户端] -->|accept| PM[主线程]
|
|||
|
|
PM -->|放入任务队列| Q[(任务队列)]
|
|||
|
|
Q -->|取任务| W1[工作线程 1]
|
|||
|
|
Q -->|取任务| W2[工作线程 2]
|
|||
|
|
Q -->|取任务| W3[工作线程 3]
|
|||
|
|
Q -->|取任务| W4[工作线程 N]
|
|||
|
|
end
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph TD
|
|||
|
|
subgraph "模型四:线程池 Thread Pool"
|
|||
|
|
C4[客户端] -->|submit| POOL[线程池管理器]
|
|||
|
|
POOL -->|dispatch| Q2[(任务队列)]
|
|||
|
|
Q2 -->|worker| WT1[工作线程 1]
|
|||
|
|
Q2 -->|worker| WT2[工作线程 2]
|
|||
|
|
Q2 -->|worker| WT3[工作线程 N]
|
|||
|
|
WT1 -->|完成| POOL
|
|||
|
|
WT2 -->|完成| POOL
|
|||
|
|
end
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 四种模型对比
|
|||
|
|
|
|||
|
|
| 模型 | 源文件 | 创建时机 | 优点 | 缺点 |
|
|||
|
|
|------|--------|----------|------|------|
|
|||
|
|
| 多进程 | `togglesp.c` | 按需 `fork` | 简单、进程隔离 | `fork` 开销大 |
|
|||
|
|
| 多线程 | `togglest.c` | 按需 `pthread_create` | 比 `fork` 轻量 | 线程创建仍有开销 |
|
|||
|
|
| 预线程 | `togglest_pre.c` | 启动时创建线程池 | 无运行时创建开销 | 需要任务队列同步 |
|
|||
|
|
| 线程池 | `togglest_pool.c` | 启动时创建线程池 | 最优性能、可复用 | 实现复杂度最高 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、任务一:验证 Toggle 服务器的四种版本
|
|||
|
|
|
|||
|
|
### 2.1 编译
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 多进程版本
|
|||
|
|
gcc -o togglesp togglesp.c csapp.c -lpthread
|
|||
|
|
|
|||
|
|
# 多线程版本
|
|||
|
|
gcc -o togglest togglest.c csapp.c -lpthread
|
|||
|
|
|
|||
|
|
# 预线程版本
|
|||
|
|
gcc -o togglest_pre togglest_pre.c csapp.c -lpthread
|
|||
|
|
|
|||
|
|
# 线程池版本
|
|||
|
|
gcc -o togglest_pool togglest_pool.c csapp.c -lpthread
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.2 分别运行与测试
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# ---- 测试多进程版本 ----
|
|||
|
|
./togglesp 8080 &
|
|||
|
|
./togglec localhost 8080
|
|||
|
|
# 输入: Hello World
|
|||
|
|
# 输出: hELLO wORLD
|
|||
|
|
|
|||
|
|
# ---- 测试多线程版本 ----
|
|||
|
|
./togglest 8081 &
|
|||
|
|
./togglec localhost 8081
|
|||
|
|
|
|||
|
|
# ---- 测试预线程版本 ----
|
|||
|
|
./togglest_pre 8082 &
|
|||
|
|
./togglec localhost 8082
|
|||
|
|
|
|||
|
|
# ---- 测试线程池版本 ----
|
|||
|
|
./togglest_pool 8083 &
|
|||
|
|
./togglec localhost 8083
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!warning] 注意
|
|||
|
|
> 测试前确保端口未被占用,可用 `lsof -i:8080` 或 `netstat -tlnp` 检查。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、多进程模型详解 — togglesp.c
|
|||
|
|
|
|||
|
|
### 3.1 整体流程
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
participant Main as 主进程
|
|||
|
|
participant Child as 子进程
|
|||
|
|
participant Client as 客户端
|
|||
|
|
|
|||
|
|
Main->>Main: Signal(SIGCHLD, handler)
|
|||
|
|
Main->>Main: listen_sock = open_listen_sock(port)
|
|||
|
|
loop 持续接受连接
|
|||
|
|
Main->>Main: conn_sock = accept(...)
|
|||
|
|
Main->>Main: fork()
|
|||
|
|
alt 子进程
|
|||
|
|
Main->>Child: 进入子进程
|
|||
|
|
Child->>Main: close(listen_sock)
|
|||
|
|
Child->>Client: toggle(conn_sock, hit)
|
|||
|
|
Child->>Child: exit(0)
|
|||
|
|
else 父进程
|
|||
|
|
Main->>Main: close(conn_sock)
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 核心代码解析
|
|||
|
|
|
|||
|
|
#### 信号处理:回收僵尸进程
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
void sigchld_handler(int sig) {
|
|||
|
|
while (waitpid(-1, 0, WNOHANG) > 0);
|
|||
|
|
// WNOHANG: 非阻塞,避免信号处理函数中阻塞
|
|||
|
|
// while 循环: 一次回收所有已终止的子进程(防止信号丢失)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!important] 为什么用 `while` 循环?
|
|||
|
|
> Unix 信号不排队。如果两个子进程几乎同时终止,可能只收到一次 `SIGCHLD`。循环调用 `waitpid` 确保所有僵尸进程都被回收。
|
|||
|
|
|
|||
|
|
#### toggle 函数:大小写转换
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
void toggle(int conn_sock, int hit) {
|
|||
|
|
int n;
|
|||
|
|
char buf[MAXLINE];
|
|||
|
|
|
|||
|
|
while ((n = recv(conn_sock, buf, MAXLINE, 0)) > 0) {
|
|||
|
|
// 逐字节进行大小写转换
|
|||
|
|
for (int i = 0; i < n; i++) {
|
|||
|
|
if (isupper(buf[i]))
|
|||
|
|
buf[i] = tolower(buf[i]);
|
|||
|
|
else if (islower(buf[i]))
|
|||
|
|
buf[i] = toupper(buf[i]);
|
|||
|
|
}
|
|||
|
|
send(conn_sock, buf, n, 0); // 原样发回转换后的数据
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 主循环:fork 并发处理
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
int main(int argc, char **argv) {
|
|||
|
|
int listen_sock, conn_sock, hit;
|
|||
|
|
socklen_t clientlen;
|
|||
|
|
struct sockaddr_storage clientaddr;
|
|||
|
|
|
|||
|
|
Signal(SIGCHLD, sigchld_handler); // 注册 SIGCHLD 处理函数
|
|||
|
|
listen_sock = open_listen_sock(argv[1]); // 创建监听套接字
|
|||
|
|
|
|||
|
|
for (hit = 1; ; hit++) {
|
|||
|
|
clientlen = sizeof(clientaddr);
|
|||
|
|
conn_sock = accept(listen_sock,
|
|||
|
|
(SA *)&clientaddr, &clientlen);
|
|||
|
|
|
|||
|
|
if (Fork() == 0) { // 子进程
|
|||
|
|
close(listen_sock); // 关闭监听套接字(子进程不需要)
|
|||
|
|
toggle(conn_sock, hit); // 处理请求
|
|||
|
|
close(conn_sock); // 关闭连接套接字
|
|||
|
|
exit(0); // 子进程退出
|
|||
|
|
}
|
|||
|
|
close(conn_sock); // 父进程关闭连接套接字
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!note] 父子进程的套接字处理
|
|||
|
|
> - **父进程**:`close(conn_sock)` — 父进程不处理该连接
|
|||
|
|
> - **子进程**:`close(listen_sock)` — 子进程不接受新连接
|
|||
|
|
> - 这是多进程服务器的标准模式,避免文件描述符泄漏
|
|||
|
|
|
|||
|
|
### 3.3 多进程模型的优缺点
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
优点 缺点
|
|||
|
|
+----------------------------------+----------------------------------+
|
|||
|
|
| 进程间地址空间隔离 | fork() 系统调用开销大 |
|
|||
|
|
| 一个进程崩溃不影响其他进程 | 每个进程占用独立内存空间 |
|
|||
|
|
| 编程模型简单直观 | 进程数受系统限制 |
|
|||
|
|
| 天然线程安全(无共享数据) | 上下文切换开销大 |
|
|||
|
|
+----------------------------------+----------------------------------+
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、多线程模型详解 — togglest.c
|
|||
|
|
|
|||
|
|
### 4.1 核心结构
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
// 线程参数结构体
|
|||
|
|
typedef struct {
|
|||
|
|
int conn_sock;
|
|||
|
|
int hit;
|
|||
|
|
} thread_args_t;
|
|||
|
|
|
|||
|
|
void *thread_toggle(void *vargp) {
|
|||
|
|
thread_args_t *args = (thread_args_t *)vargp;
|
|||
|
|
int conn_sock = args->conn_sock;
|
|||
|
|
int hit = args->hit;
|
|||
|
|
Free(vargp); // 释放参数内存
|
|||
|
|
|
|||
|
|
toggle(conn_sock, hit); // 处理请求(复用 toggle 函数)
|
|||
|
|
Close(conn_sock); // 关闭连接
|
|||
|
|
return NULL;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
int main(int argc, char **argv) {
|
|||
|
|
listen_sock = open_listen_sock(port);
|
|||
|
|
|
|||
|
|
for (hit = 1; ; hit++) {
|
|||
|
|
conn_sock = accept(listen_sock, ...);
|
|||
|
|
|
|||
|
|
// 为每个连接分配参数
|
|||
|
|
thread_args_t *args = Malloc(sizeof(thread_args_t));
|
|||
|
|
args->conn_sock = conn_sock;
|
|||
|
|
args->hit = hit;
|
|||
|
|
|
|||
|
|
// 创建分离线程(自动回收,无需 join)
|
|||
|
|
Pthread_create(&tid, NULL, thread_toggle, args);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!tip] 为什么要用 `pthread_detach`?
|
|||
|
|
> 如果不分离线程,线程终止后其资源不会自动释放,造成资源泄漏(类似僵尸进程)。使用 `pthread_detach` 或 `pthread_create` 后立即 `pthread_detach` 可避免此问题。
|
|||
|
|
|
|||
|
|
### 4.2 多线程 vs 多进程
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph LR
|
|||
|
|
subgraph 多进程
|
|||
|
|
A1[主进程] -->|fork| A2[子进程]
|
|||
|
|
A2 -->|独立地址空间| A3[处理请求]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
subgraph 多线程
|
|||
|
|
B1[主线程] -->|pthread_create| B2[新线程]
|
|||
|
|
B2 -->|共享地址空间| B3[处理请求]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
style A1 fill:#ffcdd2
|
|||
|
|
style A2 fill:#ffcdd2
|
|||
|
|
style A3 fill:#ffcdd2
|
|||
|
|
style B1 fill:#c8e6c9
|
|||
|
|
style B2 fill:#c8e6c9
|
|||
|
|
style B3 fill:#c8e6c9
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、预线程模型详解 — togglest_pre.c
|
|||
|
|
|
|||
|
|
### 5.1 设计思想
|
|||
|
|
|
|||
|
|
预线程模型在服务器启动时就创建固定数量的工作线程,所有线程共享一个任务队列,形成**生产者-消费者**模型。
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph LR
|
|||
|
|
P[主线程/生产者] -->|sbuf_insert| Q[(共享任务队列<br/>conn_sock)]
|
|||
|
|
Q -->|sbuf_remove| W1[工作线程 1]
|
|||
|
|
Q -->|sbuf_remove| W2[工作线程 2]
|
|||
|
|
Q -->|sbuf_remove| W3[工作线程 3]
|
|||
|
|
Q -->|sbuf_remove| WN[工作线程 N]
|
|||
|
|
|
|||
|
|
style P fill:#bbdefb
|
|||
|
|
style Q fill:#fff9c4
|
|||
|
|
style W1 fill:#c8e6c9
|
|||
|
|
style W2 fill:#c8e6c9
|
|||
|
|
style W3 fill:#c8e6c9
|
|||
|
|
style WN fill:#c8e6c9
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.2 核心代码解析
|
|||
|
|
|
|||
|
|
#### Sbuf:带信号量的共享缓冲区
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
typedef struct {
|
|||
|
|
int *buf; // 缓冲区数组
|
|||
|
|
int n; // 缓冲区容量
|
|||
|
|
int front; // 队头索引
|
|||
|
|
int rear; // 队尾索引
|
|||
|
|
sem_t mutex; // 互斥信号量
|
|||
|
|
sem_t slots; // 空槽数量信号量
|
|||
|
|
sem_t items; // 已有项目数信号量
|
|||
|
|
} sbuf_t;
|
|||
|
|
|
|||
|
|
void sbuf_insert(sbuf_t *sp, int item) {
|
|||
|
|
P(&sp->slots); // 等待空槽
|
|||
|
|
P(&sp->mutex); // 加锁
|
|||
|
|
sp->buf[(++sp->rear) % sp->n] = item; // 入队
|
|||
|
|
V(&sp->mutex); // 解锁
|
|||
|
|
V(&sp->items); // 增加项目数
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
int sbuf_remove(sbuf_t *sp) {
|
|||
|
|
P(&sp->items); // 等待有项目
|
|||
|
|
P(&sp->mutex); // 加锁
|
|||
|
|
int item = sp->buf[(++sp->front) % sp->n]; // 出队
|
|||
|
|
V(&sp->mutex); // 解锁
|
|||
|
|
V(&sp->slots); // 增加空槽数
|
|||
|
|
return item;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!important] 信号量的顺序不能颠倒!
|
|||
|
|
> `sbuf_insert` 中先 `P(slots)` 再 `P(mutex)`,如果反过来,可能导致死锁:线程持有 mutex 但等待 slots,而其他线程无法获取 mutex 来释放 slots。
|
|||
|
|
|
|||
|
|
#### 工作线程函数
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
void *thread_toggle(void *vargp) {
|
|||
|
|
sbuf_t *sp = (sbuf_t *)vargp;
|
|||
|
|
Pthread_detach(pthread_self());
|
|||
|
|
|
|||
|
|
while (1) {
|
|||
|
|
int conn_sock = sbuf_remove(sp); // 阻塞等待任务
|
|||
|
|
toggle(conn_sock, -1); // 处理请求
|
|||
|
|
Close(conn_sock); // 关闭连接
|
|||
|
|
}
|
|||
|
|
return NULL;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
int main(int argc, char **argv) {
|
|||
|
|
sbuf_t sbuf;
|
|||
|
|
sbuf_init(&sbuf, SBUFSIZE); // 初始化缓冲区
|
|||
|
|
|
|||
|
|
// 启动时创建 N 个工作线程
|
|||
|
|
for (int i = 0; i < NTHREADS; i++)
|
|||
|
|
Pthread_create(&tid, NULL, thread_toggle, &sbuf);
|
|||
|
|
|
|||
|
|
listen_sock = open_listen_sock(port);
|
|||
|
|
|
|||
|
|
while (1) {
|
|||
|
|
conn_sock = accept(listen_sock, ...);
|
|||
|
|
sbuf_insert(&sbuf, conn_sock); // 主线程生产
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.3 预线程模型的优势
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
传统模型(按需创建) 预线程模型(预先创建)
|
|||
|
|
accept --> fork/create accept --> 入队
|
|||
|
|
| |
|
|||
|
|
+--> [创建开销] --> 处理 +--> [零开销] --> 工作线程直接处理
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- **消除运行时创建开销**:线程在启动时一次性创建完毕
|
|||
|
|
- **控制并发度**:线程数固定,避免系统过载
|
|||
|
|
- **生产者-消费者解耦**:主线程只负责 accept,工作线程只负责处理
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、任务二~四:将 Toggle 服务器改造为 Weblet 服务器
|
|||
|
|
|
|||
|
|
### 6.1 改造思路
|
|||
|
|
|
|||
|
|
Toggle 服务器的核心逻辑是**大小写转换**,Weblet 服务器的核心逻辑是**HTTP 请求解析与响应**。改造的关键在于将 `toggle()` 函数替换为 HTTP 处理逻辑。
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph TD
|
|||
|
|
A[Toggle 服务器] -->|替换核心处理函数| B[Weblet 服务器]
|
|||
|
|
|
|||
|
|
A --> A1["toggle(): 大小写转换"]
|
|||
|
|
B --> B1["doit(): HTTP 请求处理"]
|
|||
|
|
|
|||
|
|
B1 --> B2[解析请求行 GET /path HTTP/1.1]
|
|||
|
|
B1 --> B3[解析请求头]
|
|||
|
|
B1 --> B4[构建响应: 静态/动态内容]
|
|||
|
|
B1 --> B5[发送响应给客户端]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.2 多进程 Weblet
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
// 仿照 togglesp.c 的结构
|
|||
|
|
void sigchld_handler(int sig) {
|
|||
|
|
while (waitpid(-1, 0, WNOHANG) > 0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
int main(int argc, char **argv) {
|
|||
|
|
Signal(SIGCHLD, sigchld_handler);
|
|||
|
|
listen_sock = open_listen_sock(port);
|
|||
|
|
|
|||
|
|
for (hit = 1; ; hit++) {
|
|||
|
|
conn_sock = accept(listen_sock, ...);
|
|||
|
|
|
|||
|
|
if (Fork() == 0) {
|
|||
|
|
close(listen_sock);
|
|||
|
|
doit(conn_sock, hit); // 替换为 HTTP 处理
|
|||
|
|
close(conn_sock);
|
|||
|
|
exit(0);
|
|||
|
|
}
|
|||
|
|
close(conn_sock);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.3 多线程 Weblet
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
// 仿照 togglest.c 的结构
|
|||
|
|
void *thread_doit(void *vargp) {
|
|||
|
|
int conn_sock = *((int *)vargp);
|
|||
|
|
Free(vargp);
|
|||
|
|
Pthread_detach(pthread_self());
|
|||
|
|
|
|||
|
|
doit(conn_sock, -1); // HTTP 处理
|
|||
|
|
Close(conn_sock);
|
|||
|
|
return NULL;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
int main(int argc, char **argv) {
|
|||
|
|
listen_sock = open_listen_sock(port);
|
|||
|
|
|
|||
|
|
while (1) {
|
|||
|
|
clientlen = sizeof(clientaddr);
|
|||
|
|
conn_sock = accept(listen_sock, ...);
|
|||
|
|
|
|||
|
|
int *fdp = Malloc(sizeof(int));
|
|||
|
|
*fdp = conn_sock;
|
|||
|
|
Pthread_create(&tid, NULL, thread_doit, fdp);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.4 预线程 Weblet
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
// 仿照 togglest_pre.c 的结构
|
|||
|
|
void *thread_doit(void *vargp) {
|
|||
|
|
sbuf_t *sp = (sbuf_t *)vargp;
|
|||
|
|
Pthread_detach(pthread_self());
|
|||
|
|
|
|||
|
|
while (1) {
|
|||
|
|
int conn_sock = sbuf_remove(sp);
|
|||
|
|
doit(conn_sock, -1); // HTTP 处理
|
|||
|
|
Close(conn_sock);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
int main(int argc, char **argv) {
|
|||
|
|
sbuf_t sbuf;
|
|||
|
|
sbuf_init(&sbuf, SBUFSIZE);
|
|||
|
|
|
|||
|
|
for (int i = 0; i < NTHREADS; i++)
|
|||
|
|
Pthread_create(&tid, NULL, thread_doit, &sbuf);
|
|||
|
|
|
|||
|
|
listen_sock = open_listen_sock(port);
|
|||
|
|
|
|||
|
|
while (1) {
|
|||
|
|
conn_sock = accept(listen_sock, ...);
|
|||
|
|
sbuf_insert(&sbuf, conn_sock);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七、任务五:性能对比测试(http_load)
|
|||
|
|
|
|||
|
|
### 7.1 测试方法
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 生成 URL 文件
|
|||
|
|
echo "http://localhost:8080/index.html" > urls.txt
|
|||
|
|
|
|||
|
|
# 使用 http_load 进行压力测试
|
|||
|
|
# -p 并发数,-s 测试时间(秒)
|
|||
|
|
http_load -p 10 -s 10 urls.txt # 10 并发,持续 10 秒
|
|||
|
|
http_load -p 50 -s 10 urls.txt # 50 并发,持续 10 秒
|
|||
|
|
http_load -p 100 -s 10 urls.txt # 100 并发,持续 10 秒
|
|||
|
|
http_load -p 200 -s 10 urls.txt # 200 并发,持续 10 秒
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 7.2 测试结果记录模板
|
|||
|
|
|
|||
|
|
| 并发数 | 多进程 (req/s) | 多线程 (req/s) | 预线程 (req/s) | 线程池 (req/s) |
|
|||
|
|
|--------|---------------|---------------|---------------|---------------|
|
|||
|
|
| 10 | | | | |
|
|||
|
|
| 50 | | | | |
|
|||
|
|
| 100 | | | | |
|
|||
|
|
| 200 | | | | |
|
|||
|
|
|
|||
|
|
### 7.3 预期结论
|
|||
|
|
|
|||
|
|
> [!abstract] 性能排序
|
|||
|
|
> **线程池 >= 预线程 > 多线程 > 多进程**
|
|||
|
|
>
|
|||
|
|
> - **多进程**:`fork()` 开销随并发数增加显著上升
|
|||
|
|
> - **多线程**:`pthread_create` 比 `fork` 轻量,但仍有创建开销
|
|||
|
|
> - **预线程**:启动时创建线程池,运行时零创建开销
|
|||
|
|
> - **线程池**:在预线程基础上增加了线程复用和动态管理
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 八、关键知识点总结
|
|||
|
|
|
|||
|
|
### fork 与 pthread_create 的对比
|
|||
|
|
|
|||
|
|
| 特性 | fork (多进程) | pthread_create (多线程) |
|
|||
|
|
|------|--------------|------------------------|
|
|||
|
|
| 地址空间 | 复制(写时复制) | 共享 |
|
|||
|
|
| 创建开销 | 大(页表复制) | 较小 |
|
|||
|
|
| 通信方式 | 进程间通信(管道等) | 直接共享内存 |
|
|||
|
|
| 崩溃隔离 | 互相隔离 | 一个线程崩溃可能影响全部 |
|
|||
|
|
| 文件描述符 | 复制 | 共享 |
|
|||
|
|
| 信号处理 | 各进程独立 | 需要协调 |
|
|||
|
|
|
|||
|
|
### 信号量在生产者-消费者中的应用
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
participant P as 生产者(主线程)
|
|||
|
|
participant M as mutex
|
|||
|
|
participant S as slots 信号量
|
|||
|
|
participant I as items 信号量
|
|||
|
|
participant C as 消费者(工作线程)
|
|||
|
|
|
|||
|
|
P->>S: P(slots) — 等待空槽
|
|||
|
|
P->>M: P(mutex) — 加锁
|
|||
|
|
P->>P: 插入任务到队列
|
|||
|
|
P->>M: V(mutex) — 解锁
|
|||
|
|
P->>I: V(items) — 通知有新任务
|
|||
|
|
|
|||
|
|
C->>I: P(items) — 等待任务
|
|||
|
|
C->>M: P(mutex) — 加锁
|
|||
|
|
C->>C: 从队列取出任务
|
|||
|
|
C->>M: V(mutex) — 解锁
|
|||
|
|
C->>S: V(slots) — 释放空槽
|
|||
|
|
C->>C: 处理请求
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 常见问题
|
|||
|
|
|
|||
|
|
> [!failure] 问题:多进程版本出现僵尸进程
|
|||
|
|
> **原因**:未正确处理 `SIGCHLD` 信号,或信号处理函数中未使用 `WNOHANG` 循环回收。
|
|||
|
|
> **解决**:确保 `sigchld_handler` 中使用 `while (waitpid(-1, 0, WNOHANG) > 0);`
|
|||
|
|
|
|||
|
|
> [!failure] 问题:多线程版本出现段错误(Segmentation Fault)
|
|||
|
|
> **原因**:传递给线程的参数在主线程中被覆盖。
|
|||
|
|
> **解决**:为每个线程 `malloc` 独立的参数结构体,不要传递局部变量的地址。
|
|||
|
|
|
|||
|
|
> [!failure] 问题:预线程版本程序卡死不响应
|
|||
|
|
> **原因**:信号量使用顺序错误导致死锁。
|
|||
|
|
> **解决**:`sbuf_insert` 中先 `P(slots)` 后 `P(mutex)`;`sbuf_remove` 中先 `P(items)` 后 `P(mutex)`。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 扩展阅读
|
|||
|
|
|
|||
|
|
- [[06_进程控制]] — fork、waitpid、信号处理详细原理
|
|||
|
|
- [[07_多线程编程]] — pthread 线程编程详解
|
|||
|
|
- [[10_并发服务器]] — 三种并发模型的理论对比
|
|||
|
|
- [[实践01_Web服务器初步实现]] — 实验一基础 Web 服务器
|