關於O APPEND模式write的原子性

2022-09-23 17:21:08 字數 3847 閱讀 8151

上上週的事情了,端午小長假將近,還是按往常一樣,最後一天一定要搞乙個「課題」,場面不大,一天就能搞定的東西,如果說系統學習vim或者emacs之類的,那就算了...還好,問題呼之即來,那就是write系統呼叫是不是原子的,答案很顯然,不是!但大師說帶有append標誌的write是原子的,很多軟體的日誌都是o_append開啟,然後在不加鎖的情況下直接write的,不會出現問題,此事如何證實?本文給出答案。

曾經糾結於linux的write系統呼叫是不是原子的,答案是顯然的,不是!為什麼不是呢?這個問題可不是那麼好回答,本文試圖用一種簡單的方式解釋一下。另外,本文也將說明一下o_append方式的write為什麼是原子的,同樣是簡單的方式,只做實驗或者思想試驗,不講**。但是作為基礎,我給出重要結構體的偽實現:

1.inode結構

表示乙個檔案實體,每乙個磁碟中的檔案只有乙個inode物件與之對應。

2.file結構

表示乙個檔案實體在程序中的代表,需要操作某個檔案(即某個inode)並獨立開啟它的每乙個程序都有乙份獨立的對應該inode的file物件。該物件擁有乙個pos指標,表示乙個file的當前位置,不管是read還是write均從這裡開始。

3.task結構

操作file的主體。

提到write操作,最基本的就是從**開始寫的問題,即檔案當前的position。乙個write系統呼叫的語義就是,從position開始,寫入長度為len的引數buff,僅此而已,具體的寫入很簡單,就是記憶體拷貝,快取管理,最後交給塊裝置即可,所以關鍵就是,position的定位。定位方式分為3種:

1.呼叫lseek手工定位;

2.根據歷史write操作自動定位;

3.根據o_append標誌自動定位;

lseek手工定位很簡單,即設定file的pos指標,根據歷史write操作自動定位最好理解,比如你寫入了n個位元組,那麼file的pos就向前推進n,在write操作的最開始處得到file的pos,然後開始write,write完畢後根據實際寫入的數量重新設定file的pos。o_append方式是完全和pos無關的,因為它根本就不用file的pos來定位寫入開始的位置,而是根據inode的大小來定位,也就是將write的開始位置設定到檔案的末尾。

好了,到此為止,我們完成了當前位置的定位,接下來就開始write了,現在的問題是,一次write是不是可以被另一次的write影響,為了更簡單的分析問題,我假設每次都將buffer一次性寫完(因為乙個buffer分多次寫在多程序環境下肯定是會出現交叉的,毫無疑問!),即write的count引數是多少,write的返回值就是多少。首先我將乙個write操作流程化,假設每次寫入的資料長度均為100,執行緒a寫100個a,執行緒b寫100個b:

l1.get_pos

l2.write_buffer

l3.update_pos

以下分幾個場景來討論。

場景1:

執行緒a處在l2,執行緒b進入l1,無疑兩個執行緒將獲得相同的pos,當執行緒b緊隨執行緒a其後進入l2的時候,執行緒b很有很能會將執行緒a的剛剛寫入的資料抹掉。

場景1-1:我在l2按照時間流逝的方向定義三個時間點,l2剛剛開始的時間(馬上就要寫第乙個位元組的那個點),中間的某個時間,l2結束的時間(寫完第100位元組的那個點,100是我們的假設),分別為,t1,t2,t3。

執行緒a在時間t2被從cpu排程出去,不再執行,原因可能是有rt程序來襲,也可能時間片用盡...不管怎樣,它不再執行了,執行緒b進入t1,此時執行緒a已經寫入了若干個a,假設是40個,然後執行緒b一口氣跑到了t3,此時寫入的100位元組全部都是b。執行緒b脫離l2,此時執行緒a被重新拉回cpu,從第41個位元組開始,寫入了60位元組的a結束l2,此時檔案的內容是前面40個b,後面60個a。

分析:毫無疑問,上面的場景得到的結論就是,在一次性的write中,不會出現交叉,而只能出現覆蓋,而具體如何覆蓋是不確定的,有完全覆蓋,也有上述場景1-1中描述的不完全覆蓋,但是一般而言是不會出現不完整覆蓋的情況的,甚至說在多個執行緒每次寫入檔案的位元組數量相等的情況下,是100%不會出現!為什麼呢?這是乙個很關鍵的設計,即l2的過程是不會被打斷的,即它是原子的。不管什麼模式的write,write本身都是原子的,比如你要寫x位元組的資料,但是由於某種原因只寫了x-y個位元組,那麼寫x-y位元組資料的過程是原子的,所謂的write非原子性場景指的是pos定位和write之間的那段,單獨的pos定位和write隨便乙個,都是原子的。

