打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
由mmap引发的SIGBUS
userphoto

2015.09.30

关注
    http://hi.baidu.com/_kouu/blog/item/d2c0926743c83928aa184c4d.html

一直以来都觉得使用mmap读文件是非常高效、非常优雅的做法(参见《从"read"看系统调用的耗时》)。mmap之后,就可以通过内存访问的方式访问到文件里的内容,省去了read这样的系统调用。
却不曾想过,mmap以后,如果读文件出错会发生什么……

今晚看到一篇介绍apache bug的文章,里面说到,apache使用mmap来实现对静态文件的访问。在读文件之前,apache使用stat系统调用得知了文件的长度,然后按照此长度读取已经被映射在某个内存区间上的文件。
然而如果在读静态文件(内存访问)的过程中,文件被外部势力修改了,导致文件长度被减小。则apache可能访问到映射文件之外的内存(本来这块内存是在映射文件之内的,但是现在文件减小了),导致进程收到SIGBUS信号,然后崩溃。

内核代码追踪

真的会存在这样的情况吗?在好奇心驱使下,看了看相关的内核代码。(以下,关于内存管理方面的细节请参阅《linux内存管理浅析》。)

首先是mmap的调用过程,考虑最普遍的情况,一个vma会被分配,并且与对应的file建立联系。

