【Linux】进程间通信之消息队列、信号量和共享存储

原创 2015年07月07日 19:24:16

消息队列、信号量和共享存储是IPC(进程间通信)的三种形式,它们功能不同,但有相似之处,下面先介绍它们的相似点,然后再逐一说明。

1、相似点

每个内核中的IPC结构(消息队列、信号量和共享存储)都用一个非负整数的标识符加以引用,与文件描述符不同,当一个IPC结构被创建,以后又被删除时,与这种结构相关的标识符连续加1,直至达到一个整型数的最大正直,然后又回转到0。标识符是IPC对象的内部名,还有一个外部名称为键,数据类型是key_t,通常在头文件

#include <sys/ipc.h> 
key_t ftok(const char *pathname, int proj_id);

消息队列、信号量和共享存储都有自己的get函数,msgget、semget和shmget,用于创建IPC对象,它们都设置了自己的ipc_perm结构,在头文件

struct ipc_perm { 
               key_t          __key;       /* Key supplied to msgget(2) */ 
               uid_t          uid;         /* Effective UID of owner */ 
               gid_t          gid;         /* Effective GID of owner */ 
               uid_t          cuid;        /* Effective UID of creator */ 
               gid_t          cgid;        /* Effective GID of creator */ 
               unsigned short mode;        /* Permissions */ 
               unsigned short __seq;       /* Sequence number */ 
};

消息队列、信号量和共享存储都有自己的内置限制,这些限制的大多数可以通过重新配置内核而加以更改,如sysctl命令,可以配置运行时内核参数,在Linux(Ubuntu)上,运行命令“ipcs -l”可查看相关限制,如下:

$ ipcs -l

------ Messages Limits --------
max queues system wide = 32000
max size of message (bytes) = 8192
default max size of queue (bytes) = 16384

------ Shared Memory Limits --------
max number of segments = 4096
max seg size (kbytes) = 18014398509465599
max total shared memory (kbytes) = 18014398442373116
min seg size (bytes) = 1

------ Semaphore Limits --------
max number of arrays = 32000
max semaphores per array = 32000
max semaphores system wide = 1024000000
max ops per semop call = 500
semaphore max value = 32767

需要注意的是,IPC对象是在系统范围内起作用的,没有访问计数,不同于普通文件。例如,如果进程创建了一个消息队列,在该队列中放入了几则消息,然后终止,但是该消息队列及其内容并不会被删除,它们余留在系统中直至出现下述情况:由某个进程调用msgrcv读消息或msgctl删除消息队列;或某个进程执行ipcrm命令删除消息队列;或由正在再启动的系统删除消息队列。将此与管道相比,当最后一个访问管道的进程被终止时,管道就被完全地删除了。对于FIFO而言,虽然当最后一个引用FIFO的进程终止时其名字仍保留在系统中,直至显式地删除它,但是留在FIFO中的数据却在此时被全部删除。

2、消息队列

消息队列即message queue,存放在内核中并由消息队列标识符标识,涉及如下函数和数据结构。

#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/msg.h> 

int msgget(key_t key, int msgflg);
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); 
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); 
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

struct msqid_ds { 
               struct ipc_perm msg_perm;     /* Ownership and permissions */ 
               time_t          msg_stime;    /* Time of last msgsnd(2) */ 
               time_t          msg_rtime;    /* Time of last msgrcv(2) */ 
               time_t          msg_ctime;    /* Time of last change */ 
               unsigned long   __msg_cbytes; /* Current number of bytes in 
                                                queue (nonstandard) */ 
               msgqnum_t       msg_qnum;     /* Current number of messages 
                                                in queue */ 
               msglen_t        msg_qbytes;   /* Maximum number of bytes 
                                                allowed in queue */ 
               pid_t           msg_lspid;    /* PID of last msgsnd(2) */ 
               pid_t           msg_lrpid;    /* PID of last msgrcv(2) */ 
};

struct msgbuf {
               long mtype;       /* message type, must be > 0 */
               char mtext[1];    /* message data */
};

msgget用于创建一个新队列或打开一个现存的队列,参数key可自定义或通过ftok生成,或者使用IPC_PRIVATE,需要保证的是key值没有与现存的队列相关联,msgflag为O_CREAT时创建新队列,排它性使用O_EXCL。msgsnd将新消息添加到队列尾端,参数msqid为消息队列id,msgp比较特殊,需要包括两部分内容,消息类型和实际的消息数据,如上面的msgbuf结构,msgsz指定消息长度,msgflag可以设置为IPC_NOWAIT,表示非阻塞。msgrcv用于从队列中取消息,参数msgsz表示缓冲区长度,当消息长度大于msgsz时,若msgflg设置了MSG_NOERROR则截短消息,否则出错E2BIG,msgtyp为0时获取第一个消息,大于0时获取类型为msgtyp的第一个消息,小于0时获取类型小于等于msgtyp的类型值最小的第一个消息。每个消息队列都有一个msqid_dt结构与其相关联,规定了队列的当前状态,msgctl则可以对消息队列的这种结构进行操作,参数cmd可以是IPC_STAT、IPC_SET、IPC_RMID,分别表示获取状态、设置状态、移除消息队列。

下面例子说明消息队列的用法,msgsnd.c发送消息,msgrcv.c接收消息,当输入“quit”时结束。

// msgsnd.c 
#include <stdio.h> 
#include <stdlib.h> 
#include <stdbool.h> 
#include <string.h> 
#include <errno.h> 
#include <sys/msg.h> 
#include <unistd.h> 

struct msg_st 
{ 
    long int msg_type; 
    char text[BUFSIZ]; 
}; 

int main(void) 
{ 
    struct msg_st data; 
    data.msg_type = 1; 
    char buf[BUFSIZ]; 
    key_t akey = 1000; 
    int msgid = -1; 
    bool running = true; 

    // create message queue 
    msgid = msgget(akey, 0666 | IPC_CREAT); 
    if (-1 == msgid) { 
        fprintf(stderr, "msgget failed with error: %d\n", errno); 
        exit(EXIT_FAILURE); 
    } 

    // loop for sending data to message queue 
    while (running) { 
        printf("Input text: "); 
        fgets(buf, BUFSIZ, stdin); 
        strcpy(data.text, buf); 
        // send data 
        if (-1 == msgsnd(msgid, (void*)&data, BUFSIZ, 0)) 
        { 
            fprintf(stderr, "msgsnd failed\n"); 
            exit(EXIT_FAILURE); 
        } 
        // input "quit" to finish 
        if(0 == strncmp(buf, "quit", 4)) { 
            running = false; 
        } 
        sleep(1); 
    } 

    exit(EXIT_SUCCESS); 
} 

// msgrcv.c 
#include <stdio.h> 
#include <stdlib.h> 
#include <stdbool.h> 
#include <string.h> 
#include <errno.h> 
#include <sys/msg.h> 
#include <unistd.h> 

struct msg_st 
{ 
    long int msg_type; 
    char text[BUFSIZ]; 
}; 

int main(void) 
{ 

    struct msg_st data; 
    data.msg_type = 0; 
    key_t akey = 1000; 
    int msgid = -1; 
    bool running = true; 

    // create messge queue 
    msgid = msgget(akey, 0666 | IPC_CREAT); 
    if (-1 == msgid) { 
        fprintf(stderr, "msgget failed with error: %d\n", errno); 
        exit(EXIT_FAILURE); 
    } 

    // loop for getting data from message queue 
    while (running) { 
        // receive data 
        if(-1 == msgrcv(msgid, (void*)&data, BUFSIZ, data.msg_type, 0)) 
        { 
            fprintf(stderr, "msgrcv failed with errno: %d\n", errno); 
            exit(EXIT_FAILURE); 
        } 
        printf("Receive text: %s\n",data.text); 
        // receive "quit" to finish 
        if(0 == strncmp(data.text, "quit", 4)) { 
            running = false; 
        } 
    } 

    // delete message queue 
    if (-1 == msgctl(msgid, IPC_RMID, 0)) 
    { 
        fprintf(stderr, "msgctl(IPC_RMID) failed\n"); 
        exit(EXIT_FAILURE); 
    } 

    exit(EXIT_SUCCESS); 
} 

3、信号量

信号量semaphore确切的说是一种同步方式,涉及如下函数和数据结构。

#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/sem.h> 

int semget(key_t key, int nsems, int semflg);
int semctl(int semid, int semnum, int cmd, ...); 
int semop(int semid, struct sembuf *sops, unsigned nsops);

struct semid_ds {
               struct ipc_perm sem_perm;  /* Ownership and permissions */
               time_t          sem_otime; /* Last semop time */
               time_t          sem_ctime; /* Last change time */
               unsigned long   sem_nsems; /* No. of semaphores in set */
};

union semun {
               int              val;    /* Value for SETVAL */
               struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
               unsigned short  *array;  /* Array for GETALL, SETALL */
               struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                           (Linux-specific) */
};

struct sembuf
{
  unsigned short int sem_num;   /* semaphore number */
  short int sem_op;     /* semaphore operation */
  short int sem_flg;        /* operation flag */
};

struct
{
    unsigned short  semval;   /* semaphore value */
           unsigned short  semzcnt;  /* # waiting for zero */
           unsigned short  semncnt;  /* # waiting for increase */
           pid_t           sempid;   /* ID of process that did last op */
}

信号量是一个计数器,用于多进程对共享数据对象的访问。为了获得共享资源,进程需要执行下列操作:
(1)测试控制该资源的信号量。
(2)若此信号量的值为正,则进程可以使用该资源,进程将信号量值减1,表示它使用了一个资源单位。
(3)若此信号量的值为0,则进程进入休眠状态,直至信号量值大于0,进程被唤醒后,它返回第(1)步。

当进程不再使用由一个信号量控制的共享资源时,该信号量值增1,如果有进程正在休眠等待此信号量,则唤醒它们。为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作,为此,信号量通常是在内核中实现的。常用的信号量形式被成为二元信号量或双态信号量,它控制单个资源,初始值为1,但是一般而言,信号量的初值可以是任一正值,该值说明有多少个共享资源单位可供共享使用。需要注意的是,信号量并非是单个非负值,为一个包含了一个或多个信号量值的信号量集,创建信号量需要指定信号量集中的信号个数。

semget用于获取信号量集标识符,参数nsems表示信号量个数,创建新的信号量集时必须大于0,获取已有的则为0。semctl对信号量进行操作,第四个参数可选,类型为semun联合,semnum指定信号量集中的某个信号,cmd同消息队列的msgctl一样也可以是IPC_STAT、IPC_SET、IPC_RMID,还有形如GETXXX的值。semop函数是个原子操作,自动执行信号量集合上的操作数组sops,sops为sembuf结构体,成员sem_op可以为0、正数、负数,sem_flg为IPC_NOWAIT或SEM_UNDO,后者表示进程终止时自动处理还未处理的信号量,参数nsops规定该数组中操作的数量。

先来看一个不使用信号量的例子:

// semaphore2.c 
#include <stdio.h> 
#include <stdlib.h> 

int main(int argc, char *argv[]) 
{ 
    char message = 'X'; 
    int i = 0;   

    if (argc > 1) { 
        message = argv[1][0]; 
    } 

    for (i = 0; i < 10; ++i) { 
        printf("%c", message); 
        fflush(stdout); 
        sleep(rand() % 3); 
        printf("%c", message); 
        fflush(stdout); 
        sleep(rand() % 3); 
    } 

    sleep(10); 
    printf("\n%d - finished\n", getpid()); 

    exit(EXIT_SUCCESS); 
}

编译运行:

$gcc -o sem semaphore2.c
$./sem A & ./sem
[1] 5647 
AXAXAXAXXAAXAXAXAAXXAXXAXAAXAXAXAXAXAXXA 

5648 - finished 
5647 - finished 
[1]+  Done                    ./sem A 

一个进程在for循环中连续两次输出A,并启动到后台运行,另一个进程在for循环中连续两次输出X,从上面的结果可以看出,它们相互竞争,结果是乱序的,并不是两个连续的A或者X,下面用信号量改写上面的例子:

#include <unistd.h> 
#include <sys/types.h> 
#include <sys/stat.h> 
#include <fcntl.h> 
#include <stdlib.h> 
#include <stdio.h> 
#include <string.h> 
#include <sys/sem.h> 

union semun 
{ 
    int val; 
    struct semid_ds *buf; 
    unsigned short *arry; 
}; 

static int sem_id = 0; 