為了下面論述的方便,我重新流程化了write操作:

l1.get_pos

l2-0.lock_inode

l2-1.write_buffer

l2-2.unlock_inode

l3.update_pos

因此,所謂的非原子性write導致的事故只會發生在l1和l2以及l2和l3之間!

場景2:執行緒a比執行緒b先進入l2,但是在l2和l3之間中讓出cpu,導致執行緒b覆蓋了執行緒a的資料,進而執行緒b先走出l3,按照自己的寫入長度設定了pos,導致執行緒a被重新拉回cpu後,pos又被設定了回去。

端午節假期前的最後乙個工作日,同事在糾結於乙個問題,為何ngx或者apache寫日誌的時候都是直接寫的,為何不lock,write既然是非原子的,難道就不怕亂掉嗎?確實沒有亂掉,也真的沒有lock,到底原因何在?按照上面的分析,頻繁寫的時候,應該會亂才對!由於我對ngx的**不熟,也就沒有去細看,我覺得它好像用了o_appendb標誌開啟的檔案。o_append是何方神聖?為了揭示它,我為o_append模式進一步擴充上面write的流程:

l1.get_pos

l2-0.lock_inode

l2-1.change_pos_to_inode->size

l2-2.write_buffer

l2-3.update_inode->size

l2-4.unlock_inode

l3.update_pos

我想到此為止,不用多說,也應該知道為何o_append模式開啟的檔案會是原子操作了,多個執行緒或者程序隨便寫入,不會交叉,不會覆蓋。不過要再次重申,如果一次write沒有寫完乙個buffer,分了好幾次寫,那麼即便是o_append模式的檔案write,也會出現交叉,因為兩次write之間是沒有任何機制保護的。

通過上述的分析,我們可以看出,真正寫的過程是絕對lock的,但是write系統呼叫除了真正的寫,還包括pos的定位,這個定位發生在lock之後還是之前決定了本次呼叫的write是原子的還是非原子的。

註解:場景2模擬**

說實話,在現代cpu上重現場景2造成的現象特別難,幾十行的**你看得很累,對於cpu而言,彈指一揮間就執行完了,因此必須模擬實現,在mm/filemap.c的generic_file_aio_write函式中的mutex_unlock後面加入以下的**即可(你也可以用jprobe在裡面耽擱一下):

if (!strcmp(current->comm, "child"))

}加入這些**是為了模擬線程a被排程出去的情景,既然我知道排程出去並且執行緒b趕超執行緒a之後肯定會有問題,並且這確實會發生,我只是不知道它什麼時候發生而已,因此我就製造乙個它發生的假象。

至於怎麼設計對應的應用程式,唉...fork+exec。

linus的應付之道

就事論事的linus解決原子write的方式超級優美,看一下他的風格:

重新定義兩個帶有lock機制的pos_read/write,總的來講就是為pos設定一把鎖:

+static inline loff_t file_pos_read_lock(struct file *file)

+static inline void file_pos_write_unlock(struct file *file, loff_t pos)

修改sys_write系統呼叫:

file = fget_light(fd, &fput_needed);

if (file)

這種短平快的風格一針見血指出了問題的解決之道,事實上,大多數的複雜性都是優化的副產品!

關於設計模式 策略模式

原則 將經常改變的和不經常改變的分離設計 面向介面而非面向實現程式設計,多組合,少繼承 舉個例子 現在有電影院,一年四季根據季節去打折,假設有乙個movie類,那麼正常設計裡面會有乙個打折方法,方法內部實現是根據季節不同去返回不同的折後 如果需求改變,則該方法需要重新編寫 然後測試前面的 是否會受到...

關於設計模式

這是乙個通用原則。如 方法的設計,類的設計,資料庫介面的設計,網路請求介面的設計等都用到此原則。單一職責原則 singleresponsibility principle 功能要單一。通過方法功能的單一來實現。介面隔離原則 inte ce segregation principle 介面功能細分 介...

關於設計模式

近來在看一些設計模式的資料,有些想法,想寫在這裡。首先我對 設計模式 這個詞用法的準確感到吃驚,因為它沒有提到語言,沒有提到物件導向,只是設計 程式 時的一種形式 覺得還是用模式比較好 這種形式的技巧性,靈活性,獨特性使人內心充動著,大顯身手,做乙個大架構的想法,恨不得裡面全部充滿了設計模式。其實設...