Linux C编程--进程间通信(IPC)1--进程间通信机制概述

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.信号
信号概述
信号是软件中断。信号(signal)机制是Unix系统中最为古老的进程之间的能信机制。它用于在一个或多个进程之间传递异步信号。很多条件可以产生一个信号。
A、当用户按某些终端键时,产生信号。在终端上按DELETE键通常产生中断信号(SIGINT)。这是停止一个已失去控制程序的方法。
B、硬件异常产生信号:除数为0、无效的存储访问等等。这些条件通常由硬件检测到,并将其通知内核。然后内核为该条件发生时正在运行的进程产生适当的信号。例如,对于执行一个无效存储访问的进程产生一个SIGSEGV。
C、进程用kill(2)函数可将信号发送给另一个进程或进程组。自然,有些限制:接收信号进和发送信号进程的所有都必须相同,或发送信号进程的的所有者必须是超级用户。
D、用户可用Kill(ID 值)命令将信号发送给其它进程。此程序是Kill函数的界面。常用此命令终止一个失控的后台进程。
E、当检测到某种软件条件已经发生,并将其通知有关进程时也产生信号。这里并不是指硬件产生条件(如被0除),而是软件条件。例如SIGURG(在网络连接上传来非规定波特率的数据)、SIGPIPE(在管道的读进程已终止后一个进程写此管道),以及SIGALRM(进程所设置的闹钟时间已经超时)。
内核为进程生产信号,来响应不同的事件,这些事件就是信号源。主要信号源如下:
(1)异常:进程运行过程中出现异常;
(2)其它进程:一个进程可以向另一个或一组进程发送信号;
(3)终端中断:Ctrl-c,Ctro-\等;
(4)作业控制:前台、后台进程的管理;
(5)分配额:CPU超时或文件大小突破限制;
(6)通知:通知进程某事件发生,如I/O就绪等;
(7)报警:计时器到期;
Linux中的信号
1、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)或调试程序的停止扫行信号
可以要求系统在某个信号出现时按照下列三种方式中的一种进行操作。
(1)忽略此信号。大多数信号都可使用这种方式进行处理,但有两种信号却决不能被忽略。它们是:SIGKILL和SIGSTOP。这两种信号不能被忽略的,原因是:它们向超级用户提供一种使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(例如非法存储访问或除以0),则进程的行为是示定义的。
(2)捕捉信号。为了做到这一点要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。如果捕捉到SIGCHLD信号,则表示子进程已经终止,所以此信号的捕捉函数可以调用waitpid以取得该子进程的进程ID以及它的终止状态。
(3)执行系统默认动作。对大多数信号的系统默认动作是终止该进程。每一个信号都有一个缺省动作,它是当进程没有给这个信号指定处理程序时,内核对信号的处理。有5种缺省的动作:
(1)异常终止(abort):在进程的当前目录下,把进程的地址空间内容、寄存器内容保存到一个叫做core的文件中,而后终止进程。
(2)退出(exit):不产生core文件,直接终止进程。
(3)忽略(ignore):忽略该信号。
(4)停止(stop):挂起该进程。
(5)继续(contiune):如果进程被挂起,刚恢复进程的动行。否则,忽略信号。
信号的发送与捕捉
kill()和raise()
kill()不仅可以中止进程,也可以向进程发送其他信号。
与kill函数不同的是,raise()函数运行向进程自身发送信号
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int signo);
int raise(int signo);
两个函数返回:若成功则为0,若出错则为-1。
kill的pid参数有四种不同的情况:
(1)pid>0将信号发送给进程ID为pid的进程。
(2)pid==0将信号发送给其进程组ID等于发送进程的进程组ID,而且发送进程有许可权向其发送信号的所有进程。
(3)pid<0将信号发送给其进程组ID等于pid绝对值,而且发送进程有许可权向其发送信号的所有进程。如上所述一样,“所有进程”并不包括系统进程集中的进程。
(4)pid==-1 POSIX.1未定义种情况

2.管道
普通的Linux shell都允许重定向,而重定向使用的就是管道。例如:
ps | grep vsftpd .管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的道端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。管道提供了简单的流控制机制。进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。管道主要用于不同进程间通信。

