Linux 进程间通信(一)(经典IPC:消息队列、信号量、共享存储)

有3种称作XSI IPC的IPC:消息队列、信号量、共享存储。这种类型的IPC有如下共同的特性

每个内核中的IPC都用一个非负整数标志。标识符是IPC对象的内部名称,为了使多个合作进程能够在同一IPC对象上汇聚,需要提供一个外部命名方案。因此,将每个IPC对象都与一个键相关联,将这个键(key)作为该对象的外部名。这个键的数据类型是key_t,通常在头文件<sys/types.h>中被定义为长整型,该键由内核变换成标识符。

有3种方式可以使客户进程和服务器进程在同一IPC结构上汇聚:

(1)   服务器进程可以指定键IPC_PRIVATE创建一个新的IPC结构,将返回的标识符存放在某处(如一个文件)以便客户进程取用。IPC_PRIVATE键也可用于父子进程,父进程指定IPC_PRIVATE创建一个新的IPC结构,所返回的标识符可供fork后的子进程使用。接着,子进程又可将该标识符作为exec函数的一个参数传给一个新程序。

(2)   可以在一个公共头文件中定义一个客户进程和服务器进程都认可的键。然后服务器进程指定此键创建一个新的IPC结构。这种方法的问题是该键可能已于一个IPC结构相结合,在此情况下,get函数(msgget、semget、shmget)出错返回。服务器进程必须处理这一错误,删除已存在的IPC结构,然后试着再创建它。

(3)   客户进程和服务器进程使用一个路径名和一个id,调用ftok函数根据这两个值生成一个键,然后在(2)中使用这个键:key_t ftok(const char* path, int id)(<sys/ipc.h>)。(注:path参数必须引用一个现有的文件,当产生键时,只使用id参数的低8位)。

XSI IPC为每个IPC结构关联了一个ipc_perm结构,该结构规定了权限和所有者,至少包含如下成员:

struct ipc_perm

{

    uid_t  uid;   /* owner's effective user id */

    gid_t  gid;   /* owner's effective group id */

    uid_t  cuid;  /* creator's effective user id */

    gid_t  cgid;  /* creator's effective group id */

    mode_t mode;  /* access modes */

    ...

};

上述结构定义在<sys/ipc.h>中,任何IPC结构都不存在执行权限,下图显示了每种IPC的6种权限:

 

在Linux中,可以运行ipcs –l命令来显示IPC相关的限制:

[root@benxintuzi ipc]# ipcs -l

------ Shared Memory Limits --------

max number of segments = 4096

max seg size (kbytes) = 4194303

max total shared memory (kbytes) = 1073741824

min seg size (bytes) = 1

------ Semaphore Limits --------

max number of arrays = 128

max semaphores per array = 250

max semaphores system wide = 32000

max ops per semop call = 32

semaphore max value = 32767

------ Messages: Limits --------

max queues system wide = 996

max size of message (bytes) = 65536

default max size of queue (bytes) = 65536

消息队列

消息队列是消息的链接表,存储在内核中,由消息队列ID来标识。每个队列都有一个msgid_ds结构与其相关联:

struct msgid_ds

{

    struct ipc_perm   msg_perm;

    msgqnum_t         msg_qnum;     /* # of messages on queue */

    msglen_t          msg_qbytes;   /* max # of bytes on queue */

    pid_t             msg_lspid;    /* pid of last msgsnd() */

    pid_t             msg_lrpid;    /* pid of last msgrcv() */

    time_t            msg_stime;    /* last-msgsnd() time */

    time_t            msg_rtime;    /* last-msgrcv() time */

    time_t            msg_ctime;    /* last-change time */

    ...

};

此结构定义了队列的当前状态。msgget用于创建一个新队列或打开一个现有队列,msgsnd将消息添加到队列的尾端(每个消息包括一个长整型类型字段,一个非负的长度,实际的数据长度),msgrcv用于从队列中取消息(并不一定要以先进先出次序取消息,可以按消息的类型字段取消息)。

