文章目录
前言:
Q:为什么需要进程间通信呢?
A:原因是由于进程拥有自己独立的进程虚拟地址空间,从而导致了进程的独立性,通过进程间的通信,可以让不同的进程之间进行协作。
1 管道符【|】的理解
ps aux | grep XXX
该命令的作用:是将当前正在运行的所有进程的进程名带有XXX的进程过滤打印出来
- ps和grep是两个不同的命令,同时也是两个不同的程序
- ps程序的执行结果,通过管道,传输给grep程序。
- 换言之,即ps程序的输出作为grep程序的输入
管道就是在内核中开辟一块内存,也叫缓冲区
2 匿名管道
匿名管道概念
匿名管道是程序在内核中开辟出来的一块没有标识符的内存空间
2.1 创建匿名管道:使用pipe接口
int pipe(int pipefd[2]);
参数:
- fd是一个输出型参数,在调用函数内部进行赋值并带出函数
- fd[2]:整形数组,有两个元素:fd[0] 和 fd[1]
返回值:0 代表成功,-1代表失败
头文件:unistd.h
程序演示:查看输出型参数fd从pipe函数中带出来了什么数据?
#include <stdio.h>
#include <unistd.h>
int main()
{
int fd[2];
int ret_pipe = pipe(fd);
if(ret_pipe < 0)
perror("pipe");
else
printf("fd[0] = %d , fd[1] = %d\n",fd[0],fd[1]);
while(1)
{
sleep(1);
}
return 0;
}
输出结果:
fd[0] = 3 , fd[1] = 4
查看该进程使用文件描述符情况
[gongruiyang@localhost ~]$ ll /proc/3456/fd/
总用量 0
lrwx------. 1 gongruiyang gongruiyang 64 1月 1 16:16 0 -> /dev/pts/0
lrwx------. 1 gongruiyang gongruiyang 64 1月 1 16:16 1 -> /dev/pts/0
lrwx------. 1 gongruiyang gongruiyang 64 1月 1 16:16 2 -> /dev/pts/0
lr-x------. 1 gongruiyang gongruiyang 64 1月 1 16:16 3 -> pipe:[38151]
l-wx------. 1 gongruiyang gongruiyang 64 1月 1 16:16 4 -> pipe:[38151]
其中3和4这两个文件描述符是软链接文件,指向内核中一块无标识符的内存
2.2 pipe参数fd数组再理解
-
fd数组当中保存两个文件描述符
-
fd[0]是缓冲区的读端,从匿名管道中读取数据
-
fd[1]是缓冲区的写端,向匿名管道中写入数据
-
读写是相对于缓冲区中数据的
2.3 程序演示:用匿名管道进行读写演示
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd[2];
int ret_pipe = pipe(fd);
if(ret_pipe < 0)
perror("pipe");
else
{
//向 匿名管道 中写入w_buf中的数据
char* w_buf = "Hello World!";
write(fd[1],w_buf,strlen(w_buf));
//从 匿名管道 中读取数据到r_buf中
char r_buf[1024];
read(fd[0],r_buf,sizeof(r_buf) - 1);
//输出读取出来的数据
puts(r_buf);
}
return 0;
}
Hello World!
2.4 管道特征
- 管道是单双工通信:数据只能从写端流向读端
- 管道提供流式服务:读端可以决定每次读取多少个字节,读走多少数据,管道中就减少多少数据
- 管道的生命周期跟随进程
2.5 验证管道通信方式是单双工通信方式
辅助接口
int fcntl(int fd, int cmd, ... /* arg */ );
功能:获取对某个文件的操作权限属性
参数:
- fd : 待查询文件描述符
- cmd
可选项 | 含义 |
---|---|
F_GETFL | 获取文件描述符属性信息 |
F_SETFL | 设置文件描述符属性信息 |
返回值:当时F_GETFL的时候,返回的是文件描述符的操作属性值
操作属性值 | 对应宏 | 含义 |
---|---|---|
0 | O_RDONLY | 只读权限 |
1 | O_WRONLY | 只写权限 |
2 | O_RDWR | 读写权限 |
程序演示:验证管道是单双工通信
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd[2];
int ret_pipe = pipe(fd);
if(ret_pipe < 0)
perror("pipe");
else
{
int authority_r = fcntl(fd[1],F_GETFL);
int authority_w = fcntl(fd[0],F_GETFL);
printf("fd[1] : %d\n",authority_r);
printf("fd[0] : %d\n",authority_w);
}
return 0;
}
fd[1] : 1
fd[0] : 0
-
由于fd[1]文件描述符的操作权限值是1,对应着O_WRONLY宏,意为只写权限
-
由于fd[0]文件描述符的操作权限值是0,对应着O_RDONLY宏,意为只读权限
-
所以匿名管道的通信方式是单双工通信
2.6 父子进程通过匿名管道进行进程间通信
匿名管道只支持具有情缘关系的进程之间进行通信
原因:没有亲缘关系的进程之间无法共享同一个管道
父子进程通信过程:
- 创建匿名管道
- 创建子进程
- 一个作为写进程,一个作为读进程
程序演示:子进程向匿名管道写入数据,父进程从匿名管道中读取数据
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
//1.创建匿名管道
int fd[2];
int ret_pipe = pipe(fd);
if(ret_pipe < 0)
{
perror("pipe");
return -1;
}
//2.创建子进程
int pid = fork();
if(pid < 0)
{
perror("fork");
return -1;
}
else if(pid == 0)
{
//子进程 向 管道 中写入
close(fd[0]); // 关闭读端
const char* w_buf = "Hello World!";
write(fd[1],w_buf,strlen(w_buf));
}
else
{
//父进程 从 管道 中读取
close(fd[1]); // 关闭写端
char r_buf[1024];
read(fd[0],r_buf,sizeof(r_buf));
puts(r_buf);
}
return 0;
}
输出
Hello World!
提醒:由于父子进程异步执行,如果父进程先进行读取,而子进程还未写入,父进程就会阻塞在读取中,等待子进程写入后再读取
2.7 匿名管道的最大写入量
程序演示:测试的出匿名管道的最大写入量
思路:一直写入并记录写入了多少
#include <stdio.h>
#include <unistd.h>
int main()
{
int fd[2];
int ret_pipe = pipe(fd);
if(ret_pipe < 0 )
{
perror("pipe");
return -1;
}
int count = 0;
while(1)
{
write(fd[1],"h",1);
count++;
printf("count:%d\n",count);
}
return 0;
}
count:65536
所以匿名管道最大写入量为64K
- pipe_MAXSIZE = 64K ,最多写入64K的数据
- pipe_buf = 4K ,进程向匿名管道中写入的时候,如果写入的数据量小于于4K时,才能保证写入的原子性
写入的原子性:写入只有两种可能:
全部写入
一点没写进去
不存在只写进去一部分,这就是写入的原子性
2.8 将匿名管道读写端文件描述符设置为非阻塞属性
文件描述符的非阻塞属性:O_NONBLOCK
源码定义在/usr/include/bits/fcntl-linux.h中
#define O_NONBLOCK 04000
宏 | 8进制 | 2进制 | 10进制 |
---|---|---|---|
O_NONBLOCK | 04000 | 00000000 00000000 00001000 00000000 | 2048 |
我们的目的是将 读写端的文件描述符 加上非阻塞属性,即
fd[0]读端:只读属性(O_RDONLY) + 非阻塞属性(O_NONBLOCK) —> O_RDONLY | O_NONBLOCK
fd[1]写端:只写属性(O_WRONLY) + 非阻塞属性(O_NONBLOCK) —> O_WRONLY | O_NONBLOCK
这里的按位或就相当于将非阻塞属性和原有属性相加
程序演示:将 匿名管道读写端加上非阻塞属性
#include <unistd.h>
#include <fcntl.h>
int main()
{
//创建匿名管道
int fd[2];
int ret_pipe = pipe(fd);
if(ret_pipe < 0)
{
perror("pipe");
return -1;
}
//获取读写端文件描述符原有属性
int authority_r = fcntl(fd[0],F_GETFL);
int authority_w = fcntl(fd[1],F_GETFL);
//将读写端文件描述符加上非阻塞属性
fcntl(fd[0],F_SETFL,authority_r | O_NONBLOCK);
fcntl(fd[1],F_SETFL,authority_w | O_NONBLOCK);
//查看现在的文件属性值
int authority_r_m = fcntl(fd[0],F_GETFL);
int authority_w_m = fcntl(fd[1],F_GETFL);
printf("fd[0]-authority : %d\n",authority_r_m);
printf("fd[1]-authority : %d\n",authority_w_m);
return 0;
}
fd[0]-authority : 2048
fd[1]-authority : 2049
按位或之后将非阻塞属性加到了 匿名管道读写端文件描述符 上
2.9 匿名管道的一些极端操作及现象解释
- 4中情况及其现象解释:
操作一:将写端文件描述符设置为非阻塞属性,读端文件描述符仍然为阻塞属性,读端文件描述符不关闭,但是也不读取数据,写端一直写入数据
现象:会将管道写满,满之后再调用write函数写入失败返回-1,表示资源不可用
操作二:将写端文件描述符设置为非阻塞属性,读端文件描述符仍然为阻塞属性,读端文件描述符关闭,写端一直写入数据
现象:调用write的进程会收到SIGPIPE信号,导致调用write的进程退出,若是子进程退出则会变成僵尸进程,若是父进程退出则子进程变成孤儿进程
操作三:将读端文件描述符设置为非阻塞属性,写端文件描述符仍然为阻塞属性,写端文件描述符不关闭,但是也不写入数据,读端一直读取数据
现象:read函数会返回-1,表示资源不可用
操作四:将读端文件描述符设置为非阻塞属性,写端文件描述符仍然为阻塞属性,写端文件描述符关闭,读端一直读取数据
现象:read函数返回0,表示没有读取到任何内容
3 命名管道
命名管道是在内核中开辟的一段缓冲区,这段缓冲区是有标识符的
也就意味着,不同的进程通过标识符就可以找到这个缓冲区,不再要求进程之间具有亲缘关系,任意进程间都可以通过命名管道实现通信
3.1 创建命名管道的两种方式
3.1.1 命令创建命名管道
mkfifo命令:使用指定文件名创建FIFO,也成为:命名管道
mkfifo 文件名
演示实例
[gongruiyang@localhost fifoTest]$ mkfifo fifo
[gongruiyang@localhost fifoTest]$ ll
总用量 0
prw-rw-r--. 1 gongruiyang gongruiyang 0 1月 1 21:53 fifo
文件名为fifo的命名管道文件默认权限为0664,且该文件不可使用vim编辑
3.1.2 函数创建命名管道
mkfifo函数:在代码中创建命名管道
int mkfifo(const char *pathname, mode_t mode);
头文件:
- sys/stat.h
- sys/types.h
参数:
- pathname : 文件名
- mode : 权限
返回值:0 代表创建成功;-1代表创建失败
程序演示:用mkfifo创建命名管道
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
int mkfifo_ret = mkfifo("./fifo_test",0664);
if(mkfifo_ret < 0)
{
perror("mkfifo");
return -1;
}
return 0;
}
[gongruiyang@localhost fifoTest]$ ll fifo_test
prw-rw-r--. 1 gongruiyang gongruiyang 0 1月 1 22:20 fifo_test
3.2 使用命名管道实现Client和Server通信
思路:Server端创建命名管道并阻塞等待Client端打开匿名命名管道进行输入数据,数据放入管道中后,Server端进行读取并打印出来
Server端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
int main()
{
// 创建 命名管道
int ret_mkfifo = mkfifo("mypipe",0664);
if(ret_mkfifo < 0)
{
perror("mkpipe");
return -1;
}
// sever端以 只读 方式打开 命名管道
int ret_fd = open("mypipe",O_RDONLY);
if(ret_fd < 0)
{
perror("open");
return -1;
}
while(1)
{
char buf[1024] = { 0 }; //用于存放读进来的数据
printf("Please wait...\n");
// 读取数据,若无数据 则阻塞等待
ssize_t s = read(ret_fd,buf,sizeof(buf) - 1);
if( s > 0 )
{
buf[s] = 0;
printf("client say : %s\n",buf);
}
else if(s == 0)
{
printf("Client quit,exit now!\n");
exit(EXIT_SUCCESS);
}
else
perror("read");
}
close(ret_fd);
return 0;
}
Client端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
// 客户端以只写方式打开 命名管道
int ret_fd = open("mypipe",O_WRONLY);
if(ret_fd < 0)
{
perror("open");
return -1;
}
while(1)
{
char buf[1024] = { 0 };
printf("Please Enter Message : ");
fflush(stdout);
// 从标准输入中读取数据放入buf中
ssize_t ret_read = read(0,buf,sizeof(buf) - 1);
if(ret_read > 0)
{
buf[ret_read] = 0;
// 从buf中向命名管道中写入数据
write(ret_fd,buf,strlen(buf));
}
else
perror("read");
}
close(ret_fd);
return 0;
}
交互演示
[gongruiyang@localhost TestPipeCS]$ ./Server
Please wait...
client say : Hello Server!
Please wait...
client say : I'm Client!
Please wait...
Client quit,exit now!
[gongruiyang@localhost TestPipeCS]$ ./Client
Please Enter Message : Hello Server!
Please Enter Message : I'm Client!
Please Enter Message : ^C