管道创建与关闭
创建一个简单的管道,可以使用系统调用pipe()。它接受一个参数,也就是一个包括两个整数的数组。如果系统调用成功,此数组将包括管道使用的两个文件描述符。创建一个管道之后,一般情况下进程将产生一个新的进程。
系统调用:pipe();
原型:int pipe(int fd[2]);
返回值:如果系统调用成功,返回0。如果系统调用失败返回-1:
errno=EMFILE(没有空亲的文件描述符)
      EMFILE(系统文件表已满)
      EFAULT(fd数组无效)
注意:fd[0]用于读取管道,fd[1]用于写入管道。

管道的读写
管道主要用于不同进程间通信。实际上,通常先创建一个管道,再通过fork函数创建一个子进程。
子进程写入和父进程读的命名管道:
管道读写注意事项:
可以通过打开两个管道来创建一个双向的管道。但需要在子理程中正确地设置文件描述符。必须在系统调用fork()中调用pipe(),否则子进程将不会继承文件描述符。当使用半双工管道时,任何关联的进程都必须共享一个相关的祖先进程。因为管道存在于系统内核之中,所以任何不在创建管道的进程的祖先进程之中的进程都将无法寻址它。而在命名管道中却不是这样。

命名管道(FIFO)
基本概念
命名管道和一般的管道基本相同,但也有一些显著的不同:
A、命名管道是在文件系统中作为一个特殊的设备文件而存在的。
B、不同祖先的进程之间可以通过管道共享数据。
C、当共享管道的进程执行完所有的I/O操作以后,命名管道将继续保存在文件系统中以便以后使用。
管道只能由相关进程使用,它们共同的祖先进程创建了管道。但是,通过FIFO,不相关的进程也能交换数据。

3.System V IPC机制

为了提供与其它系统的兼容性,Linux也支持三种system Ⅴ的进程间通信机制:消息、信号量(semaphores)和共享内存,Linux对这些机制的实施大同小异。我们把信号量、消息和共享内存统称System V IPC的对象,每一个对象都具有同样类型的接口,即系统调用。就像每个文件都有一个打开文件号一样,每个对象也都有唯一的识别号,进程可以通过系统调用传递的识别号来存取这些对象,与文件的存取一样,对这些对象的存取也要验证存取权限,System V IPC可以通过系统调用对对象的创建者设置这些对象的存取权限。 

在Linux内核中,System V IPC的所有对象有一个公共的数据结构pc_perm结构,它是IPC对象的权限描述,在linux/ipc.h中定义如下: 

struct ipc_perm 

key_t key; /* 键 */ 
ushort uid; /* 对象拥有者对应进程的有效用户识别号和有效组识别号 */ 
ushort gid; 
ushort cuid; /* 对象创建者对应进程的有效用户识别号和有效组识别号 */ 
ushort cgid; 
ushort mode; /* 存取模式 */ 
ushort seq; /* 序列号 */ 
}; 


在这个结构中,要进一步说明的是键(key)。键和识别号指的是不同的东西。系统支持两种键:公有和私有。如果键是公有的,则系统中所有的进程通过权限检查后,均可以找到System V IPC 对象的识别号。如果键是公有的,则键值为0,说明每个进程都可以用键值0建立一个专供其私用的对象。注意,对System V IPC对象的引用是通过识别号而不是通过键,从后面的系统调用中可了解这一点。 

7.3.1信号量 

信号量及信号量上的操作是E.W.Dijkstra 在1965年提出的一种解决同步、互斥问题的较通用的方法,并在很多操作系统中得以实现, Linux改进并实现了这种机制。 

信号量(semaphore )实际是一个整数,它的值由多个进程进行测试(test)和设置(set)。就每个进程所关心的测试和设置操作而言,这两个操作是不可中断的,或称“原子”操作,即一旦开始直到两个操作全部完成。测试和设置操作的结果是:信号量的当前值和设置值相加,其和或者是正或者为负。根据测试和设置操作的结果,一个进程可能必须睡眠,直到有另一个进程改变信号量的值。 

信号量可用来实现所谓的“临界区”的互斥使用,临界区指同一时刻只能有一个进程执行其中代码的代码段。为了进一步理解信号量的使用,下面我们举例说明。 