#include <sys/msg.h>

int msgget(key_t key, int flag);

返回值:成功,返回消息队列ID;失败,返回-1

说明:

在创建新队列时,要初始化msqid_ds结构的下列成员:

ipc_perm结构中的mode成员按flag中相应权限位设置。

msg_qnum、msg_lspid、msg_lrpid、msg_stime、msg_rtime设为0。

msg_ctime设为当前时间。

msg_qbytes设置为系统限制值。

#include <sys/msg.h>

int msgsnd(int msqid, const void* ptr, size_t nbytes, int flag);

返回值:成功,返回0;失败,返回-1

说明:

ptr是一个指向mymesg结构的指针:

struct mymesg

{

    long   mtype;     /* positive message type */

    char   mtext[512];   /* message data, of length nbytes */

};

flag可以指定为IPC_NOWAIT。类似于文件I/O中的非阻塞标志,若消息队列已满,或队列中的消息总数等于系统限制值,或队列中的字节总数等于系统限制值,则执行IPC_NOWAIT使得msgsnd立即出错返回EAGAIN。如果没有指定IPC_NOWAIT,则进程会一直阻塞到:有空间可以容纳要发送的消息;或者从系统中删除了此队列;或者捕捉到了一个信号,并从信号处理程序返回。

#include <sys/msg.h>

ssize_t msgrcv(int msqid, void* ptr, size_t nbytes, long type, int flag);

返回值:成功,返回消息数据部分的长度;失败,返回-1

说明:

参数type指定了感兴趣消息的类型:

type == 0: 返回队列中的第一个消息。

type > 0: 返回队列中消息类型为type的第一个消息。

type < 0: 返回队列中消息类型值小于等于type绝对值的消息,如果这种消息有若干个,则取类型值最小的消息。

再次解释一下flag参数:

flag指定为IPC_NOWAIT,如果没有指定类型的消息可用,则msgrcv返回-1,errno设置为ENOMSG。

flag未指定为IPC_NOWAIT,则进程会一直阻塞到有了指定类型的消息可用,或者从系统中删除了此队列(返回-1,errno设置为EIDRM),或者捕捉到一个信号,并从信号处理程序返回(返回-1,errno设置为EINTR)。

#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds* buf);

返回值:成功,返回0;失败,返回-1

说明:

cmd参数指定了对msqid指定的队列要执行的命令:

IPC_STAT: 取此队列的msqid_ds结构,并将其存放到buf指向的结构中。

IPC_SET: 将字段msg_perm.uid、msg_perm.gid、msg_perm.mode、msg_qbytes从buf指向的结构复制到与这个队列相关的msqid_ds结构中。这种命令只能由下列两种进程执行:一种是其有效用户ID等于msg_perm.cuid或msg_perm.uid;另一种是具有超级用户特权的进程。只有超级用户才能增加msg_qbytes的值。

IPC_RMID: 从系统中删除该消息队列以及该队列中的所有数据。这种删除立即生效。仍在使用这一消息队列的进程在它们下一次试图对此队列进行操作时,将得到EIDRM错误。此命令的执行权限与IPC_SET选项等效。

上述3个命令(IPC_STAT、IPC_SET、IPC_RMID)也可用于信号量和共享存储。 

注:

消息队列存在的目的是用于提供高于一般速度的IPC,但是现在与其他形式的IPC相比,并没有太大的优势了,但是使用消息队列还时不时地存在一些问题,因此目前程序设计中不推荐使用消息队列来提供解决方案。

信号量

信号量是一个计数器,用于为多个进程提供对共享对象的访问。为了正确地实现信号量,信号量的测试及加减1操作应当是原子操作,为此,信号量通常是在内核中实现的。

常用的信号形式是二元信号量(binary semaphore)。它控制单个资源,其初始值为1。但是,一般而言,信号量的初值也可以是任意一个正值,表明有多少个共享单位可供共享。

