Linux进程间通信通常用到的方法有:匿名管道、命名管道、信号、消息队列、共享内存、信号量和套接字,其中匿名管道只用于有亲属关系的父子进程之间的一种单功通信方式,在fork()创建进程之前创建匿名管道。其中个人用的最多的是命名管道、共享内存和信号量:命名管道由于返回的文件描述符,可以十分方便的融合到现有的select/poll/epoll框架下面去;信号量主要用于模拟进程间互斥的行为;共享内存用于进程间大规模的数据共享。陈硕的一句名言就是“在多进程之间共享内存无异于掩耳盗铃”,其实多进程间通过共享内存的方式共享数据弊端和限制确实很多:首先共享内存中不能共享指针,而指向共享内存段本身的指针也最好用便宜的方式退化指针;如果共享内存的数据经常会被修改,那更是个灾难。当然简单只读数据是可以的,比如Nginx的缓存也使用了共享内存。
多进程程序的好处,就是消除了进程之间的耦合度后,操作系统的保护机制可以让多个进程更加的独立可靠,而且分成多个进程之后管理进程比管理线程方便灵活的多;同时,多进程程序可以实现进程的特异化管理,比如在Nginx设计中master process是特权进程,可以读取配置文件、修改数重要数据等关键操作,而worker process是普通权限进程,只负责业务方面的处理,符合系统管理中的最小化权限原则;再有就是多进程程序可以进行业务的热更新平滑升级,下面的Nginx算是将这一功能使用的淋漓尽致啊。
但是多进程的程序也有个问题,就是很多共享的资源、同步的手段都是命名全局的,很有可能进程意外退出后这些资源都得不到回收,补救的办法只能是重启操作系统,汗~
1.3 多线程程序和信号
感觉信号一直是Linux平台下开发比较头疼的问题,尤其对于多线程情况下的程序,信号的处理将更加的复杂。
1.3.1 单线程程序中信号的处理方式
Linux中的信号的处理方式可以是SIG_IGN、SIG_DFL以及自己通过sigaction设置自定义处理函数,进程创建的时候信号都有默认的处理方式,而用户可以后续选择忽略、默认处理方式、自定义处理这些信号(SIGKILL、SIGSTOP两个信号只能默认处理方式,不能被忽略或者重定义处理),当进程接收到信号的时候就会转向信号处理历程去执行。
信号可以在某些情况下被系统发送(比如触发段错误),或者被别的进程使用kill发送,或者进程自己调用kill、raise系统调用触发信号。进程可以通过signal mask去block某些信号,默认情况下是没有信号被block的,此时如果被block的信号发送过来了,将会被设置为pending的,然后一旦该进程unblock了该信号,pending的信号将会立即被传递。
1.3.2 pthreads库多线程环境对信号处理的方式
pthreads库多线程中信号处理的方式,和信号的种类、各个线程对信号的mask状态共同决定的。
Linux中多线程环境下信号的种类可以分为同步(Synchronously)信号和异步(Asynchronously)信号:同步信号是针对某个线程的,比如某个线程执行过程中除以零(SIGFPE)、访问非法地址(SIGSEGV)、使用了broken的管道(SIGPIPE),这些信号都根某个特定的线程特定的执行上下文有关,还有就是同个进程中线程之间通过pthread_kill显式发送信号的情况;异步信号主要是其他进程向该进程通过kill向这个进程(而非其中的线程)发送信号,并不跟某个特定的线程相关联的情况。
pthreads库中多线程之间共享sigaction结构但是不共享sig_mask结构,这意味所有的线程共享相同的信号处理方式,而不论信号处理方式是谁设置的。进程在最初fork()后创建的第一个线程继承了其signal mask,而通过pthread_create创建的其他线程也继承了这个信号mask,后续可以通过pthread_sigmask接口控制本线程对某些信号的block或者unblock。
有了上面的知识,信号在多线程下的行为就可以被确定了:
(1). 所有的线程共享相同的sigaction,所以所有进程对某个信号的处理方式是完全相同的;
(2). 同步信号是针对某个特定线程的,该线程是否接收处理这个信号看其signal mask设置情况;
(3). 异步信号是针对这个进程的,当这种信号到达的时候,进程会从没有block这个信号的线程集合中随机选出一个出来处理这个信号,如果所有的线程都block该信号,那么这个信号将被pending起来,直到有线程unblock这个信号,就将其发送给那个线程处理;