新手学习UC记录——mmap和munmap的使用

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

目录

前言

一、程序是如何运行的?

二、页面换出与页面换入

1.页面换出

2.页面换入

3.交换分区

4.计算机变卡的原因

三、虚拟地址

1.mmap函数

2.装载器

a.为什么要映射虚拟地址和物理地址,直接使用一个地址不好吗?

四、虚拟地址空间

1.虚拟地址空间

 a.32位系统下的虚拟地址空间

  b.64位系统下的虚拟地址空间

  c.总结

2.用户态与内核态

  a.用户态

  b.内核态

  c.为什么要区分内核态与用户态

  d.总结

五、虚拟地址空间布局

1.总结

六、mmap函数的使用

1.内存映射的建立与接触

a.代码演示

b.验证映射是否接触

​编辑

c.munmap函数

2.基于文件的写操作

a.总线错误

b.相对于write函数来说mmap函数有什么优势



前言

关于对mmap和munmap两个函数在uc下的使用,以及个人关于这个以外的一些概述,如有错误的地方,请大家及时指出:


提示:以下是本篇文章正文内容,下面案例可供参考

一、程序是如何运行的?

要明白这两个函数,我们首先先要明白一个程序是如何在我们操作系统上跑起来的。

1.经由操作系统将a.out程序读取到内存中

2.解析器解析程序,将其转换成计算机能识别的指令

3.装载器将解析后的数据加载到内存的特定位置,以便后续使用

4.CPU读取指令,并执行指令

二、页面换出与页面换入

1.页面换出

将程序由内存移动到交换分区上,释放该程序的物理内存空间供其它进程使用

2.页面换入

将程序重新从交换分区移动回物理内存中

3.交换分区

实际是用作减少物理内存负担,减少物理内存不足的情况发生

4.计算机变卡的原因

大量的页面换入换出会导致计算机变卡,因为磁盘的读写速率远远低于内存的读写速率,也是为了加快内存读写磁盘的速率,于是便有了磁盘缓存。

三、虚拟地址

可能看到这的大家就会问,程序的运行又和mmap函数有何关系,那么接下来我们就要先搞清楚mmap函数主要用于实现的功能。

1.mmap函数

代码如下(示例):

#include <sys/mman.h>  //所包含头文件
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
//建立虚拟内存的起始地址
/*
    addr:映射区虚拟内存的起始地址,如果取NULL,则由内核选择创建映射地址
    length:指定映射长度,按页取整(4096)
    prot:映射区操作权限,可取以下值:
        -PROT_READ: 映射区可读
		-PROT_WRITE: 映射区可写
		-PROT_EXEC: 映射区可执行
        -PROT_NONE: 映射区不可访问
    flags:映射标志,可取以下值:
        -MAP_ANONYMOUS:匿名映射,将虚拟内存映射到物理内存而非文件,忽略fd和offset参数
		-MAP_PRIVATE:对映射区的写操作只反应到缓冲区中而不会真正写入文件
		-MAP_SHARED:对映射区的写操作直接反映到文件中
		-MAP_DENYWRITE:拒绝其它文件的写操作
		-MAP_FIXED:若在start上无法创建映射,则失败
    fd:文件描述符
    offset:文件偏移量,自动按页对齐
    返回值:成功返回映射虚拟内存起始地址,失败返回MAP_FAILED(-1)
*/

PS:关于代码的使用会在目录(mmap和munmap函数的使用)演示,接下来会先讲理论

2.装载器

在装载器将解析后的程序装在到内存中的时候,我们做了一个虚拟地址与物理地址的映射

虚拟内存地址:程序指令中所包含的内存地址

物理内存地址:实际该程序在内存中存储的地址

a.为什么要映射虚拟地址和物理地址,直接使用一个地址不好吗?

 1. 确保程序能够正常载入内存,在实际的程序加载时,你是无法想到你加载程序的地址所对应一样的内存上的地址有没有程序在使用。

2. 确保安全性,保证物理内存的安全,如果直接返回一个实际的物理内存地址,那么就可能会发生当前地址程序被覆盖的情况

四、虚拟地址空间

1.虚拟地址空间

  所拿到的虚拟地址存在一个地址范围,会根据操作系统的字节来分配相应地址范围中的地址

  如果使用未对应的物理地址的虚拟地址,会报错:段错误,核心已转储

 a.32位系统下的虚拟地址空间

    32bit的系统上虚拟地址范围为4G

    每个程序都有4G的虚拟地址空间,但并不代表物理内存上这个程序就占4G

    Windows系统:用户地址空间与内核地址空间各占2G

    linux系统中:4G的虚拟地址范围被分为1G和3G,3G是用户本身在用,称作用户地址空间,1G是内核在用,称作内核地址空间

  b.64位系统下的虚拟地址空间

   64bit的系统上的用户虚拟地址范围在0x0000 0000 0000 0000-0x0000 FFFF FFFF FFFF

   内核地址空间范围在:0xFFFF 0000 0000 0000 - 0xFFFF FFFF FFFF FFFF

   内核地址空间和用户地址空间之间是不规范地址空间,不允许使用

  c.总结

  用户地址空间的代码不能直接访问内核空间的代码和数据,但可以通过系统调用进入内核态,间接与系统内核交互

2.用户态与内核态

  a.用户态

   特权级别处于最低级的态(特权级为3,特权级数字越小表示可访问权限越高),在此态中,访问资源会受到限制,无法访问并使用硬件设备资源,需要通过系统调用的方式进入内核态方可使用

  b.内核态

   特权级别处于最高级的态(特权级为0),在此态中,访问不受限制,且可以使用特权指令,能使用更多的操作

  c.为什么要区分内核态与用户态

    为保证系统安全,如果当前用户所使用的所有程序都拥有内核态的权限,那么一些特权指令,比如I/O设备操作指令、系统资源控制和管理指令等等都能轻易使用,那这样是对系统来说及其危险的。于是会区分用户态与内核态。

  d.总结

    通常情况下,我们执行的一个代码程序是有用户态到内核态再到用户态的转变的,当程序执行到系统调用函数时,我们就会从用户态切换到内核态,此时寄存器就会将用户态的系统调用函数转换成系统调用号以及请求的参数(就是函数内参数)保存在寄存器中,然后执行中断操作进入内核态,内核态就会执行中断处理操作,在此期间就会将寄存器保存到内核的数据库中,再将所需要的(系统调用号和请求参数)拿出来,通过系统调用表找到系统调用号所对应的系统调用函数,将参数与之结合,便再次切换回用户态,继续执行接下来的操作

五、虚拟地址空间布局

//虚拟地址空间布局
#include <stdio.h>
#include <stdlib.h>

//bss区
int bss_b;
//数据区
int data_c = 3;
//代码区
const int text_d = 1;
int main(int argc , char* argv[] , char* envp[]){
    //栈区
    int stack_a = 1;
    const int stack_const_a = 2;
    //堆区
    int* ptr = malloc(sizeof(int));
    //BSS区
    static int bss_static_b;
    //数据区
    static int data_static_c = 3;
    //代码区
    const static int static_text_d = 1;
    char* e = "hello";
    printf("----------虚拟地址空间布局--------------\n");
    printf("-----------参数和环境区-------------\n");
    printf("命令行参数:%p\n",argv);
    printf("环境变量:%p\n",envp);
    printf("-----------栈区--------------\n");
    printf("局部变量:%p\n",&stack_a);
    printf("常属性变量:%p\n",&stack_const_a);
    printf("-----------堆区------------\n");
    printf("malloc内存分配:%p\n",ptr);
    printf("-----------bss区-------------\n");
    printf("未被初始化的全局变量:%p\n",&bss_b);
    printf("未被初始化的静态局部变量:%p\n",&bss_static_b);
    printf("-------------数据区------------\n");
    printf("初始化的全局变量:%p\n",&data_c);
    printf("初始化的静态局部变量:%p\n",&data_static_c);
    printf("-------------代码区----------\n");
    printf("具有常属性且被初始化的全局变量:%p\n",&text_d);
    printf("具有常属性且被初始化的静态局部变量:%p\n",&static_text_d);
    printf("字面值常量:%p\n",e);
    printf("函数:%p\n",main);
    return 0;
}

------------------运行结果---------------------
----------虚拟地址空间布局--------------
-----------参数和环境区-------------
命令行参数:0x7fffdb3f0578
环境变量:0x7fffdb3f0588
-----------栈区--------------
局部变量:0x7fffdb3f0470
常属性变量:0x7fffdb3f0474
-----------堆区------------
malloc内存分配:0x2108010
-----------bss区-------------
未被初始化的全局变量:0x601060
未被初始化的静态局部变量:0x60105c
-------------数据区------------
初始化的全局变量:0x601050
初始化的静态局部变量:0x601054
-------------代码区----------
具有常属性且被初始化的全局变量:0x400868
具有常属性且被初始化的静态局部变量:0x400b18
字面值常量:0x40086c
函数:0x400626

1.总结

虚拟地址空间布局使得每个进程都有着独立的地址空间,保证进程间的相互隔离,确保进程之间不会出现访问冲突,以及恶意进程盗窃重要资料等等情况的发生

六、mmap函数的使用

1.内存映射的建立与接触

a.代码演示

//内存映射的建立与解除
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>

int main(void){
    //建立匿名映射
    char* start = mmap(NULL,4096,PROT_READ | PROT_WRITE,MAP_ANONYMOUS | MAP_PRIVATE,0,0);
    if(start == MAP_FAILED)
    {   
        perror("mmap");
        return -1; 
    }   
    strcpy(start,"hello world");
    printf("start:%s\n",start);
    //解除映射
    if(munmap(start,4096) == -1) 
    {   
        perror("munmap");
        return -1; 
    }   

b.验证映射是否接触

//验证映射是否解除
    printf("start:%s\n",start);

可以发现解除映射后出现段错误,出现段错误的原因常常是因为对虚拟内存的越权访问或者访问没有映射到物理内存的虚拟地址

c.munmap函数

#include <sys/mman.h> //所需头文件
int munmap(void *addr, size_t length);
/*
    addr:映射区虚拟内存的起始地址
    length:映射区字节数,自动按页取整
    返回值:成功返回0,失败返回-1
*/

此外这个函数可以只接触一部分的映射,我们通过代码来验证一下

//内存映射的建立与解除
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>

int main(void){
    //建立映射
    char* start = mmap(NULL,4096*2,PROT_READ | PROT_WRITE , MAP_ANONYMOUS | MAP_PRIVATE , 0 ,0);
    if(start == MAP_FAILED)
    {   
        perror("mmap");
        return -1; 
    }   
    strcpy(start,"hello world");
    printf("start:%s\n",start);
    //解除部分映射
    if(munmap(start,4096) == -1) 
    {   
        perror("munmap");
        return -1; 
    }   
    //验证虚拟内存是否还存在内容
    char* start_new = start + 4096;
    printf("start:%s\n",start_new);
    return 0;
}

PS:在演示代码中,会发现新创了一个变量start_new,会什么要这么做,是为了避免段错误的再次出现,munmap函数是通过虚拟内存的起始地址开始解除映射length个字节,如果我们不加上4096,就会导致访问到没有映射物理内存的虚拟地址

2.基于文件的写操作

a.总线错误

//对文件的写操作
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int main(void){
    //打开文件,获取文件描述符
    int fd = open("./c.txt" , O_RDWR | O_CREAT | O_TRUNC , 0664);
    if(fd == -1) 
    {   
        perror("open");
        return -1; 
    }   
    /*  
    //修改文件大小
    if(ftruncate(fd,4096) == -1)
    {
        perror("ftruncate");
        return -1;
    }
    */
    //使用mmap对文件写入
    char* start = mmap(NULL , 4096 , PROT_READ | PROT_WRITE , MAP_SHARED , fd , 0); 
    if(start == MAP_FAILED)
    {   
        perror("mmap");
        return -1; 
    }   
    strcpy(start,"hello,world");
    //关闭文件
    if(munmap(start,4096) == -1) 
    {   
        perror("munmap");
        return -1; 
    }   
    close(fd);
    return 0;
}

我们可以发现,执行过程虽然没有任何的语法错误,错误处理也没有发生,但执行结果依然报错:总线错误,这原因是因为我们所创建的文件c.txt并没有分配大小

于是就要使用ftruncate函数对文件分配大小

//对文件的写操作
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int main(void){
    //打开文件,获取文件描述符
    int fd = open("./c.txt" , O_RDWR | O_CREAT | O_TRUNC , 0664);
    if(fd == -1) 
    {   
        perror("open");
        return -1; 
    }   
    //修改文件大小
    if(ftruncate(fd,4096) == -1) 
    {   
        perror("ftruncate");
        return -1; 
    }   
    //使用mmap对文件写入
    char* start = mmap(NULL , 4096 , PROT_READ | PROT_WRITE , MAP_SHARED , fd , 0); 
    if(start == MAP_FAILED)
    {   
        perror("mmap");
        return -1; 
    }   
    strcpy(start,"hello,world");
    //关闭文件
    if(munmap(start,4096) == -1) 
    {   
        perror("munmap");
        return -1; 
    }   
    close(fd);
    return 0;
}

b.相对于write函数来说mmap函数有什么优势

write函数是通过复制传入参数缓冲区的内容到内核空间,再有系统内核写入文件,这经历了用户态到内核态的转换,一次两次可能还好,但如果需要长久传输数据的话,这种频繁的拷贝和系统调用会导致资源损耗

mmap函数则是直接通过内存映射文件,在频繁的写入操作中,并不需要进行多次的用户态与内核条的切换,后续只需要在用户态中对虚拟内存进行写入便可以,这大大减少的系统调佣的开销

  • 10
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值