管道

1. 管道概述

管道(pipe) 又称之为无名管道。

无名管道是一个特殊的文件类型,在应用程序执行的过程中存在,利用两个【文件描述符】来实现管道操作。利用 write 和 read 系统调用函数,借助于两个文件描述符,实现管道操作。

无名管道采用的通信方式/协议是最古老的 UNIX IPC 方式

  • 半双工,数据在同一个时间,有且只允许单向通信
  • 数据有且只允许,从一端读取,另一端写入
  • 管道中的数据遵循 FIFO (先进先出) 原则,一旦数据从管道的一端读取之后,当前被读取数据在管道中消失。
  • 管道中的数据无格式要求,取决于当前用户设置的通信数据格式,数据要求和数据形式。
  • 无名管道不是一个【普通文件】是内核中的一个特殊标记文件,存在于内存中
  • 进程打开的管道,在系统中有明确的限制个数和限制缓冲区容量,当前 Ubuntu 18.04 中,默认管道缓冲区位 512bytes ,个数 8

2. 无名管道操作

2.1 相关函数 pipe

函数文档

#include <unistd.h>

int pipe(int pipefd[2]);
  • 功能:
    • 当前进程创建一个管道,参数要求是一个 int 类容量为 2 的数组
  • 参数解释
    • int pipefd[2] : 要求是一个 int 类型的容量为 2 的数组,管道创建完毕之后,对应的文件描述符存储在下标 0 和下标 1 的位置
    • 同时限制 pipefd[0] 管道读打开
    • 同时限制 pipefd[1] 管道写打开
  • 返回值
    • 管道创建成功返回 0
    • 管道创建失败返回 -1
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>

int main(int argc, char const *argv[])
{
    int pipefd[2] = {0};

    /*
    进程占用的默认文件描述符有
        #define STDIN_FILENO   0
        #define STDOUT_FILENO  1
        #define STDERR_FILENO  2

    默认情况下,用户可以使用的最小文件描述符为 3
    */
    int ret = pipe(pipefd);

    if (0 == ret)
    {
        printf("pipefd[0] read : %d, pipefd[1] write : %d\n", pipefd[0], pipefd[1]);
    }
    else
    {
        abort();
    }

    /*
    父进程通过 pipefd[1] 写入 write 数据,子进程通过 pipefd[0] 读取 read 数据
    */
    pid_t pid = -1;

    pid = fork();
    if (-1 == pid)
    {
        perror("Fork Failed!\n");
    }

    if (0 == pid)
    {
        // 子进程 【子进程通过 pipefd[0] 读取 read 数据】
        char buf[32] = "";
        read(pipefd[0], buf, sizeof(buf));

        printf("Son Process get data : %s\n", buf);
  
        _exit(0);
    }
    else
    {
        // 父进程 【父进程通过 pipefd[1] 写入 write 数据】
        char data[] = "Hello World!";
        printf("Parent Process write data to pipe : %s\n", data);

        // sleep(2);
        write(pipefd[1], data, sizeof(data));

        /*
        char *data = "Hello World!";
            sizeof(data);        8(64位) 4(32位)
            sizeof("Hello World!");  13

        char data[] = "Hello World!";
            sizeof(data);   
            sizeof("Hello World!");
        */
       _exit(0);

    }

    return 0;
}
2.2 dup 和 dup2

【拷贝文件描述符】

函数文档

#include <unistd.h>