假设你有很多相互协作的进程,它们正在读或写一个数据文件中的记录。你可能希望严格协调对这个文件的存取,于是你使用初始值为1的信号量,在这个信号量上实施两个操作,首先测试并且给信号量的值减1,然后测试并给信号量的值加1。当第一个进程存取文件时,它把信号量的值减1,并获得成功,信号量的值现在变为0,这个进程可以继续执行并存取数据文件。但是,如果另外一个进程也希望存取这个文件,那么它也把信号量的值减1,结果是不能存取这个文件,因为信号量的值变为-1。这个进程将被挂起,直到第一个进程完成对数据文件的存取。当第一个进程完成对数据文件的存取,它将增加信号量的值,使它重新变为1,现在,等待的进程被唤醒,它对信号量的减1操作将获得成功。 

上述的进程互斥问题,是针对进程之间要共享一个临界资源而言的,信号量的初值为1。实际上,信号量作为资源计数器,它的初值可以是任何正整数,其初值不一定为0或1。另外,如果一个进程要先获得两个或多个的共享资源后才能执行的话,那么,相应地也需要多个信号量,而多个进程要分别获得多个临界资源后方能运行,这就是信号量集合机制,Linux 讨论的就是信号量集合问题。 

1. 信号量的数据结构 
Linux中信号量是通过内核提供的一系列数据结构实现的,这些数据结构存在于内核空间,对它们的分析是充分理解信号量及利用信号量实现进程间通信的基础,下面先给出信号量的数据结构(存在于include/linux/sem.h中),其它一些数据结构将在相关的系统调用中介绍。 

(1)系统中每个信号量的数据结构(sem) 

1)系统中每个信号量的数据结构(sem) 
struct sem { 
int semval; /* 信号量的当前值 */ 
int sempid; /*在信号量上最后一次操作的进程识别号 * 
};

2)系统中表示信号量集合(set)的数据结构(semid_ds) 
struct semid_ds { 
struct ipc_perm sem_perm; /* IPC权限 */ 
long sem_otime; /* 最后一次对信号量操作(semop)的时间 */ 
long sem_ctime; /* 对这个结构最后一次修改的时间 */ 
struct sem *sem_base; /* 在信号量数组中指向第一个信号量的指针 */ 
struct sem_queue *sem_pending; /* 待处理的挂起操作*/ 
struct sem_queue **sem_pending_last; /* 最后一个挂起操作 */ 
struct sem_undo *undo; /* 在这个数组上的undo 请求 */ 
ushort sem_nsems; /* 在信号量数组上的信号量号 */ 
}; 

3) 系统中每一信号量集合的队列结构(sem_queue) 
struct sem_queue { 
struct sem_queue * next; /* 队列中下一个节点 */ 
struct sem_queue ** prev; /* 队列中前一个节点, *(q->prev) == q */ 
struct wait_queue * sleeper; /* 正在睡眠的进程 */ 
struct sem_undo * undo; /* undo 结构*/ 
int pid; /* 请求进程的进程识别号 */ 
int status; /* 操作的完成状态 */ 
struct semid_ds * sma; /*有操作的信号量集合数组 */ 
struct sembuf * sops; /* 挂起操作的数组 */ 
int nsops; /* 操作的个数 */ 
}; 

 (4)几个主要数据结构之间的关系 

从7.3图可以看出,semid_ds结构的sem_base指向一个信号量数组,允许操作这些信号量集合的进程可以利用系统调用执行操作。注意,信号量信号量集合的区别,从上面可以看出,信号量用“sem” 结构描述,而信量集合用“semid_ds"结构描述,实际上,在后面的讨论中,我们以信号量集合为讨论的主要对象。下面我们给出这几个结构之间的关系,如图7.3所示。 

 Linux对信号量的这种实现机制,是为了与消息和共享内存的实现机制保持一致,但信号量是这三者中最难理解的,因此我们将结合系统调用做进一步的介绍,通过对系统调用的深入分析,我们可以较清楚地了解内核对信号量的实现机制。 

2. 系统调用:semget() 

为了创建一个新的信号量集合,或者存取一个已存在的集合,要使用segget()系统调用,其描述如下: 

原型: int semget ( key_t key, int nsems, int semflg ); 
返回值: 如果成功,则返回信号量集合的IPC识别号 
如果为-1,则出现错误: 
semget()中的第一个参数是键值,这个键值要与已有的键值进行比较,已有的键值指在内核中已存在的其它信号量集合的键值。对信号量集合的打开或存取操作依赖于semflg参数的取值:IPC_CREAT :如果内核中没有新创建的信号量集合,则创建它。 
IPC_EXCL :当与IPC_CREAT一起使用时,但信号量集合已经存在,则创建失败。如果IPC_CREAT单独使用,semget()为一个新建的集合返回标识号,或者返回具有相同键值的已存在集合的标识号。如果IPC_EXCL与IPC_CREAT一起使用,要么创建一个新的集合,要么对已存在的集合返回-1。IPC_EXCL单独是没有用的,当与IPC_CREAT结合起来使用时,可以保证新创建集合的打开和存取。作为System V IPC的其它形式,一种可选项是把一个八进制与掩码或,形成信号量集合的存取权限。第三个参数nsems指的是在新创建的集合中信号量的个数。其最大值在“linux/sem.h”中定义: 

