Linux系统编程之进程通信
概述 and 学习目标
概述:
详细总结Linux系统下,进程间的通信的相关知识,并附上案例练习代码,以便于理解总结学习!
学习目标:
- 熟练使用 pipe 进行父子进程间通信
- 熟练使用 pipe 进行兄弟进程间通信
- 熟练使用 fifo 进行无血缘关系的进程间通信
- 使用 mmap 进行有血缘关系的进程间通信
- 使用 mmap 进行无血缘关系的进程间通信
提示:以下是本篇文章正文内容
一、进程间通信(的概念)是什么?
1.1 什么是进程间通信?
Linux环境下,进程地址空间互相独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所有进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1 把数据从用户空间拷贝到内核缓冲区,进程2 再从内核区中把数据读走,内核提供的这种机制称之为
进程间通信
(IPC
,InterProcess Communication
)。
- 人话:
两个进程间要想完成数据交换(通信),必须通过内核(无论两个进程间有无血缘关系);一个进程将数据写到内核,然后另一个进程从内核读走数据。
1.2 进程间通信的方式?
在进程间完成数据传递需要接住操作系统提供的特殊的方法,如:
文件
、管道
、信号
、共享内存
、消息队列
、先入先出队列
、套接字
、命令管道
等。随着计算机的蓬勃发展,一些方法由于自身涉及的缺陷或被淘汰或被弃用。
现今
常用的进程间通信方式
有:
管道
(使用最简单
)信号
(开销最小
)共享映射区
(无血缘
关系)本地套接字
(最稳定
)
下面我先总结
管道
以及共享映射区
的内容,后续再总结关于信号
以及本地套接字(socket网络编程)
的内容。(当然,开发中是很少用信号
来完成两个进程间的通信的。)
二、管道-pipe
2.1 管道的概念
管道是一种最基本的IPC机制,也称为匿名管道,应用于有血缘关系的进程之间,完成数据传递。调用pipe函数即可创建一个管道。
管道
特性
:
管道
的本质
上是一块内核缓冲区
。- 由两个文件描述符引用,一个表示
读端
,一个表示写端
。 - 规定数据
只能
从管道的写端流入
管道,从读端流出
。(不会叠加保存在缓冲区中,一旦被读走就没了) - 当两个进程都终结的时候,管道也
自动消失
。 - 管道的两端,
读端
和写端
默认都是阻塞的
。
2.2 管道的原理
- 管道的
实质
是内核缓冲区
,内部使用的是环形队列
这种数据结构来实现的。 - 默认的管道缓冲区大小为
4Kbit
(可用ulimit -a命令查看)。 - 实际操作过程中缓冲区会根据数据大小做适当调整。(并不是说缓冲区一存满4Kbit就报错,会有一点点的冗余调整空间,但超过4Kbit太多的话就会直接报错)。
2.3 管道的局限性
- 数据一旦被读走,便不再存在于管道中,
不可反复读取
。(水流出管道不可回收) 。 - 数据
只能
在管道的一个方向(单向)上流动
(从写端
流到读端
),若要实现双向流动,则必须使用两个管道。 只能
在有血缘关系
(父子/兄弟
进程)的进程间使用管道进行通信。
2.4 创建管道-pipe 函数
- 函数
原型
:int pipe(int fd[2]);
(==> int pipe(int* fd),只不过前者标记为传入的是只有2个元素的数组首地址,告诉你pipe只有2个端,你就算传10个元素的数组进去也没有,因为只会用到fd[0]和fd[1]!后者单纯的就告诉你传入一个数组首地址就行,本质上是一样的) - 函数
作用
:创建一个管道 - 函数
参数
:
若函数调用成功时,就会给你返回两个
文件描述符,分别是fd[0]
和fd[1]
,其中,fd[0]
存放管道的读端
,fd[1]
存放管道的写端
。
向管道读写数据是通过使用这两个文件描述符进行读,读写管道实质是操作内核缓冲区(接下来你就可以使用Linux的系统IO函数read和write对这2个文件描述符所对应的文件进行操作即可)。 - 返回值:
成功
返回0
;
失败
返回-1
,并设置errno值。
2.5 父子进程使用管道
问题:
管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间的通信呢?
解决:
一个进程在由 pipe() 创建管道后,一般再fork一个子进程;然后通过管道实现进程间的通信。(因此,不难推出,只要两个进程中存在血缘关系,这里的血缘关系指的是具有共同的祖先
,都可以才用管道方式来进行通信)。父子进程间具有相同的文件描述符,且指向同一个管道 pipe
,其他没有关系的进程不能获得 pipe() 产生的两个描述符,也就不能利用同一个管道进行通信了。
方法步骤:
第一步:
父进程创建管道(父进程fork子进程之前)
第二步:
父进程fork出子进程
第三步:
父进程关闭fd[0],子进程关闭fd[1]
(此时所父进程保留fd[1],子进程保留fd[0],即让父进程写,子进程读端意思,当然你想翻过来也ok,反正遵循用哪个留哪个,不用哪个就关闭哪个文件描述符
的原则即可)
总结:
- 父进程调用 pipe函数创建管道,得到两个文件描述符 fd[0]和fd[1],指向管道的读端和写端。
- 父进程调用fork函数创建子进程,那么子进程也有相同的两个文件描述符指向同一管道的两端。
- 父进程关闭管道读端,子进程关闭管道写端。此时,父进程可向管道中写入数据,子进程可将管道中的数据读出,这样就实现了父子进程间的通信。(当然,反过来也ok)
1 父进程创建 pipe
2 父进程调用fork函数创建子进程
3 父进程关闭一端
4 子进程关闭一端
5 父进程和子进程分别执行read或者write函数的操作,即可完成父子进程间的通信。
2.6 管道练习
-
1:一个进程能否使用管道完成读写操作呢?
答:no,进程间通信必须是2个进程间的。 -
2:使用 pipe 管道完成父子进程间通信
-
3:父子进程间通信,实现 ps aux | grep bash(将ps aux打印的管道信息写入到管道|中,然后grep bash命令从ps aux读到的信息中抓取关于bash的信息)
使用 execl 函数和 dup2 函数
要完成第3个练习,需do如下9步:1:在父进程中创建管道 pipe
2:在父进程中创建子进程 fork
3:在父进程中关闭读端fd[0]
4:在子进程中关闭写端fd[1]
5:在父进程中将标准输出(STDOUT_FILENO)重定向到管道的写端fd[1]
6:在子进程中将标准输入(STDIN_FILENO)重定向到管道的读端fd[0]
7:在父进程中调用execl函数执行(拉起) ps aux命令
(个人理解:因为 ps 命令的格式是这样的:ps linux系统的命令名,而linux系统的命令进程信息是系统自带的,这个命令是让系统把对应的信息打印输出给我们看到的,因此必须要将标准输出重定向给写端fd[1])
8:在子进程中调用execl函数执行(拉起) grep bash命令
(个人理解:因为 grep 命令的格式是这样的:grep 命令名 文件/文件夹名,而文件/文件夹名中的内容肯定是我们在Linux的terminal终端手动输入进去的,因此必须要将标准输入重定向给读端fd[0])
9:在父进程中回收子进程 wait -
4:父子进程间通信,实现 ps aux | grep bash
使用 execlp 函数和 dup2 函数
父进程要调用 waitpid 函数完成对子进程的回收
要完成第4个练习,步骤如上述第3题。 -
5:兄弟进程间通信,实现 ps aux | grep bash
使用 execlp 函数和 dup2 函数
父进程要调用 waitpid 函数完成对子进程的回收
要完成第5个练习,步骤如上述第3or4题。
test codes2:
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
int main(){
//第一步:由父进程创建管道
//int pipe(int pipefid[2]);
int fd[2];
int ret = pipe(fd);
if(ret < 0){//创建管道失败
perror("pipe error!\n");
return -1;
}
else if(ret == 0){//创建管成功
//第二步:父进程fork出子进程成功
pid_t pid = fork();//fork创建子进程
if(pid < 0){//这是fork子进程失败的case
perror("fork child error!\n");
return -1;
}
else if(pid == 0){//当前进程为子进程
//关闭写端 fd[1]
close(fd[1]);
char buf[64];
memset(buf,0x00,sizeof(buf));//给数组初始化
int n = read(fd[0],buf,sizeof(buf));//此时read是堵塞到!只要缓冲区没有数据读了肯定堵塞!
printf("child process read over! n==[%d],buf==[%s]\n",n,buf);
}
else if(pid > 0){//当前进程为父进程
//父进程fork出子进程成功
//下面通过pipe进行父子进程间通信
//第三步:父进程关闭fd[0],子进程关闭fd[1]
//关闭读端 fd[0]
close(fd[0]);
//fd,要写入的字符串,写入字符串的大小
char* str = "hello my bro!\n";
sleep(5);//sleep5s钟后再do写入的操作!
write(fd[1],str,strlen(str));//此时write是堵塞到!只要缓冲区写满了数据肯定堵塞!
//添加wait阻塞函数,等待子进程先退出
wait(NULL);//这个函数就能够确保子进程先于父进程而退出,然后父进程也能回收子进程的资源
//一旦子进程执行完成退出后,这个函数就马上取消阻塞并返回!
printf("father process write over!\n");
}
}
return 0;
}
result2:
test codes3:
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
int main(){
// 1:在父进程中创建管道 pipe
// 2:在父进程中创建子进程 fork
// 3:在父进程中关闭读端fd[0]
// 4:在子进程中关闭写端fd[1]
// 5:在父进程中将标准输出(STDOUT_FILENO)重定向到管道的写端fd[0]
// 6:在子进程中将标准输入(STDIN_FILENO)重定向到管道的读端fd[1]
// 7:在父进程中调用execl函数执行(拉起) ps aux命令
// 8:在子进程中调用execl函数执行(拉起) grep bash命令
// 9:在父进程中回收子进程 wait
// 1:在父进程中创建管道 pipe
int fd[2];
int ret = pipe(fd);
if(ret == -1){
perror("pipe error!\n");
return -1;
}
else if(ret == 0){
//pipe success!
// 2:在父进程中创建子进程 fork
pid_t pid = fork();
if(pid < 0){
perror("fork child process error!\n");
return -1;
}
else if(pid > 0){//当前进程为父进程
// 3:在父进程中关闭读端fd[0]
int closeLabel = close(fd[0]);
if(closeLabel != 0){
perror("close fd[0] error!\n");
return -1;
}
// 5:在父进程中将标准输出重定向到管道的写端fd[1]
dup2(fd[1],STDOUT_FILENO);
// 7:在父进程中调用execl函数执行(拉起) ps aux命令
// linzhuofan@VM-12-8-ubuntu:~$ which ps
// /usr/bin/ps
const char* command = "ps";
const char* path_of_command = "/usr/bin/ps";
const char* arg1 = "aux";
execl(path_of_command,command,arg1,NULL);
//当然,其实这里execl函数执行成功后并不会返回!而是直接退出!
//此时子进程后退出,变成孤儿进程,而后就会给init这个1号进程自动领养回收掉!没问题!~
//so wait函数在这里是没用的!除非执行失败后,那就会执行wait函数由父进程回收子进程
//execl函数拉起/执行命令失败才会执行下面的代码!
perror("execl error!\n");
// 9:在父进程中回收子进程 wait
wait(NULL);//阻塞等待回收子进程的资源!
}
else if(pid == 0){//当前进程为子进程
// 4:在子进程中关闭写端fd[1]
int closeLabel = close(fd[1]);
if(closeLabel != 0){
perror("close fd[1] error!\n");
return -1;
}
// 6:在子进程中将标准输入重定向到管道的读端fd[0]
dup2(fd[0],STDIN_FILENO);
// 8:在子进程中调用execl函数执行(拉起) grep bash命令
const char* command = "grep";
const char* path_of_command = "/usr/bin/grep";
const char* arg1 = "bash";
execl(path_of_command,command,arg1,NULL);
}
}
return 0;
}
result3:
test codes4:
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
int main(){
// 1:在父进程中创建管道 pipe
// 2:在父进程中创建子进程 fork
// 3:在父进程中关闭读端fd[0]
// 4:在子进程中关闭写端fd[1]
// 5:在父进程中将标准输出(STDOUT_FILENO)重定向到管道的写端fd[0]
// 6:在子进程中将标准输入(STDIN_FILENO)重定向到管道的读端fd[1]
// 7:在父进程中调用execl函数执行(拉起) ps aux命令
// 8:在子进程中调用execl函数执行(拉起) grep bash命令
// 9:在父进程中回收子进程 wait
// 1:在父进程中创建管道 pipe
int fd[2];
int ret = pipe(fd);
if(ret == -1){
perror("pipe error!\n");
return -1;
}
else if(ret == 0){
//pipe success!
// 2:在父进程中创建子进程 fork
pid_t pid = fork();
if(pid < 0){
perror("fork child process error!\n");
return -1;
}
else if(pid > 0){//当前进程为父进程
// 3:在父进程中关闭读端fd[0]
int closeLabel = close(fd[0]);
if(closeLabel != 0){
perror("close fd[0] error!\n");
return -1;
}
// 5:在父进程中将标准输出重定向到管道的写端fd[1]
dup2(fd[1],STDOUT_FILENO);
// 7:在父进程中调用execlp函数执行(拉起) ps aux命令
// linzhuofan@VM-12-8-ubuntu:~$ which ps
// /usr/bin/ps
const char* commandorFileName = "ps";
const char* command = "ps";
const char* arg1 = "aux";
execlp(commandorFileName,command,arg1,NULL);//execlp函数就不需要写路径了!
//当然,其实这里execlp函数执行成功后并不会返回!而是直接退出!
//此时子进程后退出,变成孤儿进程,而后就会给init这个1号进程自动领养回收掉!没问题!~
//so waitpid函数在这里是没用的!除非执行失败后,那就会执行waitpid函数由父进程回收子进程
//execl函数拉起/执行命令失败才会执行下面的代码!
perror("execlp error!\n");
// 9:在父进程中回收子进程 waitpid
waitpid(-1,NULL,0);//pid==-1,回收all子进程。options==0,阻塞等待回收子进程的资源!
}
else if(pid == 0){//当前进程为子进程
// 4:在子进程中关闭写端fd[1]
int closeLabel = close(fd[1]);
if(closeLabel != 0){
perror("close fd[1] error!\n");
return -1;
}
// 6:在子进程中将标准输入重定向到管道的读端fd[0]
dup2(fd[0],STDIN_FILENO);
// 8:在子进程中调用execlp函数执行(拉起) grep bash命令
const char* commandorFileName = "grep";
const char* command = "grep";
const char* arg1 = "--color=auto";//加个颜色参数给grep命令!以显示颜色!
const char* arg2 = "bash";
execlp(commandorFileName,command,arg1,arg2,NULL);//execlp函数就不需要写路径了!
}
}
return 0;
}
result4:
test codes5:
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
int main(){
// 1:创建一个管道 pipe
int fd[2];
int ret = pipe(fd);
int n = 2;//创建2个兄弟进程!
int i = 0;
if(ret == -1){
perror("pipe error!\n");
return -1;
}
//else if(ret == 0)
//pipe success!
//创建子进程!
for(;i<n;++i){
pid_t pid = fork();
if(pid < 0){
perror("fork error!\n");
return -1;
}
else if(pid==0){
break;//这个break是为了让子进程不要再创建孙子甚至是曾孙子进程了!
}
}
if(i == n){
//关闭管道的读和写端
//因为父进程此时只会do回收子进程的工作而已!
close(fd[0]);
close(fd[1]);
pid_t wpid;
int wstatus;
while(1){
//等待回收子进程
wpid = waitpid(-1,&wstatus,WNOHANG );
if(wpid==0){
//没有子进程退出了
sleep(10);
continue;
}
else if(wpid=-1){
//没有子进程了(all的子进程都死掉了!)
printf("no child is living,wpid==[%d]\n",wpid);
exit(0);//正常退出程序!
}
else if(wpid > 0){
printf("成功回收掉子进程了,此时所回收掉的子进程id==[%d]\n",wpid);
//成功回收掉子进程,才能够判断其退出状态!
if(WIFEXITED(wstatus)){
printf("子进程正常退出了!子进程的退出状态是:%d\n",WEXITSTATUS(wstatus));
}else if(WIFSIGNALED(wstatus)){
printf("子进程时由信号[%d]杀死的!\n",WTERMSIG(wstatus));
}
}
}
}
//第1个子进程
if(i==0){
close(fd[0]);//关闭读端
//将标准输出重定向到管道的写端
dup2(fd[1],STDOUT_FILENO);
execlp("ps","ps","aux",NULL);
//execlp函数执行成功后
//就不执行下面的代码了
perror("execlp error!\n");
close(fd[1]);
}
//第2个子进程
if(i==0){
printf("child: father_pid==[%d],child_pid==[%d]\n",getppid(),getpid());
close(fd[1]);//关闭写端
//将标准输入重定向到管道的读端
dup2(fd[0],STDIN_FILENO);
execlp("grep","grep","--color","bash",NULL);
//execlp函数执行成功后
//就不执行下面的代码了
perror("execlp error!\n");
close(fd[0]);
}
return 0;
}
2.7 管道的读写行为
2.7.1 读操作
管道中
有数据
时(比如上述2.6练习的第2题)
- read
正常读
,返回
读出的字节数
管道中
无数据
时
- 当
写
端全部关闭
- read
解除阻塞
,返回0,相当于读文件读到了尾部了。- 当
写
端没有全部关闭
- read
继续阻塞
,直至读到 pipe 管道被写入的数据为止。
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
int main(){
//第一步:由父进程创建管道
//int pipe(int pipefid[2]);
int fd[2];
int ret = pipe(fd);
if(ret < 0){//创建管道失败
perror("pipe error!\n");
return -1;
}
//write(fd[1],"hello world!",strlen("hello world!"));
//关闭写端
close(fd[1]);
char buf[64];
memset(buf,0x00,sizeof(buf));//给数组初始化
int n = read(fd[0],buf,sizeof(buf));//此时read是堵塞到!只要缓冲区没有数据读了肯定堵塞!
printf("read over! n==[%d],buf==[%s]\n",n,buf);
return 0;
}
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
int main(){
//第一步:由父进程创建管道
//int pipe(int pipefid[2]);
int fd[2];
int ret = pipe(fd);
if(ret < 0){//创建管道失败
perror("pipe error!\n");
return -1;
}
//给pipe管道的写端fd[1] 写入"hello world!"
write(fd[1],"hello world!",strlen("hello world!"));
//关闭写端
// close(fd[0]);
char buf[64];
memset(buf,0x00,sizeof(buf));//给数组初始化
int n = read(fd[0],buf,sizeof(buf));//此时read是堵塞到!只要缓冲区没有数据读了肯定堵塞!
printf("read over! n==[%d],buf==[%s]\n",n,buf);
return 0;
}
2.7.2 写操作
当
读
端全部关闭
时
- 管道破裂,进程终止,内核给当前进程发
SIGPIPE
信号当
读
端没全部关闭
时
- 当内核缓冲区
写满
时- write会
阻塞
- 当内核缓冲区
没写满
时继续
write
全关闭读端时:
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
int main(){
//由父进程创建管道
//int pipe(int pipefid[2]);
int fd[2];
int ret = pipe(fd);
if(ret < 0){//创建管道失败
perror("pipe error!\n");
return -1;
}
//全关闭读端
close(fd[0]);
//给pipe管道的写端fd[1] 死循环写入"hello world!"
while(1){
write(fd[1],"hello world!",strlen("hello world!"));
}
//此时因为关闭了全部读端fd[0],因此,管道会直接裂开!因为你把它的出口都堵住了!
//紧接着,你这个进程也会马上死掉(被终止掉)!!!
//so你后续的代码做任何操作都没有用!
char buf[64];
memset(buf,0x00,sizeof(buf));//给数组初始化
int n = read(fd[0],buf,sizeof(buf));//此时read是堵塞到!只要缓冲区没有数据读了肯定堵塞!
printf("read over! n==[%d],buf==[%s]\n",n,buf);
return 0;
}
不全关闭读端时:
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
int main(){
//由父进程创建管道
//int pipe(int pipefid[2]);
int fd[2];
int ret = pipe(fd);
if(ret < 0){//创建管道失败
perror("pipe error!\n");
return -1;
}
//不全部关闭读端
//close(fd[0]);
//给pipe管道的写端fd[1] 死循环写入"hello world!"一下子就会写满pipe管道这个内核缓冲区!
while(1){
write(fd[1],"hello world!",strlen("hello world!"));
printf("持续写入 ing ...\n");
}
//此时因为没有关闭全部读端fd[0],但是我用while死循环直接将管道pipe瞬间写满!
//那么,接下来write函数就堵塞在这儿了!卡死在这儿了!
//若没有加while(1)的死循环,则write还是可以正常读pipe管道中被写入的数据的
//不信你可以把上述的while(1)的代码注释掉~
char buf[64];
memset(buf,0x00,sizeof(buf));//给数组初始化
int n = read(fd[0],buf,sizeof(buf));//此时read是堵塞到!只要缓冲区没有数据读了肯定堵塞!
printf("read over! n==[%d],buf==[%s]\n",n,buf);
return 0;
}
2.8 如何设置管道为非阻塞的?
默认情况下,管道的
读写两端
都是阻塞的
,若要设置为非阻塞的
,则需要用下列三个步骤:
- 1:int flag = fcntl(fd[0],F_GETFL,0);
- 2:flag |= O_NONBLOCK;
- 3:fcntl(fd[0],F_SETFL,flag);
若是想让
读端
设置为非阻塞的
:
当写端没有关闭
:
管道中没有
数据可读时,则read返回-1。
管道中有
数据可读时,则read返回实际读到的字节数。
当写端已经关闭
:
管道中没有
数据可读时,则read返回0。
管道中有
数据可读时,则read返回实际读到的字节数。
注意:
这些东西记不住很正常,会用man 查阅对应的函数/命令,去试一试看看结果是不是如我上述所说即可,或者看自己做的一些笔记博客看回来,查到之后会用即可。
test codes:
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
#include<fcntl.h>
int main(){
//由父进程创建管道
//int pipe(int pipefid[2]);
int fd[2];
int ret = pipe(fd);
if(ret < 0){//创建管道失败
perror("pipe error!\n");
return -1;
}
//不全部关闭读端
//close(fd[0]);
//不给pipe管道的写端fd[1] 写入"hello world!"
//write(fd[1],"hello world!",strlen("hello world!"));
//设置管道pipe 的读端fd[0] 为非阻塞的!
//这是用fcntl(file-control)函数的标准设置flag属性的三步骤!
//不用记住!会copy来用即可!
int flags = fcntl(fd[0],F_GETFL,0);
flags |= O_NONBLOCK;
fcntl(fd[0],F_SETFL,flags);
char buf[64];
memset(buf,0x00,sizeof(buf));//给数组初始化
int n = read(fd[0],buf,sizeof(buf));//此时read是堵塞到!只要缓冲区没有数据读了肯定堵塞!
printf("read over! n==[%d],buf==[%s]\n",n,buf);
return 0;
}
result:
2.9 如何查看管道缓冲区的大小?
-
命令:
ulimit -a
-
函数:
-
long fpathconf(int fd,int name);
*printf(“pipe size==[%ld]\n”,fpathconf(fd[0],_PC_PIPE_BUF));//打印缓冲区大小
printf(“pipe size==[%ld]\n”,fpathconf(fd[1],_PC_PIPE_BUF));//打印缓冲区大小
命令:man fpathconf
可以查到:
test codes:
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>
#include<fcntl.h>
int main(){
//由父进程创建管道
//int pipe(int pipefid[2]);
int fd[2];
int ret = pipe(fd);
if(ret < 0){//创建管道失败
perror("pipe error!\n");
return -1;
}
//不给pipe管道的写端fd[1] 写入"hello world!"
write(fd[1],"hello world!",strlen("hello world!"));
close(fd[1]);//关闭写端
//设置管道pipe 的读端fd[0] 为非阻塞的!
//这是用fcntl(file-control)函数的标准设置flag属性的三步骤!
//不用记住!会copy来用即可!
int flags = fcntl(fd[0],F_GETFL,0);
flags |= O_NONBLOCK;
fcntl(fd[0],F_SETFL,flags);
char buf[64];
memset(buf,0x00,sizeof(buf));//给数组初始化
int n = read(fd[0],buf,sizeof(buf));//此时read是堵塞到!只要缓冲区没有数据读了肯定堵塞!
printf("read over! n==[%d],buf==[%s]\n",n,buf);
printf("pipe size==[%ld]\n",fpathconf(fd[0],_PC_PIPE_BUF));//打印缓冲区大小
printf("pipe size==[%ld]\n",fpathconf(fd[1],_PC_PIPE_BUF));//打印缓冲区大小
return 0;
}
result:
三、FIFO
3.1 FIFO介绍
FIFO
常常被称之为命名管道
,以区分管道(pipe)。管道(pipe)只能用于有“血缘关系”
(即:兄弟/父子关系)的进程之间的通信。但通过FIFO,使得不相关
/无血缘关系
的进程也能够进行数据的交换。
(当然,有血缘关系你也可以使用FIFO,但是对于这类进程使用pipe管道使它们通信会更加简单!)
FIFO是Linux
基础文件类型
中的一种(文件类型为p
,可通过ls -l来查看其文件类型)。但FIFO文件在磁盘上并没有数据块,文件大小为0,仅仅用来标识
内核中的一条通道。进程可以打开这个文件进行read/write,实际上是在读写内核缓冲区,这样就实现了进程间的通信了。
3.2 创建管道
如何创建FIFO
这种命名管道
呢?
方法一
:使用命令 mkfifo
命令格式:mkfifo
管道名
例如:mkfifo
myfifo
方法二
:使用函数
int mkfifo(const char* pathname, mode_t mode);
参数说明和返回值可以在Linux terminal下用命令来查看:man
3
mkfifo
注:
当创建了一个FIFO,就可以使用open函数打开它,常见的文件I/O函数都可以用于FIFO。如:close、read、write、unlink等。
此外,FIFO严格遵循先进先出(first in first out)
的data的读写顺序,对FIFO的读总是从开始处返回数据,对FIFO的写则把数据添加到末尾。它们不支持诸如lseek()等文件的定位操作!
3.3 使用 FIFO 完成两个进程间的通信
使用 FIFO 完成两个进程间通信的示意图:
步骤:
假设这里,先操作进程A,后操作进程B。(当然你反过来也ok)
(我怎么让两个无血缘关系
的进程间进行通信?但凡两个并不都是用fork函数
弄出来进程,就是两个无血缘关系
的进程了!下面我会直接创建2个可执行程序并一先一后运行,这就变成两个无关系的进程了)
对进程A:
- 1 创建一个fifo文件:mkfifo命令或者使用mkfifo函数
- 2 open fifo 文件,获得一个文件描述符fd
- 3 写 fifo 文件 — write(fd,“xxx”,…)
- 4 关闭 fifo 文件 — close(fd);
对进程B:
- 1 open fifo 文件,获得一个文件描述符fd
- 2 读 fifo 文件 — read(fd,buf,sizeof(buf));
- 3 关闭 fifo 文件 — close(fd);
test codes:
//fifo_write.c
//用fifo完成两个进程间通信的test
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<unistd.h>
#include<fcntl.h>
int main(){
//1 创建fifo文件
//int mkfifo(const char *pathname, mode_t mode);
//const char *pathname为所创建法fifo文件的路径
//mode_t mode为所创建fifo文件的权限,这里默认为0777
const char* pathname = "./my_fifo";
int ret = mkfifo(pathname,0777);
if(ret == -1){
perror("make fifo error!\n");
return -1;
}
//fd > 0 <==> make fifo 成功 !
//2 打开fifo文件
//O_RDWR这个flag参数表示 可读可写 的意思!(可通过man 2 open来查)
int fd = open(pathname,O_RDWR);
if(fd < 0 ){
perror("open error!\n");
return -1;
}
//3 写data 进 fifo文件
char* inputStr = "hello,my fifo file!";
write(fd,inputStr,strlen(inputStr));
printf("write over!\n");
//注意:你写入data到fifo文件时,不能够关闭fifo文件!!!
//否则的话你后面别的进程没法读这个fifo文件里的内容了!
sleep(10);//very 关键!
//休眠10s,给别的进程足够的时间来读我写入fifo的内容
//以不至于出现这样的case:
//进程A打开了fifo并写入data到fifo中了,进程B也打开fifo准备读fifo中的文件了
//但是此时进程A又关闭了fifo文件,导致进程B读不到fifo中的内容
close(fd);
getchar();
//这个函数作用:等待你键入一个字符才继续执行下面的代码!
return 0;
}
//fifo_read.c
//用fifo完成两个进程间通信的test
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<unistd.h>
#include<fcntl.h>
int main(){
//1 创建fifo文件
//int mkfifo(const char *pathname, mode_t mode);
//const char *pathname为所创建法fifo文件的路径
//mode_t mode为所创建fifo文件的权限,这里默认为0777
const char* pathname = "./my_fifo";
// int ret = mkfifo(pathname,0777);
// if(ret == -1){
// perror("make fifo error!\n");
// return -1;
// }
//fd > 0 <==> make fifo 成功 !
//2 打开fifo文件
//O_RDWR这个flag参数表示 可读可写 的意思!(可通过man 2 open来查)
int fd = open(pathname,O_RDWR);
if(fd < 0 ){
perror("open error!\n");
return -1;
}
//3 读data from fifo文件
//保存从fifo中读到的内容的 字符串
char read_data_buf[64];
//给 保存从fifo中读到的内容的 字符串 赋初值!
memset(read_data_buf,0x00,sizeof(read_data_buf));
int size = read(fd,read_data_buf,sizeof(read_data_buf));
printf("read over: string==[%s],size==[%d]\n",read_data_buf,size);
//4 关闭fifo文件
close(fd);
getchar();
//这个函数作用:等待你键入一个字符才继续执行下面的代码!
return 0;
}
result:
注:
这里你必须先执行fifo_write创建my_fifo的fifo文件并写入data到my_fifo中,然后再调用fifo_read来读取my_fifo中的data。反过来的话你都没有创建并写入data到一个fifo文件中,你叫我怎么fifo_read呢对吧?
优化
上述test代码:
因为每一次fifo_write前,我们都要创建一个名为my_fifo的fifo文件,如果存在的话,fifo_write这个可执行文件就执行不了,这样就比较麻烦(每次执行这个文件前都得ls看看是否存在my_fifo,存在就删除,不存在再执行fifo_write)
因此,下面介绍一个系统函数access
:专门用来判断一个文件是否存在。
函数原型:int access(const char *pathname, int mode);
返回值:
当 mode 参数 == F_OK 时:
文件存在,返回 0;
文件不存在,返回 -1;
codes:
把上述的fifo_write.c改成如下就可以解决上述所说的麻烦情况了:
//用fifo完成两个进程间通信的test
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<unistd.h>
#include<fcntl.h>
int main(){
//1 创建fifo文件
//int mkfifo(const char *pathname, mode_t mode);
//const char *pathname为所创建法fifo文件的路径
//mode_t mode为所创建fifo文件的权限,这里默认为0777
const char* pathname = "./my_fifo";
int isExist = access(pathname,F_OK);
//my_fifo不存在时,就创建
if(isExist < 0){
perror("./my_fifo is not exist! And now we make a fifo file\n");
int ret = mkfifo(pathname,0777);
if(ret == -1){
perror("make fifo error!\n");
return -1;
}
//fd > 0 <==> make fifo 成功 !
}
//my_fifo存在时,就直接继续do下述操作!
//2 打开fifo文件
//O_RDWR这个flag参数表示 可读可写 的意思!(可通过man 2 open来查)
int fd = open(pathname,O_RDWR);
if(fd < 0 ){
perror("open error!\n");
return -1;
}
//3 写data 进 fifo文件
char* inputStr = "hello,my fifo file!";
write(fd,inputStr,strlen(inputStr));
printf("write over!\n");
//注意:你写入data到fifo文件时,不能够关闭fifo文件!!!
//否则的话你后面别的进程没法读这个fifo文件里的内容了!
sleep(10);//very 关键!
//休眠10s,给别的进程足够的时间来读我写入fifo的内容
//以不至于出现这样的case:
//进程A打开了fifo并写入data到fifo中了,进程B也打开fifo准备读fifo中的文件了
//但是此时进程A又关闭了fifo文件,导致进程B读不到fifo中的内容
close(fd);
getchar();
//这个函数作用:等待你键入一个字符才继续执行下面的代码!
return 0;
}
test codes2:(循环读取fifo命名管道中的内容)
//fifo_write.c
//用fifo完成两个进程间通信的test
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<unistd.h>
#include<fcntl.h>
int main(){
//1 创建fifo文件
//int mkfifo(const char *pathname, mode_t mode);
//const char *pathname为所创建法fifo文件的路径
//mode_t mode为所创建fifo文件的权限,这里默认为0777
const char* pathname = "./my_fifo";
int isExist = access(pathname,F_OK);
//my_fifo不存在时,就创建
if(isExist < 0){
perror("./my_fifo is not exist! And now we make a fifo file\n");
int ret = mkfifo(pathname,0777);
if(ret == -1){
perror("make fifo error!\n");
return -1;
}
//fd > 0 <==> make fifo 成功 !
}
//my_fifo存在时,就直接继续do下述操作!
//2 打开fifo文件
//O_RDWR这个flag参数表示 可读可写 的意思!(可通过man 2 open来查)
int fd = open(pathname,O_RDWR);
if(fd < 0 ){
perror("open error!\n");
return -1;
}
//3 写data 进 fifo文件
char* inputStr = "hello,my fifo file!";
//循环写入data到fifo中!!!
int cnt = 0;
while(1){
write(fd,inputStr,strlen(inputStr));
printf("[%d]th write over!\n",cnt);
cnt++;
sleep(1);//让写操作不要那么快写入my_fifo文件!
}
//注意:你写入data到fifo文件时,不能够关闭fifo文件!!!
//否则的话你后面别的进程没法读这个fifo文件里的内容了!
// sleep(10);//very 关键!
//休眠10s,给别的进程足够的时间来读我写入fifo的内容
//以不至于出现这样的case:
//进程A打开了fifo并写入data到fifo中了,进程B也打开fifo准备读fifo中的文件了
//但是此时进程A又关闭了fifo文件,导致进程B读不到fifo中的内容
close(fd);
getchar();
//这个函数作用:等待你键入一个字符才继续执行下面的代码!
return 0;
}
//fifo_read.c
//用fifo完成两个进程间通信的test
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<unistd.h>
#include<fcntl.h>
int main(){
//1 创建fifo文件
//int mkfifo(const char *pathname, mode_t mode);
//const char *pathname为所创建法fifo文件的路径
//mode_t mode为所创建fifo文件的权限,这里默认为0777
const char* pathname = "./my_fifo";
// int ret = mkfifo(pathname,0777);
// if(ret == -1){
// perror("make fifo error!\n");
// return -1;
// }
//fd > 0 <==> make fifo 成功 !
//2 打开fifo文件
//O_RDWR这个flag参数表示 可读可写 的意思!(可通过man 2 open来查)
int fd = open(pathname,O_RDWR);
if(fd < 0 ){
perror("open error!\n");
return -1;
}
//3 读data from fifo文件
//保存从fifo中读到的内容的 字符串
char read_data_buf[64];
//给 保存从fifo中读到的内容的 字符串 赋初值!
int size = -1;
//循环读fifo中的data内容!!!
while(1){
memset(read_data_buf,0x00,sizeof(read_data_buf));
size = read(fd,read_data_buf,sizeof(read_data_buf));
printf("read over: string==[%s],size==[%d]\n",read_data_buf,size);
}
//4 关闭fifo文件
close(fd);
getchar();
//这个函数作用:等待你键入一个字符才继续执行下面的代码!
return 0;
}
result2:
四、内存映射区
4.1 内存映射区介绍
存储映射I/O(Memory-mapped I/O)使一个磁盘文件与存储空间中的一个缓冲区相映射。从缓冲区取数据,就相当于读文件中的相应字节;将数据写入缓冲区,则会将数据写入文件。这样,就可以在不使用read和write这两个系统IO函数的情况下,使用地址(指针)完成I/O操作。
使用存储映射这种方法,首先应该通知内核,将一个指定的文件映射到存储区域中。这个映射工作可以通过 mmap
函数来实现。(mmap函数既可以让无血缘关系
的两个进程间完成通信,也可以让有血缘关系
的~)
人话:
相当于把文件直接映射到内存,此时,操作内存就相当于直接操作文件内容了。接着,两个进程若可以分别对这块内存进行读写操作则可实现进程间的通信了。
4.2 mmap函数
- 函数
原型
: - void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
注:void星 是个万能指针 - 函数
作用
:建立存储映射区 - 函数
参数
:
addr
:指定映射的起始地址,通常设置为NULL就行,剩下的映射地址工作则由系统来指定;
(若你自己指定一个地址,但是这个地址已经被使用了的话,那就会报错!)
length
:映射到内存的文件长度;
(一般都是所映射的文件的大小)
prot
:映射区的保护方式,最常用的:
只读:PROT_READ
只写:PROT_WRITE
可读可写:PROT_READ | PROT_WRITE
flags
:映射区的特性,可以是:
MAP_SHARED
:写入映射区的数据会写回文件,且允许其他映射到该文件的进程共享。(对映射区的修改能反应到文件中)
MAP_PRIVATE
:对映射区的写入操作会产生一个映射区的复制(copy-on-write),对此区域所做的修改并会写回原文件中。(对映射区的修改不能反应到文件中)
fd
:由 open 函数所返回的文件描述符,代表要映射的文件。
offset
:以文件开始处的偏移量,必须是4k的整数倍
,通常为0,表示从文件头处
开始do映射。 - 返回值:
成功
返回所创建的映射区的首地址
;
失败
返回MAP_FAILED
宏。
参数简易使用的
小总结
:
addr
:一般传NULL,表示让系统内核区指定一个内存起始地址
length
:表示文件大小;
一般用lseek或者stat函数 来获取文件大小
prot
:
PROT_READ
PROT_WRITE
PROT_READ| PROT_WRITE
flags
:
MAP_SHARED:表示 对映射区的修改会反映到文件中(可以对文件进行修改,一般我们都只用MAP_SHARED而已!)
(若想读取
映射区的data的同时也想修改写入
data,就用该参数)
MAP_PRIVATE:表示 对映射区的修改不会对文件产生影响
(若只想读取
映射区的data而不想修改,就用该参数)
fd
:打开的文件描述符
fd = open();
offset
:表示 从文件的哪个位置开始映射,一般传入 0
4.3 munmap函数
- 函数
原型
: - int munmap(void *addr, size_t length);
注:void星 是个万能指针 - 函数
作用
:释放由mmap
函数建立的存储映射区 - 函数
参数
:
addr
:调用mmap
函数成功时,所返回的映射区首地址
length
:映射区大小(即文件大小—mmap
函数的第二个参数) - 返回值:
成功
返回0;
失败
返回-1,并设置 errno 值。
4.4 mmap 的注意事项(以测试代码来do提醒)
test codes1:
//使用mmap函数完成父子(有血缘关系)进程间的通信
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/mman.h>
#include<sys/wait.h>
int main(){
//使用mmap函数建立共享映射区
// void *mmap(void *addr, size_t length, int prot, int flags,
// int fd, off_t offset);
int fd = open("./test.log",O_RDWR);
if(fd < 0){
perror("open error!\n");
return -1;
}
int len = lseek(fd,0,SEEK_END);
void * addr = mmap(NULL,len,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(addr == MAP_FAILED){
perror("mmap error!\n");
return -1;
}
//创建子进程
pid_t pid = fork();
if(pid < 0){
perror("fork error!\n");
return -1;
}
else if(pid > 0){// 当前进程为父进程
memcpy(addr,"hello world!",strlen("hello world!"));
//这行代码段意思是 写data到映射区
wait(NULL);//保证父进程最后再退出!
}
else {// pid == 0 当前进程为子进程
sleep(1);//先让子进程睡个1s钟!这样防止子进程先执行完!
//然后就没法读到父进程写进去共享区的data了!
char* outputStr = (char*)addr;
printf("str==[%s]\n",outputStr);
}
return 0;
}
result1:
test codes2:
void * addr = mmap(NULL,len,PROT_READ | PROT_WRITE,MAP_PRIVATE,fd,0);
把test codes1中的这行代码段MAP_SHARED改为MAP_PRIVATE
此时就无法
进行用mmap函数完成两进程间的通信了!
result2:
test codes3:
//mmap1_write
//使用mmap函数完成 无血缘关系(非父子/兄弟关系) 的进程间的通信
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/mman.h>
#include<sys/wait.h>
int main(){
//使用mmap函数建立共享映射区
// void *mmap(void *addr, size_t length, int prot, int flags,
// int fd, off_t offset);
int fd = open("./test.log",O_RDWR);
if(fd < 0){
perror("open error!\n");
return -1;
}
int len = lseek(fd,0,SEEK_END);//获取文件大小
//建立共享映射区
void * addr = mmap(NULL,len,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(addr == MAP_FAILED){
perror("mmap error!\n");
return -1;
}
//这个进程写
char* p = (char*)addr;
char buf[64];
memset(buf,0x00,sizeof(buf));//给空数组赋初值!
memcpy(p,"0123456789",10);
return 0;
}
//mmap1_read:
//使用mmap函数完成 无血缘关系(非父子/兄弟关系) 的进程间的通信
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/mman.h>
#include<sys/wait.h>
int main(){
//使用mmap函数建立共享映射区
// void *mmap(void *addr, size_t length, int prot, int flags,
// int fd, off_t offset);
int fd = open("./test.log",O_RDWR);
if(fd < 0){
perror("open error!\n");
return -1;
}
int len = lseek(fd,0,SEEK_END);//获取文件大小
//建立共享映射区
void * addr = mmap(NULL,len,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(addr == MAP_FAILED){
perror("mmap error!\n");
return -1;
}
//这个进程读
char buf[64];
memset(buf,0x00,sizeof(buf));//给空数组赋初值!
memcpy(buf,addr,10);
printf("buf==[%s]\n",buf);
return 0;
}
result3:
4.5 有关mmap 函数的使用总结
4.6 mmap 函数相关思考
对应mmap函数实现进程间的通信,没有什么思路可言,其实就是创建一个共享映射区,以让一个进程读,一个进程写即可了!
4.7 使用 mmap 建立匿名映射
(了解即可,且只能用于父子关系(有血缘关系)的进程间的通信
)
使用命令行:man 2 mmap 查看:
注意
:
mmap 建立的的匿名映射只能够用于父子关系(有血缘关系)的进程间的通信
。
(因为fd == -1,没有文件描述符,即没有文件,要是没有血缘关系,那你怎么通过文件来互相通信呢对吧?)
MAP_ANONYMOUS必须指定与MAP_SHARED一起使用,且fd指定为-1。
test codes:
//使用mmap函数创建匿名映射区,来 完成父子进程间的通信
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/mman.h>
#include<sys/wait.h>
int main(){
//使用mmap函数建立匿名的共享映射区
void * addr = mmap(NULL,4096,PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS,-1,0);
if(addr == MAP_FAILED){
perror("mmap error!\n");
return -1;
}
//创建子进程
pid_t pid = fork();
if(pid < 0){
perror("fork error!\n");
return -1;
}
else if(pid > 0){// 当前进程为父进程
memcpy(addr,"hello world!",strlen("hello world!"));//这行代码段意思是 写data到映射区
wait(NULL);//保证父进程最后再退出!
}
else {// pid == 0 当前进程为子进程
sleep(1);//先让子进程睡个1s钟!这样防止子进程先执行完!
//然后就没法读到父进程写进去共享区的data了!
char* outputStr = (char*)addr;
printf("str==[%s]\n",outputStr);
}
return 0;
}
result:
总结
本篇文章,介绍了进程间通信(IPC)
的管道
以及共享内存映射区
这两种方式。知识点分别是pipe
,fifo
和mmap(Memory-map)
,如需要再详细之使用方式和参数详解,则需要使用man 2 xxx
命令来查看!但,我们需记住一点:无论是函数参数还是用法,其实都无需完全记住,会查阅
对应函数如何使用,会及时地用起来即可
了!