Linux进程间通信—消息队列

时间:2021-04-08 07:36:01

四.消息队列(Message Queue)

消息队列就是消息的一个链表,它允许一个或者多个进程向它写消息,一个或多个进程向它读消息。Linux维护了一个消息队列向量表:msgque,来表示系统中所有的消息队列。

消息队列克服了信号传递信息少,管道只能支持无格式字节流和缓冲区受限的缺点。

消息队列用于运行于同一台机器上的进程间通信,它和管道很相似,是一个在系统内核中用来保存消息的队列,它在系统内核中是以消息链表的形式出现。消息链表中节点的结构用msg声明。

事实上,它是一种正逐渐被淘汰的通信方式,我们可以用流管道或者套接口的方式来取代它,所以,我们对此方式也不再解释,也建议读者忽略这种方式。

消息队列(message queue)与PIPE相类似。它也是建立一个队列,先放入队列的消息被最先取出。不同的是,消息队列允许多个进程放入消息,也允许多个进程取出消息。每个消息可以带有一个整数识别符(message_type)。你可以通过识别符对消息分类 (极端的情况是将每个消息设置一个不同的识别符)。某个进程从队列中取出消息的时候,可以按照先进先出的顺序取出,也可以只取出符合某个识别符的消息(有多个这样的消息时,同样按照先进先出的顺序取出)。消息队列与PIPE的另一个不同在于它并不使用文件API。最后,一个队列不会自动消失,它会一直存在于内核中,直到某个进程删除该队列。

消息队列(也叫做报文队列)能够克服早期unix通信机制的一些缺点。消息队列就是一个消息的链表,用户可以在消息队列中添加消息和读取消息等。从这点上看,消息队列具有一定的FIFO特性,但是它可以实现消息的随机查询,比FIFO具有更大的优势。同时,消息队列是随内核持续的,这些消息存在于内核中的,由"队列ID"来标志。

下面给出了IPC随进程持续、随内核持续以及随文件系统持续的定义:

随进程持续:IPC一直存在到打开IPC对象的最后一个进程关闭该对象为止。如管道和有名管道;

随内核持续:IPC一直持续到内核重新自举或者显示删除该对象为止。如消息队列、信号灯以及共享内存等;

随文件系统持续:IPC一直持续到显示删除该对象为止。

目前主要有两种类型的消息队列:POSIX消息队列以及系统V消息队列,系统V消息队列目前被大量使用。考虑到程序的可移植性,新开发的应用程序应尽量使用POSIX消息队列。

系统V消息队列是随内核持续的,只有在内核重起或者显示删除一个消息队列时,该消息队列才会真正被删除。因此系统中记录消息队列的数据结构(struct ipc_ids msg_ids)位于内核中,系统中的所有消息队列都可以在结构msg_ids中找到访问入口。

对消息队列的操作无非有下面三种类型:

、打开或创建消息队列

消息队列的内核持续性要求每个消息队列都在系统范围内对应唯一的键值,所以,香港服务器,要获得一个消息队列的描述字,只需提供该消息队列的键值即可,使用 msgget();

注:消息队列描述字是由在系统范围内唯一的键值生成的,而键值可以看作对应系统内的一条路经。

、读写操作

消息读写操作非常简单,对开发人员来说,每个消息都类似如下的数据结构:

struct msgbuf{

long mtype;

char mtext[1];

};

。因此,对于发送消息来说,首先预置一个msgbuf缓冲区并写入消息类型和内容,调用相应的发送函数即可;对读取消息来说,首先分配这样一个msgbuf缓冲区,然后把消息读入该缓冲区即可。添加消息使用的函数是 msgsnd(),它把消息从消息队列中取走;读取消息使用的函数是msgrcv(),它把消息从消息队列中取走。

、控制消息队列:

控制消息队列使用的函数是msgctl();它可以完成多项功能。

消息队列API :

、文件名到键值

#include <sys/types.h> #include <sys/ipc.h> key_t ftok (char*pathname, char proj);

它返回与路径pathname相对应的一个键值。该函数不直接对消息队列操作,但在调用ipc(MSGGET,…)或msgget()来获得消息队列描述字前,往往要调用该函数。典型的调用代码是:

key=ftok(path_ptr, 'a'); ipc_id=ipc(MSGGET, (int)key, flags,0,NULL,0); …

