Linux进程间通信方式(一)

来源:互联网 发布:2048js游戏源代码 编辑:程序博客网 时间:2024/06/05 01:23

进程间通信就是在不同进程之间传播或交换信息,不同的进程之间有什么双方都能访问的介质呢?进程的用户空间是相互独立的,一般而言是不能相互访问的,唯一的例外就是共享内存区。Linux的进程间通信(IPC)通信方法有管道、消息队列、信号量、共享内存、套接字等。

进程间通信的目的

  1. 数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
  2. 共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
  3. 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(们)发生了某种事件(如进程终止时要通知父进程)。
  4. 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供锁和同步机制。
  5. 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有信息和异常,并能够及时知道它的状态。

管道通信

管道的通信方式分为无名管道和有名管道,无名管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制。
管道是Linux支持的最初Unix IPC形式之一,具有以下特点:
1. 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管 道;
2. 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
3. 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但 它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
4. 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的 内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。

建立无名管道

pipe函数用于建立无名管道。
int pipe(int fd[2]);pipe()会建立管道,并将文件描述符由参数fd[2]数组返回。fd[0]为管道里的读取端,fd[1]则为管道的写入端。若成功则返回0否则返回-1,错误原因存于errno中。

读写无名管道

管道的读写规则: 管道两端可分别用描述字fd[0]以及fd[1]来描述,需要注意的是,管道的两端是固定了任务的。即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。一般文件的I/O函数都可以用于管道,如close、read、write等等。 从管道中读取数据:
1.如果管道的写端不存在,则认为已经读到了数据的末尾,读函数返回的读出字节数 为0;
2.当管道的写端存在时,如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数 据字节数,如果请求的字节数目不大于PIPE_BUF,则返回管道中现有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。注:(PIPE_BUF在include/linux/limits.h中定义,不同的内核版本可能会有所不同。Posix.1要求PIPE_BUF至少为512字节,red hat 7.2中为4096)
关于管道的读规则验证:

#include <unistd.h>#include <sys/types.h> #include <errno.h> #include <stdio.h>int main() {      int pipe_fd[2];    pid_t pid;    char r_buf[100];    char w_buf[4];    char* p_wbuf;    int r_num;    int cmd;    memset(r_buf,0,sizeof(r_buf));    memset(w_buf,0,sizeof(r_buf));    p_wbuf=w_buf;    if(pipe(pipe_fd)<0)    {   printf("pipe create error\n");        return -1;    }        if((pid=fork())==0)    {        printf("\n");        close(pipe_fd[1]);        sleep(3);//确保父进程关闭写端        r_num=read(pipe_fd[0],r_buf,100);        printf( "read num is %d   the data read from the pipe is %d\n",r_num,atoi(r_buf));        close(pipe_fd[0]);        exit();    }      else if(pid>0)    {          close(pipe_fd[0]);//read        strcpy(w_buf,"111");        if(write(pipe_fd[1],w_buf,4)!=-1)            printf("parent write over\n");        close(pipe_fd[1]);//write        printf("parent close fd[1] over\n");        sleep(10);        return 0;    }}

对管道的写规则的验证:写端对读端存在的依赖性

#include <unistd.h>#include <sys/types.h>#include <stdio.h>int main() {      int pipe_fd[2];    pid_t pid;    char r_buf[4];    char* w_buf;    int writenum;    int cmd;    memset(r_buf,0,sizeof(r_buf));    if(pipe(pipe_fd)<0)    {           printf("pipe create error\n");        return -1;    }        if((pid=fork())==0)    {           close(pipe_fd[0]);        close(pipe_fd[1]);        sleep(10);        exit();    }       else if(pid>0)    {           sleep(1);  //等待子进程完成关闭读端的操作        close(pipe_fd[0]);//write        w_buf="111";        if((writenum=write(pipe_fd[1],w_buf,4))==-1)            printf("write to pipe error\n");        else            printf("the bytes write to pipe is %d \n", writenum);        close(pipe_fd[1]);    }    return 0;}

