Linux进程间通信总结
1. 管道
管道是Linux支持的最初Unix IPC形式之一,具有以下特点:
(1)管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
(2)只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
(3)单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
(4)数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
管道的创建
#include <unistd.h>
int pipe(int fd[2])
返回的fd[0]用于读,fd[1]用于写。因此,一个进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在亲缘关系,这里的亲缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。
管道的应用:
* 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;
};
第三个成员是进程中所有未决信号集,第一、第二个成员分别指向一个
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)消息队列
与命名管道相比,消息队列的优势在于,1、消息队列也可以独立于发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难。2、同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。3、接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。
(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信号发生了,这也就完成了对信号的监听工作。
- 创建一个管道(Libevent实际上使用的是socketpair)
- 为这个socketpair的一个读端创建一个event,并将之加入到多路IO复用函数的监听之中
- 设置信号捕抓函数
- 有信号发生,就往socketpair写入一个字节