#define SEMMSL 250 /* <= 8 000 max num of semaphores per id */



注意:如果你是显式地打开一个现有的集合,则nsems参数可以忽略。 

下面举例说明。 

int open_semaphore_set( key_t keyval, int numsems ) 

int sid; 
if ( ! numsems ) 
return(-1); 
if((sid = semget( keyval, numsems, IPC_CREAT | 0660 )) == -1) 

return(-1); 

return(sid); 
}


注意,这个例子显式地用了0660权限。这个函数要么返回一个集合的标识号,要么返回-1而出错。键值必须传递给它,信号量的个数也传递给它,这是因为如果创建成功则要分配空间。 

3. 系统调用: semop() 

原型: int semop ( int semid, struct sembuf *sops, unsignednsops); 
返回: 如果所有的操作都执行,则成功返回0。 
如果为-1,则出错。 
semop()中的第一个参数(semid)是集合的识别号(可以由semget()系统调用得到)。第二个参数(sops)是一个指针,它指向在集合上执行操作的数组。而第三个参数(nsop)是在那个数组上操作的个数。sops参数指向类型为sembuf的一个数组,这个结构在/inclide/linux/sem.h 中声明,是内核中的一个数据结构,描述如下: 

struct sembuf { 
ushort sem_num; /* 在数组中信号量的索引值 */ 
short sem_op; /* 信号量操作值(正数、负数或0) */ 
short sem_flg; /* 操作标志,为IPC_NOWAIT或SEM_UNDO*/ 
};


如果sem_op为负数,那么就从信号量的值中减去sem_op的绝对值,这意味着进程要获取资源,这些资源是由信号量控制或监控来存取的。如果没有指定IPC_NOWAIT,那么调用进程睡眠到请求的资源数得到满足(其它的进程可能释放一些资源)。 

如果sem_op是正数,把它的值加到信号量,这意味着把资源归还给应用程序的集合。 

最后,如果sem_op为0,那么调用进程将睡眠到信号量的值也为0,这相当于一个信号量到达了100%的利用。 

综上所述,Linux 按如下的规则判断是否所有的操作都可以成功:操作值和信号量的当前值相加大于 0,或操作值和当前值均为 0,则操作成功。如果系统调用中指定的所有操作中有一个操作不能成功时,则 Linux 会挂起这一进程。但是,如果操作标志指定这种情况下不能挂起进程的话,系统调用返回并指明信号量上的操作没有成功,而进程可以继续执行。如果进程被挂起,Linux 必须保存信号量的操作状态并将当前进程放入等待队列。为此,Linux 内核在堆栈中建立一个 sem_queue 结构并填充该结构。新的 sem_queue 结构添加到集合的等待队列中(利用 sem_pending 和 sem_pending_last 指针)。当前进程放入 sem_queue 结构的等待队列中(sleeper)后调用调度程序选择其他的进程运行。 

为了进一步解释semop()调用,让我们来看一个例子。假设我们有一台打印机,一次只能打印一个作业。我们创建一个只有一个信号量的集合(仅一个打印机),并且给信号量的初值为1(因为一次只能有一个作业)。 

每当我们希望把一个作业发送给打印机时,首先要确定这个资源是可用的,可以通过从信号量中获得一个单位而达到此目的。让我们装载一个sembuf数组来执行这个操作: 

struct sembuf sem_lock = { 0, -1, IPC_NOWAIT };


从这个初始化结构可以看出,0表示集合中信号量数组的索引,即在集合中只有一个信号量,-1表示信号量操作(sem_op),操作标志为IPC_NOWAIT,表示或者调用进程不用等待可立即执行,或者失败(另一个进程正在打印)。下面是用初始化的sembuf结构进行semop()系统调用的例子: 

