Linux进程间通讯总结

Linux进程间通讯总结

1. 管道

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

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

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

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

(4)数据的读出和写入:一个进程向管道中写的内容被管道另外一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,而且每次都是从缓冲区的头部读出数据。dom

 

管道的建立异步

 

    #include <unistd.h>socket

    int pipe(int fd[2])函数

返回的fd[0]用于读,fd[1]用于写。所以,一个进程在由pipe()建立管道后,通常再fork一个子进程,而后经过管道实现父子进程间的通讯(所以也不难推出,只要两个进程中存在亲缘关系,这里的亲缘关系指的是具备共同的祖先,均可以采用管道方式来进行通讯)。oop

 

管道的应用:设计

* shell:

管道可用于输入输出重定向,它将一个命令的输出直接定向到另外一个命令的输入。好比,当在某个shell程序键入who│wc -l后,相应shell程序将建立who以及wc两个进程和这两个进程间的管道

* 用于具备亲缘关系的进程间通讯

 

管道的局限:

* 只支持单向数据流;

* 只能用于具备亲缘关系的进程之间;

* 没有名字;

* 管道的缓冲区是有限的(管道制存在于内存中,在管道建立时,为缓冲区分配一个页面大小);

* 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式。

 

2. 有名管道

FIFO不一样于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即便与FIFO的建立进程不存在亲缘关系的进程,只要能够访问该路径,就可以彼此经过FIFO相互通讯(可以访问该路径的进程以及FIFO的建立进程之间),所以,经过FIFO不相关的进程也能交换数据。

 

有名管道建立:

 

    int mkfifo(const char * pathname, mode_t mode)

   

3. 信号

信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求能够说是同样的。信号是异步的,一个进程没必要经过任何操做来等待信号的到达,事实上,进程也不知道信号到底何时到达。信号事件的发生有两个来源:硬件来源(好比咱们按下了键盘或者其它硬件故障);软件来源,最经常使用发送信号的系统函数是kill, raise,alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操做。

 

(1)信号的种类

可靠信号与不可靠信号, 实时信号与非实时信号

可靠信号就是实时信号, 那些从UNIX系统继承过来的信号都是非可靠信号, 表如今信号

不支持排队,信号可能会丢失, 好比发送屡次相同的信号, 进程只能收到一次. 信号值小于SIGRTMIN的都是非可靠信号.

非可靠信号就是非实时信号, 后来, Linux改进了信号机制, 增长了32种新的信号, 这些信

号都是可靠信号, 表如今信号支持排队, 不会丢失, 发多少次, 就能够收到多少次. 信号值

位于 [SIGRTMIN, SIGRTMAX] 区间的都是可靠信号.

(2)信号的安装

早期的Linux使用系统调用 signal 来安装信号

#include <signal.h>

void (*signal(int signum, void (*handler))(int)))(int); 

该函数有两个参数, signum指定要安装的信号, handler指定信号的处理函数.

该函数的返回值是一个函数指针, 指向上次安装的handler

 

经典安装方式:

if (signal(SIGINT, SIG_IGN) != SIG_IGN) {

    signal(SIGINT, sig_handler);

}

先得到上次的handler, 若是不是忽略信号, 就安装此信号的handler

 

因为信号被交付后, 系统自动的重置handler为默认动做, 为了使信号在handler处理期间, 仍能对后继信号作出反应, 每每在handler的第一条语句再次调用 signal

sig_handler(ing signum)

{

    /* 从新安装信号 */

    signal(signum, sig_handler);

    ......

}

咱们知道在程序的任意执行点上, 信号随时可能发生, 若是信号在sig_handler从新安装

信号以前产生, 此次信号就会执行默认动做, 而不是sig_handler. 这种问题是不可预料的.

 

使用库函数 sigaction  来安装信号

为了克服非可靠信号并同一SVR4和BSD之间的差别, 产生了 POSIX 信号安装方式, 使用sigaction安装信号的动做后, 该动做就一直保持, 直到另外一次调用 sigaction创建另外一个动做为止. 这就克服了古老的 signal 调用存在的问题

 

#include <signal.h> 
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));

 

/* 设置SIGINT */

action.sa_handler = sig_handler;

sigemptyset(&action.sa_mask);

sigaddset(&action.sa_mask, SIGTERM);

