Files
obsidian/操作系统/实验/实验04_进程间通信.md

11 KiB
Raw Blame History

实验04 Linux 进程间通信

实验目的

  1. 掌握管道pipe的使用方法
  2. 掌握 System V 消息队列message queue的创建和使用
  3. 掌握共享内存shared memory配合信号量的同步通信
  4. 了解命名管道FIFO在多进程通信中的应用
  5. 理解不同 IPC 机制的适用场景和性能特点

涉及知识点

  • 匿名管道:pipe、父子进程间通信
  • System V 消息队列:msggetmsgsndmsgrcvmsgctl
  • System V 共享内存:shmgetshmatshmdtshmctl
  • System V 信号量:semgetsemopsemctl
  • 命名管道FIFOmkfifo
  • IPC 键值:ftok

任务一task71.c —— 父子进程管道通信

任务要求

父进程创建 2 个子进程,通过管道实现父子进程间的通信:

  1. 子进程 1 向管道写入消息
  2. 子进程 2 从管道读取消息并显示
  3. 父进程等待所有子进程结束

关键代码提示

#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向消息队列发送请求消息等待服务器回复

关键代码提示

公共头文件定义:

// 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

#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

#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接收方依次读取并显示。

关键代码提示

#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

#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

#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 机制的优缺点和适用场景