Linux 使用的进程间通信方式

一、进程间通信的背景

在 Linux 操作系统中,进程是资源分配的基本单位,每个进程都有自己独立的内存空间。因此,进程之间无法直接访问彼此的内存数据。这种隔离机制保证了进程的独立性和系统的稳定性。然而,在许多应用场景下,不同进程需要共享数据或相互通信,这时就需要使用 IPC 机制。

二、Linux 中的进程间通信方式

2.1 管道(Pipes)

管道是最基本的进程间通信方式之一,提供了单向的数据流机制。管道可以在父子进程之间传递数据,通常用于将一个进程的输出作为另一个进程的输入。

工作原理

  • 管道有两个端点:一个用于写入数据(write end),另一个用于读取数据(read end)。
  • 数据以字节流的形式通过管道传输,写入管道的数据存储在内核缓冲区中,等待被读取。
  • 管道的生命周期与进程相关,进程结束后管道也会关闭。

优点

  • 简单易用,适合父子进程之间的通信。
  • 无需显式同步,数据读写由内核管理。

缺点

  • 单向通信,只能在父子进程或具有共同祖先的进程间使用。
  • 不适合需要复杂数据结构传输的场景。

示例代码

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

int main() {
    
    
    int fd[2];
    char buffer[100];

    if (pipe(fd) == -1) {
    
    
        perror("pipe failed");
        return 1;
    }

    if (fork() == 0) {
    
      // 子进程
        close(fd[0]);   // 关闭读端
        write(fd[1], "Hello from child", 17);
        close(fd[1]);
    } else {
    
      // 父进程
        close(fd[1]);   // 关闭写端
        read(fd[0], buffer, sizeof(buffer));
        printf("Parent received: %s\n", buffer);
        close(fd[0]);
    }

    return 0;
}
2.2 命名管道(FIFO)

命名管道是一种特殊类型的管道,它具有名称并且存在于文件系统中,因此可以在不相关的进程之间进行通信。命名管道提供了一个持久的通信通道,进程可以通过文件名来访问这个管道。

工作原理

  • 命名管道通过 mkfifo 命令或系统调用创建,并在文件系统中作为一个特殊文件存在。
  • 任意进程都可以打开命名管道进行读写操作,管道的行为与普通管道类似。

优点

  • 支持无关进程之间的通信。
  • 提供一个持久的通信通道,可以跨进程和跨时间段使用。

缺点

  • 仍然是单向通信,每次只能有一个写入者和一个读取者同时操作管道。

示例代码

创建命名管道:

mkfifo /tmp/myfifo

写入数据:

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

int main() {
    
    
    int fd = open("/tmp/myfifo", O_WRONLY);
    write(fd, "Hello, FIFO", 11);
    close(fd);
    return 0;
}

读取数据:

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

int main() {
    
    
    char buffer[100];
    int fd = open("/tmp/myfifo", O_RDONLY);
    read(fd, buffer, sizeof(buffer));
    printf("Received: %s\n", buffer);
    close(fd);
    return 0;
}
2.3 消息队列(Message Queues)

消息队列是一种更为复杂的进程间通信机制,它允许进程之间通过发送和接收消息来交换数据。消息队列为每个消息分配一个消息类型,接收进程可以选择性地接收特定类型的消息。

工作原理

  • 消息队列在内核中维护,进程通过消息队列的标识符来访问它。
  • 消息队列中的每个消息都有一个类型标识符和数据内容。发送者可以指定消息类型,接收者可以根据类型选择性地接收消息。

优点

  • 支持无关进程之间的双向通信。
  • 消息队列中的消息是有序的,允许按优先级处理消息。

缺点

  • 消息队列长度有限,如果队列满了,发送进程将被阻塞。
  • 消息队列的操作比管道稍微复杂,需要更多的系统调用。

示例代码

创建和发送消息:

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

struct msgbuf {
    
    
    long mtype;
    char mtext[100];
};

int main() {
    
    
    key_t key = ftok("progfile", 65);
    int msgid = msgget(key, 0666 | IPC_CREAT);

    struct msgbuf message;
    message.mtype = 1;
    strcpy(message.mtext, "Hello, Message Queue");

    msgsnd(msgid, &message, sizeof(message), 0);

    printf("Message sent: %s\n", message.mtext);
    return 0;
}

接收消息:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>

struct msgbuf {
    
    
    long mtype;
    char mtext[100];
};

int main() {
    
    
    key_t key = ftok("progfile", 65);
    int msgid = msgget(key, 0666 | IPC_CREAT);

    struct msgbuf message;
    msgrcv(msgid, &message, sizeof(message), 1, 0);

    printf("Message received: %s\n", message.mtext);
    return 0;
}
2.4 共享内存(Shared Memory)

共享内存是最快的进程间通信方式,因为它允许多个进程直接共享一个内存区域。共享内存可以在多个进程之间实现数据的直接访问,而不需要通过内核进行数据传递。

