轉自:http://hi.baidu.com/techson/blog/item/b017442426525d31c99559f8.html
多數的 Linux 內核態程序都需要和用戶空間的進程交換數據,但 Linux 內核態無法對傳統的 Linux 進程間同步和通信的方法提供足夠的支持。本文總結並比較了幾種內核態與用戶態進程通信的實現方法,並推薦使用 netlink 套接字實現中斷環境與用戶態進程通信。
Linux 是一個源碼開放的操作系統,無論是普通用戶還是企業用戶都可以編寫自己的內核代碼,再加上對標準內核的裁剪從而製作出適合自己的操作系統。目前有很多中低 端用戶使用的網絡設備的操作系統是從標準 Linux 改進而來的,這也說明了有越來越多的人正在加入到 Linux 內核開發團體中。
一 個或多個內核模塊的實現並不能滿足一般 Linux 系統軟件的需要,因爲內核的侷限性太大,如不能在終端上打印,不能做大延時的處理等等。當我們需要做這些的時候,就需要將在內核態採集到的數據傳送到用戶 態的一個或多個進程中進行處理。這樣,內核態與用戶空間進程通信的方法就顯得尤爲重要。在 Linux 的內核發行版本中沒有對該類通信方法的詳細介紹,也沒有其他文章對此進行總結,所以本文將列舉幾種內核態與用戶態進程通信的方法並詳細分析它們的實現和適 用環境。
|
在一臺運行 Linux 的計算機中,CPU 在任何時候只會有如下四種狀態:
【1】 在處理一個硬中斷。
【2】 在處理一個軟中斷,如 softirq、tasklet 和 bh。
【3】 運行於內核態,但有進程上下文,即與一個進程相關。
【4】 運行一個用戶態進程。
其中,【1】、【2】和【3】是運行於內核空間的,而【4】是在用戶空間。其中除了【4】,其他狀態只可以被在其之上的狀態搶佔。比如,軟中斷只可以被硬中斷搶佔。
Linux 內核模塊是一段可以動態在內核裝載和卸載的代碼,裝載進內核的代碼便立即在內核中工作起來。Linux 內核代碼的運行環境有三種:用戶上下文環境、硬中斷環境和軟中斷環境。但三種環境的侷限性分兩種,因爲軟中斷環境只是硬中斷環境的延續。比較如表【1】。
內核態環境 | 介紹 | 侷限性 |
用戶上下文 | 內核態代碼的運行與一用戶空間進程相關,如系統調用中代碼的運行環境。 | 不可直接將本地變量傳遞給用戶態的內存區,因爲內核態和用戶態的內存映射機制不同。 |
硬中斷和軟中斷環境 | 硬中斷或軟中斷過程中代碼的運行環境,如 IP 數據報的接收代碼的運行環境,網絡設備的驅動程序等。 | 不可直接向用戶態內存區傳遞數據; 代碼在運行過程中不可阻塞。 |
Linux 傳統的進程間通信有很多,如各類管道、消息隊列、內存共享、信號量等等。但它們都無法介於內核態與用戶態使用,原因如表【2】。
通信方法 | 無法介於內核態與用戶態的原因 |
管道(不包括命名管道) | 侷限於父子進程間的通信。 |
消息隊列 | 在硬、軟中斷中無法無阻塞地接收數據。 |
信號量 | 無法介於內核態和用戶態使用。 |
內存共享 | 需要信號量輔助,而信號量又無法使用。 |
套接字 | 在硬、軟中斷中無法無阻塞地接收數據。 |
|
運 行在用戶上下文環境中的代碼是可以阻塞的,這樣,便可以使用消息隊列和 UNIX 域套接字來實現內核態與用戶態的通信。但這些方法的數據傳輸效率較低,Linux 內核提供 copy_from_user()/copy_to_user() 函數來實現內核態與用戶態數據的拷貝,但這兩個函數會引發阻塞,所以不能用在硬、軟中斷中。一般將這兩個特殊拷貝函數用在類似於系統調用一類的函數中,此 類函數在使用中往往"穿梭"於內核態與用戶態。此類方法的工作原理路如圖【1】。
圖【1】
其中相關的系統調用是需要用戶自行編寫並載入內核。 imp1.tar.gz是 一個示例,內核模塊註冊了一組設置套接字選項的函數使得用戶空間進程可以調用此組函數對內核態數據進行讀寫。源碼包含三個文件,imp1.h 是通用頭文件,定義了用戶態和內核態都要用到的宏。imp1_k.c 是內核模塊的源代碼。imp1_u.c 是用戶態進程的源代碼。整個示例演示了由一個用戶態進程向用戶上下文環境發送一個字符串,內容爲"a message from userspace/n"。然後再由用戶上下文環境向用戶態進程發送一個字符串,內容爲"a message from kernel/n"。
比起用戶上下文環境,硬中斷和軟中斷環境與用戶態進程無絲毫關係,而且運行過程不能阻塞。
3.2.1 使用一般進程間通信的方法
我 們無法直接使用傳統的進程間通信的方法實現。但硬、軟中斷中也有一套同步機制--自旋鎖(spinlock),可以通過自旋鎖來實現中斷環境與中斷環境, 中斷環境與內核線程的同步,而內核線程是運行在有進程上下文環境中的,這樣便可以在內核線程中使用套接字或消息隊列來取得用戶空間的數據,然後再將數據通 過臨界區傳遞給中斷過程。基本思路如圖【2】。
圖【2】
因 爲中斷過程不可能無休止地等待用戶態進程發送數據,所以要通過一個內核線程來接收用戶空間的數據,再通過臨界區傳給中斷過程。中斷過程向用戶空間的數據發 送必須是無阻塞的。這樣的通信模型並不令人滿意,因爲內核線程是和其他用戶態進程競爭CPU接收數據的,效率很低,這樣中斷過程便不能實時地接收來自用戶 空間的數據。
3.2.2 netlink 套接字
在 Linux 2.4 版以後版本的內核中,幾乎全部的中斷過程與用戶態進程的通信都是使用 netlink 套接字實現的,同時還使用 netlink 實現了 ip queue 工具,但 ip queue 的使用有其侷限性,不能自由地用於各種中斷過程。內核的幫助文檔和其他一些 Linux 相關文章都沒有對 netlink 套接字在中斷過程和用戶空間通信的應用上作詳細的說明,使得很多用戶對此只有一個模糊的概念。
netlink 套接字的通信依據是一個對應於進程的標識,一般定爲該進程的 ID。當通信的一端處於中斷過程時,該標識爲 0。當使用 netlink 套接字進行通信,通信的雙方都是用戶態進程,則使用方法類似於消息隊列。但通信雙方有一端是中斷過程,使用方法則不同。netlink 套接字的最大特點是對中斷過程的支持,它在內核空間接收用戶空間數據時不再需要用戶自行啓動一個內核線程,而是通過另一個軟中斷調用用戶事先指定的接收函 數。工作原理如圖【3】。
圖【3】
很明顯,這裏使用了軟中斷而不是內核線程來接收數據,這樣就可以保證數據接收的實時性。
當 netlink 套接字用於內核空間與用戶空間的通信時,在用戶空間的創建方法和一般套接字使用類似,但內核空間的創建方法則不同。圖【4】是 netlink 套接字實現此類通信時創建的過程。
圖【4】
以下舉一個 netlink 套接字的應用示例。示例實現了從 netfilter 的 NF_IP_PRE_ROUTING 點截獲的 ICMP 數據報,在將數據報的相關信息傳遞到一個用戶態進程,由用戶態進程將信息打印在終端上。源碼在文件 imp2.tar.gz中。 內核模塊代碼(分段詳解):
(一)模塊初始化與卸載
static struct sock *nlfd; |
其實片斷(一)的工作很簡單,模塊加載階段先在內核空間創建一個 netlink 套接字,再將一個函數掛接在 netfilter 框架的 NF_IP_PRE_ROUTING 鉤子點上。卸載時釋放套接字所佔的資源並註銷之前在 netfilter 上掛接的函數。
(二)接收用戶空間的數據
DECLARE_MUTEX(receive_sem); |
如果讀者看過 ip_queue.c 或 rtnetlink.c中的源碼會發現片斷(二)中的 03~18 和 31~38 是 netlink socket 在內核空間接收數據的框架。在框架中主要是從套接字緩存中取出全部的數據,然後分析是不是合法的數據報,合法的 netlink 數據報必須有nlmsghdr 結構的報頭。在這裏筆者使用了自己定義的消息類型:IMP2_U_PID(消息爲用戶空間進程的ID),IMP2_CLOSE(用戶空間進程關閉)。因爲 考慮到 SMP,所以在這裏使用了讀寫鎖來避免不同 CPU 訪問臨界區的問題。kernel_receive() 函數的運行在軟中斷環境。
(三)截獲 IP 數據報
static unsigned int get_icmp(unsigned int hook, |
(四)發送數據
static int send_to_user(struct packet_info *info) |
片斷(四)中所使用的宏參考如下:
/*字節對齊*/ |
運行示例時,先編譯 imp2_k.c 模塊,然後使用 insmod 將模塊加載入內核。再運行編譯好的 imp2_u 命令,此時就會顯示出本機當前接收的 ICMP 數據報的源地址和目的地址。用戶可以使用 Ctrl+C 來終止用戶空間的進程,再次啓動也不會帶來問題。
|
本文從內核態代碼的不同運行環境來實現不同方法的內核空間與用戶空間的通信,並分析了它們的實際效果。最後推薦使用 netlink 套接字實現中斷環境與用戶態進程通信,因爲 netlink 套接字是專爲此類通信定製的。