C语言进程间通信完全指南

系统编程IPC

进程间通信概述

在多进程操作系统中,进程拥有各自独立的地址空间,一个进程无法直接访问另一个进程的内存。进程间通信(IPC,Inter-Process Communication)就是解决不同进程之间数据交换与协调的机制。Linux/UNIX 系统提供了丰富的 IPC 手段,主要包括管道、消息队列、共享内存、信号量和 Socket。

IPC 机制的选择依据

选择哪种 IPC 机制,通常需要考虑以下几个维度:

  • 数据量:小数据量可用管道或消息队列,大数据量适合共享内存
  • 通信方向:单向用半双工管道,双向可用全双工管道或 Socket
  • 同步需求:是否需要阻塞等待、是否需要互斥访问
  • 亲缘关系:有亲缘关系的进程可使用匿名管道,无亲缘关系需使用命名管道或其他机制

管道通信

管道是 UNIX 系统中古老的 IPC 机制,分为匿名管道(unnamed pipe)和命名管道(FIFO)两种形式。匿名管道只能用于有亲缘关系的进程之间,而命名管道允许任意进程之间通信。

匿名管道 pipe()

匿名管道通过 pipe() 系统调用创建,返回两个文件描述符:一个用于读,一个用于写。管道是半双工的,数据只能单向流动。

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>

int main(void) {
    int fd[2];
    pid_t pid;
    char buf[128];

    if (pipe(fd) < 0) {
        perror("pipe");
        return 1;
    }

    pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {          /* 子进程:写端 */
        close(fd[0]);           /* 关闭读端 */
        const char *msg = "Hello from child!";
        write(fd[1], msg, strlen(msg) + 1);
        close(fd[1]);
    } else {                   /* 父进程:读端 */
        close(fd[1]);           /* 关闭写端 */
        read(fd[0], buf, sizeof(buf));
        printf("Parent received: %s\n", buf);
        close(fd[0]);
        waitpid(pid, NULL, 0);
    }
    return 0;
}

命名管道 mkfifo()

命名管道以文件形式存在于文件系统中,不相关的进程可以通过路径名打开它进行通信。创建命名管道使用 mkfifo() 函数。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

#define FIFO_PATH "/tmp/my_fifo"

int main(void) {
    char buf[256];

    /* 创建命名管道 */
    if (mkfifo(FIFO_PATH, 0666) < 0) {
        perror("mkfifo");
    }

    /* 以只读方式打开(阻塞直到有写端打开) */
    int fd = open(FIFO_PATH, O_RDONLY);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    read(fd, buf, sizeof(buf));
    printf("Received: %s\n", buf);
    close(fd);
    unlink(FIFO_PATH);
    return 0;
}

消息队列

消息队列是内核维护的消息链表,每个消息都有特定的类型。进程可以按类型选择性读取消息,这比管道的纯字节流模式更加灵活。System V 消息队列和 POSIX 消息队列是两种常见实现。

System V 消息队列

System V 消息队列通过 msgget()、msgsnd()、msgrcv() 和 msgctl() 四个系统调用操作。

#include <sys/msg.h>
#include <stdio.h>
#include <string.h>

struct msgbuf {
    long mtype;         /* 消息类型,必须 > 0 */
    char mtext[128];    /* 消息正文 */
};

int main(void) {
    key_t key = ftok("/tmp", 'A');
    int msgid = msgget(key, IPC_CREAT | 0666);
    if (msgid < 0) {
        perror("msgget");
        return 1;
    }

    /* 发送消息 */
    struct msgbuf snd = { .mtype = 1 };
    strncpy(snd.mtext, "Hello, Message Queue!", sizeof(snd.mtext));
    msgsnd(msgid, &snd, strlen(snd.mtext) + 1, 0);

    /* 接收消息 */
    struct msgbuf rcv;
    msgrcv(msgid, &rcv, sizeof(rcv.mtext), 1, 0);
    printf("Received: %s\n", rcv.mtext);

    /* 删除消息队列 */
    msgctl(msgid, IPC_RMID, NULL);
    return 0;
}

共享内存

共享内存是速度最快的 IPC 方式,因为它直接将同一块物理内存映射到多个进程的地址空间,省去了数据在内核态和用户态之间的拷贝开销。但共享内存本身不提供同步机制,需要配合信号量使用。

System V 共享内存

使用 shmget() 创建共享内存段,shmat() 将其映射到进程地址空间,shmdt() 断开映射,shmctl() 进行控制操作。

#include <sys/shm.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#define SHM_SIZE 1024

