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主板
第10章 存储映射 I/O
当我们需要对大量数据进行读写时,传统的I/O操作可能会因为频繁的系统调用而导致效率低下。此时,我们可以考虑使用存储映射I/O技术,将磁盘上的数据映射到内存中,通过对内存的操作来实现对数据的读写,从而提高程序的效率。在本章将对存储映射I/O进行讲解。
10.1建立映射区
本小节代码在配套资料“iTOP-3588开发板\03_【iTOP-RK3588开发板】指南教程\03_系统编程配套程序\54”目录下,如下图所示:
存储映射 I/O(Memory-Mapped I/O,MMIO)是一种特殊的输入输出(I/O)操作方式,它将输入/输出寄存器(I/O register)映射到内存地址空间,使得I/O寄存器和普通内存单元具有相同的访问方式。这样,CPU通过访问内存地址来进行I/O操作,就像读写普通内存一样。
与传统的I/O方式相比,存储映射 I/O 的优势在于:
(1)提高了数据传输的效率。传统的I/O操作需要涉及到数据缓冲区的复制,而存储映射I/O方式直接操作内存,避免了数据复制的过程,从而提高了数据传输的效率。
(2)降低了对CPU的开销。传统的I/O操作需要调用系统调用进入内核态,而存储映射I/O方式是在用户态下完成的,避免了频繁的系统调用和内核态和用户态的切换,从而降低了对CPU的开销。
(3)存储映射I/O的实现需要硬件和操作系统的支持。硬件需要将I/O寄存器映射到内存地址空间,操作系统则需要提供相关的系统调用来进行I/O操作。
下面是使用存储映射I/O的基本步骤:
步骤 | 作用 | 函数 |
1 | 打开文件,获取文件描述符 | open() |
2 | 将设备寄存器映射到进程的虚拟地址空间中 | mmap() |
3 | 对映射的内存区域进行读写操作 | 访问内存 |
4 | 解除内存映射关系 | munmap() |
5 | 关闭文件 | close() |
对与open()函数大家已经很熟悉了,本小节不再对open()函数进行讲解。使用系统调用mmap()用于创建内存映射,可以将一个文件或其他对象映射到进程的地址空间,使得文件内容可以像访问内存一样直接被读写。其函数原型和所需头文件如下所示:
所需头文件 | 函数原型 | |
1 2 | #include <sys/mman.h> | void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset); |
mmap 函数将指定的文件映射到调用进程的虚拟地址空间,并返回该映射区域的起始地址。如果映射失败,则返回 MAP_FAILED。
下面对参数进行详细解释:
参数名称 | 参数含义 | |
1 | addr | 映射区域的建议起始地址,通常传入 NULL,由内核选择。 |
2 | length | 映射区域的长度。 |
3 | prot | 映射区域的保护模式,可以是以下几种模式的组合: (1)PROT_EXEC:映射区域可被执行。 (2)PROT_READ:映射区域可被读取。 (3)PROT_WRITE:映射区域可被写入。 (4)PROT_NONE:映射区域不能被访问。 |
4 | flags | 映射区域的类型,可以是以下几种类型的组合: (1)MAP_FIXED:指定映射区域的起始地址,如果该地址已经被占用,则调用失败。 (2)MAP_PRIVATE:创建一个私有的映射区域,对该区域的写操作不会影响到原文件。 (3)MAP_SHARED:创建一个共享的映射区域,对该区域的写操作会影响到原文件。 (4)MAP_ANONYMOUS:创建一个匿名的映射区域,不需要使用文件描述符。 (5)MAP_NORESERVE:不预留交换空间。 |
5 | fd | 映射区域所关联的文件描述符。 |
存储映射 I/O 示意图如下所示:
首先创建一个名为demo53_pthread_mutex.c文件,向该文件写入以下内容,如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd; // 文件描述符
char *addr; // 映射区的起始地址
off_t size; // 文件大小
if (argc != 2)
{ // 判断是否输入了文件名
fprintf(stderr, "用法: %s <文件名>\n", argv[0]);
exit(1); // 异常退出
}
fd = open(argv[1], O_RDONLY); // 打开文件,只读方式
if (fd == -1)
{ // 判断是否打开成功
perror("打开文件"); // 打印错误信息
exit(1); // 异常退出
}
// 获取文件大小
size = lseek(fd, 0, SEEK_END); // 将文件指针定位到文件末尾
if (size == (off_t) -1)
{ // 判断是否定位成功
perror("获取文件大小"); // 打印错误信息
exit(1); // 异常退出
}
// 映射文件到内存中
addr = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED)
{ // 判断映射是否成功
perror("映射文件"); // 打印错误信息
exit(1); // 异常退出
}
printf("%.*s", (int) size, addr); // 输出映射到内存中的文件内容
if (munmap(addr, size) == -1)
{ // 解除映射
perror("解除映射"); // 打印错误信息
exit(1); // 异常退出
}
close(fd); // 关闭文件
exit(0); // 正常退出
}
保存退出之后,使用以下命令对 demo54_mmap.c进行编译,编译完成如下图所示:
gcc -o demo54_mmap demo54_mmap.c
然后创建test文件并向该文件中写入“hello mmap”,最后使用命令“./demo54_mmap test”来建立test文件的存储映射区并进行读取,如下图所示:
可以看到test文件中的内容就显示了出来,test的文件映射也就成功了。
10.2解除映射关系
在使用内存映射时,系统会将文件的部分或全部内容映射到进程的虚拟地址空间中。这样一来,进程就可以像访问内存一样来访问文件,而不必使用read或write等系统调用来进行I/O操作,这样可以大大提高文件I/O操作的性能。
但是,内存映射也有其不足之处,因为内存映射使用的是虚拟地址空间,而不是真正的物理内存,所以映射的空间是有限制的。另外,映射的内容也是与文件相关联的,如果映射的文件被删除或修改,则映射区的内容也会被影响,因此需要谨慎处理。另外,映射区域的大小也是有限制的,当映射的文件过大或者进程需要使用的映射区域过多时,也有可能导致内存不足的问题。
所以,当不再需要访问映射区域时,应该及时将其解除映射,以释放系统资源,避免资源的浪费和内存泄漏等问题。同时,解除映射也能够保证对映射区域的修改能够被同步到文件中,从而保证文件内容的完整性和一致性。
munmap()函数用于解除内存映射,也就是将一个已经映射到进程地址空间的文件或设备释放。munmap()函数所系头文件和函数原型如下所示:
所需头文件 | 函数原型 | |
1 | #include <sys/mman.h> | int munmap(void *addr, size_t length); |
其中,addr参数是内存映射区的起始地址,length参数是映射区的长度。函数成功时返回0,失败时返回-1,并设置errno变量指示错误原因。调用munmap()函数后,不能再访问该内存区域。因此,解除映射应该在程序不再需要使用内存映射区时执行,以确保不会发生任何未定义的行为。
再上一小节所举的案例中的44-48行已经添加了接触映射关系的代码,所以本小节不再进行举例。
10.3父子进程间通信
本小节代码在配套资料“iTOP-3588开发板\03_【iTOP-RK3588开发板】指南教程\03_系统编程配套程序\55”目录下,如下图所示:
本小节将通过 mmap建立的映射区完成父子进程数据通信。
首先创建一个名为demo55_mmap.c文件,向该文件写入以下内容,如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#define SHARED_MEMORY_SIZE 1024 // 定义共享内存区大小为 1024 字节
int main()
{
int *shared_memory; // 定义指向共享内存区的指针
pid_t pid; // 定义进程标识符
// 创建共享内存区
shared_memory = mmap(NULL, SHARED_MEMORY_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
/*
* mmap 函数创建一段新的共享内存区,返回该内存区的指针
* 参数 NULL 表示让系统自动选择一个未使用的地址作为共享内存区的起始地址
* 参数 SHARED_MEMORY_SIZE 表示共享内存区的大小,单位为字节
* 参数 PROT_READ | PROT_WRITE 表示共享内存区可读可写
* 参数 MAP_SHARED | MAP_ANONYMOUS 表示该共享内存区对所有进程可见,且不存在对应的文件
* 参数 -1 和 0 表示该共享内存区不使用文件
*/
if (shared_memory == MAP_FAILED)
{ // 如果创建共享内存区失败
perror("mmap"); // 输出错误信息
exit(1); // 退出程序
}
// 创建子进程
pid = fork(); // 调用 fork 函数创建子进程
if (pid == -1)
{ // 如果创建子进程失败
perror("fork"); // 输出错误信息
exit(1); // 退出程序
}
else if (pid == 0)
{ // 子进程
// 在共享内存中写入数据
*shared_memory = 123; // 向共享内存中写入数据
// 子进程等待 1 秒钟
sleep(1);
// 输出共享内存中的数据
printf("子进程: %d\n", *shared_memory);
// 解除映射关系
if (munmap(shared_memory, SHARED_MEMORY_SIZE) == -1)
{
perror("munmap");
exit(1);
}
exit(0); // 子进程退出
}
else
{ // 父进程
// 父进程等待子进程写入数据
sleep(2);
// 输出共享内存中的数据
printf("父进程: %d\n", *shared_memory);
// 在共享内存中写入数据
*shared_memory = 456; // 向共享内存中写入数据
// 父进程等待子进程读取数据
sleep(1);
// 输出共享内存中的数据
printf("父进程: %d\n", *shared_memory);
// 解除映射关系
if (munmap(shared_memory, SHARED_MEMORY_SIZE) == -1)
{
perror("munmap");
exit(1);
}
exit(0); // 父进程退出
}
}
该程序创建了一个大小为1024的共享内存区域,创建了一个子进程并使用共享内存进行通信。在子进程中,向共享内存中写入了整数123,并等待1秒钟后输出共享内存中的数据,最后解除了映射关系。在父进程中,等待子进程写入数据,然后输出共享内存中的数据,接着在共享内存中写入整数456,等待子进程读取数据,最后输出共享内存中的数据并解除了映射关系。
保存退出之后,使用以下命令对 demo55_mmap.c进行编译,编译完成如下图所示:
gcc -o demo55_mmap demo55_mmap.c
然后使用命令“./demo55_mmap”进行程序的运行,如下图所示:
其中最重要的是第二条打印信息,子进程向共享内存写入123之后,父进程也获取到了共享内存的123,证明使用存储映射IO成功进行了父子进程的通信。
10.4无血缘关系进程间通信
本小节代码在配套资料“iTOP-3588开发板\03_【iTOP-RK3588开发板】指南教程\03_系统编程配套程序\56”目录下,如下图所示:
所需头文件 | 函数原型 | |
1 2 3 | #include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> | int shm_open(const char *name, int oflag, mode_t mode); int shm_unlink(const char *name); |
shm_open() 函数的返回值是一个文件描述符,可以通过这个文件描述符操作共享内存对象。对于需要在不同进程间共享的共享内存,需要将文件描述符传递给其他进程,其他进程通过。shm_open()的参数说明如下:
参数名称 | 参数含义 | |
1 | name | 共享内存段的名字,必须以斜杠/开头,不同进程可以通过这个名字来连接到同一个共享内存段。 |
2 | oflag | 打开标志,可以是以下值之一或它们的组合: O_RDONLY:只读方式打开共享内存段; O_RDWR:读写方式打开共享内存段; O_CREAT:如果共享内存段不存在,则创建它; O_EXCL:与O_CREAT一起使用,表示如果共享内存段已存在则返回错误。 |
3 | mode | 创建共享内存段时设置的权限。 |
shm_unlink()的参数只有一个name,表示要共享内存段的名字。
共享内存段创建之后和前几个小节的步骤相同,使用mmap()函数进行存储映射即可。下面对没有血缘关系进程通过存储映射完成通信进行举例:
首先创建一个名为demo56_mmap_a.c文件,向该文件写入以下内容,如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#define SHARED_MEMORY_SIZE 1024 // 定义共享内存的大小
int main()
{
// 创建或打开共享内存对象,获取一个文件描述符
int fd = shm_open("/myshm", O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
if (fd == -1)
{
perror("shm_open"); // 打印错误信息
exit(1); // 退出程序
}
// 调整共享内存区的大小为 SHARED_MEMORY_SIZE 字节
if (ftruncate(fd, SHARED_MEMORY_SIZE) == -1)
{
perror("ftruncate"); // 打印错误信息
exit(1); // 退出程序
}
// 将共享内存区映射到当前进程的地址空间
int *shared_memory = mmap(NULL, SHARED_MEMORY_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (shared_memory == MAP_FAILED)
{
perror("mmap"); // 打印错误信息
exit(1); // 退出程序
}
// 关闭文件描述符
close(fd);
// 在共享内存中写入数据
sprintf((char *)shared_memory, "Hello, World!");
// 等待1秒
sleep(1);
// 输出共享内存中的数据
printf("进程 A: %s\n", (char *)shared_memory);
// 解除映射关系
if (munmap(shared_memory, SHARED_MEMORY_SIZE) == -1)
{
perror("munmap"); // 打印错误信息
exit(1); // 退出程序
}
return 0;
}
然后创建一个名为demo56_mmap_b.c文件,向该文件写入以下内容,如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#define SHARED_MEMORY_SIZE 1024 // 定义共享内存的大小
int main()
{
// 打开共享内存对象,并获取文件描述符
int fd = shm_open("/myshm", O_RDWR, S_IRUSR | S_IWUSR);
if (fd == -1)
{
perror("shm_open"); // 打印错误信息
exit(1); // 退出程序
}
// 将共享内存区映射到当前进程的地址空间
int *shared_memory = mmap(NULL, SHARED_MEMORY_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (shared_memory == MAP_FAILED)
{
perror("mmap"); // 打印错误信息
exit(1); // 退出程序
}
// 关闭文件描述符
close(fd);
// 等待 1 秒钟
sleep(1);
// 输出共享内存中的数据
printf("进程 B: %s\n", (char *)shared_memory);
// 在共享内存中写入数据
sprintf((char *)shared_memory, "Goodbye, World!");
// 等待 1 秒钟
sleep(1);
// 输出共享内存中的数据
printf("进程 B: %s\n", (char *)shared_memory);
// 解除映射关系
if (munmap(shared_memory, SHARED_MEMORY_SIZE) == -1)
{
perror("munmap"); // 打印错误信息
exit(1); // 退出程序
}
return 0;
}
上面两个代码都创建了一个共享内存对象并将其映射到自己的地址空间中,然后在共享内存中写入数据并读取数据,由于打开的共享内存段相同,所以两个进程可以实现通信,如果进程B中打印的数据是进程A写入的数据证明通信成功。
保存退出之后,使用以下命令对 demo56_mmap_a.c和 demo56_mmap_b.c进行编译,编译完成如下图所示:
gcc -o demo56_mmap_a demo56_mmap_a.c -lrt
gcc -o demo56_mmap_b demo56_mmap_b.c -lrt
首先使用命令“ ./demo56_mmap_a”运行进程A的可执行程序,运行成功如下图所示:
然后使用命令“ ./demo56_mmap_b”运行进程B的可执行程序,运行成功如下图所示:
可以看到进程B首先会打印Hello, World!,证明进程A和进程B之间的通信完成了。