Linux进程间通信(一):管道与mmap文件-内存映射
来源:互联网 发布:留学美国北大gpa算法 编辑:程序博客网 时间:2024/06/06 01:53
一、无名管道、有名管道与进程间通信:
1、IPC–进程间通信与管道基本概念:
(1)、IPC(进程间通信):
所谓IPC就是两个或者多个进程之间的数据交互(在不能直接进行信息交互的两个进程间增加一个“交互媒介”以达到信息交互的目的)。为什么不能直接交互?因为我们知道在应用程序执行时(即进程运行时),其占有的用户空间只有0~3G,而用户空间不共享,不共享就无法传递信息;内核空间共享,所以要实现两个进程之间的信息交互即通信,就必须通过内核空间。
IPC的方法:
①文件;
②信号(signal);
③管道;
④共享内存;
⑤消息队列;
⑥信号量集(semaphore)(与信号无关);
⑦套接字socket
今天我们就“文件–内存”与“管道”总结一下,无论是有名管道还是无名管道、也不管是消息队列还是信号量集、套接字,其本质都是在内核中实现的,而我们只是在调用一个内核提供的接口或方法。
(2)、管道基本特性:
管道文件只是媒介,只是数据的中转站,只有读写双方均就绪时才畅通,只有一方就绪时处于阻塞状态,其大小始终为0,其基本模型 如图所示:
两个进程分别持有内核管理的管道的读端与写端的权限,并且管道是单向的,或者说是单双工的。
简单测试(打开两个终端测试,file.pipe为一个管道文件关于其创建,之后会提到):
测试①
第一步:echo message > file.pipe(输入重定向到管道中,处于阻塞)
第二步:cat file.pipe(管道畅通,cat进程输出message)两个步骤是两个shell创建的子进程进行通信。
如果不执行第二步(不敲回车),则shell一直处于后台,echo写端则处于阻塞状态,当第二步执行时(敲回车以后),通过管道在两个shell的子进程之间(echo和cat两个进程)传递信息。
结果如图所示:
测试②
第一步:cat file.pipe(处于阻塞)
第二步:echo message > file.pipe(管道畅通,cat进程输出message)
与第一个测试相似,只不过测试①是开始读端未打开,写端处于阻塞状态。测试②是写端未打开,读端处于阻塞状态。
结果如图所示:
2、pipe无名管道与FIFO有名管道:
无名管道:由内核创建,只用于fork()创建的父子进程之间的通信;
有名管道:由程序员建立管道文件,用于进程间通信(管道文件是程序员创建,但是管道依旧是内核创建并且管理),前面测试的便是有名管道。
(1)、pipe无名管道:
#include<unistd.h>int pipe(int filedes[2]);//创建无名管道,由内核维护,且无名管道只能用于fork创建的父子进程之间通信(作用于有血缘关系的进程间通信)。
pipe函数的参数是一个整型数组,该数组包含两个文件描述符:一个写描述符fd[0],一个读描述符fd[1]。如图所示为单进程的管道信息传递:
pipe管道使用注意的四种情况:
①当写端关闭,读端读完管道里内容时read返回0,相当与读到文件末尾;
②写端未关闭,但是写端暂无数据,读端读完管道中数据后便阻塞;
③读端关闭,写管道的进程会收到一个SIGPIPE信号,写进程终止;
④读端未读管道数据,当写端写满数据后,再次写会阻塞。
对于fork()创建的父子进程之间的文件描述符与管道的关系如图所示:
代码实现无名管道进程间通信:
/*父写子读,关闭父进程的fd[1]和子进程的fd[0]*/#include <stdio.h>#include <string.h>#include <unistd.h>#include <stdlib.h>#include <fcntl.h>#include <errno.h>void sys_err(const char * ptr){ perror(ptr); exit(EXIT_FAILURE);}int main(void){ int fd[2]; //fd[0] 读端,fd[1] 写端 char str[1024] = "hello world!"; char buf[1024]; if(pipe(fd) < 0)/*无名管道创建失败判断*/ sys_err("pipe"); pid_t pid = fork(); if(pid < 0) sys_err("fork"); if(pid > 0){ close(fd[0]);//父进程里,关闭父进程的读端 sleep(5); write(fd[1], str, strlen(str)); close(fd[1]);//写完之后关闭写端 wait(NULL);//等待子进程结束回收子进程PCB资源,防止产生僵尸进程 } else if(pid == 0){ int len, flags; close(fd[1]);//子进程里,关闭子进程写端 flags = fcntl(fd[0], F_GETFL); flags |= O_NONBLOCK;//默认是阻塞读取,改为不阻塞 fcntl(fd[0], F_SETFL, flags); tryagain: len = read(fd[0], buf, sizeof(buf)); if(len == -1){//等于-1,说明父进程没有向管道中写数组,子进程由于不阻塞,因此循环执行 if(errno == EAGAIN){//EAGAIN信号表示读取返回-1的原因是没有数据可读,要求再试一次,那么久打印try again后再试一次 write(STDOUT_FILENO, "try again\n", 10); sleep(1); goto tryagain; }else//如果不是因为没有数据可读,就是读取出错了,就不同在循环读取了 sys_err("read"); } write(STDOUT_FILENO, buf, len);//将读取到的内容输入到标准输出上 close(fd[0]);//然后关闭子进程的读端} return 0;}
测试结果如图所示:
由于父进程睡眠5秒,而子进程循环一次睡眠一秒,所以在打印出hello world!之前会打印五次tryagain。最终我们看到,父进程可以通过无名管道给子进程发送信息。并且阻塞与否可以通过fcntl函数修改。
(2)、FIFO有名管道:
管道文件的创建:管道是通过管道文件(媒介)进行进程间信息交互的,管道文件与普通文件是有区别的,通过mkfifo(make first in first out)或者mkfifo()创建管道文件。其他方式是无法创建管道文件的,管道文件后缀是”.pipe”(类型为p)即使是touch file.pipe也不行。我们知道后缀名是无关紧要的,但是一定要使用mkpipe或者mkpipe()创建管道文件。
对于有名管道,必须先有管道文件才能进行通信。所以我们在程序中创建/使用管道文件时必须先用S_ISFIFO()判断某个文件是不是管道文件。该宏函数是用来判断stat()函数获取的struct stat{}结构体中的mode_t mode参数存储的文件类型。关于struct stat{}结构体以及stat()函数的基本形式,可参考: Linux&C编程之Linux系统命令“ls -l”的简单实现
S_ISFIFO(m) /*判断是否是管道文件,m即为mode参数*/
有名管道本质:无“血缘关系”的两个进程通过name.pipe管道文件找到内核中的pipe管道,进而实现无血缘关系的管道进程间通信。
有名管道的图解如下所示:
代码实现有名管道进程间通信:
/****头文件省略,sys_err函数省略fifo_w.c有名管道写端****/int main(int argc, char *argv[])//传递管道文件{ int fd; char buf[1024] = "hello world\n"; if(argc < 2){ printf("./fifo_w name.pipe\n"); exit(EXIT_FAILURE); } fd = open(argv[1], O_WRONLY); if(fd < 0) sys_err("open"); write(fd, buf, strlen(buf)); close(fd); return 0;}
/***头文件省略,sys_err函数省略fifo_r.c有名管道读端***/int main(int argc, char *argv[]){ int fd, len; char buf[1024]; if(argc < 2) { printf("./fifo_r name.pipe\n"); exit(EXIT_FAILURE); } fd = open(argv[1], O_RDONLY); if(fd < 0) sys_err("open"); len = read(fd, buf, sizeof(buf)); write(STDOUT_FILENO, buf, len); close(fd); return 0;}
测试结果:
管道只在内核中占用一小部分内存,而管道文件不会在磁盘上占用空间(管道文件的PCB在内核中占用内存,只消耗一个inode)。如图:
二、mmap文件-内存映射与进程间通信:
1、mmap介绍:
对于mmap不进行文件映射的操作可参考: 系统调用与内存管理(sbrk、brk、mmap、munmap)
mmap函数可以把磁盘文件的一部分直接映射进内存,这样文件的位置就有了对应的地址,对于文件的都写可以直接使用指针,而不需要read与write。
void * mmap(void * addr, size_t length, int port, int flags, int fd, off_t offset);//addr:为映射的内存起始位置,设置为NULL操作系统自动分配;//length:映射的长度;//port:内存访问权限,PORT_NONE、PORT_EXEC、PORT_READ、PORT_WRITE//flags:属性,MAP_SHARED(磁盘/内存任意一处修改同步到另外一处)、//MAP_PRIVATE(磁盘/内存任意一处修改不影响另外一处);//fd:文件描述符(映射文件已打开),如不映射文件,只申请内存时值为-1;//offset:偏移量,4096整数倍,一般先lseek确定位置然后置将offset为0即可//返回值:返回系统分配的addr起始地址,失败返回MAP_FAILED。int munmap(void * addr, size_t length);//参数与mmap对应
2、简单测试:
将文件中的内容映射到内存中,并修改内存以同步到文件中,观察文件中内容是否变化:
/****mmap.c头文件省略,sys_err函数省略*****/int main(void){ int fd, len, *p; fd = open("hello", O_RDWR); if(fd < 0) sys_err("open"); len = lseek(fd, 0, SEEK_END); p = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);//进行内存映射 if(p == MAP_FAILED) sys_err("mmap"); close(fd);//释放file结构体 p[0] = 0x30313233; munmap(p, len);//解除映射 return 0;}//注意:close(fd);并不会解除映射,close只是将file结构体计数减1,并不会对映射关系产生影响。
测试结果:
通过p[0]四个字节的空间修改了hello文件中的前四个字符(小端存储):
3、mmap实现进程间通信:
/*mmap.h*/#ifndef _MMAP_H_#define _MMAP_H_#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <sys/mman.h>#include <unistd.h>#include <stdlib.h>#include <fcntl.h>/*mmap文件大小为4K*/#define MAPLEN 0x1000 /*发送的信息结构体*/struct STU { int id; char name[20]; char sex;};#endif
/*process_mmap_w.c*/#include"mmap.h"int main(int argc, char *argv[]){ struct STU *mm; int fd, i = 0; if (argc < 2) { printf("./a.out filename\n"); exit(1); } fd = open(argv[1], O_RDWR | O_CREAT, 0777); if(fd < 0) sys_err("open"); /*将创建的空文件扩大至4KB-1*/ if(lseek(fd, MAPLEN-1, SEEK_SET) < 0) sys_err("lseek"); /*将扩大的文件末尾写一次数据保证扩大有效*/ if(write(fd, "\0", 1) < 0) sys_err("write"); /*文件到内存的映射*/ mm = mmap(NULL, MAPLEN, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); if(mm == MAP_FAILED) sys_err("mmap", 2); /*映射完毕后便可以关闭fd,回收file结构体了(PCB资源)*/ close(fd); while(1){ mm->id = i; sprintf(mm->name, "name number:%d", i); if(i % 2 == 0) mm->sex = 'm'; else mm->sex = 'w'; i++; sleep(1); } munmap(mm, MAPLEN);/*解除文件到内存的映射*/ return 0;}
/*process_mmap_r.c*/#include"mmap.h"int main(int argc, char *argv[]){ struct STU *mm; int fd, i = 0; if(argc < 2) { printf("./a.out filename\n"); exit(1); } fd = open(argv[1], O_RDWR); if(fd < 0) sys_err("open"); mm = mmap(NULL, MAPLEN, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); if(mm == MAP_FAILED) sys_err("mmap", 2); close(fd); unlink(argv[1]);/*由于创建的用于通信的文件应当是临时文件,所以用unlink删除一个硬链接*/ while(1){ printf("%d\t", mm->id); printf("%s\t", mm->name); printf("%c\n", mm->sex); sleep(1); } munmap(mm, MAPLEN); return 0;}
unlink知识点:
unlink(const char * pathname);
删除一个链接:
如果是硬链接,硬链接数减1,当计数减为0时,释放数据块与inode。
如果文件链接数为0,当时有进程已经打开该文件,并且持有文件描述符,则等待该进程关闭文件时,kernel采取真正删除该文件。
利用该特性创建临时文件:先open/create创建一个文件,然后unlink()。上例中由于要先用fd映射内存,所在在映射完内存后unlink即可。
测试结果:
unlink将临时文件删除,所以ls显示无该文件。
两个进程在各自的虚拟地址空间映射了共享文件,其映射关系如图所示:
4、mmap进程间通信注意事项:
①open权限、文件本身权限、mmap设置权限多种权限都可能导致出错。且要进行进程间通信必须以可读可写的方式打开。
②如果mmap的内存映射实现进程间通信需要写一次就刷新,否则写的进程写的内容可能在内核缓冲区,读的进程读的时候在缓冲区读到,就直接在内核缓冲区中读取,而去共享文件中读了。
③mmap的缺点:通信双方都可以写,数据可能会出错。
④创建的用于通信的文件应当是临时文件,采用ulink()进行设置:
mmap()–>close(fd)–>ulink(path)
创建文件open(O_CREAT)与删除文件unlink()一般都放在写的进程中。
⑤用于进程间通信的数据类型一般设置为结构体。
⑥如果文件大小为0,而映射大小不为0,会有总线错误。
- Linux进程间通信(一):管道与mmap文件-内存映射
- Linux\Unix IPC进程通信实例分析(一):共享内存通信---文件映射mmap方式
- Linux 管道、mmap文件映射
- Linux进程间通信--mmap共享内存(一)
- linux进程通信(一)--共享内存--mmap()
- 【linux开发】进程间通信命名管道-共享内存-内存映射-消息队列-信号量
- linux进程利用mmap映射区通信
- linux 进程间通信一(管道与信号)
- 内存映射文件与SendMessage进程间通信写写
- Linux进程间通信(一):管道
- linux进程间通信(一)--------管道
- Linux进程间通信(一)管道
- Linux进程间通信(一)-------管道
- Linux进程间通信源码剖析,共享内存(mmap)
- Linux进程间通信源码剖析,共享内存(mmap)
- Linux进程间通信--mmap()共享内存(二)
- linux 中mmap的用法(进程间通信-共享内存)
- mmap的使用之两个进程通过映射普通文件实现共享内存通信
- DNS & CDN & HTTPDNS 原理简析
- Linux Shell脚本(正则/Sed/AWK)
- Windows 网络命令
- KMP
- 2.16
- Linux进程间通信(一):管道与mmap文件-内存映射
- MySQL高级查询
- 文章标题
- poj2482 Stars in Your Window
- Android-----Android架构思考(模块化、多进程)
- Codeforces 430C Xor-Tree DFS,奇偶性
- 日语能力考试二级语法
- 最新的AS下载地址,更新及时
- 数据结构——深度优先遍历和广度优先遍历