目录
进程间通信
为什么要进行进程间通信
我们都知道,每一个进程的数据都是存储在物理内存之中的。
进程通过各自的进程虚拟地址空间,经过各自页表的映射关系,对物理内存进行访问。
以32位的操作系统为例,32位操作系统的内存最大为4G,操作系统给每个进程都画了一张4G的大饼(进程虚拟地址空间),每个进程都以为自己有4G的空间,而实际上是操作系统在后方精打细算,管理着数据的存储以及页表的映射。进程只需要单纯地执行代码即可,这便保证了进程间的独立性。
进程是一个独立的资源分配单元,通常情况下,进程之间是没有关联的,一个进程不能直接访问另一个进程的资源。
但是,进程不是孤立的,一个足够大的项目绝对不是单一的进程可以支撑的起的。所以我们需要进程间通信,来满足不同进程间信息交互与传递的需求。
进程间通信本质上是进程与进程之间交换数据的手段。
进程间通信的目的
通常来说,进程间通信的目的可总结为以下四种:
1.数据传输:进程之间需要进行数据的交换。
2.通知事件:一个进程需要向其他进程发送消息,告诉它们发生了某种事件。
3.资源共享:多个进程之间共享同样的资源。
4.进程控制:有些进程希望完全控制另一个进程的执行,比如需要其他进程来执行一些容易崩溃的代码,此时控制进程需要及时获得另一个进程的状态信息。
常见进程间通信方式
1.管道
2.共享内存
3.消息队列
4.信号量
5.信号
6.网络
PS.网络是最大的,应用最广泛的进程间通信方式。
这里只简单介绍以下管道、共享内存以及消息队列。信号量、信号、网络涉及到很多更加复杂的知识点,需要更长的篇幅来记录。
管道
管道
| //管道符号
管道符号就是一个简简单单的竖线,也即我们在C/C++编程中的按位或“ | ”。
管道是Unix系统中最古老的进程间通信方式,它是一个进程连接到另一个进程的数据流。
我们都知道,Linux系统中各种命令,如ps,ls等,其本质上都是可执行程序。有时,这些可执行程序可能会产生大量的输出,而通过管道“ | ”,我们可以将这些输出交由管道符号后的命令进行二次处理。
管道的本质
管道本质是内核当中的一块缓冲区,供进程进行读写,交换数据。
匿名管道接口
我们通常所称的管道,一般指的都是匿名管道(pipe)。
在程序中,我们通过以下函数创建一个管道:
#include <unistd.h>
int pipe(int pipefd[2]);
//参数为输出型参数,即数组pipefd[2]的值是由函数pipe填充的,供以函数调用者使用。
//pipefd为整型数组,有两个元素。
// pipefd[0]:管道的读端
// pipefd[1]:管道的写端
//返回值:管道创建成功,返回0
// 管道创建失败,返回-1
管道是一种基于文件描述符的通信方式,当一个管道建立时,操作系统会在内核中开辟一块只允许数据单向流动的缓冲区,同时会创建两个文件描述符,分别代表管道的读(pipefd[0])写(pipefd[1])两端。
#include <stdio.h>
#include <unistd.h>
int main() {
int fd[2];
if (pipe(fd) < 0) {
perror("pipe failed");
}
printf("pipefd[0]: %d\n", fd[0]);
printf("pipefd[1]: %d\n", fd[1]);
while (1) {
sleep(1);
}
return 0;
}
既然pipe函数是通过给输入性参数pipefd[2]的两个元素赋值两个文件描述符来授予调用者使用的权力。那么调用者该如何使用管道来达到通信的目的呢?
联系我之前所写的博客实际上很容易想到,Linux提供了一系列的系统调用文件接口,如open,write,read等等,这些函数无一例外,都是通过传入文件描述符来对文件进行对应的操作的。而pipe函数刚好给了我们两个文件描述符。
不过,在使用系统调用文件接口对管道进行操作的时候需要注意以下几点:
1.读写接口与pipe函数返回的读写端文件描述符要做到操作对应,write操作写端pipefd[1],read操作读端pipefd[0]。
2.lseek函数无法对管道进行操作。
3.pipe函数的调用要在fork函数的调用之前。
以下是一个简单的应用举例:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main() {
int fd[2];
if (pipe(fd) < 0) {
perror("pipe failed");
}
int f = fork();
if (f > 0) {
close(fd[0]);
char buf[] = "hello, i speak to you through the pipe";
int len = sizeof(buf)/sizeof(buf[0]);
write(fd[1], buf, len * sizeof(char));
sleep(2);
wait(NULL);
} else {
close(fd[1]);
sleep(1);
char tmp[128];
int len = read(fd[0], tmp, 128);
tmp[len] = '\0';
printf("%s\n", tmp);
exit(0);
}
return 0;
}
匿名管道的特性
我们先了解以下通信的三种方式:单工、半双工、双工。
单工通信(Simplex):消息只能单方向传输的工作方式。通信双方中只有一个可以进行发送,另一个只能接收,如广播、遥测、遥控等。
半双工通信(Half-duplex):通信双方都能收发消息,但不能同时进行收和发的工作方式。
双工通信(Duplex):通信双方可同时收发消息的工作方式。一般来说双工通信信道必须是双向信道。
匿名管道具有以下特征:
1.半双工通信,父子进程都同时拥有管道的读端和写端,故理论上数据可以通过管道由父进程流向子进程,也可以通过管道由子进程流向父进程。
2.只能用于具有亲缘关系的进程间通信通常,通常是用于父子进程间通信。因为匿名管道实质上是内核中的一个缓冲区,但是它并没有任何标识符,进程是无法在内核中找到它,进程只能通过管道创建时返回的两个文件描述符对它进行操作。只有通过父子进程间的那种“继承”关系,才可以达到通信的条件。
3.依赖于文件系统,一个管道的生命周期随进程结束而结束。当进程退出之后,管道随之被销毁。
4.管道的大小为64K字节。
5.管道的读写:一个进程向管道中写入内容被另一个进程读出。写入的内容每次都被添加在管道缓冲区的末尾,读出的内容每次都是从管道缓冲区的头部。可以理解为,管道的读端与写端是固定的,数据在管道中流动的方向只能从管道的写端流向管道的读端。
6.管道提供字节流服务,即当进程从读端进行读取时,是将数据从管道中读走了。当然,我们可以选择从管道中读取多少。
7.原子性:当读写数据的大小小于pipe_size的时候,管道会保证读写的原子性。
PS.原子性指的是指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么不执行。
管道阻塞及非阻塞属性的设置
8.当管道的文件描述符保持基础属性时,调用read读取空管道,read函数会阻塞。
9.当管道的文件描述符保持基础属性时,一直调用write函数将管道写满时,write函数会进入阻塞状态。
将上方代码简单修改后如下
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main() {
int fd[2];
if (pipe(fd) < 0) {
perror("pipe failed");
}
int f = fork();
if (f > 0) {
close(fd[0]);
int count = 0;
char buf = 'a';
while(1) {
printf("count :%d \n", count++);
write(fd[1], &buf, 1);
}
wait(NULL);
} else {
close(fd[1]);
sleep(1);
while(1);
exit(0);
}
return 0;
}
通过pstack命令查询验证,代码阻塞在了write函数处。
在有些情况下,管道默认的阻塞状态会给我们带来一些不必要的困扰。我们可以通过以下函数来设置管道的非阻塞状态:
#include <unistd.h>
#include <sys.types.h>
#include <fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock* lock);
//fd:要操作的文件描述符
//cmd:将要进行的操作
// F_GETFL:获取文件描述符的旗标(属性信息)
// F_SETFL:设置文件描述符的旗标(属性信息),设置新的属性到可变参数列表中
//arg:将要给文件描述符设置的旗标(属性信息)
//lock:flock结构体,设置记录锁的状态,本篇并不涉及。
//返回值:F_GETFL 返回文件描述符旗标(属性信息)
// F_SETFL 设置成功:0 设置失败:-1
fcntl函数的功能是十分强大的cmd可以传入的参数种类也非常之多,此处仅仅根据当下需求使用了它的一些简单功能。
我们可以通过
O_NONBLOCK
来设置文件描述符为非阻塞状态,当相应而来的问题是,Linux操作系统中,文件的属性是通过位图来记录的,单一的
fcntl(fd[1], F_SETFL, O_NONBLOCK);
可能会导致文件描述符原本属性信息被覆盖,所以我们需要先获取到目标文件描述符fd[1]的旗标,通过
flag = flag | O_NONBLOCK
来确保我们只修改了阻塞非阻塞属性控制的部分。
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/types.h>
#include <fcntl.h>
int main() {
int fd[2];
if (pipe(fd) < 0) {
perror("pipe failed");
}
int f = fork();
if (f > 0) {
close(fd[0]);
int count = 0;
char buf = 'a';
long flag = fcntl(fd[1], F_GETFL);
flag = flag | O_NONBLOCK;
fcntl(fd[1], F_SETFL, flag);
while(1) {
printf("count :%d \n", count++);
write(fd[1], &buf, 1);
}
sleep(2);
wait(NULL);
} else {
close(fd[1]);
sleep(1);
while(1);
char tmp[128];
int len = read(fd[0], tmp, 128);
tmp[len] = '\0';
printf("%s\n", tmp);
exit(0);
}
return 0;
}
在读端默认非阻塞属性情况下,
若写端不关闭,也不写入,一直读,读端调用read函数后会返回-1,errorno设置为EAGAIN (资源不可用)
写端关闭,读端一直读,返回0。
在写端默认非阻塞属性情况下,
读端不关闭,写端一直写入,在管道写满后,返回-1
读端关闭,写端一直写入,写端进程会收到SIGPIPE信号,发生崩溃,
匿名管道的零碎小问题
为什么要在父子进程中各关闭一个读写端?
这是大部分pipe示例的代码中都会有的一个操作。父进程关闭读端留下写端,子进程关闭写端留下读端,或者相反。
这是因为,虽然管道是半双工通信的,但进程是抢占式执行的,如果用户创建一个管道,使同一进程同时掌握并使用这个管道的读写,我们并不能保证在父进程的一次写与一次读的操作间隔中,子进程已经完成了对父进程想要传递数据的读取,以及子进程想要反馈的数据的写入。如果在父进程的写操作与读操作之间,子进程并没有执行,那么父进程将会将自己写入的数据读出,从而达不到程序预期的效果,甚至导致程序的崩溃。所以在使用管道通信时,父子进程能且只能使用管道的不同的一端。与其另一端打开占用着一个文件描述符,不如关闭提高资源的利用率。
说的简单点就是,如果父子进程想要通过一个管道来完成父到子以及子到父的数据交流,假设父进程先写入,等待一段时间后再读出,子进程在父进程读写的间隔中读取父进程写的数据并且写入要传送给父进程的数据。由于管道是半双工通信的,这在理论上是可行的。
但是,进程是抢占式执行的,它不会管什么尊老爱幼以及你们父子两个进程之间有什么计划。所以在父进程给子进程空出执行的那一段时间,子进程很可能抢不过其它进程,并没有执行。但是父进程是不知道这样的情况的。父进程在等待一定的时间(这个时间是我们手动设置的)后也会参与到抢占式执行的大军中,如果它比子进程先抢到,那么它就会将自己写入的消息再次读出,当作子进程的反馈来处理。如此的错过,便容易导致结果的偏离甚至程序的崩溃。
匿名管道为什么可以进行进程间通信?
这就涉及到进程控制方面的知识点了,但并不是很难。
父进程在通过fork函数创建子进程时,子进程会从父进程出拷贝很多信息,其中就包括了打开的文件描述符。
我们都知道Linux的基本“哲学”之一是“一切皆文件”。匿名管道自然也不例外。通常会把pipe函数看作一个特殊的文件系统,每次调用创建一个inode,并关联两个file,分别代表着管道的两端(感觉就是一个权限只读,一个权限只写)。
命名管道
与匿名管道只能进行亲缘进程间通信不同,命名管道可在同一台计算机的不同进程之间或在跨越一个网络的不同计算机的不同进程之间,支持可靠的、单向或双向的数据通信。
其大概原理如下:
系统在创建命名管道时给管道指定一个名字,任何进程都可以通过这个名字找到并使用命名管道。
我们可以使用以下命令在终端上创建一个命名管道:
mkfifo <管道名>
而在代码中,我们可以通过以下函数来实现创建:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char* pathname, mode_t mode);
//pathname:要创建命名管道的路径及文件名
//mode:八进制表示的权限,如0664
//返回值:成功创建则返回0,否则返回-1。
只需要在需要使用命名管道通信时,使用open函数打开命名管道文件,获取命名管道文件描述符,便可以对管道进行操作了。
命名管道本质上仍然是内核中的一块缓冲区,不过操作系统将它的一些信息整合到了一种特殊的文件中,也即我们看到的命名管道文件,通过对这个文件的访问,进程也就可以按图索骥,获得对命名管道的一些操作权限。
匿名管道的生命周期是跟随创建它的进程的,进程终止,匿名管道也会随之释放。而命名管道的生命周期是跟随内核的,直到用户删除对应的命名管道文件,管道才会销毁。
共享内存
概念
前置知识点
1.进程通过代码所获得的地址并不是物理地址,每个进程都拥有一块虚拟进程地址空间。
2.进程通过页表将物理内存映射到自己的进程虚拟地址空间中。
3.每个进程的虚拟进程地址空间中都有一块共享区。
概念
共享内存是一种用于实现进程间通信的方法,不同进程通过访问同一块物理内存区域来实现数据的交互与共享。
每个进程有自己的进程控制块和地址空间,且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。 不同进程可以通过操作自己进程虚拟地址空间当中的虚拟地址,来操纵共享内存。
若某进程更改了共享内存区的内容,其它进程都会觉察到该区域的更改。
共享内存的系统接口
创建或者获取共享内存接口
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
//key:共享内存标识符,不同进程通过相同的键值来定位同一块共享内存
//size:共享内存大小
//shmflg:获取/创建共享内存时,传递的属性信息
// IPC_CREAT:如果获取的共享内存不存在,则创建
// IPC_CREAT | IPC_EXCL:如果获取的共享内存不存在,则创建
// 如果获取的共享内存存在,则报错
// 在创建的时候还需要或( | )上操作权限,如0664
//返回值:成功则返回共享内存操作句柄
// 失败返回-1
允许进程访问共享内存
#include <sys/types.h>
#include <sys/shm.h>
void* shmat(int shmid, const void* shmadder, int shmflg);
//shmid:共享内存标识符(shmget返回的操作句柄)
//shmadder:将共享内存附加到共享区中的哪一个地址上
// 即建立共享区与共享内存间的关系
// 由于用户不确定共享区的使用情况
// 所以,一般交由操作系统进行操作,传入NULL
//shmflg:当前进程对这一块共享内存的操作权限
// SHM_RDONLY:只读
// 0 :可读可写
//返回值:成功返回附加好的共享内存地址;失败返回NULL
断开共享内存链接,分离
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void* shmadder);
//shmadder:shmat的返回值,连接的共享内存起始地址
//返回值:成功返回0;失败返回-1
共享内存管理
#include <sys/types.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
//shmid:共享内存操作句柄
//cmd:设置shmct需要完成的功能
// IPC_SET:设置共享内存属性信息
// IPC_STAT:获得共享内存属性信息
// IPC_RMID:删除共享内存,第三个参数设置为NULL
//buf:共享内存数据结构buf
//返回值:成功返回0;失败返回-1
shmget,shmat,shmdt函数的简单示例
//write
#include <stdio.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <unistd.h>
#include <string.h>
int main() {
int shm_id = shmget(0x66, 1024, IPC_CREAT | 0664);
if (shm_id < 0) {
perror("shmget:");
return 0;
}
void* addr = shmat(shm_id, NULL, 0);
if (addr == NULL) {
perror("shmat:");
return 0;
}
strcpy((char*)addr, "i speak to you through IPC");
shmdt(addr);
return 0;
}
//read
#include <stdio.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <unistd.h>
#include <string.h>
int main() {
int shm_id = shmget(0x66, 1024, IPC_CREAT | 0664);
if (shm_id < 0) {
perror("shmget:");
return 0;
}
void* addr = shmat(shm_id, NULL, 0);
if (addr == NULL) {
perror("shmat:");
return 0;
}
printf("read: %s\n", (char*)addr);
shmdt(addr);
return 0;
}
共享内存特性
1.共享内存生命周期跟随操作系统。
在Linux操作系统中,我们可以使用如下命令来查看消息队列,共享内存以及信号量的相关使用信息:
ipcs
上图中,共享内存段的列表中,我们很容易找到我们之前在程序代码中定义的key为0x66的共享内存段。而在使用该命令时,我的读写进程都已经终止了,却依然可以找到该共享内存的信息,可见共享内存的生命周期是不跟随进程的。
key | shmid | owner | perms | bytes | nattch | status |
共享内存标识符 | 操作句柄 | 共享内存所有者 | 共享内存权限 | 共享内存大小 | 附加进程数量 | 共享内存状态 |
2.共享内存是覆盖写的方式,读的时候是通过地址访问。
我们可以对上方的代码进行一些简单的修改来验证,我仅以读进程的代码示例:
#include <stdio.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <unistd.h>
#include <string.h>
int main() {
int shm_id = shmget(0x66, 1024, IPC_CREAT | 0664);
if (shm_id < 0) {
perror("shmget:");
return 0;
}
void* addr = shmat(shm_id, NULL, 0);
if (addr == NULL) {
perror("shmat:");
return 0;
}
printf("read: %s\n", (char*)addr);
getchar();
printf("read_2: %s\n", (char*)addr);
getchar();
shmdt(addr);
return 0;
}
只进行一次写入而进行两次读取,可以看出,共享内存的读取并不同于管道的读取,共享内存在读过后其内的数据仍然保留。
结合代码与上图结果可以看出,在重新向共享内存内写入后,重新写入的字符串长度明显短于原字符串,但再次读取时并没有出现字符串的残留,可见再次写入时是将之前写入的内容全部清楚后,再写入的新内容。
共享内存的删除
根据上文的共享内存的特性,我们可以知道,共享内存的生命周期是跟随操作系统的。一块共享内存创建出来后,它是会一直存在的——如果用户不进行删除操作的话。
我们可以通过以下命令来删除一块共享内存:
ipcs -m
//获取共享内存操作句柄shmid
ipcrm -m <shmid>
//删除指定shmid对应的共享内存
同样,我们也可以通过代码来实现这个功能。
其中需要用到的函数就是上边没有举例的 shmctl
#include <stdio.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <unistd.h>
#include <string.h>
int main() {
int shm_id = shmget(0x666666, 1024, IPC_CREAT | 0664);
if (shm_id < 0) {
perror("shmget:");
return 0;
}
printf("creat\n");
getchar();
shmctl(shm_id, IPC_RMID, NULL);
printf("delete");
getchar();
return 0;
}
有很多细节单纯的图片与文字并不好表达,如果感兴趣可以自己尝试一遍。
被删除且尚在使用的
这个标题看起来貌似很矛盾,被删除,尚在使用?在使用着不就表明没有被删除吗?这就要从我们上边很多图中都表现出的一个小细节来说起了。
通过前边所讲述的我们都很清楚,一块共享内存的标识符应该是唯一的,要不然我们无法保证两个进程可以访问到同一块共享内存。
但图中的共享内存信息却与我们所料想的大为不同——它们的共享内存标识符都是0x00,当然它们的另一个共同点是它们的状态都是目标/dest(destroy)。
这样的情况代表这块共享内存已经被删除掉了,它们的物理空间中已经被释放了。而我们之所以还可以访问这块共享内存,是因为操作系统内核通过结构体来存放共享内存的信息的,而我们的删除操作只是将它对应的那一块物理内存释放了,而结构体依然存在,没有被释放。当共享内存附加的进程为0时,这块共享内存所占用的资源才会被彻底释放。
如果我们不在代码中解除附加的绑定,当进程退出时,这个绑定也会自动解除。
消息队列
消息队列的原理
消息队列(Message queue)是由消息组成的链表。如果将消息看作一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读走消息。
消息队列是通过链表实现的,这个链表由操作系统来维护。
操作系统中可能会有很多msgqueue(消息队列),每个消息队列通过不同的消息队列描述符(消息队列ID:qid)来区分,消息队列的qid是唯一的。
在使用消息队列进行进程间通信时,一个进程将消息添加到消息队列尾端,另一个进程从消息队列中取出消息。
消息队列中消息的出队不一定是严格意义上的先进先出,用户可以指定某种类型的消息,只针对该类型消息先进先出地取出。(所以还是要先进先出,不过更为宽松了)
消息队列接口
创建消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
//key:消息队列标识符
//msgflg:消息队列创建标志
// IPC_CREAT:如果不存在,创建
// 需要按位或上八进制表示的权限如0664
//返回值:成功返回队列ID(qid);失败返回-1,并设置erron
发送消息
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void* msgp, size_t msgsz, int msgflg);
//msqid:消息队列地id
//msgp:指向msgbuf地指针,用来指定发送的消息
//msgsz:要发送消息的长度,仅指消息内容mtext
//msgflg:创建标记,如果指定IPC_NOWAIT(非阻塞),失败会立刻返回
// 0 :阻塞发送
// IPC_NOWAIT:非阻塞发送
//返回值:发送成功返回0;失败返回-1
struct msgbuf {
long mtype; //当前消息的类型,需要大于0
char mtext[1]; //消息数据,由用户自己去定义大小
}
接收消息
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void* msgp, size_t msgsz, long msgtyp, int msgflg);
//msqid:消息队列id
//msgp:指向msbuf的指针,用来接收消息
//msgsz:要接收消息的长度
//msgtyp:接收消息的方式
// msgtyp = 0:读取消息队列中的第一条消息
// msgtyp > 0:读取队列中类型为msgtyp的第一条消息;
// 如果在msgflg种指定了MSG_EXCEPT,则将读取第一条类型非msgtyp的消息
// magtyp < 0:读取队列中类型小于等于msgtyp的绝对值的第一条消息
// |msgtyp| >= typ,满足条件的第一条typ类型消息
//msgflg:创建标记,如果指定IPC_NOWAIT,失败会立刻返回
//返回值:成功返回实际读取消息的字节数;失败返回-1,并设置erron
消息队列操作接口
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int magctl(int msqid, int cmd, struct msqid_ds* buf);
//msqid:消息队列id
//cmd:控制命令
// IPC_RMID:从内核中删除该消息队列
// IPC_STAT:获取消息队列状态
//buf:存储消息队列相关信息的结构体
//返回值:成功根据不同cmd有不同的返回值;失败返回-1,并设置erron
如果熟悉了共享内存的接口操作,消息队列的操作也并没有太多难点。
//read
#include <stdio.h>
#include <unistd.h>
#include <sys/msg.h>
#include <string.h>
typedef struct msgbuf {
long mtype;
char mtext[256];
}msgbuf;
int main() {
int qid = msgget(0x66, IPC_CREAT | 0664);
if (qid < 0) {
perror("msgget:");
return 0;
}
printf("qid: % d\n", qid);
msgbuf m;
msgrcv(qid, &m, 256, 0, 0);
printf("read: %s\n", m.mtext);
return 0;
}
//send
#include <stdio.h>
#include <unistd.h>
#include <sys/msg.h>
#include <string.h>
typedef struct msgbuf {
long mtype;
char mtext[256];
}msgbuf;
int main() {
int qid = msgget(0x66, IPC_CREAT | 0664);
if (qid < 0) {
perror("msgget:");
return 0;
}
printf("qid: % d\n", qid);
msgbuf m;
m.mtype = 6;
strcpy(m.mtext, "i'll send it to you through message queue");
msgsnd(qid, &m, sizeof(m.mtext), 0);
return 0;
}
基于队列的特性,当我们向队列中写入一个信息时,我们是将信息打包到buf结构体中,插入到了队列当中,当我们从队列中读取一个信息的时候,是将buf结构体从队列中取了出来,队列中的每一条信息只能使用一次,一次写入对应着一次读取。
PS.不出意外就懒得写长博客了,回头看来之前的博客。。太长的我自己都懒得看。淦