vault backup: 2026-06-13 23:46:22

This commit is contained in:
2026-06-13 23:46:22 +08:00
parent 9a3d58dd3b
commit 224c3dc574
33 changed files with 14997 additions and 0 deletions

View File

@@ -0,0 +1,337 @@
# 实验01 Linux I/O 编程
## 实验目的
1. 练习 UNIX I/O 函数(`open``close``read``write``lseek`)的使用
2. 掌握标准 I/O 函数(`fgets``fread``fwrite`)的操作方式
3. 建立 API 开销的概念,理解系统调用与库函数的性能差异
4. 熟悉结构体的二进制 I/O 读写方法
5. 综合运用文件 I/O 完成文本处理任务
## 涉及知识点
- 文件描述符与 `open`/`close`/`read`/`write` 系统调用
- 标准 I/O`fopen`/`fclose`/`fgets`/`fprintf`/`fread`/`fwrite`
- 文件打开模式:`O_RDONLY``O_WRONLY``O_CREAT``O_TRUNC``O_APPEND`
- 结构体与文件 I/O 结合(二进制序列化)
- `gettimeofday` 高精度计时
- 字符串处理:`strtok``strcmp``strstr``sscanf``%[^:]`
- 排序算法(词频统计中的字典序排列)
---
## 任务一task41.c —— 学生信息文件字段处理
### 任务要求
1. 创建文件 `student.txt`,写入若干学生记录,每行格式为 `姓名:学号:学院:年龄:性别`
2.`student.txt` 中查找所有属于"计算机与网络安全学院"的记录
3. 将找到的记录字段顺序调整为 `学号:姓名:性别:年龄:学院`
4. 将调整后的记录写入 `csStudent.txt`
### 关键代码提示
```c
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
// ---- 第一步:创建并写入 student.txt ----
int fd = open("student.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) { perror("open student.txt"); exit(1); }
const char *records[] = {
"张三:2023001:计算机与网络安全学院:20:男\n",
"李四:2023002:电子信息学院:21:女\n",
"王五:2023003:计算机与网络安全学院:22:男\n",
"赵六:2023004:数学学院:19:女\n",
"钱七:2023005:计算机与网络安全学院:20:男\n",
};
for (int i = 0; i < 5; i++)
write(fd, records[i], strlen(records[i]));
close(fd);
// ---- 第二步:读取、筛选、重组字段 ----
FILE *fin = fopen("student.txt", "r");
FILE *fout = fopen("csStudent.txt", "w");
char line[256];
while (fgets(line, sizeof(line), fin) != NULL) {
if (strstr(line, "计算机与网络安全学院") != NULL) {
char name[64], id[64], college[64], age[16], gender[16];
sscanf(line, "%[^:]:%[^:]:%[^:]:%[^:]:%s",
name, id, college, age, gender);
// 调整字段顺序:学号:姓名:性别:年龄:学院
fprintf(fout, "%s:%s:%s:%s:%s\n", id, name, gender, age, college);
}
}
fclose(fin);
fclose(fout);
return 0;
}
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| `write` 后文件内容为空 | 忘记 `close`,数据还在内核缓冲区 | 写完后务必 `close(fd)` |
| 读取中文出现乱码 | 编码不匹配 | 确保源文件为 UTF-8 编码,终端 locale 一致 |
| `strtok` 分割结果不对 | 行末换行符干扰 | 分割前先去除 `\n` |
| `sscanf` 读取不完整 | 格式字符串匹配错误 | 使用 `%[^:]` 匹配非冒号字符序列 |
---
## 任务二task42.c —— 结构体二进制文件读写
### 任务要求
1. 从键盘读入 5 个学生的信息(学号、姓名、语文、数学、英语成绩),存入结构体数组
2. 将结构体数组以二进制方式写入文件 `score.dat`(使用 `write` 写入原始字节)
3. 从文件中读取第 1、3、5 条记录并显示
### 关键代码提示
```c
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
typedef struct {
int id;
char name[32];
float chinese;
float math;
float english;
} Student;
int main() {
Student stu[5];
// 从键盘读入
for (int i = 0; i < 5; i++) {
printf("请输入第%d个学生(学号 姓名 语文 数学 英语): ", i + 1);
scanf("%d %s %f %f %f", &stu[i].id, stu[i].name,
&stu[i].chinese, &stu[i].math, &stu[i].english);
}
// 二进制写入
int fd = open("score.dat", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, stu, sizeof(Student) * 5);
close(fd);
// 读取第 1、3、5 条(下标 0、2、4
fd = open("score.dat", O_RDONLY);
Student temp;
for (int i = 0; i < 5; i++) {
read(fd, &temp, sizeof(Student));
if (i == 0 || i == 2 || i == 4) {
printf("学号:%d 姓名:%s 语文:%.1f 数学:%.1f 英语:%.1f\n",
temp.id, temp.name, temp.chinese, temp.math, temp.english);
}
}
// 也可用 lseek 精确定位到第 3 条
lseek(fd, sizeof(Student) * 2, SEEK_SET);
read(fd, &temp, sizeof(Student));
printf("第3条: %s\n", temp.name);
close(fd);
return 0;
}
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 读出的数值不对 | 结构体内存对齐padding | `sizeof(Student)` 可能大于各字段大小之和,属正常现象 |
| `lseek` 定位不准 | 偏移量计算错误 | 偏移量 = `sizeof(Student) * (n - 1)` |
| 中文姓名存储异常 | `char name[32]` 对 UTF-8 中文不够 | 增大缓冲区(一个汉字占 3 字节) |
---
## 任务三task43.c —— API 执行时间测量(选做)
### 任务要求
1. 分别测量 `read`/`write``fread`/`fwrite` 在不同数据量下的执行时间
2. 对比系统调用与库函数的性能差异
3. 绘制或输出性能对比表
### 关键代码提示
```c
#include <sys/time.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
long time_diff(struct timeval *start, struct timeval *end) {
return (end->tv_sec - start->tv_sec) * 1000000L
+ (end->tv_usec - start->tv_usec);
}
int main() {
struct timeval start, end;
int N = 1000000; // 循环次数
char buf[1];
// 测量 write逐字节
int fd = open("test.dat", O_WRONLY | O_CREAT | O_TRUNC, 0644);
gettimeofday(&start, NULL);
for (int i = 0; i < N; i++)
write(fd, buf, 1);
gettimeofday(&end, NULL);
close(fd);
printf("write 逐字节: %ld 微秒\n", time_diff(&start, &end));
// 测量 fwrite逐字节带用户缓冲
FILE *fp = fopen("test2.dat", "w");
gettimeofday(&start, NULL);
for (int i = 0; i < N; i++)
fwrite(buf, 1, 1, fp);
gettimeofday(&end, NULL);
fclose(fp);
printf("fwrite 逐字节: %ld 微秒\n", time_diff(&start, &end));
return 0;
}
```
### 测量方案
| 测量项 | 操作 | 说明 |
|--------|------|------|
| `write` | 逐字节写 1MB | 基准:每次陷入内核 |
| `read` | 逐字节读 1MB | 基准:每次陷入内核 |
| `fwrite` | 逐字节写 1MB | 带用户空间缓冲 |
| `fread` | 逐字节读 1MB | 带用户空间缓冲 |
| `write` | 块写入4KB | 对比块大小影响 |
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 计时结果为 0 | 操作太快,微秒级精度不够 | 增加循环次数到百万级 |
| 系统调用比库函数慢很多 | 每次 `read`/`write` 都陷入内核 | 正常现象,体现用户缓冲的价值 |
| 结果波动大 | 系统调度干扰 | 多次测量取平均值 |
---
## 任务四task44.c —— 英文文章词频统计
### 任务要求
1. 读取一篇英文文章(从文件或标准输入)
2. 统计每个单词出现的次数
3. 输出格式:`单词:次数`
4. 按字典序排列所有单词
5. 额外输出出现频度最高的 10 个单词
### 关键代码提示
```c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#define MAX_WORDS 10000
typedef struct {
char word[64];
int count;
} WordEntry;
WordEntry dict[MAX_WORDS];
int dict_size = 0;
// 查找已有单词或插入新单词
int find_or_insert(const char *word) {
for (int i = 0; i < dict_size; i++) {
if (strcmp(dict[i].word, word) == 0) {
dict[i].count++;
return i;
}
}
strcpy(dict[dict_size].word, word);
dict[dict_size].count = 1;
return dict_size++;
}
// qsort 比较函数:字典序
int cmp_alpha(const void *a, const void *b) {
return strcmp(((WordEntry *)a)->word, ((WordEntry *)b)->word);
}
// qsort 比较函数:频度降序
int cmp_freq(const void *a, const void *b) {
return ((WordEntry *)b)->count - ((WordEntry *)a)->count;
}
int main() {
FILE *fp = fopen("article.txt", "r");
if (!fp) { perror("fopen"); return 1; }
char word[64];
while (fscanf(fp, "%63s", word) == 1) {
// 去除标点,统一小写
char clean[64];
int j = 0;
for (int i = 0; word[i]; i++) {
if (isalpha(word[i]))
clean[j++] = tolower(word[i]);
}
clean[j] = '\0';
if (j > 0)
find_or_insert(clean);
}
fclose(fp);
// 按字典序输出
qsort(dict, dict_size, sizeof(WordEntry), cmp_alpha);
for (int i = 0; i < dict_size; i++)
printf("%s:%d\n", dict[i].word, dict[i].count);
// 按频度降序输出前 10 个
qsort(dict, dict_size, sizeof(WordEntry), cmp_freq);
printf("\n频度最高的10个单词:\n");
for (int i = 0; i < 10 && i < dict_size; i++)
printf("%s:%d\n", dict[i].word, dict[i].count);
return 0;
}
```
### 注意事项
- 单词提取时需过滤标点符号(逗号、句号、引号等)
- 不区分大小写(统一转为小写)
- 连字符(如 "well-known")可按需决定是否拆分
- 文件较大时注意 `MAX_WORDS` 的上限,可改用动态分配
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 单词带着标点 | 没有清理非字母字符 | 用 `isalpha` 逐字符过滤 |
| 大小写被当成不同单词 | 未统一大小写 | 提取前用 `tolower` 转换 |
| 排序结果不对 | `qsort` 比较函数写错 | 注意比较函数的参数类型转换 |
| 数组越界 | 单词数超过 `MAX_WORDS` | 动态扩容(`realloc`)或增大数组 |
---
## 实验总结
通过本实验,应掌握以下能力:
1. 熟练使用底层 I/O`open`/`read`/`write`)和标准 I/O`fopen`/`fgets`/`fprintf`
2. 理解文件描述符与 `FILE *` 的区别
3. 能用结构体进行二进制文件读写
4. 了解系统调用与库函数的性能差异
5. 综合运用字符串处理和文件 I/O 解决实际问题

