二、Linux系统编程:进程间的通信(IPC)之管道

2 管道

管道是一个进程连接数据流到另一个进程的通道,它通常用作把一个进程的输出通过管道连接到另一个进程的输入。

2.1 简介

查看管道命令:man 7 pipe

  • 分类:
    匿名管道(仅用于父子进程之间的通信)
    FIFO管道/命名管道(可用于父子或两不相关进程之间的通信)

2.2 匿名管道

2.2.1 单工管道

程序进程与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和命令两个进程,从命令进程中读/写文件流。
    解决execsystem无法返回输出数据问题
  • 特点
    方便使用系统自带功能,并且可以执行比较复杂Shell
    默认启动两个进程,效率较低。
操作管道文件
打开popen()fopen()
关闭pclose()fclose()

实例1:sendrecv实现通信
send.cpp

#include <iostream>
using 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
#include <iostream>
using 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 <unistd.h>
#include <cstdio>
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:ab12

2.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文件描述符
cmdF_GETFL:获取文件描述符状态;F_SETFL:设置文件描述符状态;
argO_NONBLOCK:非阻塞;O_BLOCK:阻塞

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

5、关闭管道close(filedes)

实例:父子进程采用半双工管道实现读写

#include <unistd.h>
#include <iostream>
#include <fcntl.h>
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 <unistd.h>
#include <iostream>
#include <fcntl.h>
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' <<fds[1] << endl;
    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};
        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:abc

2.3 FIFO管道/命名管道

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

  • 参数
参数含义
pathname文件路径,文件必须不存在
mode模式
  • 返回值
返回含义
0成功
非零失败
  • 特点
    可以是非亲缘进程之间
    FIFO首先会阻塞在open(),等待读写文件的文件描述符都打开。接着阻塞在read()/write()操作,读写操作需要同时执行。

实例1:通过FIFO管道实现读写
创建管道:createfifo.cpp

#include <sys/types.h>
#include <cstdio>
#include <sys/stat.h>
using namespace std;
int main(int argc,char** argv){
    if(-1 == mkfifo(argv[1],0666)){
    	 perror("fifo error");
    }
}

写入终端:writefifo.cpp

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
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 <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
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 <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
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 <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
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 <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
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
world

2.4 通信分类

只写单工
在这里插入图片描述
只读单工
在这里插入图片描述
半双工
在这里插入图片描述
全双工
在这里插入图片描述

类型创建/打开关闭
单工popen()pclosefread()fwrite()
半双工pipe()/open()close()read()write()
FIFO半双工mkfifo()/openclose()/unlink()read()write()
全双工socketpair()close()read()write()

2.4.1 单进程管道

管道通常用于进程间通信
在这里插入图片描述

2.4.2 父子进程单向管道

1、概念图解

父子进程管道

在这里插入图片描述
父进程关闭fd[0] 子进程关闭fd[1]

在这里插入图片描述

父子进程单向管道
在这里插入图片描述
2、原理图解
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
3为读端,4为写端

2.4.3 父子进程双向管道

在这里插入图片描述

2.5 文件描述符

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

文件描述符

文件描述符文件流
数据int整数FILE指针
标准POSIXANSIC
打开openfopen
关闭closefclose
readfread
writefwrite
定位lseekfseek

文件流是文件描述符之上的封装。文件流通过增加缓冲区减少读写系统调用次数来提高读写效率。在进程的用户空间封装的FILE结构,以提高可移植性和效率。

2.5.2 文件描述符复制

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

分类文件描述符文件号
标准输入STDIN_FILENO0
标准输出STDOUT_FILENO1
标准出错信息STDERR_FILENO2

实例:

#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));
}
[root@localhost pipe]# ./a.out 
hello
hello
world
world

2.5.3 文件描述符原理

Linux内核使用三个关联的数据结构,表示打开的文件。
在这里插入图片描述
在这里插入图片描述
打开不同的文件
在这里插入图片描述打开相同的文件
在这里插入图片描述
父子进程共享文件
在这里插入图片描述

2.5.4 lsof命令

  • 列名含义
列名含义
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 <unistd.h>
#include <iostream>
#include <fcntl.h>
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 6513lsof -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节中的原理分析

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值