原文1:http://www.ibm.com/developerworks/cn/linux/l-ipc/



深刻理解Linux进程间通信(IPC)


一个大型的应用系统,往往需要众多进程协作,进程(Linux进程概念见附1)间通信的重要性显而易见。
本系列文章阐述了Linux环境下的几种主要进程间通信手段,并针对每个通信手段关键技术环节给出详细实例。
为达到阐明问题的目的,本文还对某些通信手段的内部实现机制进行了分析。

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

1、Linux IPC,System IPC , Unix IPC

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

        linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的。而对Unix发展做出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面的侧重点有所不同。前者对Unix早期的进程间通信手段进行了系统的改进和扩充,形成了“system V IPC”,通信进程局限在单个计算机内;后者则跳过了该限制,形成了基于套接口(socket)的进程间通信机制。Linux则把两者继承了下来,如图示:



           其中,最初Unix IPC包括:管道、FIFO、信号;System V IPC包括:System V消息队列、System V信号灯、System V共享内存区;Posix IPC包括: Posix消息队列、Posix信号灯、Posix共享内存区。有两点需要简单说明一下:1)由于Unix版本的多样性,电子电气工程协会(IEEE)开发了一个独立的Unix标准,这个新的ANSI Unix标准被称为计算机环境的可移植性操作系统界面(PSOIX)。现有大部分Unix和流行版本都是遵循POSIX标准的,而Linux从一开始就遵循POSIX标准;2)BSD并不是没有涉足单机内的进程间通信(socket本身就可以用于单机内的进程间通信)。事实上,很多Unix版本的单机IPC留有BSD的痕迹,如4.4BSD支持的匿名内存映射、4.3+BSD对可靠信号语义的实现等等。

        图一给出了linux 所支持的各种IPC手段,在本文接下来的讨论中,为了避免概念上的混淆,在尽可能少提及Unix的各个版本的情况下,所有问题的讨论最终都会归结到Linux环境下的进程间通信上来。并且,对于Linux所支持通信手段的不同实现版本(如对于共享内存来说,有Posix共享内存区以及System V共享内存区两个实现版本),将主要介绍Posix API。

System V,BSD,POSIX

       System V是Unix操作系统最早的商业发行版之一。它最初由AT&T(American Telephone & Telegraph)开发,最早在1983年发布。System V主要发行了4个版本,其中SVR4(System V Release 4)是最成功的版本。BSD(Berkeley Software Distribution,有时也被称为Berkeley Unix)是加州大学于1977至1995年间开发的。在19世纪八十年代至九十年代之间,System V和BSD代表了Unix的两种主要的操作风格。它们的主要区别如下:

    系统                          System V           BSD
    root脚本位置            /etc/init.d/           /etc/rc.d/
    默认shell                  Bshell                Cshell
    文件系统数据           /etc/mnttab        /etc/mtab
    内核位置                  /UNIX                 /vmUnix
    打印机设备                lp                     rlp
    字符串函数                memcopy        bcopy
    终端初始化设置文件    /etc/initab       /etc/ttys
    终端控制                  termio               termios

    Linux系统的操作风格往往介于这两种风格之间。

    POSIX(Portable Operating System Interface [for Unix])是由IEEE(Institute of Electrical and Electronics Engineers,电子电气工程协会)开发的。现有的大部分Unix都遵循POSIX标准,而Linux从一开始就遵循POSIX标准。

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

2、最初的Unix IPC

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

1、信号

      信号是Unix/Linux系统在一定条件下生成的事件。信号是一种异步通信机制,进程不需要执行任何操作来等待信号的到达。信号异步通知接收信号的进程发生了某个事件,然后操作系统将会中断接收到信号的进程的执行,转而去执行相应的信号处理程序。

    (1)注册信号处理函数

        #include <signal.h>
        /*typedef void (*sighandler_t)(int);  sighandler_t signal(int signum,sighandler_t handler);*/
        * void (*signal(int signum, void (*handler)(int)))(int);  //SIG_IGN && SIG_DFL
        * int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);

    (2)发送信号

        #include <signal.h>
        * int kill(pid_t pid,int sig); //#include <sys/types.h> 
        * int raise(int sig);            //kill(getpid(),sig);
        * unsigned int alarm(unsigned int seconds); //(#include <unistd.h>) seconds秒后,向进程本身发送SIGALRM信号。
    (3)信号集

        信号集被定义为:

           typedef struct {unsigned long sig[_NSIG_WORDS];} sigset_t;
        * int sigaddset(sigset_t *set,int sig);
        * int sigemptyset(sigset_t *set);


