1. 程式人生 > >核心 API,第 1 部分:從核心呼叫使用者空間應用程式

核心 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 關係
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 內部實現
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)被呼叫,此檔案包含此前定義的pathargv 清單(包含使用者空間應用程式名稱)以及環境。當該程序完成後,此執行緒通過呼叫 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 工具。