int main(int argc, char *argv[]) 
{ 
    key_t akey = 1000; 
    char message = 'X'; 
    int i = 0; 

    // create semaphore 
    sem_id = semget(akey, 1, 0666 | IPC_CREAT); 
    if (-1 == sem_id) { 
        fprintf(stderr, "Failed to create semaphore\n"); 
        exit(EXIT_FAILURE); 
    } 

    if (argc > 1) { 
        // semaphore initialization, must 
        union semun sem_union; 
        sem_union.val = 1; 
        if (-1 == semctl(sem_id, 0, SETVAL, sem_union)) { 
            fprintf(stderr, "Failed to initialize semaphore\n"); 
            exit(EXIT_FAILURE); 
        } 

        message = argv[1][0]; 
        sleep(1); 
    } 

    for (i = 0; i < 10; ++i) { 
        // go into critical zone 
        struct sembuf sem_i; 
        sem_i.sem_num = 0; 
        sem_i.sem_op = -1; 
        sem_i.sem_flg = SEM_UNDO; 
        if (-1 == semop(sem_id, &sem_i, 1)) 
        { 
            perror("semop in failed\n"); 
            exit(EXIT_FAILURE); 
        } 

        printf("%c", message); 
        fflush(stdout); 
        sleep(rand() % 3); 
        printf("%c", message); 
        fflush(stdout); 

        // leave critical zone 
        struct sembuf sem_o; 
        sem_o.sem_num = 0; 
        sem_o.sem_op = 1; 
        sem_o.sem_flg = SEM_UNDO; 
        if (-1 == semop(sem_id, &sem_o, 1)) { 
            perror("semop out failed\n"); 
            exit(EXIT_FAILURE); 
        } 

        sleep(rand() % 3); 
    } 

    sleep(10); 
    printf("\n%d - finished\n", getpid()); 

    if (argc > 1) { 
        // delete samaphore 
        sleep(3); 
        union semun sem_union; 
        if (-1 == semctl(sem_id, 0, IPC_RMID, sem_union)) { 
            fprintf(stderr, "Failed to delete semaphore\n"); 
        } 
    } 

    exit(EXIT_SUCCESS); 
} 

执行结果如下:

XXAAXXAAXXAAXXAAXXAAXXAAXXAAXXAAXXXXAAAA 

可见,使用了信号量,输出结果符合预期,两个A或者两个X连在了一起。

4、共享存储

共享存储允许两个或多个进程共享一给定的存储区,因为数据不需要在客户进程和服务器进程之间复制,所以这是最快的一种IPC。使用共享存储时需要掌握的唯一窍门是多个进程之间对一给定存储区的同步访问,若服务器进程正在将数据放入共享存储区,则在它做完这一操作之前,客户进程不应当去取这些数据,通常,信号量被用来实现对共享存储访问的同步。下面是相关的几个函数和数据结构:

#include <sys/ipc.h> 
#include <sys/shm.h> 

int shmget(key_t key, size_t size, int shmflg);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);

struct shmid_ds {
               struct ipc_perm shm_perm;    /* Ownership and permissions */
               size_t          shm_segsz;   /* Size of segment (bytes) */
               time_t          shm_atime;   /* Last attach time */
               time_t          shm_dtime;   /* Last detach time */
               time_t          shm_ctime;   /* Last change time */
               pid_t           shm_cpid;    /* PID of creator */
               pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
               shmatt_t        shm_nattch;  /* No. of current attaches */
               ...
};

shmget用于获取共享存储标识符,参数size为共享存储区的长度,单位是字节,实现通常将其向上取为系统页长的整数倍,若size并非系统页长的整数倍,那么最后一页的余下部分是不可用的,创建一个新的共享存储区时size需要大于0,引用已有的共享存储区则将size设置为0。shmctl可操作共享存储区,同样可以是IPC_STAT、IPC_SET、IPC_RMID等。
shmat用于将共享存储段连接到调用进程指定的地址shmaddr上,但一般应指定shmaddr为0,内核会自动选择合适的地址,shmflg可选SHM_RND即地址取整,SHM_RDONLY只读,默认读写。当对共享存储段的操作结束时,调用shmdt取消当前进程与共享存储段的连接。

下面是一个使用了shm的例子,程序中fork之后,子进程sleep保证父进程先执行,父进程取得共享存储区以后写入“hello world”,子进程同样也取得了这个共享存储区,然后访问同一块地址,读到了“hello world”。

#include <fcntl.h> 
#include <stdlib.h> 
#include <stdio.h> 
#include <string.h> 
#include <sys/shm.h> 

#define SIZE 1024 
#define exit_err(str) do { perror(str); exit(EXIT_FAILURE); } while (0); 
#define uint32 unsigned long 