管道的应用:

  • shell:

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

  • 用于具有亲缘关系的进程间通信

管道的局限:

  • 只支持单向数据流;

  • 只能用于具有亲缘关系的进程之间;

  • 没有名字;

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

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

有名管道

有名管道概述及相关API应用
1.有名管道相关的关键概念
管道应用的一个重大限制是它没有名字,因此,只能用于具有亲缘关系的进程间通信,在有名管道(named pipe或FIFO)提出后,该限制得到了克服。FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。
2.有名管道的创建
int mkfifo(const char * pathname, mode_t mode)
该函数的第一个参数是一个普通的路径名,也就是创建后FIFO的名字。第二个参数与打开普通文件的open()函数中的mode 参数相同。如果mkfifo的第一个参数是一个已经存在的路径名时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数就可以了。一般文件的I/O函数都可以用于FIFO,如close、read、write等等。
3.有名管道的打开规则
有名管道比管道多了一个打开操作:open。 FIFO的打开规则:
如果当前打开操作是为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打
开操作将成功返回;否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志);或者,成功返回(当前打开操作没有设置阻塞标志)。
如果当前打开操作是为写而打开FIFO时,如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,返回ENXIO错误(当前打开操作没有设置阻塞标志)。
4.有名管道的读写规则
从FIFO中读取数据:
约定:如果一个进程为了从FIFO中读取数据而阻塞打开FIFO,那么称该进程内的读操作为设置了阻塞标志的读操作。
(1)如果有进程写打开FIFO,且当前FIFO内没有数据,则对于设置了阻塞标志的读操作
来说,将一直阻塞。对于没有设置阻塞标志读操作来说则返回-1,当前errno值为EAGAIN,提醒以后再试。
(2)对于设置了阻塞标志的读操作说,造成阻塞的原因有两种:当前FIFO内有数据,但
有其它进程在读这些数据;另外就是FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,不论信写入数据量的大小,也不论读操作请求多少数据量。  读打开的阻塞标志只对本进程第一个读操作施加作用,如果本进程内有多个读操作
序列,则在第一个读操作被唤醒并完成读操作后,其它将要执行的读操作将不再阻塞,即使在执行读操作时,FIFO中没有数据也一样(此时,读操作返回0)。  如果没有进程写打开FIFO,则设置了阻塞标志的读操作会阻塞。
注:如果FIFO中有数据,则设置了阻塞标志的读操作不会因为FIFO中的字节数小于请求读的字节数而阻塞,此时,读操作会返回FIFO中现有的数据量。 向FIFO中写入数据:
约定:如果一个进程为了向FIFO中写入数据而阻塞打开FIFO,那么称该进程内的写操作为设置了阻塞标志的写操作。 对于设置了阻塞标志的写操作:
(1)当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果此时管道空
闲缓冲区不足以容纳要写入的字节数,则进入睡眠,直到当缓冲区中能够容纳要写入的字节数时,才开始进行一次性写操作。
(2)当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。FIFO缓冲区
一有空闲区域,写进程就会试图向管道写入数据,写操作在写完所有请求写的数据后返回。
对于没有设置阻塞标志的写操作:
(1)当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。在写满所有
FIFO空闲缓冲区后,写操作返回。
(2)当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果当前FIFO
空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写;
对FIFO读写规则的验证:
下面提供了两个对FIFO的读写程序,适当调节程序中的很少地方或者程序的命令行参数就可以对各种FIFO读写规则进行验证。

信号

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

(1)信号的种类

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

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

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

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

号都是可靠信号, 表现在信号支持排队, 不会丢失, 发多少次, 就可以收到多少次. 信号值

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

(2)信号的安装

早期的Linux使用系统调用 signal 来安装信号
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结构(进程只收到该信号一次),

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

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

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

信号生命终止。

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

信号的本次发送对进程的影响彻底结束。