View File

@@ -0,0 +1,437 @@
# 实验02 Linux 进程控制编程
## 实验目的
1. 掌握 `fork``exec``wait``exit` 等进程控制系统调用
2. 理解进程的创建、执行和终止过程
3. 掌握进程族亲关系(父子进程、兄弟进程)
4. 学会编写简单的交互式 shell 程序
5. 理解守护进程daemon的概念和实现方法
6. 了解信号机制的基本使用
## 涉及知识点
- `fork()` 创建子进程及返回值含义
- `exec` 家族函数:`execl``execlp``execvp``execve`
- `wait` / `waitpid` 回收子进程,避免僵尸进程
- `exit` / `_exit` 终止进程
- 进程组与会话(`setsid`
- 信号:`SIGCHLD``SIGKILL``SIGTERM``SIGALRM`
- 守护进程daemon的创建步骤
- 文件重定向:`dup2`
- 管道:`pipe`
---
## 任务一task51.c -- 进程族亲结构
### 任务要求
创建如下进程树结构,每个进程打印自己的 PID 和父进程 PPID最终输出完整的族亲关系
```
p1
+-- p11
+-- p12
+-- p121
+-- p122
```
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid;
printf("p1: PID=%d, PPID=%d\n", getpid(), getppid());
// 创建 p11
pid = fork();
if (pid == 0) {
printf("p11: PID=%d, PPID=%d\n", getpid(), getppid());
exit(0);
}
// 创建 p12
pid = fork();
if (pid == 0) {
printf("p12: PID=%d, PPID=%d\n", getpid(), getppid());
// p12 创建 p121
pid = fork();
if (pid == 0) {
printf("p121: PID=%d, PPID=%d\n", getpid(), getppid());
exit(0);
}
// p12 创建 p122
pid = fork();
if (pid == 0) {
printf("p122: PID=%d, PPID=%d\n", getpid(), getppid());
exit(0);
}
wait(NULL);
wait(NULL);
exit(0);
}
// p1 等待两个子进程
wait(NULL);
wait(NULL);
return 0;
}
```
### 关键要点
- `fork()` 返回值:父进程中返回子进程 PID子进程中返回 0
- `wait(NULL)` 阻塞等待任意一个子进程结束
- 子进程必须调用 `exit(0)` 终止,否则会继续执行父进程后续代码
- `fork` 后父子进程从同一位置继续执行,注意判断返回值分流
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 输出顺序不确定 | 父子进程并发执行 | 正常现象,可用 `wait` 控制部分顺序 |
| 进程数不对 | `fork` 位置错误 | 确保在正确的位置 `fork`,避免重复创建 |
| 僵尸进程 | 父进程未 `wait` | 每个 `fork` 后都要有对应的 `wait` |
| p11 的 PPID 不是 p1 | 父进程先退出 | 正常现象(孤儿进程被 init 收养),可用 `sleep` 验证 |
---
## 任务二task52.c -- 交互式 shell
### 任务要求
实现一个简化版的交互式 shell具备以下功能
1. 显示提示符 `%`,等待用户输入命令
2. 解析并执行用户输入的外部命令
3. 输入 `exit` 退出 shell
4. 支持输出重定向(`>`
5. 支持管道(`|`
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#define MAX_LINE 1024
#define MAX_ARGS 64
// 解析命令行为参数数组
int parse(char *line, char **args) {
int argc = 0;
char *token = strtok(line, " \t\n");
while (token != NULL && argc < MAX_ARGS - 1) {
args[argc++] = token;
token = strtok(NULL, " \t\n");
}
args[argc] = NULL;
return argc;
}
// 处理输出重定向
void handle_redirect(char **args) {
for (int i = 0; args[i] != NULL; i++) {
if (strcmp(args[i], ">") == 0) {
int fd = open(args[i + 1],
O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO);
close(fd);
args[i] = NULL;
break;
}
}
}
// 处理管道
void handle_pipe(char *line) {
char *cmd1 = strtok(line, "|");
char *cmd2 = strtok(NULL, "|");
int pipefd[2];
pipe(pipefd);
pid_t pid1 = fork();
if (pid1 == 0) {
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
char *args[MAX_ARGS];
parse(cmd1, args);
execvp(args[0], args);
perror("execvp");
exit(1);
}
pid_t pid2 = fork();
if (pid2 == 0) {
close(pipefd[1]);
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
char *args[MAX_ARGS];
parse(cmd2, args);
execvp(args[0], args);
perror("execvp");
exit(1);
}
close(pipefd[0]);
close(pipefd[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);
}
int main() {
char line[MAX_LINE];
while (1) {
printf("%% ");
fflush(stdout);
if (fgets(line, sizeof(line), stdin) == NULL) break;
if (strncmp(line, "exit", 4) == 0) break;
if (strchr(line, '|') != NULL) {
handle_pipe(line);
continue;
}
char *args[MAX_ARGS];
parse(line, args);
pid_t pid = fork();
if (pid == 0) {
handle_redirect(args);
execvp(args[0], args);
perror("execvp");
exit(1);
}
waitpid(pid, NULL, 0);
}
return 0;
}
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| shell 卡住不响应 | `fgets` 缓冲问题 | 输入后确保有换行符,提示符后用 `fflush(stdout)` |
| 重定向不生效 | `dup2` 调用时机不对 | 必须在 `execvp` 之前调用 `dup2` |
| 管道只执行一半 | 文件描述符未关闭 | 父子进程中都要关闭不用的管道端 |
| `exit` 无法退出 | 字符串比较逻辑错误 | 用 `strncmp` 精确匹配前 4 个字符 |
---
## 任务三task53.c -- daemon 文件监控
### 任务要求
1. 创建守护进程daemon后台运行
2. 每隔 5 分钟读取 `task53.c` 文件内容,计算 hash 值
3. 与上一次的 hash 值对比
4. 如果文件被篡改,将事件记录到日志文件
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
// djb2 哈希算法
unsigned long compute_hash(const char *filename) {
FILE *fp = fopen(filename, "r");
if (!fp) return 0;
unsigned long hash = 5381;
int c;
while ((c = fgetc(fp)) != EOF)
hash = ((hash << 5) + hash) + c;
fclose(fp);
return hash;
}
// daemon 创建五步法
void daemonize() {
pid_t pid = fork();
if (pid < 0) exit(1);
if (pid > 0) exit(0);
setsid();
pid = fork();
if (pid < 0) exit(1);
if (pid > 0) exit(0);
chdir("/");
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
}
int main() {
daemonize();
unsigned long last_hash = compute_hash("task53.c");
FILE *logfp;
while (1) {
sleep(300);
unsigned long current_hash = compute_hash("task53.c");
if (current_hash != last_hash) {
logfp = fopen("/var/log/task53.log", "a");
if (logfp) {
time_t now = time(NULL);
fprintf(logfp, "[%s] task53.c changed! "
"old=%lx, new=%lx\n",
ctime(&now), last_hash, current_hash);
fclose(logfp);
}
last_hash = current_hash;
}
}
return 0;
}
```
### daemon 创建步骤(五步法)
```
1. fork() + 父进程 exit -- 脱离终端控制
2. setsid() -- 创建新会话,成为会话首进程
3. fork() + 父进程 exit -- 确保不会重新获得控制终端
4. chdir("/") -- 避免占用可卸载的文件系统
5. 关闭 0/1/2 -- 释放标准输入输出
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| daemon 无法后台运行 | 没有正确 `fork` 两次 | 严格按照五步法创建 |
| 日志文件无输出 | 路径权限问题 | 检查 `/var/log/` 写权限,或改用用户目录 |
| 如何终止 daemon | 无交互终端 | `ps aux | grep task53` 找到 PID 后 `kill` |
| hash 算法冲突 | 简单 hash 可能碰撞 | 实验中 djb2 即可,正式场景可用 MD5/SHA |
---
## 任务四task54.c -- 信号管理子进程(选做)
### 任务要求
实现一个通过信号管理子进程的程序,支持以下命令:
- `create`:创建一个子进程
- `kill <pid>`:向指定子进程发送 SIGTERM
- `ps`:列出所有存活的子进程
- `exit`:终止所有子进程并退出
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#define MAX_CHILDREN 64
pid_t children[MAX_CHILDREN];
int child_count = 0;
void sigchld_handler(int sig) {
pid_t pid;
while ((pid = waitpid(-1, NULL, WNOHANG)) > 0) {
for (int i = 0; i < child_count; i++) {
if (children[i] == pid) {
children[i] = children[--child_count];
break;
}
}
printf("[parent] child %d terminated\n", pid);
}
}
void child_work() {
while (1) sleep(10);
}
int main() {
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);
char cmd[64];
while (1) {
printf("cmd> ");
fflush(stdout);
if (fgets(cmd, sizeof(cmd), stdin) == NULL) break;
if (strncmp(cmd, "create", 6) == 0) {
pid_t pid = fork();
if (pid == 0) { child_work(); exit(0); }
children[child_count++] = pid;
printf("created child PID=%d\n", pid);
} else if (strncmp(cmd, "kill", 4) == 0) {
pid_t pid;
sscanf(cmd + 5, "%d", &pid);
kill(pid, SIGTERM);
} else if (strncmp(cmd, "ps", 2) == 0) {
printf("alive children: ");
for (int i = 0; i < child_count; i++)
printf("%d ", children[i]);
printf("\n");
} else if (strncmp(cmd, "exit", 4) == 0) {
for (int i = 0; i < child_count; i++)
kill(children[i], SIGTERM);
sleep(1);
break;
}
}
return 0;
}
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 僵尸进程残留 | `SIGCHLD` 未正确处理 | handler 中用 `waitpid(-1, ..., WNOHANG)` 循环回收 |
| `ps` 列表不准 | handler 与主程序竞争共享数组 | 使用信号屏蔽(`sigprocmask`)保护临界区 |
| 子进程无法终止 | 信号被忽略或屏蔽 | 确保子进程没有屏蔽 `SIGTERM` |
---
## 实验总结
通过本实验,应掌握以下能力:
1. 使用 `fork` 创建进程树,理解父子进程关系
2. 使用 `exec` 家族函数执行外部命令
3. 使用 `wait`/`waitpid` 回收子进程,避免僵尸进程
4. 实现简单的 shell理解 shell 的工作原理
5. 创建守护进程,理解 daemon 的设计模式
6. 使用信号机制进行进程间通信

View File

@@ -0,0 +1,470 @@
# 实验03 Linux 多线程编程
## 实验目的
1. 掌握 POSIX 线程pthread的创建、等待和终止
2. 理解线程间共享地址空间的特性
3. 掌握信号量semaphore在线程同步中的使用
4. 理解竞态条件race condition的成因及修复方法
5. 学会使用生产者-消费者模型解决缓冲区同步问题
6. 了解并行计算中的加速比概念
## 涉及知识点
- `pthread_create` / `pthread_join` / `pthread_detach`
- POSIX 信号量:`sem_init` / `sem_wait`P 操作) / `sem_post`V 操作)
- 互斥锁mutex与信号量的区别
- 生产者-消费者问题
- 竞态条件与临界区保护
- 并行求和与加速比测量
- `fork` vs `pthread_create` 的开销对比
---
## 任务一task61.c —— 三线程交替打印
### 任务要求
创建 3 个线程 T1、T2、T3每个线程执行 5 次 `printf` 打印自己的线程 ID 和当前是第几次打印,每次打印后随机等待 1~5 秒。
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#define NUM_THREADS 3
#define PRINT_COUNT 5
void *thread_func(void *arg) {
int id = *(int *)arg;
for (int i = 0; i < PRINT_COUNT; i++) {
printf("T%d: 第%d次打印 (PID=%d, TID=%lu)\n",
id, i + 1, getpid(), (unsigned long)pthread_self());
int wait_time = rand() % 5 + 1; // 1~5 秒
sleep(wait_time);
}
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
int ids[NUM_THREADS];
srand(time(NULL));
for (int i = 0; i < NUM_THREADS; i++) {
ids[i] = i + 1;
pthread_create(&threads[i], NULL, thread_func, &ids[i]);
}
for (int i = 0; i < NUM_THREADS; i++)
pthread_join(threads[i], NULL);
printf("所有线程执行完毕\n");
return 0;
}
// 编译gcc -o task61 task61.c -lpthread
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 输出交错混乱 | 多线程并发写 stdout | 正常现象,可用互斥锁保护 `printf` |
| 传参错误 | 循环变量地址被覆盖 | 使用数组存储各线程参数,而非循环变量地址 |
| 编译报错 undefined reference | 未链接 pthread | 加 `-lpthread` |
---
## 任务二task62.c —— 用信号量修复竞态条件
### 任务要求
给定一个存在竞态条件的 `badcount.c` 程序(多线程同时对共享计数器自增),使用信号量修复该问题,使最终结果正确。
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define NTHREADS 4
#define NITERS 1000000
volatile long counter = 0; // 共享计数器
sem_t mutex; // 信号量(用作互斥锁)
void *badcount(void *arg) {
for (int i = 0; i < NITERS; i++) {
sem_wait(&mutex); // P 操作:进入临界区
counter++;
sem_post(&mutex); // V 操作:离开临界区
}
return NULL;
}
int main() {
pthread_t threads[NTHREADS];
sem_init(&mutex, 0, 1); // 初始值为 1相当于互斥锁
for (int i = 0; i < NTHREADS; i++)
pthread_create(&threads[i], NULL, badcount, NULL);
for (int i = 0; i < NTHREADS; i++)
pthread_join(threads[i], NULL);
printf("期望值: %d\n", NTHREADS * NITERS);
printf("实际值: %ld\n", counter);
printf("差值: %ld\n",
(long)(NTHREADS * NITERS) - counter);
sem_destroy(&mutex);
return 0;
}
```
### 修复原理
- 不加信号量时,`counter++` 不是原子操作(读-改-写三步),多线程交错执行导致丢失更新
- `sem_wait` / `sem_post``counter++` 包裹为临界区,保证同一时刻只有一个线程执行
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 修复后差值仍不为 0 | 信号量使用错误 | 确保 `sem_wait`/`sem_post` 配对,且初始值为 1 |
| 程序死锁 | `sem_wait` 多次但 `sem_post` 不足 | 检查每个 `sem_wait` 是否有对应的 `sem_post` |
| 性能下降严重 | 信号量粒度太大 | 可尝试减小临界区范围 |
---
## 任务三task63.c —— 生产者-消费者问题
### 任务要求
实现 k 个生产者线程和 m 个消费者线程,共享一个大小为 N 的环形缓冲区。生产者向缓冲区放入数据,消费者从缓冲区取出数据,使用信号量实现同步。
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define N 10 // 缓冲区大小
#define K 3 // 生产者数量
#define M 2 // 消费者数量
#define ITERS 20 // 每个生产者生产数量
int buffer[N]; // 环形缓冲区
int in = 0; // 生产者写入位置
int out = 0; // 消费者读取位置
sem_t mutex; // 互斥访问缓冲区
sem_t slots; // 空闲槽位数(初始 N
sem_t items; // 已有物品数(初始 0
void *producer(void *arg) {
int id = *(int *)arg;
for (int i = 0; i < ITERS; i++) {
int item = id * 100 + i;
sem_wait(&slots); // 等待空闲槽位
sem_wait(&mutex); // 进入临界区
buffer[in] = item;
printf("生产者%d: 放入 buffer[%d] = %d\n", id, in, item);
in = (in + 1) % N;
sem_post(&mutex); // 离开临界区
sem_post(&items); // 增加物品计数
}
return NULL;
}
void *consumer(void *arg) {
int id = *(int *)arg;
int total = (K * ITERS) / M; // 每个消费者消费数量
for (int i = 0; i < total; i++) {
sem_wait(&items); // 等待物品
sem_wait(&mutex); // 进入临界区
int item = buffer[out];
printf("消费者%d: 取出 buffer[%d] = %d\n", id, out, item);
out = (out + 1) % N;
sem_post(&mutex); // 离开临界区
sem_post(&slots); // 增加空闲槽位
}
return NULL;
}
int main() {
pthread_t ptid[K], ctid[M];
int pids[K], cids[M];
sem_init(&mutex, 0, 1);
sem_init(&slots, 0, N);
sem_init(&items, 0, 0);
for (int i = 0; i < K; i++) {
pids[i] = i;
pthread_create(&ptid[i], NULL, producer, &pids[i]);
}
for (int i = 0; i < M; i++) {
cids[i] = i;
pthread_create(&ctid[i], NULL, consumer, &cids[i]);
}
for (int i = 0; i < K; i++) pthread_join(ptid[i], NULL);
for (int i = 0; i < M; i++) pthread_join(ctid[i], NULL);
sem_destroy(&mutex);
sem_destroy(&slots);
sem_destroy(&items);
printf("所有生产者和消费者完成\n");
return 0;
}
```
### 信号量使用要点
| 信号量 | 初始值 | 含义 |
|--------|--------|------|
| `mutex` | 1 | 互斥锁,保护临界区 |
| `slots` | N | 空闲槽位数,生产者 P(slots)、消费者 V(slots) |
| `items` | 0 | 已有物品数,消费者 P(items)、生产者 V(items) |
**关键顺序:** P 操作时先 `P(slots/items)``P(mutex)`,否则可能死锁。
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 死锁 | P 操作顺序不对 | 先 `P(slots)``P(mutex)` |
| 缓冲区越界 | 环形索引计算错误 | 使用 `% N` 取模 |
| 生产者/消费者数量不匹配 | 总生产量 != 总消费量 | 合理分配每个线程的生产/消费数量 |
---
## 任务四task64.c —— 并行求和与加速比
### 任务要求
实现 `psum64.c`:将长度为 n 的数组分成若干段,每个线程计算一段的部分和,最后汇总得到总和。分别测试 1、2、4、8、16 个线程的执行时间,计算加速比。
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/time.h>
#define MAXN 100000000 // 1 亿元素
#define MAX_THREADS 16
long a[MAXN]; // 全局数组
long psum[MAX_THREADS]; // 各线程的部分和
int n, num_threads;
void *sum_thread(void *arg) {
int id = *(int *)arg;
long chunk = n / num_threads;
long start = id * chunk;
long end = (id == num_threads - 1) ? n : start + chunk;
psum[id] = 0;
for (long i = start; i < end; i++)
psum[id] += a[i];
return NULL;
}
long get_time_us() {
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000000L + tv.tv_usec;
}
int main() {
n = MAXN;
for (int i = 0; i < n; i++)
a[i] = i + 1; // 初始化数组
int thread_counts[] = {1, 2, 4, 8, 16};
long base_time = 0;
for (int t = 0; t < 5; t++) {
num_threads = thread_counts[t];
pthread_t threads[MAX_THREADS];
int ids[MAX_THREADS];
long start = get_time_us();
for (int i = 0; i < num_threads; i++) {
ids[i] = i;
pthread_create(&threads[i], NULL, sum_thread, &ids[i]);
}
for (int i = 0; i < num_threads; i++)
pthread_join(threads[i], NULL);
long total = 0;
for (int i = 0; i < num_threads; i++)
total += psum[i];
long elapsed = get_time_us() - start;
if (t == 0) base_time = elapsed;
printf("线程数=%2d, 总和=%ld, 耗时=%ldus, 加速比=%.2f\n",
num_threads, total, elapsed,
(double)base_time / elapsed);
}
return 0;
}
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 加速比不理想 | 线程创建开销、缓存竞争 | 正常现象,线程数超过核心数后收益递减 |
| 总和不对 | 分段边界计算错误 | 注意最后一段包含剩余元素 |
| 加速比超过线程数 | 计时误差 | 多次测量取平均值 |
---
## 任务五task66.c —— fork 与 pthread_create 开销对比(选做)
### 任务要求
分别测量 `fork()``pthread_create()` 的执行时间,对比进程创建与线程创建的开销差异。
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/time.h>
#define N 10000
int main() {
struct timeval start, end;
// 测量 fork
gettimeofday(&start, NULL);
for (int i = 0; i < N; i++) {
pid_t pid = fork();
if (pid == 0) _exit(0);
wait(NULL);
}
gettimeofday(&end, NULL);
printf("fork x%d: %ld us (avg %.1f us)\n", N,
end.tv_sec * 1000000L + end.tv_usec
- start.tv_sec * 1000000L - start.tv_usec,
(double)(end.tv_sec * 1000000L + end.tv_usec
- start.tv_sec * 1000000L - start.tv_usec) / N);
// 测量 pthread_create
pthread_t tid;
void *dummy(void *a) { return NULL; }
gettimeofday(&start, NULL);
for (int i = 0; i < N; i++) {
pthread_create(&tid, NULL, dummy, NULL);
pthread_join(tid, NULL);
}
gettimeofday(&end, NULL);
printf("pthread_create x%d: %ld us (avg %.1f us)\n", N,
end.tv_sec * 1000000L + end.tv_usec
- start.tv_sec * 1000000L - start.tv_usec,
(double)(end.tv_sec * 1000000L + end.tv_usec
- start.tv_sec * 1000000L - start.tv_usec) / N);
return 0;
}
```
---
## 任务六task67.c —— 动态线程池(选做)
### 任务要求
实现一个动态线程池 `sbuf_t`,当缓冲区满时容量翻倍,当缓冲区空且线程数过多时容量减半。
### 关键代码提示
```c
typedef struct {
int *buf; // 缓冲区
int n; // 容量
int front; // 队头
int rear; // 队尾
sem_t mutex; // 互斥
sem_t slots; // 空闲槽位
sem_t items; // 已有物品
} sbuf_t;
void sbuf_init(sbuf_t *sp, int n) {
sp->buf = malloc(n * sizeof(int));
sp->n = n;
sp->front = sp->rear = 0;
sem_init(&sp->mutex, 0, 1);
sem_init(&sp->slots, 0, n);
sem_init(&sp->items, 0, 0);
}
// 动态扩容:满时翻倍
void sbuf_insert(sbuf_t *sp, int item) {
sem_wait(&sp->slots);
sem_wait(&sp->mutex);
sp->buf[sp->rear] = item;
sp->rear = (sp->rear + 1) % sp->n;
// 检查是否需要扩容(简化判断)
// 实际实现需要更精细的逻辑
sem_post(&sp->mutex);
sem_post(&sp->items);
}
int sbuf_remove(sbuf_t *sp) {
sem_wait(&sp->items);
sem_wait(&sp->mutex);
int item = sp->buf[sp->front];
sp->front = (sp->front + 1) % sp->n;
sem_post(&sp->mutex);
sem_post(&sp->slots);
return item;
}
```
---
## 实验总结
通过本实验,应掌握以下能力:
1. 使用 `pthread_create`/`pthread_join` 创建和管理线程
2. 使用 POSIX 信号量实现线程同步
3. 理解竞态条件的成因,学会用信号量/互斥锁保护临界区
4. 实现经典的生产者-消费者同步模型
5. 理解并行计算中的加速比及其限制因素
6. 了解 `fork``pthread_create` 的开销差异

