模塊的初始化和關閉
更多內容請參考Linux設備驅動程序學習----目錄
1. 初始化函數
??模塊的初始化函數負責注冊模塊所提供的任何設施,即可以被應用程序訪問的新功能,可能是一個完整的驅動程序或者僅僅是一個新的軟件抽象。初始化函數的定義通常如下所示:
static int __init initialization_function(void)
{
// 初始化代碼
return 0;
}
module_init(initialization_function);
??初始化函數被聲明為static,因為初始化函數在特定文件之外沒有其他意義。__init標記表明該函數僅在初始化期間使用。在模塊被裝載之后,模塊裝載器就會將初始化函數扔掉,可將該函數占用的內存釋放出來。
??注意:不要在結束初始化之后仍要使用的函數或數據結構上使用__init和__initdata標記。對于__devinit和__devinitdata,只有在內核未被配置為支持熱插拔設備的情況下,才會被翻譯為__init和__initdata。
??module_init()宏的使用是強制性的,會在模塊的目標代碼中增加一個特殊的段,用于說明內核初始化函數所在的位置。如果沒有這個定義,初始化函數永遠不會被調用。
2. 清除函數
??每個模塊都需要一個清除函數,在模塊被移除前注銷接口并向系統中返回所有資源。該函數定義如下:
static void __exit cleanup_function(void)
{
// 清除代碼
}
module_exit(cleanup_function);
??清除函數沒有返回值,__exit修飾詞標記該代碼僅用于模塊卸載,編譯器會把該函數放在特殊的ELF段中。如果模塊被直接編譯到內核中,或者內核配置不允許卸載模塊,則被標記為__exit的函數將被直接丟棄。所以被標記為__exit的函數只能在模塊被卸載或者系統關閉時被調用,其他任何用法都是錯的。module_exit()聲明對于內核找到模塊的清除函數是必需的。如果一個模塊未定義清除函數,則內核不允許卸載該模塊。
3. 初始化過程中的錯誤處理
??在內核中注冊設施時,注冊可能會失敗。即使最簡單的動作,都需要內存分配,而所需的內存可能無法獲得。因此模塊代碼必須始終檢查返回值,并確保所請求的操作已真正執行成功。如果在注冊設施時遇到錯誤,首先要判斷模塊是否可以繼續初始化,只要可能,模塊應該繼續向前并盡可能提供其功能。
??如果在發生了某個特定類型的錯誤之后無法繼續裝載模塊,則要將出錯之前的所有注冊工作都撤銷掉。即當模塊的初始化出現錯誤之后,模塊必須自行撤銷已注冊的設施。如果未能撤銷已注冊的設施,則內核會處于一種不穩定狀態,這時,唯一有效的解決辦法就是重新引導系統。所以必須在初始化過程出現錯誤時認真完成正確的工作。
??錯誤恢復的處理有時使用goto語句非常有效。正常情況下,很少使用goto,但是唯一在錯誤處理時卻非常有效。內核經常使用goto來處理錯誤。如下例子所示:
int __init my_init_function(void)
{
int err;
// 使用指針和名稱注冊
err = register_this(ptr1, "skull");
if (err)
goto fail_this;
err = register_that(ptr2, "skull");
if (err)
goto fail_that;
err = register_those(ptr3, "skull");
if (err)
goto fail_those;
return 0; // 成功
fail_those:
unregister_that(ptr2, "skull");
fail_that:
unregister_that(ptr1, "skull");
fail_this:
return err; // 返回錯誤
}
在出錯的時候使用goto語句,將只撤銷出錯時刻以前所成功注冊的那些設施。
??另一種方法是,記錄任何成功注冊的設施,在出錯的時候調用模塊的清除函數。清除函數將僅僅回滾已成功完成的步驟。這種方法需要更多的代碼和CPU時間,因此在追求效率的代碼中使用goto語句是最好的錯誤恢復機制。
??在Linux內核中錯誤編碼是定義在<linux/errno.h>頭文件中的負整數,如果不想使用其他函數返回的錯誤碼,應該包含<linux/errno.h>頭文件,以使用如:-ENODEV、-ENOMEM之類的符號值。每次返回核時的錯誤編碼是個好習慣,因為用戶程序可以通過perror()函數或類似途徑將錯誤符號轉換為有意義的字符串。
??模塊的清除函數需要撤銷初始化函數所注冊的所有設施,并且習慣上以相反于注冊的順序撤銷設施,如下所示:
void __exit my_cleanup_function(void)
{
unregister_those(ptr3, "skull");
unregister_that(ptr2, "skull");
unregister_this(ptr1, "skull");
return;
}
??如果初始化和清除工作涉及很多設施,則goto方法可能難以管理,因為所有用于清除設施的代碼在初始化函數中給重復,同時一些標號交織在一起。
??每次發生錯誤時從初始化函數中調用清除函數,將減少代碼的重復并且時代碼更清晰、更有條理。清除函數必須在撤銷每項設施的注冊之前檢查它的狀態。如下示例:
struct something *item1;
struct somethingelse *item2;
void my_cleanup(void)
{
if (item1)
release_thing(item1);
if (item2)
release_thing2(item2);
if (stuff_ok)
unregister_stuff();
return;
}
int __init my_init(void)
{
int err = -ENOMEM;
item1 = allocate_thing(arguments);
item2 = allocate_thing2(arguments2);
if (!item1 || !item2)
goto fail;
err = register_stuff(item1, item2);
if (!err)
stuff_ok = 1;
else
goto fail;
return 0; // 返回成功
fail:
my_cleanup();
return err; // 返回錯誤
}
??如上代碼所示,根據調用的注冊/分配函數的語義,可以使用或不使用外部標記來標記每個初始化步驟的成功。這種方式的初始化能很好地擴展到對大量設施的支持。注意:因為清除函數被非退出代碼調用,因此不能將清除函數標記為__exit;
4. 模塊裝載競爭
??模塊裝載中也存在競態。在模塊注冊完成之前,內核的某些部分可能會立即使用我們剛剛注冊的任何設施,即在初始化函數還在運行的時候,內核就完全可能會調用我們的模塊。因此,在首次注冊完成之后,代碼就應該準備好被內核的其他部分調用;在支持某個設施的所有內部初始化完成之前,不要注冊任何設施。
??當模塊初始化失敗而內核的某些部分已經使用了模塊所注冊的某個設施時,此時根本不應該出現模塊初始化失敗,因為模塊已經成功導出了可用的功能及符號。如果初始化一定要失敗,則應該仔細處理內核其他部分正在進行的操作,并且要等待這些操作的完成。
更多內容請參考Linux設備驅動程序學習----目錄