【Linux高级 I/O(5)】初识存储映射 I/O——mmap()和 munmap()(附代码示例)

news2025/1/11 23:54:57

存储映射 I/O       

        存储映射 I/O(memory-mapped I/O)是一种基于内存区域的高级 I/O 操作,它能将一个文件映射到进程地址空间中的一块内存区域中,当从这段内存中读数据时,就相当于读文件中的数据(对文件进行 read 操 作),将数据写入这段内存时,则相当于将数据直接写入文件中(对文件进行 write 操作)。这样就可以在不使用基本 I/O 操作函数 read()和 write()的情况下执行 I/O 操作。

mmap()和 munmap()函数

        为了实现存储映射 I/O 这一功能,我们需要告诉内核将一个给定的文件映射到进程地址空间中的一块内存区域中,这由系统调用 mmap()来实现。其函数原型如下所示:

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

        使用该函数需要包含头文件<sys/mman.h>。

        函数参数和返回值含义如下:

        addr:参数 addr 用于指定映射到内存区域的起始地址。通常将其设置为 NULL,这表示由系统选择该映射区的起始地址,这是最常见的设置方式;如果参数 addr 不为 NULL,则表示由自己指定映射区的起始地址,此函数的返回值是该映射区的起始地址。

        length:参数 length 指定映射长度,表示将文件中的多大部分映射到内存区域中,以字节为单位,譬如 length=1024 * 4,表示将文件的 4K 字节大小映射到内存区域中。

        offset:文件映射的偏移量,通常将其设置为 0,表示从文件头部开始映射;因此参数 offset 和参数 length 确定了文件的起始位置和长度,将文件的这部分映射到内存区域中,如下图所示。

        fd:文件描述符,指定要映射到内存区域中的文件。 prot:参数 prot 指定了映射区的保护要求,可取值如下:

  • PROT_EXEC:映射区可执行;
  • PROT_READ:映射区可读;
  • PROT_WRITE:映射区可写;
  •  PROT_NONE:映射区不可访问。

        可将prot指定为PROT_NONE,也可设置为PROT_EXEC、PROT_READ、PROT_WRITE中一个或多个(通过按位或运算符任意组合)。对指定映射区的保护要求不能超过文件 open()时的访问权限,譬如,文件是以只读权限方式打开的,那么对映射区的不能指定为 PROT_WRITE。

        flags:参数 flags 可影响映射区的多种属性,参数 flags 必须要指定以下两种标志之一:

  • MAP_SHARED:此标志指定当对映射区写入数据时,数据会写入到文件中,也就是会将写入到映射区中的数据更新到文件中,并且允许其它进程共享。
  • MAP_PRIVATE:此标志指定当对映射区写入数据时,会创建映射文件的一个私人副本(copy-on-write)对映射区的任何操作都不会更新到文件中,仅仅只是对文件副本进行读写。

除此之外,还可将以下标志中的 0 个或多个组合到参数 flags 中,通过按位或运算符进行组合:

  • MAP_FIXED:在未指定该标志的情况下,如果参数 addr 不等于 NULL,表示由调用者自己指定 映射区的起始地址,但这只是一种建议、而并非强制,所以内核并不会保证使用参数 addr 指定的值作为映射区的起始地址;如果指定了 MAP_FIXED 标志,则表示要求必须使用参数 addr 指定的值作为起始地址,如果使用指定值无法成功建立映射时,则放弃!通常,不建议使用此标志,因为这不利于移植。
  • MAP_ANONYMOUS:建立匿名映射,此时会忽略参数 fd 和 offset,不涉及文件,而且映射区域无法和其它进程共享。
  • MAP_ANON:与 MAP_ANONYMOUS 标志同义,不建议使用。
  • MAP_DENYWRITE:该标志被忽略。
  • MAP_EXECUTABLE:该标志被忽略。
  • MAP_FILE:兼容性标志,已被忽略。
  • MAP_LOCKED:对映射区域进行上锁。 除了以上标志之外,还有其它一些标志,这里便不再介绍,可通过 man 手册进行查看。在众多标志当中,通常情况下,参数 flags 中只指定了 MAP_SHARED。

        返回值:成功情况下,函数的返回值便是映射区的起始地址;发生错误时,返回(void *)-1,通常使用 MAP_FAILED 来表示,并且会设置 errno 来指示错误原因。

        对于 mmap()函数,参数 addr 和 offset 在不为 NULL 和 0 的情况下,addr 和 offset 的值通常被要求是系统页大小的整数倍,可通过 sysconf()函数获取页大小,如下所示(以字节为单位):

