Linux-管道

管道

一、介绍

在 Linux 中,管道(pipe)是一种进程间通信(IPC)的方式,它允许一个进程将其输出直接连接到另一个进程的输入,而无需使用临时文件或文件系统进行中间存储。

二、管道的特点

  1. 单向通信:管道只能用于单向通信,即数据只能从一个进程的写端流向另一个进程的读端。
  2. 父子进程间通信:管道通常用于具有血缘关系的进程之间,特别是父子进程。
  3. 基于字节流的通信:管道中的数据以字节流的形式进行传输。
  4. 数据一致性:管道内部实现了同步机制,保证了数据的一致性。
  5. 生命周期:管道的生命周期与进程相关,当进程结束时,管道也会被销毁。

在 Linux 中,管道(pipe)是用于进程间通信(IPC)的一种机制。管道允许一个进程(称为写入者)将数据写入管道,并由另一个进程(称为读取者)从管道中读取数据。根据管道是否有名字,可以将其分为无名管道(也称为普通管道)和有名管道(也称为命名管道或 FIFO)。

1. 无名管道(Anonymous Pipe)

pipe() ​函数用于创建一个无名管道(匿名管道),允许相关的进程间进行半双工的数据传输。这个函数的主要目的是在具有共同祖先的进程间建立一个通信渠道,通常是在父子进程之间。无名管道是通过文件描述符实现的,其中一个文件描述符用于读取,另一个用于写入。

函数原型如下:

#include <unistd.h>

int pipe(int pipefd[2]);
  • pipefd[2] :这是一个整型数组,用于接收由 pipe() ​函数返回的两个文件描述符。
  • pipefd[0] ​是读取端(只读)
  • pipefd[1] ​是写入端(只写)
  • 如果函数执行成功,它将返回 0;如果出现错误,则返回-1 并设置 errno ​变量。

示例(C 语言)


#include <unistd.h>    // 包含unistd.h头文件,用于定义pipe、fork、write、read等函数
#include <stdio.h>     // 标准输入输出库,用于printf等
#include <sys/types.h> // 包含pid_t类型定义
#include <string.h>    // 字符串操作函数库,如strlen
#include <stdlib.h>    // 通用工具库,如exit()
#include <sys/wait.h>  // 包含wait/waitpid等用于等待子进程的函数

int main()
{
    int fd[2];           // 创建一个整型数组fd,用于存储管道的两个描述符

    // 创建管道,fd[0]为读端,fd[1]为写端。失败则返回-1。
    if(pipe(fd)==-1){
        printf("create pipe failed\n");
        return 1;       // 失败则退出程序
    }

    pid_t pid;           // 创建pid_t类型的变量pid,用于存储fork()返回的子进程ID
    char buf[128];       // 创建一个大小为128的字符数组,用于存储读取的数据

    // 调用fork创建子进程
    pid = fork();
    if(pid<0){
        perror("fork failed");  // fork失败打印错误信息
        return 1;              // 并退出程序
    }else if(pid>0){          // 父进程执行的代码块
        sleep(3);             // 父进程等待3秒,确保子进程先运行并准备好读取
        printf("this is father\n");
        close(fd[0]);         // 父进程中不需要读取,关闭读端描述符
        // 向管道写入数据,内容为"hello from father"及字符串结束符'\0'
        write(fd[1],"hello from father",strlen("hello from father")+1);
        wait(NULL);           // 等待子进程结束
    }else if(pid == 0){      // 子进程执行的代码块
        printf("this is son\n");
        close(fd[1]);         // 子进程中不需要写入,关闭写端描述符
        // 从管道读取数据到buf中,最多读取127字节
        read(fd[0],buf,127);
        buf[127]='\0';         // 确保buf是字符串,手动添加结束符
        printf("read from father:%s\n",buf); // 打印读取到的信息
        exit(0);              // 子进程执行完毕,正常退出
    }

    return 0;               // 父进程执行完毕,正常退出
}

2. 有名管道(Named Pipe 或 FIFO)

有名管道克服了无名管道只能在具有亲缘关系的进程间通信的限制,允许任何两个进程通过文件名来访问和使用管道。使用 mkfifo()​ ​或 mknod()​ ​系统调用来创建一个有名管道。

