进程间通信方式之消息队列,信号量和共享内存

1.什么是消息队列

    消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识,具有特定的格式和优先级。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点,消息队列是随内核持续的。目前主要有两种类型--POSIX消息队列和系统V消息队列。

2.消息队列API

a、msgget函数 
该函数用来创建和访问一个消息队列。它的原型为: 
[cpp] view plain copy 
int msgget(key_t, key, int msgflg); 
与其他的IPC机制一样,程序必须提供一个键来命名某个特定的消息队列。msgflg是一个权限标志,表示消息队列的访问权限,它与文件的访问权限一样。msgflg可以与IPC_CREAT做或操作,表示当key所命名的消息队列不存在时创建一个消息队列,如果key所命名的消息队列存在时,IPC_CREAT标志会被忽略,而只返回一个标识符。

它返回一个以key命名的消息队列的标识符(非零整数),失败时返回-1.

b、msgsnd函数 

该函数用来把消息添加到消息队列中。它的原型为: 
[cpp] view plain copy 
int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg); 
msgid是由msgget函数返回的消息队列标识符。

msg_ptr是一个指向准备发送消息的指针,但是消息的数据结构却有一定的要求,指针msg_ptr所指向的消息结构一定要是以一个长整型成员变量开始的结构体,接收函数将用这个成员来确定消息的类型。所以消息结构要定义成这样:

[cpp] view plain copy  
struct my_message{  
    long int message_type;  
    /* The data you wish to transfer*/  

};  

msg_sz是msg_ptr指向的消息的长度,注意是消息的长度,而不是整个结构体的长度,也就是说msg_sz是不包括长整型消息类型成员变量的长度。msgflg用于控制当前消息队列满或队列消息到达系统范围的限制时将要发生的事情。
如果调用成功,消息数据的一分副本将被放到消息队列中,并返回0,失败时返回-1.
c、msgrcv函数 
该函数用来从一个消息队列获取消息,它的原型为 
[cpp] view plain copy 
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg); 
msgid, msg_ptr, msg_st的作用也函数msgsnd函数的一样。
msgtype可以实现一种简单的接收优先级。如果msgtype为0,就获取队列中的第一个消息。如果它的值大于零,将获取具有相同消息类型的第一个信息。如果它小于零,就获取类型等于或小于msgtype的绝对值的第一个消息。
msgflg用于控制当队列中没有相应类型的消息可以接收时将发生的事情。
调用成功时,该函数返回放到接收缓存区中的字节数,消息被复制到由msg_ptr指向的用户分配的缓存区中,然后删除消息队列中的对应消息。失败时返回-1.
d、msgctl函数 
该函数用来控制消息队列,它与共享内存的shmctl函数相似,它的原型为: 
[cpp] view plain copy 
int msgctl(int msgid, int command, struct msgid_ds *buf); 
command是将要采取的动作,它可以取3个值, 
IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。 
IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值 

IPC_RMID:删除消息队列

3.消息队列与命名管道的比较分析
    消息队列跟命名管道有不少的相同之处,通过与命名管道一样,消息队列进行通信的进程可以是不相关的进程,同时它们都是通过发送和接收的方式来传递数据的。在命名管道中,发送数据用write,接收数据用read,则在消息队列中,发送数据用msgsnd,接收数据用msgrcv。而且它们对每个数据都有一个最大长度的限制。

    与命名管道相比,消息队列的优势在于,a、消息队列也可以独立于发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难。b、同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。c、接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。

4.什么是信号量

信号量:一个整数;
  大于或等于0时代表可供并发进程使用的资源实体数;
  小于0时代表正在等待使用临界区的进程数;
  用于互斥的信号量初始值应大于0;
  只能通过P、V原语操作而改变;
 信号量元素组成:
  a、表示信号量元素的值;
  b、最后操作信号量元素的进程ID
  c、等待信号量元素值+1的进程数;

  d、等待信号量元素值为0的进程数;

5.信号量API