int main(void) 
{ 
    int shmid; 
    char *shmptr; 
    key_t key; 
    pid_t pid; 

    if ((pid = fork()) < 0) { 
        exit_err("fork error"); 
    } 

    if(0 == pid) { 
        printf("child process\n"); 

        if ((key = ftok("/dev/null", O_RDWR)) < 0) { 
            exit_err("ftok error"); 
        } 

        if ((shmid = shmget(key, SIZE, 0600 | IPC_CREAT)) < 0) { 
            exit_err("shmget error"); 
        } 

        if ((shmptr = (char*)shmat(shmid, 0, 0)) == (void*)-1) { 
           exit_err("shmat error"); 
        } 

        sleep(1); 
        printf("child pid is %d, share memory from %lx to %lx, content: %s\n",getpid(), (uint32)shmptr, (uint32)(shmptr + SIZE), shmptr); 
        sleep(1); 

        if ((shmctl(shmid, IPC_RMID, 0) < 0)) { 
             exit_err("shmctl error"); 
        } 
        exit(EXIT_SUCCESS); 
    } 
    else { 
        printf("parent process\n"); 

        if ((key = ftok("/dev/null", O_RDWR)) < 0) { 
            exit_err("ftok error");    
        } 

        if ((shmid = shmget(key, SIZE, 0600 | IPC_CREAT | IPC_EXCL)) < 0) { 
          exit_err("shmget error"); 
        } 

        if((shmptr = (char*)shmat(shmid, 0, 0)) == (void*)-1) { 
          exit_err("shmat error"); 
        } 

        memcpy(shmptr, "hello world", sizeof("hello world")); 
        printf("parent pid is %d, share memory from %lx to %lx, content: %s\n",getpid(),(uint32)shmptr, (uint32)(shmptr + SIZE), shmptr); 
    } 

    waitpid(pid, NULL, 0); 
    exit(EXIT_SUCCESS); 
} 

执行结果如下:

parent process 
parent pid is 7275, share memory from 7fb2ebf4a000 to 7fb2ebf4a400, content: hello world 
child process 
child pid is 7276, share memory from 7fb2ebf4a000 to 7fb2ebf4a400, content: hello world
版权声明:本文为博主原创文章,未经博主允许不得转载。

相关文章推荐

Linux共享存储空间

一)概念:  1)Linux和所有的UNIX操作系统都允许通过共享内存在应用程序之间共享存储空间.  2)有两类基本的API函数用于在进程间共享内存:System v和POSIX.  3)这两类函数上...
  • hcmmin
  • hcmmin
  • 2011年01月10日 15:35
  • 2608

Linux进程通信 -- 共享内存实战

共享内存原理及实现system V IPC机制下的共享内存本质是一段特殊的内存区域,进程间需要共享的数据被放在该共享内存区域中,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去。...

进程通信方式:共享内存区

本文主要参考《unix网络编程卷2:进程间通信》 另外可以参考以下文章: mmap:Linux环境进程间通信(五): 共享内存(上) System V共享内存: Linux环境进程间通信(五): ...

实验六 共享存储区通信

 实验六  共享存储区通信实验目的了解和熟悉共享存储机制实验内容编制一长度为1k的共享存储区发送和接收的程序。实验指导一、共享存储区1、共享存储区机制的概念共享存储区(Share  Memory)是U...

【Linux】进程间通信之管道pipe与FIFO

管道pipe是UNIX系统IPC进程间通信的最古老形式,并且所有UNIX系统都提供此种通信机制。管道有下面两种局限性: 1、历史上,管道是半双工管道,数据只能在一个方向上流动,某些系统则提供了全双工...
  • iEearth
  • iEearth
  • 2015年07月06日 10:53
  • 937

waitpid()函数详解

waitpid系统调用在Linux函数库中的原型是: #include   #include pid_t waitpid(pid_t pid,int *status,int options)...

Linux进程间通信--信号,管道,消息队列,信号量,共享内存,socket

linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的。而对Unix发展做出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面...
  • B_H_L
  • B_H_L
  • 2013年12月03日 17:31
  • 6213

Linux进程间通信方式--信号,管道,消息队列,信号量,共享内存

通信方法 无法介于内核态与用户态的原因 管道(不包括命名管道) 局限于父子进程间的通信。 消息队列 在硬、软中断中无法无阻塞地接收数据。 信...

Linux进程间通信--进程,信号,管道,消息队列,信号量,共享内存

Linux 传统的进程间通信有很多,如各类管道、消息队列、内存共享、信号量等等。但它们都无法介于内核态与用户态使用,原因如表 通信方法 无法介于内核态与用户态的原因 ...

Linux进程间通信--进程,信号,管道,消息队列,信号量,共享内存

参考:《linux编程从入门到精通》,《Linux C程序设计大全》,《unix环境高级编程》 参考:C和指针学习  说明:本文非常的长,也是为了便于查找和比较,所以放在一起了 ...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:【Linux】进程间通信之消息队列、信号量和共享存储
举报原因:
原因补充:

(最多只允许输入30个字)