sysconf(_SC_PAGE_SIZE)
或
sysconf(_SC_PAGESIZE)

        虽然对 addr 和 offset 有这种限制,但对于参数 length 长度来说,却没有这种要求,如果映射区的长度不是页长度的整数倍时,会怎么样呢?对于这个问题的答案,我们首先需要了解到,对于 mmap()函数来说, 当文件成功被映射到内存区域时,这段内存区域(映射区)的大小通常是页大小的整数倍,即使参数 length 并不是页大小的整数倍。如果文件大小为 96 个字节,我们调用 mmap()时参数 length 也是设置为 96,假设系统页大小为 4096 字节(4K),则系统通常会提供 4096 个字节的映射区,其中后 4000 个字节会被设置为 0,可以修改后面的这 4000 个字节,但是并不会影响到文件。但如果访问 4000 个字节后面的内存区域,将会导致异常情况发生,产生 SIGBUS 信号。

        对于参数 length 任需要注意,参数 length 的值不能大于文件大小,即文件被映射的部分不能超出文件。

        与映射区相关的两个信号

  • SIGSEGV:如果映射区被 mmap()指定成了只读的,那么进程试图将数据写入到该映射区时,将会产生 SIGSEGV 信号,此信号由内核发送给进程。该信号的系统默认操作是终止进程、并生成核心可用于调试的核心转储文件。
  • SIGBUS:如果映射区的某个部分在访问时已不存在,则会产生 SIGBUS 信号。例如,调用 mmap() 进行映射时,将参数 length 设置为文件长度,但在访问映射区之前,另一个进程已将该文件截断 (譬如调用 ftruncate()函数进行截断),此时如果进程试图访问对应于该文件已截去部分的映射区, 进程将会受到内核发送过来的SIGBUS 信号,同样,该信号的系统默认操作是终止进程、并生成核心可用于调试的核心转储文件。

        munmap()解除映射

        通过 open()打开文件,需要使用 close()将将其关闭;同理,通过 mmap()将文件映射到进程地址空间中的一块内存区域中,当不再需要时,必须解除映射,使用 munmap()解除映射关系,其函数原型如下所示:

#include <sys/mman.h>

int munmap(void *addr, size_t length);

        使用该函数需要包含头文件<sys/mman.h>。

        munmap()系统调用解除指定地址范围内的映射,参数 addr 指定待解除映射地址范围的起始地址,它必须是系统页大小的整数倍;参数 length 是一个非负整数,指定了待解除映射区域的大小(字节数),被解除映射的区域对应的大小也必须是系统页大小的整数倍,即使参数 length 并不等于系统页大小的整数倍,与 mmap()函数相似。

        需要注意的是,当进程终止时也会自动解除映射(如果程序中没有显式调用 munmap()),但调用 close() 关闭文件时并不会解除映射。

        通常将参数 addr 设置为 mmap()函数的返回值,将参数 length 设置为 mmap()函数的参数 length,表示解除整个由 mmap()函数所创建的映射。

        使用示例

        通过以上介绍,接下来我们编写一个简单地示例代码,使用存储映射 I/O 进行文件复制。

        代码演示了使用存储映射 I/O 实现文件复制操作,将源文件中的内容全部复制到另一个目标文件中,其效果类似于 cp 命令。         

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>

