进程间通信之Linux C管道编程

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/qq_37653144/article/details/83927634

管道简述

管道(pipe)是Unix/Linux中最常见的进程间通信方式之一,它在两个进程之间实现一个数据流通的通道,数据以一种数据流的方式在进程间流动。在系统中,管道相当于文件系统上的一个文件,用于缓存所要传输的数据。在某些特性上又不同于文件,例如当数据读出后,管道中就没有数据了,但文件没有这个特性。但管道具有两个缺点:

·部分系统下的管道是半双工的,数据在同一时间只能向一个方向流动。从实现的角度看,Linux内核采用环形缓冲区实现管道,如果允许同时读写,可能会导致数据冲突,Linux采用锁机制来防止同时读写。

·管道通常来说只能在具有亲属关系(父子进程、兄弟进程)的进程间使用。

管道(也称为匿名管道)是Linux中最古老的进程通信机制,其应用非常广泛,也为用户在shell中提供了相应的管道操作符“|”。操作符“|”将其前后两个命令连接到一起,前一个命令的输出成为后一个命令的输入,且可以支持使用多个“|”连接多个命令。

管道特点

·管道没有名字,所以也称为匿名管道

·管道是半双工的,数据只能向一个方向流动,需要双向通信时,需要建立起两个管道。

·只能用于父子进程或兄弟进程之间。

·管道对于其两端的进程而言只是一个文件,但它不是普通的文件,它不属于操作系统的某种文件系统,而是单独构成一种文件系统,并且只存在于内存中。

·管道传送的是无格式字节流,这就要求使用管道的通信双方必须事先约定好数据的格式。

管道实现

/**
 *    struct pipe_buffer - a linux kernel pipe buffer
 *    @page: the page containing the data for the pipe buffer
 *    @offset: offset of data inside the @page
 *    @len: length of data inside the @page
 *    @ops: operations associated with this buffer. See @pipe_buf_operations.
 *    @flags: pipe buffer flags. See above.
 *    @private: private data owned by the ops.
 **/
struct pipe_buffer {
    struct page *page;
    unsigned int offset, len;
    const struct pipe_buf_operations *ops;
    unsigned int flags;
    unsigned long _private;
};


/**
 *    struct pipe_inode_info - a linux kernel pipe
 *    @mutex: mutex protecting the whole thing
 *    @wait: reader/writer wait point in case of empty/full pipe
 *    @nrbufs: the number of non-empty pipe buffers in this pipe
 *    @buffers: total number of buffers (should be a power of 2)
 *    @curbuf: the current pipe buffer entry
 *    @tmp_page: cached released page
 *    @readers: number of current readers of this pipe
 *    @writers: number of current writers of this pipe
 *    @files: number of struct file referring this pipe (protected by ->i_lock)
 *    @waiting_writers: number of writers blocked waiting for room
 *    @r_counter: reader counter
 *    @w_counter: writer counter
 *    @fasync_readers: reader side fasync
 *    @fasync_writers: writer side fasync
 *    @bufs: the circular array of pipe buffers
 *    @user: the user who created this pipe
 **/
struct pipe_inode_info {
    struct mutex mutex;
    wait_queue_head_t wait;
    unsigned int nrbufs, curbuf, buffers;
    unsigned int readers;
    unsigned int writers;
    unsigned int files;
    unsigned int waiting_writers;
    unsigned int r_counter;
    unsigned int w_counter;
    struct page *tmp_page;
    struct fasync_struct *fasync_readers;
    struct fasync_struct *fasync_writers;
    struct pipe_buffer *bufs;
    struct user_struct *user;
};

当进程创建一个匿名管道时,Linux内核为匿名管道准备了两个文件描述符:一个用于管道的输入,即在管道中写入数据;另一个用于管道的输出,也就是从管道中读出数据。抽象图如下: 

匿名管道

如果一个管道只与一个进程相联系,只实现进程自身的内部通信,则这个管道是毫无意义的。通常情况下,一个创建管道的进程接着就会创建其子进程,由于父子进程可以共享打开文件,子进程将从父进程那里继承到读写管道的文件描述符,这样便建立了父子进程之间的通信管道。

