常见的进程间通信IPC机制包括管道(pipe)、信号(signal)、消息队列(message queue)、共享内存(shared memory)和套接字(socket)。

1. 管道(Pipe)

管道是一种基于内存的、面向字节的、单向的通信方式,通常用于具有亲缘关系的进程间通信,如父子进程。管道有两种类型:匿名管道和命名管道。

通常情况下,管道设计为单向通信机制,这意味着数据只能在一个方向上流动,这种单向性是管道区别于其他IPC机制(如消息队列、共享内存等)的一个特点

在创建管道时,会生成两个文件描述符:一个用于读取(read),另一个用于写入(write)。比如,父进程会使用管道的读取端来读取数据,而子进程会使用写入端来发送数据。如果尝试在同一个管道的同一端进行读写操作,可能会导致死锁或不可预测的行为。

如果需要在两个进程之间实现双向通信,可以创建两个管道,每个管道用于一个方向的通信。例如,父进程和子进程可以分别使用一个管道进行读取和写入操作,而另一个管道则用于相反方向的操作。

读取空的管道会怎样呢?

在管道通信中,读操作和写操作是相互协调的。当一个进程(如子进程)向管道写入数据时,这些数据会被放入一个缓冲区。然后,另一个进程(如父进程)可以从管道中读取这些数据。如果父进程尝试读取数据而缓冲区为空,它将等待直到有数据可用或者写入端关闭。这种阻塞行为是管道作为同步通信手段的一个关键特性。它允许父进程和子进程在没有显式同步机制(如信号量、互斥锁等)的情况下进行协调。某个进程(如父进程)可以通过检查读操作的返回值来判断管道是否关闭,因为当管道的写入端关闭时,读操作将返回0,表示没有更多的数据可读。

关闭管道的某端会怎样呢?

管道的关闭操作是永久性的,不能被撤销或者重新打开。

如果管道的读取端被关闭,而写入端仍然尝试写入数据,写入操作将会失败,并且通常会返回一个错误。在Linux系统中,这通常会导致 write调用返回-1,并将 errno变量设置为 EPIPE,表示管道的另一端已经关闭。

如果管道的写端被关闭,而读端仍然尝试读取数据,根据UNIX和Linux系统的行为,以下是可能发生的情况:

  1. 如果管道中还有数据 :读端可以继续读取管道中已经存在的数据,直到数据被完全读取为止。
  2. 如果管道中没有数据 :一旦写端关闭,管道中的数据被读取完毕,读端再尝试读取操作将会立即返回0,表示没有更多的数据可读,并且管道的读取端也被隐式地关闭了。这种行为通常称为“EOF”(End Of File)。

在编程实践中,当从管道读取到0字节时,通常意味着管道的写入端已经关闭,且管道中没有更多的数据可以读取。这是一个信号,表明管道的通信已经结束。在这种情况下,读取进程应该相应地处理这一情况,例如通过退出循环或关闭自己的读端来清理资源。

管道的大小是多少呢?

管道(pipe)的大小并不是在创建时指定的一个固定值。实际上,管道的大小是由操作系统的内核自动管理的,它会根据系统资源和当前负载动态调整。由于管道的缓冲区是内存中的,其大小也受限于系统的内存使用情况。

管道满会怎样呢?

当管道的缓冲区接近满时,尝试向管道写入更多数据的操作将会阻塞,直到管道中有一些空间被释放(即数据被读取)。如果写入端继续尝试写入数据,而读取端没有读取数据,写入操作将会一直阻塞,直到缓冲区有足够空间为止。这种行为保证了管道中的数据不会丢失,但也可能导致写入端进程长时间等待。

如何获取管道中的数据有多少?

要确定管道中有多少数据可以使用 select系统调用或 ioctl系统调用来查询文件描述符的状态。select调用允许你的程序监控一组文件描述符,等待它们变得可读、可写或有错误。对于管道,你可以使用 select来检查管道的读取端是否包含可供读取的数据,或者管道的写入端是否可以写入更多数据而不会导致阻塞。