int main (int argc, char *argv[]){
    int srcfd, dstfd;
    void *srcaddr;
    void *dstaddr;
    int ret;
    struct stat sbuf;

    if (3 != argc) {
        fprintf(stderr, "usage: %s <srcfile> <dstfile>\n", argv[0]);
        exit(-1);
    }

    /* 打开源文件 */
    srcfd = open(argv[1], O_RDONLY);
    if (-1 == srcfd) {
        perror("open error");
        exit(-1);
    }
    /* 打开目标文件 */
    dstfd = open(argv[2], O_RDWR |
    O_CREAT | O_TRUNC, 0664);
    if (-1 == dstfd) {
        perror("open error");
        ret = -1;
        goto out1;
    }
    
    /* 获取源文件的大小 */
    fstat(srcfd, &sbuf);
    /* 设置目标文件的大小 */
    ftruncate(dstfd, sbuf.st_size);

    /* 将源文件映射到内存区域中 */
    srcaddr = mmap(NULL,sbuf.st_size,PROT_READ,MAP_SHARED,srcfd,0);
    if(MAP_FAILED == srcaddr){
        perror("mmap error");
        ret = -1;
        goto out2;
    }

    /* 将目标文件映射到内存区域中 */
    dstaddr = mmap(NULL, sbuf.st_size,PROT_WRITE, MAP_SHARED, dstfd, 0);
    if (MAP_FAILED == dstaddr) {
        perror("mmap error");
        ret = -1;
        goto out3;
    }

    /* 将源文件中的内容复制到目标文件中 */
    memcpy(dstaddr, srcaddr, sbuf.st_size);

    /* 程序退出前清理工作 */
    out4:
        /* 解除目标文件映射 */
        munmap(dstaddr, sbuf.st_size);
    out3:
        /* 解除源文件映射 */
        munmap(srcaddr, sbuf.st_size);
    out2:
        /* 关闭目标文件 */
        close(dstfd);
    out1:
        /* 关闭源文件并退出 */
        close(srcfd);
        exit(ret);
}

        当执行程序的时候,将源文件和目标文件传递给应用程序,该程序首先会将源文件和目标文件打开,源文件以只读方式打开,而目标文件以可读、可写方式打开,如果目标文件不存在则创建它,并且将文件的大小截断为 0。

        然后使用 fstat()函数获取源文件的大小,接着调用 ftruncate()函数设置目标文件的大小与源文件大小保持一致。 然后对源文件和目标文件分别调用 mmap(),将文件映射到内存当中;对于源文件,调用 mmap()时将参 数 prot 指定为 PROT_READ,表示对它的映射区会进行读取操作;对于目标文件,调用 mmap()时将参数 port 指定为 PROT_WRITE,表示对它的映射区会进行写入操作。最后调用 memcpy()将源文件映射区中的内容复 制到目标文件映射区中,完成文件复制操作。

        接下来我们进行测试,笔者使用当前目录下的 srcfile 作为源文件,dstfile 作为目标文件,先看看源文件 srcfile 的内容,如下所示:

         由打印信息可知,程序运行完之后,生成了目标文件 dstfile,使用 cat 命令查看到其内容与源文件 srcfile 相同,本测试程序成功实现了文件复制功能!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/558421.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据库系统设计栗子】——图书借阅简单设计

图书借阅简单设计&#x1f60e; 前言&#x1f64c;需求分析——数据结构1.1图书信息1.2 书库信息1.3 图书库存信息1.4 用户1.5 借阅1.6 借阅记录概念模型E-R图 逻辑模型 总结撒花&#x1f49e; &#x1f60e;博客昵称&#xff1a;博客小梦 &#x1f60a;最喜欢的座右铭&#xf…

“三化”引领潮流,时尚代名词