由于管道是半双工,因此在编程时使用管道,需要确定数据的传输方向,父子进程分别关闭与之无关的描述符。例如数据从子进程传送到父进程,则子进程关闭读管道的描述符。

管道的读写规则

由于管道是半双工通信,因此在进行相应操作时需要注意以下几点:

·如果从一个写描述符关闭的管道中读数据,当读完所有的数据后,read函数返回0,表明已到达文件末尾。严格来说,只有当没有数据继续写入后,才可以说达到了文件末尾。所以应该分清到底是暂时没有数据输入还是已经到达文件末尾。如果是前者,该进程应该等待。若为多进程写、单进程读的情况就更为复杂。

·如果向一个读描述符关闭的管道中写数据,就会产生SIGPIPE信号,不管是忽略这个信号还是处理它,write函数都返回-1。

·常数PIPE_BUF规定了内核中管道缓冲区的大小,因为管道被设计为环形缓冲区,所以在写管道时要注意写入的数据大小。如果超过规定大小就会产生交错现象,从而导致数据丢失。

Linux的管道操作

创建

Linux内核提供了函数pipe用于创建一个管道,它接收一个长度为2的int类型数组,用于内核写入文件描述符。fd[0]为读出端的描述符,fd[1]为写入端的描述符。以下示例代码在同一个进程中读写同一个管道,这并无意义,仅作为示例。

#include <iostream>
#include <cstdlib>
#include <unistd.h>

int main()
{
    int fd[2]; //创建文件描述符数组
    char writebuf[20] = {"Hello World!"}; //写缓冲区
    char readbuf[20]; //读缓冲区

    if (pipe(fd) < 0)
    {
        std::cerr << "创建管道失败\n";
        exit(0);
    }
    write(fd[1], writebuf, sizeof(writebuf)); //向管道写入端写入数据
    write(fd[0], readbuf, sizeof(readbuf)); //向管道读出端读出数据
    std::cout << readbuf << "\n";
    std::cout << "管道的读fd是" << fd[0] << "\n写fd是" << fd[1] << std::endl;
    return 0;
}

管道通信

管道的用途是为了在不同的进程中进行数据交互,但仅限于在有亲属关系的进程间使用,即父子进程、兄弟进程等。以下代码以父子进程间通信为例。

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    int fd[2];
    pid_t pid;
    char buf[20];

    if (pipe(fd) < 0)
    {
        std::cerr << "创建管道失败\n";
        exit(0);
    }

    if ((pid = fork()) < 0)
    {
        std::cerr << "创建子进程失败\n";
        exit(0);
    }
    else if (pid > 0) //父进程
    {
        close(fd[0]); //关闭读出端
        write(fd[1], "I'm your father.\n", 17);
    }
    else //子进程
    {
        close(fd[1]); //关闭写入端
        read(fd[0], buf, sizeof(buf));
        std::cout << buf;
        exit(0);
    }

    if (waitpid(pid, NULL, 0) != pid)
    {
        cerr << "销毁进程失败\n";
    }
    exit(0);
}

管道高级应用

从上一段代码示例可看出,管道创建函数pipe通常是和进程创建函数fork或fork配合使用。Linux内核提供了将两者“合二为一”的函数popen和pclose。

FILE *popen(const char *cmdstring, const char *type);
int pclose(FILE *stream);

与fopen和fclose函数一样,popen和pclose必须配合使用。

函数popen用于创建管道,内部调用fork和exec函数执行第一个参数中指出的命令,返回一个FILE结构的指针,即用于访问管道的指针。第二个参数则用于指出管道的类型,如果管道是以类型“r”打开的,那么这个管道的输入端将连接到命令行cmdstring的标准输出端,此时命令行的输出可以从管道中读入。反之,如果是以类型“w”打开的,那么这个管道的输出端将连接到命令行的标准输入端,此时向管道中写入的数据就成为命令行的输入数据。

猜你喜欢

转载自blog.csdn.net/qq_37653144/article/details/83927634