进程间通讯的方式——信号、管道、消息队列、共享内存

多进程:

首先,先来说一下fork以后,发生了什么事情。node

由fork建立的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新进程(子进程)的进程 id。将子进程id返回给父进程的理由是:由于一个进程的子进程能够多于一个,没有一个函数使一个进程能够得到其全部子进程的进程id。对子进程来讲,之因此fork返回0给它,是由于它随时能够调用getpid()来获取本身的pid;也能够调用getppid()来获取父进程的id。(进程id 0老是由交换进程使用,因此一个子进程的进程id不可能为0 )。linux

fork以后,操做系统会复制一个与父进程彻底相同的子进程,虽然说是父子关系,可是在操做系统看来,他们更像兄弟关系,这2个进程共享代码空间,可是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也彻底相同,子进程拥有父进程当前运行到的位置两进程的程序计数器pc值相同,也就是说,子进程是从fork返回处开始执行的),但有一点不一样,若是fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,若是fork不成功,父进程会返回错误。
能够这样想象,2个进程一直同时运行,并且步调一致,在fork以后,他们分别做不一样的工做,也就是分岔了。这也是fork为何叫fork的缘由算法

至于那一个最早运行,可能与操做系统(调度算法)有关,并且这个问题在实际应用中并不重要,若是须要父子进程协同,能够经过原语的办法解决。shell


 

常见的通讯方式:

1. 管道pipe:管道是一种半双工的通讯方式,数据只能单向流动,并且只能在具备亲缘关系的进程间使用。进程的亲缘关系一般是指父子进程关系。
2. 命名管道FIFO:有名管道也是半双工的通讯方式,可是它容许无亲缘关系进程间的通讯。
4. 消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
5. 共享存储SharedMemory:共享内存就是映射一段能被其余进程所访问的内存,这段共享内存由一个进程建立,但多个进程均可以访问。共享内存是最快的 IPC 方式,它是针对其余进程间通讯方式运行效率低而专门设计的。它每每与其余通讯机制,如信号两,配合使用,来实现进程间的同步和通讯。
6. 信号量Semaphore:信号量是一个计数器,能够用来控制多个进程对共享资源的访问。它常做为一种锁机制,防止某进程正在访问共享资源时,其余进程也访问该资源。所以,主要做为进程间以及同一进程内不一样线程之间的同步手段。
7. 套接字Socket:套解口也是一种进程间通讯机制,与其余通讯机制不一样的是,它可用于不一样及其间的进程通讯。
8. 信号 ( sinal ) : 信号是一种比较复杂的通讯方式,用于通知接收进程某个事件已经发生。异步

 

信号:

信号是Linux系统中用于进程之间通讯或操做的一种机制,信号能够在任什么时候候发送给某一进程,而无须知道该进程的状态。若是该进程并未处于执行状态,则该信号就由内核保存起来,知道该进程恢复执行并传递给他为止。若是一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。函数

 

Linux提供了几十种信号,分别表明着不一样的意义。信号之间依靠他们的值来区分,可是一般在程序中使用信号的名字来表示一个信号。在Linux系统中,这些信号和以他们的名称命名的常量被定义在/usr/includebitssignum.h文件中。一般程序中直接包含<signal.h>就好。spa

 

信号是在软件层次上对中断机制的一种模拟,是一种异步通讯方式,信号能够在用户空间进程和内核之间直接交互。内核也能够利用信号来通知用户空间的进程来通知用户空间发生了哪些系统事件。信号事件有两个来源:操作系统

1)硬件来源,例如按下了cltr+C,一般产生中断信号sigint线程

2)软件来源,例如使用系统调用或者命令发出信号。最经常使用的发送信号的系统函数是kill,raise,setitimer,sigation,sigqueue函数。软件来源还包括一些非法运算等操做。设计

 

一旦有信号产生,用户进程对信号产生的相应有三种方式:

1)执行默认操做,linux对每种信号都规定了默认操做。

2)捕捉信号,定义信号处理函数,当信号发生时,执行相应的处理函数。

3)忽略信号,当不但愿接收到的信号对进程的执行产生影响,而让进程继续执行时,能够忽略该信号,即不对信号进程做任何处理。

  有两个信号是应用进程没法捕捉和忽略的,即SIGKILL和SEGSTOP,这是为了使系统管理员能在任什么时候候中断或结束某一特定的进程。

上图表示了Linux中常见的命令

一、信号发送:

信号发送的关键使得系统知道向哪一个进程发送信号以及发送什么信号。下面是信号操做中经常使用的函数:

例子:建立子进程,为了使子进程不在父进程发出信号前结束,子进程中使用raise函数发送sigstop信号,使本身暂停;父进程使用信号操做的kill函数,向子进程发送sigkill信号,子进程收到此信号,结束子进程。

二、信号处理

当某个信号被发送到一个正在运行的进程时,该进程即对次特定的信号注册相应的信号处理函数,以完成所需处理。设置信号处理方式的是signal函数,在程序正常结束前,在应用signal函数恢复系统对信号的

默认处理方式。

3.信号阻塞

有时候既不但愿进程在接收到信号时马上中断进程的执行,也不但愿此信号彻底被忽略掉,而是但愿延迟一段时间再去调用信号处理函数,这个时候就须要信号阻塞来完成。

 

例子:主程序阻塞了cltr+c的sigint信号。用sigpromask将sigint假如阻塞信号集合。

 

管道:

管道容许在进程之间按先进先出的方式传送数据,是进程间通讯的一种常见方式。

管道是Linux 支持的最初Unix IPC形式之一,具备如下特色:

1) 管道是半双工的,数据只能向一个方向流动;须要双方通讯时,须要创建起两个管道

2) 匿名管道只能用于父子进程或者兄弟进程之间(具备亲缘关系的进程);

3) 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,而且只存在与内存中。

 

管道分为pipe(无名管道)和fifo(命名管道)两种,除了创建、打开、删除的方式不一样外,这两种管道几乎是同样的。他们都是经过内核缓冲区实现数据传输。

  • pipe用于相关进程之间的通讯,例如父进程和子进程,它经过pipe()系统调用来建立并打开,当最后一个使用它的进程关闭对他的引用时,pipe将自动撤销。
  • FIFO即命名管道,在磁盘上有对应的节点,但没有数据块——换言之,只是拥有一个名字和相应的访问权限,经过mknode()系统调用或者mkfifo()函数来创建的。一旦创建,任何进程均可以经过文件名将其打开和进行读写,而不局限于父子进程,固然前提是进程对FIFO有适当的访问权。当再也不被进程使用时,FIFO在内存中释放,但磁盘节点仍然存在。

管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据:管道一端的进程顺序地将进程数据写入缓冲区,另外一端的进程则顺序地读取数据,该缓冲区能够看作一个循环队列,读和写的位置都是自动增长的,一个数据只能被读一次,读出之后再缓冲区都不复存在了。当缓冲区读空或者写满时,有必定的规则控制相应的读进程或写进程是否进入等待队列,当空的缓冲区有新数据写入或慢的缓冲区有数据读出时,就唤醒等待队列中的进程继续读写。

无名管道:

pipe的例子:父进程建立管道,并在管道中写入数据,而子进程从管道读出数据

命名管道:

和无名管道的主要区别在于,命名管道有一个名字,命名管道的名字对应于一个磁盘索引节点,有了这个文件名,任何进程有相应的权限均可以对它进行访问。

而无名管道却不一样,进程只能访问本身或祖先建立的管道,而不能访任意访问已经存在的管道——由于没有名字。

 

Linux中经过系统调用mknod()或makefifo()来建立一个命名管道。最简单的方式是经过直接使用shell

mkfifo myfifo

 

 等价于

mknod myfifo p

 

以上命令在当前目录下建立了一个名为myfifo的命名管道。用ls -p命令查看文件的类型时,能够看到命名管道对应的文件名后有一条竖线"|",表示该文件不是普通文件而是命名管道。

使用open()函数经过文件名能够打开已经建立的命名管道,而无名管道不能由open来打开。当一个命名管道再也不被任何进程打开时,它没有消失,还能够再次被打开,就像打开一个磁盘文件同样。

能够用删除普通文件的方法将其删除,实际删除的事磁盘上对应的节点信息。

例子:用命名管道实现聊天程序,一个张三端,一个李四端。两个程序都创建两个命名管道,fifo1,fifo2,张三写fifo1,李四读fifo1;李四写fifo2,张三读fifo2。

用select把,管道描述符和stdin假如集合,用select进行阻塞,若是有i/o的时候唤醒进程。(粉红色部分为select部分,黄色部分为命名管道部分)

 

 

在linux系统中,除了用pipe系统调用创建管道外,还可使用C函数库中管道函数popen函数来创建管道,使用pclose关闭管道。