有名管道(Named Pipe),也称为 FIFO(First In First Out),在 Linux 中可以使用 mkfifo() ​系统调用来创建。以下是 mkfifo() ​函数的原型和一些基本的使用说明。

函数原型

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

int mkfifo(const char *pathname, mode_t mode);
  • pathname​:指定要创建的有名管道的路径名。
  • mode​:指定新创建的有名管道的权限位,与 chmod ​命令的模式参数相似,通常会结合 S_IRUSR​、S_IWUSR​、S_IRGRP​、S_IWGRP​、S_IROTH​、S_IWOTH ​等宏来设置。

返回值

  • 成功时返回 0。
  • 出错时返回-1,并设置 errno​。

错误代码

常见的错误代码包括但不限于:

  • EACCES​:没有足够的权限去创建文件。
  • EEXIST​:指定的路径名已经存在并且不是一个 FIFO。
  • ENOENT​:路径名的某个目录成分不存在。
  • ENOMEM​:无法分配内存。

使用说明

  1. 创建有名管道:首先,你需要使用 mkfifo() ​函数在一个指定的路径下创建一个有名管道。例如:

    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <errno.h>
    
    int main() {
        if (mkfifo("/tmp/myfifo", 0666) == -1) {  // 0666为权限模式
            perror("mkfifo");
            return 1;
        }
        printf("FIFO created successfully.\n");
        return 0;
    }
    
  2. 读写操作:一旦有名管道创建成功,就可以像普通文件那样通过文件描述符进行读写操作。通常,一个进程打开管道进行写入,另一个进程打开同一管道进行读取。

    • 读取端:使用 open() ​函数以读取模式(O_RDONLY​)打开管道,然后使用 read() ​函数读取数据。
    • 写入端:使用 open() ​函数以写入模式(O_WRONLY​)打开管道,然后使用 write() ​函数写入数据。
  3. 关闭管道:读写完成后,应使用 close() ​函数关闭文件描述符。

注意:有名管道遵循先进先出的原则,且必须先有读进程打开管道,否则写进程在尝试写入时会被阻塞(除非使用了非阻塞模式)。同样,如果只有写进程而没有读进程,则写进程在写满管道缓存后也会被阻塞。

三、有名管道(FIFO)在 Linux 中的阻塞行为主要体现在读取和写入操作上,具体表现如下:

1、阻塞读取(读端)

当一个进程尝试从有名管道读取数据时,它的行为取决于管道的状态:

  • 有数据可用:如果管道中有数据等待读取,read() ​调用将立即返回,读取数据并解除阻塞状态。

  • 管道为空

    • 阻塞模式:如果该进程是以阻塞模式打开管道(即在 open() ​调用中没有指定 O_NONBLOCK ​标志),那么 read() ​调用将会阻塞,直到有数据可读或者接收到一个信号中断了阻塞状态。
    • 非阻塞模式:如果是以非阻塞模式打开(指定了 O_NONBLOCK​),read() ​会立即返回-1,并设置 errno ​为 EAGAIN ​或 EWOULDBLOCK​,表明没有数据可读且不希望进程阻塞。

2、阻塞写入(写端)

写入有名管道的行为也依赖于管道的状态:

  • 空间足够:如果管道的缓冲区有足够的空间来容纳待写入的数据,write() ​调用将立即执行并返回实际写入的字节数。

  • 管道已满

    • 阻塞模式:如果写进程是以阻塞模式打开管道,write() ​调用将阻塞,直到有其他进程从管道中读取数据腾出空间,或者接收到一个信号中断了阻塞状态。
    • 非阻塞模式:在非阻塞模式下,如果管道已满,write() ​调用会立即返回-1,并设置 errno ​为 EAGAIN ​或 EWOULDBLOCK​,告知调用者现在不能写入更多数据。

3、实际应用中的考虑

在设计使用有名管道的程序时,开发者需要根据程序的需求选择合适的阻塞模式:

  • 同步操作:如果需要保证数据的顺序性和完整性,可能会选择阻塞模式,确保数据被完全处理后再进行下一步操作。
  • 异步操作:对于高性能或实时性要求较高的应用,非阻塞模式更合适,程序可以在数据不可用时执行其他任务,而不是等待。

四、管道程序例子示范

首先是 read4.c ​文件,这个程序的目的是创建一个命名管道(FIFO),然后从这个管道中读取数据。

#include <sys/types.h>  // 包含基本的系统类型定义
#include <sys/stat.h>   // 包含文件和文件系统相关的系统级操作
#include <cstdio>        // 包含标准输入输出流的定义
#include <errno.h>      // 包含错误号定义
#include <fcntl.h>      // 包含文件控制选项的定义
#include <unistd.h>     // 包含UNIX标准函数,如close()

int main() {
    // 创建一个命名管道,mode设置为0600,表示只有所有者有读写权限
    if(mkfifo("./file", 0600) == -1 && errno != EEXIST) {
        printf("mkfifo failed\n");  // 如果创建失败且不是因为管道已存在
        perror("why");             // 打印错误原因
    }
  
    int fd = open("./file", O_RDONLY);  // 以只读方式打开命名管道
    if(fd == -1) {
        perror("open failed");        // 如果打开失败,打印错误原因
        return 1;                    // 退出程序
    }
    printf("open succeed\n");          // 成功打开,打印提示信息

    char buf[30] = {0};               // 创建一个缓冲区用于存储读取的数据
    ssize_t n_read = 0;               // 用于存储每次读取的字节数

    while(1) {                        // 无限循环,持续读取
        n_read = read(fd, buf, 20);  // 尝试从文件描述符fd读取最多20个字节到缓冲区buf
        if(n_read == -1) {            // 如果读取失败
            perror("read failed");    // 打印错误原因
            close(fd); // 确保读取失败时关闭文件描述符
            return 1;
        } else if(n_read == 0) {
            printf("FIFO closed by the writer\n");
            break;
        }
            buf[n_read] = '\0'; // 添加字符串结束符
            printf("read:%zd, byte:%s\n", n_read, buf);  // 打印读取的字节数和内容
     
    }
    close(fd);                       // 关闭文件描述符

    return 0;                       // 正常退出程序
}

接下来是 write4z.c ​文件,这个程序的目的是向先前创建的命名管道中写入数据。

#include <sys/types.h>  // 包含基本的系统类型定义
#include <sys/stat.h>   // 包含文件和文件系统相关的系统级操作
#include <cstdio>        // 包含标准输入输出流的定义
#include <errno.h>      // 包含错误号定义
#include <fcntl.h>      // 包含文件控制选项的定义
#include <unistd.h>     // 包含UNIX标准函数,如write()和close()
#include <cstring>      // 包含字符串操作函数,如strlen()

int main() {
    char *str = "message from fifo";  // 要写入管道的消息
    int cnt = 0;                      // 计数器,用于控制写入次数

    int fd = open("./file", O_WRONLY);  // 以只写方式打开命名管道
    if(fd == -1) {
        perror("open failed");         // 如果打开失败,打印错误原因
        return 1;                     // 退出程序
    }
    printf("write open succeed\n");    // 成功打开,打印提示信息

    while(1) {
        ssize_t n_written = write(fd, str, strlen(str));
        if(n_written == -1) {
            perror("write failed");
            close(fd); // 确保写入失败时关闭文件描述符
            return 1;
        }
        if(n_written < strlen(str)) {
            printf("Buffer not large enough to write the whole string\n");
            break;
        }
        sleep(1);
        cnt++; // 增加计数器
        if(cnt == 5) {
            break;
        }
    }
    close(fd);                        // 关闭文件描述符

    return 0;                        // 正常退出程序
}

这两个程序共同演示了 Linux系统中命名管道(FIFO)的使用。read4.c ​创建并打开一个 FIFO 用于读取,而 write4z.c ​打开同一个 FIFO 用于写入。read4.c ​中的循环会持续读取 FIFO 中的数据,直到程序读取 5 次中断退出。write4z.c ​中的循环会写入固定的消息到 FIFO 中,并且每写入一次就暂停一秒,5 次后退出

在这里插入图片描述

在这里插入图片描述
要使这两个程序协同工作,首先需要运行 read4.c ​程序,然后运行 write4z.c ​程序。这样,write4z.c ​程序就可以向 FIFO 中写入数据,而 read4.c ​程序则可以从 FIFO 中读取这些数据。需要注意的是,FIFO 是一种进程间通信的方式,通常在不同的进程中运行读取和写入操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值