原文 : http://hi.baidu.com/tim_bi/blog/item/63728b8b613f33dcfd1f10dd.html
UNIX環境進階編程的3.11節講述了原子操作,其中有一種情形是在檔案尾端添加資料。文中說,如果多個進程都需要將資料添加到某一檔案,那麼為了保證定位和寫資料這兩步是一個原子操作,需要在開啟檔案時設定O_APPEND標誌,看到這裡我們就會想,雖然保證了定位和寫資料是一個原子操作,但是是否能夠保證多個進程或線程寫入的資料不會交錯呢,比如A進程調用write(filedes1, "AAA", 3),B進程調用write(filedes2, "BBBB", 4)(其中filedes1和filedes2指向同一個檔案),但是最後檔案中的資料是否有可能是AABBBAB,如果這個檔案是一個管道或socket呢。linux man手冊頁中關於write調用的說明很不詳細,並未說明寫操作是否是原子的,所以我們有必要尋找Single UNIX Specification(SUS)對write調用的說明,在SUS中對此調用的說明還是比較詳細的。在繼續討論之前我們需要清楚核心在寫檔案之前會對該檔案加鎖,不管是否成功完成寫操作,在返回之前都會解鎖。下面我們就以三種常見的檔案根據SUS標準來討論上面提出的這個問題:
1.普通檔案
SUS中也沒有說明在寫普通檔案時是否會保證是原子操作,但是它說明了write調用可能並不能完全把我們需要寫入的資料寫到檔案中去,那麼什麼情況下可能少寫資料呢。SUS說明了兩種情況:磁碟已滿或則要寫入的檔案的大小超過了當前進程的檔案大小限制。其實至少還有一種情況,那就是核心中的快取不夠用的時候,比如linux核心在發現快取不夠用的時候就唯寫入實際能夠容下的資料然後返回。正是由於存在上述最後一種情況,所以說按照APUE那種方法在linux下面寫檔案並不能保證我們的資料不會交錯(不過我們可以根據write的傳回值得知是否有發生交錯的可能)。其它的unix核心可能會在實現上不同於linux核心,他們可能在寫之前就判斷一下緩衝區是否足夠容納所有資料,如果是這種情況,寫操作應該就是原子的;也可能寫了一部分資料後才發現緩衝區不夠用並讓當前進程進入睡眠狀態,此時核心如果解鎖,那麼在當前進程睡眠期間其它進程可能寫了資料,如果不解鎖,那麼就是原子操作,其他進程不可能在這個時候寫入資料。由上面的分析可知,正是由於SUS標準不太完整的標準,我們不能確定一定可以按APUE的方法來同時向同一個普通檔案寫資料。如果我們非要在同一個檔案中記錄多個進程產生的資料,我們最好採用unix日誌系統採用的方法,用一個專用進程處理檔案IO,其它進程把需要寫的資料發送給這個專用進程,這樣應該比多個進程同時寫一個檔案可靠和高效。
2.管道
SUS對管道的寫操作說得更多也更明確,我們只需遵照其標準就可以了。對於write(pipefd, buf, nbyte),其要點如下:
如果nbyte <= PIPE_BUF,不管O_NONBLOCK是否設定,其寫操作都是原子的,就是說多個進程都在此條件下同時寫同一個管道不會引起資料交錯。
如果nbyte > PIPE_BUF,是不能保證寫操作是原子的,寫入的資料可能與其他進程寫入的資料交錯。
3.socket
SUS中對於寫socket並沒有說很多,我們無法從標準中得知write是否保證寫操作的原子性。我看了一下linux 2.6.14核心關於tcp資料的寫操作,發現它不是原子的,也從網上查到了這部分代碼的作者(們)對這個問題的看法,他(們)認為對一個可能永久阻塞的操作保證原子性是錯誤的。我們也只能姑且這麼認為了。
補充:
對於用UNIX日誌系統伺服器的方法,串連端必須每個線程connect一次logsvr,這樣才能保證發過來的日誌資料不互相錯亂,保證原子性;此時logsvr只要用reactor方法來處理每個線程的串連就好,把這些fd放到隊列裡輪流處理,寫檔案,也保證了寫檔案的原子性。
實際上Log Service器一般都是用UDP來完成的。