vault backup: 2026-06-13 23:46:22
This commit is contained in:
337
操作系统/实验/实验01_IO编程.md
Normal file
337
操作系统/实验/实验01_IO编程.md
Normal 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 解决实际问题
|
||||
437
操作系统/实验/实验02_进程控制.md
Normal file
437
操作系统/实验/实验02_进程控制.md
Normal 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. 使用信号机制进行进程间通信
|
||||
470
操作系统/实验/实验03_多线程编程.md
Normal file
470
操作系统/实验/实验03_多线程编程.md
Normal 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` 的开销差异
|
||||
437
操作系统/实验/实验04_进程间通信.md
Normal file
437
操作系统/实验/实验04_进程间通信.md
Normal 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]` 写端
|
||||
- 不使用的端口必须关闭,否则可能导致读端永远阻塞
|
||||
- 管道是半双工的,数据单向流动
|
||||
- 管道缓冲区大小通常为 64KB(Linux)
|
||||
|
||||
### 常见问题
|
||||
|
||||
| 问题 | 原因 | 解决方法 |
|
||||
|------|------|----------|
|
||||
| 读端永远阻塞 | 写端未关闭 | 父进程中关闭两端,子进程中关闭不用的端 |
|
||||
| 数据不完整 | `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 机制的优缺点和适用场景
|
||||
423
操作系统/实验/实验05_网络通信.md
Normal file
423
操作系统/实验/实验05_网络通信.md
Normal 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` 在网络编程中的应用
|
||||
526
操作系统/实验/实验06_并发服务器.md
Normal file
526
操作系统/实验/实验06_并发服务器.md
Normal 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 代理服务器的实现方法
|
||||
Reference in New Issue
Block a user