工作原理

  • 共享内存区是一个在内存中分配的区域,多个进程可以映射这个区域,并直接读写其中的数据。
  • 共享内存的创建和管理由系统调用(如 shmgetshmat 等)处理,内核负责提供访问权限和同步机制。

优点

  • 高效,进程之间可以直接访问内存,不需要额外的系统调用。
  • 适合大数据量的共享,尤其是需要频繁访问的数据。

缺点

  • 需要额外的同步机制(如信号量)来避免竞态条件。
  • 内存管理复杂,可能会导致数据一致性问题。

示例代码

创建和写入共享内存:

#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>

int main() {
    
    
    key_t key = ftok("shmfile", 65);
    int shmid = shmget(key, 1024, 0666 | IPC_CREAT);

    char *str = (char*) shmat(shmid, (void*)0, 0);
    strcpy(str, "Hello, Shared Memory");

    printf("Data written to shared memory: %s\n", str);
    shmdt(str);

    return 0;
}

读取共享内存:

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

int main() {
    
    
    key_t key = ftok("shmfile", 65);
    int shmid = shmget(key, 1024, 0666 | IPC_CREAT);

    char *str = (char*) shmat(shmid, (void*)0, 0);
    printf("Data read from shared memory: %s\n", str);
    shmdt(str);

    return 0;
}
2.5 信号量(Semaphores)

信号量是一种用于进程间同步的机制,主要用于管理共享资源的访问。信号量可以用来解决竞态条件问题,是共享内存等通信机制的常用同步工具。

工作原理

  • 信号量是一个计数器,用于控制对共享资源的访问。进程

可以增加或减少信号量的值,以表示占用或释放资源。

  • 当信号量的值为 0 时,表示资源已被占用,其他试图访问资源的进程将被阻塞。

优点

  • 提供了有效的进程同步机制,可以用于控制对共享资源的访问。
  • 与共享内存结合使用时,能够解决并发访问问题。

缺点

  • 仅用于同步,不传递数据。
  • 信号量操作复杂,容易引发死锁和资源泄露问题。

示例代码

创建和操作信号量:

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

int main() {
    
    
    key_t key = ftok("semfile", 65);
    int semid = semget(key, 1, 0666 | IPC_CREAT);

    // 初始化信号量
    semctl(semid, 0, SETVAL, 1);

    // 减少信号量,进入临界区
    struct sembuf sb = {
    
    0, -1, 0};
    semop(semid, &sb, 1);

    printf("Critical section entered\n");
    sleep(2);  // 模拟临界区操作

    // 增加信号量,离开临界区
    sb.sem_op = 1;
    semop(semid, &sb, 1);

    printf("Critical section left\n");

    return 0;
}
2.6 套接字(Sockets)

套接字不仅用于网络通信,也可以用于本地进程间通信(IPC)。本地套接字通过 AF_UNIX 地址族实现进程间通信。它是一种非常强大的 IPC 机制,可以用于无关进程之间的双向通信。

工作原理

  • 套接字是通信端点,通过 socket 系统调用创建,进程可以通过套接字发送和接收数据。
  • AF_UNIX 套接字用于本地通信,AF_INET 等用于网络通信。

优点

  • 适用于复杂和异构系统的通信。
  • 提供可靠的数据传输机制,支持双向通信。

缺点

  • 实现和管理较为复杂。
  • 相比于共享内存和信号量,通信开销较大。

示例代码

服务器端:

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

int main() {
    
    
    int server_fd, client_fd;
    struct sockaddr_un server_addr;

    server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    server_addr.sun_family = AF_UNIX;
    strcpy(server_addr.sun_path, "/tmp/socketfile");

    bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    listen(server_fd, 5);

    client_fd = accept(server_fd, NULL, NULL);
    char buffer[100];
    read(client_fd, buffer, sizeof(buffer));
    printf("Received: %s\n", buffer);
    close(client_fd);
    close(server_fd);

    return 0;
}

客户端:

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

int main() {
    
    
    int client_fd;
    struct sockaddr_un client_addr;

    client_fd = socket(AF_UNIX, SOCK_STREAM, 0);
    client_addr.sun_family = AF_UNIX;
    strcpy(client_addr.sun_path, "/tmp/socketfile");

    connect(client_fd, (struct sockaddr*)&client_addr, sizeof(client_addr));
    write(client_fd, "Hello, Socket", 13);
    close(client_fd);

    return 0;
}

三、总结

Linux 提供了丰富的进程间通信方式,每种方式都有其独特的特点和适用场景:

  • 管道和命名管道:适用于简单的父子进程通信,数据以字节流形式传输。
  • 消息队列:适用于需要传递结构化数据的场景,支持多种消息类型。
  • 共享内存:适用于需要频繁访问大数据块的场景,但需要配合同步机制使用。
  • 信号量:用于同步和控制对共享资源的访问,避免竞态条件。
  • 套接字:适用于复杂的通信场景,尤其是需要跨网络或在本地进程间进行双向通信的场景。

猜你喜欢

转载自blog.csdn.net/Flying_Fish_roe/article/details/143475743