内核mmap_sem锁的危害和相关优化

mmap_sem锁简介

mmap_sem锁是进程为了保护自身虚拟地址空间不受多线程并发访问影响而设计的。

多线程环境下,如果想访问进程的虚拟地址空间(比如find_vma等),是要先持有该mmap_sem锁才能访问的,这样可以避免多线程并发修改进程vma区域造成的冲突。

mmap_sem锁的一些问题总结

内核mmap_sem锁设计目前存在一些问题,简单总结如下:

1:保护的东西太多,范围太广了。

mmap_sem目前保护:

1)Rbtree of VMA,比如做find_vma()时

arm64系统上,由于虚拟地址空间增大,进程的vma数量会特别多,每个vma操作几乎都要首先获取一把这样的mmap_sem大锁。

这样会造成锁的粒度太大,锁整个进程的vma地址空间的。

2)VMA list,会Lock the whole address space for even touching one byte

3) VMA flags, 会Need hold write lock to update vm_flags

4)Most of the fields of the mm_struct are protected using the mm.mmap_sem

fields can be arg_start, arg_end, env_start, env_end等。

2: 内核会频繁做page fault, 这样会频繁获取mmap_sem锁。

soft page fault的描述:

Page faults can be quite expensive, especially those which must be resolved by reading data from disk. On a typical system, though, there are a lot of page faults which do not require I/O. A page fault happens because a specific process does not have a valid page table entry for the needed page, but that page might already be in the page cache, in which case handling the fault is just a matter of fixing the page table entry and increasing the page's reference count; this happens often with shared pages or those brought in via the readahead mechanism. Faults for new anonymous pages (application data and stack space, mostly), instead, can be handled through the allocation of a zero-filled page. In either case, the fault is quickly taken care of with no recourse to backing store required.

In many workloads, this kind of "soft" fault happens much more often than hard faults requiring actual I/O. So it's important that they be executed quickly. Various developers had concluded that the kernel was, in fact, not handling this kind of fault quickly enough, and they identified the use of the mmap_sem reader/writer semaphore as the core of the problem.
Contention wasn't the issue in this case - only a reader lock is required for page fault handling - but the cache line bouncing caused by continual acquisition of the lock was killing performance. As the number of cores in systems increases, this kind of problem can  get worse.

内核里面这样的soft page fault会发生很多,势必造成mmap_sem获取很频繁,引起多核cache颠簸,对多核程序性能也不好。

所以Linux kernel很多地方架构设计不怎么匹配如今的多核cpu架构。

3: 一旦有个写请求在排队了,该mmap_sem就会变成互斥意义上的锁了。

mmap_sem这种读写锁是有好处,可以实现一些并发的多线程读访问。

但是它的这种并发读访问是有条件的:

如果一个读写信号量当前没有被写者拥有并且也没有写者等待读者释放信号量,那么任何读者都可以成功获得该读写信号量,否则,读者必须被挂起直到写者释放该信号量。

如果一个读写信号量当前没有被读者或写者拥有并且也没有写者等待该信号量,那么一个写者可以成功获得该读写信号量,否则写者将被挂起,直到没有任何访问者。

简单来看个问题场景:

线程1以读者身份持mmap_sem,然后该线程由于某种原因sleep了。

下来线程2以写者身份请求持该mmap_sem锁,因为该锁已经被线程1持有,所以失败就开始排队。

再下来该锁就变成互斥锁了,再来的read请求(对应线程2,3...)就都得排队了,不能发挥读写锁并发读的优势了。

结论:

所以综合上面3个问题特点,在多核,多线程并发环境下(比如安卓系统),势必造成mmap_sem锁竞争激烈,程序性能不好。

mmap_sem锁在产品开发中的优化总结

优化方向1:方便快捷地找到持锁线程。

目前很多方法是通过在出现问题时,人为地让内核崩溃,然后再用crash工具分析内核内存dump镜像,从而在一大堆等锁和持锁线程中,找到

导致问题出现的持锁线程信息。一旦找到持锁线程,就明白问题出现的root casue, 就会有优化方案了。

但这种crash工具分析方法还是有点笨重,当然由于mmap_sem锁竞争导致的内核崩溃用这种方法是最好最对口的。

但是很多时候出问题是内核并未崩溃,只是android上层发生watchdog超时重启,或者只是某进程工作timeout。

如果是mmap_sem竞争导致的这些问题发生可以尝试用些简单快捷的方法找到持锁线程。

1) 可以在安卓fwk层发生watchdog超时时,打印下system server进程中每个线程的内核栈回溯信息。

这样如果对内核代码熟悉的话,会知道哪些地方会长时间持有mmap_sem锁,这样看下栈回溯信息,大概能猜出来哪些线程在持锁或者等锁。

如果信息还不够,还可以通过sysrq,打印出系统此时所有处于D状态和sleep状态的线程内核栈回溯信息。
因为有持锁等锁导致的内核性能问题,基本上都出现在D和sleep状态的线程里面,通过对这些信息分析,也可以大概猜出来可能的持锁者。