大家好&#xff01;我是微三云小鱼&#xff01; 下面给大家分享一下“潮流文化” 如今越来越多的企业都在向数字化前进 而且也相信数字化代表着 向上、未来还是时尚&#xff0c; 各个企业都希望通过数字化 改变现代管理理念。 希望 像打开云计算一样拓展 另外一个渠道。 除了数…

Redis存在线程安全问题吗?让我们来谈谈!

大家好&#xff0c;我是你们的小米。在之前的文章中&#xff0c;我们谈到了Redis存在的线程安全问题。今天&#xff0c;我将以一个电商项目的实际案例来演示&#xff0c;为大家详细解析Redis线程安全问题的原因&#xff0c;并分享一些具体的解决措施。 为什么存在线程安全问题&…

Java程序设计入门教程--物体的抽象过程

类的概念 面向对象的思想来源于对客观世界的认知。 现实的世界是缤纷复杂、种类繁多&#xff0c;难于认识和理解的&#xff0c;但聪明的人们学会了把这些错综复杂的事物进行分类&#xff0c;从而使世界变得井井有条。比如我们由各式各样的汽车抽象出汽车的概念&#xf…

chatgpt赋能Python-python_label颜色

Python Label 颜色的重要性 在Python编程中&#xff0c;我们经常会使用Label来表示文本标签&#xff0c;并且经常需要为这些标签创建不同的颜色&#xff0c;以区分和凸显关键信息。正确选择和使用标签颜色将有助于提高代码的可读性和可维护性&#xff0c;并且在应用程序和Web开…

云渲染时能否关机或断网?

先说一下云渲染&#xff1a; 基于渲染农场&#xff0c;用户可以将自己制作好的文件打包&#xff0c;通过云渲染客户端将打包文件上传到云渲染的服务器进行渲染。以下是 云渲染中能否关电脑的相关回答&#xff1a; 1.提交、上传文件时可以关电脑吗&#xff1f; 不能。文件提交是…

【敬伟ps教程】颜色和图案的填充

文章目录 油漆桶工具填充命令前景色内容识别图案历史记录黑白灰 渐变图层样式填充填充图层 油漆桶工具 油漆桶工具可以填充前景色和图案&#xff0c;快捷键 G 选好前景色&#xff0c;点击画布&#xff0c;画布就会被填充前景色&#xff1b; 建立选区后&#xff0c;填充会在选…

迅为龙芯2K1000开发板国产处理器操作系统

1、硬件配置 国产龙芯处理器&#xff0c;双核64位系统&#xff0c;板载2GDDR3内存&#xff0c;流畅运行Busybox、Buildroot、Loognix、QT5.12 系统! 2、接口全 板载4路USB HOST、2路千兆以太网、2路UART、2路CAN总线、Mini PCIE、SATA固态盘接口、4G接口、GPS接口WIFI、蓝牙…

算法27:从暴力递归到动态规划(1)

题目&#xff1a;已知数列的规则为 1 1 2 3 5 8 13 21 ..... * 按照这种规则&#xff0c;求第n项, n > 2. 这是典型的斐波拉切数列, 公式为 F(n)F(n - 1)F(n - 2) 那么就可以推导出 F(n)F(n - 1) F(n - 2) F(n-1)F(n - 2) F(n - 3) F(n-2)F(n - 3) F(n - 4) F(3)F(n -…

Spring Boot 如何处理国际化

Spring Boot 国际化 在全球化的今天&#xff0c;很多应用程序需要支持多种语言和地区。为了满足不同用户的需求&#xff0c;应用程序需要提供多语言的支持。Spring Boot 提供了强大的国际化支持&#xff0c;使得开发人员能够轻松地为应用程序添加多语言支持。本文将介绍如何使…

chatgpt赋能Python-python_id用法

