RK3588是一款低功耗、高性能的处理器,适用于基于arm的PC和Edge计算设备、个人移动互联网设备等数字多媒体应用,RK3588支持8K视频编解码,内置GPU可以完全兼容OpenGLES 1.1、2.0和3.2。RK3588引入了新一代完全基于硬件的最大4800万像素ISP,内置NPU,支持INT4/INT8/INT16/FP16混合运算能力,支持安卓12和、Debian11、Build root、Ubuntu20和22版本登系统。了解更多信息可点击迅为官网
【粉丝群】824412014
【实验平台】:迅为RK3588开发板
【内容来源】《iTOP-3588开发板系统编程手册》
【全套资料及网盘获取方式】联系淘宝客服加入售后技术支持群内下载
【视频介绍】:【强者之芯】 新一代AIOT高端应用芯片 iTOP -3588人工智能工业AI主板
第8章 进程间通信
在之前的小节中已经讲解过了信号间通信机制中的信号,用来进程间传递简单的信息和通知,那如果要在进程与进程之间进行数据的传递需要怎样呢?信号明显已经不能满足当前的需求了,理所当然的其他进程间通信机制就需要出场了!
8.1 进程间通信概述
学习前提出的问题
1.什么是进程间通信?
2.为什么要学习进程间通信机制?
3.进程间通信机制都有哪些,分别有什么区别?
进程间通信(IPC, Inter-Process Communication)是指在操作系统中,不同进程之间交换数据、信息和命令的过程。在一个多任务的操作系统中,多个进程可以同时运行,但是这些进程是相互独立的,它们有自己的地址空间和上下文,无法直接访问对方的内存空间。如果多个进程需要协作来完成某项任务,或者需要共享某些数据,就需要使用进程间通信机制来进行通信和协作。
下面对进程间通信的作用进行说明:
(1)实现进程间数据共享:进程间通信可以使不同进程之间共享数据,避免了数据复制的开销,提高了系统的性能。共享内存和消息队列是实现进程间数据共享的常用方法。
(2)提高系统的可靠性:进程间通信可以将多个进程组织成一个整体,使得它们可以协同工作,提高了系统的可靠性。例如,多个进程可以共同访问同一个文件,避免了数据的竞争和冲突。
(3)实现进程间协作:进程间通信可以使进程之间相互协作,共同完成任务。例如,一个进程可以向另一个进程发送请求,请求另一个进程提供某些服务,如打印文件等。管道、消息队列、信号量、共享内存等机制都可以用来实现进程间协作。
(4)提高系统的安全性:进程间通信可以实现不同进程之间的数据隔离和保护,从而提高了系统的安全性。例如,共享内存和消息队列可以通过权限控制来保护数据的安全。
进程间通信是实现多个进程之间协同工作的必要手段,可以提高系统的性能、可靠性和安全性,同时也扩展了操作系统的功能和应用范围。根据IPC机制所依赖的资源类型可以划分为基于系统资源的IPC机制和基于文件系统的IPC机制。
基于文件系统的IPC机制主要是通过操作文件系统中的文件和管道来进行进程间的通信,这些通信机制依赖于文件系统提供的特定功能,如普通文件、命名管道、套接字文件等。
而基于系统资源的进程间通信机制主要是利用系统内核中提供的一些共享资源来实现进程间的通信,这些共享资源包括共享内存、消息队列、信号量等。这些通信机制依赖于操作系统内核所提供的特定功能,如内存管理、进程调度、信号处理等。接下来对这些机制进行一一介绍。
(1)管道(Pipe):管道是一种基于文件描述符的通信机制,可以实现进程间的单向通信,分为无名管道和有名管道(有时候也被叫命名管道)两种。无名管道是在进程创建时自动创建的,只能在具有亲缘关系的进程间使用;有名管道则是由系统中的一个特殊文件来实现的,可以在任意进程之间进行通信。
(2)消息队列(Message Queue):消息队列是一种基于内核对象的通信机制,可以实现进程间的异步通信。消息队列中包含多个消息,每个消息都有一个特定的类型和长度,进程可以通过指定消息类型来选择接收特定类型的消息。
(3)共享内存(Shared Memory):共享内存是一种基于内核对象的通信机制,可以让多个进程共享同一块物理内存。多个 进程可以同时访问这块内存,从而实现快速、高效的数据共享。共享内存需要考虑数据同步和互斥等问题,以保证数据的一致性和正确性。
(4)信号量(Semaphore):信号量是一种基于内核对象的通信机制,用于实现进程间的同步和互斥。信号量可以用于实现多个进程之间的资源共享和互斥,通过对信号量进行加锁和解锁操作,可以控制多个进程之间的访问顺序和数量。
(5)Socket:Socket是一种基于网络的通信机制,可以实现不同计算机之间的进程间通信。Socket可以实现进程间的数据传输和通信,可以通过网络协议(如TCP、UDP等)来进行数据传输和路由。
最后对上述IPC机制进行总结,具体内容如下表所示:
机制名称 | 机制类型 | 实现原理 | 数据共享 | 进程关系 | 通信方式 |
无名管道 | 基于文件描述符 | 单向通信 | 否 | 有血缘关系的进程 | 无名管道 |
命名管道 | 基于文件描述符 | 单向通信 | 否 | 无关进程 | 有名管道 |
消息队列 | 基于内核对象 | 异步通信 | 是 | 无关进程 | 先进先出 |
共享内存 | 基于内核对象 | 共享同一块物理内存 | 是 | 无关进程 | 直接读写 |
信号量 | 基于内核对象 | 同步和互斥 | 是 | 无关进程 | 加锁和解锁 |
Socket | 基于网络 | 跨网络实现进程间通信 | 是 | 无关进程 | 客户端和服务器端 |
在接下来的小节中将会对出Socket之外的信号间通信机制进行讲解和实际例程实验,需要注意的是信号量、消息队列和共享内存分为System V和POSIX两个标准的API,本章节首先会讲解System V 标准的API,POSIX标准的API则根据需要后续再进行补充讲解。
8.2 无名管道
本小节代码在配套资料“iTOP-3588开发板\03_【iTOP-RK3588开发板】指南教程\03_系统编程配套程序\40/41”目录下,如下图所示:
无名管道是一种单向的、字节流的通信管道,可以在进程之间传递数据。由于是匿名管道,所以无法通过文件系统来访问它,只能通过文件描述符在具有亲缘关系的进程之间使用,如父子进程、兄弟进程等。
无名管道的特点
(1)单向传输:无名管道是单向的,数据只能从管道的一端进入,从另一端出去。
(2)字节流传输:管道中的数据是以字节流的形式传输的,没有消息边界。
(3)亲缘关系:只有具有亲缘关系的进程才能使用无名管道进行通信。
(4)阻塞式读写:管道的读写操作是阻塞式的,即如果管道为空(写端没有写入数据),读操作会一直阻塞等待,直到有数据写入为止。
无名管道的创建需要使用系统调用pipe(),该系统调用所需头文件和函数原型如下所示:
所需头文件 | 函数原型 | |
1 | #include <unistd.h> | int pipe(int pipefd[2]); |
该函数有一个int 类型的参数pipefd,是一个有两个元素的数组,分别代表管道的读端和写端。其中,pipefd[0]代表管道的读端,pipefd[1]代表管道的写端。调用pipe()函数后,系统会自动创建一个无名管道,并将读端和写端返回给调用进程。如果成功创建管道,返回0;否则返回-1,表示创建失败。
无名管道实际上是一种特殊的文件,它存在于内存中,当一个进程创建了一个无名管道之后,它实际上创建了两个文件描述符,一个用于读取数据,另一个用于写入数据。在进程调用fork()函数之后,子进程会继承父进程的文件描述符,因此可以通过这两个文件描述符实现进程间通信。使用无名管道进行进程间通信的一般步骤如下:
(1)使用pipe()函数创建无名管道。
(2)使用fork()函数创建子进程,父子进程共享管道的读写端。
(3)在父进程中关闭管道的读端,向管道的写端写入数据。
(4)在子进程中关闭管道的写端,从管道的读端读取数据。
(5)在合适的时机关闭管道的读端和写端,以防止发生资源泄露的情况。
接下来编写demo40_pipe.c程序代码,对无名管道的单向通信进行举例,具体代码如下所示:
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
int fd[2]; // 创建一个长度为2的整型数组,用于存储管道两端的文件描述符
char buffer[22]; // 创建一个长度为20的字符数组,用于存储从管道中读取的数据
pid_t pid; // 创建一个pid_t类型的变量,用于存储fork()函数的返回值
// 创建一个管道,如果失败,则输出错误信息并退出程序
if (pipe(fd) < 0)
{
perror("pipe");
exit(-1);
}
// 创建一个新的进程,如果失败,则输出错误信息并退出程序
pid = fork();
if (pid < 0)
{
perror("fork");
exit(-1);
}
else if (pid > 0)
{ // 父进程
close(fd[0]); // 关闭管道读端
printf("this is father process,pid = %d\n", getpid());
write(fd[1], "Hello, child process\n", 22); // 向管道写入数据
close(fd[1]); // 关闭管道写端
}
else
{ // 子进程
close(fd[1]); // 关闭管道写端
printf("this is child process,pid = %d\n", getpid());
read(fd[0], buffer, 22); // 从管道读取数据
printf("Message from parent process: %s\n", buffer); // 输出从管道中读取的数据
close(fd[0]); // 关闭管道读端
}
return 0;
}
在上面的代码中,子进程从管道中读取数据并将其输出。父进程向管道中进行数据的写入。由于管道是单向的,因此需要close()函数来关闭不需要使用的端口。保存退出之后,使用以下命令对程序进行编译,编译完成如下图所示:
gcc -o demo40_pipe demo40_pipe.c
然后使用命令“./demo40_pipe”运行该可执行程序,如下图所示:
父进程会打印自己的进程ID,然后向管道写入“Hello, child process\n”字符串,然后在子进程之中会接收父进程向管道的数据。由于管道具有单向传输的特点,所以在上述程序中只能由父进程写入数据到管道,由子进程读取管道中的数据,示意图如下所示:
那如何通过无名管道进行双向通信呢,显然一个管道已经不能满足我们的需求了,这是就需要用到两条管道了,一条管道的方向是由父进程指向子进程,另一条管道的方向由子进程指向父进程。示意图如下所示:
根据上图和所学到知识进行来编写demo41_pipe2.c程序代码,对无名管道的双向通信进行举例,具体代码如下所示:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
int fd1[2]; // 管道1
int fd2[2]; // 管道2
char buffer[20]; // 缓冲区
pid_t pid; // 进程ID
// 创建管道1,如果失败,则输出错误信息并退出程序
if (pipe(fd1) < 0)
{
perror("pipe 1");
exit(-1);
}
// 创建管道2,如果失败,则输出错误信息并退出程序
if (pipe(fd2) < 0)
{
perror("pipe 2");
exit(-1);
}
// 创建子进程,如果失败,则输出错误信息并退出程序
pid = fork();
if (pid < 0)
{
perror("fork");
exit(-1);
}
else if (pid > 0)
{ // 父进程
close(fd1[0]); // 关闭管道1的读端
close(fd2[1]); // 关闭管道2的写端
printf("this is father process,pid = %d\n", getpid());
// 父进程向子进程发送数据
write(fd1[1], "Hello, child process\n", 22);
read(fd2[0], buffer, 20);
printf("Message from child process: %s\n", buffer);
close(fd1[1]); // 关闭管道1的写端
close(fd2[0]); // 关闭管道2的读端
}
else
{ // 子进程
close(fd1[1]); // 关闭管道1的写端
close(fd2[0]); // 关闭管道2的读端
printf("this is child process,pid = %d\n", getpid());
// 子进程向父进程发送数据
read(fd1[0], buffer, 20);
printf("Message from parent process: %s\n", buffer);
write(fd2[1], "Hello, parent process\n", 22);
close(fd1[0]); // 关闭管道1的读端
close(fd2[1]); // 关闭管道2的写端
}
return 0;
}
在上面的代码中,父进程向子进程发送数据的管道是fd1,子进程向父进程发送数据的管道是fd2。在父进程中,先向fd1写入数据,然后再从fd2读取数据;在子进程中,先从fd1读取数据,然后再向fd2写入数据。。保存退出之后,使用以下命令对程序进行编译,编译完成如下图所示:
gcc -o demo41_pipe2 demo41_pipe2.c
然后使用命令“./demo41_pipe2”运行该可执行程序,如下图所示:
可以看到父进程和子进程双向通信的数据就都被打印了出来。至此,关于无名管道的讲解就完成了。
8.3 有名管道
本小节代码在配套资料“iTOP-3588开发板\03_【iTOP-RK3588开发板】指南教程\03_系统编程配套程序\42”目录下,如下图所示:
上一小节讲解的无名管道只能在有血缘关系的进程间使用,那怎样在普通进程之间进行数据传输呢,本小节的有名管道将会带给你答案。
有名管道是一种特殊类型的Unix/Linux文件,也被称为FIFO(First-In-First-Out)管道,用来在进程之间传输数据的,与匿名管道不同,有名管道是通过文件系统路径命名的管道,可以在进程之间进行通信。有名管道的操作方式类似于打开文件,即进程可以打开有名管道来读取或写入其中的数据。
有名管道可以在终端使用命令“mkfifo pipe_name”命令创建,pipe_name是有名管道的名称,可以是任何有效的文件名。创建有名管道后,它就会在文件系统中以文件的形式存在,但是它的内容为空,且文件类型为p表示管道,如下图所示:
还有另一种创建创建方法可以直接在程序中使用mkfifo()函数进行创建,mkfifo()函数所需头文件和函数原型如下图所示:
所需头文件 | 函数原型 | |
1 2 | #include <sys/types.h> #include <sys/stat.h> | int mkfifo(const char *pathname, mode_t mode); |
管道文件创建完成之后,和前面读写文件步骤相同,下面编写两个程序分别用来进行向管道写入数据和接收管道数据。
首先编写向管道写入数据的程序demo42_fifo_write.c,程序内容如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
// 创建一个命名管道,如果已经存在就不会创建
mkfifo ("my_fifo", 0644);
// 打开命名管道,以只读方式打开,获取文件描述符
int fd = open("my_fifo", O_RDONLY);
// 如果打开文件失败,输出错误信息并退出程序
if (fd == -1)
{
perror("open");
exit(1);
}
// 用于存储读取到的数据的缓冲区
char buf[256];
// 从命名管道中读取数据,最多读取256字节,读取的数据存储到buf中,返回实际读取的字节数
int n = read(fd, buf, 256);
// 输出读取到的数据
printf("Received message: %s\n", buf);
// 关闭文件描述符
close(fd);
return 0;
}
保存退出之后编写读取管道内容的程序demo42_fifo_read.c,程序内容如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
// 创建一个命名管道,如果已经存在就不会创建
mkfifo ("my_fifo", 0644);
// 打开命名管道,以只读方式打开,获取文件描述符
int fd = open("my_fifo", O_RDONLY);
// 如果打开文件失败,输出错误信息并退出程序
if (fd == -1)
{
perror("open");
exit(1);
}
// 用于存储读取到的数据的缓冲区
char buf[256];
// 从命名管道中读取数据,最多读取256字节,读取的数据存储到buf中,返回实际读取的字节数
int n = read(fd, buf, 256);
// 输出读取到的数据
printf("Received message: %s\n", buf);
// 关闭文件描述符
close(fd);
return 0;
}
保存退出之后分别使用了以下命令进行编译,编译完成如下图所示:
然后使用“ ./demo42_fifo_write”命令运行向管道写入的可执行文件,由于没有对管道的数据进行读取,所以会阻塞,如下图所示:
打开第二个终端之后输入“./demo42_fifo_read”,就可以接收管道的数据了,如下图所示:
至此,关于有名管道的讲解就完成了,最后对有名管道和无名管道进行对比:
特点 | 有名管道 | 无名管道 |
是否在文件系统中创建 | 是 | 否 |
亲缘关系进程之间通信 | 是 | 是 |
无亲缘关系进程之间通信 | 是 | 否 |
是否具有固定的读写端 | 是 | 是 |
是否可以单向传递数据 | 否 | 是 |
是否占用文件系统中的实际文件 | 是 | 否 |
是否可以通过文件系统的权限控制来限制访问权限 | 是 | 否 |
有名管道和无名管道它们各自具有自己的特点和用途,有名管道主要用于不同进程之间进行通信,而无名管道则主要用于具有亲缘关系的进程之间进行通信。在选择管道时,需要根据实际情况进行判断和选择。
8.4 IPC对象和IPC key
IPC(Inter-Process Communication,进程间通信)对象是一种在操作系统中用于实现进程间通信和同步的抽象数据结构,可以在不同的进程之间共享,以便实现进程间的数据交换和同步。
Linux 中的 IPC 对象包括消息队列、共享内存和信号量,具体介绍如下所示:
机制 | 应用场景 |
消息队列 | 一对多或多对一的进程通信,日志记录,任务分发,进程间通信 |
共享内存 | 多进程协作,高速缓存,大型数据处理,共享数据缓存 |
信号量 | 进程同步,进程互斥,进程间通信,控制共享资源的访问,保证资源的独占性 |
那么问题来了,前面两个小节讲解的有名管道和无名管道为什么不属于IPC对象呢?原来呀,管道(Pipe)虽然也是一种进程间通信(IPC)机制,但是管道并不是一种抽象的数据结构,也不需要使用 IPC key 进行标识(关于 IPC key 稍后会进行讲解),所以管道并不是IPC对象。
而IPC key是为了方便不同进程之间对IPC对象的访问和操作而产生的,IPC key和IPC对象与文件描述符和文件之间的关系相似,每个IPC对象都会有一个唯一的 IPC key,当一个进程创建或获取一个IPC key时,就可以使用该IPC key来创建或连接相应的 IPC 对象,并对其进行访问。
IPC key是一个长整型数值,可以由任意一个进程指定,并且在整个系统中都是唯一的。使用 IPC key,进程可以对一个已经存在的IPC对象进行连接、删除等操作,也可以创建新的IPC对象并将其共享给其他进程。IPC key的存在可以方便地实现进程之间的通信和同步。
而在具体实现中,Linux 内核会维护一个IPC对象的列表,通过IPC key可以在该列表中查找对应的 IPC对象。因此,进程可以通过指定IPC key来访问和操作 IPC 对象,而不需要关心IPC对象的具体实现细节。
为了避免 IPC key 重复,Linux 提供了一个名为 ftok() 的函数来将一个普通的文件路径和一个整数值转换为一个唯一的 IPC key,该函数所需头文件及其函数原型如下所示:
所需头文件 | 函数原型 | |
1 2 | #include <sys/types.h> #include <sys/ipc.h> | key_t ftok(const char *pathname, int proj_id); |
该函数接受两个参数:文件路径和一个整数值。它会根据文件的 inode 节点号和整数值来生成一个唯一的IPC key。由于不同的文件具有不同的 inode 节点号,因此不同的文件路径和整数值组合会生成不同的 IPC key。
消息队列、共享内存、信号量在操作过程中具有很多相似之处,为了方便后续章节的讲解这里将IPC对象的基本操作步骤进行概括,如下所示:
(1)创建 IPC 对象:首先需要创建相应的IPC对象,这个过程通常会返回一个唯一的IPC key,用来标识该对象。
(2)获取IPC对象:在使用IPC对象之前,需要获取它们的引用或者指针。
(3)操作 IPC 对象:一旦获取了 IPC 对象的引用或者指针,就可以进行相应的操作,如发送消息、读取消息、读写共享内存、加锁解锁等。这些操作都需要使用相应的系统调用,并传递 IPC 对象的引用或者指针。
(4)控制 IPC 对象:在某些情况下,需要控制 IPC 对象的状态和属性,如设置消息队列的大小、删除共享内存、修改信号量的值等。
(5)释放 IPC 对象:最后,在使用完 IPC 对象之后,需要及时释放它们,以避免资源泄漏和竞争等问题。
关于IPC对象和IPC key相关知识就讲解完成了,在接下来的几个小节中将分别对三个IPC对象进行讲解。
8.5 消息队列
本小节代码在配套资料“iTOP-3588开发板\03_【iTOP-RK3588开发板】指南教程\03_系统编程配套程序\43”目录下,如下图所示:
本小节要讲解的是消息队列,消息队列是一种先进先出的消息缓冲区,用于在多个进程之间传递消息。在Linux中,每个消息队列都由一个唯一的消息队列标识符(Message Queue Identifier)进行标识。进程可以通过该标识符来连接和访问相应的消息队列,从而进行进程间通信。
Linux 消息队列的使用步骤及所使用的函数如下表所示:
步骤 | 描述 | 所使用的函数 |
1. | 创建或获取一个 IPC key | ftok() |
2. | 创建或打开一个消息队列 | msgget() |
3. | 进程通过消息队列发送消息 | msgsnd() |
4. | 进程通过消息队列接收消息 | msgrcv() |
5. | 删除消息队列 | msgctl() |
接下来对上述每个步骤及其对应的函数进行详细解释:
(1)创建或获取IPC key
关于IPC key的详细知识在上一小节已经讲解过了,IPC key可以通过调用ftok函数来创建。ftok函数需要指定一个文件路径和一个整数ID,它将根据这些参数生成一个唯一的IPC key。
(2)创建或打开消息队列
要创建或打开消息队列,需要使用msgget函数,msgget函数所需头文件和函数原型如下所示:
所需头文件 | 函数原型 | |
1 2 3 | #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> | int msgget(key_t key, int msgflg); |
函数调用成功,会返回一个非负整数表示消息队列的标识符(即消息队列的唯一标识),调用失败返回-1,并设置errno。
msgget()函数的两个参数含义如下所示:
参数名称 | 参数含义 | |
1 | key | 消息队列的关键字,即使用ftok()函数生成的IPC key值 |
2 | msgflg | 标志位参数,用于指定创建或获取消息队列的方式和权限控制,可以用|符号组合多个标志位。 |
(3)发送消息
在消息队列中要发送消息,需要使用msgsnd函数。msgsnd函数所需头文件和函数原型如下所示:
所需头文件 | 函数原型 | |
1 2 3 | #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> | int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); |
函数调用成功返回 0。函数调用失败,则返回 -1 并设置 errno 来指示错误类型。
msgsnd()函数的四个参数含义如下所示:
参数名称 | 参数含义 | |
1 | msqid | 消息队列的标识符 |
2 | msgp | 指向要发送的消息的指针 |
3 | msgsz | 要发送的消息的大小,不能超过消息队列的最大容量 |
4 | msgflg | 用于指定函数的操作方式和阻塞行为。可以使用多个标志位,用按位或运算符 | 进行组合。以下是可能的标志位: IPC_NOWAIT:如果消息队列已满或无法立即接收消息,则不阻塞进程,而是立即返回 -1 并设置 errno 为 EAGAIN。 MSG_NOERROR:如果消息大小超过了消息队列的最大容量,将截断消息而不是返回错误 |
(4)接收消息
要从消息队列中读取消息,需要使用msgrcv函数。msgrcv函数所需头文件和函数原型如下所示:
所需头文件 | 函数原型 | |
1 2 3 | #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,并设置errno。
msgrcv()函数的5个参数含义如下所示:
参数名称 | 参数含义 | |
1 | msqid | 消息队列的标识符 |
2 | msgp | 指向接收消息的缓冲区的指针 |
3 | msgsz | 接收消息缓冲区的大小,一般设置为消息数据部分的大小 |
4 | msgtyp | 指定接收消息的类型,如果指定为0,则表示接收队列中第一条消息 |
5 | msgflg | 接收消息的标志,可以是以下值之一: (1)0:阻塞模式,如果队列中没有消息,则一直等待,直到有消息到来。 (2)PC_NOWAIT:非阻塞模式,如果队列中没有消息,则立即返回-1,并设置 errno 为 ENOMSG。 (3)MSG_EXCEPT:在接收消息时,接收消息队列中第一个类型字段不等于 msgtyp 的消息。 (4)MSG_NOERROR:若消息的长度大于 msgsz 字段的长度,则消息会被截断。 (5)MSG_COPY:消息从内核中拷贝到用户空间,如果消息队列中的数据是静态分配的内存,使用这个标志位将导致系统性能降低。 |
(5)删除消息队列
使用msgctl函数可以删除不再需要的消息队列,除了上述功能外msgctl函数还能获取和设置消息队列的属性信息,msgctl函数所需头文件和函数原型如下所示:
所需头文件 | 函数原型 | |
1 2 3 | #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> | int msgctl(int msqid, int cmd, struct msqid_ds *buf); |
函数调用成功之后返回值为0,否则表示执行失败。msgctl()函数的两个参数含义如下所示:
参数名称 | 参数含义 | |
1 | msqid | 要操作的消息队列的标识符。 |
2 | cmd | 要执行的操作,cmd 参数有以下几个可选值: IPC_STAT:获取消息队列的属性信息,将信息存储在 buf 指向的结构体中。 IPC_SET:设置消息队列的属性信息,通过 buf 指向的结构体来设置。 IPC_RMID:删除消息队列。 |
3 | buf | 一个指向 msqid_ds 结构体的指针,用于传递消息队列的属性信息 |
至此,关于消息队列的使用步骤及其相应的函数就讲解完成了,但在使用过程中仍然有一些需要注意的地方,如下所示:
(1)消息类型的选择:消息类型是消息队列中的重要属性,应该根据实际需求进行选择。一般来说,可以将不同类型的消息分配到不同的消息类型中,以方便接收方对不同类型的消息进行处理。
(2)消息长度的限制:Linux 消息队列的消息长度是有限制的,通常为 8192 字节。如果消息长度超过了限制,就会导致发送和接收失败。
(3)消息发送和接收顺序的保证:在多进程或多线程环境中,由于程序执行的不确定性,可能会导致消息发送和接收的顺序与预期的不一致。因此,需要设计合理的同步机制,保证消息的发送和接收顺序。
(4)消息队列的清理:在使用消息队列之后,需要及时清理消息队列,以免消息队列占用过多的系统资源。可以使用 msgctl 函数来删除消息队列。
(5)错误处理:在使用消息队列的过程中,可能会发生各种错误,比如消息队列已满、消息类型不匹配等等。因此,在编写程序时需要适当处理这些错误,以保证程序的稳定性和可靠性。
最后编写两个应用程序demo43_msg_recv.c和demo43_msg_send.c来对消息队列进行举例,首先编写消息队列发送端程序demo43_msg_send.c,程序内容如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define MSG_TYPE 1 // 定义消息类型为 1
struct msgbuf {
long mtype; // 消息类型,必须为long类型,必须大于0
char mtext[1024]; // 消息内容
};
int main()
{
key_t key;
int msgid;
struct msgbuf msg;
// 创建一个 IPC key,需要和发送端保持一致
key = ftok(".", 'a');
// 打开消息队列,以读写模式打开,需要和发送端保持一致
msgid = msgget(key, 0666);
if (msgid == -1)
{ // 打开失败
perror("msgget error");
exit(EXIT_FAILURE);
}
// 接收消息,将接收到的消息存入 msg 中,MSG_TYPE 为指定的消息类型
if (msgrcv(msgid, &msg, sizeof(msg.mtext), MSG_TYPE, 0) == -1)
{
perror("msgrcv error");
exit(EXIT_FAILURE);
}
printf("Received message: %s\n", msg.mtext); // 打印接收到的消息
// 删除消息队列
if (msgctl(msgid, IPC_RMID, NULL) == -1)
{
perror("msgctl error");
exit(EXIT_FAILURE);
}
return 0;
}
保存退出之后,编写消息队列接收端程序demo43_msg_recv.c,具体代码如下图所示:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define MSG_TYPE 1 // 定义消息类型为 1
struct msgbuf {
long mtype; // 消息类型,必须为long类型,必须大于0
char mtext[1024]; // 消息内容
};
int main()
{
key_t key;
int msgid;
struct msgbuf msg;
// 创建一个 IPC key,需要和发送端保持一致
key = ftok(".", 'a');
// 打开消息队列,以读写模式打开,需要和发送端保持一致
msgid = msgget(key, 0666);
if (msgid == -1)
{ // 打开失败
perror("msgget error");
exit(EXIT_FAILURE);
}
// 接收消息,将接收到的消息存入 msg 中,MSG_TYPE 为指定的消息类型
if (msgrcv(msgid, &msg, sizeof(msg.mtext), MSG_TYPE, 0) == -1)
{
perror("msgrcv error");
exit(EXIT_FAILURE);
}
printf("Received message: %s\n", msg.mtext); // 打印接收到的消息
// 删除消息队列
if (msgctl(msgid, IPC_RMID, NULL) == -1)
{
perror("msgctl error");
exit(EXIT_FAILURE);
}
return 0;
}
保存退出之后,分别使用以下命令进行程序的编译,编译完成如下图所示:
gcc -o demo43_msg_recv demo43_msg_recv.c
gcc -o demo43_msg_send demo43_msg_send.c
首先使用“./demo43_msg_send”命令,像消息队列中发送数据,发送成功如下图所示:
然后使用“ipcs -q”都系统中的消息队列进行查看,如下图所示:
可以看到刚刚向消息队列发送的大小为1024,权限666的数据已经在列表中了,然后使用“./demo43_msg_recv”命令从消息队列中取出数据,如下图所示:
发送的数据“ Hello, this is a message from message sender”被成功接收到了,至此,关于消息队列的内容就讲解完成了。
8.6 共享内存
消息队列的缓存区域是在内核空间中开辟的,因此当进程发送或接收消息时,需要经过内核态和用户态之间的多次切换,多次状态间的切换会带来一定的开销,特别是在发送和接收大量数据时,切换开销可能会严重影响系统性能。而本小节要学习的共享内存只需要在进程之间进行少量的数据传输和同步,避免了频繁的内核态和用户态之间的切换,因此在数据传输较为频繁的场景下,使用共享内存可以提高系统性能。就让我们一起进入共享内存的学习吧。
本小节代码在配套资料“iTOP-3588开发板\03_【iTOP-RK3588开发板】指南教程\03_系统编程配套程序\44”目录下,如下图所示:
共享内存是通过操作系统内核在不同进程之间共享内存区域的一种机制。在创建共享内存时,操作系统会分配一块内存区域,并将其映射到各个进程的地址空间中。进程可以直接读写这个内存区域,而不需要进行任何数据传输的操作。
共享内存的优点是速度快,因为不需要数据的复制操作,而且不需要操作系统进行上下文切换,所以它通常比其他 IPC 机制(如管道和消息队列)更快。
Linux 共享内存的使用步骤及所使用的函数如下表所示:
步骤 | 描述 | 所使用的函数 |
1 | 创建或获取一个 IPC key | ftok() |
2 | 创建共享内存 | shmget() |
3 | 将共享内存映射到进程的地址空间中 | shmat() |
4 | 解除共享内存映射 | shmdt() |
5 | 删除共享内存 | shmctl() |
接下来对上述每个步骤及其对应的函数进行详细解释:
(1)创建或获取IPC key
关于IPC key的详细知识在8.4小节已经讲解过了,IPC key可以通过调用ftok函数来创建。ftok函数需要指定一个文件路径和一个整数ID,它将根据这些参数生成一个唯一的IPC key。
(2)创建或打开消息队列
要创建或打开消息队列,需要使用shmget函数,shmget函数所需头文件和函数原型如下所示:
所需头文件 | 函数原型 | |
1 2 | #include <sys/ipc.h> #include <sys/shm.h> | int shmget(key_t key, size_t size, int shmflg); |
函数调用成功,返回值为共享内存标识符(shmid),用于标识创建或获取的共享内存对象。如果出错,则返回 -1 并设置 errno 错误码。
shmget()函数的两个参数含义如下所示:
参数名称 | 参数含义 | |
1 | key | 消息队列的关键字,即使用ftok()函数生成的IPC key值 |
2 | size | 共享内存大小,以字节为单位。 |
3 | msgflg | 标志位参数,用于指定创建或获取消息队列的方式和权限控制,可以用|符号组合多个标志位。可选标志位如下: (1)IPC_CREAT:如果共享内存不存在则创建它。 (2)IPC_EXCL:如果同时指定了 IPC_CREAT 和 IPC_EXCL,并且共享内存已经存在,则创建共享内存失败并返回错误。 (3)IPC_NOWAIT:如果同时指定了 IPC_CREAT 和 IPC_NOWAIT,并且共享内存已经存在,则不等待直接返回错误。 (4)SHM_HUGETLB:指定使用 huge page 来映射共享内存。 (5)SHM_NORESERVE:指定不保留物理内存。 |
(3)映射共享内存
在消息队列中要发送消息,需要使用shmat函数。shmat函数所需头文件和函数原型如下所示:
所需头文件 | 函数原型 | |
1 2 | #include <sys/types.h> #include <sys/shm.h> | void *shmat(int shmid, const void *shmaddr, int shmflg); |
函数返回值为共享内存段的首地址,类型为 void *。如果出错,则返回 -1 并设置 errno 错误码。
shmat()函数的3个参数含义如下所示:
参数名称 | 参数含义 | |
1 | shmid | 共享内存标识符,通过 shmget() 函数创建或获取。 |
2 | shmaddr | 指定共享内存段的连接地址,通常设置为 NULL,让系统自动选择一个合适的地址。 |
3 | shmflg | 连接共享内存的标志位,可选值为以下几个: (1)SHM_RDONLY:以只读方式连接共享内存,即不能修改共享内存中的数据。 (2)SHM_RND:指定连接地址必须按页面大小(4096 字节)的整数倍进行对齐。 (3)SHM_REMAP:如果共享内存已经连接到其他进程,则重新映射共享内存而不是创建新的映射。 |
(4)解除共享内存映射
要从消息队列中读取消息,需要使用shmdt函数。shmdt函数所需头文件和函数原型如下所示:
所需头文件 | 函数原型 | |
1 2 | #include <sys/types.h> #include <sys/shm.h> | int shmdt(const void *shmaddr); |
函数返回值为0表示成功,如果出错,则返回-1并设置 errno 错误码。shmdt()函数只有一个参数,参数含义如下所示:
参数名称 | 参数含义 | |
1 | shmaddr | 是指向共享内存段的首地址,该地址是通过shmat()函数连接到当前进程的 |
(5)删除共享内存
使用shmctl函数可以删除不再需要的消息队列,除了上述功能外shmctl函数还能获取和设置消息队列的属性信息,shmctl函数所需头文件和函数原型如下所示:
所需头文件 | 函数原型 | |
1 2 | #include <sys/ipc.h> #include <sys/shm.h> | int shmctl(int shmid, int cmd, struct shmid_ds *buf); |
函数调用成功之后返回值为0,否则表示执行失败。shmget()函数的两个参数含义如下所示:
参数名称 | 参数含义 | |
1 | shmid | 共享内存标识符,通过 shmget() 函数创建或获取。 |
2 | cmd | 控制命令,可选值为以下几个: IPC_STAT:获取共享内存的状态信息,将共享内存的信息保存到 buf 结构体中。 IPC_SET:设置共享内存的状态信息,将 buf 结构体中的信息设置到共享内存中。 IPC_RMID:销毁共享内存。 SHM_LOCK:锁定共享内存,防止被换出到磁盘上。 SHM_UNLOCK:解锁共享内存。 |
3 | buf | 指向 shmid_ds 结构体的指针,用于存储共享内存的状态信息。如果 cmd 为 IPC_STAT 或 IPC_SET,则需要传递该参数,否则可以设置为 NULL。 |
至此,关于共享内容的使用步骤及其相应的函数就讲解完成了,但在使用过程中仍然有一些需要注意的地方,如下所示:
(1)内存泄漏:由于共享内存段在多个进程之间共享,因此需要注意避免出现内存泄漏的情况。特别是在共享内存段不再需要时,应当及时将其从当前进程中分离,并且在最后一个使用该共享内存的进程退出时销毁该共享内存段。
(2)进程同步:由于多个进程可以同时访问共享内存,因此需要进行进程同步,避免出现竞争条件和数据不一致的情况。可以使用信号量等机制进行进程同步。
(3)内存访问:由于多个进程可以同时访问共享内存,因此需要注意避免出现数据访问冲突的情况。特别是在对共享内存进行写操作时,需要考虑到多个进程同时修改共享内存的情况,需要采用相应的机制进行同步。
(4)数据结构:在共享内存中存储的数据结构需要考虑到不同进程的字节对齐和大小端等问题,需要进行兼容性处理。
最后编写两个应用程序demo44_shm_write.c和demo44_shm_read.c来对共享内存进行举例,首先编写共享内存写入数据的程序demo44_shm_write.c,程序内容如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHM_SIZE 1024
int main()
{
// 生成一个 key
key_t key = ftok(".", 1); // 使用当前目录和一个非零整数生成一个 key
if (key == -1)
{
perror("ftok"); // 打印错误信息
exit(1); // 退出程序
}
// 创建共享内存段
int shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT); // 创建一个大小为 SHM_SIZE 的共享内存段
if (shmid == -1)
{
perror("shmget"); // 打印错误信息
exit(1); // 退出程序
}
// 连接共享内存段
char *shmaddr = (char *) shmat(shmid, NULL, 0); // 连接到共享内存段
if (shmaddr == (char *) -1)
{
perror("shmat"); // 打印错误信息
exit(1); // 退出程序
}
// 写入共享内存
strncpy(shmaddr, "Hello, shared memory!", SHM_SIZE); // 向共享内存中写入字符串
// 分离共享内存
if (shmdt(shmaddr) == -1)
{
perror("shmdt"); // 打印错误信息
exit(1); // 退出程序
}
printf("数据已经写入共享内存中。\n"); // 打印提示信息
return 0; // 退出程序
}
然后编写读取共享内存数据的程序demo44_shm_read.c,程序内容如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHM_SIZE 1024
int main()
{
// 生成一个 key
key_t key = ftok(".", 1); // 使用当前目录和一个非零整数生成一个 key
if (key == -1) // 如果生成 key 失败
{
perror("ftok"); // 打印错误信息
exit(1); // 退出程序
}
// 获取共享内存段 ID
int shmid = shmget(key, SHM_SIZE, 0666); // 使用 shmget 函数获取共享内存段 ID,0666 表示权限为读写
if (shmid == -1) // 如果获取失败
{
perror("shmget"); // 打印错误信息
exit(1); // 退出程序
}
// 连接共享内存段
char *shmaddr = (char *) shmat(shmid, NULL, 0); // 使用 shmat 函数连接共享内存段,NULL 表示让系统自动选择一个地址,0 表示以可读写方式连接
if (shmaddr == (char *) -1) // 如果连接失败
{
perror("shmat"); // 打印错误信息
exit(1); // 退出程序
}
// 读取共享内存
printf("从共享内存中读取到的数据为:%s\n", shmaddr);
// 分离共享内存
if (shmdt(shmaddr) == -1) // 使用 shmdt 函数分离共享内存段
{
perror("shmdt"); // 如果分离失败,打印错误信息
exit(1); // 退出程序
}
// 删除共享内存
if (shmctl(shmid, IPC_RMID, 0) == -1)
{
perror("shmctl"); // 打印错误信息
exit(1); // 退出程序
}
printf("共享内存已经删除。\n"); // 打印提示信息
return 0;
}
保存退出之后,分别使用以下命令进行程序的编译,编译完成如下图所示:
gcc -o demo44_shm_write demo44_shm_write.c
gcc -o demo44_shm_read demo44_shm_read.c
首先使用“./demo44_shm_write”命令,向共享内存中写入数据,发送成功如下图所示:
然后使用“ipcs -m”对系统中的共享内存进行查看,如下图所示:
可以看到刚刚向消息队列发送的大小为1024,权限666的数据已经在共享内存中了,然后使用“./demo44_shm_read”命令读取共享内存中的数据,如下图所示:
写入共享内存的数据“Hello, shared memory!”被读了出来,然后再次使用“ipcs -m”对系统中的共享内存进行查看,如下图所示:
可以看到我们创建的共享内存区域已经被删除了,至此,关于消息队列的内容就讲解完成了。
8.7 信号量
进程间通信机制可以在多个进程之间传递消息和共享数据,同时还能保证多个进程之间的同步和互斥,以达到协作工作的目的。以上一小节讲解的共享内存为例,它可以在多个进程之间共享同一块内存区域,由于多个进程都可以访问同一块内存,因此需要一种机制来保证多个进程之间的同步和互斥,避免出现数据不一致或竞争条件等问题。而本小节将对该机制-信号量进行讲解。
本小节代码在配套资料“iTOP-3588开发板\03_【iTOP-RK3588开发板】指南教程\03_系统编程配套程序\45”目录下,如下图所示:
信号量是一种计数器,用于对多个进程共享的资源进行计数和控制。它是一种 IPC 对象,通常用于进程间互斥和同步,确保多个进程对共享资源的访问顺序和正确性。信号量通常用于解决并发访问共享资源的同步问题。
在使用信号量之前,需要先创建一个信号量集。信号量集是由一组信号量组成的集合,每个信号量都可以有不同的初值和权限。信号量集由一个唯一的整数 ID 来标识,通常使用 semget() 系统调用创建。
创建信号量集后,就可以使用 semctl()、semop() 等系统调用对信号量集进行操作。semctl() 用于对信号量集进行控制操作,如获取或设置信号量集的属性,删除信号量集等;semop() 用于进行信号量的操作,如增加或减少信号量的值,等待或唤醒进程等。
在使用 semop() 系统调用时,需要指定要操作的信号量在信号量集中的位置和要进行的操作类型。信号量的操作类型有三种:P 操作、V 操作和 Z 操作。P 操作会使信号量的值减一,如果信号量的值小于 0,则阻塞等待;V 操作会使信号量的值加一,如果有进程正在等待该信号量,则唤醒其中一个;Z 操作会将信号量的值设置为 0。
Linux 信号量的使用步骤及所使用的函数如下表所示:
步骤 | 描述 | 所使用的函数 |
1 | 创建或获取一个 IPC key | ftok() |
2 | 创建或获取信号量 | semget() |
3 | 初始化信号量 | semctl() |
4 | P操作(等待)/ V操作(释放) | semop() |
5 | 删除信号量 | semctl() |
下面对上述每个步骤及其对应的函数进行详细解释:
(1)创建或获取IPC key
关于IPC key的详细知识在8.4小节已经讲解过了,IPC key可以通过调用ftok函数来创建。ftok函数需要指定一个文件路径和一个整数ID,它将根据这些参数生成一个唯一的IPC key。
(2)创建或获取信号量
要创建或获取一个信号量时,需要使用semget函数,shmget函数所需头文件和函数原型如下所示:
所需头文件 | 函数原型 | |
1 2 3 | #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> | int semget(key_t key, int nsems, int semflg); |
函数调用成功,返回值为获取或创建的信号量集的标识符。如果出错,则返回 -1 并设置 errno 错误码。
semget()函数的3个参数含义如下所示:
参数名称 | 参数含义 | |
1 | key | 标识信号量集的关键字,它必须与其他进程共享,因此通常是通过ftok函数生成的。 |
2 | nsems | 信号量集中包含的信号量数目。 |
3 | semflg | 标志位,用于指定函数的行为。它可以是0,也可以是以下标志的按位或运算结果: IPC_CREAT:如果关键字对应的信号量集不存在,则创建一个新的信号量集,并返回标识符。如果已经存在,则返回其标识符。 IPC_EXCL:如果关键字对应的信号量集已经存在,则返回一个错误。它只能和IPC_CREAT一起使用。 |
(3)初始化信号量
创建一个信号量之后,并不能直接对信号量进行使用,往往需要进行初始值设置等操作,,需要使用semctl函数来完成,semctl函数除了设置信号量外,还能进行获取、删除信号量等操作,semctl函数所需头文件和函数原型如下所示:
参数名称 | 参数含义 | |
1 | semid | 信号量集的标识符 |
2 | semnum | 要操作的信号量编号 |
3 | cmd | 是要执行的命令semctl 函数的命令参数 cmd 主要有以下几种: (1)IPC_STAT:获取信号量的信息,并将其保存在 arg 中的 semid_ds 结构体中。 (2)IPC_SET:设置信号量的信息,使用 arg 中的 semid_ds 结构体中的信息进行设置。 (3)IPC_RMID:删除信号量集,此时 arg 必须是一个空指针。 (4)GETVAL:获取信号量的当前值,并将其保存在 arg 中的整型变量中。 (5)SETVAL:设置信号量的值,使用 arg 中的整型变量进行设置 |
arg | 是一个联合体参数,用于传递不同类型的参数值 |
(4)信号量的P/V操作
对信号量进行加、减等操作,需要使用semop函数。semop函数所需头文件和函数原型如下所示:
所需头文件 | 函数原型 | |
1 2 3 | #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> | int semop(int semid, struct sembuf *sops, size_t nsops); |
函数返回值为0表示成功,如果出错,则返回-1并设置 errno 错误码。shmdt()函数只有一个参数,参数含义如下所示:
参数名称 | 参数含义 | |
1 | semid | 信号量集的标识符。 |
2 | sops | 指向一组 sembuf 结构的指针,每个 sembuf 结构用于描述一个操作。 |
3 | nsops | 需要执行的操作数。 |
下面对sembuf 结构体进行讲解。该结构体定义在头文件 <sys/sem.h> 中,结构体成员如下:
所需头文件 | |
1 2 3 4 5 | struct sembuf { unsigned short sem_num; // 信号量在信号量集中的编号 short sem_op; // 对信号量的操作:增加、减少、等于 0 short sem_flg; // 操作标记:0 或 IPC_NOWAIT }; |
对于上诉参数的讲解如下所示:
参数名称 | 参数含义 | |
1 | sem_num | 信号量在信号量集中的编号,从 0 开始计数。 |
2 | sem_op | 对信号量进行的操作。如果 sem_op 的值为正数,则表示要增加信号量值,如果为负数,则表示要减少信号量值,如果为 0,则表示要检查信号量值是否为 0。如果检查到信号量值不为 0,则 semop() 函数会阻塞调用进程,直到信号量的值变为 0 |
3 | sem_flg: | 操作标记,取值为 0 或 IPC_NOWAIT。如果 sem_flg 的值为 0,则表示进程要等待操作完成后再返回。如果 sem_flg 的值为 IPC_NOWAIT,则表示进程不等待操作完成就立即返回 |
(5)删除信号量
要删除一个信号量,需要使用semctl函数,并将第二个参数设置为IPC_RMID。在第三步的初始化信号量中已经对semctl函数进行讲解了,这里就不再进行过多的解释。
关于信号量的使用步骤及其相应的函数就讲解完成了,下面编写测试例程demo45_sem.c,程序内容如下所示
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
// 定义一个信号量的初始值
#define SEM_INIT_VAL 1
int main()
{
int sem_id;
struct sembuf sem_buf;
// 生成一个 key
key_t key = ftok(".", 1); // 使用当前目录和一个非零整数生成一个 key
if (key == -1)
{
perror("ftok"); // 打印错误信息
exit(1); // 退出程序
}
// 创建一个信号量
sem_id = semget(key, 1, IPC_CREAT | 0666);
if (sem_id < 0)
{
perror("创建信号量失败");
exit(1);
}
// 初始化信号量
if (semctl(sem_id, 0, SETVAL, SEM_INIT_VAL) < 0)
{
perror("初始化信号量失败");
exit(1);
}
// 获取信号量
sem_buf.sem_num = 0;
sem_buf.sem_op = -1;
sem_buf.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_buf, 1) < 0)
{
perror("获取信号量失败");
exit(1);
}
printf("信号量当前值为:%d\n", semctl(sem_id, 0, GETVAL)); // 打印信号量的当前值
// 释放信号量
sem_buf.sem_num = 0;
sem_buf.sem_op = 1;
sem_buf.sem_flg = SEM_UNDO;
if (semop(sem_id, &sem_buf, 1) < 0)
{
perror("释放信号量失败");
exit(1);
}
printf("信号量当前值为:%d\n", semctl(sem_id, 0, GETVAL)); // 打印信号量的当前值
// 删除信号量
if (semctl(sem_id, 0, IPC_RMID, 0) < 0)
{
perror("删除信号量失败");
exit(1);
}
printf("信号量程序成功执行完毕\n");
return 0;
}
保存退出之后,分别使用以下命令进行程序的编译,编译完成如下图所示:
gcc -o demo45_sem demo45_sem.c
首先使用“./demo45_sem”命令,运行该可执行程序如下图所示:
可以看到信号量的增减过程,至此,关于信号量相关的内容就讲解完成了。