if((semop(sid, &sem_lock, 1) == -1) 
fprintf(stderr,"semop\n");


第三个参数(nsops)是说我们仅仅执行了一个操作(在我们的操作数组中只有一个sembuf结构),sid参数是我们集合的IPC识别号。 
当我们使用完打印机,我们必须把资源返回给集合,以便其它的进程使用。 

struct sembuf sem_unlock = { 0, 1, IPC_NOWAIT };


上面这个初始化结构表示,把1加到集合数组的第0个元素,换句话说,一个单位资源返回给集合。 

4. 系统调用 : semctl() 

原型: int semctl ( int semid, int semnum, int cmd, unionsemun arg ); 
返回值: 成功返回正数,出错返回-1。 

注意:semctl()是在集合上执行控制操作。 

semctl()的第一个参数(semid)是集合的标识号,第二个参数(semnn)是将要操作的信号量个数,从本质上说,它是集合的一个索引,对于集合上的第一个信号量,则该值为0。 

·cmd参数表示在集合上执行的命令,这些命令及解释如表7.2所示: 
·arg参数的类型为semun,这个特殊的联合体在include/linux/sem.h中声明,对它的描述如下: 

/* arg for semctl system calls. */ 
union semun { 
int val; /* value for SETVAL */ 
struct semid_ds *buf; /* buffer for IPC_STAT & IPC_SET */ 
ushort *array; /* array for GETALL & SETALL */ 
struct seminfo *__buf; /* buffer for IPC_INFO */ 
void *__pad; 
};

 

 这个联合体中,有三个成员已经在表7-1中提到,剩下的两个成员_buf 和_pad用在内核中信号量的实现代码,开发者很少用到。事实上,这两个成员是Linux操作系统所特有的,在UINX中没有。这个系统调用比较复杂,我们举例说明。 

下面这个程序段返回集合上索引为semnum对应信号量的值。当用GETVAL命令时,最后的参数(semnum)被忽略。 

int get_sem_val( int sid, int semnum ) 

return( semctl(sid, semnum, GETVAL, 0)); 
}


关于信号量的三个系统调用,我们进行了详细的介绍。从中可以看出,这几个系统调用的实现和使用都和系统内核密切相关,因此,如果在了解内核的基础上,再理解系统调用,相对要简单地多,也深入地多。 

5. 死锁 

和信号量操作相关的概念还有“死锁”。当某个进程修改了信号量而进入临界区之后,却因为崩溃或被“杀死(kill)"而没有退出临界区,这时,其他被挂起在信号量上的进程永远得不到运行机会,这就是所谓的死锁。Linux 通过维护一个信号量数组的调整列表(semadj)来避免这一问题。其基本思想是,当应用这些“调整”时,让信号量的状态退回到操作实施前的状态。 

关于调整的描述是在sem_undo数据结构中,在include/linux/sem.h描述如下: 

/*每一个任务都有一系列的恢复(undo)请求,当进程退出时,自动执行undo请求*/ 
struct sem_undo { 
struct sem_undo * proc_next; /*在这个进程上的下一个sem_undo节点 */ 
struct sem_undo * id_next; /* 在这个信号量集和上的下一个sem_undo节点*/ 
int semid; /* 信号量集的标识号*/ 
short * semadj; /* 信号量数组的调整,每个进程一个*/ 
};


sem_undo结构也出现在task_struct数据结构中。 

每一个单独的信号量操作也许要请求得到一次“调整”,Linux将为每一个信号量数组的每一个进程维护至少一个sem_undo结构。如果请求的进程没有这个结构,当必要时则创建它,新创建的sem_undo数据结构既在这个进程的task_struct数据结构中排队,也在信号量数组的semid_ds结构中排队。当对信号量数组上的一个信号量施加操作时,这个操作值的负数与这个信号量的“调整”相加,因此,如果操作值为2,则把-2加到这个信号量的“调整”域。 

当进程被删除时,Linux完成了对sem_undo数据结构的设置及对信号量数组的调整。如果一个信号量集合被删除,sem_undo结构依然留在这个进程的task_struct结构中,但信号量集合的识别号变为无效。 

7.3.2 消息队列 

一个或多个进程可向消息队列写入消息,而一个或多个进程可从消息队列中读取消息,这种进程间通讯机制通常使用在客户/服务器模型中,客户向服务器发送请求消息,服务器读取消息并执行相应请求。在许多微内核结构的操作系统中,内核和各组件之间的基本通讯方式就是消息队列。例如,在 MINIX 操作系统中,内核、I/O 任务、服务器进程和用户进程之间就是通过消息队列实现通讯的。 

