 緩沖區(qū))
目錄1.知識回顧2.前置知識:fwrite函數(shù)2.對比示例代碼的運行結(jié)果示例代碼1示例代碼2:去掉示例代碼1中所有字符串中的\n示例代碼3:注釋掉示例代碼2的close(1)3.分析示例代碼,進一步引入C語言的緩沖區(qū)前置知識: 刷新緩沖區(qū)初步結(jié)論進一步分析從內(nèi)核代碼看默認情況下的write將數(shù)據(jù)先寫入內(nèi)核緩沖區(qū),再落磁盤結(jié)論得出原因查看printf、fprintf和fwrite寫入的緩沖區(qū)FILE結(jié)構(gòu)體4.回顧exit和_exit5.緩沖區(qū)刷新策略C語言緩沖區(qū)刷新所有方法1.知識回顧之前在一些文章提到過C語言的緩沖區(qū):24.【C語言】getchar和putchar的使用89.【C語言】文件操作(6)本文進一步解釋2.前置知識:fwrite函數(shù)函數(shù)聲明為:size_t fwrite(const void ptr[restrict .size * .nmemb], size_t size, size_t nmemb, FILE *restrict stream);fwrite以二進制形式對文件進行操作(把ptr所指向的數(shù)組中的數(shù)據(jù)寫入到給定流stream中),不局限于文本文件,可以看到ptr是指向用于寫入的元素數(shù)組的指針,其類型為constvoid,那么傳參給fwrite涉及到ptr的類型轉(zhuǎn)換size表示一個塊的大小(單位是字節(jié)),nmemb表示要求寫入的塊的個數(shù),那么一共需要寫入的大小為nmemb*size,但fwrite返回的是實際寫入的塊數(shù)(不一定等于nmemb),可能ptr指向數(shù)組的大小是小于nmemb*size的)stream是FILE*類型的指針2.對比示例代碼的運行結(jié)果示例代碼1#include stdio.h #include string.h #include unistd.h int main() { const char* str1 hello printf\n; const char* str2 hello fprintf\n; const char* str3 hello fwrite\n; printf(%s,str1); // stdout - 1 fprintf(stdout, %s,str2); // stdout - 1 fwrite(str3, strlen(str3), 1, stdout); // fwrite, stdout - 1 close(1); return 0; }運行結(jié)果:正常打印所有字符串示例代碼2:去掉示例代碼1中所有字符串中的\n#include stdio.h #include string.h #include unistd.h int main() { const char* str1 hello printf; const char* str2 hello fprintf; const char* str3 hello fwrite; printf(%s,str1); // stdout - 1 fprintf(stdout, %s,str2); // stdout - 1 fwrite(str3, strlen(str3), 1, stdout); // fwrite, stdout - 1 close(1); return 0; }運行結(jié)果:什么都沒有打印示例代碼3:注釋掉示例代碼2的close(1)#include stdio.h #include string.h int main() { const char* str1 hello printf; const char* str2 hello fprintf; const char* str3 hello fwrite; printf(str1); // stdout - 1 fprintf(stdout, str2); // stdout - 1 fwrite(str3, strlen(str3), 1, stdout); // fwrite, stdout - 1 //close(1); return 0; }運行結(jié)果:正常打印3.分析示例代碼,進一步引入C語言的緩沖區(qū)前置知識: 刷新緩沖區(qū)刷新緩沖區(qū)指的是將緩沖區(qū)的數(shù)據(jù)寫入流中然后清空緩沖區(qū),那么刷新到顯示器指的是刷新緩沖區(qū)的數(shù)據(jù)到顯示器初步結(jié)論之前講過,進程退出之前會自動刷新緩沖區(qū)對于示例代碼2,printf、fprintf和fwrite都向緩沖區(qū)中寫入不含有\(zhòng)n字符串,那么緩沖區(qū)不會強制刷新到顯示器上,執(zhí)行close(1),接著進程退出之前執(zhí)行自動刷新緩沖區(qū)的操作,但沒有字符串被打印出來推出初步的結(jié)論:這個緩沖區(qū)一定不在操作系統(tǒng)內(nèi),是C語言自帶的緩沖區(qū),它不是操作系統(tǒng)(內(nèi)核)的緩沖區(qū)可以反向思考: 如果緩沖區(qū)在操作系統(tǒng)內(nèi)部,那么調(diào)用C語言文件函數(shù),其內(nèi)部使用write系統(tǒng)調(diào)用將字符串寫入到操作系統(tǒng)的緩沖區(qū),由于close是Linux的系統(tǒng)調(diào)用,那么close(1)時會將字符串刷新到屏幕,但示例代碼2的現(xiàn)象是沒有字符串被打印出來進一步分析通過printf、fprintf和fwrite寫入字符串,字符串會先暫存到C語言提供的緩沖區(qū),等到合適的時機時再通過write將它們寫入到系統(tǒng)的緩沖區(qū),且顯示字符串需要刷新操作系統(tǒng)內(nèi)部的緩沖區(qū)準確來說: 默認情況下(類似int fd open(testfile, O_WRONLY|O_CREAT, 0644);write(fd, hello, 5);),write先將數(shù)據(jù)寫入到系統(tǒng)的緩沖區(qū),待時機成熟后再將緩沖區(qū)的數(shù)據(jù)刷新到磁盤從內(nèi)核代碼看默認情況下的write將數(shù)據(jù)先寫入內(nèi)核緩沖區(qū),再落磁盤以EXT4系統(tǒng)為例,看linux v6.19.10的write函數(shù)的調(diào)用鏈(讀者可以去read_write.c - fs/read_write.c - Linux source code v6.19.10 - Bootlin Elixir Cross Referencer追蹤驗證)write系統(tǒng)調(diào)用→ksys_write→vfs_write→ext4的struct file(定義在/fs/ext4/file.c中)沒有file-f_op-write→new_sync_write→ext4_file_write_iter→沒有DAX和O_DIRECT標志的情況→ext4_buffered_write_iter→generic_perform_write→copy_folio_from_iter_atomic→......→generic_write_sync→......這里給出關(guān)鍵函數(shù)的定義:static ssize_t ext4_buffered_write_iter(struct kiocb *iocb, struct iov_iter *from) { ssize_t ret; struct inode *inode file_inode(iocb-ki_filp); if (iocb-ki_flags IOCB_NOWAIT) return -EOPNOTSUPP; inode_lock(inode); ret ext4_write_checks(iocb, from); if (ret 0) goto out; ret generic_perform_write(iocb, from); out: inode_unlock(inode); if (unlikely(ret 0)) return ret; return generic_write_sync(iocb, ret); }看兩個函數(shù)的作用:先看ssize_t generic_perform_write(struct kiocb *iocb, struct iov_iter *i) { struct file *file iocb-ki_filp; loff_t pos iocb-ki_pos; struct address_space *mapping file-f_mapping; const struct address_space_operations *a_ops mapping-a_ops; size_t chunk mapping_max_folio_size(mapping); long status 0; ssize_t written 0; do { struct folio *folio; size_t offset; /* Offset into folio */ size_t bytes; /* Bytes to write to folio */ size_t copied; /* Bytes copied from user */ void *fsdata NULL; bytes iov_iter_count(i); retry: offset pos (chunk - 1); bytes min(chunk - offset, bytes); balance_dirty_pages_ratelimited(mapping); if (fatal_signal_pending(current)) { status -EINTR; break; } status a_ops-write_begin(iocb, mapping, pos, bytes, folio, fsdata); if (unlikely(status 0)) break; offset offset_in_folio(folio, pos); if (bytes folio_size(folio) - offset) bytes folio_size(folio) - offset; if (mapping_writably_mapped(mapping)) flush_dcache_folio(folio); /* * Faults here on mmap()s can recurse into arbitrary * filesystem code. Lots of locks are held that can * deadlock. Use an atomic copy to avoid deadlocking * in page fault handling. */ copied copy_folio_from_iter_atomic(folio, offset, bytes, i); flush_dcache_folio(folio); status a_ops-write_end(iocb, mapping, pos, bytes, copied, folio, fsdata); if (unlikely(status ! copied)) { iov_iter_revert(i, copied - max(status, 0L)); if (unlikely(status 0)) break; } cond_resched(); if (unlikely(status 0)) { /* * A short copy made -write_end() reject the * thing entirely. Might be memory poisoning * halfway through, might be a race with munmap, * might be severe memory pressure. */ if (chunk PAGE_SIZE) chunk / 2; if (copied) { bytes copied; goto retry; } /* * folio is now unlocked and faults on it can be * handled. Ensure forward progress by trying to * fault it in now. */ if (fault_in_iov_iter_readable(i, bytes) bytes) { status -EFAULT; break; } } else { pos status; written status; } } while (iov_iter_count(i)); if (!written) return status; iocb-ki_pos written; return written; } EXPORT_SYMBOL(generic_perform_write);generic_perform_write內(nèi)部調(diào)用了write_begin函數(shù):status a_ops-write_begin(iocb, mapping, pos, bytes,folio, fsdata);在/Documentation/filesystems/vfs.rst里面明確提到了write_begin函數(shù)的職責The filesystem must returnthe locked pagecache(頁緩存! 說明了generic_perform_write負責將數(shù)據(jù)寫入頁緩存) foliofor thespecified offset:write_begin Called by the generic buffered write code to ask the filesystem to prepare to write len bytes at the given offset in the file. The address_space should check that the write will be able to complete, by allocating space if necessary and doing any other internal housekeeping. If the write will update parts of any basic-blocks on storage, then those blocks should be pre-read (if they havent been read already) so that the updated blocks can be written out properly. The filesystem must return the locked pagecache folio for the specified offset, in *foliop, for the caller to write into. It must be able to cope with short writes (where the length passed to write_begin is greater than the number of bytes copied into the folio). A void * may be returned in fsdata, which then gets passed into write_end. Returns 0 on success; 0 on failure (which is the error code), in which case write_end is not called.繼續(xù)看generic_write_sync:/* * Sync the bytes written if this was a synchronous write. Expect ki_pos * to already be updated for the write, and will return either the amount * of bytes passed in, or an error if syncing the file failed. */ static inline ssize_t generic_write_sync(struct kiocb *iocb, ssize_t count) { if (iocb_is_dsync(iocb)) { int ret vfs_fsync_range(iocb-ki_filp, iocb-ki_pos - count, iocb-ki_pos - 1, (iocb-ki_flags IOCB_SYNC) ? 0 : 1); if (ret) return ret; } else if (iocb-ki_flags IOCB_DONTCACHE) { struct address_space *mapping iocb-ki_filp-f_mapping; filemap_flush_range(mapping, iocb-ki_pos - count, iocb-ki_pos - 1); } return count; } /** * vfs_fsync_range - helper to sync a range of data metadata to disk * file: file to sync * start: offset in bytes of the beginning of data range to sync * end: offset in bytes of the end of data range (inclusive) * datasync: perform only datasync * * Write back data in range start..end and metadata for file to disk. If * datasync is set only metadata needed to access modified file data is * written. */ int vfs_fsync_range(struct file *file, loff_t start, loff_t end, int datasync) { struct inode *inode file-f_mapping-host; if (!file-f_op-fsync) return -EINVAL; if (!datasync (inode_state_read_once(inode) I_DIRTY_TIME)) mark_inode_dirty_sync(inode); return file-f_op-fsync(file, start, end, datasync); } EXPORT_SYMBOL(vfs_fsync_range);generic_write_sync內(nèi)部調(diào)用vfs_fsync_range,在vfs_fsync_range定義上方的注釋寫得很清楚:helper to sync a range of data metadata to disk,說明了generic_write_sync負責將數(shù)據(jù)同步到磁盤結(jié)論結(jié)論1: write內(nèi)部調(diào)用generic_perform_write和generic_write_sync,其中 generic_perform_write負責將數(shù)據(jù)寫入頁緩存(Page Cache),generic_write_sync負責將數(shù)據(jù)同步到磁盤繼而得出結(jié)論2: 默認情況下(類似int fd open(testfile, O_WRONLY|O_CREAT, 0644);write(fd, hello, 5); 不含DAX和O_DIRECT標志),write先將數(shù)據(jù)寫入到內(nèi)核的緩沖區(qū)(準確來說是頁緩存!),待時機成熟后再將緩沖區(qū)的數(shù)據(jù)刷新到磁盤得出原因通過進一步分析,使用close(1)導(dǎo)致fd1的文件(stdout)被關(guān)閉,導(dǎo)致進程退出前自動刷新內(nèi)核緩沖區(qū)時,無法將C語言提供的緩沖區(qū)中字符串的指針鏈接(不是拷貝,可以通過gdb的rwatch命令驗證)到操作系統(tǒng)的緩沖區(qū)(因為找不到fd1對應(yīng)的緩沖區(qū))然后刷新操作系統(tǒng)的緩沖區(qū)那么可以在close(1)之前將C語言緩沖區(qū)的字符串寫入到操作系統(tǒng)的緩沖區(qū),可以使用write系統(tǒng)調(diào)用例如以下代碼:#include stdio.h #include string.h #include unistd.h int main() { const char* str1 hello printf; const char* str2 hello fprintf; const char* str3 hello fwrite; write(1, str1, strlen(str1)); write(1, str2, strlen(str2)); write(1, str3, strlen(str3)); close(1); return 0; }即使在關(guān)閉文件描述符fd1后之前使用write寫入的數(shù)據(jù)也已經(jīng)確實發(fā)送到顯示器了運行結(jié)果:或者這樣改:#include stdio.h #include string.h #include unistd.h int main() { const char* str1 hello printf; const char* str2 hello fprintf; const char* str3 hello fwrite; printf(%s,str1); // stdout - 1 fprintf(stdout, %s,str2); // stdout - 1 fwrite(str3, strlen(str3), 1, stdout); // fwrite, stdout - 1 fflush(stdout); close(1); return 0; }運行結(jié)果:查看printf、fprintf和fwrite寫入的緩沖區(qū)以含有fflush(stdout)的代碼為例,先在這些地方下斷點:r命令執(zhí)行rprintf、fprintf和fwrite寫入的緩沖區(qū)在stdout指向的FILE結(jié)構(gòu)體中,gdb下可以這樣查看stdout指向的FILE結(jié)構(gòu)體:p *stdoutc命令繼續(xù)執(zhí)行c再次查看stdout指向的FILE結(jié)構(gòu)體,發(fā)現(xiàn)已經(jīng)寫入了一個字符串:執(zhí)行fprintf后,又寫入了字符串:執(zhí)行fwrite后,又寫入了字符串:執(zhí)行fflush(stdout),會將FILE結(jié)構(gòu)體中的字符串的指針鏈接到操作系統(tǒng)的緩沖區(qū)中,而是否刷新到屏幕上是操作系統(tǒng)決定的,可以看C89文檔的描述:fflush函數(shù)使得該流中任何未寫入的數(shù)據(jù)被傳送給主機環(huán)境,并寫入文件對于fflush內(nèi)部的實現(xiàn),可以看glibc在libio/iofflush.c的源碼結(jié)論: 用戶刷新的本質(zhì),就是將數(shù)據(jù)通過fd1和write系統(tǒng)調(diào)用寫入到內(nèi)核(上面提到的主機環(huán)境)中(*注: 目前認為,只要將數(shù)據(jù)刷新到了內(nèi)核,數(shù)據(jù)就到可以硬件了)FILE結(jié)構(gòu)體由前面的分析可知: 文件操作繞不開FILE,FILE結(jié)構(gòu)體里面含有打開文件的緩沖區(qū)字段和維護信息那么打開n個文件就有n個文件描述符,也就有n個語言級別上的緩沖區(qū)FILE結(jié)構(gòu)體的源代碼在glibc-2.42的libio/bits/types/FILE.h中給出了FILE的重定義:#ifndef __FILE_defined #define __FILE_defined 1 struct _IO_FILE; /* The opaque type of streams. This is the definition used elsewhere. */ typedef struct _IO_FILE FILE; #endif可以發(fā)現(xiàn)FILE是struct _IO_FILE的簡寫在glibc-2.42的libio/bits/types/struct_FILE.h中,給出了結(jié)構(gòu)體完整的定義:/* The tag name of this struct is _IO_FILE to preserve historic C mangled names for functions taking FILE* arguments. That name should not be used in new code. */ struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ /* The following pointers correspond to the C streambuf protocol. */ char *_IO_read_ptr; /* Current read pointer */ char *_IO_read_end; /* End of get area. */ char *_IO_read_base; /* Start of putbackget area. */ char *_IO_write_base; /* Start of put area. */ char *_IO_write_ptr; /* Current put pointer. */ char *_IO_write_end; /* End of put area. */ char *_IO_buf_base; /* Start of reserve area. */ char *_IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; int _flags2:24; /* Fallback buffer to use when malloc fails to allocate one. */ char _short_backupbuf[1]; __off_t _old_offset; /* This used to be _offset but its too small. */ /* 1column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE };1.可以看到里面的int _fileno就是文件描述符,原因如下:因為IO相關(guān)函數(shù)與系統(tǒng)調(diào)用接口對應(yīng),并且?guī)旌瘮?shù)封裝系統(tǒng)調(diào)用,所以本質(zhì)上訪問文件都是通過fd訪問的,因此C庫當中的FILE結(jié)構(gòu)體內(nèi)部必定封裝了fd2.FILE對象屬于用戶(因為是C語言標準規(guī)定的,而且編程語言屬于用戶層),里面的緩沖區(qū)是用戶的緩沖區(qū)3.每用C語言的fopen函數(shù)打開一個文件就要創(chuàng)建該文件的FILE對象4.回顧exit和_exit之前在OS23.【Linux】進程終止文章提到了exit和_exit,這里簡單回顧:1. exit是C語言退出函數(shù),訪問C語言提供的緩沖區(qū)是合情合理的,那么使用exit退出時會刷新C語言的緩沖區(qū)到內(nèi)核2. _exit是系統(tǒng)調(diào)用,作用也是退出,但和exit不一樣的是: _exit處于底層,無法訪問C語言提供的緩沖區(qū),也就不會刷新C語言提供的緩沖區(qū),_exit會close(1)然后結(jié)束進程5.緩沖區(qū)刷新策略C語言提供的緩沖區(qū)和操作系統(tǒng)內(nèi)部的緩沖區(qū)的刷新策略有所不同,這里分開說C語言緩沖區(qū)刷新所有方法1.行緩沖: 直到見到\n才刷新,其余情況不刷新(例如顯示器)2.全緩沖: 緩沖區(qū)滿了才刷新(例如普通文件的寫入)3.無緩沖: 直接刷新如果是打印到顯示器,行刷新或程序結(jié)束的時候刷新如果是打印到文件,那么只有程序結(jié)束的時候才刷新沖刷緩沖區(qū)的策略可能丟棄數(shù)據(jù)也可能將數(shù)據(jù)寫入文件