返回

一文读懂 Linux 上的进程间通信(IPC)方式

后端

Linux IPC 方式详解:跨越进程间通信的鸿沟

在现代计算环境中,应用程序之间的通信至关重要,使它们能够共享数据、同步动作并有效协作。在 Linux 操作系统中,进程间通信 (IPC) 提供了多种机制,允许不同进程在系统范围内进行交互。本文将深入探讨 Linux 中最常见的 IPC 方式,提供示例代码并阐述它们的优点和缺点。

1. 管道:单向数据传输

管道是一种轻量级的 IPC 方式,允许两个进程之间进行单向通信。它提供了一个匿名管道或一个有名管道。匿名管道仅在创建它的进程和它的子进程之间可见,而有名管道可以在任何进程之间使用。通过管道,一个进程可以写入数据,而另一个进程可以读取它,从而实现单向数据流。

优点:

  • 简单易用
  • 高效的单向通信
  • 适合父进程与子进程之间的通信

缺点:

  • 只支持单向通信
  • 匿名管道只能在相关进程之间使用

示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
  int pipefd[2];
  if (pipe(pipefd) == -1) {
    perror("pipe");
    exit(EXIT_FAILURE);
  }

  pid_t pid = fork();
  if (pid == -1) {
    perror("fork");
    exit(EXIT_FAILURE);
  }

  if (pid == 0) {  // 子进程
    close(pipefd[0]);  // 关闭读端
    write(pipefd[1], "Hello, world!", 13);  // 向写端写入数据
    close(pipefd[1]);  // 关闭写端
  } else {  // 父进程
    close(pipefd[1]);  // 关闭写端
    char buf[13];
    read(pipefd[0], buf, 13);  // 从读端读取数据
    close(pipefd[0]);  // 关闭读端
    printf("Received: %s\n", buf);
  }

  return 0;
}

2. 消息队列:消息传递

消息队列是一种更灵活的 IPC 方式,允许进程在队列中发送和接收消息。消息队列可以是本地队列或远程队列。本地队列限制于同一台计算机上的进程,而远程队列允许不同计算机上的进程进行通信。通过消息队列,进程可以发送和接收可变长度的数据块,从而实现双向异步通信。

优点:

  • 双向异步通信
  • 支持多个进程之间的通信
  • 可靠的消息传递,确保消息不会丢失

缺点:

  • 比管道开销更大
  • 队列大小有限制

示例代码:

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

int main() {
  key_t key = ftok(".", 'a');
  if (key == -1) {
    perror("ftok");
    exit(EXIT_FAILURE);
  }

  int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
  if (msgid == -1) {
    perror("msgget");
    exit(EXIT_FAILURE);
  }

  pid_t pid = fork();
  if (pid == -1) {
    perror("fork");
    exit(EXIT_FAILURE);
  }

  if (pid == 0) {  // 子进程
    struct msgbuf {
      long mtype;
      char mtext[100];
    } msg;
    msg.mtype = 1;
    strcpy(msg.mtext, "Hello, world!");
    if (msgsnd(msgid, &msg, sizeof(msg.mtext), 0) == -1) {
      perror("msgsnd");
      exit(EXIT_FAILURE);
    }
  } else {  // 父进程
    struct msgbuf {
      long mtype;
      char mtext[100];
    } msg;
    if (msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0) == -1) {
      perror("msgrcv");
      exit(EXIT_FAILURE);
    }
    printf("Received: %s\n", msg.mtext);
  }

  msgctl(msgid, IPC_RMID, NULL);

  return 0;
}

3. 共享内存:共享数据段

共享内存是一种高级 IPC 方式,允许多个进程访问同一块物理内存。它提供了一个匿名共享内存段或一个命名共享内存段。匿名共享内存段只能在创建它的进程和它的子进程之间共享,而命名共享内存段可以在任何进程之间共享。通过共享内存,进程可以高效地读取和写入共享数据,而无需复制数据。

优点:

  • 高效的数据共享
  • 允许多个进程同时访问数据
  • 适用于需要频繁数据交换的情况

缺点:

  • 同步访问共享内存的复杂性
  • 必须手动管理内存分配和释放

示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>

int main() {
  key_t key = ftok(".", 'a');
  if (key == -1) {
    perror("ftok");
    exit(EXIT_FAILURE);
  }

  int shmid = shmget(key, 1024, IPC_CREAT | IPC_EXCL | 0666);
  if (shmid == -1) {
    perror("shmget");
    exit(EXIT_FAILURE);
  }

  pid_t pid = fork();
  if (pid == -1) {
    perror("fork");
    exit(EXIT_FAILURE);
  }

  if (pid == 0) {  // 子进程
    char *shm = shmat(shmid, NULL, 0);
    if (shm == (char *)-1) {
      perror("shmat");
      exit(EXIT_FAILURE);
    }
    strcpy(shm, "Hello, world!");
    shmdt(shm);
  } else {  // 父进程
    char *shm = shmat(shmid, NULL, 0);
    if (shm == (char *)-1) {
      perror("shmat");
      exit(EXIT_FAILURE);
    }
    printf("Received: %s\n", shm);
    shmdt(shm);
  }

  shmctl(shmid, IPC_RMID, NULL);

  return 0;
}

4. 信号量:同步进程访问

信号量是一种 IPC 方式,用于同步对共享资源的访问。它提供了一个二值信号量或一个计数信号量。二值信号量只有两个状态:0 和 1,用于防止多个进程同时访问共享资源。计数信号量可以取任意非负整数的值,用于限制同时可以访问共享资源的进程数。通过信号量,进程可以协调对共享资源的访问,避免竞争条件。

优点:

  • 防止对共享资源的竞争条件
  • 适用于需要同步进程访问的情况
  • 灵活的信号量类型(二值和计数)

缺点:

  • 比其他 IPC 方式开销更大
  • 需要小心处理死锁情况

示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/sem.h>

int main() {
  key_t key = ftok(".", 'a');
  if (key == -1) {
    perror("ftok");
    exit(EXIT_FAILURE);
  }

  int semid = semget(key, 1, IPC_CREAT | IPC_EXCL | 0666);
  if (semid == -1) {
    perror("semget");
    exit(EXIT_FAILURE);
  }

  pid_t pid = fork();
  if (pid == -1) {
    perror("fork");
    exit(EXIT_FAILURE);
  }

  if (pid == 0) {  // 子进程
    struct sembuf semop;
    semop.sem_num = 0;
    semop.sem_op = -1;
    semop.sem_flg = 0;
    if (semop(semid, &semop, 1) == -1) {
      perror("semop");
      exit(EXIT_FAILURE);
    }
    printf("Child process entered critical section.\n");
    sleep(10);
    semop.sem_op = 1;
    if (semop(semid, &semop, 1) == -1) {
      perror("semop");