、linux为操作系统V进程间通信的三种方式(消息队列、信号灯、共享内存区)提供了一个统一的用户界面:

int ipc(unsigned int call, int first, int second, int third, void * ptr, long fifth);

第一个参数指明对IPC对象的操作方式,对消息队列而言共有四种操作:MSGSND、MSGRCV、MSGGET以及MSGCTL,分别代表向消息队列发送消息、从消息队列读取消息、打开或创建消息队列、控制消息队列;first参数代表唯一的IPC对象;下面将介绍四种操作。

 int ipc( MSGGET, intfirst, intsecond, intthird, void*ptr, longfifth);

与该操作对应的系统V调用为:int msgget( (key_t)first,second)。

 int ipc( MSGCTL, intfirst, intsecond, intthird, void*ptr, longfifth)

与该操作对应的系统V调用为:int msgctl( first,second, (struct msqid_ds*) ptr)。

 int ipc( MSGSND, intfirst, intsecond, intthird, void*ptr, longfifth);

与该操作对应的系统V调用为:int msgsnd( first, (struct msgbuf*)ptr, second, third)。

 int ipc( MSGRCV, intfirst, intsecond, intthird, void*ptr, longfifth);

与该操作对应的系统V调用为:int msgrcv( first,(struct msgbuf*)ptr, second, fifth,third),

注:本人不主张采用系统调用ipc(),而更倾向于采用系统V或者POSIX进程间通信API。原因如下:

 虽然该系统调用提供了统一的用户界面,但正是由于这个特性,它的参数几乎不能给出特定的实际意义(如以first、second来命名参数),在一定程度上造成开发不便。

 正如ipc手册所说的:ipc()是linux所特有的,编写程序时应注意程序的移植性问题;

 该系统调用的实现不过是把系统V IPC函数进行了封装,没有任何效率上的优势;

 系统V在IPC方面的API数量不多,形式也较简洁。

3.系统V消息队列API

系统V消息队列API共有四个,使用时需要包括几个头文件:

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

)int msgget(key_t key, int msgflg)

参数key是一个键值,由ftok获得;msgflg参数是一些标志位。该调用返回与健值key相对应的消息队列描述字。

在以下两种情况下,该调用将创建一个新的消息队列:

 如果没有消息队列与健值key相对应,并且msgflg中包含了IPC_CREAT标志位;

 key参数为IPC_PRIVATE;

参数msgflg可以为以下:IPC_CREAT、IPC_EXCL、IPC_NOWAIT或三者的或结果。

调用返回:成功返回消息队列描述字,否则返回-1。

注:参数key设置成常数IPC_PRIVATE并不意味着其他进程不能访问该消息队列,只意味着即将创建新的消息队列。

)int msgrcv(int msqid, struct msgbuf *msgp, int msgsz, long msgtyp, int msgflg);

该系统调用从msgid代表的消息队列中读取一个消息,并把消息存储在msgp指向的msgbuf结构中。

msqid为消息队列描述字;消息返回后存储在msgp指向的地址,msgsz指定msgbuf的mtext成员的长度(即消息内容的长度),msgtyp为请求读取的消息类型;读消息标志msgflg可以为以下几个常值的或:

 IPC_NOWAIT 如果没有满足条件的消息,调用立即返回,此时,errno=ENOMSG

 IPC_EXCEPT 与msgtyp>0配合使用,返回队列中第一个类型不为msgtyp的消息

 IPC_NOERROR 如果队列中满足条件的消息内容大于所请求的msgsz字节,则把该消息截断,截断部分将丢失。

msgrcv手册中详细给出了消息类型取不同值时(>0; <0; =0),调用将返回消息队列中的哪个消息。

msgrcv()解除阻塞的条件有三个:

 消息队列中有了满足条件的消息;

 msqid代表的消息队列被删除;

 调用msgrcv()的进程被信号中断;

调用返回:成功返回读出消息的实际字节数,否则返回-1。

)int msgsnd(int msqid, struct msgbuf *msgp, int msgsz, int msgflg);

向msgid代表的消息队列发送一个消息,即将发送的消息存储在msgp指向的msgbuf结构中,消息的大小由msgze指定。

信号是软件中断。信号(signal)机制是Unix系统中最为古老的进程之间的能信机制。它用于在一个或多个进程之间传递异步信号。很多条件可以产生一个信号。

