3

linux进程间通信-管道

 4 months ago
source link: https://blog.51cto.com/u_16704018/10676327
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

常见的进程间通信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删除该命名管道。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK