# 实验八 进程通信——共享内存 ## 一、实验内容 理解共享内存通信,掌握 shmget/shmat/shmdt/shmctl,信号量同步。 创建共享内存,fork 子进程写,父进程读入文件,信号量同步。 ## 二、实验设计 ### 2.1 通信架构 ``` +------------------+ | 共享内存 | | shm_addr | +------------------+ ↑ | +------+------+ | 子进程写 | 父进程读 +-------------+-------------+ | stdin | shmget | fwrite +-----------+ | abc.txt ``` ### 2.2 数据结构 ```cpp #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 共享内存创建 ```cpp 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 子进程写入 ```cpp 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 父进程读取 ```cpp 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 清理 ```cpp wait(NULL); shmdt(shm_addr); // 脱离共享内存 shmctl(shm_id, IPC_RMID, NULL); // 删除共享内存 ``` ### 3.5 编译与运行 ```bash g++ exp08_source.cpp -o shmcomm ./shmcomm # 输入示例: Hello World This is a test Q # 输出: abc.txt 包含输入内容 ``` ## 四、实验结果 ### 4.0 测试命令 ```bash # 编译 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:循环使用共享内存缓冲区** 可以设计环形缓冲区: ```cpp #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 线程的修改点** 1. fork() → pthread_create() 2. 共享内存和信号量仍可用 3. 线程间无需 shmdt/shmat,直接使用指针 ### 5.4 实验体会 1. **共享内存优势**:最快的进程间通信方式,数据无需复制 2. **同步重要性**:共享内存访问需要同步,否则会出现竞争条件 3. **信号量作用**: - 生产者(子进程):sem_op = +1(V操作) - 消费者(父进程):sem_op = -1(P操作) 4. **数据传输流程**:stdin → 共享内存 → 文件,比管道更高效 5. **IPC 机制对比**: - 管道:简单,但需复制数据 - 消息队列:基于链表,但有容量限制 - 共享内存:最快,但需同步 - 信号量:用于同步,不适合数据传输