mmap适用场景
设备节点文件介绍
在 Linux 中,设备节点文件(通常称为设备文件)是特殊类型的文件,它们代表系统中的物理设备或虚拟设备。这些文件位于 /dev
目录下,用于与设备进行交互。设备节点文件允许用户空间程序通过文件系统接口与硬件设备进行交互。程序可以像读取或写入普通文件一样操作设备节点文件,从而实现与硬件设备的通信。
- 设备节点文件属于 Linux 七种基本文件类型中的“字符设备文件”和“块设备文件”。
- 设备节点文件通常挂载在
/dev
目录下。 - 一个设备可能不止对应一个设备节点文件,比如说我们的磁盘,一个磁盘的每个分区都对应着一个设备节点文件。还有一些复杂设备,设备的不同功能需要多个设备文件对设备进行更好的管理,比如LCD屏幕,显示图像的功能和触摸功能分别对应不同的设备节点文件进行管理。对于简单的设备,比如键盘鼠标和串口设备,只需要一个设备节点文件就够了。
- 这些设备节点文件通常在系统启动时由设备管理器(如
udevd
)自动创建。当然,如果有需要的话,你也可以使用mknod
命令手动创建设备节点文件。
mmap和write的对比
说到读写文件,我们肯定会先想到我们之前学习到的read和write函数,但是对于一些设备节点文件来说,用read和write来读写文件是十分不方便的,下面我们就拿LCD屏幕来举例说明。
read和write操作的特点
-
数据流模式:
write
以数据流的方式将数据写入设备,可能需要多次调用write
来传输整个图像。这对于需要频繁更新显示内容的应用可能会导致性能问题,因为每次write
调用都会涉及到系统调用的开销和数据传输延迟。 -
数据处理复杂:对于复杂的图像数据,可能需要在应用层进行图像处理(例如调整格式、大小,图像的显示位置),然后通过
write
将处理后的数据写入设备。这可能会导致额外的代码复杂度和性能开销,也会增加我们写代码的难度。
所以综合看来在LCD上显示图片需要进行大量数据的快速读写,这时候使用read和write不仅速度上显得力不从心,代码编写上难度也更大。
mmap操作的特点
-
内存映射:
mmap
将设备的内存区域直接映射到进程的虚拟地址空间,使得你可以直接在内存中操作图像数据,然后一次性更新设备内容。这种方法减少了系统调用的开销,适合处理大块数据或频繁更新的场景。 -
直接访问:通过
mmap
,你可以将整个图像数据直接写入映射的内存区域,这样可以快速地更新屏幕显示。例如,你可以将整个图像数据拷贝到映射的内存中,然后更新屏幕显示,这样效率更高。
对比write和mmap
write
适用于小块数据和简单的数据流操作,但对于大块数据或频繁更新的场景可能不够高效。mmap
提供了高效的内存映射方式,适合处理大块数据和需要频繁更新的应用,例如 LCD 屏幕显示图片。
当我们使用 mmap
将设备的内存区域映射到进程的虚拟地址空间时,这个内存区域可以被视为显存(或称为显示内存)。
如何利用mmap来读写设备节点文件
mmap
是一个用于内存映射文件的系统调用,它将文件或设备的内容映射到进程的虚拟内存空间中。通过这种方式,应用程序可以通过内存操作来访问和更改对应文件内容,而不是使用传统的读写系统调用来对文件进行读写。这种方法在处理大数据文件或设备时,尤其是需要高效读写操作时非常有用。
mmap参数详细介绍
addr: 指定映射内存的起始地址。通常设置为 NULL
,让系统选择合适的地址。如果指定了地址,系统将尝试在该地址进行映射,但可能会失败。
length:映射内存的大小。映射内存和要显示的内容大小不一定要一定相等,映射内存大小可以是显示页面大小的整数倍,这样的话我们可以做到之后显示的图片提前加载,显示不同的图片通过控制offset偏移量来实现,显示速度更快。如下图所示
prot:用于设置映射区域的内存保护标志,决定了当前进程对映射内存区域的访问权限。
flags:flag中可以选的值比较多,我们这里重点关注一下MAP_SHARED和MAP_PRIVATE这两个值,它们是互斥的,不能同时使用。
MAP_SHARED
:多个进程都想要读写某个设备文件,对于设备文件的映射内存是同一块,多个进程共享和同步对设备(如 LCD 屏幕)的访问的情况。所有进程都能看到并影响同一块内存区域的内容,并且修改会直接影响设备的显示(即对映射内存区域的修改会自动同步到原始文件或设备上)。MAP_PRIVATE
:多个进程都想要读写某个设备文件,每个进程对这个设备文件都有单独的映射内存,映射区域对其他进程不可见,对映射区域的写操作不会直接影响原始文件(对映射内存区域的修改不会自动同步到原始文件或设备上)。多进程显示不同内容的情况下,可能需要通过硬件支持、屏幕分区或额外的屏幕管理机制来实现最终显示效果。
fd:文件描述符
offset:文件或设备的起始偏移量,从该位置开始映射。这个偏移量一般是页面大小的整数倍。
mmap使用示例
注意点:
1. 在较旧的Linux内核(如2.6内核)中,可以直接使用mmap()来给LCD设备映射内存,但在较新Linux内核(如4.4内核)中,则需要经由DRM统一管理,不可直接mmap映射显存。
2. mmap函数理论上可以对任意文件进行内存映射,但通常用来映射一些比较特别的设备文件,比如液晶屏LCD。
映射LCD文件:
int main()
{
// 以读写权限打开液晶屏文件
int lcd = open("/dev/fb0", O_RDWR);
// 给LCD设备映射一块内存(或称显存)
char *p = mmap(NULL, 800*480*4, PROT_WRITE,
MAP_SHARED, lcd, 0);
// 通过映射内存,将LCD屏幕的每一个像素点涂成红色
int red = 0x00FF0000;
for(int i=0; i<800*480; i++)
memcpy(p+i*4, &red, 4);
// 解除映射
munmap(p, 800*480*4);
return 0;
}
1.这里映射内存的大小是800*480*4,其中800*480是LCD屏幕的尺寸,4是每个像素点的深度,即每个像素点的颜色数据所占4个字节。这里假如我们不知道设备的尺寸,可以通过fcntl函数来获取,获取方法这里不详细展开。
2. 这里为什么一个像素点的颜色占4个字节,以及这里的红色定义为什么是0x00FF0000?
使用四个字节(32 位)表示颜色通常包括红色、绿色、蓝色和透明度(Alpha)四个通道。这样的表示方式称为 RGBA 颜色模式。在 32 位颜色表示中,每个字节分配给一个颜色通道,通常的字节顺序是:透明度,红色,绿色,蓝色。
| Alpha (A) | Red (R) | Green (G) | Blue (B) |
| 8 位 | 8 位 | 8 位 | 8 位 |
3. memcpy函数用于将内存中的一块区域的内容复制到另一块内存区域。它的定义在 <string.h>
头文件中。
错误码的进一步了解
回顾
在前面的文章中:认识I/O(文件的open,close,read,write,lseek,错误码,文件描述符,文件读写位置)-CSDN博客
我们已经了解过了错误码可以在库函数执行出错的时候自动设置,我们不仅可以通过库函数的返回值 (比如-1)来知道这个函数运行发生了错误(定位),还可以通过错误码来更精确地知道具体是什么原因导致了函数执行错误(找具体原因)。
并且我们了解到了有两种使用错误码的方式:strerror(errno)
1. perror("打开a.txt失败"); //输出提示字符串+详细错误原因,这种方式使用起来简单。
2. strerror(errno)函数,可以将 errno 这个整数值转换为对应的错误描述字符串。在这个错误描述字符串前后我们可以更加灵活地加上一些提示字符串信息,但是这种方式代码更加麻烦。
补充内容
1.errno
是一个全局变量,可以被所有包含<errno.h>
头文件的库函数使用,专门用于存储系统调用和标准库函数在发生错误时的错误码。它由标准库函数和系统调用自动设置,以便在错误发生时提供具体的错误信息。
2. errno能在自定义函数中被使用,这里的使用指的是自定义函数包含<errno.h>
头文件,对错误码进行查看,但是不能对其进行修改赋值操作,错误码通常只能由库函数来修改赋值。
3.errno的值在每次库函数调用之后可能会被更改,全局变量errno只会存储最近一次发生错误时的错误码,因此在进行错误检查时,需要在调用库函数后立即处理 errno,防止错误码在其他库函数执行后改变。
4.自定义函数不能直接修改 errno
,也不能将自定义错误码保存到 errno
中,只能查看。所以在自定义函数中,我们可以定义自己的错误码,具体可以通过函数的返回值或设置其他全局或静态变量来报告错误。这些错误码可以使用宏定义或枚举类型来表示,使代码更加清晰。除此之外,我们还可以采用定义封装单独的错误码的.c和.h文件,来让我们的多个文件之间共享错误码,更加方便我们的程序。这种做法可以使代码更模块化、可维护性更高,并且方便不同部分的代码之间共享错误码而不必重复定义。
库函数返回值+错误码—>定位错误位置+错误类型的例子
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h> // 全局错误码声明所在的头文件
int main()
{
int fd = open("a.txt", O_RDWR);
if(fd == -1)
{
// 以下两条语句效果完全一致:输出提示+函数出错的原因
perror("打开a.txt失败");
//perror 函数将输出一个描述性错误消息。消息的内容包括你传递给 perror 的字符串 s,相当于作为提示信息了,后面跟的是系统错误的说明
printf("打开a.txt失败:%s\n", strerror(errno));
}
return 0;
}
还有值得注意的一点是:
不同库函数执行错误时的返回值也不一样,我们需要仔细查阅库函数的帮助文档来查看该函数返回值。
比如mmap函数成功执行,它将返回一个指向映射区域的指针。错误执行时我们不能想当然地认为是-1,这里mmap返回值类型是指针类型,也不可能是返回-1。事实上 mmap
函数执行失败,它将返回 MAP_FAILED
。MAP_FAILED
是一个宏,通常定义为 (void *) -1
。你可以通过检查 mmap
的返回值是否等于 MAP_FAILED/
(void *) -1
来判断函数是否执行失败。