内核为每个信号量集合维护着一个semid_ds结构:

struct semid_ds

{

    struct ipc_perm sem_perm;

    unsigned short sem_nsems;   /* # of semaphores in set */

    time_t sem_otime;        /* last-semop() time */

    time_t sem_ctime;        /* last-change time */

    ...

};

每个信号量由一个无名结构体表示,至少包含下列成员:

struct

{

    unsigned short semval;      /* semaphore value, always >= 0 */

    pid_t sempid;               /* pid for last operation */

    unsigned short semncnt;     /* # processes awaiting semval > curval */

    unsigned short semzcnt;     /* # processes awaiting semval == 0 */

    ...

};

影响信号量集合的系统限制如下:

 

当我们想使用XSI信号量时,首先需要通过调用函数semget来获得一个信号量ID:

#include <sys/sem.h>

int semget(key_t key, int nsems, int flag);

返回值:成功,返回信号量ID;失败,返回-1

说明:

nsems是该集合中的信号量数。如果是创建一个新集合(一般在服务器进程中),则必须指定nsems;如果是引用现有集合(一个客户进程),则将nsems指定为0。

#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, .../* union semun arg* */);

说明:

semctl函数包含了多种信号量操作。

第4个参数是可选的。如果使用该参数,则其类型是semun联合:

union semun

{

int val;                    /* for SETVAL */

struct semid_ds* buf;      /* for IPC_STAT and IPC_SET */

unsigned short* array;     /* for GETALL and SETALL */

};

需要留意的是,这个选项是一个联合,而非指向联合的指针。

cmd参数指定了下列10种命令中的一种,这些命令是运行在semid指定的信号量集合上,其中有5种命令是针对一个特定的信号量值的。用semnum指定该信号量集合中的一个成员,semnum的值在0~nsems-1之间。

IPC_STAT: 取此队列的msqid_ds结构,并将其存放到buf指向的结构中。

IPC_SET: 将字段msg_perm.uid、msg_perm.gid、msg_perm.mode、msg_qbytes从buf指向的结构复制到与这个队列相关的msqid_ds结构中。这种命令只能由下列两种进程执行:一种是其有效用户ID等于msg_perm.cuid或msg_perm.uid;另一种是具有超级用户特权的进程。只有超级用户才能增加msg_qbytes的值。

IPC_RMID: 从系统中删除该消息队列以及该队列中的所有数据。这种删除立即生效。仍在使用这一消息队列的进程在它们下一次试图对此队列进行操作时,将得到EIDRM错误。此命令的执行权限与IPC_SET选项等效。

GETVAL: 返回成员semnum的semval值。

SETVAL: 设置成员semnum的semval值。

GETPID: 返回成员semnum的sempid值。

GETNCNT: 返回成员semnum的semncnt值。

GETZCNT: 返回成员semnum的semzcnt值。

GETALL: 取该集合中所有的信号量值,这些值存储在arg.array指向的数组中。

SETALL: 将该集合中所有的信号量值设置成arg.array指向的数组中的值。

#include <sys/sem.h>

int semop(int semid, struct sembuf semoparray[], size_t nops);

返回值:成功,返回0;失败,返回-1

说明:

semoparray是一个指针,指向一个由sembuf结构表示的信号量操作数组:

struct sembuf

{

    unsigned short    sem_num;   /* member # in set(0, 1, ..., nsems - 1) */

    short         sem_op;       /* operation(negative, 0, or pasitive) */

    short         sem_flg;   /* IPC_NOWAIT, SEM_UNDO */

};

其中,nops说明了该数组中操作的数量。

集合中每个成员的操作由相应的sem_op值规定,可以为正值、负值、0:

(1)   sem_op为正,对应进程释放的资源数,sem_op值会加到对应信号量的值上。如果指定了undo标志,也从该进程的信号量值中减去sem_op。