int main(void) {
    key_t key = ftok("/tmp", 'B');
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
    if (shmid < 0) {
        perror("shmget");
        return 1;
    }

    pid_t pid = fork();
    if (pid == 0) {
        char *addr = (char *)shmat(shmid, NULL, 0);
        strncpy(addr, "Hello from shared memory!", SHM_SIZE);
        shmdt(addr);
    } else {
        waitpid(pid, NULL, 0);
        char *addr = (char *)shmat(shmid, NULL, SHM_RDONLY);
        printf("Parent read: %s\n", addr);
        shmdt(addr);
        shmctl(shmid, IPC_RMID, NULL);
    }
    return 0;
}

POSIX 共享内存

POSIX 共享内存使用 shm_open() 创建对象,再通过 mmap() 映射到进程地址空间。相比 System V,POSIX 接口更简洁,且使用文件描述符操作。

#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#define SHM_NAME "/my_posix_shm"
#define SHM_SIZE 4096

int main(void) {
    int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    if (fd < 0) { perror("shm_open"); return 1; }
    ftruncate(fd, SHM_SIZE);

    void *addr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE,
                        MAP_SHARED, fd, 0);
    close(fd);

    strncpy((char *)addr, "POSIX shared memory works!", SHM_SIZE);
    printf("Wrote: %s\n", (char *)addr);

    munmap(addr, SHM_SIZE);
    shm_unlink(SHM_NAME);
    return 0;
}

信号量

信号量是一种用于控制多个进程对共享资源访问的计数器,主要用于同步而非数据传输。当进程需要访问共享资源时,先执行 P 操作(减1,也称 wait),使用完毕后执行 V 操作(加1,也称 signal)。信号量常与共享内存配合使用。

System V 信号量

System V 信号量以信号量集为单位管理,通过 semget()、semop()、semctl() 操作。

#include <sys/sem.h>
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

/* 联合体,用于 semctl 的 SETVAL 操作 */
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
};

int main(void) {
    key_t key = ftok("/tmp", 'C');
    int semid = semget(key, 1, IPC_CREAT | 0666);
    if (semid < 0) { perror("semget"); return 1; }

    /* 初始化信号量值为 1(二值信号量 / 互斥锁) */
    union semun arg = { .val = 1 };
    semctl(semid, 0, SETVAL, arg);

    struct sembuf p_op = { .sem_num = 0, .sem_op = -1, .sem_flg = 0 };
    struct sembuf v_op = { .sem_num = 0, .sem_op = +1, .sem_flg = 0 };

    /* P 操作 — 获取资源 */
    semop(semid, &p_op, 1);
    printf("Critical section...\n");
    /* V 操作 — 释放资源 */
    semop(semid, &v_op, 1);

    semctl(semid, 0, IPC_RMID);
    return 0;
}

Socket 通信

Socket 是一种通用的 IPC 机制,既可以用于同一主机上的进程间通信(UNIX 域 Socket),也可以用于跨网络的进程间通信(网络 Socket)。相比其他 IPC 机制,Socket 的通用性更强。

UNIX 域 Socket

UNIX 域 Socket 使用文件系统路径作为地址,通信双方在同一台主机上,数据不需要经过网络协议栈,效率高于网络 Socket。

#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#define SOCK_PATH "/tmp/unix_sock"

int main(void) {
    int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sfd < 0) { perror("socket"); return 1; }

    struct sockaddr_un addr;
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);

    unlink(SOCK_PATH);
    if (bind(sfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        perror("bind");
        return 1;
    }
    listen(sfd, 5);

    printf("Server listening on %s\n", SOCK_PATH);

    struct sockaddr_un cli;
    socklen_t cli_len = sizeof(cli);
    int cfd = accept(sfd, (struct sockaddr *)&cli, &cli_len);

    char buf[256];
    ssize_t n = read(cfd, buf, sizeof(buf) - 1);
    buf[n] = '\0';
    printf("Received: %s\n", buf);

    close(cfd);
    close(sfd);
    unlink(SOCK_PATH);
    return 0;
}

总结

本文系统介绍了 C 语言中五种主要的进程间通信机制:

  • 管道:使用简单,适合亲缘进程间的字节流通信;命名管道可扩展到无亲缘关系进程
  • 消息队列:支持按类型选择性接收消息,适合结构化数据的异步传递
  • 共享内存:速度最快的 IPC 方式,适合大数据量传输,需配合信号量同步
  • 信号量:主要用于进程同步与互斥,常与共享内存搭配使用
  • Socket:通用性极强,支持本地和跨网络通信,是分布式系统的基石

在实际项目中,选择 IPC 机制时应综合考虑通信规模、同步需求、部署环境和可维护性。没有放之四海皆优的方案,理解每种机制的原理和适用场景,才能做出合理的技术选型。掌握这些 IPC 机制,是成为合格系统程序员的必经之路。