LWN:kernel里進(jìn)行基于生命周期的資源管理!
關(guān)注了就能看到更多這么棒的文章哦~
Scope-based resource management for the kernel
By Jonathan Corbet
June 15, 2023
DeepL assisted translation
https://lwn.net/Articles/934679/
C 語(yǔ)言不提供那些更加新的語(yǔ)言中的資源管理(resource-management)功能。因此,比如內(nèi)存泄漏或無(wú)法釋放 lock 的錯(cuò)誤在用 C 編寫(xiě)的程序中相對(duì)常見(jiàn),Linux 內(nèi)核也是如此。不過(guò),kernel 項(xiàng)目從未將自己限制 C 語(yǔ)言標(biāo)準(zhǔn)中。內(nèi)核開(kāi)發(fā)人員很樂(lè)意使用編譯器提供的擴(kuò)展,只要它們被證明是有用的。看起來(lái)編譯器所提供的相對(duì)簡(jiǎn)單的功能可能會(huì)導(dǎo)致一些常見(jiàn)的內(nèi)核編碼方式發(fā)生很大的變化。
具體來(lái)說(shuō),我們討論的是 cleanup 這個(gè) attribute,它在 GCC 和 Clang 中都有實(shí)現(xiàn)。允許使用以下語(yǔ)法聲明變量:
type my_var __attribute__((__cleanup__(cleanup_func)));
這個(gè)新增的屬性表示,當(dāng)該 type 的變量 my_var 超出范圍時(shí),應(yīng)調(diào)用:
cleanup_func(&my_var);
也就是認(rèn)為會(huì)用此函數(shù)在該變量永遠(yuǎn)消失之前對(duì)其進(jìn)行某種清理動(dòng)作。例如,可以這樣聲明一個(gè)指針(在內(nèi)核中):
void auto_kfree(void **p) { kfree(*p); }
struct foo *foo_ptr __attribute__((__cleanup__(auto_kfree))) = NULL;
/* ... */
foo_ptr = kmalloc(sizeof(struct foo));
之后就不用再花心思去釋放所分配的內(nèi)存了。如果 foo_ptr 不再存在了,那么編譯器確保會(huì)對(duì)它調(diào)用 kfree()。再也不可能泄露這段內(nèi)存了,除非有人非常努力去讓它泄露。
這個(gè)屬性不是最近剛出現(xiàn)的,但內(nèi)核從未利用過(guò)它。5月下旬,Peter Zijlstra 決定改變這種情況,于是發(fā)布了一組 patch set,利用這個(gè)功能實(shí)現(xiàn)了“鎖和指針保護(hù)”。此后不久發(fā)布了第二個(gè)版本,并引起了相當(dāng)多的討論,Linus Torvalds 鼓勵(lì) Zijlstra 將這項(xiàng)工作擴(kuò)充一下,不僅僅是保護(hù) lock。于是 6 月 12 日發(fā)布的基于作用域的資源管理(scope-based resource management)patch set 就出現(xiàn)了,它創(chuàng)建了一組新的宏,希望簡(jiǎn)化 cleanup 屬性的使用。由 57 個(gè) patch 組成,還把大量代碼都改成了使用新增的宏,并提供了一組詳細(xì)的示例,來(lái)介紹如何用這些宏改變內(nèi)核代碼庫(kù)的樣子。
Cleanup functions in the kernel
首先定義了一個(gè)新的宏 __cleanup() ,它把上面展示的 attribute 語(yǔ)法進(jìn)行了簡(jiǎn)化。然后可以用一組宏來(lái)創(chuàng)建和管理這些自己會(huì)釋放自己的指針:
#define DEFINE_FREE(name, type, free) \
static inline void __free_##name(void *p) { type _T = *(type *)p; free;}
#define __free(name) __cleanup(__free_##name)
#define no_free_ptr(p) \
({ __auto_type __ptr = (p); (p) = NULL; __ptr; })
#define return_ptr(p) return no_free_ptr(p)
DEFINE_FREE() 的目的是將一個(gè)清理函數(shù)跟特定 type 關(guān)聯(lián)起來(lái)(盡管 “type” 實(shí)際上只是另一個(gè)標(biāo)識(shí)符,并不會(huì)跟某種具體的 C 語(yǔ)言 type 關(guān)聯(lián))。因此,可以使用類似下面的聲明設(shè)置一個(gè) free 函數(shù):
DEFINE_FREE(kfree, void *, if (_T) kfree(_T))
在這個(gè)宏中,創(chuàng)建了一個(gè)名為 __free_kfree() 的新函數(shù),如果傳入的指針不是 NULL,則調(diào)用 kfree()。沒(méi)有人會(huì)直接調(diào)用該函數(shù),但這個(gè)聲明之后就可以編寫(xiě)如下代碼了:
struct obj *p __free(kfree) = kmalloc(...);
if (!p)
return NULL;
if (!initialize_obj(p))
return NULL;
return_ptr(p);
__free() 這個(gè) attribute 會(huì)把我們的清理函數(shù)與指針 p 相關(guān)聯(lián),確保當(dāng)該指針不再使用時(shí)(無(wú)論是因?yàn)槭裁丛颍┒紩?huì)調(diào)用這個(gè) __free_kfree()。因此舉例來(lái)說(shuō),上面的第二個(gè) return 語(yǔ)句就不會(huì)把給 p 分配的內(nèi)存泄露掉,哪怕沒(méi)有顯式調(diào)用 kfree() 也是安全的。
但是有時(shí)并不需要自動(dòng) free,比如一切都按預(yù)期正常進(jìn)行,并且指向已分配對(duì)象的指針應(yīng)給調(diào)用方返回的情況。專門為此設(shè)計(jì)的 return_ptr() 宏就通過(guò)把 p 復(fù)制到另一個(gè)變量、將 p 設(shè)置為 NULL 、然后返回復(fù)制后的值來(lái)阻止自動(dòng) free 動(dòng)作。通常出錯(cuò)的情況會(huì)有很多中,而正常完成只有一種方式,因此以這種方式來(lái)對(duì)正常路徑進(jìn)行標(biāo)注會(huì)更有意義。
From cleanup functions to classes
自動(dòng)清理函數(shù)只是一個(gè)開(kāi)始,事實(shí)證明,使用此編譯器功能可以完成更多的工作。經(jīng)過(guò)一番討論,人們認(rèn)為處理內(nèi)核中資源管理的更通用功能的最佳命名是“class”。因此,下一步是將內(nèi)核使用的“class”添加到 C 語(yǔ)言中:
#define DEFINE_CLASS(name, type, exit, init, init_args...) \
typedef type class_##name##_t; \
static inline void class_##name##_destructor(type *p) \
{ type _T = *p; exit; } \
static inline type class_##name##_constructor(init_args) \
{ type t = init; return t; }
此宏使用了指定的 name 創(chuàng)建一個(gè)新的 “class”,封裝了該 type 的值。 exit() 函數(shù)是此 class 的析構(gòu)函數(shù)(也就是 cleanup 函數(shù)),而 init() 則是構(gòu)造函數(shù),它將接收 init_args 作為參數(shù)。宏定義了一個(gè)類型和幾個(gè)新函數(shù)來(lái)處理初始化和銷毀的工作。
然后,就可以使用 CLASS() 宏來(lái)定義此類的變量:
#define CLASS(name, var) \
class_##name##_t var __cleanup(class_##name##_destructor) = \
class_##name##_constructor
此宏會(huì)被替換為變量 var 的聲明,該變量通過(guò)調(diào)用構(gòu)造函數(shù)來(lái)進(jìn)行初始化。請(qǐng)注意,得到的結(jié)果是一個(gè)不完整的語(yǔ)句;必須向構(gòu)造函數(shù)提供參數(shù)才能讓這個(gè)語(yǔ)句變完整,如下所示。此處使用 __cleanup() 宏可確保當(dāng) class 的變量不再可用時(shí)將調(diào)用此 class 的析構(gòu)函數(shù)。
在 patch set 中展示了這個(gè)宏的一個(gè)用途是將一些結(jié)構(gòu)引入對(duì)文件的引用的管理上,因?yàn)槲募煤苋菀仔孤?chuàng)建了一個(gè)名為 fdget 的新 class,用于管理這些文件引用的獲取和釋放。
DEFINE_CLASS(fdget, struct fd, fdput(_T), fdget(fd), int fd)
創(chuàng)建一個(gè)構(gòu)造函數(shù)(名為 class_fdget_constructor() ,但該名稱永遠(yuǎn)不會(huì)顯式出現(xiàn)在代碼中)以通過(guò)調(diào)用 fdget() 來(lái)初始化類,并將整數(shù) fd 作為其參數(shù)。此初始化創(chuàng)建對(duì)文件的引用,該文件必須在某個(gè)時(shí)候返回。類定義還創(chuàng)建了一個(gè)析構(gòu)函數(shù),該析構(gòu)函數(shù)調(diào)用 fdput() ,當(dāng)此類的變量超出范圍時(shí),編譯器將調(diào)用該析構(gòu)函數(shù)。
想要使用文件描述符 fd,可以通過(guò)如下調(diào)用來(lái)利用這個(gè) class structure:
CLASS(fdget, f)(fd);
此行聲明了一個(gè)名為 f 的新變量,類型為 1,由 fdget 類來(lái)管理。
最后,有一些宏來(lái)定義與 lock 相關(guān)的類:
#define DEFINE_GUARD(name, type, lock, unlock) \
DEFINE_CLASS(name, type, unlock, ({ lock; _T; }), type _T)
#define guard(name) \
CLASS(name, __UNIQUE_ID(guard))
DEFINE_GUARD() 針對(duì) lock 類型創(chuàng)建了一個(gè) class。例如,它與具有以下聲明的 mutex 配合使用:
DEFINE_GUARD(mutex, struct mutex *, mutex_lock(_T), mutex_unlock(_T)):
然后用 guard() 宏創(chuàng)建此 class 的一個(gè)實(shí)例,為其生成一個(gè)唯一的名稱(沒(méi)有人會(huì)看到,也不用關(guān)心)。在此 patch 中就可以看到這個(gè)基礎(chǔ)設(shè)施的示例用法,把:
mutex_lock(&uclamp_mutex);
替換為:
guard(mutex)(&uclamp_mutex);
之后,可以刪除專門釋放 uclamp_mutex 的代碼了,在各種分支情況下用來(lái)進(jìn)行 unlock 動(dòng)作的所有錯(cuò)誤處理代碼都可以刪除掉了。
The guard-based future
在上面的示例中,刪除錯(cuò)誤處理代碼這個(gè)步驟非常重要。內(nèi)核中的一種常見(jiàn)情況就是在函數(shù)結(jié)束時(shí)來(lái)執(zhí)行清理動(dòng)作,并在出現(xiàn)問(wèn)題時(shí)使用 goto 語(yǔ)句跳轉(zhuǎn)到這些 cleanup 代碼中的適當(dāng)位置。偽代碼類似這樣:
err = -EBUMMER;
mutex_lock(&the_lock);
if (!setup_first_thing())
goto out;
if (!setup_second_thing())
goto out2;
/* ... */
out2:
cleanup_first_thing();
out:
mutex_unlock(&the_lock);
return err;
這是對(duì) goto 的相對(duì)比較克制的使用方式了,但在內(nèi)核代碼中加起來(lái)仍然有大量的 goto 語(yǔ)句,并且相對(duì)比較容易出錯(cuò)。廣泛采用這種新機(jī)制的話就可以使上述代碼模式看起來(lái)像這樣:
guard(mutex)(&the_lock);
CLASS(first_thing, first)(...);
if (!first or !setup_second_thing())
return -EBUMMER;
return 0;
代碼更加緊湊,并且降低了出現(xiàn)與資源相關(guān)的 bug 的機(jī)會(huì)。
所實(shí)現(xiàn)的宏肯定不止此處討論的這些,還包括了用于管理 read-copy-update(RCU) 關(guān)鍵區(qū)(critical section)的一個(gè)專門變種。好奇的讀者可以在相關(guān) patch 中找到。
這組 patch 中一個(gè)挺有意思的副作用是刪除了針對(duì)首次使用之后才做聲明的這種編譯器警告,這個(gè) warning 是內(nèi)核開(kāi)發(fā)工作中長(zhǎng)期以來(lái)的要求,從而避免用這種方式來(lái)混合聲明和使用。如果不放寬該規(guī)則了話,這些宏就無(wú)法生效。Torvalds 同意這一改動(dòng),他說(shuō)也許規(guī)則可以稍微放松一些:
我認(rèn)為這個(gè)特定的規(guī)則確實(shí)是一個(gè)挺好的規(guī)則,但同時(shí)我也認(rèn)為可以不讓它成為一個(gè)硬性規(guī)則,而是只讓它成為一個(gè)通常情況下的編碼風(fēng)格,但在確有必要的時(shí)候可以允許把聲明和代碼混起來(lái)。
對(duì)這項(xiàng)工作的回復(fù)大多都是很正面的。Torvalds 似乎對(duì)這種新機(jī)制的總體方向感到滿意,僅僅抱怨了幾個(gè)特定轉(zhuǎn)換中的潛在 bug,以及這組 patch 看起來(lái)太大了。因此,這個(gè)功能似乎很有可能會(huì)合入未來(lái)的 kernel。最終可能讓內(nèi)核中的資源管理更安全,并且 goto 也更少了。
全文完
LWN 文章遵循 CC BY-SA 4.0 許可協(xié)議。
長(zhǎng)按下面二維碼關(guān)注,關(guān)注 LWN 深度文章以及開(kāi)源社區(qū)的各種新近言論~
