Linux系统编程2:管道

图片来自网络

1. 简介

  • 查看管道命令:man 7 pipe

分类

  • 匿名管道
  • FIFO管道/命名管道

2 匿名管道

2.1 单工管道

程序进程与Shell命令行进程单项通信。

① 打开管道FILE* popen (const char *command, const char *open_mode)

  • 参数
No. 参数 含义
1 command 命令行字符串
2 open_mode "r"只读"w"只写
  • 返回值
No. 返回值 含义
1 NULL 文件描述符
2 NULL 打开失败

② 读取size_t fread ( void *buffer, size_t size, size_t count, FILE *stream)

  • 参数
No. 参数 含义
1 buffer 用于接收数据的内存地址
2 size 读取每个数据项的字节数
3 count 数据项个数
4 stream 输入流
  • 返回值
No. 返回值 含义
1 >count 出错
2 正数 真实读取的数据项个数

③ 写入size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream)

  • 参数
No. 参数 含义
1 buffer 写入数据的内存地址
2 size 写入数据项的字节数
3 count 写入数据项的个数
4 stream 目标文件指针
  • 返回值
No. 返回值 含义
1 >count 出错
2 正数 真实写入的数据项个数

④ 关闭管道int pclose(FILE *stream);

  • 参数
No. 参数 含义
1 stream 文件描述符
  • 返回值
No. 返回值 含义
1 -1 失败
2 0 成功

示例

  • write
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main(){
    FILE* fd = popen("wc","w");
    //FILE* fd = popen("ls -l","r");
    //char str[] = "123 456";       
    char str[] = "123 456\n";       
    size_t n = fwrite(str,sizeof(char),sizeof(str),fd);
    if(n > sizeof(str)){
        fprintf(stderr,"FILE:%d,LINE:%d-fwrite error",__FILE__,__LINE__);
        exit(EXIT_FAILURE);
    }
    pclose(fd);
}
  • read
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main(){
    FILE* fd = popen("ps -ef","r");
    //FILE* fd = popen("ls -l","r");
         
    char buf[BUFSIZ];
    size_t count = 0;
    printf("read data:\n");
    do{
        memset(buf,'\0',BUFSIZ);
        size_t n = fread(buf,sizeof(char),BUFSIZ-1,fd);
        if( n > BUFSIZ - 1 ){
            perror("fread error");
            exit(EXIT_FAILURE);
        }
        count += n;
        printf("\n%d:\n%s",n,buf);
    }while(!feof(fd));
    printf("total size:%ld\n",count);
    pclose(fd);
}

本质

  • 启动shell和命令两个进程,从命令进程中读/写文件流。
  • 解决exec和system无法返回输出数据问题

特点

  • 方便使用系统自带功能,并且可以执行比较复杂Shell
  • 默认启动两个进程,效率较低。
操作 管道 文件
打开 popen() fopen()
关闭 pclose() fclose()

2.2 半双工管道

① 创建管道int pipe(int filedes[2])

  • 参数
No. 参数 含义
1 filedes[0]
2 filedes[1]
  • 返回值
No. 返回值 含义
1 -1 失败
2 0 成功

② 读取ssize_t write(int fd, const void *buf, size_t nbyte)

  • 参数
No. 参数 含义
1 fd 文件描述符
2 buf 写入数据的内存单元
3 nbyte 写入文件指定的字节数
  • 返回值
No. 返回值 含义
1 -1 出错
2 正数 写入的字节数

③ 写入ssize_t read(int fd, void *buf, size_t count)

  • 参数
No. 参数 含义
1 fd 文件描述符
2 buf 读取数据的内存单元
  • 返回值
No. 返回值 含义
1 -1 出错
2 0 无数据
3 正数 读取的字节数

④ 控制int fcntl(int fd, int cmd, long arg)

如果管道是空的,read()默认是阻塞

  • 参数
No. 参数 含义
1 fd 文件描述符
2 cmd F_GETFL:获取文件描述符状态;F_SETFL:设置文件描述符状态;
3 arg O_NONBLOCK:非阻塞;O_BLOCK:阻塞

把文件描述符改为非阻塞的fcntl(filedes,F_SETFL,O_NONBLOCK);

⑤ 关闭管道close(filedes)

