这里写目录标题
简介
原理
父子进程,对于0到3G的区域,是用户段,映射到内存中是各自独立的
而3G到4G的区域,是内核段,映射到内存中是同一块内存地址空间,而既然是在同一块内存地址空间下,就说明二者可以进行通信,进行数据交流、共享
而进程间通信,简称IPC
方法
我们在两个进程之间搭建一个桥梁,或者一个共享区,进行进程间通信(如上图),而具体的实现方式,有下面常用的四种:
管道(pipe)
简介
管道只能作用于有血缘关系的进程之间,我们可以使用pipe这个函数来创建一个管道,或者在命令行使用命令:mkfifo f1
但是其本质上是一个伪文件,不是真正存在磁盘空间中的文件,他实际上是一个内核缓冲区
其内部实现是一个队列,是一个循环队列,借助内核缓冲区实现,他在两端分别有一个读端和一个写端
(socket也就是一个伪文件)
局限性:
管道必须两个端,读端和写端都准备好了,才可以进行数据通信,其中任何一个进程无法单方面决定数据的通信
且管道由于是一个循环队列,其数据不可以反复读取,一旦读走,管道中的数据不再存在(虽然借助缓冲区实现,缓冲区的数据是可以重复读的,但是由于使用了队列机制,所以无法进行反复读取)
且管道是一个双向半双工,半双工,就是数据只能单方向流动,无法同时进行双向通信,但是由于其流动方向可以改变,所以是双向半双工
(socket是双向全双工)
最后一个就是,只有在有公共祖先的进程,或者说有血缘关系的进程之间才能使用管道
总结:
pipe函数
介绍
代码
由前面可知,父子进程之间会共享全局变量、栈区、…的内容,所以,在fork()执行之后,前面的变量,子进程仍然可以使用
而在fork之前,已经使用fd[2]+pipe()创建出了一个管道,并把他的读端和写端存在了fd数组里,其中,fd[0]是读端,fd[1]是写端
而fork之后,子进程也会有fd[2],如上图
而我们需要将其加工成如下图:
代码:
父进程中,我们关闭读端,让父进程写数据,写完之后,将写端关闭
子进程中,我们关闭写端,接收父进程数据,写到屏幕上,读完之后,将读端关闭
管道的读写行为
其中,关于没有写端、没有读端,是指fd[0] 、 fd[1]被关闭, 若其没有被关闭,哪怕没有任何的读写行为,都会认为有读端和有写端,与是否有write、read无关
练习
需求
实现ls | wc -l
该命令可以统计当前目录下有多少个文件/文件夹
代码
其中,对于dup2:
在父进程,表示本来是向标准输出写,重定向到向fd[1]写
在子进程,表示本来是从标准输入读,重定向到从fd[0]读
这里我们不再进行fd的关闭,因为execlp执行后,下面的代码无法执行
但是有时会出现如下情况:
也就是父进程先执行完,输出了主机名,之后子进程输出了2
常规思路是,我们在父进程所有操作执行完之后,进行sleep,但是这里的execlp,在执行过后,如果执行成功,父进程就到此为止了,不会向下执行,所以,该常规方法也不可行
解决方案:
我们将父子进程中的代码交换位置,由父进程负责输出,那么现在只有父进程有输出,父进程执行完输出后,父进程才结束,bash才会输出主机名,这样,就保证了bash不会比程序输出更早:
练习(兄弟进程)
代码:
我们在父进程进行两次进行回收,由于wait会一直阻塞,所以,这里不会发生bash先输出主机名的情况
但是,我们发现,运行之后,没有任何的输出,接下来我们来分析一下是哪里出错
纠错:
通过画图我们发现,虽然我们兄弟进程之间建立了管道的数据流通,但是父进程还在连接着管道,而管道要保证其单入单出,所以,要将父进程的读写都进行关闭
效果:
补充
对于上面有一个思考:
关于这个问题,可以回答:
pipe可以是多个写端,一个读端,因为他是消息队列,所以,这个实现方式就有点像实习时的写线程的消息队列+读线程的阻塞监听
但是我们如果不加以调控,他们写到消息队列中的先后顺序无法保证
补充2:
总结
命名管道(FIFO)
简介
原理
其实FIFO的原理与普通管道的通信原理一样,都是在内核创建一个缓冲区
原型
参数一:管道文件的名称,可以指定路径,精确到文件本身,如果没有前面的路径,那么就是管道名即可
(这是与前面的pipe不同的地方,因为他是命名管道,所以他有名字,可以进行名字的设置)
参数二:代表创建出来的管道的权限,与open打开文件一样,他的权限也是经过umask掩码后的结果
返回值:成功:0
失败:-1 errno
代码:创建管道
这样就得到了一个管道文件,该文件与pipe一样,都是一个伪文件
代码:利用FIFO进行进程间通信
此时我们已经创建出了一个fifo文件,那么接下来利用该文件进行无血缘关系的进程间通信
左边是写进程,右边是读进程
实际上我们可以看到,整个代码都在不断地进行文件IO,实际上就是文件的读写操作,我们之前使用命令行mkfifo或者使用函数mkfifo创建出来的fifo文件就是两个毫无关系的进程之间通信的桥梁
多个写端,一个读端:
一个写端,多个读端:
两个读端读的时候可以多sleep几秒,这样可以看清楚读的顺序
这两个读端,依次交替进行数据的读取
当然这里是先创建出来了一个fifo文件,我们也可以将这一步操作使用函数mkfifo在程序运行中去创建,创建出来之后拿到文件名,之后就是如上图所示,进行文件的open、 IO
使用文件
父子进程
这里是父子进程,通过文件进行通信的代码,其中父进程sleep一秒,保证父进程可以先把数据写进去
这里是先fork创建出进程之后,父子进程分别进行文件的打开,然后进行数据的读写,这样是稳妥的,但是如果先打开文件,然后进行fork,也是可以的,因为父子进程之间也会共享文件描述符
如果,父进程等待3秒再去写数据,那么子进程的read会不会因为阻塞而等待3秒后读父进程写来的数据呢:
结果(注意先将test.txt文件清空):
可以看到,read并没有读到数据,
因为:这是一个普通文件,没有阻塞/非阻塞的特性,这里要注意一个点,即阻塞非阻塞是文件的特性,不是read的特性,只有设备文件或者网络socket文件才会有阻塞非阻塞的相关设置与讨论,而除此之外,read读的时候,不会进行阻塞等待,所以此处不会读到数据
无血缘关系的进程
对于无血缘关系的进程,与父子进程的区别是,其打开同一份文件,对应的文件描述符序号不同,所以,要分别进行打开,也只能分别进行打开,因为二者无法共享文件描述符
之后,上图代码的逻辑是,先让1进程写数据到文件test.txt,之后让2进程读数据并打印到屏幕,然后再让2进程写数据到文件,此时不更改文件的读写位置,在2进程写数据期间,1进程进行sleep,然后1进程将读写位置改到起始位置,进行数据的读取,然后打印到屏幕
这里注意一个点:在文件没有被关闭时,其读写位置是保留当前位置的,而如果文件被关闭了重新打开,那么读写位置就会重新刷新,会根据打开的模式进行刷新,例如:如果默认打开,则在文件的开头,如果追加打开,则在文件的末尾
mmap
简介
函数介绍
参数一:可以指定最终映射区的首地址,也可以传入NULL,让系统去分配
参数二:映射区内存的大小(一般是小于等于要映射的文件的大小)
参数三:映射区的读写属性
参数四:标注共享内存的共享属性
参数五:用于创建映射区的那个文件的 文件描述符
参数六:指定偏移位置,默认是0
参数二和参数六是配合使用的,当参数二的大小是整个文件的大小,那么参数六传入0即可,当参数二的大小比整个文件要小,也就是要采取文件的一部分进行映射,那么就可以指定文件的偏移位置,表示从文件的哪个位置开始取一部分大小的内容进行映射