总的来说,管道是一种可靠的通信机制,它确保了数据的顺序传输和不丢失。但是,在使用管道时,需要考虑可能出现的阻塞情况,并适当地处理这些情况,以确保程序的正确性和效率。

1.1. 匿名管道(Anonymous Pipe)

匿名管道是一种临时的、不存储在文件系统中的通信方式。它用于具有亲缘关系的进程,如父子进程之间的通信。匿名管道是单向的,数据只能在一个方向上流动,例如从父进程到子进程或从子进程到父进程。

特点:

  • 单向性:数据只能在一个方向上流动。
  • 临时性:匿名管道在创建它的进程结束后就会消失,不会在文件系统中创建特殊文件。
  • 无需文件系统:不需要在文件系统中创建特殊文件。
  • 操作函数:使用 pipe 创建匿名管道,使用 read, write, close等常规文件操作函数进行通信,close后不可以再重新 open
  • 适用场景:只适用于亲缘关系的进程。

1.2. 命名管道(Named Pipe,也称为FIFO)

命名管道是一种持久的、存储在文件系统中的特殊文件,可以用于任意两个进程之间的通信,无论它们是否有亲缘关系。命名管道也是单向的,但与匿名管道不同,即使创建它的进程已经结束,命名管道仍然存在,直到被显式删除。

特点:

  • 单向性:数据只能在一个方向上流动。
  • 持久性:命名管道在文件系统中以文件形式存在,直到被删除,是一种特殊的文件系统对象。
  • 操作函数:使用 mkfifounlink 创建和删除有名管道,使用 open, read, write, close 等常规文件操作函数进行通信,close后可以再重新 open
  • 适用场景: 任意进程间通信,不要求进程具有亲缘关系。

实验1:匿名管道(Anonymous Pipe)

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

#define PARENT_WRITE "Hello from parent"
#define CHILD_WRITE "Child response"
#define BUF_SIZE 50

void parent_communication(int read_fd, int write_fd) {
    char buffer[BUF_SIZE]={'\0'};
    int bytes_available = 0;
    int maxfd, n;

    // 向子进程发送数据
    write(write_fd, PARENT_WRITE, strlen(PARENT_WRITE));
    close(write_fd);

    // 等待读取端准备就绪
    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(read_fd, &readfds);
    maxfd = read_fd + 1; // 确保maxfd是最大的文件描述符

    struct timeval timeout;
    timeout.tv_sec = 2;
    timeout.tv_usec = 0;

    // 使用select等待读取数据
    while ((n=(select(maxfd, &readfds, NULL, NULL, &timeout))) > 0) {
        if (FD_ISSET(read_fd, &readfds)) {
            ioctl(read_fd, FIONREAD, &bytes_available);
            read(read_fd, buffer, bytes_available);
            printf("Parent received: %s\n", buffer);
        } else if (n == 0) {    // 超时
            break;
        }
    }

    close(read_fd);

}

void child_communication(int read_fd, int write_fd) {
    char buffer[BUF_SIZE]={'\0'};
    fd_set readfds, writefds, exceptfds;
    struct timeval timeout;
    int maxfd, n;
    int bytes_available = 0;

    // 从父进程接收数据
    FD_ZERO(&readfds);
    FD_SET(read_fd, &readfds);
    maxfd = read_fd + 1; // 确保maxfd是最大的文件描述符

    // 设置超时时间
    timeout.tv_sec = 1;
    timeout.tv_usec = 0;

    // 使用select等待读取数据
    while ((n = select(maxfd, &readfds, NULL, &exceptfds, &timeout)) > 0) {
        if (FD_ISSET(read_fd, &readfds)) {
            ioctl(read_fd, FIONREAD, &bytes_available);
            read(read_fd, buffer, bytes_available);
            printf("[%s: %d]: bytes_available=%d\n", __func__, __LINE__, bytes_available);
            printf("Child received: %s\n", buffer);
        } else if (n == 0) {    // 超时
            break;
        }
    }

    // 向父进程发送数据
    write(write_fd, CHILD_WRITE, strlen(CHILD_WRITE));

    close(read_fd);
    close(write_fd);
}

