- 论坛徽章:
- 0
|
本帖最后由 kouu 于 2010-04-17 13:30 编辑
最近注意到这样一个现象: 使用mmap映射一个文件以后, 如果文件大小被其他进程减小, 则访问map以内 文件大小以外的内存时, 进程将收到SIGBUS信号而退出.
设有进程A和B.
进程A 通过 mmap 映射一个普通文件, 设映射的到内存的起始地址为p, 大小为a(单位为page大小, 以下都使用相同的单位).
进程B 将该文件的size减小为b(b<a).
这时, 进程A 读p+n(b<n<a)的内存时(这个地址的vma还存在, 但是已经超出文件之外了), 内核会抛出一个SIGBUG信号, 使得进程A退出.
(下文会附上一些代码.)
从这个现象可以发现, 通过mmap去访问文件是非常危险的. 一旦文件被其他进程修改(比如被编辑, 被cp覆盖, 等), 通过mmap去访问该文件的进程就有可能因为SIGBUG而非预期地退出.
现在有两个问题想与大家讨论:
1, 这种现象有可能避免吗?
我只想到给文件加强制锁的方法, 避免文件被其他进程修改. 还有其他什么办法吗?
2, 进程在读p+n的内存时, 内核为什么要发出SIGBUG信号呢? 考虑到p+n的内存是处在合法的map之内, 内核如果给进程映射一个零页面(或其他), 让进程读到一些无用的数据. 这样会有什么问题呢?
望大家不吝指教, 非常感谢~
附, 用户态的测试程序:- #include <stdio.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <signal.h>
- #include <sys/mman.h>
- #include <unistd.h>
- #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<FILESIZE; 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的调用过程,考虑最普遍的情况,一个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,
- };
复制代码 接下来,当对应的虚拟内存被访问时,将触发访存异常。内核捕捉到异常,再完成内存分配和读文件的事情。
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信号被发送给进程。 |
|