免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
123下一页
最近访问板块 发新帖
查看: 13438 | 回复: 22

通过mmap读文件, 进程收到SIGBUS而退出 [复制链接]

论坛徽章:
0
发表于 2010-04-17 13:28 |显示全部楼层
本帖最后由 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之内, 内核如果给进程映射一个零页面(或其他), 让进程读到一些无用的数据. 这样会有什么问题呢?


望大家不吝指教, 非常感谢~


附, 用户态的测试程序:
  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <sys/stat.h>
  4. #include <fcntl.h>
  5. #include <signal.h>
  6. #include <sys/mman.h>
  7. #include <unistd.h>
  8. #define FILESIZE 8192
  9. void handle_sigbus(int sig)
  10. {
  11.     printf("SIGBUS!\n");
  12.     _exit(0);
  13. }
  14. void main()
  15. {
  16.     int i;
  17.     char *p, tmp;
  18.     int fd = open("tmp.ttt", O_RDWR);
  19.     p = (char*)mmap(NULL,FILESIZE, PROT_READ|PROT_WRITE,MAP_SHARED, fd,
  20. 0);
  21.     signal(SIGBUS, handle_sigbus);
  22.     getchar();
  23.     for (i=0; i<FILESIZE; i++) {
  24.         tmp = p[i];
  25.     }
  26.     printf("ok\n");
  27. }
复制代码
在执行这个程序前:
  1. kouu@kouu-one:~/test$ stat tmp.ttt
  2. File: "tmp.ttt"
  3. Size: 239104     Blocks: 480        IO Block: 4096   普通文件
复制代码
把程序跑起来,显然8192大小的内存是可以映射的。然后程序会停在getchar()处。
  1. kouu@kouu-one:~/test$ echo "" > tmp.ttt
  2. kouu@kouu-one:~/test$ stat tmp.ttt
  3. File: "tmp.ttt"
  4. Size: 1          Blocks: 8          IO Block: 4096   普通文件
复制代码
现在我们将 tmp.ttt弄成1字节的。然后给程序一个输入,让它从getchar()返回。
  1. kouu@kouu-one:~/test$ ./a.out

  2. SIGBUS!
复制代码
立刻,程序就收到SIGBUS信号了。

附, 内核代码导读:

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