action.sa_flags = 0;

 

/* 获取上次的handler, 若是不是忽略动做, 则安装信号 */

sigaction(SIGINT, NULL, &old_action);

if (old_action.sa_handler != SIG_IGN) {

    sigaction(SIGINT, &action, NULL);

}

 

基于 sigaction 实现的库函数: signal

sigaction 天然强大, 但安装信号很繁琐, 目前linux中的signal()是经过sigation()函数实现的,所以,即便经过signal()安装的信号,在信号处理函数的结尾也没必要再调用一次信号安装函数。

 

3)如何屏蔽信号

所谓屏蔽, 并非禁止递送信号, 而是暂时阻塞信号的递送, 

解除屏蔽后, 信号将被递送, 不会丢失. 相关API为

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum);

int sigdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);

int sigsuspend(const sigset_t *mask);

int sigpending(sigset_t *set);

-----------------------------------------------------------------

int  sigprocmask(int  how,  const  sigset_t *set, sigset_t *oldset));

sigprocmask()函数可以根据参数how来实现对信号集的操做,操做主要有三种:

* SIG_BLOCK  在进程当前阻塞信号集中添加set指向信号集中的信号

* SIG_UNBLOCK   若是进程阻塞信号集中包含set指向信号集中的信号,则解除

   对该信号的阻塞

* SIG_SETMASK    更新进程阻塞信号集为set指向的信号集

屏蔽整个进程的信号:

4)信号的生命周期

从信号发送到信号处理函数的执行完毕

对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来讲,

能够分为三个重要的阶段,这三个阶段由四个重要事件来刻画:

信号诞生;信号在进程中注册完毕;信号在进程中的注销完毕;信号处理函数执行完毕。

 

下面阐述四个事件的实际意义:

信号"诞生"。信号的诞生指的是触发信号的事件发生(如检测到硬件异常、定时器超时以及调用信号发送函kill()或sigqueue()等)。

信号在目标进程中"注册";

进程的task_struct结构中有关于本进程中未决信号的数据成员:

struct sigpending pending:

struct sigpending{

      struct sigqueue *head, **tail;

      sigset_t signal;

};

 

第三个成员是进程中全部未决信号集,第1、第二个成员分别指向一个

sigqueue类型的结构链(称之为"未决信号链表")的首尾,链表中

的每一个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个

sigqueue结构:

struct sigqueue{

      struct sigqueue *next;

      siginfo_t info;

}

信号的注册

信号在进程中注册指的就是信号值加入到进程的未决信号集中

(sigpending结构的第二个成员sigset_t signal),

而且加入未决信号链表的末尾。 只要信号在进程的未决信号集中,

代表进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。

当一个实时信号发送给一个进程时,无论该信号是否已经在进程中注册,

都会被再注册一次,所以,信号不会丢失,所以,实时信号又叫作"可靠信号"。

这意味着同一个实时信号能够在同一个进程的未决信号链表中添加屡次. 

当一个非实时信号发送给一个进程时,若是该信号已经在进程中注册,

则该信号将被丢弃,形成信号丢失。所以,非实时信号又叫作"不可靠信号"。

这意味着同一个非实时信号在进程的未决信号链表中,至多占有一个sigqueue结构.

一个非实时信号诞生后,

(1)、若是发现相同的信号已经在目标结构中注册,则再也不注册,对于进程来讲,

至关于不知道本次信号发生,信号丢失.

(2)、若是进程的未决信号中没有相同信号,则在进程中注册本身。

 

信号的注销。

在进程执行过程当中,会检测是否有信号等待处理

(每次从系统空间返回到用户空间时都作这样的检查)。若是存在未决

信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,

进程会把信号在未决信号链中占有的结构卸掉。是否将信号从进程未决信号集

中删除对于实时与非实时信号是不一样的。对于非实时信号来讲,因为在未决信

号信息链中最多只占用一个sigqueue结构,所以该结构被释放后,应该把信

号在进程未决信号集中删除(信号注销完毕);而对于实时信号来讲,可能在

未决信号信息链中占用多个sigqueue结构,所以应该针对占用sigqueue结构

的数目区别对待:若是只占用一个sigqueue结构(进程只收到该信号一次),

