Files
2026-06-25 00:09:09 +08:00

6.5 KiB
Raw Permalink Blame History

实验八 进程通信——共享内存

一、实验内容

理解共享内存通信,掌握 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 线程的修改点

  1. fork() → pthread_create()
  2. 共享内存和信号量仍可用
  3. 线程间无需 shmdt/shmat直接使用指针

5.4 实验体会

  1. 共享内存优势:最快的进程间通信方式,数据无需复制

  2. 同步重要性:共享内存访问需要同步,否则会出现竞争条件

  3. 信号量作用

    • 生产者子进程sem_op = +1V操作
    • 消费者父进程sem_op = -1P操作
  4. 数据传输流程stdin → 共享内存 → 文件,比管道更高效

  5. IPC 机制对比

    • 管道:简单,但需复制数据
    • 消息队列:基于链表,但有容量限制
    • 共享内存:最快,但需同步
    • 信号量:用于同步,不适合数据传输