2) 如果问题可以复现,可以用一些bcc工具找到mmap_sem的持锁owner信息。

1>输出持锁的owner信息

sudo ./trace 'rwsem_down_read_slowpath(struct rw_semaphore *sem, int state) "count=0x%lx owner=%s", sem->count.counter, ((struct task_struct *)((sem->owner.counter)&~0x7))->comm'
/virtual/main.c:44:66: warning: comparison of array '((struct task_struct *)((sem->owner.counter) & ~7))->comm' not equal to a null pointer is
always true [-Wtautological-pointer-compare]
if (((struct task_struct *)((sem->owner.counter)&~0x7))->comm != 0) {
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~ ~
1 warning generated.
PID TID COMM FUNC -
10127 10127 sync rwsem_down_read_slowpath count=0x103 owner=modprobe //是modprobe进程持有该rwsem锁。

2> 输出持锁owner的其他信息

sudo ./trace 'rwsem_down_read_slowpath(struct rw_semaphore *sem, int state) "count=0x%lx fs name=%s", sem->count.counter, (((struct super_block*)((void *)sem-(void*)(&(((struct super_block*)0)->s_umount))))->s_id)'
PID TID COMM FUNC -
10144 10144 sync rwsem_down_read_slowpath count=0x103 fs name=nfsd
从上面可以找到持锁owner:modprobe进程此时正在挂载 nfsd 模块。

优化方向2:核心路径中避免对/proc目录下每进程子目录做遍历访问,规避mmap_sem导致的问题。

之前碰到一个问题是由于system_server进程发生watchdog超时导致安卓fwk层重启。

watchdog超时原因是:

system_server进程一个核心路径上代码做了遍历/proc/每进程/cmdline工作,正常情况下做该工作不会出现问题。

但是由于做该工作,需要down_read获取每进程的mmap_sem锁,只要有一个进程的该锁已经被写者身份持有了,那么再获取该锁时,就得等待了。

所以异常就发生在这个地方,所以该异常会导致该核心路径中遍历每进程cmdline工作耗时了,核心路径一旦性能受影响,就会导致问题出现。

优化方法:

避免在核心路径上做这种潜在的耗时工作:遍历每进程状态。改用其他方法去达到目的。

优化方向3:同步社区一些patch,避免出现因mmap_sem竞争导致的cgroup优先级反转问题。

cgroup v2中容易出现这种优先级反转问题:

一个高优先级group里面某个进程A正在做遍历访问/proc/每进程下面状态信息的工作,所以需要获取系统中每进程的mmap_sem锁。

另外一个低优先级group里面,某进程B中有些线程在做耗时长的io操作(进入内核filemap_fault函数里面做的),操作前提前以写身份获取了mmap_sem锁。

所以写身份先获取了这把锁,那么进程A如果想通过访问获取进程B的状态信息时,就会阻塞在等待进程B的mmap_sem这个地方。

这样因为进程B是在低优先级group里面,io访问也比高优先级group慢,但是此时却阻塞住了高优先级group里面的进程。

优化方法:

同步社区该patch

[RFC][PATCH 0/9][V2] drop the mmap_sem when doing IO in the fault path

优化方向4: 其他的一些优化持锁线程的工作负载方法。

前面提了,解决因mmap_sem竞争导致的问题,关键是找到持锁线程信息。找到后,还需要优化该持锁线程在持锁后的工作负载。

只有保证持锁过程中,工作时间越短,就越能降低性能问题出现的概率。

1: 比如之前还碰到过一个问题:

某业务进程包含若干个工作线程和一个数据加载线程。数据加载线程需要将工作线程不再使用的上一份数据释放掉,具体需要做munmap 大块内存 (20G+)工作 ,

结果在释放数据过程中,工作线程的性能受到了影响。

2: 这类问题原因也是:

释放数据时,需要做munmap工作,进一步需要以写着身份拿工作线程的mmap_sem锁。这样会导致工作线程获取自身mmap_sem的等待时间变长。

通过问题进一步分析,发现munmap中最耗时的是free_pgtables,做这个也需要写者拿mmap_sem锁。

网络上一些好的建议是:实现分段munmap,或者Drop mmap_sem during unmapping large map。

3: 其实还有个好的优化方法:

不考虑mmap_sem的影响,munmap工作本身就会比较耗时,所以后来有了madvise MADV_FREE和MADV_DONTNEED的优化。

所以这个地方可以尝试用madvise来代替munmap,会缩短释放数据的时间的。

优化方向5: Speculative page faults

前面那些优化方向都是正面回避该mmap_sem自身的一些特性问题(详见上面第二大节的总结),从侧面,从程序自身业务着手去优化解决问题。

这个投机性缺页异常则是尝试从正面优化该mmap_sem问题。

你可能感兴趣的:(os工作经历,linux内核)