semget 
semget函数创建一个新的信号量或是获得一个已存在的信号量键值。
int semget(key_t key, int num_sems, int sem_flags);
    第一个参数key是一个用来允许不相关的进程访问相同信号量的整数值。所有的信号量是为不同的程序通过提供一个key来间接访问的,对于每一个信号量系统 生成一个信号量标识符。信号量键值只可以由semget获得,所有其他的信号量函数所用的信号量标识符都是由semget所返回的。
    还有一个特殊的信号量key值,IPC_PRIVATE(通常为0),其作用是创建一个只有创建进程可以访问的信号量。这通常并没有有用的目的,而幸运的是,因为在某些Linux系统上,手册页将IPC_PRIVATE并没有阻止其他的进程访问信号量作为一个bug列出。
num_sems参数是所需要的信号量数目。这个值通常总是1。

sem_flags参数是一个标记集合,与open函数的标记十分类似。低九位是信号的权限,其作用与文件权限类似。另外,这些标记可以与 IPC_CREAT进行或操作来创建新的信号量。设置IPC_CREAT标记并且指定一个已经存在的信号量键值并不是一个错误。如果不需 要,IPC_CREAT标记只是被简单的忽略。我们可以使用IPC_CREAT与IPC_EXCL的组合来保证我们可以获得一个新的,唯一的信号量。如果 这个信号量已经存在,则会返回一个错误。如果成功,semget函数会返回一个正数;这是用于其他信号量函数的标识符。如果失败,则会返回-1。

semop 

函数semop用来改变信号量的值:

int semop(int sem_id, struct sembuf *sem_ops, size_t num_sem_ops);

第一个参数,sem_id,是由semget函数所返回的信号量标识符。第二个参数,sem_ops,是一个指向结构数组的指针,其中的每一个结构至少包含下列成员:

struct sembuf {

    short sem_num;
    short sem_op;
    short sem_flg;
}
    第一个成员,sem_num,是信号量数目,通常为0,除非我们正在使用一个信号量数组。sem_op成员是信号量的变化量值。(我们可以以任何量改变信 号量值,而不只是1)通常情况下中使用两个值,-1是我们的P操作,用来等待一个信号量变得可用,而+1是我们的V操作,用来通知一个信号量可用。
    最后一个成员,sem_flg,通常设置为SEM_UNDO。这会使得操作系统跟踪当前进程对信号量所做的改变,而且如果进程终止而没有释放这个信号量, 如果信号量为这个进程所占有,这个标记可以使得操作系统自动释放这个信号量。将sem_flg设置为SEM_UNDO是一个好习惯,除非我们需要不同的行 为。如果我们确实变我们需要一个不同的值而不是SEM_UNDO,一致性是十分重要的,否则我们就会变得十分迷惑,当我们的进程退出时,内核是否会尝试清 理我们的信号量。

semop的所用动作会同时作用,从而避免多个信号量的使用所引起的竞争条件。我们可以在手册页中了解关于semop处理更为详细的信息。

semctl 

semctl函数允许信号量信息的直接控制:

int semctl(int sem_id, int sem_num, int command, ...);

第一个参数,sem_id,是由semget所获得的信号量标识符。sem_num参数是信号量数目。当我们使用信号量数组时会用到这个参数。通常,如果 这是第一个且是唯一的一个信号量,这个值为0。command参数是要执行的动作,而如果提供了额外的参数,则是union semun,根据X/OPEN规范,这个参数至少包括下列参数:
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;

}

6.竞争问题

两个相同的进程,同时向stdout中输出字符,只是没有使用信号量,两个进程在互相竞争stdout。它的代码非常简单,文件名为normalprint.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() % 2);  
    }  
    sleep(10);  
    printf("\n%d - finished\n", getpid());  
    exit(EXIT_SUCCESS);  

}  

运行结果如下:


从上面的输出结果,我们可以看到字符‘X’和‘O’并不像前面的例子那样,总是成对出现,因为当第一个进程A输出了字符后,调用sleep休眠时,另一个进程B立即输出并休眠,而进程A醒来时,再继续执行输出,同样的进程B也是如此。所以输出的字符就是不成对的出现。这两个进程在竞争stdout这一共同的资源。通过两个例子的对比,我想信号量的意义和使用应该比较清楚了。

7.共享内存

共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址,就好像它们是由用C语言函数malloc分配的内存一样。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。注意:共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取。所以我们通常需要用其他的机制来同步对共享内存的访问。

8.共享内存的特点

共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
因为多个进程可以同时操作,所以需要进行同步。

信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

9.共享内存的常用API介绍

头文件 sys/shm.h中
q、shmget函数

该函数用来创建共享内存

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