A、当用户按某些终端键时,产生信号。在终端上按DELETE键通常产生中断信号(SIGINT)。这是停止一个已失去控制程序的方法。

、无效的存储访问等等。这些条件通常由硬件检测到,并将其通知内核。然后内核为该条件发生时正在运行的进程产生适当的信号。例如,对于执行一个无效存储访问的进程产生一个SIGSEGV。

C、进程用kill(2)函数可将信号发送给另一个进程或进程组。自然,有些限制:接收信号进和发送信号进程的所有都必须相同,或发送信号进程的的所有者必须是超级用户。

D、用户可用Kill(ID 值)命令将信号发送给其它进程。此程序是Kill函数的界面。常用此命令终止一个失控的后台进程。

除),而是软件条件。例如SIGURG(在网络连接上传来非规定波特率的数据)、SIGPIPE(在管道的读进程已终止后一个进程写此管道),以及SIGALRM(进程所设置的闹钟时间已经超时)。

内核为进程生产信号,来响应不同的事件,这些事件就是信号源。主要信号源如下:

)异常:进程运行过程中出现异常;

)其它进程:一个进程可以向另一个或一组进程发送信号;

)终端中断:Ctrl-c,Ctro-\等;

)作业控制:前台、后台进程的管理;

)分配额:CPU超时或文件大小突破限制;

)通知:通知进程某事件发生,如I/O就绪等;

)报警:计时器到期;

Linux中的信号

、SIGHUP 2、SIGINT(终止) 3、SIGQUIT(退出) 4、SIGILL 5、SIGTRAP 6、SIGIOT  7、SIGBUS   8、SIGFPE   9、SIGKILL 10、SIGUSER 11、 SIGSEGV SIGUSER 12、 SIGPIPE 13、SIGALRM 14、SIGTERM 15、SIGCHLD 16、SIGCONT 17、SIGSTOP 18、SIGTSTP 19、SIGTTIN 20、SIGTTOU 21、SIGURG 22、SIGXCPU 23、SIGXFSZ 24、SIGVTALRM 25、SIGPROF 26、SIGWINCH 27、SIGIO 28、SIGPWR

常用的信号:

SIGHUP:从终端上发出的结束信号;

SIGINT:来自键盘的中断信号(Ctrl+c)

SIGQUIT:来自键盘的退出信号;

SIGFPE:浮点异常信号(例如浮点运算溢出);

SIGKILL:该信号结束接收信号的进程;

SIGALRM:进程的定时器到期时,发送该信号;

SIGTERM:kill命令生出的信号;

SIGCHLD:标识子进程停止或结束的信号;

SIGSTOP:来自键盘(Ctrl-Z)或调试程序的停止扫行信号

可以要求系统在某个信号出现时按照下列三种方式中的一种进行操作。

)忽略此信号。大多数信号都可使用这种方式进行处理,但有两种信号却决不能被忽略。它们是:SIGKILL和SIGSTOP。这两种信号不能被忽略的,原因是:它们向超级用户提供一种使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(例如非法存储访问或除以0),则进程的行为是示定义的。

)捕捉信号。为了做到这一点要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。如果捕捉到SIGCHLD信号,则表示子进程已经终止,所以此信号的捕捉函数可以调用waitpid以取得该子进程的进程ID以及它的终止状态。

)执行系统默认动作。对大多数信号的系统默认动作是终止该进程。每一个信号都有一个缺省动作,它是当进程没有给这个信号指定处理程序时,内核对信号的处理。有5种缺省的动作:

)异常终止(abort):在进程的当前目录下,把进程的地址空间内容、寄存器内容保存到一个叫做core的文件中,而后终止进程。

)退出(exit):不产生core文件,直接终止进程。

)忽略(ignore):忽略该信号。

)停止(stop):挂起该进程。

)继续(contiune):如果进程被挂起,刚恢复进程的动行。否则,忽略信号。

信号的发送与捕捉

kill()和raise()

kill()不仅可以中止进程,也可以向进程发送其他信号。

与kill函数不同的是,raise()函数运行向进程自身发送信号

#include<sys/types.h>

#include<signal.h>

int kill(pid_t pid,int signo);

int raise(int signo);

,若出错则为-1。

