Linux- pipe()系统调用

管道

管道(Pipe)是一种用于进程间通信(IPC)的简单而有效的方式。在UNIX和类UNIX操作系统(如Linux)中,管道提供了一种让一个进程将其输出发送给另一个进程的输入的机制。管道通常用于数据流的单向传输。

在底层,管道其实是一个由操作系统内核维护的缓冲区。一个进程向管道的一端(写端)写入数据,而另一个进程可以从管道的另一端(读端)读取数据。

管道实现的基本思想

  1. 缓冲区管理:内核维护一个缓冲区,这个缓冲区用于存储写入管道的数据。
  2. 同步与互斥:内核还提供了同步和互斥机制,以保证数据能够安全地从一个进程传输到另一个进程。
  3. 文件描述符:从用户态的角度来看,管道其实就是一对文件描述符,其中一个用于读,另一个用于写。

pipe()

在UNIX-like操作系统中,pipe()系统调用用于创建一个新的管道,这是一种允许两个进程进行单向数据传输的IPC(进程间通信)机制。一个进程写入管道的一端,而另一个进程从管道的另一端读取。

函数原型

管道是通过以下函数原型创建的:

#include 
int pipe(int pipefd[2]);

参数

  • pipefd: 这是一个包含两个int型元素的数组。函数调用成功时,pipefd[0]将成为管道的读取端(read end)的文件描述符,而pipefd[1]将成为管道的写入端(write end)的文件描述符。

返回值

  • 成功:返回0。
  • 失败:返回-1,并设置errno

工作原理

  1. 数据流向:数据从pipefd[1](写端)流向pipefd[0](读端)。

  2. 阻塞和非阻塞pipe()通常是阻塞的。也就是说,读操作会阻塞,直到有数据写入;写操作也会阻塞,直到读端读取了数据。

  3. 数据缓冲:数据首先被写入内核缓冲区,然后由读操作从缓冲区中读取。

  4. 文件描述符的继承pipe()创建的文件描述符可以在fork()之后由子进程继承,这使得pipe()非常适用于父子进程或兄弟进程之间的通信。

  5. 关闭规则:当写端被关闭后,任何尝试从读端读取的操作将立即返回,读取到的数据长度为0(表示EOF)。当读端被关闭后,任何尝试写入写端的操作都将导致发送SIGPIPE信号。

应用场景

管道经常用在多进程应用中,例如shell命令中的管道操作符|,它允许一个命令的输出可以作为另一个命令的输入。

综上所述,pipe()是一种在两个进程之间简单有效地传输数据的低级IPC机制。然而,它有局限性,如仅能进行单向通信,且通常用于有共同祖先的进程之间。

示例

下面的C程序示例演示了如何使用pipe()fork()read()/write()函数来实现简单的进程间通信。这个例子中,父进程向管道中写入字符串,子进程从管道中读取并打印这个字符串。

#include 
#include 
#include 
#include 

int main() {
    int pipefd[2];
    pid_t pid;

    // 创建管道
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    // 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {  // 子进程
        char buffer[128];
        close(pipefd[1]);  // 关闭不需要的写端

        // 从管道读取数据
        read(pipefd[0], buffer, sizeof(buffer));
        printf("Child read: %s\n", buffer);

        close(pipefd[0]);  // 关闭读端
        exit(EXIT_SUCCESS);
    } else {  // 父进程
        const char *msg = "Hello, Pipe!";
        close(pipefd[0]);  // 关闭不需要的读端

        // 写入数据到管道
        write(pipefd[1], msg, strlen(msg) + 1);
        printf("Parent wrote: %s\n", msg);

        close(pipefd[1]);  // 关闭写端

        // 等待子进程结束
        wait(NULL);
    }

    return 0;
}

代码解析

  1. 创建管道:使用pipe(pipefd)创建一个新的管道。成功后,pipefd[0]是读端,pipefd[1]是写端。

  2. 创建子进程:使用fork()创建一个新的进程。

  3. 父进程

    • 关闭读端,因为父进程只需要写。
    • 使用write()将字符串写入管道。
    • 关闭写端。
  4. 子进程

    • 关闭写端,因为子进程只需要读。
    • 使用read()从管道中读取数据。
    • 打印读取的数据。
    • 关闭读端。
  5. 父进程等待:父进程使用wait(NULL)等待子进程结束。

这就是一个简单的使用pipe()进行进程间通信的例子。运行这个程序,应该会看到类似下面的输出:

Parent wrote: Hello, Pipe!
Child read: Hello, Pipe!

这表明父进程写入的数据成功地被子进程读出。


】当一个进程(父进程)调用fork()创建子进程时,子进程会继承父进程的文件描述符表。这意味着pipefd[0]pipefd[1]在子进程中的值将与父进程中的值相同,它们指向同一个管道。

具体地说,父进程和子进程将拥有指向同一个内核管道对象的文件描述符。这使得父子进程可以通过这个管道进行通信。

因为子进程继承了父进程的文件描述符,所以:

  • 在子进程中,pipefd[0]仍然是管道的读端。
  • 在子进程中,pipefd[1]仍然是管道的写端。

这就是为什么在创建管道和fork()之后,通常会看到一些close()调用:每个进程通常只需要管道的一端,所以会关闭不需要的那一端。这样做有助于避免潜在的死锁和资源泄漏。

例如,在上面的示例代码中:

  • 子进程关闭了写端(close(pipefd[1]);),因为它只从管道中读取数据。
  • 父进程关闭了读端(close(pipefd[0]);),因为它只向管道中写入数据。

这种做法使得管道更容易管理,并且可以确保当所有的写端或读端被关闭时,相关的操作(如read()write())能够正确地返回。

你可能感兴趣的:(Linux,C,linux,运维,服务器)