基於作用域的清理輔助函式¶
“goto error”模式因引入隱蔽的資源洩漏而臭名昭著。在已經有多個展開條件的執行路徑中新增新的資源獲取約束是繁瑣且容易出錯的。“cleanup”輔助函式使編譯器能夠幫助處理這種繁瑣,並有助於維護 LIFO(後進先出)展開順序,以避免意外洩漏。
由於驅動程式構成了核心程式碼庫的大部分,這裡有一個使用這些輔助函式清理 PCI 驅動程式的例子。清理的目標是在返回前使用 goto 來展開裝置引用(pci_dev_put())或解鎖裝置(pci_dev_unlock())的場合。
DEFINE_FREE() 宏可以安排在相關變數超出作用域時釋放 PCI 裝置引用。
DEFINE_FREE(pci_dev_put, struct pci_dev *, if (_T) pci_dev_put(_T))
...
struct pci_dev *dev __free(pci_dev_put) =
pci_get_slot(parent, PCI_DEVFN(0, 0));
上述程式碼會在 dev 超出作用域(自動變數作用域)時,如果 dev 非空,則自動呼叫 pci_dev_put()。如果一個函式希望在出錯時呼叫 pci_dev_put(),但在成功時返回 dev(即不釋放它),它可以這樣做:
return no_free_ptr(dev);
...或者
return_ptr(dev);
DEFINE_GUARD() 宏可以安排在呼叫 guard() 的作用域結束時釋放 PCI 裝置鎖。
DEFINE_GUARD(pci_dev, struct pci_dev *, pci_dev_lock(_T), pci_dev_unlock(_T))
...
guard(pci_dev)(dev);
由 guard() 輔助函式獲取的鎖的生命週期遵循自動變數宣告的作用域。請看以下示例:
func(...)
{
if (...) {
...
guard(pci_dev)(dev); // pci_dev_lock() invoked here
...
} // <- implied pci_dev_unlock() triggered here
}
注意,鎖在“if ()”塊的剩餘部分持有,而不是在“func()”的剩餘部分持有。
現在,當一個函式同時使用 __free() 和 guard(),或者多個 __free() 例項時,變數定義的 LIFO 順序很重要。GCC 文件指出:
“當同一作用域中的多個變數具有 cleanup 屬性時,在退出該作用域時,它們的關聯清理函式將按照定義的相反順序執行(最後定義的變數,首先清理)。”
當展開順序很重要時,需要將變數定義在函式作用域的中間,而不是檔案的頂部。請看以下示例並注意“!!”突出顯示的錯誤:
LIST_HEAD(list);
DEFINE_MUTEX(lock);
struct object {
struct list_head node;
};
static struct object *alloc_add(void)
{
struct object *obj;
lockdep_assert_held(&lock);
obj = kzalloc(sizeof(*obj), GFP_KERNEL);
if (obj) {
LIST_HEAD_INIT(&obj->node);
list_add(obj->node, &list):
}
return obj;
}
static void remove_free(struct object *obj)
{
lockdep_assert_held(&lock);
list_del(&obj->node);
kfree(obj);
}
DEFINE_FREE(remove_free, struct object *, if (_T) remove_free(_T))
static int init(void)
{
struct object *obj __free(remove_free) = NULL;
int err;
guard(mutex)(&lock);
obj = alloc_add();
if (!obj)
return -ENOMEM;
err = other_init(obj);
if (err)
return err; // remove_free() called without the lock!!
no_free_ptr(obj);
return 0;
}
該錯誤可以透過將 init() 修改為按此順序呼叫 guard() 並定義 + 初始化 obj 來修復:
guard(mutex)(&lock);
struct object *obj __free(remove_free) = alloc_add();
鑑於在函式頂部定義的變數使用“__free(...) = NULL”模式會帶來這種潛在的相互依賴問題,建議在使用 __free() 時,始終在一個語句中定義和賦值變數,而不是將變數定義集中在函式頂部。
最後,鑑於清理輔助函式的好處是消除“goto”,並且“goto”語句可以在不同作用域之間跳轉,因此期望在同一個函式中永遠不要混合使用“goto”和清理輔助函式。也就是說,對於給定的例程,要麼將所有需要“goto”清理的資源轉換為基於作用域的清理,要麼不轉換任何一個。