示例

  • 单进程读写管道
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main(){
    int fd[2];
    pipe(fd);
    char in[] = "Hello pipe";
    write(fd[1],in,sizeof(in));
    printf("write:%s\n",in);
 
    char out[sizeof(in)]={0};
    ssize_t n = read(fd[0],out,sizeof(out));
    if(-1 == n){
        perror("read error");
        return -1;
    }
    printf("read:%s\n",out);
 
    close(fd[0]);
    close(fd[1]);
}
  • 两进程读写
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main(){
    int fd[2];
    pipe(fd);
    if(!fork()){// child
        char in[] = "Hello pipe";
        write(fd[1],in,sizeof(in));
        printf("child %d write:%s\n",getpid(),in);
    }else{// parent
        char out[BUFSIZ]={0};
        ssize_t n = read(fd[0],out,sizeof(out));
        if(-1 == n){
            perror("read error");
            return -1;
        }
        printf("parent %d read:%s\n",getpid(),out);
    }
    close(fd[0]);
    close(fd[1]);
}
  • 两进程先读后写
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main(){
    int fd[2];
    pipe(fd);
    if(!fork()){// child
        char in[] = "Hello pipe";
        sleep(3);
        write(fd[1],in,sizeof(in));
        printf("child %d write:%s\n",getpid(),in);
    }else{// parent
        char out[BUFSIZ]={0};
        ssize_t n = read(fd[0],out,sizeof(out));
        if(-1 == n){
            perror("read error");
            return -1;
        }
        printf("parent %d read:%s\n",getpid(),out);
    }
    close(fd[0]);
    close(fd[1]);
}
  • 两进程先读后写(非阻塞)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
 
int main(){
    int fd[2];
    pipe(fd);
    if(!fork()){// child
        char in[] = "Hello pipe";
        sleep(3);
        write(fd[1],in,sizeof(in));
        printf("child %d write:%s\n",getpid(),in);
    }else{// parent
        fcntl(fd[0],F_SETFL,O_NONBLOCK);
        char out[BUFSIZ]={0};
        ssize_t n = read(fd[0],out,sizeof(out));
        if(-1 == n){
            perror("read error");
            return -1;
        }
        printf("parent %d read:%s\n",getpid(),out);
    }
    close(fd[0]);
    close(fd[1]);
}
  • 更加规范的写法(关闭不需要的管道)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
 
int main(){
    int fd[2];
    pipe(fd);
    if(!fork()){// child
        close(fd[0]);
        char in[] = "Hello pipe";
        sleep(3);
        write(fd[1],in,sizeof(in));
        printf("child %d write:%s\n",getpid(),in);
        close(fd[1]);
    }else{// parent
        close(fd[1]);
        fcntl(fd[0],F_SETFL,O_NONBLOCK);
        char out[BUFSIZ]={0};
        ssize_t n = read(fd[0],out,sizeof(out));
        if(-1 == n){
            perror("read error");
            return -1;
        }
        printf("parent %d read:%s\n",getpid(),out);
        close(fd[0]);
    }
}

2.3 FIFO管道/命名管道

① 创建命名管道int mkfifo(pathname,mode)

  • 参数
No. 参数 含义
1 pathname 文件路径,文件必须不存在
2 mode 模式
  • 返回值
No. 返回值 含义
1 0 成功
2 非零 失败
  • 示例
#include <stdio.h>
#include <unistd.h>

int main(){
    if(-1 == mkfifo("/tmp/test",0644)){
        perror("mkfifo error");
        return 1;
    }
}

注意:

  1. 管道文件通常在/tmp目录下创建。
  2. 管道文件大小通常是0

② 打开FIFO文件int open(const char *path, int mode)

  • 参数
No. 参数 含义
1 pathname 文件路径
2 mode 模式
  • 模式
No. 模式 含义
1 O_RDONLY 阻塞只读
2 O_RDONLY | O_NONBLOCK 非阻塞只读
3 O_WRONLY 阻塞只写
4 O_WRONLY | O_NONBLOCK 非阻塞只写
  • 返回值
No. 返回值 含义
1 -1 失败
2 其他 文件描述符

示例

  1. 阻塞读
#include <iostream>
#include <unistd.h>
#include <fcntl.h> // open() O_RDONLY
using namespace std;
#define BUFSIZE 258

