核心 API,第 1 部分:從核心呼叫使用者空間應用程式
呼叫特定的核心函式(系統呼叫)是 GNU/Linux 中軟體開發的原本就有的組成部分。但如果方向反過來呢,核心空間呼叫使用者空間?確實有一些有這種特性的應用程式需要每天使用。例如,當核心找到一個裝置,這時需要載入某個模組,程序如何處理?動態模組載入在核心通過 usermode-helper 程序進行。
讓我們從探索 usermode-helper 應用程式程式設計介面(API)以及在核心中使用的例子開始。 然後,使用 API 構造一個示例應用程式,以便更好地理解其工作原理與侷限。
usermode-helper API
usermode-helper API 是個很簡單的 API,其選項為使用者熟知。例如,要建立一個使用者空間程序,通常只要設定名稱為 executable,選項都為 executable,以及一組環境變數(指向 execve
核心版本
本文探討的是 2.6.27 版核心的 usermode-helper API。
表 1 展示的是 usermode-helper API 中一組關鍵的核心函式
表 1. usermode-helper API 中的核心函式
API 函式 | 描述 |
---|---|
call_usermodehelper_setup |
準備 user-land 呼叫的處理函式 |
call_usermodehelper_setkeys |
設定 helper 的會話金鑰 |
call_usermodehelper_setcleanup |
為 helper 設定一個清空函式 |
call_usermodehelper_stdinpipe |
為 helper 建立 stdin 管道 |
call_usermodehelper_exec |
呼叫 user-land |
表 2 中還有一些簡化函式,它們封裝了的幾個核心函式(用一個呼叫代替多個呼叫)。這些簡化函式在很多情況下都很有用,因此儘可能使用他們。
表 2. usermode-helper API 的簡化
API 函式 | 描述 |
---|---|
call_usermodehelper |
呼叫 user-land |
call_usermodehelper_pipe |
使用 stdin 管道呼叫
user-land |
call_usermodehelper_keys |
使用會話金鑰呼叫 user-land |
讓我們先瀏覽一遍這些核心函式,然後探索簡化函式提供了哪些功能。核心 API 使用了一個稱為 subprocess_info
結構的處理函式引用進行操作。該結構(可在
./kernel/kmod.c 中找到)集合了給定的 usermode-helper 例項的所有必需元素。該結構引用從call_usermodehelper_setup
呼叫返回。該結構(以及後續呼叫)將會在 call_usermodehelper_setkeys
(用於儲存憑證)、call_usermodehelper_setcleanup
以及 call_usermodehelper_stdinpipe
的呼叫中進一步配置。最後,一旦配置完成,就可通過呼叫 call_usermodehelper_exec
來呼叫配置好的使用者模式應用程式。
宣告
該方法提供了一個從核心呼叫使用者空間應用程式必需的函式。儘管這項功能有合理用途,還應仔細考慮是否需要其他實現。這是一個方法,但其他方法會更合適。
核心函式提供了最大程度的控制,其中 helper 函式在單個呼叫中完成了大部分工作。管道相關呼叫(call_usermodehelper_stdinpipe
和
helper 函式call_usermodehelper_pipe
)建立了一個相聯管道供
helper 使用。具體地說,建立了管道(核心中的檔案結構)。使用者空間應用程式對管道可讀,核心對管道可寫。對於本文,核心轉儲只是使用 usermode-helper 管道的應用程式。在該應用程式(./fs/exec.cdo_coredump()
)中,核心轉儲通過管道從核心空間寫到使用者空間。
這些函式與 sub_processinfo
以及 subprocess_info
結構的細節之間的關係如圖
1 所示。
圖 1. Usermode-helper API 關係
表 2 中的簡化函式內部執行 call_usermodehelper_setup
函式和 call_usermodehelper_exec
函式。表
2 中最後兩個呼叫分別呼叫的是call_usermodehelper_setkeys
和 call_usermodehelper_stdinpipe
。可以在
./kernel/kmod.c 找到 call_usermodehelper_pipe
和call_usermodehelper
的程式碼,在
./include/linux/kmod.h 中找到 call_usermodhelper_keys
的程式碼。
為什麼要從核心呼叫使用者空間應用程式?
現在讓我們看一看 usermode-helper API 所使用的核心空間。表 3 提供的並不是專門的應用程式列表,而是一些有趣應用的示例。
表 3. 核心中的 usermode-helper API 應用程式
應用程式 | 原始檔位置 |
---|---|
核心模組呼叫 | ./kernel/kmod.c |
電源管理 | ./kernel/sys.c |
控制組 | ./kernel/cgroup.c |
安全密匙生成 | ./security/keys/request_key.c |
核心事件交付 | ./lib/kobject_uevent.c |
最直接的 usermode-helper API 應用程式是從核心空間載入核心模組。request_module
函式封裝了
usermode-helper API 的功能並提供了簡單的介面。在一個常用的模組中,核心指定一個裝置或所需服務並呼叫 request_module
來載入模組。通過使用
usermode-helper API,模組通過 modprobe
載入到核心(應用程式通過 request_module
在使用者空間被呼叫)。
與模組載入類似的應用程式是裝置熱插拔(在執行時新增或刪除裝置)。該特性是通過使用 usermode-helper API,呼叫使用者空間的 /sbin/hotplug 工具實現的。
關於 usermode-helper API 的一個有趣的應用程式(通過 request_module
)是文字搜尋
API(./lib/textsearch.c)。該應用程式在核心中提供了一個可配置的文字搜尋基礎架構。該應用程式使用 usermode-helper API 將搜尋演算法當作可載入模組進行動態載入。在 2.6.30 核心版本中,支援三個演算法,包括 Boyer-Moore(./lib/ts_bm.c),簡單固定狀態機方法(./lib/ts_fsm.c),以及 Knuth-Morris-Pratt 演算法(./lib/ts_kmp.c)。
usermode-helper API 還支援 Linux 按照順序關閉系統。當需要系統關閉電源時,核心呼叫使用者空間的 /sbin/poweroff 命令來完成。其他應用程式如 表 3 所示,表中附有其原始檔位置。
Usermode-helper API 內部
在 kernel/kmod.c 中可以找到 usermode-helper API 的原始碼 和 API(展示了主要的用作核心空間的核心模組載入器)。這個實現使用kernel_execve
完成髒工作(dirty
work)。請注意 kernel_execve
是在啟動時開啟 init
程序的函式,而且未使用
usermode-helper API。
usermode-helper API 的實現相當簡單直觀(見圖 2)。usermode-helper 從呼叫 call_usermodehelper_exec
開始執行(它用於從預先配置好的 subprocess_info
結構中清除使用者空間應用程式)。該函式接受兩個引數:subprocess_info
結構引用和一個列舉型別(不等待、等待程序中止及等待程序完全結束)。subprocess_info
(或者是,該結構的 work_struct
元素)然後被壓入工作佇列(khelper_wq
),然後佇列非同步執行呼叫。
圖 2. usermode-helper API 內部實現
當一個元素放入 khelper_wq
時,工作佇列的處理函式就被呼叫(本例中是__call_usermodehelper
),它在 khelper
執行緒中執行。該函式從將 subprocess_info
結構出隊開始,此結構包含所有使用者空間呼叫所需資訊。該路徑下一步取決於 wait
列舉變數。如果請求者想要等整個程序結束,包含使用者空間呼叫(UMH_WAIT_PROC
)或者是根本不等待(UMH_NO_WAIT
),那麼會從 wait_for_helper
函式建立一個核心執行緒。否則,請求者只是等待使用者空間應用程式被呼叫(UMH_WAIT_EXEC
),但並不完全。這種情況下,會為____call_usermodehelper()
建立一個核心執行緒。
在 wait_for_helper
執行緒中,會安裝一個
SIGCHLD 訊號處理函式,併為 ____call_usermodehelper
建立另一個核心執行緒。但在wait_for_helper
執行緒中,會呼叫 sys_wait4
來等待 ____call_usermodehelper
核心執行緒(由
SIGCHLD 訊號指示)結束。然後執行緒執行必要的清除工作(為 UMH_NO_WAIT
釋放結構空間或簡單地向 call_usermodehelper_exec()
回送一個完成報告)。
函式 ____call_usermodehelper
是實際讓應用程式在使用者空間啟動的地方。該函式首先解鎖所有訊號並設定會話金鑰環。它還安裝了 stdin
管道(如果有請求)。進行了一些安裝以後,使用者空間應用程式通過 kernel_execve
(來自
kernel/syscall.c)被呼叫,此檔案包含此前定義的path
、argv
清單(包含使用者空間應用程式名稱)以及環境。當該程序完成後,此執行緒通過呼叫 do_exit()
而產生。
該程序還使用了 Linux 的 completion,它是像訊號一樣的操作。當 call_usermodehelper_exec
函式被呼叫後,就會宣告
completion。當subprocess_info
結構放入 khelper_wq
後,會呼叫 wait_for_completion
(使用
completion 變數作為引數)。請注意此變數會儲存到subprocess_info
結構作為 complete
欄位。當子執行緒想要喚醒 call_usermodehelper_exec
函式,會呼叫核心方法 complete
,並判斷來自 subprocess_info
結構的
completion 變數。該呼叫會解鎖此函式使其能繼續。可以在 include/linux/completion.h 中找到 API 的實現。
通過點選 參考資料 部分中的連結可以找到更多關於 usermode-helper API 的資料。
應用程式示例
現在,讓我們看看 usermode-helper API 的簡單應用。首先看一下標準 API,然後學習如何使用 helper 函式使事情更簡單。
在該例中,首先開發了一個簡單的呼叫 API 的可載入核心模組。清單 1 展示了樣板模組功能,定義了模組入口和出口函式。這兩個函式根據模組的 modprobe
(模組入口函式)或 insmod
(模組入口函式),以及 rmmod
(模組出口函式)被呼叫。
清單 1. 模組樣板函式
#include <linux/module.h> #include <linux/init.h> #include <linux/kmod.h> MODULE_LICENSE( "GPL" ); static int __init mod_entry_func( void ) { return umh_test(); } static void __exit mod_exit_func( void ) { return; } module_init( mod_entry_func ); module_exit( mod_exit_func );
usermode-helper API 的使用如 清單
2 所示,其中有詳細描述。函式開始是宣告所需變數和結構。以 subprocess_info
結構開始,它包含所有的執行使用者空間呼叫的資訊。該呼叫在呼叫 call_usermodehelper_setup
時初始化。下一步,定義引數列表,使 argv
被呼叫。該列表與普通 C
程式中的 argv
列表類似,定義了應用程式(陣列第一個元素)和引數列表。需要
NULL 終止符來提示列表末尾。請注意這裡的 argc
變數(引數數量)是隱式的,因為 argv
列表的長度已經知道。該例中,應用程式名是
/usr/bin/logger,引數是 help!
,然後是
NULL 終止符。下一個所需變數是環境陣列(envp
)。該陣列是一組定義使用者空間應用程式執行環境的引數列表。本例中,定義一些常用的引數,這些引數用於定義
shell 並以 NULL 條目結束。
清單 2. 簡單的 usermode_helper API 測試
static int umh_test( void ) { struct subprocess_info *sub_info; char *argv[] = { "/usr/bin/logger", "help!", NULL }; static char *envp[] = { "HOME=/", "TERM=linux", "PATH=/sbin:/bin:/usr/sbin:/usr/bin", NULL }; sub_info = call_usermodehelper_setup( argv[0], argv, envp, GFP_ATOMIC ); if (sub_info == NULL) return -ENOMEM; return call_usermodehelper_exec( sub_info, UMH_WAIT_PROC ); }
下一步,呼叫 call_usermodehelper_setup
來建立已初始化的 subprocess_info
結構。請注意使用了先前初始化的變數以及指示用於記憶體初始化的
GFP 遮蔽第四個引數。在安裝函式內部,呼叫了 kzalloc
(分配核心記憶體並清零)。該函式需要 GFP_ATOMIC
或 GFP_KERNEL
標誌(前者定義呼叫不可以休眠,後者定義可以休眠)。快速測試新結構(即,非
NULL)後,使用 call_usermodehelper_exec
函式繼續呼叫。該函式使用 subprocess_info
結構以及定義是否等待的列舉變數(在
“Usermode-helper API 內部” 一節中有描述)。全部完成! 模組一旦載入,就可以在 /var/log/messages 檔案中看到資訊。
還可以通過 call_usermodehelper
API
函式進一步簡化程序,它同時執行 call_usermodehelper_setup
和 call_usermodehelper_exec
函式。如清單
3 所示,它不僅刪除函式,還消除了呼叫者管理 subprocess_info
結構的必要性。
清單 3. 更簡單的 usermode-helper API 測試
static int umh_test( void )
{
char *argv[] = { "/usr/bin/logger", "help!", NULL };
static char *envp[] = {
"HOME=/",
"TERM=linux",
"PATH=/sbin:/bin:/usr/sbin:/usr/bin", NULL };
return call_usermodehelper( argv[0], argv, envp, UMH_WAIT_PROC );
}
請注意在清單 3 中,有著同樣的安裝並呼叫(例如初始化 argv
和 envp
陣列)的需求。此處惟一的區別是
helper 函式執行 setup
和 exec
函式。
更深入的內容
usermode-helper API 是核心中重要的部分,這是由於其廣泛多樣的用途(從核心模組載入、裝置熱插拔到 udev 事件釋出)。儘管 API 實際的應用程式非常重要,但核心也是很重要的一部分,是非常有用的 Linux 工具。