由mmap引發的SIGBUS

2021-07-11 17:41:35 字數 3559 閱讀 5437

來自

一直以來都覺得使用mmap讀檔案是非常高效、非常優雅的做法(參見《

從"read"看系統呼叫的耗時

》)。mmap之後,就可以通過記憶體訪問的方式訪問到檔案裡的內容,省去了read這樣的系統呼叫。

卻不曾想過,mmap以後,如果讀檔案出錯會發生什麼……

今晚看到一篇介紹apache bug的文章,裡面說到,apache使用mmap來實現對靜態檔案的訪問。在讀檔案之前,apache使用stat系統呼叫得知了檔案的長度,然後按照此長度讀取已經被對映在某個記憶體區間上的檔案。

然而如果在讀靜態檔案(記憶體訪問)的過程中,檔案被外部勢力修改了,導致檔案長度被減小。則apache可能訪問到對映檔案之外的記憶體(本來這塊記憶體是在對映檔案之內的,但是現在檔案減小了),導致程序收到sigbus訊號,然後崩潰。

核心**追蹤

真的會存在這樣的情況嗎?在好奇心驅使下,看了看相關的核心**。(以下,關於記憶體管理方面的細節請參閱《

linux記憶體管理**

》。)首先是mmap的呼叫過程,考慮最普遍的情況,乙個vma會被分配,並且與對應的file建立聯絡。

mmap_region()

......

vma = kmem_cache_zalloc(vm_area_cachep, gfp_kernel);

......

if (file) else if (vm_flags & vm_shared) ;

注意這裡對vma->vm_ops的賦值,下面會用到。然後,mmap就完成了,僅僅是建立了vma,及其與file的對應關係。沒有分配記憶體、更沒有讀檔案。

接下來,當對應的虛擬記憶體被訪問時,將觸發訪存異常。核心捕捉到異常,再完成記憶體分配和讀檔案的事情。(其中細節還是詳見《

linux記憶體管理**

》。)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訊號被傳送給程序。

使用者程式驗證

雖然看到核心**就是這麼實現的了,寫個使用者程式來驗證一下總會讓人更信服。乙個簡單的測試程式如下:

#include

#include

#include

#include

#include

#include

#include

#define filesize 8192

void handle_sigbus(int sig)

void main()

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還有什麼意義?

即便如此,「檢查通過」與「讀操作」並不是原子的,這兩個操作之間還是可能存在檔案被縮小的問題(儘管可能性變小了)。

所以,目前使用mmap的程式是會存在這樣的風險,而收到sigbus訊號。

是否你異想天開,打算把sigbus給捕捉了,然後忽略掉呢?

可以試一下上面的程式,在handle_sigbus函式中把_exit一句注釋掉,看看會有什麼樣的結果。

其結果就是,handle_sigbus會重複重複再重複地被呼叫,就像乙個死迴圈。為什麼會這樣呢?因為如果在handle_sigbus函式中把收到sigbus的事情給忽略了,核心也就會從前面提到的訪存異常中返回,回到使用者態,然後cpu會重新執行引起異常的那條指令(這條指令因為異常而未被執行完,必須得重新執行)。

正常情況下,這個時候頁面已經分配了、頁表已經建立了、檔案也讀好了。重新執行引起異常的指令時,就不會再引起異常了。但是現在這些條件不滿足,這條指令還會引起異常,於是又走到上面講的那一套流程,然後又觸發sigbus,然後又被忽略……於是就成了死迴圈。

所以,面對這種情況,使用者程式是沒招的。

或許在開啟檔案之後,mmap之前,先給檔案加乙個強制鎖(

),這是一種解決辦法。但是使用強制鎖的限制很多(檔案系統要支援、mount時要特殊處理,還要給檔案加sgid),並且鎖本身很黃很暴力(確實可以阻止別人寫入,但是如果程式bug、控制代碼洩漏,別人就真沒法改了),據說移植性又不好。這一招不到萬不得已還是別使……

那麼核心程式呢?

如果使用者程式通過read來讀檔案,則每次讀檔案都是通過系統呼叫來進行的。當發生錯誤的時候,read系統呼叫可以盡可能地返回失敗(而不必武斷地發出sigbus訊號),然後讓程式決定下一步該怎麼辦。

但是像mmap這樣,是以讀記憶體的方式來讀檔案。有什麼機制讓你選擇讀記憶體失敗了該怎麼辦嗎?沒有,只能是「不成功,便成仁」。(好比你寫下「i=j;」這麼一句,不可能還有辦法檢查讀取j或者寫i是否成功。)

然而,我不知道核心為什麼要在判斷訪問檔案越界時丟擲sigbus,或許有些東西我沒能理解透徹。

在這個地方(filemap_fault函式中),如果發現訪問越界,是否可以返回乙個0頁面,讓它給對映上呢(也就是說,如果讀越界,讀到的內容就是0)。(這個0頁面在核心中其實也是存在的,頁面的內容全是0,當程式去讀沒有被對映的頁面時,這個0頁面就對映給它,而並不用分配新的頁面。因為頁面都沒對映,顯然沒被寫過,也就是說這些記憶體沒有初值,所以預設都填0了。)

並且,這裡的訪問越界,我覺得,應該沒有什麼危害。因為再怎麼越界,都不會越過mmap時建立的那個vma,這些位址應該說都是合法的。

由mmap引發的SIGBUS

一直以來都覺得使用mmap讀檔案是非常高效 非常優雅的做法 參見 從 read 看系統呼叫的耗時 mmap之後,就可以通過記憶體訪問的方式訪問到檔案裡的內容,省去了read這樣的系統呼叫。卻不曾想過,mmap以後,如果讀檔案出錯會發生什麼 今晚看到一篇介紹apache bug的文章,裡面說到,apa...

由 引發的思考

前陣子在乙個移動專案中,通過 的方式 繫結click 事件來提交乙個表單,由於表單資訊比較敏感,於是採用的post 同步提交的方式,原本到也沒有什麼。後來萬惡的pm說 你這個按鈕呀,要固定在底部比較好 於是乎就通過 position fixed 固定到底部了。那麼,問題來了 在ios 下,虛擬鍵盤是...

由Typedef引發的問題

由typedef 引發的問題 自 用來宣告乙個別名,typedef 後面的語法,是乙個宣告。本來筆者以為這裡不會產生什麼誤解的,但結果卻出乎意料,產生誤解的人不在少數。罪魁禍首又是那些害人的教材。在這些教材中介紹 typedef 的時候通常會寫出如下形式 typedef int para 這種形式跟...