int main(){
    // 打开命名管道
    int fd = open("/tmp/fifo",O_RDONLY);
    if(-1 == fd){
        perror("open error");
        return EXIT_FAILURE;
    }else{
        cout << "filo open ok" << endl;
    }
    
    // 读取字符串
    char buffer[BUFSIZE] = {0};
    read(fd,buffer,BUFSIZE);
    
    // 打印读取的字符串
    cout << buffer << endl;
    
    // 关闭管道
    close(fd);
    return EXIT_SUCCESS;
}
  • 堵塞写
#include <iostream>
#include <fcntl.h> // open() O_WRONLY
#include <unistd.h>
using namespace std;

int main(){
    // 打开命名管道
    int fd = open("/tmp/fifo",O_WRONLY);
    if(-1 == fd){
        perror("open error");
        return EXIT_FAILURE;
    }else{
        cout << "filo open ok" << endl;
    }
    
    // 读取字符串
    string str;
    getline(cin,str);
    
    // 写入管道
    write(fd,str.c_str(),str.size()+1);
    
    // 关闭管道
    close(fd);
    return EXIT_SUCCESS;
}
  • 写非阻塞
    说明:
    1. 只需要在open()添加O_NONBLOCK
    2. open()非阻塞,读open()阻塞的情况。读open()需要先执行,否则,写open()会出现No such device or address
#include <iostream>
#include <fcntl.h> // open() O_WRONLY O_NONBLOCK
#include <unistd.h>
using namespace std;

int main(){
    // 打开命名管道
    int fd = open("/tmp/fifo",O_WRONLY|O_NONBLOCK);
    if(-1 == fd){
        perror("open error");
        return EXIT_FAILURE;
    }else{
        cout << "filo open ok" << endl;
    }
    
    // 读取字符串
    string str;
    getline(cin,str);
    
    // 写入管道
    write(fd,str.c_str(),str.size()+1);
    
    // 关闭管道
    close(fd);
    return EXIT_SUCCESS;
}
  • 读非阻塞
    说明:
    1. 只需要在open()添加O_NONBLOCK
    2. open()阻塞,读open()非阻塞的情况。读read()需要处理写open()未执行(read()返回0)和读不到数据(写open()打开但是没有写数据,read()返回-1)的情况。
#include <iostream>
#include <unistd.h>
#include <fcntl.h> // open() O_RDONLY O_NONBLOCK
using namespace std;
#define BUFSIZE 258

int main(){
    // 打开命名管道
    int fd = open("/tmp/fifo",O_RDONLY|O_NONBLOCK);
    if(-1 == fd){
        perror("open error");
        return EXIT_FAILURE;
    }else{
        cout << "filo open ok" << endl;
    }
    
    // 读取字符串
    char buffer[BUFSIZE] = {0};
    int n = -1;
    while(n<=0){
        n=read(fd,buffer,BUFSIZE);
    }
    // 打印读取的字符串
    cout << buffer << endl;
    
    // 关闭管道
    close(fd);
    return EXIT_SUCCESS;
}

特点

  • 可以是非亲缘进程之间
  • FIFO首先会阻塞在open(),等待读写文件的文件描述符都打开。接着阻塞在read()/write()操作,读写操作需要同时执行。

案例

  • FIFO工具箱

3. 通信分类

只写单工
只读单工
半双工
全双工
No. 类型 创建/打开 关闭
1 单工 popen() pclose fread() fwrite()
2 半双工 pipe()/open() close() read() write()
3 FIFO半双工 mkfifo()/open close()/unlink() read() write()
4 全双工 socketpair() close() read() write()

3.1 单进程管道

单进程管道

管道通常用于进程间通信

3.2 父子进程单向管道

3.3.1 概念图解

父子进程管道
父进程关闭fd[0] 子进程关闭fd[1]
父子进程单向管道

3.3.2 原理图解

3.3 父子进程双向管道

父子进程双向管道

4. 文件描述符

4.1 Linux文件读写与标准C的文件读写

文件描述符

文件描述符 文件流
数据 int整数 FILE指针
标准 POSIX ANSI C
打开 open fopen
关闭 close fclose
read fread
write fwrite
定位 lseek fseek
  • 文件流是文件描述符之上的封装。文件流通过增加缓冲区减少读写系统调用次数来提高读写效率。在进程的用户空间封装的FILE结构,以提高可移植性和效率。