kill的pid参数有四种不同的情况:

)pid>0将信号发送给进程ID为pid的进程。

)pid==0将信号发送给其进程组ID等于发送进程的进程组ID,而且发送进程有许可权向其发送信号的所有进程。

)pid<0将信号发送给其进程组ID等于pid绝对值,而且发送进程有许可权向其发送信号的所有进程。如上所述一样,"所有进程"并不包括系统进程集中的进程。

)pid==-1 POSIX.1未定义种情况

kill.c

#include<stdio.h>

#include<stdlib.h>

#include<signal.h>

#include<sys/types.h>

#include<sys/wait.h>

int main()

{

pid_t pid;

int ret;

if((pid==fork())<0){

perro("fork");

exit(1);

}

if(pid==0){

raise(SIGSTOP);

exit(0);

}else {

printf("pid=%d\n",pid);

if((waitpid(pid,NULL,WNOHANG))==0){

if((ret=kill(pid,SIGKILL))==0)

printf("kill %d\n",pid);

else{

perror("kill");

}

}

}

}

alarm和pause函数

使用alarm函数可以设置一个时间值(闹钟时间),在将来的某个时刻时间值会被超过。当所设置的时间被超过后,产生SIGALRM信号。如果不忽略或不捕捉引信号,则其默认动作是终止该进程。

#include<unistd.h>

unsigned int alarm(unsigned int secondss);

或以前设置的闹钟时间的余留秒数。

参数seconds的值是秒数,经过了指定的seconds秒后产生信号SIGALRM。每个进程只能有一个闹钟时间。如果在调用alarm时,以前已为该进程设置过闹钟时间,而且它还没有超时,则该闹钟时间的余留值作为本次alarm函数调用的值返回。以前登记的闹钟时间则被新值代换。

,则取消以前的闹钟时间,其余留值仍作为函数的返回值。

pause函数使用调用进程挂起直至捕捉到一个信号

#include<unistd.h>

int pause(void);

返回:-1,errno设置为EINTR

只有执行了一信号处理程序并从其返回时,pause才返回。

alarm.c

#include<unistd.h>

#include<stdio.h>

#include<stdlib.h>

int main()

{

int ret;

ret=alarm(5);

pause();

printf("I have been waken up.\n",ret);

}

信号的处理

当系统捕捉到某个信号时,可以忽略谁信号或是使用指定的处理函数来处理该信号,或者使用系统默认的方式。信号处理的主要方式有两种,一种是使用简单的signal函数,别一种是使用信号集函数组。

signal()

#include<signal.h>

void (*signal (int signo,void (*func)(int)))(int)

返回:成功则为以前的信号处理配置,若出错则为SIG_ERR

func的值是:(a)常数SIGIGN,或(b)常数SIGDFL,或(c)当接到此信号后要调用的的函数的地址。如果指定SIGIGN,则向内核表示忽略此信号(有两个信号SIGKILL和SIGSTOP不能忽略)。如果指定SIGDFL,则表示接到此信号后的动作是系统默认动作。当指定函数地址时,我们称此为捕捉此信号。我们称此函数为信号处理程序(signal handler)或信号捕捉函数(signal-catching funcgion).signal函数原型太复杂了,如果使用下面的typedef,则可以使其简化。

type void sign(int);

sign *signal(int,handler *);

实例见:mysignal.c

#include<signal.h>

#include<stdio.h>

#include<stdlib.h>

void my_func(int sign_no)

{

if(sign_no==SIGINT)

printf("I have get SIGINT\n");

else if(sign_no==SIGQUIT)

printf("I have get SIGQUIT\n");

}

int main()

{

printf("Waiting for signal SIGINT or SIGQUTI\n");

signal(SIGINT,my_func);

signal(SIGQUIT,my_func);

pasue();

exit(0);

}

信号集函数组

我们需要有一个能表示多个信号——信号集(signal set)的数据类型。将在sigprocmask()这样的函数中使用这种数据类型,以告诉内核不允许发生该信号集中的信号。信号集函数组包含水量几大模块:创建函数集、登记信号集、检测信号集。

图见附件。

创建函数集

#include<signal.h>

int sigemptyset(sigset_t* set);

int sigfillset(sigset_t* set);

int sigaddset(sigset_t* set,int signo );

int sigdelset(sigset_t* set,int signo);

,若出错则为-1

int sigismember(const sigset_t* set,int signo);

,若假则为0;

signemptyset:初始化信号集合为空。