例子:设计一个程序用popen建立管道,实现 ls -l |grep main.c的功能

分析:先用popen函数建立一个读管道,调用fread函数将ls -l的结果存入buf变量,用printf函数输出内容,用pclose关闭读管道;

接着用popen函数建立一个写管道,调用fprintf函数将buf的内容写入管道,运行grep命令。

popen的函数原型:

FILE* popen(const char* command,const char* type);

 

参数说明:command是子进程要执行的命令,type表示管道的类型,r表示读管道,w表明写管道。若是成功返回管道文件的指针,不然返回NULL。

使用popen函数读写管道,实际上也是调用pipe函数调用创建一个管道,再调用fork函数创建子进程,接着会创建一个shell 环境,并在这个shell环境中执行参数所指定的进程。

消息队列:

消息队列,就是一个消息的链表,是一系列保存在内核中消息的列表。用户进程能够向消息队列添加消息,也能够向消息队列读取消息。

消息队列与管道通讯相比,其优点是对每一个消息指定特定的消息类型,接收的时候不须要按照队列次序,而是能够根据自定义条件接收特定类型的消息。

能够把消息看作一个记录,具备特定的格式以及特定的优先级。对消息队列有写权限的进程能够向消息队列中按照必定的规则添加新消息,对消息队列有读权限的进程能够从消息队列中读取消息。

消息队列的经常使用函数以下表:

进程间经过消息队列通讯,主要是:建立或打开消息队列,添加消息,读取消息和控制消息队列。

例子:用函数msget建立消息队列,调用msgsnd函数,把输入的字符串添加到消息队列中,而后调用msgrcv函数,读取消息队列中的消息并打印输出,最后再调用msgctl函数,删除系统内核中的消息队列。(黄色部分是消息队列相关的关键代码,粉色部分是读取stdin的关键代码)

共享内存:

共享内存容许两个或多个进程共享一个给定的存储区,这一段存储区能够被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,能够被其余使用这个共享内存的进程,经过一个简单的内存读取错作读出,从而实现了进程间的通讯。

 

采用共享内存进行通讯的一个主要好处是效率高,由于进程能够直接读写内存,而不须要任何数据的拷贝,对于像管道和消息队里等通讯方式,则须要再内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次:一次从输入文件到共享内存区,另外一次从共享内存到输出文件。

通常而言,进程之间在共享内存时,并不老是读写少许数据后就解除映射,有新的通讯时在从新创建共享内存区域;而是保持共享区域,直到通讯完毕为止,这样,数据内容一直保存在共享内存中,并无写回文件。共享内存中的内容每每是在解除映射时才写回文件,所以,采用共享内存的通讯方式效率很是高。

共享内存有两种实现方式:一、内存映射 二、共享内存机制

一、内存映射

内存映射 memory map机制使进程之间经过映射同一个普通文件实现共享内存,经过mmap()系统调用实现。普通文件被映射到进程地址空间后,进程能够

像访问普通内存同样对文件进行访问,没必要再调用read/write等文件操做函数。

例子:建立子进程,父子进程经过匿名映射实现共享内存。

分析:主程序中先调用mmap映射内存,而后再调用fork函数建立进程。那么在调用fork函数以后,子进程继承父进程匿名映射后的地址空间,一样也继承mmap函数的返回地址,这样,父子进程就能够经过映射区域进行通讯了。

二、UNIX System V共享内存机制

IPC的共享内存指的是把全部的共享数据放在共享内存区域(IPC shared memory region),任何想要访问该数据的进程都必须在本进程的地址空间新增一块内存区域,用来映射存放共享数据的物理内存页面。

和前面的mmap系统调用经过映射一个普通文件实现共享内存不一样,UNIX system V共享内存是经过映射特殊文件系统shm中的文件实现进程间的共享内存通讯。

例子:设计两个程序,经过unix system v共享内存机制,一个程序写入共享区域,另外一个程序读取共享区域。

分析:一个程序调用fotk函数产生标准的key,接着调用shmget函数,获取共享内存区域的id,调用shmat函数,映射内存,循环计算年龄,另外一个程序读取共享内存。

(fotk函数在消息队列部分已经用过了,

根据pathname指定的文件(或目录)名称,以及proj参数指定的数字,ftok函数为IPC对象生成一个惟一性的键值。)

key_t ftok(char* pathname,char proj)

相关文章
相关标签/搜索