查看管道命令:man 7 pipe
- 分类:
匿名管道(仅用于父子进程之间的通信)
FIFO管道/命名管道(可用于父子或两不相关进程之间的通信)
程序进程与Shell命令行进程单项通信。
1、打开管道FILE* popen (const char *command, const char *open_mode)
- 参数
| 参数 | 含义 |
|---|---|
| command | 命令行字符串 |
| open_mode | "r"只读"w"只写 |
- 返回值
| 返回值 | 含义 |
|---|---|
| NULL | 文件描述符 |
| 非NULL | 打开失败 |
2、读取size_t fread ( void *buffer, size_t size, size_t count, FILE *stream)
| 参数 | 含义 |
|---|---|
| buffer | 用于接收数据的内存地址 |
| size | 读取每个数据项的字节数 |
| count | 数据项个数 |
| stream | 输入流 |
- 返回值
| 返回值 | 含义 |
|---|---|
| >count | 出错 |
| 正数 | 真实读取的数据项个数 |
3、写入size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream)
| 参数 | 含义 |
|---|---|
| buffer | 写入数据的内存地址 |
| size | 写入数据项的字节数 |
| count | 写入数据项的字节数 |
| stream | 目标文件指针 |
- 返回值
| 返回值 | 含义 |
|---|---|
| >count | 出错 |
| 正数 | 真实写入的数据项个数 |
4、 关闭管道int pclose(FILE *stream);
- 本质
启动shell和命令两个进程,从命令进程中读/写文件流。
解决exec和system无法返回输出数据问题 - 特点
方便使用系统自带功能,并且可以执行比较复杂Shell
默认启动两个进程,效率较低。
| 操作 | 管道 | 文件 |
|---|---|---|
| 打开 | popen() | fopen() |
| 关闭 | pclose() | fclose() |
实例1:send和recv实现通信
send.cpp
#includeusing namespace std; int main(int argc,char* argv[]){ for(int i = 1;i < argc;i++){ cout << argv[i] << endl; } }
recv.cpp
//receive与send交互:./send hello world | ./rece #includeusing namespace std; int main(){ string s; string t; while(cin >> s){ t+=s; } cout << "receive:" << t << endl; }
执行./send hello world | ./rece
[root@localhost pipe]# ./send 123 abc | ./recv receive:123abc
实力2:加入mypipe.cpp
// 执行:./a.out "./send ab 12" "./recv" #include#include int main(int argc,char** argv){ const char* readcmd = argv[1]; const char* writecmd = argv[2]; char buffer[1024] = {0}; FILE* readfile = popen(readcmd,"r"); fread(buffer,1,1024,readfile); pclose(readfile); readfile = NULL; FILE* writefile = popen(writecmd,"w"); fwrite(buffer,1,1024,writefile); pclose(writefile); writefile = NULL; }
[root@localhost pipe]# ./a.out "./send ab 12" "./recv" receive:ab122.2.2 半双工管道
1、创建管道int pipe(int filedes[2])
- 参数
| 参数 | 含义 |
|---|---|
| filedes[0] | 读 |
| filedes[1] | 写 |
- 返回值
| 返回值 | 含义 |
|---|---|
| -1 | 失败 |
| 0 | 成功 |
2、读取ssize_t write(int fd, const void *buf, size_t nbyte)
- 参数
| 参数 | 含义 |
|---|---|
| fd | 文件描述符 |
| buf | 写入数据的内存单元 |
| nbyte | 写入文件指定的字节数 |
- 返回值
| 返回值 | 含义 |
|---|---|
| -1 | 出错 |
| 正数 | 写入的字节数 |
3、写入ssize_t read(int fd, void *buf, size_t count)
- 参数
| 参数 | 含义 |
|---|---|
| fd | 文件描述符 |
| buf | 读取数据的内存单元 |
- 返回值
| 返回值 | 含义 |
|---|---|
| -1 | 出错 |
| 0 | 无数据 |
| 正数 | 读取的字节数 |
4、控制int fcntl(int fd, int cmd, long arg)
如果管道是空的,read()默认是阻塞
- 参数
| 参数 | 含义 |
|---|---|
| fd | 文件描述符 |
| cmd | F_GETFL:获取文件描述符状态;F_SETFL:设置文件描述符状态; |
| arg | O_NONBLOCK:非阻塞;O_BLOCK:阻塞 |
把文件描述符改为非阻塞的fcntl(filedes,F_SETFL,O_NONBLOCK);
5、关闭管道close(filedes)
实例:父子进程采用半双工管道实现读写
#include#include #include using namespace std; // fds为半双工 int main(){ int fds[2]; //fds[0](读) fds[1](写) /// file fds[0] = open(file,"r"); /// file fds[1] = open(file,"w"); pipe(fds); if(fork()){ close(fds[0]); //不用的先关掉 cout << getpid() << " send msg:"; string msg; cin >> msg; // block阻塞 write(fds[1],msg.c_str(),msg.size()+1); close(fds[1]); }else{ //fcntl(fds[0],F_SETFL,O_NONBLOCK); //禁用阻塞 close(fds[1]); char buffer[1024] = {0}; read(fds[0],buffer,1024); // block阻塞,往终端写入数据后才执行从终端读的操作 cout << getpid() << " recv msg:" << buffer << endl; close(fds[0]); } }
[root@localhost pipe]# ./a.out 22557 send msg:abc 22558 recv msg:abc
实力2:添加阻塞控制输入等的时间,如果不添加阻塞就会一直等
#include#include #include using namespace std; // fds为半双工 int main() { int fds[2]; //fds[0](读) fds[1](写) /// file fds[0] = open(file,"r"); /// file fds[1] = open(file,"w"); pipe(fds); cout << fds[0] << 't' < > msg; // block阻塞 write(fds[1],msg.c_str(),msg.size()+1); close(fds[1]); } else { fcntl(fds[0],F_SETFL,O_NONBLOCK); //禁用阻塞 close(fds[1]); char buffer[1024] = {0}; int count = 5; //添加延迟 while(-1 == read(fds[0],buffer,1024)) { sleep(1); if(--count == 0) break; } if(count == 0) { cout << "recv data timeout 5s" << endl; } else { cout << getpid() << " recv msg:" << buffer << endl; } close(fds[0]); } }
不做任何输入,超时自动结束。
[root@localhost pipe]# g++ fork_pipe2.cpp [root@localhost pipe]# ./a.out 3 4 22747 send msg:recv data timeout 5s q
若在5s内输入,可以正常输出到终端
[root@localhost pipe]# ./a.out 3 4 22818 send msg:abc [root@localhost pipe]# 22819 recv msg:abc2.3 FIFO管道/命名管道
1、创建命名管道int mkfifo(pathname,mode)
- 参数
| 参数 | 含义 |
|---|---|
| pathname | 文件路径,文件必须不存在 |
| mode | 模式 |
- 返回值
| 返回 | 含义 |
|---|---|
| 0 | 成功 |
| 非零 | 失败 |
- 特点
可以是非亲缘进程之间
FIFO首先会阻塞在open(),等待读写文件的文件描述符都打开。接着阻塞在read()/write()操作,读写操作需要同时执行。
实例1:通过FIFO管道实现读写
创建管道:createfifo.cpp
#include#include #include using namespace std; int main(int argc,char** argv){ if(-1 == mkfifo(argv[1],0666)){ perror("fifo error"); } }
写入终端:writefifo.cpp
#include#include #include #include #include using namespace std; int main(int argc,char** argv){ int fd = open(argv[1],O_WRONLY); string s; cin >> s; write(fd,s.c_str(),s.size()+1); close(fd); }
从终端读出:readfifo.cpp
#include#include #include #include #include using namespace std; int main(int argc,char* argv[]){ int fd = open(argv[1],O_RDONLY); char buffer[1024] = {0}; read(fd,buffer,sizeof(buffer)); cout << getpid() << " recv:" << buffer << endl; close(fd); }
执行过程:
执行 ./writefifo ./fifofile
另一shell执行 ./readfifo ./fifofile
[root@localhost fifo]# ./writefifo ./createfifo hello ... ... [root@localhost fifo]# ./readfifo ./createfifo 3632 recv:hello
实例2:通过FIFO管道实现读写(加入阻塞)
- 写非阻塞
1、只需要在open()添加O_NONBLOCK。
2、写open()非阻塞,读open()阻塞的情况。读open()需要先执行,否则,写open()会出现No such device or address。
#include#include #include #include #include using namespace std; //读的程序若在后台执行,写入执行后会一直等待输入,否则等待5s不写入自动结束 int main(int argc,char** argv){ int count = 5; int fd = -1; while(--count){ fd = open(argv[1],O_WRONLY|O_NONBLOCK); if(-1 != fd) break; sleep(1); } if(0 == count){ cout << "open fifo timeout 5s" << endl; }else{ string s; cin >> s; write(fd,s.c_str(),s.size()+1); } close(fd); }
- 读非阻塞
1、只需要在open()添加O_NONBLOCK。
2、写open()阻塞,读open()非阻塞的情况。读read()需要处理写open()未执行(read()返回0)和读不到数据(写open()打开但是没有写数据,read()返回-1)的情况。
#include#include #include #include #include using namespace std; int main(int argc,char* argv[]){ int fd = open(argv[1],O_RDONLY); cout << fd << endl; if(-1 == fd){ perror("open fifo error"); return 1; } char buffer[1024] = {0}; int count = 15; int res = read(fd,buffer,sizeof(buffer)); while(0 == res){ sleep(1); if(--count == 0) break; res = read(fd,buffer,sizeof(buffer)); } cout << res << endl; if(count == 0){ cout << "recv timeout 15s" << endl; }else{ cout << getpid() << " recv:" << buffer << endl; } close(fd); }
执行步骤:
1、执行./createfifo fifo创造一个名为fifo的管道
2、./writefifo2 fifo 往管道里写
3、./readfifo2 fifo 从管道里读
写为非阻塞,读为阻塞。
注:读的程序若在后台执行,写入执行后会一直等待输入,否则等待5s不写入自动结束
(1)若读未打开,则写超时5s自动退出
[root@localhost fifo]# ./writefifo2 fifo open fifo timeout 5s
(2)若写打开未输入退出,则读15s自动退出
[root@localhost fifo]# ./writefifo2 fifo ^C ... .... [root@localhost fifo]# 3 0 recv timeout 15s
(3)写和读都打开,读一直等待写,写也不会退出,直到写入并读出。
[root@localhost fifo]# ./writefifo2 fifo jkl ... ... [root@localhost fifo]# ./readfifo2 fifo 3 4 5187 recv:jkl
实例3:父子进程通过两个管道完成双向读写
读写不能同时阻塞
#include#include #include #include using namespace std; int main(int argc,char* argv[]) { int wfd = -1,rfd = -1; while(-1 == wfd) { wfd = open(argv[2],O_WRONLY|O_NONBLOCK); rfd = open(argv[1],O_RDONLY); } if(-1 == wfd || -1 == rfd) { perror("open fifo error"); return 1; } if(fork()) { for(;;) { string s; cin >> s; write(wfd,s.c_str(),s.size()+1); } } else { for(;;) { char buffer[1024] = {0}; int res = read(rfd,buffer,sizeof(buffer)); cout << getpid() << " recv:" << buffer << endl; } } close(wfd); close(rfd); }
步骤:
(1)./createfifo a ./createfifo b创造两个管道
(2)./talker a b 打开另一shell执行./talker b a
(3)实现双向通信
[root@localhost fifo]# ./talker b a hello 5778 recv:world
[root@localhost fifo]# ./talker a b 5779 recv:hello world2.4 通信分类
只写单工
只读单工
半双工
全双工
| 类型 | 创建/打开 | 关闭 | 读 | 写 |
|---|---|---|---|---|
| 单工 | popen() | pclose | fread() | fwrite() |
| 半双工 | pipe()/open() | close() | read() | write() |
| FIFO半双工 | mkfifo()/open | close()/unlink() | read() | write() |
| 全双工 | socketpair() | close() | read() | write() |
管道通常用于进程间通信
1、概念图解
父子进程管道
父进程关闭fd[0] 子进程关闭fd[1]
父子进程单向管道
2、原理图解
3为读端,4为写端
文件描述符
| 文件描述符 | 文件流 | |
|---|---|---|
| 数据 | int整数 | FILE指针 |
| 标准 | POSIX | ANSIC |
| 打开 | open | fopen |
| 关闭 | close | fclose |
| 读 | read | fread |
| 写 | write | fwrite |
| 定位 | lseek | fseek |
文件流是文件描述符之上的封装。文件流通过增加缓冲区减少读写系统调用次数来提高读写效率。在进程的用户空间封装的FILE结构,以提高可移植性和效率。
2.5.2 文件描述符复制内核为每个进程创建的文件描述符。
| 分类 | 文件描述符 | 文件号 |
|---|---|---|
| 标准输入 | STDIN_FILENO | 0 |
| 标准输出 | STDOUT_FILENO | 1 |
| 标准出错信息 | STDERR_FILENO | 2 |
实例:
#include#include #include int main(){ char str[1024]; scanf("%s",str); printf("%sn",str); read(STDIN_FILENO,str,sizeof(str)); write(STDOUT_FILENO,str,strlen(str)); }
[root@localhost pipe]# ./a.out hello hello world world2.5.3 文件描述符原理
Linux内核使用三个关联的数据结构,表示打开的文件。
打开不同的文件
打开相同的文件
父子进程共享文件
- 列名含义
| 列名 | 含义 |
|---|---|
| COMMAND | 进程的名称 |
| PID | 进程标识符 |
| USER | 进程所有者 |
| FD | 文件描述符,应用程序通过文件描述符识别该文件。如cwd、txt等 |
| TYPE | 文件类型,如PIPE、DIR、REG等 |
| DEVICE | 指定磁盘的名称 |
| SIZE | 文件的大小 |
| NODE | 索引节点(文件在磁盘上的标识) |
| NAME | 打开文件的确切名称 |
- 常用方法
| 命令 | 作用 |
|---|---|
| lsof 文件名 | 查看文件打开信息 |
| lsof -d 文件描述符 | 查看文件描述符信息 |
| lsof -p PID | 查看进程PID打开的文件信息 |
实例1:fork_pipe.cpp
#include#include #include using namespace std; // fds为半双工 int main(){ int fds[2]; //fds[0](读) fds[1](写) // file fds[0] = open(file,"r"); // file fds[1] = open(file,"w"); pipe(fds); if(fork()){ close(fds[0]); //不用的先关掉 cout << getpid() << " send msg:"; string msg; cin >> msg; // block阻塞 write(fds[1],msg.c_str(),msg.size()+1); close(fds[1]); }else{ //fcntl(fds[0],F_SETFL,O_NONBLOCK); //禁用阻塞 close(fds[1]); char buffer[1024] = {0}; read(fds[0],buffer,1024); // block阻塞,往终端写入数据后才执行从终端读的操作 cout << getpid() << " recv msg:" << buffer << endl; close(fds[0]); } }
执行ps -ef查看进程,可以看到两个a.out,即父子进程
执行lsof -p 6513和lsof -p 6514查看父子进程的文件描述符信息
root 6513 3032 0 18:44 pts/0 00:00:00 ./a.out root 6514 6513 0 18:44 pts/0 00:00:00 ./a.out root 6515 3422 0 18:44 pts/1 00:00:00 ps -ef [root@localhost fifo]# lsof -p 6513 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME a.out 6513 root cwd DIR 8,3 198 83106692 /root/Desktop/2022/internet/2/pipe a.out 6513 root rtd DIR 8,3 247 128 / a.out 6513 root txt REG 8,3 17056 83106698 /root/Desktop/2022/internet/2/pipe/a.out a.out 6513 root mem REG 8,3 3201344 100727036 /usr/lib64/libc-2.28.so a.out 6513 root mem REG 8,3 104248 100663439 /usr/lib64/libgcc_s-8-20180905.so.1 a.out 6513 root mem REG 8,3 2303696 100733760 /usr/lib64/libm-2.28.so a.out 6513 root mem REG 8,3 1661496 100735443 /usr/lib64/libstdc++.so.6.0.25 a.out 6513 root mem REG 8,3 243520 100727029 /usr/lib64/ld-2.28.so a.out 6513 root 0u CHR 136,0 0t0 3 /dev/pts/0 a.out 6513 root 1u CHR 136,0 0t0 3 /dev/pts/0 a.out 6513 root 2u CHR 136,0 0t0 3 /dev/pts/0 a.out 6513 root 4w FIFO 0,12 0t0 75877 pipe [root@localhost fifo]# lsof -p 6514 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME a.out 6514 root cwd DIR 8,3 198 83106692 /root/Desktop/2022/internet/2/pipe a.out 6514 root rtd DIR 8,3 247 128 / a.out 6514 root txt REG 8,3 17056 83106698 /root/Desktop/2022/internet/2/pipe/a.out a.out 6514 root mem REG 8,3 3201344 100727036 /usr/lib64/libc-2.28.so a.out 6514 root mem REG 8,3 104248 100663439 /usr/lib64/libgcc_s-8-20180905.so.1 a.out 6514 root mem REG 8,3 2303696 100733760 /usr/lib64/libm-2.28.so a.out 6514 root mem REG 8,3 1661496 100735443 /usr/lib64/libstdc++.so.6.0.25 a.out 6514 root mem REG 8,3 243520 100727029 /usr/lib64/ld-2.28.so a.out 6514 root 0u CHR 136,0 0t0 3 /dev/pts/0 a.out 6514 root 1u CHR 136,0 0t0 3 /dev/pts/0 a.out 6514 root 2u CHR 136,0 0t0 3 /dev/pts/0 a.out 6514 root 3r FIFO 0,12 0t0 75877 pipe
3为r(读),4为w(写),对应2.4.2节中的原理分析



