目录
一、进程间通信(IPC)
二、信号量(Semaphore)
1、基本概念
2、同步关系与互斥关系
3、临界区与临界资源
4、信号量的工作原理
5、信号量编程
6、实战演练
1、基本概念
2、共享内存的优点
3、共享内存的缺点
4、共享内存编程
5、实战演练
四、消息队列(Message Queue)
1、基本概念
2、特点和用途
3、消息队列编程
4、实战演练
五、学习心得
一、进程间通信(IPC)
在计算机编程和操作系统中,进程间通信(Inter-Process Communication,IPC)是实现不同进程之间数据传输和共享资源的关键技术。在多任务和多进程系统中,各个进程可能需要相互通信以协调任务、共享数据或进行同步操作。本文将深入探讨几种常见的进程间通信方式,如下图所示:
本篇文章将会详细讲解进程间通信的三种常见方法:信号量、共享内存、消息队列。如果有同学对于另外四种方法不是很熟悉,推荐看这篇文章---《APUE学习之进程间通信(IPC)(上篇)》,这篇文章详细讲解了进程间通信的另外四种常见方法:信号、管道、命名管道、命名socket。
二、信号量(Semaphore)
1、基本概念
在《APUE学习之进程间通信(IPC)(上篇)》这篇文章中我们讲到了信号(Signal),大家不要混淆二者。信号是使用信号处理器来进行的,而信号量是使用PV操作来实现的。
信号量主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。每个执行流在进入临界区之前都应该先申请信号量,申请成功就有了操作特点的临界资源的权限,当操作完毕后就应该释放信号量。流程如下:
信号量就是用来解决进程间的同步与互斥问题的一种进程间通信机制,包括一个称为信号量的变量和在该信号量下等待资源的进程等待队列,以及对信号量进行的两个原子操作。信号量值(sem_id)指的是当前可用的该资源的数量,若等于0则意味着目前没有可用的资源。
在学习信号量前,大家先一起了解几个概念。
2、同步关系与互斥关系
(1) 同步关系
同步关系指的是多个线程或进程之间通过某种机制协调执行顺序,以达到特定的目的或保证数据的一致性。在同步关系中,各个线程或进程之间可能会互相等待、协作或交换信息,以实现某种有序的执行流程。(实现先执行A,在执行B)
(2)互斥关系
互斥关系指的是多个线程或进程之间通过某种机制保证对临界资源的互斥访问,即同一时间只能有一个线程或进程可以访问共享资源,以避免数据竞争和数据一致性问题。(A和B都想干同一件事,但同一时间只能有一个人干)
(3)区别
同步关系侧重于协调多个线程或进程的执行顺序和操作,以实现特定的目的或保证数据的一致性;而互斥关系侧重于保证对临界资源的互斥访问,避免数据竞争和数据不一致性问题。
3、临界区与临界资源
(1)临界区(Critical Section)
临界区是指一段代码,当多个线程或进程同时执行这段代码时,可能会导致竞态条件或数据不一致的情况发生。因此,需要确保在任何时刻,最多只有一个线程或进程可以进入临界区执行代码,以保证数据的正确性和一致性。
在实际编程中,通过使用同步原语(如互斥锁、信号量等)来保护临界区,使得每次只有一个线程或进程可以获得访问权限,从而避免了竞态条件的发生。
(2)临界资源(Critical Resource)
临界资源是指需要被临界区代码段保护的共享资源,可能是内存、文件、数据库连接、硬件设备等。多个线程或进程需要对这些资源进行访问,但同时只能有一个线程或进程进行访问,否则可能会导致数据损坏、不一致性等问题。
对于临界资源,需要确保在对其进行访问时,只有一个线程或进程可以进行操作,以防止数据竞争和不一致性。
注意:临界区是指需要互斥访问的代码段,而临界资源是指需要被保护的共享资源。这就好比A和B一起去卫生间,但是卫生间只有一个坑位,也就是卫生间同一时间只允许一个人进入。在这个情境下,卫生间就属于临界区,而坑位则属于临界资源。
4、信号量的工作原理
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
(1)P操作(等待)
P 操作用于请求资源或进入临界区,它会检查信号量的值。如果信号量的值大于 0,则将其减 1,并允许线程或进程继续执行,表示资源已被占用。如果信号量的值为 0,则线程或进程将被阻塞,直到信号量的值变为大于 0 为止。
(2)V操作(释放)
V 操作用于释放资源或退出临界区,它会增加信号量的值。当线程或进程完成对资源的访问时,执行 V 操作将信号量的值加 1,表示资源已被释放。如果有其他线程或进程正在等待资源,则执行 V 操作后会唤醒其中一个等待的线程或进程,使其可以继续执行。
(3)PV 操作与信号量的关系
多个执行流为了访问临界资源会竞争式的申请信号量, 因此信号量是会被多个执行流同时访问的,也就是说信号量本质也是临界资源。
信号量本质就是用于保护临界资源的,我们不可能再用信号量去保护信号量,所以信号量的PV操作必须是原子操作。
5、信号量编程
在Linux系统中,我们如果想要使用信号量进行编程,需要用到下面四个函数,让我们一起来看看吧!
(1)ftok()函数
ftok()用来创建IPC键的函数,通常用于在多个进程间共享 IPC 对象时生成唯一的键值。无论是信号量、共享内存还是消息队列,都需要一个key_t类型的关键字ID值。该函数是将文件的索引节点号(可用ls -i进行查看)取出, 前面加上子序号即可得到key_t的返回值。
让我们一起来看看函数原型吧:
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
参数说明:
(1)第一个参数pathname:一个指向路径名的字符串,会使用该文件的文件索引号。【注意:指向的文件必须存在】
(2)第二个参数proj_id:用户指定的一个子序号,取值范围是1~255 。
(3)返回值:如果成功,返回一个与 pathname
和 proj_id
相关联的唯一 IPC 键。如果失败,则返回-1 。
(2)semget()函数
该函数用来创建一个信号集,或者获取已经存在的信号集。让我们一起来看看函数原型吧:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int num_sems, int sem_flags);
参数说明:
(1)第一个参数key:即ftok()的返回值。
(2)第二个参数num_sems:指定要创建的信号量集中信号量的数量,即信号量集中包含的信号量个数。通常为1 。
(3)第三个参数sem_flags:指定信号量集的权限标志和创建/打开标志,通常使用 IPC_CREAT。设置 IPC_CREAT后,即使给出的键是一个已有信号量的键,也不会产生错误。
(4)返回值:成功返回信号集的标识符,失败返回-1 。
(3)semctl()函数
该函数用来初始化信号集,或者删除信号集。让我们一起来看看函数的原型吧:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
参数说明:
(1)第一个参数semid:semget()返回的信号量集标识符。
(2)第二个参数semnum:一个整数值,用于指定要操作的信号量在信号量集中的索引(从0开始)。
(3)第三个参数cmd:一个整数值,用于指定要执行的操作类型,可以是以下之一:
GETVAL | 获取指定信号量的值 |
SETVAL | 设置指定信号量的值,此时需传入第四个参数 |
IPC_RMID | 删除指定的信号量集 |
(4)第四个参数是可选的,如果使用该参数,则其类型是semun,它是多个特定命令参数的联合(union),该联合不在任何系统头文件中定义,需要我们自己在代码中定义:
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
(5)返回值:根据操作类型的不同返回相应的一个正数。失败则返回-1 。
注意:使用 IPC_RMID
命令删除信号量集时,需要确保该信号量集中的所有资源都不再被使用,否则可能导致资源泄漏或其他错误。
(4)semop()函数
该函数操作一个或者一组信号,也可以叫做PV操作。让我们先来看看函数原型吧:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
参数说明:
(1) 第一个参数semid:semget()返回的信号量集标识符。
(2)第二个参数sops:其指向一个信号量操作数组。信号量操作由结构体sembuf结构表示如下:
struct sembuf {
short sem_num; // 信号量在信号量集中的索引
short sem_op; // 对信号量的操作值//操作为负则是P操作,操作为正则是V操作
short sem_flg; // 操作标志,通常为 IPC_NOWAIT 或 SEM_UNDO
};
【IPC_NOWAIT
表示非阻塞操作,或 SEM_UNDO
表示在进程结束时撤销未完成的操作 ,避免程序在异常情况下结束时未解锁锁定的资源,造成资源被永远锁定(死锁)】
(3)第三个参数nsops:信号操作结构的数量,恒大于或等于1 。
6、实战演练
题目:
编写一个程序,通过PV操作来实现父子进程同步运行。要求如下:在初始化信号量时将信号量初始值设为0,如果父进程先运行的话,将会调用semaphore_p(semid),这时因为资源为0所以父进程会阻塞。而之后子进程运行时会执行semaphore_v(semid)将资源+1,父进程之后就可以运行了。
代码如下:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define FTOK_PATH "/dev/zero"
#define FTOK_PROJID 0x22
union semun
{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
int semaphore_init(void);
int semaphore_p(int semid);
int semaphore_v(int semid);
void semaphore_term(int semid);
int main(int argc,char *argv[])
{
int semid;
pid_t pid;
int i;
if((semid = semaphore_init()) < 0)
{
printf("semaphore initial failure:%s\n",strerror(errno));
return -1;
}
if((pid = fork()) < 0)
{
printf("fork() failure:%s\n",strerror(errno));
return -2;
}
else if(0 == pid) /*child process*/
{
printf("Child process start running and do something now...\n");
sleep(3);
printf("Child process do something over...\n");
semaphore_v(semid);
sleep(1);
printf("Child process exit now\n");
exit(0);
}
/*Parent process*/
/*前面的semaphore_init()函数里将信号量的值设为0,如果这时候父进程先执行的话,p操作会阻塞。直到子进程执行V操作后,父进程的P操作才能返回继续执行*/
printf("Parent process P operator wait child process over\n");
semaphore_p(semid);
printf("Parent process start to run...\n");
sleep(1);
printf("Parent process do something over...\n");
printf("Parent process destroy samaphore and exit\n");
semaphore_term(semid);
sleep(2);
return 0;
}
int semaphore_init(void)
{
key_t key;
int semid;
union semun sem_union;
if((key = ftok(FTOK_PATH,FTOK_PROJID)) < 0)
{
printf("ftok() get IPC token failure:%s\n",strerror(errno));
return -1;
}
semid = semget(key,1,IPC_CREAT|0664);
if(semid < 0)
{
printf("semget() get semid failure:%s\n",strerror(errno));
return -2;
}
sem_union.val = 0;
if(semctl(semid,0,SETVAL,sem_union) < 0)
{
printf("semctl() set initial value failure:%s\n",strerror(errno));
return -3;
}
printf("Semaphore get key_t[0x%x] and semid[%d]\n",key,semid);
return semid;
}
int semaphore_p(int semid)
{
struct sembuf _sembuf;
_sembuf.sem_num = 0;
_sembuf.sem_op = -1;
_sembuf.sem_flg= SEM_UNDO;
if(semop(semid,&_sembuf,1) < 0)
{
printf("semop p operator failure:%s\n",strerror(errno));
return -1;
}
return 0;
}
int semaphore_v(int semid)
{
struct sembuf _sembuf;
_sembuf.sem_num = 0;
_sembuf.sem_op = 1;
_sembuf.sem_flg= SEM_UNDO;
if(semop(semid,&_sembuf,1) < 0)
{
printf("semop v operator failure:%s\n",strerror(errno));
return -1;
}
return 0;
}
void semaphore_term(int semid)
{
union semun sem_union;
if(semctl(semid,0,IPC_RMID,sem_union) < 0)
{
printf("semctl() delete semaphore ID failure:%s\n",strerror(errno));
}
return ;
}
运行结果:
三、共享内存(Shared Memory)
1、基本概念
共享内存本质上就是内存中的一块区域,用于进程间通信使用。该内存空间由操作系统分配与管理。与文件系统类似的是,操作系统在管理共享内存时,不仅仅有内存数据块,同时还会创建相应结构体来记录该共享内存属性,以便于管理。因此,共享内存不只有一份,可以根据需求申请多个。进程之间进行通信的时候,会获取 到共享内存的地址,写端进程写入数据,读端进程通过直接访问内存完成数据读取。
2、共享内存的优点
(1)高效性:共享内存是最快的进程间通信方式,一旦这样的内存映射到共享它的进程的地址空间,进程不再执行进入内核的系统调用来传递彼此的数据。
(2)灵活性:共享内存提供了灵活的数据共享方式,进程可以自由地读写共享内存中的数据,而无需进行额外的同步和通信操作。
(3)适用于大数据量:对于大数据量的数据共享,共享内存是一种比较合适的选择,因为它不需要将数据复制到其他进程的地址空间,节省了内存和 CPU 资源。
(4)支持随机访问:共享内存允许进程对内存区域进行随机访问,可以快速定位和访问需要的数据,适用于需要频繁访问的情况。
3、共享内存的缺点
(1)进程同步:由于多个进程可以同时访问共享内存,因此需要额外的同步机制来保证数据的一致性和正确性,如信号量、互斥锁等。
(2)数据一致性:共享内存中的数据可能会被多个进程同时修改,需要仔细考虑数据一致性和同步的问题,避免数据竞争和不一致性。
(3)安全性:共享内存的安全性受到进程权限控制的限制,如果不加以限制和保护,可能会导致安全漏洞或数据泄露的风险。
(4)可移植性:共享内存的实现和使用在不同的操作系统和平台上可能存在差异,需要考虑其可移植性和兼容性问题。
(5)复杂性:使用共享内存进行进程间通信可能会增加程序的复杂性,因为需要处理并发访问和同步的问题,编写和维护相应的同步代码较为复杂。
4、共享内存编程
在Linux系统下,如果想要使用共享内存进行编程,需要用到下面五个函数,其中ftok()已在上文讲过,所以下面着重讲解其他四个函数:
(1)shmget()函数
该函数用来创建共享内存,让我们先来看看函数原型吧:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数说明:
(1)第一个参数key:即ftok()的返回值。
(2)第二个参数size:指定了共享内存段的大小(以字节为单位)。
(3)第三个参数shmflg:一个整数值,用于指定共享内存段的创建方式和权限标志,通常使用 IPC_CREAT
。
(4)返回值:成功返回该共享内存段的标识码,失败则返回-1 。
(2)shmat()函数
该函数用于将共享内存段连接到调用进程的地址空间,使得进程可以访问共享内存中的数据。让我们一起来看看函数原型吧:
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明:
(1)第一个参数shmid:shmget()返回的共享内存标识符。
(2)第二个参数shmaddr:一个指向共享内存的地址,指定共享内存连接到当前进程中的地址位置,通常为 NULL
,表示由系统自动选择合适的地址。
(3)第三个参数shmflg:一个整数值,用于指定共享内存的连接方式和权限标志,通常为 0。
(4)返回值:成功时返回一个指向共享内存第一个字节的指针,失败则返回-1 。
【注意:连接共享内存后,应该谨慎地使用指向共享内存的指针,确保不会越界访问共享内存以避免导致未定义行为。】
(3)shmdt()函数
该函数用于将共享内存段从调用进程的地址空间中分离,即断开进程与共享内存段之间的连接,防止内存泄漏。让我们一起来看看函数原型吧:
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数说明:
(1) 参数shmaddr:一个指向共享内存段的指针,即连接到进程地址空间的共享内存的起始地址。也就是shmat()函数的返回值。
(2)返回值:调用成功返回0 ,失败则返回-1 。
【注意:该函数只是将连接到当前进程地址空间的共享内存段进行分离,使得进程无法再访问该共享内存段中的数据。但共享内存段本身不会被删除,其他仍连接的进程仍然可以访问共享内存。】
(4)shmctl()函数
该函数用于对共享内存段进行控制操作,包括获取共享内存段的状态信息、设置共享内存段的权限和删除共享内存段等。让我们一起来看看函数原型吧:
#include <sys/types.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数说明:
(1)第一个参数shmid:shmget()返回的共享内存标识符。
(2)第二个参数cmd:一个整数值,用于指定要执行的操作类型,可以是以下之一:
IPC_STAT | 获取共享内存段的状态信息,将其存储在 buf 参数中。 |
IPC_SET | 设置共享内存段的权限和状态信息,通过 buf 参数传递要设置的值。 |
IPC_RMID | 删除指定的共享内存段。 |
(3)第三个参数buf:一个指向 shmid_ds
结构体的指针,用于存储获取到的共享内存段的状态信息,或者传递要设置的共享内存段的权限和状态信息。结构体定义如下:
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
}
(4)返回值:如果成功,根据操作类型的不同返回相应的值。如果失败则返回-1 。
【注意:删除共享内存段时,需要确保所有连接到该共享内存段的进程都已经分离并且不再需要该共享内存段,否则可能导致数据丢失或内存泄漏。】
5、实战演练
题目:
编写一个程序,让两个进程使用共享内存方式共享一个结构体变量。要求如下:
(1)编写一个程序(write.c)用来创建一个student结构体共享内存并更新里面的成员内容。
(2)编写一个程序(read.c)在另一个毫无关系的进程中同步访问该结构体里的内容。
代码如下:
write.c
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define FTOK_PATH "/dev/zero"
#define FTOK_PROJID 0x22
typedef struct st_student
{
char name[64];
int age;
}t_student;
int main(int argc,char *argv[])
{
key_t key;
int shmid;
int i;
t_student *student;
if((key = ftok(FTOK_PATH,FTOK_PROJID)) < 0)
{
printf("ftok() get IPC token failure:%s\n",strerror(errno));
return -1;
}
shmid = shmget(key,sizeof(t_student),IPC_CREAT|0666);
if(shmid < 0)
{
printf("shmget() create shared memory failure:%s\n",strerror(errno));
return -2;
}
student = shmat(shmid,NULL,0);
if((void *)-1 == student)
{
printf("shmat() alloc shared memory failure:%s\n",strerror(errno));
return -2;
}
strncpy(student->name,"xinhongbo",sizeof(student->name));
student->age = 18;
for(i=0 ; i<4 ; i++)
{
student->age ++;
printf("Student '%s' age [%d]\n",student->name,student->age);
sleep(1);
}
shmdt(student);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
read.c
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define FTOK_PATH "/dev/zero"
#define FTOK_PROJID 0x22
typedef struct st_student
{
char name[64];
int age;
}t_student;
int main(int argc,char *argv[])
{
key_t key;
int shmid;
int i;
t_student *student;
if((key = ftok(FTOK_PATH,FTOK_PROJID)) < 0)
{
printf("ftok() get IPC token failure:%s\n",strerror(errno));
return -1;
}
shmid = shmget(key,sizeof(t_student),IPC_CREAT|0666);
if(shmid < 0)
{
printf("shmget() create shared memroy failure:%s\n",strerror(errno));
return -2;
}
student = shmat(shmid,NULL,0);
if((void *)-1 == student)
{
printf("shmat() alloc shared memroy failure:%s\n",strerror(errno));
return -2;
}
for(i=0 ; i<4 ; i++)
{
printf("Student '%s' age [%d]\n",student->name,student->age);
sleep(1);
}
shmdt(student);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
运行结果:
四、消息队列(Message Queue)
1、基本概念
消息队列一般简称为 MQ ,是指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成,是在消息的传输过程中保存消息的容器。消息队列本质上是一个队列,而队列中存放的是一个个消息。
消息队列是一种在多个进程间进行通信的机制,它是一种先进先出(FIFO)的数据结构,用于在不同的进程之间传递消息。消息队列通常由操作系统内核维护,提供了一种可靠的、异步的进程间通信方式。
MQ传递的是消息,消息即是我们需要需要在进程间传递的数据。MQ采用链表来实现消息队列,该链表是由系统内核来维护。每个MQ用消息队列描述符(消息队列ID:qid)来区分,qid是唯一的,用来区分不同的MQ。在进行进程间通信时,一个进程将消息加到MQ尾端,另一个进程从消息队列中取消息(不一定以先进先出来取消息),这样就实现了进程间的通信。
2、特点和用途
(1)异步通信: 消息队列允许发送者和接收者之间进行异步通信,即发送者发送消息后可以立即继续执行其他任务,而无需等待接收者的响应。
(2)可靠性: 消息队列通常由操作系统内核维护,确保消息传递的可靠性和正确性。即使发送者和接收者不在同一时间活动,消息也能够安全地传递和存储,直到接收者准备好接收。
(3)缓冲: 消息队列通常具有一定的缓冲能力,允许发送者发送消息的速率大于接收者接收消息的速率,从而平衡系统的吞吐量。
(4)解耦: 使用消息队列可以将发送者和接收者解耦,使它们之间的通信变得灵活和独立。发送者只需将消息发送到队列,而无需知道具体的接收者是谁,接收者也只需从队列中获取消息,而无需知道消息的来源。
(5)多对多通信: 消息队列支持多对多的通信模式,即多个发送者可以向同一个队列发送消息,多个接收者也可以从同一个队列接收消息。
3、消息队列编程
在Linux系统下,我们如果想要使用消息队列进行编程,需要用到如下的五个函数。其中ftok()函数已在上文讲过,这里着重讲解其余四个函数。
(1)msgget()函数
函数是用于创建或打开一个消息队列、创建消息队列ID的函数。消息队列是一种在多个进程之间进行通信的机制,它提供了一个先进先出(FIFO)的缓冲区,用于在不同的进程之间传递消息。让我们一起来看看函数原型吧:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数说明:
(1)第一个参数key: 即ftok()的返回值。
(2)第二个参数msgflg:一个整数值,用于指定消息队列的创建方式和权限标志,通常使用 IPC_CREAT
。不存在则根据 key
参数创建一个新的消息队列。,存在则返回其标识符。
(3)返回值:如果成功返回一个非负整数的消息队列标识符,如果失败则返回-1 。
(2)msgsnd()函数
函数用于向消息队列发送消息,前提是要有写消息队列的权限。让我们先来看看函数的原型吧:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数说明:
(1)第一个参数msqid:由 msgget()
返回的消息队列标识符。
(2)第二个参数msgp:一个指向消息缓冲区的指针,包含要发送的消息内容。消息结构在两方面受到制约。首先,它必须小于系统规定的上限值;其次,它必须以一个long int长整数开始,接收者函数将利用这个长整数确定消息的类型,其参考类型定义形式如下:
typedef struct s_msgbuf
{
long mtype;char mtext[512];
}t_msgbuf;
(3)第三个参数msgsz:指定了要发送消息的大小(以字节为单位)。
(4)第四个参数msgflg: 控制着当前消息队列满或到达系统上限时要发生的事情,设置为IPC_NOWAIT表示队列满不等待,返回EAGAIN错误。
(5)返回值:成功返回0,失败返回-1 。
【注意:如果发送的消息大小超过了消息队列的限制,将会导致发送失败并返回错误。】
(3)msgrcv()函数
该函数用于从消息队列接收消息。让我们先来看看函数的原型吧:
#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);
参数说明:
(1)第一个参数 msqid:由 msgget()
返回的消息队列标识符。
(2)第二个参数msgp:一个指向消息缓冲区的指针,指向准备接受的消息,用于存储接收到的消息内容。
(3)第三个参数msgsz:指定了消息缓冲区的长度,即可接收的最大消息大小。(这个长度不包含保存消息类型的那个long int长整型)
(4)第四个参数msgtyp:一个长整型值,用于指定要接收的消息类型。
msgtype=0 | 返回队列第一条信息 |
msgtype>0 | 返回队列第一条类型等于msgtype的消息 |
msgtype<0 | 返回队列第一条类型小于等于msgtype绝对值的消息 |
(5)第五个参数msgflg:控制着队列中没有相应类型的消息可供接收时将要发生的事。
msgflg=IPC_NOWAIT | 队列中没有可读消息不等待,返回ENOMSG错误 |
msgflg=MSG_NOERROR | 消息大小超过msgsz时被截断 |
msgtype>0且msgflg=MSG_EXCEPT | 接收类型不等于msgtype的第一条消息 |
(6)返回值:如果成功,返回接收到的消息的字节数。如果失败则返回-1 。
(4)msgctl()函数
该函数用于对消息队列执行控制操作,包括获取消息队列的状态信息、设置消息队列的权限和删除消息队列等。让我们先来看看函数原型吧:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数说明:
(1)第一个参数 msqid:由 msgget()
返回的消息队列标识符。
(2)第二个参数cmd:用于指定要执行的操作类型。
IPC_STAT | 把msqid_ds结构体中的数据设置为消息队列的当前关联值 |
IPC_SET | 如果进程有足够的权限,就把消息队列的当前关联值设置为msqid_ds结构中给出的值 |
IPC_RMID | 删除消息队列 |
(3)第三个参数buf:一个指向 msqid_ds
结构体的指针,用于存储获取到的消息队列的状态信息,或者传递要设置的消息队列的权限和状态信息。
(4)返回值:如果成功,根据操作类型的不同返回相应的值。如果出错,返回 -1 。
4、实战演练
题目:
编写一个程序,实现不同进程通过消息队列来实现收发消息。要求如下:
(1)编写一个程序,往内核的消息队列里写入内容“ping”。
(2)编写一个程序,从消息队列里读出并打印该消息。
send.c
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define FTOK_PATH "/dev/zero"
#define FTOK_PROJID 0x22
typedef struct s_msgbuf
{
long mtype;
char mtext[512];
}t_msgbuf;
int main(int argc,char *argv[])
{
key_t key;
int msgid;
t_msgbuf msgbuf;
int msgtype;
int i;
if((key = ftok(FTOK_PATH,FTOK_PROJID)) < 0)
{
printf("ftok() get IPC token failure:%s\n",strerror(errno));
return -1;
}
msgid = msgget(key,IPC_CREAT|0666);
if(msgid < 0)
{
printf("shmget() create share memroy failure:%s\n",strerror(errno));
return -2;
}
msgtype = (int)key;
printf("key[%d] msgid[%d] msytype[%d]\n",(int)key,msgid,msgtype);
for(i=0 ; i<4 ; i++)
{
msgbuf.mtype = msgtype;
strcpy(msgbuf.mtext,"Ping");
if(msgsnd(msgid,&msgbuf,sizeof(msgbuf.mtext),IPC_NOWAIT) < 0)
{
printf("msgsnd() send message failure:%s\n",strerror(errno));
break;
}
printf("Send message:%s\n",msgbuf.mtext);
sleep(2);
}
msgctl(msgid,IPC_RMID,NULL);
return 0;
}
recver.c
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define FTOK_PATH "/dev/zero"
#define FTOK_PROJID 0x22
typedef struct s_msgbuf
{
long mtype;
char mtext[512];
}t_msgbuf;
int main(int argc,char *argv[])
{
key_t key;
int msgid;
t_msgbuf msgbuf;
int msgtype;
int i;
if((key = ftok(FTOK_PATH,FTOK_PROJID)) < 0)
{
printf("ftok() get IPC token failure:%s\n",strerror(errno));
return -1;
}
msgid = msgget(key,IPC_CREAT|0666);
if(msgid < 0)
{
printf("shmget() create shared memroy failure:%s\n",strerror(errno));
return -2;
}
msgtype = (int)key;
printf("key[%d] msgid[%d] msgtype[%d]\n",(int)key,msgid,msgtype);
for(i=0 ; i<4 ; i++)
{
memset(&msgbuf,0,sizeof(msgbuf));
if(msgrcv(msgid,&msgbuf,sizeof(msgbuf.mtext),msgtype,IPC_NOWAIT) < 0)
{
printf("msgsnd() receive message failure:%s\n",strerror(errno));
break;
}
printf("Recive Message:%s\n",msgbuf.mtext);
sleep(2);
}
msgctl(msgid,IPC_RMID,NULL);
return 0;
}
运行结果:
五、学习心得
学习进程间通信(IPC)是操作系统和并发编程中的重要主题之一,它涉及多种技术和机制,包括信号、管道、命名管道、命名socket、消息队列、共享内存、信号量等。下面是我对学习进程间通信的一些心得体会:
1. 理解通信需求:
在学习进程间通信之前,首先要理解为什么需要进程间通信以及不同场景下的通信需求。通信需求可能涉及数据交换、资源共享、同步和协作等方面。
2. 掌握通信方式:
学习进程间通信需要掌握不同的通信方式和机制,包括信号、管道、命名管道、命名socket、消息队列、共享内存、信号量等。每种通信方式都有其特点和适用场景,理解它们的原理和使用方法对解决特定问题非常重要。
3. 深入理解进程和线程:
进程间通信通常是在多个进程之间进行的,因此深入理解进程的概念、进程的生命周期以及进程间的关系对理解通信机制非常有帮助。同时,对于多线程编程也应有一定的了解,因为线程间通信也是一种常见的通信方式。
4. 注意通信原理:
学习进程间通信时要注意理解通信的原理和底层实现机制,例如操作系统是如何管理进程和线程、如何实现进程间数据传输和同步等。深入理解这些原理有助于更好地应用和调优通信机制。
5. 实践和应用:
学习进程间通信不仅需要理论知识,更需要实践和应用。通过编写实际的程序来使用不同的通信方式,解决实际的问题,才能更好地掌握和理解进程间通信的技术和技巧。
6.注意安全和稳定性:
在实际应用中,要注意进程间通信的安全性和稳定性。例如要处理好并发访问和竞争条件,避免死锁等问题,确保通信过程的安全可靠。
7. 持续学习和探索:
进程间通信是一个广阔而复杂的领域,随着技术的发展和需求的变化,不断涌现出新的通信方式和机制。因此,要保持持续学习和探索的态度,关注最新的进展和技术,不断丰富和提升自己的知识和技能。
总的来说,学习进程间通信是一项极具挑战性但也非常有意义的任务。通过不断学习、实践和探索,可以更好地理解和应用进程间通信的技术,为解决实际问题提供更好的解决方案。