Linux中的消息可以被描述成在内核地址空间的一个内部链表,每一个消息队列由一个IPC的标识号唯一的标识。Linux 为系统中所有的消息队列维护一个 msgque 链表,该链表中的每个指针指向一个 msgid_ds 结构,该结构完整描述一个消息队列。 

1. 数据结构 

(1)消息缓冲区(msgbuf)
 
我们在这里要介绍的第一个数据结构是msgbuf结构,可以把这个特殊的数据结构看成一个存放消息数据的模板,它在include/linux/msg.h中声明,描述如下: 

/* msgsnd 和msgrcv 系统调用使用的消息缓冲区*/ 
struct msgbuf { 
long mtype; /* 消息的类型,必须为正数 */ 
char mtext[1]; /* 消息正文 */ 
};


注意:对于消息数据元素(mtext),不要受其描述的限制。实际上,这个域(mtext)不仅能保存字符数组,而能保存任何形式的任何数据。这个域本身是任意的,因为这个结构本身可以由应用程序员重新定义: 

struct my_msgbuf { 
long mtype; /* 消息类型 */ 
long request_id; /* 请求识别号 */ 
struct client info; /* 客户消息结构 */ 
};


我们看到,消息的类型还是和前面一样,但是结构的剩余部分由两个其它的元素代替,而且有一个是结构。这就是消息队列的优美之处,内核根本不管传送的是什么样的数据,任何信息都可以传送。 

但是,消息的长度还是有限制的,在Linux中,给定消息的最大长度在include/linux/msg.h中定义如下: 

#define MSGMAX 8192 /* max size of message (bytes) */


消息总的长度不能超过8192字节,包括mtype域,它是4字节长。 

(2)消息结构(msg) 

内核把每一条消息存储在以msg结构为框架的队列中,它在include/linux/msg.h中定义如下: 

struct msg { 
struct msg *msg_next; /* 队列上的下一条消息 */ 
long msg_type; /*消息类型*/ 
char *msg_spot; /* 消息正文的地址 */ 
short msg_ts; /* 消息正文的大小 */ 
};


注意:msg_next是指向下一条消息的指针,它们在内核地址空间形成一个单链表。 

(3)消息队列结构(msgid_ds) 

当在系统中创建每一个消息队列时,内核创建、存储及维护这个结构的一个实例。 

/* 在系统中的每一个消息队列对应一个msqid_ds 结构 */ 
struct msqid_ds { 
struct ipc_perm msg_perm; 
struct msg *msg_first; /* 队列上第一条消息,即链表头*/ 
struct msg *msg_last; /* 队列中的最后一条消息,即链表尾 */ 
time_t msg_stime; /* 发送给队列的最后一条消息的时间 */ 
time_t msg_rtime; /* 从消息队列接收到的最后一条消息的时间 */ 
time_t msg_ctime; /* 最后修改队列的时间*/ 
ushort msg_cbytes; /*队列上所有消息总的字节数 */ 
ushort msg_qnum; /*在当前队列上消息的个数 */ 
ushort msg_qbytes; /* 队列最大的字节数 */ 
ushort msg_lspid; /* 发送最后一条消息的进程的pid */ 
ushort msg_lrpid; /* 接收最后一条消息的进程的pid */ 
};


2. 系统调用: msgget() 

为了创建一个新的消息队列,或存取一个已经存在的队列,要使用msgget()系统调用。 

原型: int msgget ( key_t key, int msgflg ); 
返回: 成功,则返回消息队列识别号,失败,则返回-1, 

semget()中的第一个参数是键,这个键值要与现有的键值进行比较,现有的键值指在内核中已存在的其它消 
息队列的键值。对消息队列的打开或存取操作依赖于msgflg参数的取值: 

IPC_CREAT :如果这个队列在内核中不存在,则创建它。 
IPC_EXCL :当与IPC_CREAT一起使用时,如果这个队列已存在,则创建失败。 

如果IPC_CREAT单独使用,semget()为一个新创建的消息队列返回标识号,或者返回具有相同键值的已存在队列的标识号。如果IPC_EXCL与IPC_CREAT一起使用,要么创建一个新的队列,要么对已存在的队列返回-1。 
IPC_EXCL单独是没有用的,当与IPC_CREAT结合起来使用时,可以保证新创建队列的打开和存取。 

