linux下多進程寫入文件的原子性


一、文件寫入的原子性

管道在整個unix系統中有重要的基礎設施意義,它使unix工具設計的“職能簡單”原則得以實現的基礎,不同的工具使用管道協調完成自己的功能,並把一個功能做好。一個想法的提出通常具有明確的場景和簡潔的原理,后來需求的不斷發展導致問題看起來極為復雜,就像我們現在社會的進化,可能原始社會中大家都是餓了吃,困了睡,兩者都找不到就去死的節奏。
shell通過管道,讓各個工具協調工作,基本的方法也是通過管道,看shell的語法文件中,單單是重定向着一個語法就占用了十幾條。匿名管道之后,又有了命名管道,當命名管道創建之后,多個進程寫入同一個管道的可能就會變大。在這種情況下,一些鑽牛角尖的人就想確定下多進程寫入一個管道時,讀出方讀到的數據和寫入方的寫入是否一致,更糟糕的是,不同的寫入者一次寫入的內容是否會與其他寫入者一次性寫入的數據交錯在一起?
二、glibc中對於fprintf函數的實現
1、glibc的FILE鎖
其實想想printf的實現,也有些細節。比方說,用戶提供的格式化字符串沒有任何限制,例如,對於簡單的
printf("%s%s", str1, str2)
這樣的命令,用戶態提供了兩個任意起始地址的字符串,寫入時需要將兩這個字符串放在一個連續的內存空間中寫入內核,此時printf應該如何連接這個字符串。最為直觀的辦法就是分配一個足以包含兩個字符串長度的連續空間,把它們連接在一起,然后傳遞給系統調用。
這里只是最為簡單的情況,單單是打印一個hello world,沒有問題,考慮到格式化的任意性,這種把所有的結果字符串整理完成之后再寫入,明顯是不切合實際的。這相當於要求任意的字符串都要有兩份相同的備份,並且對於連續空間的要求也過於苛刻。
glibc對於格式化文件輸出的代碼位於glibc-2.6\stdio-common\vfprintf.c,這個文件中使用了較多的宏,看起來有些詭異,但是並不影響我們對於此處實現的理解。從該文件的實現可以看到
   _IO_flockfile (s);
……
  f = lead_str_end = __find_specwc ((const UCHAR_T *) format);
……
  /* Process whole format string.  */
  do
    {
……
/* Write the following constant string.  */
outstring (specs[nspecs_done].end_of_fmt,
   specs[nspecs_done].next_fmt
   - specs[nspecs_done].end_of_fmt);
  }
all_done:
  if (__builtin_expect (workstart != NULL, 0))
    free (workstart);
  /* Unlock the stream.  */
   _IO_funlockfile (s);
  _IO_cleanup_region_end (0);
