比ls快8倍?百萬級檔案遍歷的奇技淫巧
1.問題背景
在Linux下當我們操作一個檔案數較少的目錄時,例如執行ls列出當前目錄下所有的檔案,這個命令可能會瞬間執行完畢,但是當一個目錄下有上百萬個檔案時,執行ls命令會發生什麼呢,帶著疑問,我們做了如下實驗(實驗中使用的儲存裝置為NVMe介面的SSD):
[root@localhost /data1/test_ls]# for i in {1..1000000}; do echo 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' > $i.txt ; done[root@localhost /data1/test_ls]# time ls -l | wc -l1000001real 0m5.802s
user 0m2.544s
sys 0m3.328s
可以看到,統計一個包含1000000個小檔案的目錄下的檔案個數花費了將近6秒的時間,那麼檔案個數多造成ls緩慢的原因是什麼呢,且聽我們詳細分析。
2.原理分析
眾所周知,strace是分析系統呼叫的利器,所以我們用strace來分析在大目錄下執行ls命令的結果,其中這樣的輸出引起了我們的注意:
...getdents(3, /* 1024 entries */, 32768) = 32768getdents(3, /* 1024 entries */, 32768) = 32768getdents(3, /* 1024 entries */, 32768) = 32768getdents(3, /* 1024 entries */, 32768) = 32768brk(0) = 0x12e8000brk(0x1309000) = 0x1309000getdents(3, /* 1024 entries */, 32768) = 32768mremap(0x7f93b6246000, 2461696, 4919296, MREMAP_MAYMOVE) = 0x7f93b5d95000getdents(3, /* 1024 entries */, 32768) = 32768getdents(3, /* 1024 entries */, 32768) = 32768getdents(3, /* 1024 entries */, 32768) = 32768brk(0) = 0x1309000brk(0x132a000) = 0x132a000...
可以看到,在大目錄下執行ls命令會頻繁呼叫getdents
這一系統呼叫,實際上我們通過檢視coreutils的ls.c原始碼可以發現:
ls會首先呼叫opendir
開啟一個目錄,然後迴圈呼叫readdir
這個glibc中的函式直到遇到目錄流的結尾,也即讀完所有的目錄項(dentry)為止。我們首先看一下man page裡面對於readdir
的定義:
struct dirent *readdir(DIR *dirp);
readdir
返回一個指向dirent結構體的指標,指向目錄流dirp中的下一個目錄項,所以在print_dir
的迴圈中,每次從目錄流中取出一個目錄項並賦值給next
變數。既然說到目錄流(directory stream),我們順便看一下glibc中對它的定義:
從上面的定義中可以看到,目錄流實則維護一個buffer,這個buffer的大小由allocation來確定,那麼問題來了,allocation值什麼時候確定,其實是在opendir
過程中確定下來的。opendir
的呼叫路徑如下所示:
__opendir-->__opendirat-->__alloc_dir
在__alloc_dir
中,
會分配sizeof(DIR) + allocation
大小的記憶體空間,最後將allocation賦值給目錄流dirp的allocation變數。allocation的預設值通過比較4*BUFSIZ的大小和dirent64結構體的大小(<32768)來確定,BUFSIZ的大小在以下幾個標頭檔案中定義:
stdio.h: #define BUFSIZ _IO_BUFSIZ
libio.h: #define _IO_BUFSIZ _G_BUFSIZ
_G_config.h: #define _G_BUFSIZ 8192
回看一下strace中的輸出,getdents第三個引數以及返回值32768就是這麼來的。
講完目錄流的buffer大小是怎麼確定的之後,讓我們回到readdir
的glibc實現。
這段程式碼的邏輯還是比較清晰的,首先判斷目錄流的偏移量有沒有超過buffer的大小,如果超過,則說明已經讀完緩衝區中的所有內容,需要重新呼叫getdents
讀取,getdents
一次最多讀取32768個位元組(有_DIRENT_HAVE_D_RECLEN
定義時為dirp->allocation
),並將讀取到的buffer返回給dirp->data
,讀取到的位元組數返回給dirp->size
,然後重置偏移量為0。如果沒有超過buffer大小,則從dirp->offset
開始讀,然後將偏移量增加reclen
個位元組作為下次讀取的起點,reclen
記錄在目錄項結構體dirent
的d_reclen
變數中,表示當前目錄項的長度,dirent
(DIRENT_TYPE)這個結構體的定義如下所示:
struct dirent{
__ino_t d_ino; /* inode number */
__off_t d_off; /* offset to the next dirent */
unsigned short int d_reclen; /* length of this record */
unsigned char d_type; /* type of file */
char d_name[256]; /* filename */};
總結一下以上整個過程就是,ls命令每次呼叫readdir
都會從目錄流中讀取一個目錄項,如果目錄流的buffer讀完,就會重新呼叫getdents
填充這一buffer,下次從新buffer的開頭開始讀,buffer的預設大小為32K,這也就意味著如果一個目錄下有大量的目錄項(目錄項的總大小可以通過ls -dl
檢視),則執行ls命令時將會頻繁地呼叫getdents
,導致目錄下的檔案數越多時ls的執行時間越長。
3.解決方法
既然glibc中readdir
的buffer大小我們沒法控制,何不繞過readdir
直接呼叫getdents
,在這個系統呼叫中我們可以直接控制buffer的大小,以下就是一個簡單的例子listdir.c:
在這段程式碼中,我們將getdents
的buffer大小設定為5M,編譯執行這段程式碼,我們得到如下結果:
[root@localhost /data1]# time ./listdir test_rm | wc -l1000016real 0m0.755s
user 0m0.432s
sys 0m0.320s
統計目錄中的檔案數由預設的5.802s縮短為0.755s,可以看到提升還是較為明顯的。
4. 總結
其實不止是ls命令,其他一些命令如rm -r
等的實現中都會用到glibc中的readdir
函式,所以如果遇到操作百萬級檔案的大目錄這種場景(當然實踐中不提倡一個目錄下放這麼多檔案),不妨直接呼叫getdents並加上自己的一些邏輯,這樣就可以在實現標準命令功能的基礎上,還能獲得其不具備的效能提升。
公眾號推薦:
相關文章
- JNI 檔案遍歷
- 遍歷檔案Java中遍歷出指定目錄中的所有檔案Java
- UTL_FILE遍歷檔案
- 遍歷目錄下的所有檔案
- PHP遍歷目錄和檔案PHP
- 利用 python 遍歷多級資料夾處理不同檔案Python
- oracle 10g 以上 遍歷檔案Oracle 10g
- 迭代及用遞迴遍歷File檔案遞迴
- matlab遍歷資料夾下的所有檔案Matlab
- Python字典的遍歷,包括key遍歷/value遍歷/item遍歷/Python
- JavaScript 陣列遍歷方法的對比JavaScript陣列
- ls命令 & 檔案屬性
- ls -l檔案型別型別
- C/C++遍歷資料夾和檔案C++
- 陣列常見的遍歷迴圈方法、陣列的迴圈遍歷的效率對比陣列
- js的map遍歷和array遍歷JS
- VBA遍歷 Excel 合併到一個 Excel 檔案Excel
- Godot遍歷目錄下檔案,並建立按鈕Go
- 列出目錄/檔案命令ls
- PHP超低記憶體遍歷目錄檔案和讀取超大檔案PHP記憶體
- Linux迴圈遍歷所有檔案,刪除指定字尾名檔案Linux
- golang for range 遍歷 對比 PHP、pythonGolangPHPPython
- JavaScript遍歷方法總結與對比JavaScript
- IOS各種集合遍歷效率對比iOS
- ASP.NET Core 中的檔案提供程式 遍歷資料夾讀取檔案資訊ASP.NET
- 二叉樹的建立、前序遍歷、中序遍歷、後序遍歷二叉樹
- jQuery遍歷函式,javascript中的each遍歷jQuery函式JavaScript
- Java zip解壓,並遍歷zip中的配置檔案 .cfg或.propertiesJava
- 遞迴遍歷磁碟下的某一資料夾中所有檔案,並copy檔案生成檔案和帶資料夾的檔案遞迴
- jQuery的遍歷結構設計之遍歷同胞jQuery
- jQuery的遍歷結構設計之遍歷祖先jQuery
- 圖的遍歷演算法-馬遍歷棋盤演算法
- 喂,不是吧!一遍弄懂樹、圖的遍歷操作------入門級
- ls命令+檔案型別+別名(alias)型別
- java集合遍歷的幾種方式總結及比較Java
- 如何遍歷資料量億級別Mongo庫Go
- python遍歷之批次更改檔名Python
- 二叉樹的廣度遍歷和深度遍歷()二叉樹