一、现象
so文件被不同的进程共享,映射入各个进程的地址空间中,这也是SO文件存在的重要原因。作为文件,它的只读部分可以供系统中任意多的进程使用,从而节省系统物理内存使用以及磁盘空间的使用。对于系统级的so文件,我们一般不会修改这些文件的内容,即使修改可能也是无意修改。但是对于一些自己编写的so文件,我们可能需要频繁的修改和调试这些文件的内容,对我们来说,它就是整个全部的可执行文件,所有的逻辑代码在该文件中生存。
在新的so生成之后,我们通常会迫不及待的将整个文件拷贝到指定位置,此时如果该so文件正在被一个进程使用,那么此时很可能会出现SIGBUS或者SEGV错误。这个问题对很多人来说只是感觉到些许困惑,就像世界上大部分令人困惑的问题一样,随便一想,转眼即忘。但是如果知道一些操作系统的只是,其实觉得这个现象不仅仅是令人困惑,而是困惑的诡异的地步。
当一个so文件被映射到一个进程的地址空间之后,此时该进程地址空间中的内存管理单元MMU已经将进程的虚拟地址映射到内存页面,此时文件的概念已经不存在,理论上说,当我们覆盖硬盘上文件的时候,此时操作的是硬盘文件,而内存中页面不应该会变化(假设此时so文件中大部分代码和数据都已经被执行到,按照按需调页机制,此时的整个so文件内的数据访问和普通内存的访问没有任何差别)。
二、举个例子
1、例子代码
[root@Harry OpSo]# cat Toucher.cpp 该简单代码被编译到一个so文件中,代码非常简单,一个页面足够存放。
int toucher(int NewVal)
{
static int meaningless = 0;
meaningless = NewVal;
return meaningless;
}
[root@Harry OpSo]# cat main.cpp 主程序引用so中定义函数,主进程每个1s调用一次so函数,保证在我们执行操作之前so页面已经被分配,并且还会继续访问该so中页面。
#include <unistd.h>
int main()
{
extern int toucher(int );
for (int i = 0; i < 10000; i++)
{
toucher(i);
sleep(1);
}
}
[root@Harry OpSo]# cat Makefile 避免每次输入命令,编写一个简单Makefile
all:
g++ -shared Toucher.cpp -o libtoucher.so -g
g++ -g main.cpp -L`pwd` -ltoucher -o toucher.exe
[root@Harry OpSo]#
2、复现下现象
[root@Harry OpSo]# make
g++ -shared Toucher.cpp -o libtoucher.so -g
g++ -g main.cpp -L`pwd` -ltoucher -o toucher.exe
[root@Harry OpSo]# LD_LIBRARY_PATH=`pwd` ./toucher.exe &
[2] 10543
[root@Harry OpSo]# cp /dev/null libtoucher.so
cp: overwrite `libtoucher.so'? y
[root@Harry OpSo]#
[2]+ Bus error LD_LIBRARY_PATH=`pwd` ./toucher.exe
[root@Harry OpSo]#
三、从现象看
SIGBUS一般出现在mmap一个文件,此时只是建立了进程逻辑地址空间到文件之间的一个映射结构,并没有为这个vma分配任何的物理页面。但是当进程真正访问到这个部分页面之后,此时如果发现虚拟地址对应的内容在被映射文件中没有对应的地址空间,则此时内核给用户态进程发送一个SIGBUS信号。当然这只是通常情况下,其它情况下也有,但是本人没有遇到过。
验证一下进程VMA结构的变化
1、SIGBUS前内存布局
[root@Harry OpSo]# LD_LIBRARY_PATH=`pwd` ./toucher.exe &
[3] 10590
[root@Harry OpSo]# cat /proc/10590/smaps | more
001e8000-00206000 r-xp 00000000 fd:00 1280 /lib/ld-2.11.2.so
Size: 120 kB
Rss: 96 kB
Pss: 1 kB
……
00b6d000-00b6e000 r-xp 00000000 fd:00 569809 /home/tsecer/CodeTest/OpSo/libt
oucher.so
Size: 4 kB
Rss: 4 kB RSS和Size相等,说明整个结构页面都已经在内存中,
Pss: 4 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 4 kB
Referenced: 4 kB
Swap: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
00b6e000-00b6f000 rw-p 00000000 fd:00 569809 /home/tsecer/CodeTest/OpSo/libt
oucher.so
Size: 4 kB
Rss: 4 kB
Pss: 4 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 4 kB
Referenced: 4 kB
Swap: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
00db5000-00dd2000 r-xp 00000000 fd:00 9035 /lib/libgcc_s-4.4.2-20091027.so
.1
2、内核对RSS的统计方法
show_smap--->>smaps_pgd_range--->>smaps_pud_range--->>smaps_pmd_range--->>>smaps_pte_range
从这调用链来看,这个RSS是遍历了一个进程的所有地址空间,然后看进程对应的页面映射表中该逻辑地址对应的页面有没有被分配,这是一个非常精细的操作,也非常准确。
3、SIGBUS前进程的地址空间
为了看到这个信息,先使用gdb附加到进程上,截获导致进程退出的SIGBUS信号,让我们有充足的时间查看进程地址空间的变化。
附加之后,通过cp重复之前的步骤覆盖so文件,此时gdb中显示如下信息
(gdb) c
Continuing.
Dwarf Error: Can't read DWARF data from '/home/tsecer/CodeTest/OpSo/libtoucher.so'
(gdb)
再次查看进程的smaps信息
00b6d000-00b6e000 r-xp 00000000 fd:00 569809 /home/tsecer/CodeTest/OpSo/libt
oucher.so
Size: 4 kB
Rss: 0 kB
Pss: 0 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 0 kB
Referenced: 0 kB
Swap: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
00b6e000-00b6f000 rw-p 00000000 fd:00 569809 /home/tsecer/CodeTest/OpSo/libt
oucher.so
Size: 4 kB
Rss: 0 kB
Pss: 0 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 0 kB
Referenced: 0 kB
Swap: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
4、地址被反向映射
从这个RSS为零,说明使用该进程的toucher.exe逻辑地址空间中so对应的VMA地址空间被从进程的页面映射中拆除,这也说明进程执行cp时,进程cp直接在内核中修改了其它进程的逻辑映射结构,这一点和我们耳熟能详的进程地址空间相互独立的概念是不一致的。
四、何时被拆除映射
1、拆除时机
从RSS的计算中可以看到,它走的页面遍历方法是遍历也进程的页表结构,在cp执行之后,通过该结构不能找到RSS页面,说明拆除映射的代码一定走过了这个映射的反响操作。和mmap中对应,mmap中拆除的操作为unmap_page_range,我们在这个地方打断点,然后用户态执行cp操作,当然此时可能会有很多次断点命中,但是都是在进程退出的时候,可以忽略。
真正有意义的调用链来自这里,使用2.6.21内核
(gdb) bt
#0 unmap_page_range (tlb=0xcff81f64, vma=0x92, addr=3231767404,
end=3086774272, zap_work=0xcfe29a5c, details=0xcfe29bc8) at mm/memory.c:754
#1 0xc019bb1a in unmap_vmas (tlbp=0xcfe29af8, vma=0xcf6ceb00,
start_addr=3086770176, end_addr=3086774272, nr_accounted=0xcfe29af4,
details=0xcfe29bc8) at mm/memory.c:848
#2 0xc019bf4d in zap_page_range (vma=0xcf6ceb00, address=3086770176,
size=4096, details=0xcfe29bc8) at mm/memory.c:894
#3 0xc019e7cf in unmap_mapping_range_vma (vma=0xcf6ceb00,
start_addr=3086770176, end_addr=3086774272, details=0xcfe29bc8)
at mm/memory.c:1744
#4 0xc019eb6e in unmap_mapping_range_tree (details=0xcfe29bc8,
root=0xcfe192a8) at mm/memory.c:1791
#5 unmap_mapping_range (details=0xcfe29bc8, root=0xcfe192a8)
at mm/memory.c:1880
#6 0xc019edc3 in vmtruncate (inode=0xcfe191ec, offset=0) at mm/memory.c:1910
#7 0xc01e252c in inode_setattr (inode=0xcfe191ec, attr=0xcfe29d4c)
at fs/attr.c:73
#8 0xc01e2917 in notify_change (dentry=0xcfe16d74, attr=0xcfe29d4c)
at fs/attr.c:159
#9 0xc01bcb65 in do_truncate (dentry=0xcfe16d74, length=0, time_attrs=96,
filp=0x0) at fs/open.c:215
#10 0xc01ce84b in may_open (nd=0xcfe29f04, acc_mode=2, flag=33346)
at fs/namei.c:1583
---Type <return> to continue, or q <return> to quit---
#11 0xc01cecfc in open_namei (dfd=-100,
pathname=0xcf6eb000 "/lib/libtoucher.so", flag=33346, mode=438,
nd=0xcfe29f04) at fs/namei.c:1727
#12 0xc01bddf2 in do_filp_open (dfd=-100,
filename=0xcf6eb000 "/lib/libtoucher.so", flags=33345, mode=438)
at fs/open.c:759
#13 0xc01be324 in do_sys_open (dfd=-100,
filename=0xbfc28f8e "/lib/libtoucher.so", flags=33345, mode=438)
at fs/open.c:962
#14 0xc01be41f in sys_open (filename=0xbfc28f8e "/lib/libtoucher.so",
flags=33345, mode=438) at fs/open.c:983
#15 0xc0107a84 in ?? ()
2、使用的数据结构
对于磁盘上的文件,它在内存中对应address_space结构是唯一一直的,通过一个文件名找到inode,再通过inode的mapping可以找到同一份address_space,这也是进程间共享页面的实现基础。
do_mmap_pgoff--->>vma_link--->>__vma_link_file
vma_prio_tree_insert(vma, &mapping->i_mmap);
所有映射了这个文件的进程都会在i_mmap结构下组成一个链表,当这个文件被truncate打开时,此时函数遍历这个结构中的所有节点,每个节点包含了一个vma结构,而vma结构中又包含了它所在的mm_struct结构,通过这个结构找到vma所在进程的页表结构,调用上面看到的映射拆除结构将所有vma(可以在其它进程中)从进程页表中拆除。发送SIGBUS的调用链
gdb) bt
#0 send_signal (sig=-1061947580, info=0xcfe29cd8, t=0xc0b3f744,
signals=0xcffadf1c) at kernel/signal.c:738
#1 0xc01536f0 in specific_send_sig_info (sig=7, info=0xcfe29e20, t=0xcffadab0)
at kernel/signal.c:817
#2 0xc0153838 in force_sig_info (sig=7, info=0xcfe29e20, t=0xcffadab0)
at kernel/signal.c:852
#3 0xc012e9a7 in force_sig_info_fault (si_signo=7, si_code=196610,
address=3086271648, tsk=0xcffadab0) at arch/i386/mm/fault.c:218
#4 0xc08b4706 in do_page_fault (regs=0xcfe29fb8, error_code=4)
at arch/i386/mm/fault.c:588
3、现象的解释
对于这里的现象,在truncate之后,由于我们使用了/dev/null清空了文件大小,所以调整后文件大小变为零,在页表拆除之后,进程访问时再次发生缺页,但是此时由于文件的大小已经变化,无法再文件中找到对应的mmap地址,此时被发送SIGBUS信号。
但是如果cp的不是一个空文件,而是另外一份文件,此时缺页映射会重新加载页面,但是由于此时整个内存布局被打乱,地址布局也已经和之前的布局不同,所以很容易出现引用到错误地址的SEGV问题,当然,如果出现SIGILL也不要觉得奇怪。