mmap_region()
......
    vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
    ......
    if (file) {
        ......
        vma->vm_file = file;
        get_file(file);
        error = file->f_op->mmap(file, vma);
        ......
    } else if (vm_flags & VM_SHARED) {
    ......

这里是通过file->f_op->mmap函数来“建立联系”的,而一般情况下,这个函数等于generic_file_mmap。

generic_file_mmap()
    ......
    vma->vm_ops = &generic_file_vm_ops;
    vma->vm_flags |= VM_CAN_NONLINEAR;
    ......

其中:

struct vm_operations_struct generic_file_vm_ops = {
    .fault  = filemap_fault,
};


注意这里对vma->vm_ops的赋值,下面会用到。然后,mmap就完成了,仅仅是建立了vma,及其与file的对应关系。没有分配内存、更没有读文件。

接下来,当对应的虚拟内存被访问时,将触发访存异常。内核捕捉到异常,再完成内存分配和读文件的事情。(其中细节还是详见《linux内存管理浅析》。)
do_page_fault就是内核用于捕捉访存异常的函数。其中内核会先确认引起异常的内存地址是合法的,并且找出它所对应的vma(如果找不到就是不合法)。然后分配内存、建立页表。对于本文中描述的mmap映射了某个文件的这种情况,内核还需要把文件对应位置上的数据读到新分配的内存上,这个工作主要是由vma->vm_ops->fault来完成的。前面我们看到vma->vm_ops是如何被赋值的了,而且这个vma->vm_ops->fault就等于filemap_fault。

filemap_fault()
    ......
    size = (i_size_read(inode) + PAGE_CACHE_SIZE - 1) >> PAGE_CACHE_SHIFT;
    if (vmf->pgoff >= size)
        return VM_FAULT_SIGBUS;
    ......

这个函数做的第一件事情就是检查要访问的地址偏移(相对于文件的)是否超过了文件大小,如果超过就返回VM_FAULT_SIGBUS,这将导致SIGBUS信号被发送给进程。

用户程序验证

虽然看到内核代码就是这么实现的了,写个用户程序来验证一下总会让人更信服。一个简单的测试程序如下:

#include
#include
#include
#include
#include
#include
#include
#define FILESIZE 8192
void handle_sigbus(int sig)
{
    printf("SIGBUS!\n");
    _exit(0);
}
void main()
{
    int i;
    char *p, tmp;
    int fd = open("tmp.ttt", O_RDWR);
    p = (char*)mmap(NULL,FILESIZE, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
    signal(SIGBUS, handle_sigbus);
    getchar();
    for (i=0; i        tmp = p[i];
    }
    printf("ok\n");
}

在执行这个程序前:
kouu@kouu-one:~/test$ stat tmp.ttt
File: "tmp.ttt"
Size: 239104     Blocks: 480        IO Block: 4096   普通文件

把程序跑起来,显然8192大小的内存是可以映射的。然后程序会停在getchar()处。
kouu@kouu-one:~/test$ echo "" > tmp.ttt
kouu@kouu-one:~/test$ stat tmp.ttt
File: "tmp.ttt"
Size: 1          Blocks: 8          IO Block: 4096   普通文件


现在我们将 tmp.ttt弄成1字节的。然后给程序一个输入,让它从getchar()返回。
kouu@kouu-one:~/test$ ./a.out

SIGBUS!


立刻,程序就收到SIGBUS信号了。

解决办法

这样的问题在用户态有办法解决吗?我的理解是:没有!

或许你会说,为什么不在每次读之前都取一下文件大小,以确保不越界呢?读文件是通过读内存来进行的,那么应当每读一个字就检查一下吗?这样做的话效率将大打折扣,mmap还有什么意义?
即便如此,“检查通过”与“读操作”并不是原子的,这两个操作之间还是可能存在文件被缩小的问题(尽管可能性变小了)。

所以,目前使用mmap的程序是会存在这样的风险,而收到SIGBUS信号。

是否你异想天开,打算把SIGBUS给捕捉了,然后忽略掉呢?
可以试一下上面的程序,在handle_sigbus函数中把_exit一句注释掉,看看会有什么样的结果。
其结果就是,handle_sigbus会重复重复再重复地被调用,就像一个死循环。为什么会这样呢?因为如果在handle_sigbus函数中把收到SIGBUS的事情给忽略了,内核也就会从前面提到的访存异常中返回,回到用户态,然后CPU会重新执行引起异常的那条指令(这条指令因为异常而未被执行完,必须得重新执行)。
正常情况下,这个时候页面已经分配了、页表已经建立了、文件也读好了。重新执行引起异常的指令时,就不会再引起异常了。但是现在这些条件不满足,这条指令还会引起异常,于是又走到上面讲的那一套流程,然后又触发SIGBUS,然后又被忽略……于是就成了死循环。

所以,面对这种情况,用户程序是没招的。
或许在打开文件之后,mmap之前,先给文件加一个强制锁(百度一下?),这是一种解决办法。但是使用强制锁的限制很多(文件系统要支持、mount时要特殊处理,还要给文件加SGID),并且锁本身很黄很暴力(确实可以阻止别人写入,但是如果程序BUG、句柄泄漏,别人就真没法改了),据说移植性又不好。这一招不到万不得已还是别使……

那么内核程序呢?

如果用户程序通过read来读文件,则每次读文件都是通过系统调用来进行的。当发生错误的时候,read系统调用可以尽可能地返回失败(而不必武断地发出SIGBUS信号),然后让程序决定下一步该怎么办。
但是像mmap这样,是以读内存的方式来读文件。有什么机制让你选择读内存失败了该怎么办吗?没有,只能是“不成功,便成仁”。(好比你写下“i=j;”这么一句,不可能还有办法检查读取j或者写i是否成功。)

然而,我不知道内核为什么要在判断访问文件越界时抛出SIGBUS,或许有些东西我没能理解透彻。
在这个地方(filemap_fault函数中),如果发现访问越界,是否可以返回一个0页面,让它给映射上呢(也就是说,如果读越界,读到的内容就是0)。(这个0页面在内核中其实也是存在的,页面的内容全是0,当程序去读没有被映射的页面时,这个0页面就映射给它,而并不用分配新的页面。因为页面都没映射,显然没被写过,也就是说这些内存没有初值,所以默认都填0了。)
并且,这里的访问越界,我觉得,应该没有什么危害。因为再怎么越界,都不会越过mmap时创建的那个vma,这些地址应该说都是合法的。



关于之前的《由mmap引发的SIGBUS》问题,这几天又做了一些较深入的探索,整理如下。

SIGBUS的必要性

为什么内核在上述情况下要抛出SIGBUS信号呢?
原来这是POSIX的规定,引用一段:

The mmap() function can be used to map a region of memory that is larger than the current size of the object. Memory access within the mapping but beyond the current end of the underlying objects may result in SIGBUS signals being sent to the process. The reason for this is that the size of the object can be manipulated by other processes and can change at any moment. The implementation should tell the application that a memory reference is outside the object where this can be detected; otherwise, written data may be lost and read data may not reflect actual data in the object.

参阅mmap文档:http://www.opengroup.org/onlinepubs/000095399/functions/mmap.html


本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
缺页中断
Linux内核虚拟内存管理之匿名映射缺页异常分析
linux进程地址空间
2--共享内存的实践到内核--共享内存的映射 - 如何从应用程序进入linux内核 - 无...
linux内核分析笔记----进程地址空间
linux内核分析之进程地址空间
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服