4.2 文件描述符原理

Linux内核使用三个关联的数据结构,表示打开的文件。

  • 打开不同的文件


  • 打开相同的文件


  • 父子进程共享文件


4.3 命令lsof

lsof(list open files):列出当前系统打开文件

  • 列名含义
No. 列名 含义
1 COMMAND 进程的名称
2 PID 进程标识符
3 USER 进程所有者
4 FD 文件描述符,应用程序通过文件描述符识别该文件。如cwd、txt等
5 TYPE 文件类型,如PIPEDIRREG
6 DEVICE 指定磁盘的名称
7 SIZE 文件的大小
8 NODE 索引节点(文件在磁盘上的标识)
9 NAME 打开文件的确切名称
  • 常用方法
No. 命令 作用
1 lsof 文件名 查看文件打开信息
2 lsof -d 文件描述符 查看文件描述符信息
3 lsof -p PID 查看进程PID打开的文件信息

4.3 文件描述符复制

分类 文件描述符 文件号
标准输入 STDIN_FILENO 0
标准输出 STDOUT_FILENO 1
标准出错信息 STDERR_FILENO 2

内核为每个进程创建的文件描述符。

  • 示例
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main(){
    char str[1024];
    scanf("%s",str);
    printf("%s\n",str);

    read(STDIN_FILENO,str,sizeof(str));
    write(STDOUT_FILENO,str,strlen(str));
}

① 函数int dup(int oldfd)

  • 参数
No. 参数 含义
1 oldfd 旧文件描述符
  • 返回值
No. 返回值 含义
1 -1 失败
2 其他 新文件描述符

② 函数int dup2(int oldfd, int newfd)

  • 参数
No. 参数 含义
1 oldfd 旧文件描述符
2 newfd 新文件描述符
  • 返回值
No. 返回值 含义
1 -1 失败
2 其他 最小及尚未使用的文件描述符

示例

  • 复制标准输出
    新文件描述符与旧文件描述符不同,但是具备旧文件描述符功能
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
 
int main(){
    int fd = dup(STDOUT_FILENO);
    fprintf(fdopen(fd,"w"),"%d printf:Hello dup\n",fd);
}
  • 复制文件描述符
    新文件描述符与旧文件描述符不同,但是具备旧文件描述符功能
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#define FILE_MODE (S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH)
 
int main(){
    int fd = open("./test",O_CREAT|O_RDWR,FILE_MODE); 
    char str[]="Hello dup\n";
    write(fd,str,sizeof(str));
    int cp_fd = dup(fd);
    printf("copy %d to %d",fd,cp_fd);
    write(cp_fd,str,sizeof(str));
    //fprintf(fdopen(fd,"w"),"%d printf:Hello dup\n",fd);
    close(fd);
}
  • 把文件描述符重定向(复制)到标准输出
    printf()直接输出到文件中,不再输出到终端
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
 
#define FILE_MODE (S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH)
 
int main(){
    int fd = open("./test",O_CREAT|O_RDWR,FILE_MODE); 
    char str[]="Hello dup\n";
    dup2(fd,STDOUT_FILENO);
    printf("%d printf:Hello dup\n",fd);
}
  • 把文件描述符重定向(复制)到标准输出,并且输出后还原
    注意把标准输出流从文件重定向(复制)回终端,需要清除缓冲区。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#define FILE_MODE (S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH)
int main(){
    int save_fd = dup(STDOUT_FILENO);
    int fd = open("./test",O_CREAT|O_RDWR,FILE_MODE); 
    if(-1 == dup2(fd,STDOUT_FILENO)){
        perror("dup2 error0");
        return 1;
    }
    close(fd);
    printf("%d printf:Hello dup\n",fd);
    fflush(stdout);// 一定要清除缓冲区,否则会输出到终端
    if(-1 == dup2(save_fd,STDOUT_FILENO)){
        perror("dup2 error");
        return 1;
    }
    close(save_fd);
    printf("%d printf:this is save\n",save_fd);
}

特点

必须是亲缘进程之间

dup()/dup2()原理图

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容