Python ID用法介绍 在 Python 编程中&#xff0c;ID 是一个极其重要的概念。ID 是一个对象在内存中的唯一标识符&#xff0c;每个对象都有一个唯一的 ID。在本文中&#xff0c;我们将介绍 Python ID 的用途和用法&#xff0c;并且给出一些示例&#xff0c;以帮助读者更好地理解…

lab4:以time/gettimeofday系统调用为例分析ARM64 Linux 5.4.34

一、ARM64 Linux系统调用过程 &#xff08;1&#xff09;svc指令触发系统调用。 &#xff08;2&#xff09;保存现场&#xff08;el0_sync处的内核汇编代码保存异常发生时程序的执行现场&#xff09;&#xff0c;然后根据异常发生的原因&#xff08;ESR_EL1寄存器&#xff09;…

“不务正业”的奶茶店三个月实现30+万收入

今天我和大家分享一个 我身边的案例。 我有一个朋友 和我分享他朋友的 一个奶茶店 互联网商城的故事。 19年李某开了一家 奶茶店&#xff0c;同时呢 自己在平台做了一个 线上购买奶茶的商城 他是怎么做的呢&#xff1f; 原来每次有客户来到店 购买奶茶的时候。 他会和客户说 扫…

BetaFlight Mark4 H7 Dual270 + BN880 + CRSF 配置存档

BetaFlight Mark4 H7 Dual270 BN880 CRSF 配置存档 1. 源由2. 配置2.1 端口2.2 系统2.3 对齐2.4 GPS2.5 救援2.6 PID2.7 Rate2.8 滤波2.9 接收器2.10 模式2.11 电机 3.差异4. 整机效果5. 飞行效果6. 参考资料 1. 源由 手头这台航模四轴&#xff0c;基本调试的差不多&#xf…

【数据分析之道-Numpy(八)】numpy统计函数

文章目录 专栏导读1、np.mean()2、np.median()3、np.std()4、np.var()5、np.min()6、np.max()7、np.sum()8、np.prod()9、np.percentile()10、np.any()11、np.all() 专栏导读 ✍ 作者简介&#xff1a;i阿极&#xff0c;CSDN Python领域新星创作者&#xff0c;专注于分享python领…

Qt QGenericPlugin插件使用案例

问题描述: Qt插件的编写,有两种方式,一种是直接通过自定义接口类Interface来实现,一种是通过QtCreator自带的插件模板来创建。 这里我们先来实现第二种。 功能为点击主界面的按钮,显示插件界面。(插件和开发库一样,什么都可以放进去,只不过就是封装成方便调用的模块…

Vue|非单文件组件

传统网页一些不可避免的小问题: 1.网页JS、CSS等资源依赖关系混乱,不方便维护 2.代码复用率很低 使用组件将代码进行复用,简化项目结构,提高运行效率,便于维护 组件定义传统网页组件 传统代码实现步骤 组件代码定义组件注册组件局部注册全局注册 使用组件避坑 组件定义 组件即为…

聊聊如何利用spring插件来实现策略模式

前言 偶然的机会发现spring有个spring-plugin&#xff0c;官网对它的介绍是 Spring Plugin provides a more pragmatic approach to plugin development by providing the core flexibility of having plugin implementations extending a core system’s functionality but o…

linux上使用系统安装和Docker安装mysql的两种方式

一、安装到linux 1、安装mysql-server 1、在安装之前查看下系统是否已经安装了mysql ls /usr/share2、安装mysql-server sudo apt-get install mysql-server3、再次查看&#xff0c;发现多了个mysql ls /usr/share | grep mysql //在ls打印结果中搜索mysql关键字4、登陆 在…

chatgpt赋能Python-python_lamb

Python Lambdas - 强大的匿名函数 Python是一个充满了强大特性的编程语言&#xff0c;其中之一就是Python的lambda函数。在这篇文章中&#xff0c;我们将介绍Python lambdas的基础知识、使用方法、优缺点以及与普通函数的区别。 什么是Python Lambda函数 Python Lambda函数&…