(2)   sem_op为负,表示阻塞在该信号量上的进程数。如果该信号量的值大于等于sem_op的绝对值(具有所需资源),则从信号量值中减去sem_op的绝对值。这能保证信号量的结果值大于等于0。如果指定了undo标志,则sem_op绝对值也加到该进程的此信号量调整值上;如果信号量值小于sem_op绝对值(资源不能满足要求),则有如下情况:

  1. 若指定了IPC_NOWAIT,则semop出错返回EAGAIN。
  2. 若未指定IPC_NOWAIT,则该信号量的semncnt值加1(因为调用进程将进入睡眠状态),然后调用进程被挂起,直至下列事件之一发生:
    1. 此信号量值变为大于等于sem_op的绝对值(表示某个进程已释放了某些资源)。此信号量的semncnt值减1(因为已结束等待),并且从信号量值中减去sem_op的绝对值。如果指定了undo标志,则sem_op的绝对值也加到该进程的此信号量调整值上。
    2. 从系统中删除了此信号量,在这种情况下,函数出错返回EIDRM。
    3. 进程捕捉到一个信号,并从信号的处理程序返回,在这种情况下,此信号量的semncnt值减1(因为调用进程不再等待),并且函数出错返回EINTR。

(3)   若sem_op为0,表示调用进程希望扽带该信号量的值变为0。

如果信号量值当前为0,函数立即返回。

如果信号量值当前不为0,则有如下情况:

  1. 若指定了IPC_NOWAIT,则semop出错返回EAGAIN。
  2. 若未指定IPC_NOWAIT,则该信号量的semzcnt值加1(因为调用进程将进入睡眠状态),然后调用进程被挂起,直至下列事件之一发生:
    1. 此信号量值变为0。此信号量的semzcnt值减1(因为已结束等待)。
    2. 从系统中删除了此信号量,在这种情况下,函数出错返回EIDRM。
    3. 进程捕捉到一个信号,并从信号的处理程序返回,在这种情况下,此信号量的semzcnt值减1(因为调用进程不再等待),并且函数出错返回EINTR。

注:

semop函数具有原子性,要么执行数组中的所有操作,要么一个也不做。

共享存储

共享存储允许两个或多个进程共享一个给定的存储区。因为数据不需要在客户进程和服务器进程之间复制,所以这是最快的一种的IPC。在多个进程之间同步访问一个给定的存储区时,若服务器进程正在将数据放入共享存储区,则在它完成操作之前,客户进程不应当去取这些数据(此时可以通过信号量、记录锁或互斥量进行同步)。

XSI共享存储和内存映射文件之间的区别在于:前者没有相关文件,XSI共享存储段是内存的匿名段。

内核为每个共享存储段维护一个结构,至少包含如下成员:

struct shmid_ds

{

    struct ipc_perm sh_perm;

    size_t        shm_segsz; /* size of segment in bytes */

    pid_t         shm_lpid;  /* pid of last shmop() */

    pid_t         shm_cpid;  /* pid of creator */

    shmatt_t      shm_nattch;   /* number of current attaches */

    time_t        shm_atime; /* last-attach time */

    time_t        shm_dtime; /* last-detach time */

    time_t        shm_ctime; /* last-change time */

    ...

};

影响共享存储的系统限制:

 

函数shmget用来获得一个共享存储标识符:

#include <sys/shm.h>

int shmget(key_t key, size_t size, int flag);

返回值:成功,返回共享存储ID;失败,返回-1

说明:

size是该共享存储段的长度,以字节为单位。一般而言,size长度是页长的整数倍。如果应用程序指定的size值并非系统页长的整数倍,那么最后一页的剩下部分是不可使用的。如果正在创建一个新段(通常是服务器进程),则必须指定其size;如果正在引用一个现存的段(通常是客户进程),则将size 指定为0。

#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds* buf);

返回值:成功,返回0;失败,返回-1

说明:

cmd指定下列5种命令中的一种:

IPC_STAT: 取此队列的shmid_ds结构,并将其存放到buf指向的结构中。

IPC_SET: 按buf指向的结构中的值设置与此共享存储段相关的shmid_ds结构中的下列3个字段:shm_perm.uid、shm_perm.gid、shm_perm.mode。这种命令只能由下列两种进程执行:一种是其有效用户ID等于shm_perm.cuid或shm_perm.uid;另一种是具有超级用户特权的进程。

IPC_RMID: 从系统中删除该共享存储段。因为每个共享存储段维护着一个连接计数(shmid_ds结构中的shm_nattch字段),所以除非使用该段的最后一个进程终止或与该段分离,否则不会实际上删除该存储段。不管此段是否仍在使用,该段标识符都会被立即删除。此命令的执行权限与IPC_SET选项等效。

额外选项:

SHM_LOCK: 在内存中对共享存储段加锁,此命令只能由超级用户执行。

SHM_UNLOCK: 解锁共享存储段,此命令只能由超级用户执行。

一旦创建了一个共享存储段,进程就可以调用shmat将其连接到它的地址空间中:

#include <sys/shm.h>

void* shmat(int shmid, const void* addr, int flag);

返回值:成功,返回指向共享存储段的指针;失败,返回-1

说明:

关于addr:

如果addr为0,则此段连接到由内核选择的第一个可用的地址上(推荐此法)。

如果addr非0,并且没有指定SHM_RND,则此段连接到addr指定的地址上。

如果addr非0,并且指定了SHM_RND,则此段连接到(addr – (addr mod SHMLBA))所指定的地址上。

关于flag:

如果在flag中指定了SHM_RDONLY,则以只读方式连接此段,否则以读写方式连接此段。

当对该共享存储段的操作已经结束时,调用shmdt与该段分离。但这并不会从系统中删除其标识符以及相关的数据结构,该标识符仍然存在,直至某个进程(带IPC_RMID命令)调用shmctl删除它们为止。

#include <sys/shm.h>

int shmdt(const void* addr);

返回值:成功,返回0;失败,返回-1

如下程序打印系统存放各种类型数据的位置信息,包括未初始化数据段、栈、堆、共享存储等:

 1 [root@benxintuzi ipc]# cat printshm.c
 2 #include <sys/shm.h>
 3 #include <stdio.h>
 4 
 5 #define ARRAY_SIZE      40000
 6 #define MALLOC_SIZE     100000
 7 #define SHM_SIZE        100000
 8 #define SHM_MODE        0600    /* user read/write */
 9 
