多數的 Linux 內核態程序都需要和用戶空間的進程交換數據,但 Linux 內核態無法對傳統的 Linux 進程間同步和通信的方法提供足夠的支持。本文總結並比較了幾種內核態與用戶態進程通信的實現方法,並推薦使用 netlink 套接字實現中斷環境與用戶態進程通信。
1 引言
Linux 是一個源碼開放的操作系統,無論是普通用戶還是企業用戶都可以編寫自己的內核代碼,再加上對標准內核的裁剪從而制作出適合自己的操作系統。目前有很多中低端用戶使用的網絡設備的操作系統是從標准 Linux 改進而來的,這也說明了有越來越多的人正在加入到 Linux 內核開發團體中。
一個或多個內核模塊的實現並不能滿足一般 Linux 系統軟件的需要,因為內核的局限性太大,如不能在終端上打印,不能做大延時的處理等等。當我們需要做這些的時候,就需要將在內核態采集到的數據傳送到用戶態的一個或多個進程中進行處理。這樣,內核態與用戶空間進程通信的方法就顯得尤為重要。在 Linux 的內核發行版本中沒有對該類通信方法的詳細介紹,也沒有其他文章對此進行總結,所以本文將列舉幾種內核態與用戶態進程通信的方法並詳細分析它們的實現和適用環境。
回頁首
2 Linux 內核模塊的運行環境與傳統進程間通信
在一台運行 Linux 的計算機中,CPU 在任何時候只會有如下四種狀態:
【1】 在處理一個硬中斷。
【2】 在處理一個軟中斷,如 softirq、tasklet 和 bh。
【3】 運行於內核態,但有進程上下文,即與一個進程相關。
【4】 運行一個用戶態進程。
其中,【1】、【2】和【3】是運行於內核空間的,而【4】是在用戶空間。其中除了【4】,其他狀態只可以被在其之上的狀態搶占。比如,軟中斷只可以被硬中斷搶占。
Linux 內核模塊是一段可以動態在內核裝載和卸載的代碼,裝載進內核的代碼便立即在內核中工作起來。Linux 內核代碼的運行環境有三種:用戶上下文環境、硬中斷環境和軟中斷環境。但三種環境的局限性分兩種,因為軟中斷環境只是硬中斷環境的延續。比較如表【1】。
表【1】 | 內核態環境 | 介紹 | 局限性 | 用戶上下文內核態代碼的運行與一用戶空間進程相關,如系統調用中代碼的運行環境。不可直接將本地變量傳遞給用戶態的內存區,因為內核態和用戶態的內存映射機制不同。硬中斷和軟中斷環境硬中斷或軟中斷過程中代碼的運行環境,如 IP 數據報的接收代碼的運行環境,網絡設備的驅動程序等。不可直接向用戶態內存區傳遞數據;
代碼在運行過程中不可阻塞。Linux 傳統的進程間通信有很多,如各類管道、消息隊列、內存共享、信號量等等。但它們都無法介於內核態與用戶態使用,原因如表【2】。
表【2】 | 通信方法 | 無法介於內核態與用戶態的原因 | 管道(不包括命名管道)局限於父子進程間的通信。消息隊列在硬、軟中斷中無法無阻塞地接收數據。信號量無法介於內核態和用戶態使用。內存共享需要信號量輔助,而信號量又無法使用。套接字在硬、軟中斷中無法無阻塞地接收數據。回頁首
3 Linux內核態與用戶態進程通信方法的提出與實現
3.1 用戶上下文環境
運行在用戶上下文環境中的代碼是可以阻塞的,這樣,便可以使用消息隊列和 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 硬、軟中斷環境
比起用戶上下文環境,硬中斷和軟中斷環境與用戶態進程無絲毫關系,而且運行過程不能阻塞。
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;
struct
{
__u32 pid;
rwlock_t lock;
}user_proc;
/*掛接在 netfilter 框架的 NF_IP_PRE_ROUTING 點上的函數為 get_icmp()*/
static struct nf_hook_ops imp2_ops =
{
.hook = get_icmp, /*netfilter 鉤子函數*/
.pf = PF_INET,
.hooknum = NF_IP_PRE_ROUTING,
.priority = NF_IP_PRI_FILTER -1,
};
static int __init init(void)
{
rwlock_init(&user_proc.lock);
/*在內核創建一個 netlink socket,並注明由 kernel_recieve() 函數接收數據
這裡協議 NL_IMP2 是自定的*/
nlfd = netlink_kernel_create(NL_IMP2, kernel_receive);
if(!nlfd)
{
printk("can not create a netlink socket\n");
return -1;
}
/*向 netfilter 的 NF_IP_PRE_ROUTING 點掛接函數*/
return nf_register_hook(&imp2_ops);
}
static void __exit fini(void)
{
if(nlfd)
{
sock_release(nlfd->socket);
}
nf_unregister_hook(&imp2_ops);
}
module_init(init);
module_exit(fini);
其實片斷(一)的工作很簡單,模塊加載階段先在內核空間創建一個 netlink 套接字,再將一個函數掛接在 netfilter 框架的 NF_IP_PRE_ROUTING 鉤子點上。卸載時釋放套接字所占的資源並注銷之前在 netfilter 上掛接的函數。
(二)接收用戶空間的數據DECLARE_MUTEX(receive_sem);
01: static void kernel_receive(struct sock *sk, int len)
02: {
03: do
04: {
05: struct sk_buff *skb;
06: if(down_trylock(&receive_sem))
07: return;
08:
09: while((skb = skb_dequeue(&sk-<receive_queue)) != NULL)
10: {
11: {
12: struct nlmsghdr *nlh = NULL;
13: if(skb-<len <= sizeof(struct nlmsghdr))
14: {
15: nlh = (struct nlmsghdr *)skb-<data;
16: if((nlh-<nlmsg_len <= sizeof(struct nlmsghdr))
17: && (skb-<len <= nlh-<nlmsg_len))
18: {
19: if(nlh-<nlmsg_type == IMP2_U_PID)
20: {
21: write_lock_bh(&user_proc.pid);
22: user_proc.pid = nlh-<nlmsg_pid;
23: write_unlock_bh(&user_proc.pid);
24: }
25: else if(nlh-<nlmsg_type == IMP2_CLOSE)
26: {
27: write_lock_bh(&user_proc.pid);
28: if(nlh-<nlmsg_pid == user_proc.pid) user_proc.pid = 0;
29: write_unlock_bh(&user_proc.pid);
30: }
31: }
32: }
33: }
34: kfree_skb(skb);
35: }
36: up(&receive_sem);
37: }while(nlfd && nlfd-<receive_queue.qlen);
38: }
如果讀者看過 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,
struct sk_buff **pskb,
const struct net_device *in,
const struct net_device *out,
int (*okfn)(struct sk_buff *))
{
struct iphdr *iph = (*pskb)->nh.iph;
struct packet_info info;
if(iph->protocol == IPPROTO_ICMP) /*若傳輸層協議為 ICMP*/
{
read_lock_bh(&user_proc.lock);
if(user_proc.pid != 0)
{
read_unlock_bh(&user_proc.lock);
info.src = iph->saddr; /*記錄源地址*/
info.dest = iph->daddr; /*記錄目的地址*/
send_to_user(&info); /*發送數據*/
}
else
read_unlock_bh(&user_proc.lock);
}
return NF_ACCEPT;
}
(四)發送數據static int send_to_user(struct packet_info *info)
{
int ret;
int size;
unsigned char *old_tail;
struct sk_buff *skb;
struct nlmsghdr *nlh;
struct packet_info *packet;
size = NLMSG_SPACE(sizeof(*info));
/*開辟一個新的套接字緩存*/
skb = alloc_skb(size, GFP_ATOMIC);
old_tail = skb->tail;
/*填寫數據報相關信息*/
nlh = NLMSG_PUT(skb, 0, 0, IMP2_K_MSG, size-sizeof(*nlh));
packet = NLMSG_DATA(nlh);
memset(packet, 0, sizeof(struct packet_info));
/*傳輸到用戶空間的數據*/
packet->src = info->src;
packet->dest = info->dest;
/*計算經過字節對其後的數據實際長度*/
nlh->nlmsg_len = skb->tail - old_tail;
NETLINK_CB(skb).dst_groups = 0;
read_lock_bh(&user_proc.lock);
ret = netlink_unicast(nlfd, skb, user_proc.pid, MSG_DONTWAIT); /*發送數據*/
read_unlock_bh(&user_proc.lock);
return ret;
nlmsg_failure: /*若發送失敗,則撤銷套接字緩存*/
if(skb)
kfree_skb(skb);
return -1;
}
片斷(四)中所使用的宏參考如下:/*字節對齊*/
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
/*計算包含報頭的數據報長度*/
#define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))
/*字節對齊後的數據報長度*/
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
/*填寫相關報頭信息,這裡使用了nlmsg_failure標簽,所以在程序中要定義*/
#define NLMSG_PUT(skb, pid, seq, type, len) \
({ if (skb_tailroom(skb) < (int)NLMSG_SPACE(len)) goto nlmsg_failure; \
__nlmsg_put(skb, pid, seq, type, len); })
static __inline__ struct nlmsghdr *
__nlmsg_put(struct sk_buff *skb, u32 pid, u32 seq, int type, int len)
{
struct nlmsghdr *nlh;
int size = NLMSG_LENGTH(len);
nlh = (struct nlmsghdr*)skb_put(skb, NLMSG_ALIGN(size));
nlh->nlmsg_type = type;
nlh->nlmsg_len = size;
nlh->nlmsg_flags = 0;
nlh->nlmsg_pid = pid;
nlh->nlmsg_seq = seq;
return nlh;
}
/*跳過報頭取實際數據*/
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
/*取 netlink 控制字段*/
#define NETLINK_CB(skb) (*(struct netlink_skb_parms*)&((skb)->cb))
運行示例時,先編譯 imp2_k.c 模塊,然後使用 insmod 將模塊加載入內核。再運行編譯好的 imp2_u 命令,此時就會顯示出本機當前接收的 ICMP 數據報的源地址和目的地址。用戶可以使用 Ctrl+C 來終止用戶空間的進程,再次啟動也不會帶來問題。
回頁首
4 總結
本文從內核態代碼的不同運行環境來實現不同方法的內核空間與用戶空間的通信,並分析了它們的實際效果。最後推薦使用 netlink 套接字實現中斷環境與用戶態進程通信,因為 netlink 套接字是專為此類通信定制的。