一、进程间通信
在多进程创建中,fork之后,操作系统会复制一个与父进程完全相同的子进程,这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同。这两个进程一直同时运行,而且步调一致,在fork之后,他们分别作不同的工作,也就是分岔了。这也是fork为什么叫fork的原因。至于哪一个最先运行,这个与操作系统进程调度算法有关,而且这个问题在实际应用中并不重要,如果需要父子进程协同,可以通过原语的办法解决。
那么如果多个进程之间需要协同处理某个任务时,这时就需要进程间的同步和数据交流。常用的进程间通信(IPC,InterProcess Communication)的方法有:
1. 信号 ( Sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
2. 管道(Pipe): 管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系(通常是指父子进程关系)的进程间使用;
3. 命名管道FIFO: 命名管道(Named Pipe)也是半双工的通信方式,但是它允许无亲缘关系进程间的通信; ( 注:单工:单向车道 半双工:是双向通信,但是同一时刻只能是单向的 全双工:双向通信)
4. 命名socket或UNIX域socket(Named Socket或Unix Domain Socket):socket也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同进程间的进程通信;
5. 信号量(Semaphore): 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段;
6. 共享存储(Shared Memory): 共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信;
7. 消息队列(Message Queue): 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
二、信号
- 信号是Linux系统中用于进程之间通信或操作的一种机制,信号可以在任何时候发送给某一进程,而无须知道该进程的状态。如果该进程并未处于执行状态,则该信号就由内核保存起来,直到该进程恢复执行并传递给他为止。如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。
- Linux提供了几十种信号,分别代表着不同的意义。信号之间依靠他们的值来区分,但是通常在程序中使用信号的名字来表示一个信号。信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式,信号可以在用户空间进程和内核之间直接交互。内核也可以利用信号来通知用户空间的进程来通知用户空间发生了哪些系统事件。
- 下面是一个父子进程之间使用信号进行同步的例程。
在下面的这个程序中,如果父进程先执行则进入到循环休眠等待状态,直到子进程给他发送信号之后才能跳出循环继续运行,这就可以确保子进程先执行它的任务。同样子进程在执行完成任务之后,就等待父进程给他发送信号之后才能退出,而父进程则通过调用wait()系统调用等待子进程退出后,父进程再退出。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
int g_child_stop = 0;
int g_parent_run = 0;
void sig_child(int signum)
{
if( SIGUSR1 == signum )
{
g_child_stop = 1;
}
}
void sig_parent(int signum)
{
if( SIGUSR2 == signum )
{
g_parent_run = 1;
}
}
int main(int argc, char **argv)
{
int pid;
int wstatus;
signal(SIGUSR1, sig_child);
signal(SIGUSR2, sig_parent);
if( (pid=fork()) < 0 )
{
printf("Create child process failure: %s\n", strerror(errno));
return -2;
}
else if(pid == 0)
{
//子进程先运行的话,会通过kill给父进程发一个SIGUSR2的信号
printf("Child process start running and send parent a signal\n");
kill(getppid(), SIGUSR2);
while( !g_child_stop )
{
sleep(1);
}
printf("Child process receive signal from parent and exit now\n");
return 0;
}
//如果父进程先运行,会等子进程先运行
printf("Parent hangs up untill receive signal from child!\n");
while( !g_parent_run )
{
sleep(1);
}
//父进程完成了之后给子进程发出一个退出的信号
printf("Parent start running now and send child a signal to exit\n");
kill(pid, SIGUSR1);
//父进程等待子进程退出
wait(&wstatus);
printf("Parent wait child process die and exit now\n");
return 0;
}
三、管道
- 管道的实质是一个内核缓冲区,进程以先进先出(FIFO, First In First Out)的方式从缓冲区存取数据。
- 两个局限性:
(1)半双工,数据只能在一个方向流动,现在有些系统可以支持全双工管道,但是为了最佳的可移植性,应认为系统不支持全双工管道;
(2)管道只能在具有公共祖先之间的两个进程之间使用; - 管道的创建:
管道可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
管道是通过调用pipe函数创建的。
#include <unistd.h>
int pipe(int fd[2]); //返回值:若成功,返回0,若出错,返回-1.
经由参数fd返回的两个文件描述符:fd[0]为读而打开,fd[1]为写而打开,fd[1]的输出是fd[0]的输入。
通常,进程会先调用pipe,接着调用fork,从而创建了父进程与子进程的IPC通道。
fork之后做什么取决于我们想要的数据流的方向,对于从父进程到子进程,父进程关闭管道的读端fd[0],子进程关闭写端fd[1]。
4. 例程:用于父进程给子进程方向发送数据。首先父进程创建管道之后fork(),这时子进程会继承父进程所有打开的文件描述符(包括管道),这时对于一个管道就有4个读写端(父子进程各有一对管道读写端),如果需要父进程往子进程里写数据,则需要在父进程中关闭读端,在子进程中关闭写端;而如果需要子进程往父进程中写数据,则可以在父进程关闭写端,然后子进程中关闭读端。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#define MSG_STR "This infomation is from parent: Hello, child process!"
int main(int argc, char **argv)
{
int pipe_fd[2];//定义一个数组,有两个元素,为读写两端,0是读,1是写
int rv;
int pid;
char buf[512];
int wstatus;
if( pipe(pipe_fd) < 0)//出错了
{
printf("Create pipe failure: %s\n", strerror(errno));
return -1;
}
if( (pid=fork()) < 0 )//创建子进程失败
{
printf("Create child process failure: %s\n", strerror(errno));
return -2;
}
else if(pid == 0)
{
close(pipe_fd[1]);//子进程把写端关闭了
memset(buf, 0, sizeof(buf));
rv=read(pipe_fd[0], buf, sizeof(buf));
if(rv < 0 )
{
printf("Child process read from pipe failure: %s\n", strerror(errno));
return -3;
}
printf("Child process read %d bytes data from pipe: \"%s\"\n", rv, buf);
return 0;
}
close(pipe_fd[0]);//父进程关闭读端,
if( write(pipe_fd[1], MSG_STR, strlen(MSG_STR)) < 0)
{
printf("Parent process write data to pipe failure: %s\n", strerror(errno));
return -3;
}
printf("Parent start wait child process exit...\n");
wait(&wstatus);
return 0;
}
四、命名管道
- 未命名的管道只能在两个具有亲缘关系的进程之间通信,但是通过命名管道(Named PiPe)FIFO,不相关的进程也能交换数据。
- 下面这个例程创建了两个掩藏的命名管道文件(.fifo_chat1和.fifo_chat2)在不同的进程间进行双向通信。该程序需要运行两次(即两个进程),其中进程0(mode=0)从标准输入里读入数据后通过命名管道2(.fifo_chat2)写入数据给进程1(mode=1);
而进程1(mode=1)则从标准输入里读入数据后通过命名管道1(.fifo_chat1)写给进程0,这样使用命名管道实现了一个进程间聊天的程序:
1 #include <stdio.h>
2 #include <string.h>
3 #include <unistd.h>
4 #include <errno.h>
5 #include <fcntl.h>
6 #include <sys/types.h>
7 #include <sys/wait.h>
8 #include <sys/stat.h>
9 #include <libgen.h>
10 #include <stdlib.h>
11
12 #define FIFO_FILE1 ".fifo_chat1"
13 #define FIFO_FILE2 ".fifo_chat2"
14
15 int g_stop = 0;
16
17 void sig_pipe(int signum)
18 {
19 if(SIGPIPE == signum)
20 {
21 printf("get pipe broken signal and let programe exit\n");
22 g_stop = 1;
23 }
24 }
25
26 int main(int argc, char **argv)
27 {
28 int fdr_fifo;
29 int fdw_fifo;
30 int rv;
31 fd_set rdset;
32 char buf[1024];
33 int mode = 0;
34 if( argc != 2 )
35 {
36 printf("Usage: %s [0/1]\n", basename(argv[0]));
37 printf("This chat program need run twice, 1st time run with [0] and 2nd time with [1]\n");
38 return -1;
39 }
40
41 mode = atoi(argv[1]);
42 /* 管道是一种半双工的通信方式,如果要实现两个进程间的双向通信则需要两个管道,即两个管道分别作为两个进程的读端和写端 */
43 if( access(FIFO_FILE1 , F_OK) )
44 {
45 printf("FIFO file \"%s\" not exist and create it now\n", FIFO_FILE1);
46 mkfifo(FIFO_FILE1, 0666);
47 }
48 if( access(FIFO_FILE2 , F_OK) )
49 {
50 printf("FIFO file \"%s\" not exist and create it now\n", FIFO_FILE2);
51 mkfifo(FIFO_FILE2, 0666);
52 }
53
54 signal(SIGPIPE, sig_pipe);
55 if( 0 == mode )
56 {
57 /* 这里以只读模式打开命名管道FIFO_FILE1的读端,默认是阻塞模式;如果命名管道的写端被不打开则open()将会一直阻塞,所以另外一个进程必须首先以写模式打开该文件FIFO_FILE1,否则会出现死锁 */
58 printf("start open '%s' for read and it will blocked untill write endpoint opened...\n",
59 FIFO_FILE1);
60 if( (fdr_fifo=open(FIFO_FILE1, O_RDONLY)) < 0 )
61 {
62 printf("Open fifo[%s] for chat read endpoint failure: %s\n", FIFO_FILE1, strerror(errno));
63 return -1;
64 }
65 printf("start open '%s' for write...\n", FIFO_FILE2);
66 if( (fdw_fifo=open(FIFO_FILE2, O_WRONLY)) < 0 )
67 {
68 printf("Open fifo[%s] for chat write endpoint failure: %s\n", FIFO_FILE2, strerror(errno));
69 return -1;
70 }
71 }
72 else
73 {
74 /* 这里以只写模式打开命名管道FIFO_FILE1的写端,默认是阻塞模式;如果命名管道的读端被不打开则open()将会一直阻塞,因为前一个进程是先以读模式打开该管道文件的读端,所以这里必须先以写模式打开该文件的写端,否则会出现死锁 */
75 printf("start open '%s' for write and it will blocked untill read endpoint opened...\n",
76 FIFO_FILE1);
77 if( (fdw_fifo=open(FIFO_FILE1, O_WRONLY)) < 0 )
78 {
79 printf("Open fifo[%s] for chat write endpoint failure: %s\n", FIFO_FILE1, strerror(errno));
80 return -1;
81 }
82 printf("start open '%s' for read...\n", FIFO_FILE2);
83 if( (fdr_fifo=open(FIFO_FILE2, O_RDONLY)) < 0 )
84 {
85 printf("Open fifo[%s] for chat read endpoint failure: %s\n", FIFO_FILE2, strerror(errno));
86 return -1;
87 }
88 }
89 printf("start chating with another program now, please input message now: \n");
90 while( !g_stop )
91 {
92 FD_ZERO(&rdset);
93 FD_SET(STDIN_FILENO, &rdset);
94 FD_SET(fdr_fifo, &rdset);
95 /* select多路复用监听标准输入和作为输入的命名管道读端 */
96 rv = select(fdr_fifo+1, &rdset, NULL, NULL, NULL);
97 if( rv <= 0 )
98 {
99 printf("Select get timeout or error: %s\n", strerror(errno));
100 continue;
101 }
102 /* 如果是作为输入的命名管道上有数据到来则从管道上读入数据并打印到标注输出上 */
103 if( FD_ISSET(fdr_fifo, &rdset) )
104 {
105 memset(buf, 0, sizeof(buf));
106 rv=read(fdr_fifo, buf, sizeof(buf));
107 if( rv < 0)
108 {
109 printf("read data from FIFO get errorr: %s\n", strerror(errno));
110 break;
111 }
112 else if( 0==rv ) /* 如果从管道上读到字节数为0,说明管道的写端已关闭 */
113 {
114 printf("Another side of FIFO get closed and program will exit now\n");
115 break;
116 }
117 printf("<-- %s", buf);
118 }
119 /* 如果标准输入上有数据到来,则从标准输入上读入数据后,将数据写入到作为输出的命名管道上给另外一个进程 */
120 if( FD_ISSET(STDIN_FILENO, &rdset) )
121 {
122 memset(buf, 0, sizeof(buf));
123 fgets(buf, sizeof(buf), stdin);
124 write(fdw_fifo, buf, strlen(buf));
125 }
五、命名socket
- 使用socket除了可以实现网络间不同主机间的通信外,还可以实现同一主机的不同进程间的通信,且建立的通信是双向的通信。
- 下图显示了Unix域socket与TCP/IP网络socket之间的区别:
- 命名socket函数:
int socket(int domain, int type, int protocol);
说明:创建一个socket,可以是TCP/IP网络socket,也是命名socket,具体由domain参数决定。该函数的返回值为生成的套接字描述
符。
参数domain指定协议族:对于命名socket/Unix域socket,其值须被置为 AF_UNIX或AF_LOCAL;如果是网络socket则其值应该
为 AF_INET;
参数type指定套接字类型,它可以被设置为 SOCK_STREAM(流式套接字)或 SOCK_DGRAM(数据报式套接字);
参数protocol应被设置为 0;
SOCK_STREAM 式本地套接字的通信双方均需要具有本地地址,其中服务器端的本地地址需要明确指定,指定方法是使用struct sockaddr_un 类型的变量。
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[UNIX_PATH_MAX]; /* 路径名 */
};
- 命名socket命名方式有两种。
- 一是普通的命名,socket会根据此命名创建一个同名的socket文件,客户端连接的时候通过读取该socket文件连接到socket服务端。这种方式的弊端是服务端必须对socket文件的路径具备写权限,客户端必须知道socket文件路径,且必须对该路径有读权限。
- 另外一种命名方式是抽象命名空间,这种方式不需要创建socket文件,只需要命名一个全局名字,即可让客户端根据此名字进行连接。这种方式的实现过程与前者的差别是,后者在对地址结构成员sun_path数组赋值的时候,必须把第一个字节置0,即sun_path[0] = 0。
六、信号量
- 信号量是用来解决进程间的同步与互斥问题的一种进程间通信机制,包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操作(P/V操作)。其中,信号量对应于某一种资源,取一个非负的整形值。信号量值(常用sem_id表示)指的是当前可用的该资源的数量,若等于0则意味着目前没有可用的资源。
- PV原子操作的具体定义如下:
- P操作:如果有可用的资源(信号量值>0),则此操作所在的进程占用一个资源(此时信号量值减1,进入临界区代码);如果没有可用的资源(信号量值=0),则此操作所在的进程被阻塞直到系统将资源分配给该进程(进入等待队列,一直等到资源轮到该进程)。
- V操作:如果在该信号量的等待队列中有进程在等待资源,则唤醒一个阻塞进程;如果没有进程等待它,则释放一个资源(即信号量值加1)。
- 在Linux系统中,使用信号量通常分为以下4个步骤:
- 创建信号量或获得在系统中已存在的信号量,此时需要调用 semget() 函数。不同进程通过使用同一个信号量键值来获得同一个信号量。
- 初始化信号量,此时使用 semctl() 函数的SETVAL操作。当使用互斥信号量时,通常将信号量初始化为1。
- 进行信号量的PV操作,此时,调用 semop()函数。这一步是实现进程间的同步和互斥的核心工作部分。
- 如果不需要信号量,则从系统中删除它,此时使用semctl()函数的 IPC_RMID操作。需要注意的是,在程序中不应该出现对已经被删除的信号量的操作。
- 一些相关函数:
- ftok()函数获取IPC关键字:
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
该函数根据pathname指定的文件或目录的索引节点号和proj_id计算并返回一个key_t类型的ID值,如果失败则返回-1;
第一个参数pathname是一个系统中必须存在的文件或文件夹的路径,会使用该文件的索引节点;
第二个参数proj_id是用户指定的一个子序号,这个数字有的称之为project ID。它是一个8bit的整数,取值范围是1~255。
需要注意的是:如果要确保key值不变,要么确保ftok()的文件不被删除,要么不用ftok()指定一个固定的key值。查询文件索引节点号的方法是: ls -i filename
- semget()函数创建或获取信号量
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget( key_t key, int nsems, int semflg);
该函数用来创建一个信号集,或者获取已存在的信号集。成功返回信号量集的标识符,失败返回-1,errno被设置成以下的某个值:
第一个参数key是所创建或打开信号量集的键值(ftok成果执行的返回值),不相关的进程可以通过它访问一个信号量,它代表程序可
能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget()函数并提供一个键,再由系统生成一个相应的信
号标识符(semget()函数的返回值),只有semget()函数才直接使用信号量键,所有其他的信号量函数使用由semget()函数返回的信
号量标识符。如果多个程序使用相同的key值,key将负责协调工作;
第二个参数nsems指定需要的信号量数目,它的值几乎总是1;
第三个参数semflg 是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作,设置了
IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的
信号量,如果信号量已存在,返回一个错误。
- semctl()函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semun, int cmd, ...);
该用来初始化信号集,或者删除信号集。成功返回一个正数,失败返回-1。
第一个参数semid是前面讲的semget()函数返回的信号量键值;
第二个参数semun是操作信号在信号集中的编号,第一个信号是0;
第三个参数cmd是在semid指定的信号量集合上执行此命令,可以是:
SETVAL:设置信号量集中的一个单独的信号量的值,此时需要传入第四个参数;
IPC_RMID:从系统中删除该信号量集合;
IPC_SEAT:对此集合取semid_ds 结构,并存放在由arg.buf指向的结构中;
第四个参数是可选的,如果使用该参数,则其类型是semun,它是多个特定命令参数的联合(union),该联合不在任何系统头文件中定
义,需要我们自己在代码中定义:
union semun
{
int val;
struct semid_ds * buf;
unsigned short * array;
struct seminfo * __buf;
};
- semop()函数
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf * sops, unsigned nsops);
操作一个或一组信号,也可以叫PV操作。成功执行时,都会回0,失败返回-1,并设置errno错误信息。
第一个参数semid是前面讲的semget()函数返回的信号量键值;
第二个参数sops是一个指针,指向一个信号量操作数组。信号量操作由结构体sembuf 结构表示如下:
struct sembuf
{
unsigned short sem_num; // 操作信号在信号集中的编号,第一个信号的编号是0,最后一个信号的编号是nsems-1。
short sem_op; //操作 为负(P操作), 其绝对值又大于信号的现有值,操作将会阻塞,直到信号值大于或等于
sem_op的绝对值。通常用于获取资源的使用权。
// 为正(V操作), 该值会加到现有的信号内值上。通常用于释放所控制资源的使用权。
// 为0: 如果后面的sem_flag没有设置IPC_NOWAIT,则调用该操作的进程或线程将暂时睡眠,
直到信号量的值为0;否则进程或线程会返回错误EAGAIN。
short sem_flg; // 信号操作标识,有如下两种选择:
// IPC_NOWAIT:对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息。
// SEM_UNDO:程序结束时(正常退出或异常终止),保证信号值会被重设为semop()调用前的值。避
免程序在异常情况下结束时未解锁锁定的资源,早成资源被永远锁定。造成死锁。
};
第三个参数nsops:信号操作结构的数量,恒大于或等于1.
七、共享内存
- 两个或多个进程都可以访问的同一块内存空间,一个进程对这块空间内容的修改可为其他参与通信的进程所看到的就是共享内存。需要做到两件事:一件是在内存划出一块区域来作为共享区;另一件是把这个区域映射到参与通信的各个进程空间。
- 相关函数
- ftok()函数获取IPC关键字:
key_t ftok(const char *pathname, int proj_id);
该函数根据pathname指定的文件或目录的索引节点号和proj_id计算并返回一个key_t类型的ID值,如果失败则返回-1;
第一个参数pathname是一个系统中必须存在的文件或文件夹的路径,会使用该文件的索引节点;
第二个参数proj_id是用户指定的一个子序号,这个数字有的称之为project ID。它是一个8bit的整数,取值范围是1~255。
- shmget()创建共享内存:
int shmget(key_t key, size_t size, int shmflg);
说明:该函数用来创建共享内存,成功返回一个非负整数,即该共享内存段的标识码;失败返回-1。
第一个参数key是ftok()返回的key_t类型键值;
第二个参数size以字节为单位指定需要共享的内存容量。;
第三个参数shmflg是权限标志,它的作用与open函数的mode参数一样,如果要想在key标识的共享内存不存在就创建它的话,可以与IPC_CREAT做或操作。共享内存的权限标志与文件的读写权限一样,举例来说,0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。
- shmat()函数启动对该共享内存的访问:
void *shmat(int shmid, const void *shmaddr, int shmflg);
说明:第一次创建完共享内存时,它还不能被任何进程访问,shmat函数的作用就是用来启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1.
第一个参数shmid是由shmget函数返回的共享内存标识;
第二个参数shmaddr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。
第三个参数shmflg是一组标志位,通常为0。
- shmdt()函数用于将共享内存从当前进程中分离:
int shmdt(const void *shmaddr);
说明:该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。 参数shmaddr是shmat函数返回的地址指针,调用成功时返回0,失败时返回-1。
- shmctl函数用于控制共享内存:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
第一个参数shmid是shmget函数返回的共享内存标识符;
第二个参数cmd是要采取的操作,它可以取下面的三个值:
IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值;
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值;
IPC_RMID:删除共享内存段
第三个参数,buf是一个结构指针,它指向共享内存模式和访问权限的结构。其结构体类型定义如下:
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
}
八、消息队列
- 消息队列(Message Queue,简称MQ)提供了一个从一个进程向另外一个进程发送一块数据的方法,每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。 消息队列也有管道一样的不足,就是每个消息的最大长度是有上限的(MSGMAX),每个消息队列的总的字节数是有上限的(MSGMNB),系统上消息队列的总数也有一个上限
(MSGMNI)。 - MQ 传递的是消息,消息即是我们需要在进程间传递的数据。在进行进程间通信时,一个进程将消息加到 MQ 尾端,另一个进程从消息队列中取消息(不一定以先进先出来取消息,也可以按照消息类型字段取消息),这样就实现了进程间的通信。
- 进程 A 向内核维护的消息队列中发消息,进程 B 从消息队列中取消息,从而实现了 A 和 B 的进程间通信。MQ 的 API 操作与共享内存几乎是相同的,分为下面 4 个步骤:
- 创建和访问 MQ
- 发送消息
- 接受消息
- 删除 MQ
- 相关函数
- ftok()函数获取IPC关键字:
key_t ftok(const char *pathname, int proj_id);
该函数根据pathname指定的文件或目录的索引节点号和proj_id计算并返回一个key_t类型的ID值,如果失败则返回-1;
第一个参数pathname是一个系统中必须存在的文件或文件夹的路径,会使用该文件的索引节点;
第二个参数proj_id是用户指定的一个子序号,这个数字有的称之为project ID。它是一个8bit的整数,取值范围是1~255。
- msgget创建消息队列ID:
int msgget(key_t key, int msgflg);
说明:该函数用来创建消息队列ID,成功返回一个非负整数,即该共享内存段的标识码;失败返回-1。
第一个参数key是ftok()返回的key_t类型键值;
第二个参数msgflg是创建标志:IPC_CREAT,不存在则创建,存在则返回已有的mqid,IPC_CREAT|IPC_EXCL,不存在则创建,存在则返回出错.。
- msgsnd()用来发送一个消息:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
说明:用来发送一个消息,必须要有写消息队列的权限。成功返回 0, 失败返回 -1, 并设置 erron。
第一个参数msgid是由msgget函数返回的消息队列 ID;
第二个参数msgp是一个指针,它指向要发送的消息结构体类型的变量。消息结构在两方面受到制约。首先,它必须小于系统规定的上限值;
其次,它必须以一个long int长整数开始,接收者函数将利用这个长整数确定消息的类型,其参考类型定义形式如下:
typedef struct s_msgbuf
{
long mtype;
char mtext[512];
} t_msgbuf;
第三个参数msgsz是要发送消息的长度;
第四个参数msgflg 控制着当前消息队列满或到达系统上限时将要发生的事情,设置为IPC_NOWAIT表示队列满不等待,返回EAGAIN错误。
*msgrcv()函数用来从一个消息队列接收消息:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
说明:用来从一个消息队列接收消息。成功返回实际放到接收缓冲区里去的字符个数,失败返回-1, 并设置 erron。
第一个参数msgid是由msgget函数返回的消息队列 ID;
第二个参数msgp是一个指针,它指向准备接收的消息;
第三个参数msgsz是msgp指向的消息长度,这个长度不含保存消息类型的那个long int长整型;
第四个参数msgtype是消息的类型,它可以实现接收优先级的简单形式;
msgtype=0返回队列第一条信息
msgtype>0返回队列第一条类型等于msgtype的消息
msgtype<0返回队列第一条类型小于等于msgtype绝对值的消息,并且是满足条件的消息类型最小的消息
第五个参数msgflg 控制着队列中没有相应类型的消息可供接收时将要发生的事;
msgflg=IPC_NOWAIT,队列没有可读消息不等待,返回ENOMSG错误。
msgflg=MSG_NOERROR,消息大小超过msgsz时被截断
msgtype>0且msgflg=MSG_EXCEPT,接收类型不等于msgtype的第一条消息。
- msgctl()函数用于控制消息队列
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
说明:该函数用于控制消息队列
第一个参数msgid是由msgget函数返回的消息队列 ID;
第二个参数cmd是要采取的操作,它可以取下面的三个值:
IPC_STAT:把msqid_ds结构中的数据设置为消息队列的当前关联值;
IPC_SET: 如果进程有足够的权限,就把消息队列的当前关联值设置为msqid_ds结构中给出的值;
IPC_RMID:删除消息队列;
第三个参数,buf是一个结构指针,它指向存储消息队列的相关信息的 buf;