glibc的做法就是化整為零,在處理一個格式化輸出的時候首先加鎖,這里的“鎖”位於FILE結構中,通過相同的FILE結構訪問到相同的鎖,共享相同的互斥。反過來說,就是FILE中使用的fd相同,只要它們封裝在不同的FILE結構中,它們之間的寫入也並不互斥。加上鎖之后,glibc不再一次性把所有的字符格式化完成之后傳遞給write函數執行,而是只處理自己“基本職責”功能,就是掃描一個格式化字符串中的描述並逐個處理。對於上面的printf("%s%s", str1, str2)例子,vfpintf函數根據%s來講str1的內容通過outstring傳遞給更底層的FILE結構緩沖管理層。這個緩沖的大小默認情況下在文件打開時通過stat函數獲得文件所在的文件系統的block大小,FILE緩沖一個block作為自己的緩沖區大小
tsecer@harry #touch hehe
tsecer@harry #stat hehe
  File: `hehe'
  Size: 0               Blocks: 0          IO Block:  4096   regular empty file
2、FILE結構的緩存機制
執行outstring之后,該輸出內容被放到緩沖層進行排隊及溢出處理,這部分代碼主要位於genops.c文件中。當一個緩沖區慢之后,執行沖刷。也預讀緩存數據,減少不必要的讀取操作。
迄今為止,可以看到進程內調用fprintf可以保證不同線程調用同一個FILE結構的printf具有原子性。但是由於這個鎖只是進程范圍內有效的一個粒度,所以不同進程之間對於同一個底層文件的寫入不能保證原子性。甚至對於同一個文件,如果通過不同fopen調用返回的FILE結構寫入,也不能保證寫入的互斥型。
整個緩存的管理代碼比較繁瑣,而且當前沒有遇到需要理解這部分代碼才能解釋的問題,所以這部分先掠過。
三、常規文件(ext2)對write系統調用的實現
sys_write-->>vfs_write--->>>do_sync_write-->>generic_file_aio_write
 mutex_lock(&inode->i_mutex); 
 ret = __generic_file_aio_write_nolock(iocb, iov, nr_segs, &iocb->ki_pos); 
 mutex_unlock(&inode->i_mutex);
從這個調用關系中可以看到,整個文件的寫入經過了系統級的mutex鎖,所以這個寫入是原子性的。也就是說,對於一次write系統調用寫入的內容不會與系統中任意一個系統調用寫入的內容重疊。
四、pipe文件系統對write的實現
1、真正write的實現
 

sys_write-->>vfs_write--->>>do_sync_write-->>generic_file_aio_write

ret = 0;
mutex_lock(&inode->i_mutex);
pipe = inode->i_pipe;
 
if (!pipe->readers) {
send_sig(SIGPIPE, current, 0);
ret = -EPIPE;
goto out;
}
 
/* We try to merge small writes */
chars = total_len & (PAGE_SIZE-1); /* size of the last buffer */
if (pipe->nrbufs && chars != 0) {
……
}
 
for (;;) {
int bufs;
 
if (!pipe->readers) {
send_sig(SIGPIPE, current, 0);
if (!ret)
ret = -EPIPE;
break;
}
bufs = pipe->nrbufs;
if (bufs < PIPE_BUFFERS) {
……
if (do_wakeup) {
wake_up_interruptible_sync(&pipe->wait);
kill_fasync(&pipe->fasync_readers, SIGIO, POLL_IN);
do_wakeup = 0;
}
pipe->waiting_writers++;
pipe_wait(pipe);
pipe->waiting_writers--;
}
out:
mutex_unlock(&inode->i_mutex);
2、互斥鎖操作
和其它函數一樣,在函數的最開始也煞有介事的加上了一把互斥鎖,所以很容易讓人以為每次寫入的操作是一個原子性操作。但是在寫入的循環中調用了一個特殊的pipe_wait操作,
void pipe_wait(struct pipe_inode_info *pipe)
{
DEFINE_WAIT(wait);
 
/*
 * Pipes are system-local resources, so sleeping on them
 * is considered a noninteractive wait:
 */
prepare_to_wait(&pipe->wait, &wait,
TASK_INTERRUPTIBLE | TASK_NONINTERACTIVE);
if (pipe->inode)
mutex_unlock(&pipe->inode->i_mutex);
schedule();
finish_wait(&pipe->wait, &wait);
if (pipe->inode)
mutex_lock(&pipe->inode->i_mutex);
}
在寫入過程中,如果寫入緩沖區已滿,write調用通過pipe_wait釋放互斥鎖,相當於“秦人失其鹿,天下共逐之”,這個鎖就這樣從指縫間溜走。在pipe_read系統調用中,也會獲得這把鎖,由於管道內核中是通過共享的環形緩沖區實現的,所以這一點不足為奇。
3、如果保證PIPE_BUFF以內寫操作的原子性
在pipe_write函數中,首先嘗試將對頁面取模剩余部分和上次寫操作進行合並
/* We try to merge small writes */
chars = total_len & (PAGE_SIZE-1); /* size of the last buffer */
if (pipe->nrbufs && chars != 0) 
if (ops->can_merge && offset + chars <= PAGE_SIZE)
這里的合並操作檢測其實非常嚴格,只有當本次小於頁面部分和上個頁面剩余空間的和小於一個頁面的時候才可以進行寫操作,這個地方是保證小於頁面大小寫入原子性的一個保障。舉個例子,當前最后一個緩沖頁面剩余2K,此時有3K數據寫入,由於兩者之和大於一個頁面,所以不會將3K拆分到該頁面中,而是直接分配一個新的頁面,從而保證小於頁面大小寫入的原子性。這也說明了內核中雖然為一個pipe預留了16個頁面緩沖,但是並不一定能緩存16個頁面的內容。
在pipe_read側,這個地方也進行了優化
if (!buf->len) {
buf->ops = NULL;
ops->release(pipe, buf);
curbuf = (curbuf + 1) & (PIPE_BUFFERS-1);
pipe->curbuf = curbuf;
pipe->nrbufs = --bufs;
do_wakeup = 1;
}
只有讀空了一個頁面之后才進行wakeup操作,這只是一個優化,並不是用來保證寫操作的原子性。
posix中相關規定
POSIX.1-2001 says  that write(2)s of less than PIPE_BUF bytes must be atomic: the output data is written to the pipe as a contiguous sequence. Writes of more than  PIPE_BUF bytes may be nonatomic: the kernel may interleave the data with data written by other processes. POSIX.1-2001 requires PIPE_BUF to be at least 512 bytes. (On Linux, PIPE_BUF is 4096 bytes.) The precise semantics depend on whether the file descriptor is nonblocking (O_NONBLOCK), whether there are multiple writers to the pipe, and on n, the number of bytes to be written:
這里也注意內核中的
#define PIPE_BUFFERS (16)
#define PIPE_BUF PAGE_SIZE
並不相同,PIPE_BUFFERS定義為16並不是表示內核保證16頁面寫入的原子性,這個只是為了減少讀寫操作的阻塞。
五、writev系統調用的原子性
sys_writev--->>>vfs_writev--->>do_readv_writev
{
fn = (io_fn_t)file->f_op->write;
fnv = file->f_op->aio_write;
}
if (fnv)
ret = do_sync_readv_writev(file, iov, nr_segs, tot_len,
pos, fnv);
else
ret = do_loop_readv_writev(file, iov, nr_segs, pos, fn);
如果一個文件系統沒有提供aio_write操作,則執行do_loop_readv_writev,這個操作循環調用文件的write接口並且循環中沒有加解鎖操作,明顯地,這個地方可能存在非原子性的地方。但是對於我們常見的ext2文件系統,這點不用擔心,因為它們都提供了aio_write操作。
關於這一點,Google搜索驗證下可以看到有人也有這種疑慮, 但是沒有確切回復,搜索函數名do_loop_readv_writev,第一個結果就是關於這個函數寫入原子性的質疑。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM