APUE学习之进程间通信(IPC)(下篇)
目录
一、进程间通信(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 0x22union 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 0x22typedef 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 0x22typedef 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 0x22typedef 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 0x22typedef 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. 持续学习和探索:
进程间通信是一个广阔而复杂的领域,随着技术的发展和需求的变化,不断涌现出新的通信方式和机制。因此,要保持持续学习和探索的态度,关注最新的进展和技术,不断丰富和提升自己的知识和技能。
总的来说,学习进程间通信是一项极具挑战性但也非常有意义的任务。通过不断学习、实践和探索,可以更好地理解和应用进程间通信的技术,为解决实际问题提供更好的解决方案。
相关文章:
APUE学习之进程间通信(IPC)(下篇)
目录 一、进程间通信(IPC) 二、信号量(Semaphore) 1、基本概念 2、同步关系与互斥关系 3、临界区与临界资源 4、信号量的工作原理 5、信号量编程 6、实战演练 三、共享内存(Shared Memory) 1、…...
【Java 设计模式】行为型之中介者模式
文章目录 1. 定义2. 应用场景3. 代码实现结语 中介者模式(Mediator Pattern)是一种行为型设计模式,用于通过一个中介对象来集中管理多个对象之间的交互关系,从而降低对象之间的耦合度。中介者模式通过将对象之间的通信委托给中介者…...
MySql 慢SQL配置,查询,处理
一.慢SQL配置相关 1.查看慢SQL是否开启 执行下面命令查看是否开启慢SQL show variables like %slow_query_log; 复制代码 OFF: 未开启ON: 2.打开慢SQL配置 执行下面的命令开启慢查询日志 set global slow_query_logON; 复制代码 3.修改慢查询阈值 前面介绍了SQL执行到达了…...
算法:分界线
一、算法描述 电视剧《分界线》里面有一个片段,男主为了向警察透露案件细节,且不暴露自己,于是将报刊上的字 剪切下来,剪拼成匿名信。 现在有一名举报人,希望借鉴这种手段,使用英文报刊完成举报操作。 但为…...
STM32单片机基本原理与应用(四)
直流电机驱动控制原理 1、电机正反转控制 在STM32中,直流电机的正反转控制主要通过改变电机输入电源的极性来实现。当电机的电压极性发生变化时,电机的旋转方向也会相应改变。在硬件电路中,可以通过继电器或晶体管等电子开关来切换电机的电源…...
elk之安装和简单配置
写在前面 本文看下elk的安装和简单配置,安装我们会尝试通过不同的方式来完成,也会介绍如何使用docker,docker-compose安装。 1:安装es 1.1:安装单实例 下载es安装包 在这里 下载,下载后解压到某个目录…...
springboot(ssm环保网站 绿色环保宣传系统Java系统
springboot(ssm环保网站 绿色环保宣传系统Java系统 开发语言:Java 框架:springboot(可改ssm) vue JDK版本:JDK1.8(或11) 服务器:tomcat 数据库:mysql 5.7࿰…...
【MBtiles数据索引和服务发布】GeoServer改造Springboot番外系列二
xyz地图服务访问示例:http://192.168.1.240:8081/gmserver/raster/xyz/firstWP:Imagery-raster/{z}/{x}/{y}.jpg 访问示例如下: mbtiles目录结构 根据z,x,y获取对应mbtiles文件路径的工具方法 说明:重点是使用getMb…...
Redis抓取数据到Logstash再推到Elasticsearch集群
一、安装Logstash 前面安装过Logstash了,不做解释直接跳过 参考:上一篇文章 二、配置Logstash 在logstash目录下,编辑我们之前的配置文件logstash.conf vim logstash.confinput、output字面意思,从redis去拿取数据,输出到Elasticsearch data_type:数据类型为list k…...
【代码随想录-链表】反转链表
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学习,不断总结,共同进步,活到老学到老导航 檀越剑指大厂系列:全面总结 jav…...
32GPIO输入LED闪烁蜂鸣器
一.GPIO简介 所有的GPIO都挂载到APB2上,每个GPIO有16个引脚 内核可以通过APB2对寄存器进行读写,寄存器都是32位的,但每个引脚端口只有16位 驱动器用于增加信号的驱动能力 二.具体…...
Qt|QPushButton控件讲解
前提 按钮分为了四种状态:常态、聚焦、按下、禁用 前一段时间更新了MFC框架下CButton的自绘。因为MFC框架下的按钮限制性很高,所以只能由自绘实现各种风格,但是QT框架完美的解决了这个问题,我们只需要了解如何调用,就…...
再学webpack
1 优化 webpack 打包体积的思路 优化 webpack 打包体积的思路包括: 提取第三方库或通过引用外部文件的方式引入第三方库:将第三方库单独打包,并通过 CDN 引入,减少打包体积。使用代码压缩插件:例如 UglifyJsPlugin&a…...
systemd:service与target使用及相关命令
文章目录 一、 unit1.1 unit常用命令 二、 service系统服务2.1 unit service配置文件2.1.1 [Unit]区块2.1.2 [Service]区块2.1.3 [Install]区块2.1.4 示例介绍 2.2 service常用命令 三、target3.1 tartget有关命令 四、其他系统命令4.1 systemctl 相关系统管理命令4.2 journalc…...
FairGuard游戏加固入选《CCSIP 2023中国网络安全行业全景册(第六版)》
2024年1月24日, FreeBuf咨询正式发布《CCSIP 2023中国网络安全行业全景册(第六版)》。本次发布的全景图,共计展示20个一级分类、108个细分安全领域,旨在为广大企业提供网络安全产品选型参考,帮助企业了解中国网络安全技术与市场的…...
文心一言 VS ChatGPT :谁是更好的选择?
前言 目前各种大模型、人工智能相关内容覆盖了朋友圈已经各种媒体平台,对于Ai目前来看只能说各有千秋。GPT的算法迭代是最先进的,但是它毕竟属于国外产品,有着网络限制、注册限制、会员费高昂等弊端,难以让国内用户享受。文心一言…...
七街八巷×实在RPA丨财务凭证录入零出错,效率提升8倍
在如今的数字化时代,企业财务面临着海量且复杂的数据流程。特别是在凭证录入这一看似简单却又频繁的环节中,传统的手工操作已无法满足日益增长的业务需求。 中国轻食知名品牌七街八巷,通过部署实在智能集“自动化平台开箱即用解决方案咨询”…...
线性代数----------学习记录
线性代数发展历程 (1)线性方程组:例如二元一次方程组; (2)行列式:determinant,克莱默,莱布尼兹; (3)矩阵:方程个数与未知数的个数可…...
Ubuntu如何安装使用Nginx反向代理?
在Ubuntu上安装Nginx并配置使其生效是相对简单的过程。以下是一步一步的指南: 步骤 1:安装 Nginx 打开终端,并执行以下命令来安装 Nginx: sudo apt update sudo apt install nginx步骤 2:启动 Nginx 服务 安装完成…...
Linux系统——正则表达式
有一段时间本机访问量过高,如何查看日志提取出访问量前十的信息 1.使用提取命令(cut、awk、sed)提取出ip地址的那一列 2.使用sort按数字排序,将相同的地址整合到一起 3.使用uniq -c统计出数量 4.使用sort 数字 数字倒序排序 5.最…...
网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...
Vue记事本应用实现教程
文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展:显示创建时间8. 功能扩展:记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...
从零实现富文本编辑器#5-编辑器选区模型的状态结构表达
先前我们总结了浏览器选区模型的交互策略,并且实现了基本的选区操作,还调研了自绘选区的实现。那么相对的,我们还需要设计编辑器的选区表达,也可以称为模型选区。编辑器中应用变更时的操作范围,就是以模型选区为基准来…...
ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放
简介 前面两期文章我们介绍了I2S的读取和写入,一个是通过INMP441麦克风模块采集音频,一个是通过PCM5102A模块播放音频,那如果我们将两者结合起来,将麦克风采集到的音频通过PCM5102A播放,是不是就可以做一个扩音器了呢…...
第一篇:Agent2Agent (A2A) 协议——协作式人工智能的黎明
AI 领域的快速发展正在催生一个新时代,智能代理(agents)不再是孤立的个体,而是能够像一个数字团队一样协作。然而,当前 AI 生态系统的碎片化阻碍了这一愿景的实现,导致了“AI 巴别塔问题”——不同代理之间…...
【python异步多线程】异步多线程爬虫代码示例
claude生成的python多线程、异步代码示例,模拟20个网页的爬取,每个网页假设要0.5-2秒完成。 代码 Python多线程爬虫教程 核心概念 多线程:允许程序同时执行多个任务,提高IO密集型任务(如网络请求)的效率…...
NLP学习路线图(二十三):长短期记忆网络(LSTM)
在自然语言处理(NLP)领域,我们时刻面临着处理序列数据的核心挑战。无论是理解句子的结构、分析文本的情感,还是实现语言的翻译,都需要模型能够捕捉词语之间依时序产生的复杂依赖关系。传统的神经网络结构在处理这种序列依赖时显得力不从心,而循环神经网络(RNN) 曾被视为…...
华硕a豆14 Air香氛版,美学与科技的馨香融合
在快节奏的现代生活中,我们渴望一个能激发创想、愉悦感官的工作与生活伙伴,它不仅是冰冷的科技工具,更能触动我们内心深处的细腻情感。正是在这样的期许下,华硕a豆14 Air香氛版翩然而至,它以一种前所未有的方式&#x…...
视觉slam十四讲实践部分记录——ch2、ch3
ch2 一、使用g++编译.cpp为可执行文件并运行(P30) g++ helloSLAM.cpp ./a.out运行 二、使用cmake编译 mkdir build cd build cmake .. makeCMakeCache.txt 文件仍然指向旧的目录。这表明在源代码目录中可能还存在旧的 CMakeCache.txt 文件,或者在构建过程中仍然引用了旧的路…...
【Go语言基础【12】】指针:声明、取地址、解引用
文章目录 零、概述:指针 vs. 引用(类比其他语言)一、指针基础概念二、指针声明与初始化三、指针操作符1. &:取地址(拿到内存地址)2. *:解引用(拿到值) 四、空指针&am…...