第一个参数,与信号量的semget函数一样,程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,shmget函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1.
不相关的进程可以通过该函数的返回值访问同一共享内存,它代表程序可能要使用的某个资源,程序对所有共享内存的访问都是间接的,程序先通过调用shmget函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget函数的返回值),只有shmget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。
第二个参数,size以字节为单位指定需要共享的内存容量

第三个参数,shmflg是权限标志,它的作用与open函数的mode参数一样,如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。共享内存的权限标志与文件的读写权限一样,举例来说,0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。

b、shmat函数

第一次创建完共享内存时,它还不能被任何进程访问,shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。它的原型如下:

在CODE上查看代码片派生到我的代码片

void *shmat(int shm_id, const void *shm_addr, int shmflg); 

第一个参数,shm_id是由shmget函数返回的共享内存标识。
第二个参数,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
第三个参数,shm_flg是一组标志位,通常为0。

调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.

c、shmdt函数
该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。它的原型如下:

在CODE上查看代码片派生到我的代码片

int shmdt(const void *shmaddr); 

参数shmaddr是shmat函数返回的地址指针,调用成功时返回0,失败时返回-1.

d、shmctl函数
与信号量的semctl函数一样,用来控制共享内存,它的原型如下:

在CODE上查看代码片派生到我的代码片

int shmctl(int shm_id, int command, struct shmid_ds *buf);    

第一个参数,shm_id是shmget函数返回的共享内存标识符。
第二个参数,command是要采取的操作,它可以取下面的三个值 :
    IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
    IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
    IPC_RMID:删除共享内存段
第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构。
shmid_ds结构至少包括以下成员:
[cpp] view plain copy
print?在CODE上查看代码片派生到我的代码片
    struct shmid_ds  
    {  
        uid_t shm_perm.uid;  
        uid_t shm_perm.gid;  
        mode_t shm_perm.mode;  

    };  

10.共享内存删除的陷阱

当进程结束使用共享内存区时,要通过函数 shmdt 断开与共享内存区的连接。该函数声明在 sys/shm.h 中,其原型如下:
#include 
#include 

int shmdt(const void *shmaddr); 

参数 shmaddr 是 shmat 函数的返回值。

进程脱离共享内存区后,数据结构 shmid_ds 中的 shm_nattch 就会减 1 。但是共享段内存依然存在,只有 shm_attch 为 0 后,即没有任何进程再使用该共享内存区,共享内存区才在内核中被删除。一般来说,当一个进程终止时,它所附加的共享内存区都会自动脱离。
我们通过:
int shmctl( int shmid , int cmd , struct shmid_ds *buf ); 
来删除已经存在的共享内存。
第一个参数,shmid,是由shmget所返回的标记符。
第二个参数,cmd,是要执行的动作。他可以有三个值:
命令 描述
IPC_STAT 设置shmid_ds结构中的数据反射与共享内存相关联的值。
IPC_SET 如果进程有相应的权限,将与共享内存相关联的值设置为shmid_ds数据结构中所提供的值。
IPC_RMID 删除共享内存段。
第三个参数,buf,是一个指向包含共享内存模式与权限的结构的指针,删除的时候可以默认为0。
如果共享内存已经与所有访问它的进程断开了连接,则调用IPC_RMID子命令后,系统将立即删除共享内存的标识符,并删除该共享内存区,以及所有相关的数据结构;
如果仍有别的进程与该共享内存保持连接,则调用IPC_RMID子命令后,该共享内存并不会被立即从系统中删除,而是被设置为IPC_PRIVATE状态,并被标记为”已被删除”(使用ipcs命令可以看到dest字段);直到已有连接全部断开,该共享内存才会最终从系统中消失。
需要说明的是:一旦通过shmctl对共享内存进行了删除操作,则该共享内存将不能再接受任何新的连接,即使它依然存在于系统中!所以,可以确知, 在对共享内存删除之后不可能再有新的连接,则执行删除操作是安全的;否则,在删除操作之后如仍有新的连接发生,则这些连接都将可能失败!
Shmdt和shmctl的区别:
Shmdt 是将共享内存从进程空间detach出来,使进程中的shmid无效化,不可以使用。但是保留空间。
而shmctl(sid,IPC_RMID,0)则是删除共享内存,彻底不可用,释放空间。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值