sigfillset:初始化信号集合为所有的信号集合。

sigaddset:将指定信号添加到现存集中。

sigdelset:从信号集中删除指定信号。

sigismember:查询指定信号是否在信号集中。

登记信号集

登记信号处理机主要用于决定进程如何处理信号。首先要判断出当前进程阻塞能不能传递给该信号的信号集。这首先使用sigprocmask函数判断检测或更改信号屏蔽字,然后使用sigaction函数改变进程接受到特定信号之后的行为。

一个进程的信号屏蔽字可以规定当前阻塞而不能递送给该进程的信号集。调用函数sigprocmask可以检测或更改(或两者)进程的信号屏蔽字。

#include<signal.h>

int sigprocmask(int how,const sigset_t* set,sigset_t* oset);

,若出错则为-1

oset是非空指针,进程是当前信号屏蔽字通过oset返回。其次,若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。

用sigprocmask更改当前信号屏蔽字的方法。how参数设定:

SIG_BLOCK该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集。set包含了我们希望阻塞的附加信号。

SIG_NUBLOCK该进程新的信号屏蔽字是其当前信号屏蔽字和set所指向信号集的交集。set包含了我们希望解除阻塞的信号。

SIG_SETMASK该进程新的信号屏蔽是set指向的值。如果set是个空指针,则不改变该进程的信号屏蔽字,how的值也无意义。

sigaction函数的功能是检查或修改(或两者)与指定信号相关联的处理动作。此函数取代了UNIX早期版本使用的signal函数。

#include<signal.h>

int sigaction(int signo,const struct sigaction* act,struct sigaction* oact);

,若出错则为-1

参数signo是要检测或修改具体动作的信号的编号数。若act指针非空,则要修改其动作。如果oact指针为空,则系统返回该信号的原先动作。此函数使用下列结构:

struct sigaction{

void (*sa_handler)(int signo);

sigset_t sa_mask;

int sa_flags;

void (*sa_restore);

};

sa_handler是一个函数指针,指定信号关联函数,可以是自定义处理函数,还可以SIG_DEF或SIG_IGN;

sa_mask是一个信号集,它可以指定在信号处理程序执行过程中哪些信号应当被阻塞。

sa_flags中包含许多标志位,是对信号进行处理的各种选项。具体如下:

SA_NODEFER\SA_NOMASK:当捕捉到此信号时,在执行其信号捕捉函数时,系统不会自动阻塞此信号。

SA_NOCLDSTOP:进程忽略子进程产生的任何SIGSTOP、SIGTSTP、SIGTTIN和SIGTOU信号

SA_RESTART:可让重启的系统调用重新起作用。

SA_ONESHOT\SA_RESETHAND:自定义信号只执行一次,在执行完毕后恢复信号的系统默认动作。

检测信号是信号处理的后续步骤,但不是必须的。sigpending函数运行进程检测"未决"信号(进程不清楚他的存在),并进一步决定对他们做何处理。

sigpending返回对于调用进程被阻塞不能递送和当前未决的信号集。

#include<signal.h>

int sigpending(sigset_t * set);

,若出错则为-1

信号集实例见:sigaction.c

#include<sys/types.h>

#include<unistd.h>

#include<signal.h>

#include<stdio.h>

#include<stdlib.h>

void my_func(int signum){

printf("If you want to quit,please try SIGQUIT\n");

}

int main()

{

sigset_t set,pendset;

struct sigaction action1,action2;

if(sigemptyse(&set)<0)

perror("sigemptyset");

if(sigaddset(&set,SIGQUIT)<0)

perror("sigaddset");

if(sigaddset(&set,SIGINT)<0)

perror("sigaddset");

if(sigprocmask(SIG_BLOCK,&set,NULL)<0)

perror("sigprcmask");

esle{

printf("blocked\n");

sleep(5);

}

if(sigprocmask(SIG_UNBLOCK,&set,NULL)

perror("sigprocmask");

else

printf("unblock\n");

while(1){

if(sigismember(&set,SIGINT)){

sigemptyset(&action1.sa_mask);

action1.sa_handler=my_func;

sigaction(SIGINT,&action1,NULL);

}else if(sigismember(&set,SIGQUIT)){

sigemptyset(&action2.sa_mask);

action2.sa_handler=SIG_DEL;

sigaction(SIGTERM,&action2,NULL);

}

}

}