int main() {
    int pipefd1[2];     // 父进程 -> 子进程
    int pipefd2[2];     // 父进程 <- 子进程
    int status;

    // 创建两个管道
    if (pipe(pipefd1) < 0 || pipe(pipefd2) < 0) {
        perror("pipe");
        exit(1);
    }

    pid_t pid = fork();

    if (pid == 0) { // 子进程
        child_communication(pipefd1[0], pipefd2[1]);
    } else if (pid > 0) { // 父进程
        parent_communication(pipefd2[0], pipefd1[1]);
        wait(&status); // 等待子进程结束
    } else {
        perror("fork");
        exit(1);
    }

    return 0;
}

实验1结果:

eon@ubuntu:~/code/test/test_process$ gcc -o test_pipe test_pipe.c 
eon@ubuntu:~/code/test/test_process$ ./test_pipe
[child_communication: 68]: bytes_available=17
Child received: Hello from parent
Parent received: Child response

实验解释:

通过pipe函数创建了两个管道 pipefd1 和 pipefd2,分别用于父进程向子进程发送消息和子进程向父进程发送消息,并且使用 select 调用监控文件描述符的状态,等待它们变得可读、可写或有错误。使用 ioctl 配合 FIONREAD 来获取管道中的数据量。

实验2:命名管道(Named Pipe,也称为FIFO)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/select.h>
#include <string.h>
#include <fcntl.h> // 包含fcntl.h以获取open函数和O_WRONLY宏的定义
#include <sys/stat.h> // 包含这个头文件以获取mkfifo函数的声明

#define FIFO_FILE "/tmp/myfifo"

void child_write_to_fifo(const char *fifo_path) {
    int fd = open(fifo_path, O_WRONLY);
    if (fd < 0) {
        perror("open");
        exit(1);
    }

    char message[] = "Hello, reader!\n";
    write(fd, message, strlen(message));
    close(fd);
}

void child_read_from_fifo(const char *fifo_path) {
    int fd = open(fifo_path, O_RDONLY);
    if (fd < 0) {
        perror("open");
        exit(1);
    }

    char buffer[20];
    fd_set readfds;
    struct timeval timeout;

    FD_ZERO(&readfds);
    FD_SET(fd, &readfds);

    timeout.tv_sec = 5;
    timeout.tv_usec = 0;

    while (1) {
        // 使用select等待读取数据
        if (select(fd + 1, &readfds, NULL, NULL, &timeout) > 0) {
            if (FD_ISSET(fd, &readfds)) {
                read(fd, buffer, sizeof(buffer));
                write(1, buffer, strlen(buffer));
                break; // 读取到数据后退出循环
            }
        }
    }

    close(fd);
}

int main() {
    pid_t pid1, pid2;

    // 创建命名管道(FIFO)
    if (mkfifo(FIFO_FILE, 0666) < 0) {
        perror("mkfifo");
        exit(1);
    }

    pid1 = fork();
    if (pid1 == 0) { // 子进程1 - 写入命名管道
        child_write_to_fifo(FIFO_FILE);
    } else if (pid1 > 0) { // 父进程
        pid2 = fork();
        if (pid2 == 0) { // 子进程2 - 读取命名管道
            child_read_from_fifo(FIFO_FILE);
        } else { // 父进程等待子进程结束
            wait(NULL);
        }
    }

    // 删除命名管道文件
    unlink(FIFO_FILE);
    return 0;
}

实验2结果:

eon@ubuntu:~/code/test/test_process$ gcc -o test_fifo test_fifo.c 
eon@ubuntu:~/code/test/test_process$ ./test_fifo
Hello, reader!
eon@ubuntu:~/code/test/test_process$

实验解释:

使用mkfifo创建命名管道用于两个子进程间的通信,最后使用unlink删除该命名管道。