2、管道(Pipe)

    管道用来连接不同进程之间的数据流。


    (1)在两个程序之间传递数据的最简单的方法是使用popen()和pclose()函数:

        #include <stdio.h>
        FILE *popen(const char *command, const char *open_mode);
        int pclose(FILE *stream);
    popen()函数首先调用一个shell,然后把 command 作为参数传递给shell。这样每次调用popen()函数都需要启动两个进程;但是由于在Linux中,所有的参数扩展(parameter expansion)都是由shell执行的,这样 command 中包含的所有参数扩展都可以在 command 程序启动之前完成。


    (2)pipe()函数:

        #include <unistd.h>
        int pipe(int pipefd[2]);
    popen()函数只能返回一个管道描述符,并且返回的是文件流(file stream),可以使用函数fread()和fwrite()来访问。pipe()函数可以返回两个管道描述符: pipefd[0] pipefd[1] ,任何写入 pipefd[1] 的数据都可以从 pipefd[0] 读回;pipe()函数返回的是文件描述符(file descriptor),因此只能使用底层的read()和write()系统调用来访问。pipe()函数通常用来实现父子进程之间的通信。


    (3)命名管道:FIFO    

   #include <sys/types.h>
        #include <sys/stat.h>
        int mkfifo(const char *fifo_name, mode_t mode);
    前面两种管道只能用在相关的程序之间,使用命名管道可以解决这个问题。在使用open()打开FIFO时,mode中不能包含O_RDWR。mode最常用的是O_RDONLY,O_WRONLY与O_NONBLOCK的组合。O_NONBLOCK影响了read()和write()在FIFO上的执行方式。

    PS:要想查看库函数用法,最可靠的资料来自Linux manual page:

    $sudo apt-get install manpages-dev

    $man 3 function name


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

3、System V IPC

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

     System V IPC指的是AT&T在System V.2发行版中引入的三种进程间通信工具:

     (1)信号量,用来管理对共享资源的访问

     (2)共享内存,用来高效地实现进程间的数据共享

     (3)消息队列,用来实现进程间数据的传递。

     我们把这三种工具统称为System V IPC的对象,每个对象都具有一个唯一的IPC标识符(identifier)。要保证不同的进程能够获取同一个IPC对象,

     必须提供一个IPC关键字(IPC key),内核负责把IPC关键字转换成IPC标识符。   


    3.1 System V IPC具有相似的语法:


    (1) 选择IPC关键字,可以使用如下三种方式:

            a)IPC_PRIVATE。由内核负责选择一个关键字然后生成一个IPC对象并把IPC标识符直接传递给另一个进程。

            b)直接选择一个关键字。

            c)使用ftok()函数生成一个关键字。

    (2) 使用semget()/shmget()/msgget()函数根据IPC关键字key和一个标志flag创建或访问IPC对象。如果key是IPC_PRIVATE;

          或者key尚未与已经存在的IPC对象相关联且flag中包含IPC_CREAT标志,那么就会创建一个全新的IPC对象。

    (3) 使用semctl()/shmctl()/msgctl()函数修改IPC对象的属性。

    (4) 使用semctl()/shmctl()/msgctl()函数和IPC_RMID标志销毁IPC实例。

    System V IPC为每个IPC对象设置了一个ipc_perm结构体并在创建IPC对象的时候进行初始化。这个结构体中定义了IPC对象的访问权限和所有者:


     struct ipc_perm{
       uid_t uid;   //所有者的用户id
       gid_t gid;   //所有者的组id
       uid_t cuid;  //创建者的用户id
       gid_t cgid;  //创建者的组id
       mode_t mode; //访问模式
       …
    };

    shell中管理IPC对象的命令是ipcs、ipcmk和ipcrm。


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

    3.1  System V IPC 具体通信机制

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

1、信号量(Semaphores)

    System V的信号量集表示的是一个或多个信号量的集合。内核为每个信号量集维护一个semid_ds数据结构,而信号量集中的每个信号量使用一个无名结构体表示,这个结构体至少包含以下成员:


     struct{
         unsigned  short semval; //信号量值,总是>=0
        pid_t sempid;   //上一次操作的pid
       …
    };
    # include  <sys /types.h >
    # include  <sys /ipc.h >
    # include  <sys /sem.h >


    (1)创建或访问信号量
        * int semget(key_t key,int nsems,int flag); 
    nsems指定信号量集中信号量的个数,如果只是获取信号量集的标识符(而非新建),那么nsems可以为0。flag的低9位作为信号量的访问权限位,类似于文件的访问权限;如果flag中同时指定了IPC_CREAT和IPC_EXCL,那么如果key已与现存IPC对象想关联的话,函数将会返回EEXIST错误。例如,flag可以为IPC_CREAT|0666。

    (2)控制信号量集
        * int semctl(int semid,int semnum,int cmd,union semun arg);
    对semid信号量集合执行cmd操作;cmd常用的两个值是:SETVAL初始化第semnum个信号量的值为arg.val;IPC_RMID删除信号量。

    (3)对一个或多个信号量进行操作

        * int semop(int semid,struct sembuf *sops,unsigned nsops);
        * struct sembuf{
              unsigned short sem_num;  //信号量索引
              short   sem_op;     //对信号量进行的操作,常用的两个值为-1和+1,分别代表P、V操作
              short   sem_flag;   //比较重要的值是SEM_UNDO:当进程结束时,相应的操作将被取消;同时,如果进程结束时没有释放资源的话,系统会自动释放
           };


