1. 简介
分类
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 |
-1 |
失败 |
2 |
0 |
成功 |
示例
#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);
}
#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;
}
}
注意:
- 管道文件通常在
/tmp
目录下创建。
- 管道文件大小通常是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 |
其他 |
文件描述符 |
示例
- 阻塞读
#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;
}
- 写非阻塞
说明:
- 只需要在
open()
添加O_NONBLOCK
。
- 写
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;
}
- 读非阻塞
说明:
- 只需要在
open()
添加O_NONBLOCK
。
- 写
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()
操作,读写操作需要同时执行。
案例
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 概念图解
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 |
文件类型,如PIPE 、DIR 、REG 等 |
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 |
-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()原理图