mmap_region()
  1.     ......
  2.     vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
  3.     ......
  4.     if (file) {
  5.         ......
  6.         vma->vm_file = file;
  7.         get_file(file);
  8.         error = file->f_op->mmap(file, vma);
  9.         ......
  10.     } else if (vm_flags & VM_SHARED) {
  11.     ......
复制代码
这里是通过file->f_op->mmap函数来“建立联系”的,而一般情况下,这个函数等于generic_file_mmap。

generic_file_mmap()
  1.     ......
  2.     vma->vm_ops = &generic_file_vm_ops;
  3.     vma->vm_flags |= VM_CAN_NONLINEAR;
  4.     ......
复制代码
其中:
  1. struct vm_operations_struct generic_file_vm_ops
  2. = {
  3.     .fault  = filemap_fault,
  4. };
复制代码
接下来,当对应的虚拟内存被访问时,将触发访存异常。内核捕捉到异常,再完成内存分配和读文件的事情。
do_page_fault就是内核用于捕捉访存异常的函数。其中内核会先确认引起异常的内存地址是合法的,并且找出它所对应的vma(如果找不到就是不合法)。然后分配内存、建立页表。对于本文中描述的mmap映射了某个文件的这种情况,内核还需要把文件对应位置上的数据读到新分配的内存上,这个工作主要是由vma->vm_ops->fault来完成的。前面我们看到vma->vm_ops是如何被赋值的了,而且这个vma->vm_ops->fault就等于filemap_fault。

filemap_fault()
  1.     ......
  2.     size = (i_size_read(inode) + PAGE_CACHE_SIZE - 1) >> PAGE_CACHE_SHIFT;
  3.     if (vmf->pgoff >= size)
  4.         return VM_FAULT_SIGBUS;
  5.     ......
复制代码
这个函数做的第一件事情就是检查要访问的地址偏移(相对于文件的)是否超过了文件大小,如果超过就返回VM_FAULT_SIGBUS,这将导致SIGBUS信号被发送给进程。

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
发表于 2010-04-17 21:43 |显示全部楼层
刚刚测试了一下kouu兄提到的代码,感觉情况并不是kouu上面提到的那样。
  1. signal(SIGBUS, handle_sigbus);
  2.      getchar();
  3.      for (i=0; i<FILESIZE; i++) {
  4.          tmp = p[i];
  5.      }
  6.      printf("ok\n");
复制代码
我测试文件的大小为560bytes。但是上面的那个for循环可以执行到4096,都OK。如果i执行到4097就报错Buss error.
这个是不是正好和stat现出文件中 IO Block的大小4096相关呢。

[root@localhost tmp]# stat tmp.ttt
  File: `tmp.ttt'
  Size: 560             Blocks: 8          IO Block: 4096   regular file

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
发表于 2010-04-17 21:47 |显示全部楼层
再次验证了一下,只读取前4096,这是将文件截断为1bytes。仍然不会报出BUS Error。因此,感觉你mmap访问内存的时候,如果超出文件的长度的话,应该不能超过文件尾部所在IO Block的边界。

论坛徽章:
0
发表于 2010-04-18 11:12 |显示全部楼层
Godbach 说得没错, 文件cache是以page为单位的, 所以只要文件还有一个byte, cache里面就会映射一个页面, 32位机器下访问4K字节以前的地址就不会有问题.
所以我举的例子里面, 文件是大于8K的, 然后映射了8K, 然后读的时候再超过4K.

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
发表于 2010-04-18 16:36 |显示全部楼层
本帖最后由 Godbach 于 2010-04-18 16:39 编辑

回复 4# kouu
Kouu兄,我将你的代码做了些修改,将创建大于8192字节的文件,并mmap之后trunc的过程都在程序里面实现了,代码如下,主要注重功能,风格上和细节上没有做过多工作
  1. #include <stdio.h>
  2. #include <sys/types.h>
  3. #include <sys/stat.h>
  4. #include <fcntl.h>
  5. #include <signal.h>
  6. #include <sys/mman.h>
  7. #include <unistd.h>
  8. #include <ctype.h>
  9. #include <stdlib.h>

  10. #define FILESIZE 8192
  11. #define TMPFILE "/tmp/tmp.ttt"

  12. void handle_sigbus(int sig)
  13. {
  14.      printf("SIGBUS!\n");
  15.      _exit(0);
  16. }
  17. int main(int argc, char *argv[])
  18. {
  19.      int i, len, trunc_len;
  20.      char *p, tmp;
  21.          char cmdstr[1024] = {0x00};
  22.          if(argc != 3)
  23.          {
  24.                  printf("Usage: %s readcount trunc_len\n", argv[0]);
  25.                 exit(0);
  26.          }
  27.          len = atoi(argv[1]);
  28.          trunc_len = atoi(argv[2]);
  29.          if(len <= 0 || trunc_len < 0)
  30.          {
  31.                  printf("invalid readcount or trunc_len.\n");
  32.                 exit(0);
  33.          }
  34.          printf("create file %s.....\n.....stat file %s.....\n", TMPFILE, TMPFILE);
  35.          sprintf(cmdstr, "cat /tmp/testfile > %s && stat %s", TMPFILE, TMPFILE);
  36.          system(cmdstr);

  37.      int fd = open(TMPFILE, O_RDWR);
  38.      p = (char*)mmap(NULL,FILESIZE, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
  39.          printf("...........\nmmap %s...\n", TMPFILE);

  40.      signal(SIGBUS, handle_sigbus);
  41.          
  42.          if(truncate(TMPFILE, trunc_len) != 0)
  43.          {
  44.                  printf("truncate file %s to %d byte(s) failed.\n", TMPFILE, trunc_len);
  45.                 exit(0);
  46.          }       
  47.          printf("truncate file %s to %d byte(s).\n ",TMPFILE, trunc_len);
  48.        
  49.      getchar();
  50.          printf("traverse mmap memory %d bytes.\n", len);
  51.      for (i=0; i<len; i++) {
  52.          tmp = p[i];
  53.      }
  54.      printf("ok\n");
  55.          return 0;
  56. }
复制代码
然后我默认是创建一个超过8192字节的tmp.ttt进行测试,同时还用一个小于8192大于4096的tmp.ttt进行测试。执行结果相同,如下。只是这个地方用的是8019字节的tmp.ttt。
[root@localhost tmp]# ./a.out 4096 4095
create file /tmp/tmp.ttt.....
.....stat file /tmp/tmp.ttt.....
  File: `/tmp/tmp.ttt'
  Size: 8019            Blocks: 16         IO Block: 4096   regular file
Device: 801h/2049d      Inode: 1244082     Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2010-04-18 16:31:24.000000000 +0800
Modify: 2010-04-18 16:31:42.000000000 +0800
Change: 2010-04-18 16:31:42.000000000 +0800
...........
mmap /tmp/tmp.ttt...
truncate file /tmp/tmp.ttt to 4095 byte(s).

traverse mmap memory 4096 bytes.
ok
[root@localhost tmp]# ./a.out 4097 4096
create file /tmp/tmp.ttt.....
.....stat file /tmp/tmp.ttt.....
  File: `/tmp/tmp.ttt'
  Size: 8019            Blocks: 16         IO Block: 4096   regular file
Device: 801h/2049d      Inode: 1244082     Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2010-04-18 16:31:42.000000000 +0800
Modify: 2010-04-18 16:31:51.000000000 +0800
Change: 2010-04-18 16:31:51.000000000 +0800
...........
mmap /tmp/tmp.ttt...
truncate file /tmp/tmp.ttt to 4096 byte(s).

traverse mmap memory 4097 bytes.
SIGBUS!
[root@localhost tmp]# ./a.out 8193 8192
create file /tmp/tmp.ttt.....
.....stat file /tmp/tmp.ttt.....
  File: `/tmp/tmp.ttt'
  Size: 8019            Blocks: 16         IO Block: 4096   regular file
Device: 801h/2049d      Inode: 1244082     Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2010-04-18 16:31:51.000000000 +0800
Modify: 2010-04-18 16:31:56.000000000 +0800
Change: 2010-04-18 16:31:56.000000000 +0800
...........
mmap /tmp/tmp.ttt...
truncate file /tmp/tmp.ttt to 8192 byte(s).

traverse mmap memory 8193 bytes.
ok


从以上命令执行的三次结果,也有疑问的地方。
(1)文件被截断为4095,这次读取4096个字节,应该没有问题,因为在一个page内;
(2)文件被截断为4096,这次读取4097个字节,BUS Error,因为文件只用了一个page,但是访问的内存已经超过了1个page;
(3)文件被截断为8192,这次读取8197个字节,感觉上应该也出现BUS Error啊。情形应该和(2)中相同,但是没有出现。

论坛徽章:
0
发表于 2010-04-18 17:10 |显示全部楼层
Godbach兄的实验做得很充分啊~

第三个结果,是不是因为只mmap了8192字节,读8193字节的时候已经是另一个vma了?

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
发表于 2010-04-18 20:51 |显示全部楼层
Godbach兄的实验做得很充分啊~

第三个结果,是不是因为只mmap了8192字节,读8193字节的时候已经是另一 ...
kouu 发表于 2010-04-18 17:10

呵呵,过奖了。
当时没想到修改mmap的大小问题。
不知道你对此进行测试了一下没有。

论坛徽章:
0
发表于 2010-04-18 23:38 |显示全部楼层
嗯,试过了,的确是因为只mmap了8192字节。把FILESIZE改为8193就会SIGBUS。

并且这个文件map后面紧跟了另一个map,所以越界访问就访问到另一个map去。
  1. kouu@kouu-one:~/test$ cat /proc/24242/maps
  2. ......
  3. 08048000-08049000 r-xp 00000000 08:08 1097733    /home/kouu/test/a.out
  4. 08049000-0804a000 r--p 00000000 08:08 1097733    /home/kouu/test/a.out
  5. 0804a000-0804b000 rw-p 00001000 08:08 1097733    /home/kouu/test/a.out
  6. b7846000-b7847000 rw-p 00000000 00:00 0
  7. b7856000-b7857000 rw-p 00000000 00:00 0
  8. b7857000-b7859000 rw-s 00000000 08:08 409672     /tmp/tmp.ttt
  9. b7859000-b785c000 rw-p 00000000 00:00 0
  10. bfeec000-bff01000 rw-p 00000000 00:00 0          [stack]
复制代码
不知道后面那个map是否在所有系统中都会存在,如果不存在,那应该就会SIGSEGV了。

论坛徽章:
36
IT运维版块每日发帖之星
日期:2016-04-10 06:20:00IT运维版块每日发帖之星
日期:2016-04-16 06:20:0015-16赛季CBA联赛之广东
日期:2016-04-16 19:59:32IT运维版块每日发帖之星
日期:2016-04-18 06:20:00IT运维版块每日发帖之星
日期:2016-04-19 06:20:00每日论坛发贴之星
日期:2016-04-19 06:20:00IT运维版块每日发帖之星
日期:2016-04-25 06:20:00IT运维版块每日发帖之星
日期:2016-05-06 06:20:00IT运维版块每日发帖之星
日期:2016-05-08 06:20:00IT运维版块每日发帖之星
日期:2016-05-13 06:20:00IT运维版块每日发帖之星
日期:2016-05-28 06:20:00每日论坛发贴之星
日期:2016-05-28 06:20:00
发表于 2010-04-19 10:34 |显示全部楼层
那kouu兄提到的问题算是找到答案了吗?

论坛徽章:
0
发表于 2010-04-19 11:02 |显示全部楼层
其实我的问题并不在于这个现象,而是:

1, 这种现象有可能避免吗?
我只想到给文件加强制锁的方法, 避免文件被其他进程修改. 还有其他什么办法吗?

2, 进程在读p+n的内存时, 内核为什么要发出SIGBUG信号呢? 考虑到p+n的内存是处在合法的map之内, 内核如果给进程映射一个零页面(或其他), 让进程读到一些无用的数据. 这样会有什么问题呢?

还望Godbach兄指点~
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP