摘要
1、pipe进行父子进程间通信
2、pipe进行兄弟进程间通信
3、fifo进行无血缘关系的进程间通信
4、mmap函数:如何使用、创建匿名映射区、进行有血缘关系的进程间通信和进行无血缘关系的进程间通信
概念
IPC: InterProcess Communication 进程间通信,通过内核提供的缓冲区进行数据交换的机制。
IPC通信的方式:
- pipe 管道 --最简单
- fifo 有名管道
- mmap 文件映射共享IO --速度最快
- 本地socket --最稳定
- 信号 --携带信息量最小
- 共享内存
- 消息队列
1、pipe管道
管道函数:
#include <unistd.h>
int pipe(int pipefd[2]);
参数:pipefd 读写文件描述符,0-代表读,1-代表写
返回值:失败返回-1,成功返回0
管道pipe的优劣:
- 优点:简单,比信号、套接字实现进程间通信简单很多。
- 缺点:(1)只能单向通信,双工需要建立两个管道(每个管道需要关闭一个端口形成单向);只能用于父子、兄弟进程(用共同祖先)之间通信,该问题可以使用fifo有名管道解决。
管道缓冲区大小:使用ulimit -a命令查看当前系统中创建管道文件所对应的内核缓冲区大小。pipe size (512 bytes, -p) 8
也可以使用fpathconf函数,借助参数、选项来查看,使用该函数引入头文件<unistd.h>,long fpathconf(int fd, int name); ,成功返回管道大小,失败返回-1,设置errno。
(1)练习1:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
int fd[2];
pipe(fd);
pid_t pid = fork();
if(pid == 0)
{
//子进程 --写
sleep(3);
write(fd[1],"hello",5);
}
else if(pid > 0)
{
//父进程 --读
char buf[12]={0};
int ret = read(fd[0],buf,sizeof(buf));
if(ret > 0)
{
write(STDOUT_FILENO,buf,ret);
}
}
return 0;
}
输出:
~$ gcc pipe_.c
~$ ./a.out
hello
(2)练习2:父子进程实现pipe通信,实现ps aux|grep bash 功能
#include<stdio.h>
#include<unistd.h>
int main()
{
int fd[2];
pipe(fd);
pid_t pid = fork();
if(pid == 0)
{
//子进程
//子进程只用到pipe管道的写端,把读端关闭
close(fd[0]);
//重定向
dup2(fd[1],STDOUT_FILENO); //标准输出重定向到管道的写端
//使用execlp函数
execlp("ps","ps","aux",NULL);
}
else if(pid > 0)
{
//父进程
//父进程只用到pipe管道的读端,把写端关闭
close(fd[1]);
//先重定向,标准输入重定向到管道的读端
dup2(fd[0],STDIN_FILENO);
//使用execlp函数
execlp("grep","grep","bash",NULL);
}
return 0;
}
输出:
~$ gcc pipe_ps.c
~$ ./a.out
chen 7559 0.0 0.0 25272 6552 pts/4 Ss 21:39 0:00 bash
chen 12009 0.0 0.0 15968 1080 pts/4 S+ 22:09 0:00 grep bash
等价于:
~$ ps aux|grep bash
chen 7559 0.0 0.0 25272 6552 pts/4 Ss 21:39 0:00 bash
chen 12015 0.0 0.0 15968 968 pts/4 S+ 22:09 0:00 grep --color=auto bash
注意:pipe管道是双向的,如果不关闭另一端,如父进程认为还有写端存在,就有可能还有人给它发数据,会继续等待。
(3)pipe 读写管道的几种情况
读管道
- 写端全部关闭,read读到0,相当于读到文件末尾
#include<stdio.h>
#include<unistd.h>
int main()
{
int fd[2];
pipe(fd);
pid_t pid = fork();
if(pid == 0)
{
//子进程
sleep(3);
//关闭读端
close(fd[0]);
write(fd[1],"hello",5);
close(fd[1]); //关闭写端
while(1)
{
sleep(1);
}
}
else if(pid > 0)
{
//父进程
close(fd[1]); //关闭写端
char buf[12]={0};
while(1)
{
int ret = read(fd[0],buf,sizeof(buf));
//如果读到0,认为读到文件末尾
if(ret == 0)
{
printf("read over!\n");
break;
}
if(ret > 0)
{
write(STDOUT_FILENO,buf,ret);
}
}
}
return 0;
}
输出:
helloread over!
- 写端没有全部关闭。有数据--read读到数据;没有数据--read阻塞,fcntl函数可以更改为非阻塞。
写管道
- 读端全部关闭----产生一个信号SIGPIPE,程序异常终止。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
int fd[2];
pipe(fd);
pid_t pid = fork();
if(pid == 0)
{
//子进程
sleep(3);
close(fd[0]); //关闭读端
write(fd[1],"hello",5);
close(fd[1]); //关闭写端
while(1)
{
sleep(1);
}
}
else if(pid > 0)
{
//父进程
close(fd[1]); //关闭写端
close(fd[0]); //关闭读端
int status;
wait(&status);
if(WIFSIGNALED(status))
{
printf("killed by %d\n",WTERMSIG(status));
}
//父进程只是关闭读写两端,但是并不退出
while (1)
{
sleep(1);
}
char buf[12]={0};
while(1)
{
int ret = read(fd[0],buf,sizeof(buf));
//如果读到0,认为读到文件末尾
if(ret == 0)
{
printf("read over!\n");
break;
}
if(ret > 0)
{
write(STDOUT_FILENO,buf,ret);
}
}
}
return 0;
}
输出:
killed by 13
- 读端未全部关闭。管道已满,--write阻塞,如果要显示现象,读端一直不读,写端狂写;管道未满,--write正常写入。
2、FIFO
fifo常被称为命名管道,以区分管道pipe,管道(pipe)只能用于“有血缘关系”的进程间,但FIFO不相关的进程也能交换数据。
FIFO是linux基础文件类型中的一种,但FIFO文件在磁盘上没有数据交换,仅仅用来标识内核中一条通道,各进程可以打开这个文件进行read\write,实际上是在读写内核通道,这样就实现了进程间通信。
实现方式:
- 命令:mkfifo 管道名
- 库函数:int mkfifo(const char *pathname, mode_t mode); 成功返回0,失败-1。
一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I\O函数都可以用于FIFO,如close、read、write、unlink等。
内核会针对fifo文件开辟一个缓冲区,操作fifo文件,可以操作缓冲区,实现进程间通信--实际上就是文件读写。fifo文件既可以直接“mkfifo+文件”名创建,也可以调用库函数mkfifo()创建。
练习:两个读写程序通过myfifo通信
- 第一步:在当前目录下创建一个fifo文件用于标识“通道”
~/linux$ mkfifo myfifo
~/linux$ ls
myfifo
- 第二步:创建一个写程序,并编译 gcc fifo_w.c -o fifo_w
/*fifo_w.c*/
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main(int agrc,char *agrv[])
{
if(agrc != 2)
{
printf("./a.out fifoname\n");
return -1;
}
//当前目录下已使用“mkfifo myfifo”创建了一个myfifo文件
//打开myfifo文件
int fd = open(agrv[1],O_WRONLY);
//向fifo文件中写
char buf[256];
int num = 1;
while (1)
{
memset(buf,0x00,sizeof(buf));
sprintf(buf,"xiaoming%04d",num++);
write(fd,buf,strlen(buf));
sleep(1);
}
close(fd);
return 0;
}
- 第三步:创建一个读程序用于读,并编译 gcc fifo_r.c -o fifo_r
/*fifo_r.c*/
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main(int argc,char *agrv[])
{
if(argc != 2)
{
printf("./a.out fifoname\n");
return -1;
}
int fd = open(agrv[1],O_RDONLY);
char buf[256];
int ret;
while (1)
{
//循环读
ret = read(fd,buf,sizeof(buf));
if(ret > 0)
{
printf("read:%s\n",buf);
}
}
close(fd);
return 0;
}
- 第四步:运行程序
//运行读程序
./fifo_r myfifo
//运行写程序
./fifo_w myfifo
输出结果:
:~$ ./fifo_r myfifo
read:xiaoming0001
read:xiaoming0002
read:xiaoming0003
read:xiaoming0004
read:xiaoming0005
read:xiaoming0006
read:xiaoming0007
read:xiaoming0008
read:xiaoming0009
...
说明:读写程序通过fifo实现了通信,其中创建的myfifo只是一个标识“通道”。
FIFO的注意事项
打开FIFO文件的时候,read端会阻塞等待write端open,write端同理,也会阻塞等待另一端打开。对上面代码进行稍微改造,添加一些打印:
//fifo_w.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main(int agrc,char *agrv[])
{
if(agrc != 2)
{
printf("./a.out fifoname\n");
return -1;
}
//当前目录下已使用“mkfifo myfifo”创建了一个myfifo文件
//打开myfifo文件
printf("Begin open ...\n");
int fd = open(agrv[1],O_WRONLY);
printf("End open ...\n");
//向fifo文件中写
char buf[256];
int num = 1;
while (1)
{
memset(buf,0x00,sizeof(buf));
sprintf(buf,"xiaoming%04d",num++);
write(fd,buf,strlen(buf));
sleep(1);
}
close(fd);
return 0;
}
--------------------------------------------------------------------------
//fifo_r.c
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main(int argc,char *agrv[])
{
if(argc != 2)
{
printf("./a.out fifoname\n");
return -1;
}
printf("Begin open read ...\n");
int fd = open(agrv[1],O_RDONLY);
printf("End open read ...\n");
char buf[256];
int ret;
while (1)
{
//循环读
ret = read(fd,buf,sizeof(buf));
if(ret > 0)
{
printf("read:%s\n",buf);
}
}
close(fd);
return 0;
}
3、mmap 创建/释放映射区
(1)mmap函数原型--创建映射区
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
参数:
- addr 传NULL
- length 映射区的长度
- prot:PROT_READ,可读;PROT_WRITE,可写。
- flags:MAP_SHARED,共享的(对内存的修改会影响到源文件);MAP_PRIVATE,私有的。
- fd 文件描述符,open打开一个文件
- offset 偏移量
返回值:
- 成功,返回可用的内存首地址
- 失败,返回MAP_FAILED
(2)munmap函数原型--释放映射区
int munmap(void *addr, size_t length);
参数:
- addr 传mmap的返回值,即创建映射区内存首地址
- length mmap创建的长度
返回值:
- On success, munmap() returns 0. On failure, it returns -1, and errno
(3)练习
首先,在当前目录下创建 mem.txt 文件
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<sys/mman.h>
#include<string.h>
#include<stdio.h>
#include<unistd.h>
int main()
{
int fd = open("mem.txt",O_RDWR);
//int fd = open("mem.txt",O_RDWR|O_CREAT|O_TRUNC,0664); //问题5:创建并且截断文件
//ftruncate(fd,8);
//int fd = open("mem.txt",O_WRONLY); //问题6:open文件选择O_WRONLY
//创建映射区
char *mem = mmap(NULL,8,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
//char *mem = mmap(NULL,8,PROT_READ|PROT_WRITE,MAP_SHARED,fd,1000); //问题3:文件偏移量
if(mem == MAP_FAILED)
{
perror("mmap err");
return -1;
}
//close(fd); //问题4:提前关闭文件描述符
//拷贝数据
strcpy(mem,"hello");
//strcpy(mem,"helloworld"); //问题2:对mem越界操作
//mem++; //问题1:更改mem变量地址
//释放mmap
munmap(mem,8);
close(fd);
return 0;
}
4、mmap相关问题
- 问题1:如果更改mem变量的地址,释放的时候munmap传入mem还能成功吗?例如:在代码munmap前,mem++; 。
不能。
- 问题2:如果对mem越界操作会怎样?
文件的大小对映射区有影响,应尽量避免。
- 问题3:如果文件偏移量随便填个数会怎样?
offset必须是4k的整数倍。
- 问题4:如果文件描述符先关闭,对mmap映射有没有影响?
没有影响。
- 问题5:open的时候,可以新创建一个文件来创建映射区吗?
不可以用大小为0的文件。
- 问题6:open文件选择O_WRONLY,可以吗?
不可以:permission denied
- 问题7:当选择MAP_SHARED时,open文件选择O_RDONLY,port可以选择PROT_READ|PROT_WRITE吗?
Permission denied,SHARED的时候,映射区的权限小于等于open文件的权限。
int main()
{
int fd = open("mem.txt",O_RDONLY);
ftrunccate(fd,8);
//创建映射区
char *mem = mmap(NULL,20,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(mem == MAP_FAILED)
{
perror("mmap err");
return -1;
}
close(fd);
//拷贝数据
strcpy(mem,"helloworld");
//释放mmap
if(munmap(mem,20) < 0)
{
perror("munmap err");
}
return 0;
}
- 问题8:mmap什么情况下会报错?多种情况下。
- 问题9:如果不判断返回值会怎样?一定要判断。
5、父子进程mmap映射区共享
要求:父进程创建映射区,然后fork子进程,子进程修改映射区内容,然后,父进程读取映射区内容,查验是否共享。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<fcntl.h>
#include<sys/wait.h>
int main()
{
//先创建映射区
//int fd = open("mem.txt",O_RDWR);
//int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0); //系统提供匿名映射区
if(mem == MAP_FAILED)
{
perror("mmap err");
return -1;
}
//fork子进程
pid_t pid = fork();
//父子进程交替修改数据
if(pid == 0)
{
//子进程
*mem = 100;
printf("child, *mem=%d\n",*mem);
sleep(3);
printf("child, *mem=%d\n",*mem);
}
else if(pid > 0)
{
//父进程
sleep(1);
printf("parent, *mem=%d\n",*mem);
*mem = 1001;
printf("parent, *mem=%d\n",*mem);
wait(NULL); //进程回收,如果不想查看回收状态,传出参数NULL
}
munmap(mem,4);
//close(fd);
return 0;
}
输出:
child, *mem=100
child, *mem=1001 //子进程中也相应改变。说明父子进程共享映射区
parent, *mem=100
parent, *mem=1001 //父进程修改了映射区值
6、匿名映射
通过使用可以发现,使用映射区来完成文件读写锁操作十分方便,父子进程间通信也较为容易。但是缺点是,每次创建映射区一定要依赖一个文件才能实现。(有时部分架构系统,如AMD可能会出错,Bus error (core dumped))
通常为了建立映射区,要open一个temp文件,创建好文件后再unlink、close掉,比较麻烦。可直接使用匿名映射区来代替,其实linux系统给提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。同样需要借助标志位参数flag指定:
使用 MAP_ANONYMOUS(或MAP_ANON),如
int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0); //系统提供匿名映射区
“4”随意举例,表示映射区大小,可依实际需要填写。
注意:MAP_ANONYMOUS(或MAP_ANON)这两个宏是linux操作系统特有的宏。
实际上,在linux系统中有一些文件可以作为temp文件使用,不用再使用open创建,/dev/zero 聚宝盆,可以随意映射;/dev/null 无底洞。
int fd = open("/dev/zero",O_RDWR);
7、mmap实现进程间通信
用mmap支持无血缘关系进程通信,flag必须设为MAP_SHARED。
两个程序,一个读,一个写,使用mmap进行通信。
~~小技巧:~/Linux_C$ head -7 mmap_w.c > mmap_r.c 使用该命令可以将mmap_w.c 文件中的7行头文件定向复制到 mmap_r.c 文件中。
//mmap_w.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<fcntl.h>
#include<sys/wait.h>
typedef struct _Student
{
int sid;
char sname[20];
}Student;
int main(int agrc,char *agrv[])
{
if(agrc != 2)
{
printf("./a.out filename\n");
return -1;
}
//第一步:open file
int fd = open(agrv[1],O_RDWR|O_CREAT|O_TRUNC,0666);
int length = sizeof(Student);
ftruncate(fd,length);
//第二步:创建mmap映射区
Student * stu = mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(stu == MAP_FAILED)
{
perror("mmap err");
return -1;
}
//第三步:修改内存数据
int num = 1;
while (1)
{
stu->sid = num;
sprintf(stu->sname,"xiaoming-%03d",num++);
sleep(1); //相当于每隔1s修改一次映射区的内容
}
//第四步:释放映射区和关闭文件描述符
munmap(stu,length);
close(fd);
return 0;
}
//mmap_r.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/mman.h>
#include<fcntl.h>
#include<sys/wait.h>
typedef struct _Student
{
int sid;
char sname[20];
}Student;
int main(int agrc,char *agrv[])
{
//第一步:open file
int fd = open(agrv[1],O_RDWR);
//第二步:创建映射区
int length = sizeof(Student);
Student * stu = mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(stu == MAP_FAILED)
{
perror("mmap err");
return -1;
}
//第三步:read data 读数据
while (1)
{
printf("sid=%d,sname=%s\n",stu->sid,stu->sname);
sleep(1);
}
//第四步:释放映射区和关闭文件描述符
munmap(stu,length);
close(fd);
return 0;
}
~/Linux_C$ ./mmap_w xxx(xxx代表文件名)
打开另一个终端:
~/Linux_C$ ./mmap_r xxx
sid=9,sname=xiaoming-009
sid=10,sname=xiaoming-010
sid=11,sname=xiaoming-011
sid=12,sname=xiaoming-012
sid=13,sname=xiaoming-013
sid=14,sname=xiaoming-014
sid=15,sname=xiaoming-015
sid=16,sname=xiaoming-016
sid=17,sname=xiaoming-017
sid=18,sname=xiaoming-018
......