int dup(int oldfd);
  • 函数功能:
    • 复制当前现有的文件描述符,得到一个新的文件描述,同时新的文件描述符是目前进程中可以使用的最小文件描述符。
  • 函数参数:
    • int oldfd : 现有的文件描述符,如果文件描述符已关闭 或者存在,【ERROR】
  • 返回值:
    • 得到的新的文件描述符,与 oldfd 指向同一个文件资源,操作新的 fd 和 oldfd 是一致的。
    • 如果执行失败,返回 -1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char const *argv[])
{
    int fd = open("./1.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);

    if (-1 == fd)
    {
        perror("open failed!\n");
    }

    printf("fd : %d\n", fd);

    /*
    当前文件对应的文件描述符,进行复制,得到新的文件描述符,同时
    新的文件描述,具备原文件描述符的所有内容。
    */
    int new_fd = dup(fd);

    write(fd, "1. Hello World!\n", strlen("1. Hello World!\n"));
    write(new_fd, "2. Hello World!\n", strlen("2. Hello World!\n"));

    /*
    当前 old_fd 关闭之后,对于通过 dup 函数拷贝的新的 文件描述符没有任何的影响
    新的文件描述符依然可以使用
    【注意】
        考虑到文件资源问题,需要对当前新的文件描述 new_fd 同样进行 close 操作
    */
    close(fd);

    write(new_fd, "3. Hello World!\n", strlen("3. Hello World!\n"));

    close(new_fd);

    return 0;
}

函数文档

#include <unistd.h>

int dup2(int oldfd, int newfd);
  • 函数功能:
    • 将已有的文件描述 oldfd 【复制】/【重定向】到 newfd ,
    • 如果 newfd 已存在,将覆盖原本的文件描述符属性,同时将原本文件描述对应的资源进行关闭。
    • 【少见】如果 newfd 不存在,则根据当进程中,最小文件描述符情况分配新的文件描述符,功能类似于 dup
  • 函数参数:
    • int oldfd : 现有的文件描述符,如果文件描述符已关闭 或者存在,【ERROR】
    • int newfd : 已有的文件描述符,重定向之后,newfd 原本资源被关闭,newfd 和 oldfd 资源一致。
  • 返回值:
    • 若调用成功,返回新的文件描述符(即 newfd)。
    • 如果执行失败,返回 -1

3. 有名管道操作

3.1 基本概述
  • 半双工,数据在同一个时间,有且只允许单向通信
  • 数据有且只允许,从一端读取,另一端写入
  • 管道中的数据遵循 FIFO (先进先出) 原则,一旦数据从管道的一端读取之后,当前被读取数据在管道中消失。
  • 管道中的数据无格式要求,取决于当前用户设置的通信数据格式,数据要求和数据形式。
  • 有名管道是利用 Linux 中的管道文件,实现管道操作,在硬盘中真实存在对应的文件内容。
  • 进程打开的管道,在系统中有明确的限制个数和限制缓冲区容量,当前 Ubuntu 18.04 中,默认管道缓冲区位 512bytes ,个数 8
3.2 相关函数

函数文档

#include <sys/types.h>
#include <sys/stat.h>
​
int mkfifo(const char *pathname, mode_t mode);
  • 功能
  • 在用户指定的路径中,创建一个管道文件,并且指定当前管道文件的操作权限。
  • 参数解释
  • const char *pathname : 指定管道文件所在路径和对应名称。一般情况下,管道文件没有对应的后缀名
  • mode_t mode: 设置当管道文件的相关操作权限,主要是拥有者,同组用户和访客的 RWX 操作权限,可以利用 八进制 权限方式限制
  • 返回值类型
  • 如果命名管道创建成功,mkfifo 函数返回 0。
    • **如果创建过程中出现错误,函数返回 -1,并设置 **errno 来指示具体的错误类型。
  • 可能的错误类型
  • EACCESS:调用进程没有足够的权限在指定的目录下创建命名管道。
  • EEXIST:指定的路径名已经存在,可能是一个普通文件、目录或其他类型的文件。
  • ENAMETOOLONG:路径名太长。
  • ENOENT:指定路径中的某个目录不存在。
  • ENOTDIR:路径中的某个组件不是一个目录。
  • EROFS:指定的路径位于只读文件系统中。
3.3 案例代码

创建管道

#include <stdio.h>
​
#include <sys/types.h>
#include <sys/stat.h>
​
int main(int argc, char const *argv[])
{
    int ret = mkfifo("./fifo", 0666);
    
    if (!ret) 
    {
        printf("Create Pipe Successful!\n");
    }
​
    return 0;
}
​

写入数据到管道

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
​
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
​
#define PATH_NAME "./fifo"
​
int main(int argc, char const *argv[])
{
    // 打开管道,利用 write 函数向管道中写入数据
​
    printf("Write Data to Pipe!\n");
    // 1. 利用 open 函数打开管道,打开方式为只写方式 O_WRONLY 
    int fd = open(PATH_NAME, O_WRONLY);
​
    char * data = "Hello World!\n";
    // 2. 利用 write 函数写入数据到管道中
    write(fd, data, strlen(data));
​
    // 3. 关闭 文件描述符
    close(fd);
    return 0;
}
​

从管道中读取数据

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
​
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
​
#define PATH_NAME "./fifo"
#define BUFFER_SIZE 64
​
int main(int argc, char const *argv[])
{
    // 打开管道,利用 read 函数从管道中读取数据
​
    printf("Read Data From Pipe!\n");
    // 1. 利用 open 函数打开管道,打开方式为只读方式 O_RDONLY 
    int fd = open(PATH_NAME, O_RDONLY);
​
    char buf[BUFFER_SIZE];
    memset(buf, 0, BUFFER_SIZE);
​
    // 2. 利用 read 函数从管道文件中读取数据
    read(fd, buf, BUFFER_SIZE);
​
    printf("Data : %s\n", buf);
​
    // 3. 关闭 文件描述符
    close(fd);
    return 0;
}