2、共享内存

    共享内存允许两个或多个进程共享一定的存储区,因为不需要拷贝数据,所以这是最快的一种IPC。

    #include <sys/ipc.h>
    
#include <sys/shm.h>
    (1)创建或访问共享内存
        * int shmget(key_t key,size_t size,int shmflg);

    (2)附加共享内存到进程的地址空间
        * void *shmat(int shmid,const void *shmaddr,int shmflg);//shmaddr通常为NULL,由系统选择共享内存附加的地址;shmflg可以为SHM_RDONLY

    (3)从进程的地址空间分离共享内存
        * int shmdt(const void *shmaddr); //shmaddr是shmat()函数的返回值

    (4)控制共享内存
        * int shmctl(int shmid,int cmd,struct shmid_ds *buf);
        * struct shmid_ds{
              struct ipc_perm shm_perm;
              …
          
}; 
    cmd的常用取值有:(a)IPC_STAT获取当前共享内存的shmid_ds结构并保存在buf中(2)IPC_SET使用buf中的值设置当前共享内存的shmid_ds结构(3)IPC_RMID删除当前共享内存


3、消息队列

    消息队列保存在内核中,是一个由消息组成的链表。

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>
    (1)创建或访问消息队列
    * int msgget(key_t key,int msgflg);

    (2)操作消息队列
        * int msgsnd(int msqid,const void *msg,size_t nbytes,int msgflg);
    msg指向的结构体必须以一个long int成员开头,作为msgrcv()的消息类型,必须大于0。nbytes指的是msg指向结构体的大小,但不包括long int部分的大小
        * ssize_t msgrcv(int msqid,void *msg,size_t nbytes,long msgtype,int msgflg);
    如果msgtype是0,就返回消息队列中的第一个消息;如果是正整数,就返回队列中的第一个该类型的消息;如果是负数,就返回队列中具有最小值的第一个消息,并且该最小值要小于等于msgtype的绝对值。

    (3)控制消息队列

        * int msgctl(int msqid,int cmd,struct msqid_ds *buf);
        * struct msqid_ds{
              struct ipc_perm msg_perm;
              …
           };

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

4、Socket套接字

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  套接字(Socket)是由Berkeley在BSD系统中引入的一种基于连接的IPC,是对网络接口(硬件)和网络协议(软件)的抽象。它既解决了无名管道只能在相关进程间单向通信的问题,又解决了网络上不同主机之间无法通信的问题。

  套接字有三个属性:域(domain)、类型(type)和协议(protocol),对应于不同的域,套接字还有一个地址(address)来作为它的名字。

  域(domain)指定了套接字通信所用到的协议族,最常用的域是AF_INET,代表网络套接字,底层协议是IP协议。对于网络套接字,由于服务器端有可能会提供多种服务,客户端需要使用IP端口号来指定特定的服务。AF_UNIX代表本地套接字,使用Unix/Linux文件系统实现。

  IP协议提供了两种通信手段:流(streams)和数据报(datagrams),对应的套接字类型(type)分别为流式套接字和数据报套接字。流式套接字(SOCK_STREAM)用于提供面向连接、可靠的数据传输服务。该服务保证数据能够实现无差错、无重复发送,并按顺序接收。流式套接字使用TCP协议。数据报套接字(SOCK_DGRAM)提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP协议。

  一种类型的套接字可能可以使用多于一种的协议来实现,套接字的协议(protocol)属性用于指定一种特定的协议。


linux下进程间通信的几种主要手段简介:

  1. 管道(Pipe)及有名管道(named pipe):管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
  2. 信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数);
  3. 报文(Message)队列(消息队列):消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  4. 共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
  5. 信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
  6. 套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。

下面将对上述通信机制做具体阐述。

附1:参考文献[2]中对linux环境下的进程进行了概括说明:

一般来说,linux下的进程包含以下几个关键要素:

  • 有一段可执行程序;
  • 有专用的系统堆栈空间;
  • 内核中有它的控制块(进程控制块),描述进程所占用的资源,这样,进程才能接受内核的调度;
  • 具有独立的存储空间

进程和线程有时候并不完全区分,而往往根据上下文理解其含义。


Logo

更多推荐