45-内存映射的陷阱

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35733751/article/details/82948869

       本篇将介绍内存映射过程中的一些系统分页边界陷阱,可助你打通任督二脉,掌握内存映射基本的使用和出错处理。

 

1. 第一种情况

之前有说过在调用mmap函数创建内存映射时,建议指定参数length最好是系统内存分页的整数倍,虽然这并不是强制的,但这么做当然是有原因的,因为参数length恰好是系统内存分页的整数倍,那么内存映射区和文件映射区范围恰好是一样的。

我们来看一个mmap函数在映射时,length不是系统内存分页的整数倍的情况:调用mmap(NULL , 6000 , prot , MAP_SHARED , fd , 0); ,创建一个6000字节大小的内存映射区,我们来看看系统内核会怎么做

                                                                       图1-length不是系统分页的整数倍

由于length不是系统内存分页的整数倍,那么在真实的映射过程中,系统内核会在内存创建8192大小的映射区(也就是说,系统内核会偷偷调整为内存分页的整数倍)。同理,被打开的文件实际上也会被映射成8192字节大小的真实映射区域。

也就是说,应用程序调用mmap希望创建一个6000字节的内存映射区,但实际上系统内核并没有这样做。应用程序依然可以访问6000 - 8191字节区域,且这个区域的修改操作也会反映到打开的文件中

当程序试图内存映射区(0 - 8191)之外的字节区域时将会发生段错误并收到SIGSEGV信号,该信号的默认动作是终止进程并打印core dump,SIGSEGV信号表示访问的地址无效,即没有物理内存对应该地址,只要超过了映射区域就是非法访问。

 

扫描二维码关注公众号,回复: 3447039 查看本文章

2. 模拟收到SIGSEGV信号实验

模拟程序访问内存映射区之外的区域引发SIGSEGV信号。

 

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

//信号处理函数
void sig_handler(int sig){
        if(sig == SIGSEGV){
                puts("catch SIGSEGV");
                //模拟收到信号,然后进程退出
                exit(-1);
        }
}

int main(void){
        char *addr;
        int len = 0;
        int ret;
        int fd = open("test.txt", O_RDWR|O_CREAT, 0644);
        if (fd < 0){
                perror("open error");
        }

        //捕捉SIGSEGV信号
        signal(SIGSEGV , sig_handler);

        //拓展文件大小9500
        lseek(fd , 9500 , SEEK_SET);
        write(fd , "0" , 1);
        //指向文件开头
        lseek(fd , 0 , SEEK_SET);

        //创建6000字节大小的共享映射区,可读写
        addr =(char *)mmap(NULL , 6000 , PROT_WRITE|PROT_READ , MAP_SHARED , fd , 0);
        if (addr == MAP_FAILED){
                perror("mmap err: ");
        }
        close(fd);
        //正常访问
        strcpy(addr , "hello world");
        printf("%s\n", addr);

        //指针越界
        int i;
        for(i = 0; i < 8192; i++){
                addr++;
        }

        //ddr指针指向超过了内存映射区,此时访问指针将会收到SIGSEGV信号
        strcpy(addr , "ABCDEF");
        //解除映射
        /*
        ret = munmap(addr , 1024);
        if(ret < 0){
                perror("munmap error: ");
        }
        */
        return 0;
}

 

程序执行结果:

收到SIGSEGV信号后,进程终止时打印了core dump。

 

 

3. 第二种情况

再来看一个更加复杂的情况,当内存映射区域超过了打开文件的大小:调用mmap(NULL , 8192, prot , MAP_SHARED , fd , 0); ,创建一个8192字节大小的内存映射区。

                                                      图2-内存映射超过了打开的文件大小

 

如上图所示,当调用mmap创建一个8192字节大小的内存映射区时(是系统分页的整数倍),系统会在内核中分配一个8192大小的缓冲区。由于打开的文件只有2200字节大小,不是系统分页的整数倍,这种情况下,在进行映射过程中系统内核会偷偷调整为系统分页的整数倍。

虽然内存映射区域被系统调整为4096大小,但实际上内存映射区只有0 - 2199区域被映射到文件,剩下的2200-4095区域依然可以访问,但是在该区域的修改操作将不会反映到打开的文件中,并且这段区域将会初始化为0,同时该区域不会对其他进程共享。

如果试图访问超过映射区域(4096-8191区域)将会收到SIGBUS信号并产生Bus error错误,SIGBUS信号表示指针对应的地址是有效的,但总线不能正常使用该指针。因此创建一个超过打开文件大小的内存映射区可能会产生错误,但是可以通过扩展文件大小(例如lseek函数)使得映射之前不可访问的部分变得可用。

 

4. 第二种情况模拟实验

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

//SIGSEGV信号
void sig_handler1(int sig){
        if(sig == SIGSEGV){
                puts("catch SIGSEGV");
                //模拟收到信号,然后进程退出
                exit(-1);

        }
}

//SIGBUS信号
void sig_handler2(int sig){
        if(sig == SIGBUS){
                puts("catch SIGBUS");
                exit(-1);
        }
}

int main(void){
        char *addr;
        int len = 0;
        int ret;
        int fd = open("test.txt", O_RDWR|O_CREAT, 0644);
        if (fd < 0){
                perror("open error");
        }

        //捕捉SIGSEGV信号
        signal(SIGSEGV , sig_handler1);
        //捕捉SIGBUS信号
        signal(SIGBUS , sig_handler2);

        //拓展文件大小2200
        lseek(fd , 2200 , SEEK_SET);
        write(fd , "0" , 1);
        //指向文件开头
        lseek(fd , 0 , SEEK_SET);

        //创建8192字节大小的共享映射区,可读写
        addr =(char *)mmap(NULL , 8192 , PROT_WRITE|PROT_READ , MAP_SHARED , fd , 0);
        if (addr == MAP_FAILED){
                perror("mmap err: ");
        }

        //正常访问
        strcpy(addr , "hello world");
        printf("%s\n", addr);

        //指针位移到2200区域
        int i;
        for(i = 0; i < 2220; i++){
                addr++;
        }
        //可以访问,但修改操作不会反映到文件中
        strcpy(addr , "PHP is best");


        //指针位移到4096区域
        for(i = 0; i < 1896; i++){
                addr++;
        }
        //访问未被映射区域,会收到SIGBUS信号
        strcpy(addr , "ABCDEF");

        //解除映射
        /*
        ret = munmap(addr , 1024);
        if(ret < 0){
                perror("munmap error: ");
        }
        */
        return 0;
}

 

程序执行结果:

 

5. 总结

1. 掌握内存映射过程中的系统分页边界

2. 理解引发SIGSEGV和SIGBUS信号的错误原因和处理

 

 

猜你喜欢

转载自blog.csdn.net/qq_35733751/article/details/82948869