则应该把信号在进程的未决信号集中删除(信号注销完毕)。不然,不该该在进程

的未决信号集中删除该信号(信号注销完毕)。 

进程在执行信号相应处理函数以前,首先要把信号在进程中注销。

 

信号生命终止。

进程注销信号后,当即执行相应的信号处理函数,执行完毕后,

信号的本次发送对进程的影响完全结束。

 

4. system V 提供的进程间通讯的三种方式

(1)消息队列

 

与命名管道相比,消息队列的优点在于,一、消息队列也能够独立于发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难。二、同时经过发送消息还能够避免命名管道的同步和阻塞问题,不须要由进程本身来提供同步方法。三、接收程序能够经过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。

 

(2)信号量

 

信号量与其余进程间通讯方式不大相同,它主要提供对进程间共享资源访问控制机制。至关于内存中的标志,进程能够根据它断定是否可以访问某些共享资源,同时,进程也能够修改该标志。除了用于访问控制外,还可用于进程同步。信号量有如下两种类型:

二值信号量:最简单的信号量形式,信号灯的值只能取0或1,相似于互斥锁。

计算信号量:信号量的值能够取任意非负值(固然受内核自己的约束)。

 

信号量只能进行两种操做等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:

P(sv):若是sv的值大于零,就给它减1;若是它的值为零,就挂起该进程的执行

V(sv):若是有其余进程因等待sv而被挂起,就让它恢复运行,若是没有进程因等待sv而挂起,就给它加1.

 

(3)共享内存

 

速度最快,效率最高的进程间通讯方式,进程之间直接访问内存,而不是经过传送数据。可是使用共享内存须要本身提供同步机制。

 

5. 套接字(unix域协议)

socket API本来是为网络通信设计的,但后来在socket的框架上发展出一种IPC机制,就是UNIX Domain Socket。虽然网络socket也可用于同一台主机的进程间通信(经过loopback地址127.0.0.1),可是UNIX Domain Socket用于IPC更有效率:不须要通过网络协议栈,不须要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另外一个进程。UNIX域套接字与TCP套接字相比较,在同一台传输主机的速度前者是后者的两倍。这是由于,IPC机制本质上是可靠的通信,而网络协议是为不可靠的通信设计的。UNIX Domain Socket也提供面向流和面向数据包两种API接口,相似于TCP和UDP,可是面向消息的UNIX Domain Socket也是可靠的,消息既不会丢失也不会顺序错乱。

 值得注意的是,Unix域协议表示协议地址的是路径名,而不是Internet域的IP地址和端口号。

 

    #define UNIX_PATH_MAX    108

    struct sockaddr_un 

    { 

    sa_family_t sun_family;               /* AF_UNIX */

    char        sun_path[UNIX_PATH_MAX];  /* pathname */

    };

   

socketpair 函数:建立一个全双工的流管道

   

    int socketpair(int domain, int type, int protocol, int sv[2]);

 

使用unix域协议的例子

* libevent网络库对信号的封装:libevent实现了对于socket网络套接口,定时器事件,信号事件的统一监听, 即统一事件源。简单地说,就是把信号也转换成IO事件,集成到Libevent中。网络套接口实际为文件描述符fd,能够在epoll中直接监听,定时器事件能够设置epoll的超时时间进行监听,信号的产生是随机的,libevent网络库是如何进行处理使得能用epoll来实现信号的监听呢?

 

假如用户要监听SIGINT这个信号,那么在实现的内部就对SIGINT这个信号设置捕抓函数。此外,在实现的内部还要创建一条管道(pipe),并把这个管道加入到多路IO复用函数中。当SIGINT这个信号发生后,捕抓函数将会被调用。而这个捕抓函数的工做就是往管道写入一个字符(这个字符每每等于所捕抓到信号的信号值)。此时,这个管道就变成是可读的了,多路IO复用函数能检测到这个管道变成可读的了。换句话说就是多路IO复用函数检测到这个SIGINT信号发生了,这也就完成了对信号的监听工做。

 

  1. 建立一个管道(Libevent实际上使用的是socketpair)
  2. 为这个socketpair的一个读端建立一个event,并将之加入到多路IO复用函数的监听之中
  3. 设置信号捕抓函数
  4. 有信号发生,就往socketpair写入一个字节
相关文章
相关标签/搜索