进程间通信之管道

进程间通信(IPC):操作系统为用户提供的几种进程间的通信方式
进程之间具有独立性:一个进程是不能修改另一个进程内存中的数据的,也不能去读另一个进程的虚拟地址空间中的内容。而且用户的程序也不能访问内核空间。
每个进程都有自己的一份独立的虚拟地址空间。一个进程只能访问自己的虚拟地址空间。
虚拟地址空间有个非常大的好处,那就是使进程间互不干扰,相互独立。
但是,进程的独立性也有一个问题,如果多个进程之间想相互沟通,此时却无法直接进行通信,因此需要操作系统提供一些公共的媒介来让多个进程都能通过访问这个媒介进行通信。
进程间通信是建立在进程独立的前提上的。让多个进程能有一块公共访问的资源,譬如说,内存、文件之类的东西就可以了。

因为进程间通信的场景不同,因此操作系统提供了几种不同的进程间通信方式:

1、管道:用于数据传输,最古老的通信方式。并且是半双工(可以选择方向的单向传输,我可以给你发,你也可以给我发,但是确定方向之后就只能这么发了。还有全双工通信、单工通信:只能她给他发,已经确定方向的单向通信)的通信方式。
管道的本质是操作系统在内核中创建出的一块内存,只要多个进程都能访问到这块内存就可以实现通信了。并且通过io接口实现管道这块内存的操作,因为Linux下一切皆文件。
(1) 匿名管道:这块内存没有名字
(2) 命名管道:这块内存是有名字的
2、System V(阿拉伯数字5) IPC,一套比较古老的标准。
(1) System V消息队列:用于数据传输,了解一下原理就行,因为它的限制很多,现在基本上已经没人用了。
(2) System V共享内存:用于数据共享。
(3) System V信号量:某种程度上的进程控制,在接下来的博客中着重讲解POSIX的信号量,因为SV的信号量比较复杂,用起来比较难,接口也比较恶心。
3、POSIX IPC,定制这套标准是为了跨平台。
(1)消息队列
(2)共享内存
(3)信号量
(4)互斥量
(5)条件变量
(6)读写锁
System V(也是UNIX的一个版本,详见Linux发展史)和POSIX是两套不同的标准
进程间通信都有哪些方式?
除了上面那些,还有其他的方式,其实,只要有一个公共资源能够被多个进程访问到,那就可以完成进程间通信。比如说给一个普通的文件,多个进程是可以打开同一个文件的,借助这个普通文件也能完成进程间通信,只不过不够典型,在此不做过多讨论。

socket也是进程间通信,只不过不是同一台电脑而已。

管道其实就是操作系统内核中的一块缓冲区
Linux源于Unix,而Unix又一切皆文件,将软件资源、硬件设备抽象成文件对其进行管理。
标准输入、标准输出、标准错误当成文件打开,但这三个并不是普通的文件,键盘、显示器,这都是硬件设备,操作系统将其抽象成文件,当作文件来处理。
我们在使用管道的时候也是把它当作文件来使用的。
ps aux | grep
第一个指令的标准输出写到管道,第二个指令从管道这块内存中读取数据来作为它的一个标准输入。其中,ps和grep的父进程都是bash,所以ps和grep之间是兄弟关系。

把程序的运行分成两个部分,虚拟地址也分为两个部分
用户态,用户空间3G,4G总空间
内核态,内核空间1G

匿名管道
#include<unistd.h>
int pipe(int fd[2]);
输出型参数,pipe调用成功,就会返回一对文件描述符放到fd数组中。
成功返回0,失败返回-1,并设置错误码。
一个进程通过系统调用pipe创建出匿名管道,操作系统就会在内核中创建一块没有明确标识的缓冲区,并返回给创建进程两个文件描述符作为管道的操作句柄供进程来操作管道,一个描述符用于从管道中读,另一个用于往管道中写。返回两个文件描述符是为了让用户自己确定半双工的方向。其中,fd[0]表示读端,fd[1]表示写端。
但是匿名管道这块缓冲区没有明确标识,这也就意味着其它进程无法找到该缓冲区,也就无法通信。因此匿名管道只能用于具有亲缘关系的进程间通信,因为子进程能复制父进程的文件描述符表
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
匿名管道特点:
1、只能用于具有亲缘关系的进程,像父子进程、兄弟进程、祖孙进程、叔侄进程。其中,指令中的竖线,ps aux | grep,是兄弟关系,因为ps和grep的父进程都是bash。
2、半双工,在需要双方进行通信的时候,需要建立起两个管道。
3、**进程退出,管道释放。这里的进程是指持有管道的最后一个进程。**当然,你也可以选择将所有进程手动关闭掉那两个文件描述符。
4、内核会对管道(匿名/命名管道)进行同步与互斥。
管道空,读就会阻塞;管道满,写(这个缓冲区65536字节,64K)也会阻塞。
管道读写规则
当没有数据可读时
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
当管道满/剩余的空间不够 的时候
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据,管道中有足够大的剩余空间。
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则 read 返回 0。
如果所有管道读端对应的文件描述符被关闭,则 write 操作会产生信号 SIGPIPE,进而可能导致 write 进程退出。
匿名管道默认是阻塞模式,如果想修改为非阻塞模式,例如:
fcntl(fd[0], F_SETFL, flags | O_NONBLOCK);//设置读的非阻塞
fcntl(fd[1], F_SETFL, flags | O_NONBLOCK);//设置写的非阻塞
在这里插入图片描述 在这里插入图片描述
程序卡住
5、面向字节流,意思就是读和写非常灵活,一次性写10字节,分10次读,或5次读或……,也可以写10次,一次1字节。

互斥:保证操作在同一时间的惟一性/保证对临界资源(公共资源)访问的惟一性,我在操作的时候别人不能操作,互斥只能保证安全,但不能保证合理。我在写的时候,别人不能读,但也不能我一直写吧。
同步:保证操作的时序合理性/保证对临界资源访问的时序合理性,我操作完了,别人再操作,写完之后才能读。
管道(匿名/命名管道)的写(读操作即便超了PIPE_BUF,它最多也只能读出PIPE_BUF带下的数据,因为,如果顺利read()会返回实际读到的字节数,最好能将返回值与参数count作比较,若返回的字节数比要求读取的字节数少,则有可能读到了文件尾、从管道(pipe)或终端机读取,或者是read()被信号中断了读取动作。)数据大小在不超过PIPE_BUF(65536字节)时,内核能保证操作的原子性。如果写入的数据大小超过PIPI_BUF,内核很有可能会把此数据与其它进程的对此管道的写操作交替起来,这样的话,数据就乱了。
POSIX规定,小于PIPE_BUF(ulimit -a)的写操作必须是原子操作:要写的数据应该被连续地写到管道;大于PIPE_BUF的写操作可能是非原子的:内核可能会将数据与其他进程写入的数据交织在一起。
n <= PIPE_BUF,O_NONBLOCK无效:原子的写入n个字节。如果管道当前的剩余空间不足以立即写入n个字节,就阻塞到有足够的空间;
n <= PIPE_BUF,O_NONBLOCK有效:写入具有原子性,如果有足够的空间写入n个字节,write立即成功返回。否则一个都不写,返回-1,并设置errno为EAGAIN;
n > PIPE_BUF,O_NONBLOCK无效:非原子写,可能会和其他的写进程交替写。write阻塞直到将n个字节写入管道;
n > PIPE_BUF,O_NONBLOCK有效:如果管道满,则write失败,返回-1,并将errno设置为EAGAIN;如果不满,则返回写入的字节数为1~n,即部分写入,写入时可能有其他进程穿插写入。

在这里插入图片描述

结论:
1、当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性;
2、当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。

面向字节流优点是传输灵活,但是存在粘包(原因是两条数据间没有明显的间隔)问题。

命名管道
在内核中这块缓冲区是有标识的,意味着所有的进程都可以通过这个标识找到这块缓冲区进行通信,也就是命名管道可以用于同一主机上的任意进程进行通信。
命名管道的标识实际上是一个文件,可见于文件系统,意味着所有进程都可以通过打开文件进而访问到内核中的缓冲区。
mkfifo myfifo:创建命名管道,也可以在程序中用mkfifo函数。
管道文件:p
myfifo并不是命名管道的本体,只是命名管道的入口。
命名管道也是内核中的一块内存。通过上述命令创建管道文件的时候,其实内核中的那块内存还没有呢。
在这里插入图片描述
有了myfifo这个文件之后,我们就可以像操作一个普通文件一样来操作管道文件。
在这里插入图片描述
运行时open阻塞,因为你尝试的是打开一个命名管道文件,并且是按照只读方式打开,此时的打开函数就会阻塞到有其它的进程按照写方式打开。**open在打开一个命名管道文件的时候就具有阻塞的特性。**这个管道必须同时既有人读有人写才能打开,或者你按照写方式打开,也会阻塞到有进程按照读方式打开。
命名管道的打开规则(看open参数2中有没有O_NONBLOCK)
如果当前打开操作是为读而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

在管道的情况下,无论是匿名还是命名管道,把所有写/读的文件描述符关闭,注意是所有,你再尝试读/写(这个操作没意义,操作系统会直接认为这是一个不合理的操作,则write会触发异常(SIGPIPE信号),进程退出),肯定读不到东西,但此时read不会阻塞,这时候读会返回EOF,也就是read的返回值为0。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

int main()
{
	char *fifo = “./test.fifo”;
	umask(0);//我们可以使用此函数来设置允许当前进程创建的文件或目录的最大可操作权限,如果在程序的一开始就加上这样一条语句,那么程序在接下来创建的文件和目录的最大权限都被限制了,只是此处的语句的限制相当于没有限制,因为这里的0就代表权限777,即所有的权限都有,也就是没有屏蔽任何权限。必须要有,否则最终生成文件的权限可能与你设置的不符。
	int ret = mkfifo(fifo, 0664);
	if(ret < 0)
	{
		perror(“mkfifo perror”);
		return -1;
	}
	return 0;
}//运行上面的代码,一旦目录下有同名文件/目录,那程序就直接退出了,并报错,文件已存在,但是进程不应该退出,因为存在了大不了就不用创建了。
int main()
{
	char *fifo = “./test.fifo”;
	umask(0);
	int ret = mkfifo(fifo, 0664);
	if(ret < 0)
	{
		perror(“mkfifo perror”);
		if(errno != EEXIST)//如果文件是因为已存在,那就不能让程序退出。
		return -1;
	}
	return 0;
}//运行上面的代码,一旦目录下有同名文件,那程序就直接退出了,并报错,文件以存在,但是进程不应该退出,因为存在了大不了就不用创建了。其余代码和通过命令mkfifo的一样。

命名管道小结:
1、半双工
2、面向字节流
3、同步与互斥
4、可以是任意进程
5、进程退出,内核中的内存也就释放了,但是命名管道文件还在。
命名管道文件和命名管道压根就不是一回事,前者只是后者的入口,即便文件在也没啥意义了,因为管道的本体已经没了。
匿名管道和命名管道的区别:

匿名管道只能用于具有亲缘关系的进程间通信,而命名管道可以用于同一主机上任意进程间通信。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值