目录
1 概念
虚拟内存技术实现了进程间的隔离,但是有时候需要进行进程间的通信,因为不同进程的虚拟地址空间中内核态的部分是所有进程共享的,因此可以通过内核提供的缓冲区进行数据交换。
注意这里的pipe管道与linux的管道命令 | 不一样, | 是将前边命令的标准输出作为后边命令的标准输入。但本质上,管道命令 | 可以由管道通信实现,前边命令对应的进程将内容写到缓冲区,后边命令对应的进程将内容读出来再写到终端就实现了管道命令
2 pipe管道通信
pipe系统api定义,unistd.h是unix系统内置头文件,包含系统调用等
输入是一个int类型数组,数组长度为2,对应两个文件描述符,返回值也是int类型,成功返回0,失败返回-1并设置errno 这两个文件描述符指向同一个文件,这个文件是内核区创建的管道伪文件,为什么不用普通文件进行通信? 第一文件IO速度慢,第二一边读一边写实现有点麻烦。 如下图所示
管道只能用于有亲缘关系的进程之间的通信,且管道创建在子进程之前,这样当父进程创建管道伪文件的时候,两个文件描述符一个用来写信息,一个用来读信息,如下图中的w和r,当创建子进程的时候,子进程会继承父进程的文件描述符,因此此时子进程也有w和r对应的两个文件描述符。 从图上看,可以让父进程写,子进程读,也可以让子进程写,父进程读,但是同一时刻只能是一个写一个读,即管道通信是一种半双工通信。
3 pipe管道使用例子
运行结果
不出所料,pipe函数给管道伪文件的两个文件描述符是当前最小可用文件描述符3,4 因为read在读设备、管道、网络的时候,默认是阻塞的,因此就算我们在子进程逻辑中加了sleep也会阻塞等待直到读到数据
4 pipe实现ps-grep命令
第一版
ps命令:将所有进程信息输出到STDOUT_FILENO文件描述符对应的文件,默认是终端,毕竟系统调用一般都是操作文件描述符,而不是直接操作文件,linux命令应该也一样吧。 grep:检索命令, grep pattern file,意思是从文件file中用正则表达式匹配pattern grep pattern ,当不指定file文件的时候,默认从终端输入读取内容来匹配,即输入grep pattern则此时会堵塞,直到终端输入pattern则匹配到了,然后把pattern输出到终端之后继续堵塞,等待下一次输入,除非关掉终端
第二版
第一版代码有两个问题 1 子进程死后成为僵尸进程 2 父进程一直没死,相当于处于阻塞状态 第一个问题好解决,只要父进程按时死亡就行了 第二个问题,为什么子进程已经死了,成为僵尸进程了,父进程还没有死?理论上对于grep命令来说,如果输入端文件关闭,那么就应该结束,所以导致这个问题的原因是,输入端没有关闭! 如下图,因为父进程和子进程同时都打开了写端和读端,所以当子进程死亡后,父进程自己还在打开着写端,所以父进程会检测到写端还开着,就会阻塞等待写端的输入。解决方法就是可以把管道的半双工通信特性直接改成单工,单向流动即可
5 管道的读写行为
以下讨论默认没有设置非阻塞
读管道的时候:
写端全部关闭:read会返回0,相当于读到文件末尾
写端没有全部关闭:
有数据:read正常读取
没有数据:read阻塞(可以通过fcntl设置非阻塞,阻塞与非阻塞是与文件描述符关联的)
写管道的时候:
读端全部关闭:产生一个信号,SIGPIPE,程序异常终止
读端未全部关闭:
管道已满:write阻塞
管道未满:write正常写入
管道的本质就是内核区划出来的一段缓冲区,所以有满和不满
管道的本质就是内核区的一段缓冲区,用两个文件描述符来分别读和写这段缓冲区,当然一个进程也可以自己读自己写,即与自身通信,但这样是没有意义的。
6 管道大小和优劣
ulimit -a可以看到系统可以使用的资源 其中有pipe size即为管道缓冲区大小,512*8字节
优点:使用简单
缺点:
只能是有学院关系的进程之间通信,比如父子进程和兄弟进程
本质上只能单向通信,一旦某个管道确定了进程a写进程b读,就不能反过来,如果要双向通信,可以定义两个管道来实现
7 FIFO命令管道进程间通信
PIPE只能实现有血缘关系的进程通信,是匿名管道。
FIFO是先进先出的意思,实际上是通过先进先出的队列来创建了一个管道伪文件,FIFO虽然是Linux基础文件类型的一种,但并不是磁盘上的真正的文件。FIFO可以实现无血缘关系的进程通信,各进程可以打开FIFO文件进行read/write,实际上是读写一个内核通道。 FIFO是有名管道。
创建FIFO伪文件:
mkfifo myfifo命令创建
int mkfifo(const char *pathname, mode_t mode)
内核会针对fifo文件开辟一个缓冲区,不同进程可以通过读写这个fifo文件,即读写这个缓冲区来实现进程间通信,p开头即为管道类型的文件
创建用来写数据的进程:
创建用来读数据的进程:
打开两个终端启动两个进程:
先启动写,再启动读:
当读端关闭的时候,写端也跟着关闭
也可以开多个读对应多个写,即多对多,多对多的时候都是混乱传输的,比如二对二,不存在两两配对的情况
当一个写对应两个读时候,这两个读读出来的内容加一块才是写的,也就是说,read来读fifo命名管道的时候,读的同时就把fifo中的内容取了出来,读出来了以后fifo中就没有了,这与普通文件的读是不一样的。
如果启动写之后,没有启动读,则写进程阻塞
先启动读,再启动写:
这个时候读会阻塞,等待写入
关闭写不会连带关闭读,读会阻塞
阻塞探究 fifo与open
当先启动写,尚未启动读的时候,写进程阻塞,阻塞的是 open函数,即不是最后写入信息的write函数处阻塞,而是在
这里就阻塞了,根本就没能成功打开fifo伪文件
居然是在open的地方阻塞了!
这里牵涉到open函数读写fifo时候的一个特性
如果要打开的是一个fifo文件的读端和写端,那么必须读端和写端都打开了以后open才是返回对应的文件描述符。即打开fifo文件的时候,读端会阻塞等待写端,写端也会阻塞等待读端。
8 mmap文件映射共享IO
基本思想
在磁盘上创建一个文件,通过mmap函数把该文件的一部分映射到一块内存区域,该文件的一部分由offset和length指定,即距离文件开头的偏移以及字节数,这样我们就可以操作内存区域来影响文件,操作内存要比直接操作文件快多了(用memcpy等函数比read快)。
mmap函数定义
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)
void *addr: 直接传NULL或nullptr应该就行
size_t length: 要映射的区域大小,单位字节
int prot: 此参数设置映射内存区域的读写特性
PROT_READ 可读
PTOR_WRITE 可写
一般通过或运算设置可读可写
int flags: 如果进程要通信,必须设置成共享的
MAP_SHARED 共享的,设置为共享的,则修改内存会影响文件
MAP_PRIVATE 私有的,修改内存不影响原文件
int fd: 文件描述符,就是打开的那个磁盘文件的描述符
offset: 映射开始位置到开头的偏移量,设置成0就行
返回值:
成功:返回内存中映射区域的首地址
失败:返回MAP_FAILED
munmap函数定义
释放内存中的映射区域
int munmap(void *addr, size_t length)
void *addr: 传mmap的返回值,即开辟的内存映射区域
size_t length: mmap创建的长度
返回值:成功返回0,失败返回-1
void *类型指针:可以存放任何变量的地址,从void *的角度看,任何的内存空间仅仅是内存空间,我们是不能对void *类型指针解引用的。但是由于void*可以认为是泛型的,可以转化成任意类型,因此 也可以这样 int *mem = mmap() 即用int*来接收映射区地址
使用例子
第一步,创建一个磁盘上文件,并写入一些数据ggg....,可以看到一共写了19各g
第二步,使用mmap和munmap创建和释放内存映射区域
查看磁盘上的文件发现文件被改动了,虽然是用memcpy改的内存区域,但是磁盘上文件也被同步改动了,如果mmap的第四个参数设置为私有的,则改变内存区域不会改变文件。
9 mmap八问
1. 如果改变一下mmap返回的地址mem,再传入munmem函数还能成功释放吗?
答:不能
2. 如果对mem越界操作,比如映射区域是6字节,结果传入了11个字符会怎么样?
答:只要原文件装的下,都能成功写入原文件,但是如果原文件装不下,则会把传入的“hello world”截断写入原文件。但是如果原文件只有6字节,传入11个字符,只要我们把映射区域设置成11个字节,用这11个字节通信完全没问题,只是没办法写入原文件。即通信只与映射区域有关,与原文件无关。
3. 最后一个参数文件偏移量能不能随便填个数?
答:不能,首先这个偏移量不能超过文件大小,其次,有一个很重要的限制,文件偏移量offset必须是4k的整数倍,即0,4096,8192等,这是操作系统的限制
4. 如果用mmap创建映射内存区域以后,把原文件的文件描述符关闭,还能不能通过 修改内存来影响文件?
答:没有影响,因为用mmap函数前边已经把内存区域和原文件之间的通道打通了,因此用完mmap之后在哪里关闭文件描述符完全没影响。
5. open的时候,可以创建一个新的文件来创建映射区域吗?
答:可以创建一个新文件来做映射的原文件,但是这个文件不能是空文件,应该在创建之后扩展一下大小,至少让大小足够用才行。
扩展大小可以用下边这两个函数
6. open的时候选择O_WRONLY可以吗?
答:不可以,因为在创建映射区域的时候隐含了一次读操作
在调用mmap的时候要先读原文件的内容,然后才能映射到内存缓冲区
7. 选择MAP_SHARED的时候,open选择O_RDONLY,prot可以选择 PROT_READ|PROT_WRITE吗?
答:不可以,当选择共享的时候,对内存区域的修改会影响原文件,prot设置的是内存区域的权限,如果内存区域是读写的,那么也就意味着可以向内存区域写入,这个时候由于内存区域与原文件共享,所以也要修改原文件,但是原文件只有读权限,因此出错。 也就是说,当选择共享的时候,原文件的权限一定是包含内存区域的权限的。
8. 如果mmap报错不判断返回值会怎么样?
答:一定要判断返回值!mmap一定要判断返回值!!!
10 mmap实现父子进程之间的通信
应该先创建内存映射,再fork生成子进程,这样两个进程才可以共享内存映射区域
由结果可看出,子进程和父进程修改都生效了
但是要注意共享的不能改成私有的,改成私有的则无法通信
再做一个实现,原文件只有5个字节大,而映射区的大小为40个字节,远大于原文件
结果分析:
原文件5个字节,映射区域40个字节,但是由输出结果可知,父子进程通信是通过内核去内存映射区域进行的,也就是说原文件只要大小不等于0就行了,我们需要多大的内存映射区域可以按需要设定,无需考虑原文件大小,即原文件只是创建内存映射区域的一个媒介,一旦内存映射区域创建完成,原文件就没啥用了。
每次都要创建一个新文件太麻烦了,因此有两个解决方法:
一:用匿名映射,仅限于linux系统
二:用现成的文件/dev/zero
11 匿名映射
前面方法创建映射区必须依赖一个文件,使用匿名映射的方法可以不依赖文件直接创建映射区域
使用匿名映射的话,就不需要再mmap中指定文件描述符了,因为根本不需要打开文件,因此fd参数的位置直接放-1即可
但是MAP_ANONYMOUS 和MAP_ANON是linux系统特有的,有些unix系统没有这个东西
这个时候就需要借助unix系统中一个叫 /dev/zero的文件,还有一个 /dev/null文件也很有趣,但是我们只用zero就行了,如果我们不想自己创建文件去映射,那就直接打开zero这个文件。
12 mmap无血缘关系进程通信
无学院关系进程通信就没办法用匿名映射了,还是需要借助一个文件来创建内存映射区域
写进程:
读进程:
结果分析:
先启动写进程,再启动读进程,则读进程可能不是从写进程的第一条信息开始读的,可能写进程已经改过好几次了,这点与管道和有名管道是不一样的,pipe和fife写东西如果写满了就阻塞,mmap中内存映射区域就是一块进程的共享内存
fifo中如果一个进程用read把数据读走了,则另一个进程就读不到了
mmap中多个读进程可以同时读内存映射区域的内存而读到相同的内容
fifo中读就像水杯接水,接走了那杯水不在管子里了
mmap中读就像相机照相,多个进程可以在共享内存区域照到相同的照片