View File

@@ -0,0 +1,437 @@
# 实验04 Linux 进程间通信
## 实验目的
1. 掌握管道pipe的使用方法
2. 掌握 System V 消息队列message queue的创建和使用
3. 掌握共享内存shared memory配合信号量的同步通信
4. 了解命名管道FIFO在多进程通信中的应用
5. 理解不同 IPC 机制的适用场景和性能特点
## 涉及知识点
- 匿名管道:`pipe`、父子进程间通信
- System V 消息队列:`msgget``msgsnd``msgrcv``msgctl`
- System V 共享内存:`shmget``shmat``shmdt``shmctl`
- System V 信号量:`semget``semop``semctl`
- 命名管道FIFO`mkfifo`
- IPC 键值:`ftok`
---
## 任务一task71.c —— 父子进程管道通信
### 任务要求
父进程创建 2 个子进程,通过管道实现父子进程间的通信:
1. 子进程 1 向管道写入消息
2. 子进程 2 从管道读取消息并显示
3. 父进程等待所有子进程结束
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main() {
int pipefd[2]; // pipefd[0]=读端, pipefd[1]=写端
pid_t pid1, pid2;
if (pipe(pipefd) < 0) {
perror("pipe");
exit(1);
}
// 子进程 1写端
pid1 = fork();
if (pid1 == 0) {
close(pipefd[0]); // 关闭读端
char *msg = "Hello from child 1!";
write(pipefd[1], msg, strlen(msg) + 1);
printf("子进程1(PID=%d): 发送消息 -> %s\n", getpid(), msg);
close(pipefd[1]);
exit(0);
}
// 子进程 2读端
pid2 = fork();
if (pid2 == 0) {
close(pipefd[1]); // 关闭写端
char buf[256];
int n = read(pipefd[0], buf, sizeof(buf));
printf("子进程2(PID=%d): 收到消息 -> %s (共%d字节)\n",
getpid(), buf, n);
close(pipefd[0]);
exit(0);
}
// 父进程:关闭两端,等待子进程
close(pipefd[0]);
close(pipefd[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);
printf("父进程: 所有子进程已结束\n");
return 0;
}
```
### 关键要点
- `pipe()` 创建一对文件描述符:`pipefd[0]` 读端、`pipefd[1]` 写端
- 不使用的端口必须关闭,否则可能导致读端永远阻塞
- 管道是半双工的,数据单向流动
- 管道缓冲区大小通常为 64KBLinux
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 读端永远阻塞 | 写端未关闭 | 父进程中关闭两端,子进程中关闭不用的端 |
| 数据不完整 | `read` 可能短读 | 循环读取直到满足期望字节数 |
| 管道破裂 SIGPIPE | 读端已关闭但写端仍在写 | 检查 `write` 返回值,捕获 `SIGPIPE` |
---
## 任务二task72s.c / task72c.c —— 消息队列客户端/服务器
### 任务要求
- 服务器task72s.c创建消息队列等待接收客户端消息处理后回复
- 客户端task72c.c向消息队列发送请求消息等待服务器回复
### 关键代码提示
**公共头文件定义:**
```c
// msg_def.h
#define MSG_KEY 1234
#define MSG_TYPE_REQUEST 1
#define MSG_TYPE_REPLY 2
typedef struct {
long mtype; // 消息类型(必须 > 0
char mtext[256]; // 消息内容
} MsgBuf;
```
**服务器 task72s.c**
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include "msg_def.h"
int main() {
// 创建消息队列
int msqid = msgget(MSG_KEY, IPC_CREAT | 0666);
if (msqid < 0) { perror("msgget"); exit(1); }
printf("服务器: 消息队列已创建 (ID=%d)\n", msqid);
MsgBuf msg;
while (1) {
// 掭收类型为 MSG_TYPE_REQUEST 的消息
if (msgrcv(msqid, &msg, sizeof(msg.mtext),
MSG_TYPE_REQUEST, 0) < 0) {
perror("msgrcv");
break;
}
printf("服务器: 收到请求 -> %s\n", msg.mtext);
// 处理请求(简单回显)
msg.mtype = MSG_TYPE_REPLY;
snprintf(msg.mtext, sizeof(msg.mtext),
"服务器回复: 已收到你的消息");
msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0);
}
// 删除消息队列
msgctl(msqid, IPC_RMID, NULL);
return 0;
}
```
**客户端 task72c.c**
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include "msg_def.h"
int main() {
int msqid = msgget(MSG_KEY, 0666);
if (msqid < 0) { perror("msgget"); exit(1); }
MsgBuf msg;
msg.mtype = MSG_TYPE_REQUEST;
printf("请输入消息: ");
fgets(msg.mtext, sizeof(msg.mtext), stdin);
// 发送请求
msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0);
// 接收回复
msgrcv(msqid, &msg, sizeof(msg.mtext), MSG_TYPE_REPLY, 0);
printf("客户端: %s\n", msg.mtext);
return 0;
}
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| `msgget` 失败 | 消息队列不存在或权限不足 | 确保服务器先启动,检查权限 |
| `msgrcv` 阻塞 | 没有对应类型的消息 | 检查 `mtype` 是否匹配 |
| 消息队列残留 | 程序异常退出未删除 | 用 `ipcrm -q <msqid>` 手动删除 |
---
## 任务三task73.c —— 共享内存 + IPC 信号量同步
### 任务要求
使用共享内存实现两个进程间的数据传输,配合 System V 信号量实现同步。发送方写入 1~10接收方依次读取并显示。
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/wait.h>
#define SHM_KEY 5678
#define SEM_KEY 9012
// 共享内存结构
typedef struct {
int data;
int done; // 发送方是否完成
} SharedData;
// semop 辅助函数
void sem_op(int semid, int semnum, int op) {
struct sembuf sb = {semnum, op, 0};
semop(semid, &sb, 1);
}
int main() {
// 创建共享内存
int shmid = shmget(SHM_KEY, sizeof(SharedData),
IPC_CREAT | 0666);
SharedData *shm = (SharedData *)shmat(shmid, NULL, 0);
// 创建信号量0=可用槽位, 1=已有数据
int semid = semget(SEM_KEY, 2, IPC_CREAT | 0666);
semctl(semid, 0, SETVAL, 1); // sem[0]=1: 可写
semctl(semid, 1, SETVAL, 0); // sem[1]=0: 无数据
pid_t pid = fork();
if (pid == 0) {
// 接收方
for (int i = 0; i < 10; i++) {
sem_op(semid, 1, -1); // 等待有数据
printf("接收: %d\n", shm->data);
sem_op(semid, 0, +1); // 释放可写
}
shmdt(shm);
exit(0);
} else {
// 发送方
for (int i = 1; i <= 10; i++) {
sem_op(semid, 0, -1); // 等待可写
shm->data = i;
printf("发送: %d\n", i);
sem_op(semid, 1, +1); // 通知有数据
}
wait(NULL);
// 清理
shmdt(shm);
shmctl(shmid, IPC_RMID, NULL);
semctl(semid, 0, IPC_RMID);
}
return 0;
}
```
### IPC 资源管理
| 操作 | 命令 |
|------|------|
| 查看共享内存 | `ipcs -m` |
| 查看信号量 | `ipcs -s` |
| 查看消息队列 | `ipcs -q` |
| 删除共享内存 | `ipcrm -m <shmid>` |
| 删除信号量 | `ipcrm -s <semid>` |
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 共享内存未清理 | 程序异常退出 | 用 `ipcs` 查看,`ipcrm` 手动删除 |
| 数据竞争 | 未使用信号量同步 | 确保发送方写完后才通知接收方读取 |
| `shmat` 返回 -1 | 权限不足或 key 冲突 | 检查权限,换用不同 key |
---
## 任务四task74s.c / task74c.c —— 多进程并发服务器(选做)
### 任务要求
使用命名管道FIFO实现一个多进程并发服务器
- 服务器创建一个公共 FIFO 接收客户端请求
- 每个客户端创建自己的私有 FIFO 用于接收回复
- 服务器为每个请求 fork 一个子进程处理
### 关键代码提示
**服务器 task74s.c**
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <signal.h>
#define SERVER_FIFO "/tmp/server_fifo"
#define CLIENT_FIFO_TEMPLATE "/tmp/client_%d_fifo"
typedef struct {
pid_t client_pid;
char message[256];
} Request;
void sigchld_handler(int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main() {
// 创建服务器 FIFO
unlink(SERVER_FIFO);
mkfifo(SERVER_FIFO, 0666);
signal(SIGCHLD, sigchld_handler);
printf("服务器启动,等待连接...\n");
int server_fd = open(SERVER_FIFO, O_RDONLY);
Request req;
while (1) {
int n = read(server_fd, &req, sizeof(Request));
if (n <= 0) continue;
pid_t pid = fork();
if (pid == 0) {
// 子进程处理请求
char client_fifo[256];
snprintf(client_fifo, sizeof(client_fifo),
CLIENT_FIFO_TEMPLATE, req.client_pid);
int client_fd = open(client_fifo, O_WRONLY);
char reply[256];
snprintf(reply, sizeof(reply),
"服务器已处理: %s", req.message);
write(client_fd, reply, strlen(reply) + 1);
close(client_fd);
exit(0);
}
}
close(server_fd);
unlink(SERVER_FIFO);
return 0;
}
```
**客户端 task74c.c**
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#define SERVER_FIFO "/tmp/server_fifo"
#define CLIENT_FIFO_TEMPLATE "/tmp/client_%d_fifo"
typedef struct {
pid_t client_pid;
char message[256];
} Request;
int main() {
// 创建私有 FIFO
char client_fifo[256];
snprintf(client_fifo, sizeof(client_fifo),
CLIENT_FIFO_TEMPLATE, getpid());
unlink(client_fifo);
mkfifo(client_fifo, 0666);
// 发送请求
Request req;
req.client_pid = getpid();
printf("请输入消息: ");
fgets(req.message, sizeof(req.message), stdin);
int server_fd = open(SERVER_FIFO, O_WRONLY);
write(server_fd, &req, sizeof(Request));
close(server_fd);
// 接收回复
int client_fd = open(client_fifo, O_RDONLY);
char reply[256];
read(client_fd, reply, sizeof(reply));
printf("收到回复: %s\n", reply);
close(client_fd);
unlink(client_fifo);
return 0;
}
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| FIFO 文件残留 | 程序异常退出 | 启动时 `unlink` 清理旧 FIFO |
| `open` 阻塞 | FIFO 另一端未打开 | 确保服务器先启动 |
| 多客户端并发时数据混乱 | FIFO 是字节流,无消息边界 | 使用定长结构体或长度前缀协议 |
---
## 实验总结
通过本实验,应掌握以下能力:
1. 使用匿名管道实现父子进程间通信
2. 使用消息队列实现任意进程间的消息传递
3. 使用共享内存实现高速数据传输,配合信号量同步
4. 使用命名管道实现无亲缘关系进程间的通信
5. 理解不同 IPC 机制的优缺点和适用场景