与文件系统的存取权限一样,每一个IPC对象也具有存取权限,因此,可以把一个八进制与掩码或,形成对消息队列的存取权限。 

让我们来创建一个打开或创建消息队列的函数: 

int open_queue( key_t keyval ) 

int qid; 
if((qid = msgget( keyval, IPC_CREAT | 0660 )) == -1) 

return(-1); 

return(qid); 
}


注意,这个例子显式地用了0660权限。这个函数要么返回一个消息队列的标识号,要么返回-1而出错。键值作为唯一的参数必须传递给它。 

3. 系统调用: msgsnd() 

一旦我们有了队列识别号,我们就可以在这个队列上执行操作。要把一条消息传递给一个队列,你必须用msgsnd()系统调用。 

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

返回:成功为0,失败为-1。 

msgsnd()的第一个参数是队列识别号,由msgget()调用返回。第二个参数msgp是一个指针,指向我们重新声和装载的消息缓冲区。msgsz参数包含了消息以字节为单位的长度,其中包括了消息类型的4个字节。 

msgflg参数可以设置成0(忽略),或者: 

IPC_NOWAIT :如果消息队列满,消息不写到队列中,并且控制权返回给调用进程(继续执行)。如果不指定IPC_NOWAIT,调用进程将挂起(阻塞)直到消息被写到队列中。 

让我们来看一个发送消息的简单函数: 

int send_message( int qid, struct mymsgbuf *qbuf ) 

int result, length; 
/* mymsgbuf结构的实际长度 */ 
length = sizeof(struct ) - sizeof(long); 
if((result = msgsnd( qid, qbuf, length, 0)) == -1) 

return(-1); 

return(result); 
}


这个小函数试图把缓冲区qbuf中的消息,发送给队列识别号为qid的消息队列。 

现在,我们在消息队列里有了一条消息,可以用ipcs命令来看你队列的状态。如何从消息队列检索消息,可以用msgrcv()系统调用。 

4.系统调用:msgrcv() 

原型:int msgrcv ( int msqid, struct msgbuf *msgp, int msgsz,long mtype, int msgflg ); 

返回值:成功,则为拷贝到消息缓冲区的字节数,失败为-1。 

很明显,第一个参数用来指定要检索的队列(必须由msgget()调用返回),第二个参数(msgp)是存放检索到消息的缓冲区的地址,第三个参数(msgsz)是消息缓冲区的大小,包括消息类型的长度(4字节)。 

第四个参数(mtype)指定了消息的类型。内核将搜索队列中相匹配类型的最早的消息,并且返回这个消息的一个拷贝,返回的消息放在由msgp参数指向的地址。这里存在一个特殊的情况,如果传递给mytype参数的值为0,就可以不管类型,只返回队列中最早的消息。 

如果传递给参数msgflg的值为IPC_NOWAIT,并且没有可取的消息,那么给调用进程返回ENOMSG错误消息,否则,调用进程阻塞,直到一条消息到达队列并且满足msgrcv()的参数。如果一个客户正在等待消息,而队列被删除,则返回EIDRM。如果当进程正在阻塞,并且等待一条消息到达但捕获到了一个信号,则返回EINTR。 

让我们来看一个从我们已建的消息队列中检索消息的例子 

int read_message( int qid, long type, struct mymsgbuf *qbuf ) 

int result, length; 
* 计算mymsgbuf结构的实际大小*/ 
length = sizeof(struct mymsgbuf) - sizeof(long); 
if((result = msgrcv( qid, qbuf, length, type, 0)) == -1) 

return(-1); 

return(result); 
}


当从队列中成功地检索到消息后,这个消息将从队列删除。 

7.3.3 共享内存

共享内存可以被描述成内存一个区域(段)的映射,这个区域可以被更多的进程所共享。这是IPC机制中最快的一种形式,因为它不需要中间环节,而是把信息直接从一个内存段映射到调用进程的地址空间。一个段可以直接由一个进程创建,随后,可以有任意多的进程对其读和写。但是,一旦内存被共享之后,对共享内存的访问同步需要由其他 IPC 机制,例如信号量来实现。象所有的System V IPC 对象一样,Linux 对共享内存的存取是通过对访问键和访问权限的检查来控制的。 

1. 数据结构 
与消息队列和信号量集合类似,内核为每一个共享内存段(存在于它的地址空间)维护着一个特殊的数据结构shmid_ds,这个结构在include/linux/shm.h中定义如下: 