10 char    array[ARRAY_SIZE];      /* uninitialized data = bss */
11 
12 int main(void)
13 {
14         int             shmid;
15         char    *ptr, *shmptr;
16 
17         printf("array[] from %p to %p
", (void *)&array[0],
18           (void *)&array[ARRAY_SIZE]);
19         printf("stack around %p
", (void *)&shmid);
20 
21         if ((ptr = malloc(MALLOC_SIZE)) == NULL)
22                 printf("malloc error
");
23         printf("malloced from %p to %p
", (void *)ptr,
24           (void *)ptr+MALLOC_SIZE);
25 
26         if ((shmid = shmget(IPC_PRIVATE, SHM_SIZE, SHM_MODE)) < 0)
27                 printf("shmget error
");
28         if ((shmptr = shmat(shmid, 0, 0)) == (void *)-1)
29                 printf("shmat error
");
30         printf("shared memory attached from %p to %p
", (void *)shmptr,
31           (void *)shmptr+SHM_SIZE);
32 
33         if (shmctl(shmid, IPC_RMID, 0) < 0)
34                 printf("shmctl error
");
35 
36         return (0);
37 }
38 
39 [root@benxintuzi ipc]# ./printshm
40 array[] from 0x8049140 to 0x8052d80
41 stack around 0xbfe0e4d4
42 malloced from 0x846d008 to 0x84856a8
43 shared memory attached from 0xb7726000 to 0xb773e6a0
44 
45 注:
46 共享存储段是紧靠在栈之下的。
View Code

实例:/dev/zero存储映射

设备/dev/zero可以看成是字节为0的无限资源,其接收写向它的任何数据并且忽略掉。如果我们将此设备作为IPC,那么当对其进行存储映射时,具有如下性质:

(1)   创建一个未命名的存储区,其长度是mmap的第二个参数,将其向上取整为系统的最近页长。

(2)   存储区都初始化为0。

(3)   如果多个进程的共同祖先进程对mmap指定了MAP_SHARED,则这些进程可共享此存储区。

如下程序打开/dev/zero设备,然后指定长整型的长度调用mmap。注意,一旦存储区映射成功,就关闭此设备。然后,进程创建了一个子进程,由于在调用mmap时指定了MAP_SHARED,所以一个进程写到存储映射区的数据可被另一个进程看到。父子进程交替运行,各自对共享存储映射区中的长整型数加1。存储映射区由mmap初始化为0。父进程先对其增1,然后子进程再对其增1,...。

 1 #define    NLOOPS        1000
 2 #define    SIZE        sizeof(long)    /* size of shared memory area */
 3 
 4 static int
 5 update(long *ptr)
 6 {
 7     return((*ptr)++);    /* return value before increment */
 8 }
 9 
10 int main(void)
11 {
12     int        fd, i, counter;
13     pid_t    pid;
14     void    *area;
15 
16     if ((fd = open("/dev/zero", O_RDWR)) < 0)
17         printf("open error
");
18     if ((area = mmap(0, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED,
19       fd, 0)) == MAP_FAILED)
20         printf("mmap error
");
21     close(fd);        /* can close /dev/zero now that it's mapped */
22 
23     TELL_WAIT();
24 
25     if ((pid = fork()) < 0) {
26         printf("fork error
");
27     } else if (pid > 0) {            /* parent */
28         for (i = 0; i < NLOOPS; i += 2) {
29             if ((counter = update((long *)area)) != i)
30             {
31                 printf("parent: expected %d, got %d
", i, counter);
32                 return (-1);
33             }
34 
35             TELL_CHILD(pid);
36             WAIT_CHILD();
37         }
38     } else {                        /* child */
39         for (i = 1; i < NLOOPS + 1; i += 2) {
40             WAIT_PARENT();
41 
42             if ((counter = update((long *)area)) != i)
43             {
44                 printf("child: expected %d, got %d
", i, counter);
45                 return (-1);
46             }
47 
48             TELL_PARENT(getppid());
49         }
50     }
51 
52     return (0);
53 }
View Code

使用/dev/zero的优点是:在调用mmap创建映射区之前,无需存在一个实际文件。很多实现提供了一种类似于/dev/zero的设施,称为匿名存储映射。为了使用这种功能,在调用mmap时指定了MAP_ANON标志,并将文件描述符指定为-1。结果得到的区域是匿名的,并且创建了一个可与后代进程共享的存储区。

对上述程序做如下3处修改即可:a.删除/dev/zero的open语句;b.删除fd的close语句;c.将mmap调用改为:if ((area = mmap(0, SIZE, PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED, -1, 0)) == MAP_FAILED)。

总结:

如果要在两个无关进程之间使用共享存储段,有两种替代方法:一种是应用程序使用XSI共享存储段;另一种是使用mmap将同一文件映射到它们的地址空间,同时使用MAP_SHARED标志。

 

关于经典IPC,有如下建议:要学会使用管道和FIFO,因为这两种基本技术可以有效地应用于大量应用程序。在新的程序设计中,尽可能地避免使用消息队列以及信号量,而应当考虑全双工管道和记录锁,它们使用起来更加简单。共享存储段的功能在多数情况下也可以由mmap函数替代。

原文地址:https://www.cnblogs.com/benxintuzi/p/4782902.html