LWN:vfs鎖帶來的一個意外!
關(guān)注了就能看到更多這么棒的文章哦~
A virtual filesystem locking surprise
By Jonathan Corbet
July 31, 2023
ChatGPT assisted translation
https://lwn.net/Articles/939389/
人們都知道,并發(fā)情況會使編程變得更加困難;內(nèi)核開發(fā)中固有的高并發(fā)性就是內(nèi)核開發(fā)工作具有挑戰(zhàn)性的原因之一。然而,如果并發(fā)訪問發(fā)生在代碼中完全沒有預(yù)料到的地方,情況可能會變得更糟。Christian Brauner 的這個短 patch 附帶的長篇說明就是一個關(guān)于并發(fā)性的假設(shè)被證明不正確時可能出現(xiàn)的問題。
在內(nèi)核內(nèi)部,struct file 用于表示打開的文件。它包含與該文件一起工作所需的信息,包括一個非常復(fù)雜的 operation 指針列表、引用計數(shù)、指向相關(guān) inode 的指針、當(dāng)前的讀/寫位置等等。由于打開的文件可以有多個引用,必須有一種方法來把對這個結(jié)構(gòu)的訪問串行起來。在大多數(shù)情況下使用 f_lock 這個 spinlock,但還有一個名為 f_pos_lock 的互斥體是用來訪問文件位置的。
獲取和釋放鎖這個動作本身就是有開銷的。許多 I/O 操作都會影響文件位置,因此 I/O 密集型的工作負(fù)載可能會導(dǎo)致重復(fù)獲取和釋放 f_pos_lock,增加內(nèi)核的開銷。然而,事實是,對打開的文件有多個引用的情況相對較少。如果對給定文件只有一個引用,就不可能發(fā)生對文件中多個位置的并發(fā)訪問,而這個鎖引入開銷就會被浪費掉。為了避免出現(xiàn)這種浪費,獲取 f_pos_lock(__fdget_pos())的函數(shù)中包含了一種優(yōu)化:
if (file_count(file) > 1)
mutex_lock(&file->f_pos_lock);
這里的想法很簡單:如果文件只有一個引用,那么并發(fā)訪問是不可能發(fā)生的,因此沒有必要獲取鎖,可以直接跳過 mutex_lock()調(diào)用。
io_uring 子系統(tǒng)自 2019 年引入以來一直在進(jìn)行密集開發(fā);它正在迅速成為許多內(nèi)核功能的獨立接口。目前正在開展的許多工作是添加與 waitid()、futex 和 getdents()相對應(yīng)的 io_uring operation。最后一個 patch 將 getdents()系統(tǒng)調(diào)用添加到 io_uring 中,就跟這個主題有關(guān)了,因為 getdents() 在多次調(diào)用中需要很多次獲取文件的位置(以及可能由文件系統(tǒng)底層實現(xiàn)來保存的狀態(tài)),以允許進(jìn)程通過多個調(diào)用讀取一個內(nèi)容非常多的目錄。
io_uring 的"fixed files"特性也與碰到的問題有關(guān);它允許一個文件在 io_uring 操作中被多次使用,而不需要承擔(dān)使用常規(guī)系統(tǒng)調(diào)用時必需付出的每次調(diào)用的開銷。這些開銷包括要獲取對文件的引用,并驗證進(jìn)程對其的訪問權(quán)限,在 I/O 密集型應(yīng)用程序中這種開銷可能會很大;將文件 fix (固定?。┚褪沟萌藗冎恍柚Ц兑淮芜@個成本就好,從而提高性能。當(dāng)一個文件被固定到 io_uring 中時,會創(chuàng)建一個新的引用,因此引用計數(shù)將增加。然而,進(jìn)程可以在將文件固定到 io_uring 后關(guān)閉自己的文件描述符,從而只留下這個固定文件的引用。結(jié)果,引用計數(shù)將降至 1。在 io_uring 中進(jìn)行的文件上的 I/O 操作正在進(jìn)行時,這個引用計數(shù)也就一直保持不變。固定文件的目的就是避免重復(fù)獲取和釋放引用的開銷。
Brauner 指出了 getdents() patch 中的一個問題:如果一個文件在 io_uring 中被固定下來,并且其引用計數(shù)為 1,那么在 io_uring 中可能同時運行多個 getdents()操作,每個操作都將在不獲取鎖的情況下訪問 f_pos。這種并發(fā)操作的結(jié)果極有可能不是開發(fā)者所期望的。有人可能會爭辯說,這是一種非常簡單的"直接禁止這樣做"的情況,但正如 Brauner 在他的修復(fù)該問題的 patch 中所描述的那樣,io_uring 并不是唯一會遇到麻煩的情況。
在 2020 年,內(nèi)核引入了一個有趣的系統(tǒng)調(diào)用,名為 pidfd_getfd(),允許有相應(yīng)權(quán)限的進(jìn)程從正在運行的進(jìn)程中提取一個打開的文件描述符。這個操作在某些情況下非常有用,比如允許有特權(quán)的監(jiān)管進(jìn)程來執(zhí)行其他進(jìn)程無法自己執(zhí)行的操作;例如,在容器之外打開文件。為了能正常工作,pidfd_getfd() 創(chuàng)建的文件描述符必須引用與目標(biāo)進(jìn)程中描述符相同的已打開的 file 結(jié)構(gòu)。因此事實上是給該結(jié)構(gòu)創(chuàng)建了第二個引用,也就相應(yīng)地增加了引用計數(shù)。
然而,當(dāng)目標(biāo)進(jìn)程正在進(jìn)行 getdents()系統(tǒng)調(diào)用時如果它的文件描述符被 pidfd_getfd()抓取時會出現(xiàn)問題。由于在調(diào)用 getdents()時,文件的引用計數(shù)為 1,目標(biāo)進(jìn)程不會獲取 f_pos_lock。如果獲取了由 pidfd_getfd()獲得的文件描述符的進(jìn)程還將其傳遞給 getdents(),問題可能會發(fā)生。第二個調(diào)用將看到增加的引用計數(shù)并獲取 f_pos_lock,但由于第一個調(diào)用沒有獲取該鎖,獲取將立即成功,導(dǎo)致兩個 getdents()調(diào)用并發(fā)運行,再次產(chǎn)生設(shè)計之外的情況。
解決方案相當(dāng)簡單:只需無條件地刪除對 f_count 的檢查,并無條件獲取 f_pos_lock。這將引入性能開銷,但似乎沒有人真正擔(dān)心其影響,以至于根本沒有人去實際測量這個開銷。Linus Torvalds 在編輯了 changelog 后將此 patch 合入了 6.5-rc4 版本,他對 pidfd_getfd() 共享文件結(jié)構(gòu)的方式提出了擔(dān)憂。他表示最好是直接重新打開文件(創(chuàng)建新的文件結(jié)構(gòu)),但這將違背 pidfd_getfd()的目的,因為新的文件描述符將不再可以用來代表其他進(jìn)程執(zhí)行操作。
盡管 Torvalds 對 pidfd_getfd() 引入的共享訪問 struct file 仍然感到不滿,但這種行為似乎已經(jīng)成為一種即成事實了。無論如何,這個問題已經(jīng)得到解決,為在 io_uring 中對固定文件(eventual)使用 getdents()清除了道路。但這個事件提供了一個關(guān)于如何在意想不到的方式中出現(xiàn)錯誤的示例,強(qiáng)調(diào)了對并發(fā)性的一些很小的假設(shè)可能會引出的意外問題。
全文完
LWN 文章遵循 CC BY-SA 4.0 許可協(xié)議。
長按下面二維碼關(guān)注,關(guān)注 LWN 深度文章以及開源社區(qū)的各種新近言論~
