操作系统:进程间通信


每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。

在这里插入图片描述

进程间通信目的一般有共享数据,数据传输,消息通知,进程控制等。以Unix/Linux为例,介绍几种重要的进程间通信方式:共享内存,管道,消息队列,信号量,信号。

使用ipcs命令即可查看IPC信息,可跟接的选项有:

  • -a或者什么都不跟:消息队列、共享内存、信号量的信息都会显示出来
  • -m:只显示共享内存的信息
  • -q:只显示消息队列的信息
  • -s:只显示信号量的信息

管道

$ ps auxf | grep mysql

上面命令行里的竖线就是一个管道,它的功能是将前一个命令(ps auxf))的输出,作为后一个命令(grep mysql)的输入,从这功能描述,可以看出管道传输数据是单向的,如果想相互通信,我们需要创建两个管道才行。
同时,我们得知上面这种管道是没有名字,所以|表示的管道称为匿名管道,用完了就销毁。管道还有另外一个类型是命名管道,也被叫做FIF0,因为数据是先进先出的传输方式。
在使用命名管道前,先需要通过mkfifo命令来创建,并且指定管道名字:

$ mkfifo myPipe

myPipe就是这个管道的名称,基于Linux一切皆文件的理念,所以管道也是以文件的方式存在,我们可以用ls看一下,这个文件的类型是p,也就是pipe (管道)的意思:

$ ls -l      
prw-r--r-- 1 root root 0 Nov  9 21:00 mypipe 

接下来,我们往myPipe这个管道写入数据:

$ echo "hello" > myPipe //将数据写进管道

你操作了后,你会发现命令执行后就停在这了,这是因为管道里的内容没有被读取,只有当管道里的数据被读完后,命令才可以正常退出。

于是,我们执行另外一个命令来读取这个管道里的数据:

 $ cat < myPipe //读取管道里的数据
 hello

可以看到,管道里的内容被读取出来了,并打印在了终端上,另外一方面,echo那个命令也正常退出了。

我们可以看出,管道这种通信方式效率低,不适合进程间频繁地交换数据。当然,它的好处,自然就是简单,同时我们也很容易得知管道里的数据已经被另一个进程读取了。
我们可以得知,对于匿名管道,它的通信范围是存在父子关系的进程。因为管道没有实体,也就是没有管道文件,只能通过fork 来复制父进程fd文件描述符,来达到通信的目的。

如何操作管道

以文件的方式来读写管道,以文件方式来操作管道时

  1. 需要有读写用的文件描述符
  2. 读写时会用write、read等文件按IO函数

匿名管道

函数原型

#include <unistd.h>
int pipe(int pipefd[2]);
  • 功能:创建一个用于父子进程之间通信的无名管道(缓存)﹐并将管道与两个读写文件描述符关联起来。
  • 参数:读写管道的文件描述符
    元素[0]:里面存放的是读文件描述符
    元素[1]:里面存放的是写文件描述符
  • 返回值:成功返回0,失败返回-1,并设置errno

特点:

  1. 无名管道只能用于亲缘进程之间通信。由于没有文件名,因此进程没办法使用open打开管道文件,从而得到文件描述符,所以只有一种办法,那就是父进程先调用pipe创建出管道,并得到读写管道的文件描述符。然后再fork出子进程,让子进程通过继承父进程打开的文件描述符,父子进程就能操作同一个管道,从而实现通信。
  2. 读管道时,如果没有数据的话,读操作会休眠(阻塞)

父子进程单向通信

实现步骤:

(a)父进程在fork之前先调用pipe创建无名管道,并获取读、写文件描述符
(b)fork创建出子进程,子进程继承无名管道读、写文件描述符
(c)父子进程使用各自管道的读写文件描述符进行读写操作,即可实现通信

父子进程双向通信

创建两个管道,一个用来读,一个用来写。

有名管道

不管是有名管道,还是无名管道,它们的本质其实都是一样的,它们都是内核所开辟的一段缓存空间。进程间通过管道通信时,本质上就是通过共享操作这段缓存来实现,只不过操作这段缓存的方式,是以读写文件的形式来操作的。

特点

  • 能够用于非亲缘进程之间的通信
    因为有文件名,所以进程可以直接调用open函数打开文件,从而得到文件描述符,不需要像无名管道一样,必须在通过继承的方式才能获取到文件描述符。
    所以任何两个进程之间,如果想要通过"有名管道"来通信的话,不管它们是亲缘的还是非亲缘的,只要调用open函数打开同一个"有名管道"文件,然后对同一个"有名管道文件"进行读写操作,即可实现通信。

  • 读管道时,如果管道没有数据的话,读操作同样会阻塞(休眠)

  • 当进程写一个所有读端都被关闭了的管道时,进程会被内核返回SIGPIPE信号

使用步骤

  1. 进程调用mkfifo创建有名管道
  2. open打开有名管道
  3. read/write读写管道进行通信

对于通信的两个进程来说,创建管道时,只需要一个人创建,另一个直接使用即可。
为了保证管道一定被创建,最好是两个进程都包含创建管道的代码,谁先运行就谁先创建,后运行的发现管道己经创建好了,那就直接open打开使用。

函数原型

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname,mode_t mode);
  1. 功能
    创建有名管道文件,创建好后便可使用open打开。
  2. 参数
    • pathname:被创建管道文件的文件路径名。
    • mode:指定被创建时原始权限,一般为0664 (110110100),必须包含读写权限。
  3. 返回值:成功返回0,失败则返回-1,并且errno被设置。
int ret=mkfifo(fifoname,0664);
if(ret==-1&&errno!=EEXIST) printf("mkfifo fail");
fd=open(fifoname,open_mode);

SIGPIPE信号

写管道时,如果管道的读端被close了话,向管道"写"数据的进程会被内核发送一个SIGPIPE信号,发这个信号的目的就是想通知你,管道所有的"读"都被关闭了。

由于这个信号的默认动作是终止,所以收到这个信号的进程会被终止,如果你不想被终止的话,你可以忽略、捕获、或者屏蔽这个信号。

只有当管道所有的读端都被关闭时,才会产生这个信号,只有还有一个读端开着,就不会产生。

消息队列

消息队列的本质
消息队列的本质就是由内核创建的用于存放消息的链表,由于是存放消息的,所以我们就把这个链表称为消息队列。
通信的进程通过共享操作同一个消息队列,就能实现进程间通信。

消息队列这个链表有很多的节点,链表上的每一个节点就是一个消息。

收发数据的过程

发送消息

  1. 进程先封装一个消息包
    这个消息包其实就是如下类型的一个结构体变量,封包时将消息编号和消息正文写到结构体的成员中。

    struct msgbuf
    {
         
    	long mtype;/*放消息编号,必须>0*/
    	char mtext[msgsz]; /*消息内容(消息正文)*/
    };
    
  2. 调用相应的API发送消息
    调用API时通过"消息队列的标识符"找到对应的消息队列,然后将消息包发送给消息队列,消息包( 存放消息的结构体变量)会被作为一个链表节点插入链表。

接收消息

调用API接收消息时,必须传递两个重要的信息,

  • 消息队列标识符
  • 你要接收消息的编号

有了这两个信息,API就可以找到对应的消息队列,然后从消息队列中取出你所要编号的消息,如此就收到了别人所发送的信息。
"消息队列"有点像信息公告牌,发送信息的人把某编号的消息挂到公告牌上,接收消息的人自己到公告牌上去取对应编号的消息,如此,发送者和接受者之间就实现了通信。

消息队列的使用步骤

  1. 使用msgget函数创建新的消息队列、或者获取已存在的某个消息队列,并返回唯一标识消

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Shilong Wang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值