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問題。
-
內核
+關注
關注
3文章
1377瀏覽量
40326 -
Linux
+關注
關注
87文章
11322瀏覽量
209865 -
優化
+關注
關注
0文章
220瀏覽量
23928 -
線程
+關注
關注
0文章
505瀏覽量
19709
原文標題:內核mmap_sem鎖的危害和相關優化
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論