6.5 KiB
6.5 KiB
实验八 进程通信——共享内存
一、实验内容
理解共享内存通信,掌握 shmget/shmat/shmdt/shmctl,信号量同步。
创建共享内存,fork 子进程写,父进程读入文件,信号量同步。
二、实验设计
2.1 通信架构
+------------------+
| 共享内存 |
| shm_addr |
+------------------+
↑
|
+------+------+
| 子进程写 | 父进程读
+-------------+-------------+
| stdin | shmget | fwrite
+-----------+ | abc.txt
2.2 数据结构
#define BUFFERSIZE 200 // 行缓冲区大小
#define SEMKEY 251 // 信号量key
#define SHMKEY 231 // 共享内存key
#define SHMSIZE 1024 // 共享内存大小
#define FILENAME "abc.txt" // 输出文件名
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
2.3 函数设计
| 函数 | 功能 |
|---|---|
shmget() |
创建共享内存 |
shmat() |
附加共享内存到进程地址空间 |
shmdt() |
脱离共享内存 |
shmctl() |
控制共享内存(删除) |
semget() |
创建信号量 |
semop() |
P/V操作 |
2.4 调用关系
main()
├── semget() 创建信号量,初值0
├── shmget() 创建共享内存
├── shmat() 附加到进程地址空间
├── fork() 创建子进程
│ └── 子进程: stdin → 共享内存
│ └── 每写一行,sem_op +1 通知父进程
└── 父进程: 共享内存 → 文件
└── 每读一行,sem_op -1 等待子进程
└── 读到结束符(-1)则退出
三、编码实现
3.1 共享内存创建
int sem_id = semget(SEMKEY, 1, IPC_CREAT | 0600);
union semun sem_val;
sem_val.val = 0;
semctl(sem_id, 0, SETVAL, sem_val); // 信号量初值为0
int shm_id = shmget(SHMKEY, SHMSIZE, IPC_CREAT | 0600);
char *shm_addr = (char *)shmat(shm_id, NULL, 0); // 附加到地址空间
memset(shm_addr, 0, SHMSIZE);
要点:
- 信号量初值为0,父进程初始阻塞
- 共享内存大小1024字节
3.2 子进程写入
if (fork() == 0) {
int isend = 0;
int line = 1;
printf("\n#line%d$ ", line);
while (!isend && fgets(buf, BUFFERSIZE, stdin) != NULL) {
line++;
printf("\n#line%d$ ", line);
if (buf[0] == 'Q' && strlen(buf) <= 2) {
isend = 1;
printf("\nExit...\n");
} else {
memcpy(cur, buf, strlen(buf)); // 复制到共享内存
cur += strlen(buf);
struct sembuf sem_op = {0, 1, 0}; // V操作
semop(sem_id, &sem_op, 1); // 通知父进程
}
}
*cur = -1; // 写入结束符
shmdt(shm_addr); // 脱离共享内存
exit(0);
}
要点:
- 从标准输入读取行
- 每行写入共享内存后 V 操作通知父进程
- 输入 Q 退出,写入 -1 作为结束标记
3.3 父进程读取
FILE *fp = fopen(FILENAME, "wb");
while (1) {
struct sembuf sem_op = {0, -1, 0}; // P操作
semop(sem_id, &sem_op, 1); // 等待子进程
if (*cur == -1) break; // 收到结束符
// 从共享内存复制一行到buf
int i;
for (i = 0; *cur != '\n'; cur++, i++)
buf[i] = *cur;
cur++;
buf[i] = '\n';
buf[++i] = 0;
fwrite(buf, strlen(buf), 1, fp); // 写入文件
}
fclose(fp);
要点:
- P 操作等待子进程生产数据
- 从共享内存解析行(以\n分隔)
- 写入文件
3.4 清理
wait(NULL);
shmdt(shm_addr); // 脱离共享内存
shmctl(shm_id, IPC_RMID, NULL); // 删除共享内存
3.5 编译与运行
g++ exp08_source.cpp -o shmcomm
./shmcomm
# 输入示例:
Hello World
This is a test
Q
# 输出: abc.txt 包含输入内容
四、实验结果
4.0 测试命令
# 编译
g++ exp08_source.cpp -o shmcomm
# 运行(输入内容,按 Q 退出)
./shmcomm
# 输入内容:
Hello World
This is a test
Q
# 查看输出文件
cat abc.txt
# 查看共享内存(在另一终端)
ipcs -m
# 清理共享内存(如果进程异常退出)
ipcrm -m $(ipcs -m | grep 231 | awk '{print $2}')
4.1 运行示例
$ ./shmcomm
#line1$ Hello World
#line2$ This is a test
#line3$ Q
Exit...
# abc.txt 内容:
Hello World
This is a test
4.2 同步机制说明
时间线:
子进程: 写"Hello World\n" → V(+1)
父进程: P(-1) → 读"Hello World\n" → 写文件
子进程: 写"This is a test\n" → V(+1)
父进程: P(-1) → 读"This is a test\n" → 写文件
子进程: 写结束符(-1)
父进程:读到(-1),退出
说明:
- 信号量实现子进程和父进程同步
- 子进程生产数据后 V 通知父进程
- 父进程消费后 P 等待下一个数据
4.3 abc.txt 文件内容
Hello World
This is a test
五、实验结果思考与体会
5.1 共享内存特点
| 特性 | 说明 |
|---|---|
| 高效 | 直接内存访问,无需复制 |
| 共享 | 多个进程可附加到同一共享内存 |
| 同步 | 需要额外机制(信号量)同步访问 |
| 生命周期 | 内核管理,不随进程结束消失 |
5.2 共享内存 vs 管道
| 特性 | 共享内存 | 管道 |
|---|---|---|
| 数据传输 | 直接读写内存 | 需经过内核缓冲区 |
| 速度 | 更快 | 较慢 |
| 同步 | 需额外机制 | 内置同步(读阻塞) |
| 关联 | 需亲缘或shmget | 需亲缘或fork |
5.3 思考问题解答
问题1:循环使用共享内存缓冲区
可以设计环形缓冲区:
#define BUF_SIZE 8
char buffer[BUF_SIZE];
int in = 0, out = 0;
// 生产者: buffer[in++ % BUF_SIZE] = data; sem_post()
// 消费者: data = buffer[out++ % BUF_SIZE]; sem_wait()
问题2:改用 pthread 线程的修改点
- fork() → pthread_create()
- 共享内存和信号量仍可用
- 线程间无需 shmdt/shmat,直接使用指针
5.4 实验体会
-
共享内存优势:最快的进程间通信方式,数据无需复制
-
同步重要性:共享内存访问需要同步,否则会出现竞争条件
-
信号量作用:
- 生产者(子进程):sem_op = +1(V操作)
- 消费者(父进程):sem_op = -1(P操作)
-
数据传输流程:stdin → 共享内存 → 文件,比管道更高效
-
IPC 机制对比:
- 管道:简单,但需复制数据
- 消息队列:基于链表,但有容量限制
- 共享内存:最快,但需同步
- 信号量:用于同步,不适合数据传输