View File

@@ -0,0 +1,423 @@
# 实验05 Linux 网络通信编程
## 实验目的
1. 掌握 Socket 编程的基本流程socket/bind/listen/accept/connect
2. 理解 TCP 客户端/服务器模型
3. 学会实现文件下载和远程 shell 等网络应用
4. 掌握 HTTP 协议的基本交互方式
5. 了解静态网页和动态网页的生成原理
## 涉及知识点
- Socket 地址结构:`sockaddr_in``inet_ntoa``htonl`/`htons`
- TCP 服务器流程:`socket` -> `bind` -> `listen` -> `accept` -> `read/write`
- TCP 客户端流程:`socket` -> `connect` -> `read/write`
- HTTP 请求/响应格式
- 文件传输与 `send`/`recv`
- 进程与网络 I/O 结合(远程 shell
- Wrapper 库辅助函数:`open_listen_sock``open_client_sock`
---
## 任务一toggle 服务器测试
### 任务要求
测试课程提供的 toggle 服务器和客户端程序,理解基本的 Socket 通信流程。
### 操作步骤
```bash
# 编译
gcc -o toggles toggle_server.c -L. -lwrapper
gcc -o togglec toggle_client.c -L. -lwrapper
# 终端 1启动服务器
./toggles 8080
# 终端 2启动客户端
./togglec localhost 8080
```
### TCP 服务器基本流程
```
socket() -- 创建套接字
|
bind() -- 绑定地址和端口
|
listen() -- 监听连接
|
while(1) {
accept() -- 接受客户端连接
|
read() -- 读取请求
|
write() -- 发送响应
|
close() -- 关闭连接
}
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| `bind` 失败 "Address already in use" | 端口被占用 | 等待几分钟或用 `setsockopt` 设置 `SO_REUSEADDR` |
| 客户端连接超时 | 服务器未启动或防火墙 | 检查服务器状态和端口是否开放 |
| 中文乱码 | 编码不一致 | 统一使用 UTF-8 |
---
## 任务二weblet 服务器
### 任务要求
测试课程提供的 weblet 服务器,理解 HTTP 请求处理:
1. 静态网页服务(返回 HTML 文件)
2. 动态网页生成CGI 方式)
### 操作步骤
```bash
# 编译
gcc -o weblet weblet.c -L. -lwrapper
# 启动 weblet 服务器
./weblet 8080
# 在浏览器访问
# http://localhost:8080/index.html
# http://localhost:8080/cgi-bin/hello
```
### HTTP 请求格式
```
GET /index.html HTTP/1.1
Host: localhost:8080
Connection: close
```
### HTTP 响应格式
```
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 123
<html>...</html>
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 404 Not Found | 文件路径错误 | 检查 `DocumentRoot` 和请求路径 |
| 中文乱码 | Content-Type 缺少 charset | 添加 `Content-Type: text/html; charset=utf-8` |
| 浏览器无法访问 | 防火墙或端口未开放 | 关闭防火墙或开放对应端口 |
---
## 任务三task83s.c / task83c.c —— 文件下载
### 任务要求
实现一个简单的文件下载服务:
- 客户端发送文件名请求
- 服务器查找文件并返回文件内容
- 客户端接收并保存到本地
### 关键代码提示
**服务器 task83s.c**
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 8192
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "用法: %s <端口>\n", argv[0]);
exit(1);
}
int port = atoi(argv[1]);
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(port);
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listen_fd, 5);
printf("文件下载服务器启动,端口 %d\n", port);
while (1) {
struct sockaddr_in cliaddr;
socklen_t clien = sizeof(cliaddr);
int conn_fd = accept(listen_fd,
(struct sockaddr *)&cliaddr, &clien);
printf("客户端 %s:%d 已连接\n",
inet_ntoa(cliaddr.sin_addr),
ntohs(cliaddr.sin_port));
// 读取文件名
char filename[MAXLINE];
int n = recv(conn_fd, filename, MAXLINE - 1, 0);
filename[n] = '\0';
// 打开并发送文件
int file_fd = open(filename, O_RDONLY);
if (file_fd < 0) {
send(conn_fd, "FILE_NOT_FOUND", 14, 0);
} else {
char buf[MAXLINE];
while ((n = read(file_fd, buf, MAXLINE)) > 0)
send(conn_fd, buf, n, 0);
close(file_fd);
}
close(conn_fd);
}
close(listen_fd);
return 0;
}
```
**客户端 task83c.c**
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#define MAXLINE 8192
int main(int argc, char **argv) {
if (argc != 4) {
fprintf(stderr, "用法: %s <主机> <端口> <文件名>\n", argv[0]);
exit(1);
}
// 解析服务器地址
struct hostent *hp = gethostbyname(argv[1]);
int port = atoi(argv[2]);
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
memcpy(&servaddr.sin_addr, hp->h_addr, hp->h_length);
servaddr.sin_port = htons(port);
connect(sock_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
// 发送文件名
send(sock_fd, argv[3], strlen(argv[3]), 0);
// 接收文件内容
char buf[MAXLINE];
int n;
char save_name[256];
snprintf(save_name, sizeof(save_name), "downloaded_%s", argv[3]);
int out_fd = open(save_name, O_WRONLY | O_CREAT | O_TRUNC, 0644);
while ((n = recv(sock_fd, buf, MAXLINE, 0)) > 0) {
write(out_fd, buf, n);
}
close(out_fd);
close(sock_fd);
printf("文件已下载为 %s\n", save_name);
return 0;
}
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 下载的文件不完整 | `send`/`recv` 短传 | 循环发送/接收,检查返回值 |
| 文件名含路径 | 安全隐患 | 生产环境应过滤 `..` 等路径遍历 |
| 大文件传输失败 | 缓冲区不够大 | 分块传输,每块用 `send` 发送 |
---
## 任务四task84s.c / task84c.c —— 远程 shell
### 任务要求
实现一个远程 shell 服务:
- 客户端发送 shell 命令
- 服务器执行命令并返回输出结果
- 客户端显示命令输出
### 关键代码提示
**服务器 task84s.c**
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 8192
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "用法: %s <端口>\n", argv[0]);
exit(1);
}
int port = atoi(argv[1]);
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(port);
int optval = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,
&optval, sizeof(optval));
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listen_fd, 5);
printf("远程 shell 服务器启动,端口 %d\n", port);
while (1) {
struct sockaddr_in cliaddr;
socklen_t clien = sizeof(cliaddr);
int conn_fd = accept(listen_fd,
(struct sockaddr *)&cliaddr, &clien);
printf("客户端 %s 已连接\n", inet_ntoa(cliaddr.sin_addr));
// 用 dup2 将命令输出重定向到 socket
if (fork() == 0) {
close(listen_fd);
dup2(conn_fd, STDOUT_FILENO);
dup2(conn_fd, STDERR_FILENO);
char cmd[MAXLINE];
int n;
while ((n = recv(conn_fd, cmd, MAXLINE - 1, 0)) > 0) {
cmd[n] = '\0';
// 去除换行符
if (cmd[n - 1] == '\n') cmd[n - 1] = '\0';
system(cmd);
}
exit(0);
}
close(conn_fd);
}
close(listen_fd);
return 0;
}
```
**客户端 task84c.c**
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#define MAXLINE 8192
int main(int argc, char **argv) {
if (argc != 3) {
fprintf(stderr, "用法: %s <主机> <端口>\n", argv[0]);
exit(1);
}
struct hostent *hp = gethostbyname(argv[1]);
int port = atoi(argv[2]);
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
memcpy(&servaddr.sin_addr, hp->h_addr, hp->h_length);
servaddr.sin_port = htons(port);
connect(sock_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
printf("已连接到 %s:%d\n", argv[1], port);
char cmd[MAXLINE];
while (1) {
printf("remote> ");
fflush(stdout);
if (fgets(cmd, sizeof(cmd), stdin) == NULL) break;
send(sock_fd, cmd, strlen(cmd), 0);
// 接收输出
char buf[MAXLINE];
int n;
// 简单方式:等待并读取(生产环境需要更复杂的协议)
usleep(100000); // 等待服务器执行
while ((n = recv(sock_fd, buf, MAXLINE - 1, MSG_DONTWAIT)) > 0) {
buf[n] = '\0';
printf("%s", buf);
}
}
close(sock_fd);
return 0;
}
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 命令输出不完整 | `recv` 时机不确定 | 使用长度前缀协议或特殊结束标记 |
| 服务器僵尸进程 | 未处理 `SIGCHLD` | 注册 `SIGCHLD` handler 用 `waitpid` 回收 |
| 安全风险 | `system()` 执行任意命令 | 仅用于实验,生产环境需严格限制命令 |
---
## 实验总结
通过本实验,应掌握以下能力:
1. 使用 Socket API 实现 TCP 客户端/服务器
2. 理解 HTTP 协议的基本请求/响应格式
3. 实现文件下载服务,理解数据传输流程
4. 实现远程 shell理解 I/O 重定向与网络结合
5. 掌握 `dup2` 在网络编程中的应用

View File

@@ -0,0 +1,526 @@
# 实验06 并发网络应用编程
## 实验目的
1. 理解迭代式服务器与并发式服务器的区别
2. 掌握多进程并发服务器的实现
3. 掌握多线程并发服务器的实现
4. 学会使用预线程化prethreading技术提高服务器性能
5. 了解 Web 代理服务器的工作原理
6. 掌握 I/O 多路复用select的基本使用
## 涉及知识点
- 迭代式 vs 并发式服务器模型
- `fork` 实现多进程并发
- `pthread_create` 实现多线程并发
- 预线程化线程池(生产者-消费者模型)
- `select` I/O 多路复用
- 临界区保护与线程安全
- 代理服务器的工作原理
---
## 任务一:测试 togglesp / togglest / togglest_pre
### 任务要求
分别测试三种并发服务器模型:
| 模型 | 文件 | 说明 |
|------|------|------|
| 多进程 | `togglesp.c` | 每个连接 fork 一个子进程 |
| 多线程 | `togglest.c` | 每个连接创建一个线程 |
| 预线程化 | `togglest_pre.c` | 固定线程池 + 任务队列 |
### 操作步骤
```bash
# 分别编译三种服务器
gcc -o togglesp togglesp.c -L. -lwrapper
gcc -o togglest togglest.c -L. -lwrapper -lpthread
gcc -o togglest_pre togglest_pre.c -L. -lwrapper -lpthread
# 测试多进程服务器
./togglesp 8080 &
./togglec localhost 8080
# 测试多线程服务器
./togglest 8081 &
./togglec localhost 8081
# 测试预线程化服务器
./togglest_pre 8082 &
./togglec localhost 8082
```
### 多线程服务器核心代码
```c
#include "wrapper.h"
void toggle(int conn_fd);
void *serve_client(void *vargp);
int main(int argc, char **argv) {
int listen_fd, conn_fd, *conn_fd_p;
struct sockaddr_in clientaddr;
socklen_t clientlen = sizeof(clientaddr);
pthread_t tid;
listen_fd = open_listen_sock(atoi(argv[1]));
while (1) {
conn_fd_p = malloc(sizeof(int));
*conn_fd_p = Accept(listen_fd,
(SA *)&clientaddr, &clientlen);
Pthread_create(&tid, NULL, serve_client, conn_fd_p);
}
}
void *serve_client(void *vargp) {
int conn_fd = *((int *)vargp);
Pthread_detach(pthread_self());
Free(vargp);
toggle(conn_fd);
Close(conn_fd);
return NULL;
}
```
### 预线程化服务器核心代码
```c
#include "wrapper.h"
#define NTHREADS 4
#define SBUFSIZE 16
// Sbuf 结构(生产者-消费者缓冲区)
typedef struct {
int *buf;
int n;
int front;
int rear;
sem_t mutex;
sem_t slots;
sem_t items;
} sbuf_t;
sbuf_t sbuf;
void sbuf_init(sbuf_t *sp, int n) {
sp->buf = Calloc(n, sizeof(int));
sp->n = n;
sp->front = sp->rear = 0;
Sem_init(&sp->mutex, 0, 1);
Sem_init(&sp->slots, 0, n);
Sem_init(&sp->items, 0, 0);
}
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;
}
void *thread(void *vargp) {
Pthread_detach(pthread_self());
while (1) {
int conn_fd = sbuf_remove(&sbuf);
toggle(conn_fd);
Close(conn_fd);
}
}
int main(int argc, char **argv) {
int listen_fd = open_listen_sock(atoi(argv[1]));
sbuf_init(&sbuf, SBUFSIZE);
for (int i = 0; i < NTHREADS; i++)
Pthread_create(NULL, NULL, thread, NULL);
while (1) {
struct sockaddr_in clientaddr;
socklen_t clientlen = sizeof(clientaddr);
int conn_fd = Accept(listen_fd,
(SA *)&clientaddr, &clientlen);
sbuf_insert(&sbuf, conn_fd);
}
}
```
### 三种模型对比
| 特性 | 多进程 | 多线程 | 预线程化 |
|------|--------|--------|----------|
| 并发方式 | fork 子进程 | pthread_create | 固定线程池 |
| 进程/线程数 | 动态增长 | 动态增长 | 固定 |
| 创建开销 | 高 | 中 | 无(已预创建) |
| 资源消耗 | 高(独立地址空间) | 低(共享地址空间) | 低 |
| 编程复杂度 | 简单 | 中等 | 较高 |
| 适用场景 | 连接数少 | 通用 | 高并发 |
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 多进程服务器僵尸进程 | 子进程未回收 | 注册 `SIGCHLD` handler |
| 多线程服务器段错误 | 线程间共享变量竞争 | 使用互斥锁保护共享数据 |
| 预线程化服务器阻塞 | 缓冲区满 | 增大 SBUFSIZE 或增加线程数 |
---
## 任务二task92.c —— 多进程 weblet
### 任务要求
将 weblet 服务器改造为多进程并发模型:每个客户端连接 fork 一个子进程处理。
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void sigchld_handler(int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0);
}
void handle_request(int conn_fd) {
// 读取 HTTP 请求
char buf[8192];
int n = recv(conn_fd, buf, sizeof(buf) - 1, 0);
if (n <= 0) { close(conn_fd); return; }
buf[n] = '\0';
// 解析请求行GET /path HTTP/1.1
char method[16], path[256], version[16];
sscanf(buf, "%s %s %s", method, path, version);
// 处理静态文件请求
// ... 打开文件,发送 HTTP 响应头和文件内容 ...
close(conn_fd);
}
int main(int argc, char **argv) {
if (argc != 2) { exit(1); }
signal(SIGCHLD, sigchld_handler);
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
int optval = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,
&optval, sizeof(optval));
struct sockaddr_in servaddr = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_ANY),
.sin_port = htons(atoi(argv[1]))
};
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listen_fd, 1024);
printf("多进程 weblet 启动,端口 %s\n", argv[1]);
while (1) {
struct sockaddr_in cliaddr;
socklen_t clien = sizeof(cliaddr);
int conn_fd = accept(listen_fd,
(struct sockaddr *)&cliaddr, &clien);
if (fork() == 0) {
close(listen_fd);
handle_request(conn_fd);
exit(0);
}
close(conn_fd);
}
return 0;
}
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 服务器响应慢 | fork 开销大 | 改用多线程或预线程化 |
| 文件描述符泄漏 | 子进程继承了 listen_fd | 子进程中 `close(listen_fd)` |
---
## 任务三task93.c —— 多线程 weblet
### 任务要求
将 weblet 服务器改造为多线程并发模型:每个客户端连接创建一个线程处理。
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void *handle_request(void *arg) {
int conn_fd = *((int *)arg);
free(arg);
pthread_detach(pthread_self());
char buf[8192];
int n = recv(conn_fd, buf, sizeof(buf) - 1, 0);
if (n <= 0) { close(conn_fd); return NULL; }
buf[n] = '\0';
// 解析并处理 HTTP 请求
char method[16], path[256], version[16];
sscanf(buf, "%s %s %s", method, path, version);
// 发送响应...
// 注意:多个线程共享 listen_fd但 conn_fd 是各线程独有的
close(conn_fd);
return NULL;
}
int main(int argc, char **argv) {
if (argc != 2) { exit(1); }
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
int optval = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,
&optval, sizeof(optval));
struct sockaddr_in servaddr = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_ANY),
.sin_port = htons(atoi(argv[1]))
};
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listen_fd, 1024);
printf("多线程 weblet 启动,端口 %s\n", argv[1]);
while (1) {
struct sockaddr_in cliaddr;
socklen_t clien = sizeof(cliaddr);
int *conn_fd = malloc(sizeof(int));
*conn_fd = accept(listen_fd,
(struct sockaddr *)&cliaddr, &clien);
pthread_t tid;
pthread_create(&tid, NULL, handle_request, conn_fd);
}
return 0;
}
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 线程数爆炸 | 每个请求创建新线程 | 改用预线程化或限制最大线程数 |
| 线程不安全的函数 | `strtok``ctime` 等非线程安全 | 使用 `_r` 后缀的可重入版本 |
| 内存泄漏 | 未 `free(arg)` 或未 `pthread_detach` | 确保线程退出前释放资源 |
---
## 任务四task94.c —— 预线程化 weblet动态增减线程
### 任务要求
实现预线程化 weblet 服务器,使用固定线程池处理请求。支持根据负载动态增减线程数量。
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#define MIN_THREADS 2
#define MAX_THREADS 16
#define SBUFSIZE 32
typedef struct {
int *buf;
int n, front, rear;
sem_t mutex, slots, items;
} sbuf_t;
sbuf_t sbuf;
int current_threads = 0;
pthread_mutex_t count_mutex = PTHREAD_MUTEX_INITIALIZER;
void *worker(void *arg) {
pthread_detach(pthread_self());
pthread_mutex_lock(&count_mutex);
current_threads++;
pthread_mutex_unlock(&count_mutex);
while (1) {
int conn_fd = sbuf_remove(&sbuf);
// 处理 HTTP 请求
handle_http_request(conn_fd);
close(conn_fd);
// 动态缩减:如果缓冲区长时间为空且线程数过多
// 可在此处实现缩减逻辑
}
}
void *manager(void *arg) {
// 监控线程:根据负载动态增减工作线程
while (1) {
sleep(5);
int pending = ...; // 获取待处理请求数
pthread_mutex_lock(&count_mutex);
if (pending > current_threads && current_threads < MAX_THREADS) {
// 扩容
pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
} else if (pending == 0 && current_threads > MIN_THREADS) {
// 缩减(通过向缓冲区插入特殊值 -1 实现)
sbuf_insert(&sbuf, -1);
}
pthread_mutex_unlock(&count_mutex);
}
}
int main(int argc, char **argv) {
sbuf_init(&sbuf, SBUFSIZE);
// 创建初始线程池
for (int i = 0; i < MIN_THREADS; i++) {
pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
}
// 创建管理线程
pthread_t mgr_tid;
pthread_create(&mgr_tid, NULL, manager, NULL);
// 主线程接受连接
int listen_fd = open_listen_sock(atoi(argv[1]));
while (1) {
struct sockaddr_in cliaddr;
socklen_t clien = sizeof(cliaddr);
int conn_fd = accept(listen_fd,
(struct sockaddr *)&cliaddr, &clien);
sbuf_insert(&sbuf, conn_fd);
}
return 0;
}
```
### 动态增减策略
| 条件 | 操作 |
|------|------|
| 待处理请求数 > 当前线程数 且 < 最大线程数 | 创建新线程 |
| 待处理请求数 = 0 且 当前线程数 > 最小线程数 | 终止一个线程 |
| 线程数已达上限 | 等待(请求在缓冲区排队) |
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 线程缩减无效 | 线程阻塞在 `sbuf_remove` | 发送特殊值唤醒线程 |
| 线程数波动过大 | 扩缩策略过于敏感 | 设置冷却时间和阈值 |
| 队列溢出 | SBUFSIZE 太小 | 增大缓冲区或动态扩容 |
---
## 任务五task95.c —— Web 代理服务器(选做)
### 任务要求
实现一个 Web 代理服务器:
1. 客户端连接代理,发送 HTTP 请求
2. 代理解析请求中的目标 URL
3. 代理向目标服务器发起请求
4. 将目标服务器的响应转发给客户端
### 关键代码提示
```c
void handle_proxy(int client_fd) {
char buf[8192];
int n = recv(client_fd, buf, sizeof(buf) - 1, 0);
if (n <= 0) { close(client_fd); return; }
buf[n] = '\0';
// 解析 HTTP 请求中的 URL
char method[16], url[512], version[16];
sscanf(buf, "%s %s %s", method, url, version);
// 解析主机名和端口
char host[256];
int port = 80;
// url 格式: http://host:port/path
// 解析 host 和 port ...
// 连接目标服务器
int server_fd = open_client_sock(host, port);
// 转发请求
send(server_fd, buf, n, 0);
// 转发响应
while ((n = recv(server_fd, buf, sizeof(buf), 0)) > 0)
send(client_fd, buf, n, 0);
close(server_fd);
close(client_fd);
}
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| HTTPS 站点无法代理 | 代理不支持 CONNECT 方法 | 仅支持 HTTP |
| URL 解析错误 | 格式多样 | 仔细处理 `http://`、端口号、路径等 |
| 性能差 | 每次请求都新建连接 | 可实现连接池缓存 |
---
## 实验总结
通过本实验,应掌握以下能力:
1. 区分迭代式和并发式服务器模型
2. 使用 `fork` 实现多进程并发服务器
3. 使用 `pthread` 实现多线程并发服务器
4. 使用预线程化技术构建高性能服务器
5. 理解线程池的工作原理和动态管理策略
6. 了解 Web 代理服务器的实现方法