/* 在系统中 每一个共享内存段都有一个shmid_ds数据结构. */ 
struct shmid_ds { 
struct ipc_perm shm_perm; /* 操作权限 */ 
int shm_segsz; /* 段的大小(以字节为单位) */ 
time_t shm_atime; /* 最后一个进程附加到该段的时间 */ 
time_t shm_dtime; /* 最后一个进程离开该段的时间 */ 
time_t shm_ctime; /* 最后一次修改这个结构的时间 */ 
unsigned short shm_cpid; /*创建该段进程的 pid */ 
unsigned short shm_lpid; /* 在该段上操作的最后一个进程的pid */ 
short shm_nattch; /*当前附加到该段的进程的个数 */ 
/* 下面是私有的 */ 
unsigned short shm_npages; /*段的大小(以页为单位) */ 
unsigned long *shm_pages; /* 指向frames -> SHMMAX的指针数组 */ 
struct vm_area_struct *attaches; /* 对共享段的描述 */ 
};

 

 
2. 共享内存的处理过程 

某个进程第一次访问共享虚拟内存时将产生缺页异常。这时,Linux 找出描述该内存的 vm_area_struct 结构,该结构中包含用来处理这种共享虚拟内存段的处理函数地址。共享内存缺页异常处理代码对shmid_ds 的页表项表进行搜索,以便查看是否存在该共享虚拟内存的页表项。如果没有,系统将分配一个物理页并建立页表项,该页表项加入 shmid_ds 结构的同时也添加到进程的页表中。这就意味着当下一个进程试图访问这页内存时出现缺页异常,共享内存的缺页异常处理代码则把新创建的物理页给这个进程。因此说,第一个进程对共享内存的存取引起创建新的物理页面,而其它进程对共享内存的存取引起把那个页加添加到它们的地址空间。 

当某个进程不再共享其虚拟内存时,利用系统调用将共享段从自己的虚拟地址区域中移去,并更新进程页表。当最后一个进程释放了共享段之后,系统将释放给共享段所分配的物理页。 

当共享的虚拟内存没有被锁定到物理内存时,共享内存也可能会被交换到交换区中。 

3. 系统调用:shmget() 

原型:int shmget ( key_t key, int size, int shmflg ); 

返回:成功,则返回共享内存段的识别号, 失败返回-1 

shmget()系统调用类似于信号量和消息队列的系统调用,在此不进一步赘述。 

4. 系统调用:shmat() 

原型: int shmat ( int shmid, char *shmaddr, int shmflg); 

返回:成功,则返回附加到进程的那个段的地址,失败返回-1。 

其中shmid是由shmget()调用返回的共享内存段识别号,shmaddr是你希望共享段附加的地址,shmflag允许你规定希望所附加的段为只读(利用SHM_RDONLY)以代替读写。通常,并不需要规定你自己的shmaddr,可以用传递参数值零使得系统为你取得一个地址。 

这个调用可能是最简单的,下面看一个例子,把一个有效的识别号传递给一个段,然后返回这个段被附加到内存的内存地址。

char *attach_segment( int shmid ) 

return(shmat(shmid, 0, 0)); 
}

 一旦一个段适当地被附加,并且一个进程有指向那个段起始地址的一个指针,那么,对那个段的读写就变得相当容易。 

5. 系统调用: shmctl() 

原型: int shmctl ( int shmqid, int cmd, struct shmid_ds *buf); 

返回:成功为 0 ,失败为-1 

这个特殊的调用和semctl()调用几乎相同,因此,这里不进行详细的讨论。有效命令的值是: 

IPC_STAT :检索一个共享段的shmid_ds结构,把它存到buf参数的地址中。 

IPC_SET :对一个共享段来说,从buf 参数中取值设置shmid_ds结构的ipc_perm域的值。 

IPC_RMID :把一个段标记为删除 

IPC_RMID 命令实际上不从内核删除一个段,而是仅仅把这个段标记为删除,实际的删除发生在最后一个进程离开这个共享段时。 
当一个进程不再需要共享内存段时,它将调用shmdt()系统调用取消这个段,但是,这并不是从内核真正地删除这个段,而是把相关shmid_ds结构的 shm_nattch域的值减1,当这个值为0时,内核才从物理上删除这个共享段。 


其中第三部分,IPC机制转载自http://www.eefocus.com/article/09-06/75167s.html

你可能感兴趣的:(linux,C编程)