多數(shù)的 Linux 內(nèi)核態(tài)程序都需要和用戶空間的進(jìn)程交換數(shù)據(jù),但 Linux 內(nèi)核態(tài)無法對傳統(tǒng)的 Linux 進(jìn)程間同步和通信的方法提供足夠的支持。本文總結(jié)并比較了幾種內(nèi)核態(tài)與用戶態(tài)進(jìn)程通信的實(shí)現(xiàn)方法,并推薦使用 netlink 套接字實(shí)現(xiàn)中斷環(huán)境與用戶態(tài)進(jìn)程通信。
1 引言
Linux 是一個(gè)源碼開放的操作系統(tǒng),無論是普通用戶還是企業(yè)用戶都可以編寫自己的內(nèi)核代碼,再加上對標(biāo)準(zhǔn)內(nèi)核的裁剪從而制作出適合自己的操作系統(tǒng)。目前有很多中低端用戶使用的網(wǎng)絡(luò)設(shè)備的操作系統(tǒng)是從標(biāo)準(zhǔn) Linux 改進(jìn)而來的,這也說明了有越來越多的人正在加入到 Linux 內(nèi)核開發(fā)團(tuán)體中。
一個(gè)或多個(gè)內(nèi)核模塊的實(shí)現(xiàn)并不能滿足一般 Linux 系統(tǒng)軟件的需要,因?yàn)閮?nèi)核的局限性太大,如不能在終端上打印,不能做大延時(shí)的處理等等。當(dāng)我們需要做這些的時(shí)候,就需要將在內(nèi)核態(tài)采集到的數(shù)據(jù)傳送到用戶態(tài)的一個(gè)或多個(gè)進(jìn)程中進(jìn)行處理。這樣,內(nèi)核態(tài)與用戶空間進(jìn)程通信的方法就顯得尤為重要。在 Linux 的內(nèi)核發(fā)行版本中沒有對該類通信方法的詳細(xì)介紹,也沒有其他文章對此進(jìn)行總結(jié),所以本文將列舉幾種內(nèi)核態(tài)與用戶態(tài)進(jìn)程通信的方法并詳細(xì)分析它們的實(shí)現(xiàn)和適用環(huán)境。
2 Linux 內(nèi)核模塊的運(yùn)行環(huán)境與傳統(tǒng)進(jìn)程間通信
在一臺運(yùn)行 Linux 的計(jì)算機(jī)中,CPU 在任何時(shí)候只會有如下四種狀態(tài):
【1】 在處理一個(gè)硬中斷。
【2】 在處理一個(gè)軟中斷,如 softirq、tasklet 和 bh。
【3】 運(yùn)行于內(nèi)核態(tài),但有進(jìn)程上下文,即與一個(gè)進(jìn)程相關(guān)。
【4】 運(yùn)行一個(gè)用戶態(tài)進(jìn)程。
其中,【1】、【2】和【3】是運(yùn)行于內(nèi)核空間的,而【4】是在用戶空間。其中除了【4】,其他狀態(tài)只可以被在其之上的狀態(tài)搶占。比如,軟中斷只可以被硬中斷搶占。
Linux 內(nèi)核模塊是一段可以動(dòng)態(tài)在內(nèi)核裝載和卸載的代碼,裝載進(jìn)內(nèi)核的代碼便立即在內(nèi)核中工作起來。Linux 內(nèi)核代碼的運(yùn)行環(huán)境有三種:用戶上下文環(huán)境、硬中斷環(huán)境和軟中斷環(huán)境。但三種環(huán)境的局限性分兩種,因?yàn)檐浿袛喹h(huán)境只是硬中斷環(huán)境的延續(xù)。比較如表【1】。
表【1】
?
內(nèi)核態(tài)環(huán)境 介紹 局限性 用戶上下文 內(nèi)核態(tài)代碼的運(yùn)行與一用戶空間進(jìn)程相關(guān),如系統(tǒng)調(diào)用中代碼的運(yùn)行環(huán)境。 不可直接將本地變量傳遞給用戶態(tài)的內(nèi)存區(qū),因?yàn)閮?nèi)核態(tài)和用戶態(tài)的內(nèi)存映射機(jī)制不同。 硬中斷和軟中斷環(huán)境 硬中斷或軟中斷過程中代碼的運(yùn)行環(huán)境,如 IP 數(shù)據(jù)報(bào)的接收代碼的運(yùn)行環(huán)境,網(wǎng)絡(luò)設(shè)備的驅(qū)動(dòng)程序等。 不可直接向用戶態(tài)內(nèi)存區(qū)傳遞數(shù)據(jù);代碼在運(yùn)行過程中不可阻塞。
?
Linux 傳統(tǒng)的進(jìn)程間通信有很多,如各類管道、消息隊(duì)列、內(nèi)存共享、信號量等等。但它們都無法介于內(nèi)核態(tài)與用戶態(tài)使用,原因如表【2】。
表【2】
?
通信方法 無法介于內(nèi)核態(tài)與用戶態(tài)的原因 管道(不包括命名管道) 局限于父子進(jìn)程間的通信。 消息隊(duì)列 在硬、軟中斷中無法無阻塞地接收數(shù)據(jù)。 信號量 無法介于內(nèi)核態(tài)和用戶態(tài)使用。 內(nèi)存共享 需要信號量輔助,而信號量又無法使用。 套接字 在硬、軟中斷中無法無阻塞地接收數(shù)據(jù)。?
?
3 Linux內(nèi)核態(tài)與用戶態(tài)進(jìn)程通信方法的提出與實(shí)現(xiàn)
3.1 用戶上下文環(huán)境
運(yùn)行在用戶上下文環(huán)境中的代碼是可以阻塞的,這樣,便可以使用消息隊(duì)列和 UNIX 域套接字來實(shí)現(xiàn)內(nèi)核態(tài)與用戶態(tài)的通信。但這些方法的數(shù)據(jù)傳輸效率較低,Linux 內(nèi)核提供 copy_from_user()/copy_to_user() 函數(shù)來實(shí)現(xiàn)內(nèi)核態(tài)與用戶態(tài)數(shù)據(jù)的拷貝,但這兩個(gè)函數(shù)會引發(fā)阻塞,所以不能用在硬、軟中斷中。一般將這兩個(gè)特殊拷貝函數(shù)用在類似于系統(tǒng)調(diào)用一類的函數(shù)中,此類函數(shù)在使用中往往"穿梭"于內(nèi)核態(tài)與用戶態(tài)。此類方法的工作原理路如圖【1】。
圖【1】
其中相關(guān)的系統(tǒng)調(diào)用是需要用戶自行編寫并載入內(nèi)核。 imp1.tar.gz是一個(gè)示例,內(nèi)核模塊注冊了一組設(shè)置套接字選項(xiàng)的函數(shù)使得用戶空間進(jìn)程可以調(diào)用此組函數(shù)對內(nèi)核態(tài)數(shù)據(jù)進(jìn)行讀寫。源碼包含三個(gè)文件,imp1.h 是通用頭文件,定義了用戶態(tài)和內(nèi)核態(tài)都要用到的宏。imp1_k.c 是內(nèi)核模塊的源代碼。imp1_u.c 是用戶態(tài)進(jìn)程的源代碼。整個(gè)示例演示了由一個(gè)用戶態(tài)進(jìn)程向用戶上下文環(huán)境發(fā)送一個(gè)字符串,內(nèi)容為"a message from userspace\n"。然后再由用戶上下文環(huán)境向用戶態(tài)進(jìn)程發(fā)送一個(gè)字符串,內(nèi)容為"a message from kernel\n"。
3.2 硬、軟中斷環(huán)境
比起用戶上下文環(huán)境,硬中斷和軟中斷環(huán)境與用戶態(tài)進(jìn)程無絲毫關(guān)系,而且運(yùn)行過程不能阻塞。
3.2.1 使用一般進(jìn)程間通信的方法
我們無法直接使用傳統(tǒng)的進(jìn)程間通信的方法實(shí)現(xiàn)。但硬、軟中斷中也有一套同步機(jī)制--自旋鎖(spinlock),可以通過自旋鎖來實(shí)現(xiàn)中斷環(huán)境與中斷環(huán)境,中斷環(huán)境與內(nèi)核線程的同步,而內(nèi)核線程是運(yùn)行在有進(jìn)程上下文環(huán)境中的,這樣便可以在內(nèi)核線程中使用套接字或消息隊(duì)列來取得用戶空間的數(shù)據(jù),然后再將數(shù)據(jù)通過臨界區(qū)傳遞給中斷過程。基本思路如圖【2】。
圖【2】
因?yàn)橹袛噙^程不可能無休止地等待用戶態(tài)進(jìn)程發(fā)送數(shù)據(jù),所以要通過一個(gè)內(nèi)核線程來接收用戶空間的數(shù)據(jù),再通過臨界區(qū)傳給中斷過程。中斷過程向用戶空間的數(shù)據(jù)發(fā)送必須是無阻塞的。這樣的通信模型并不令人滿意,因?yàn)閮?nèi)核線程是和其他用戶態(tài)進(jìn)程競爭CPU接收數(shù)據(jù)的,效率很低,這樣中斷過程便不能實(shí)時(shí)地接收來自用戶空間的數(shù)據(jù)。
3.2.2 netlink 套接字
在 Linux 2.4 版以后版本的內(nèi)核中,幾乎全部的中斷過程與用戶態(tài)進(jìn)程的通信都是使用 netlink 套接字實(shí)現(xiàn)的,同時(shí)還使用 netlink 實(shí)現(xiàn)了 ip queue 工具,但 ip queue 的使用有其局限性,不能自由地用于各種中斷過程。內(nèi)核的幫助文檔和其他一些 Linux 相關(guān)文章都沒有對 netlink 套接字在中斷過程和用戶空間通信的應(yīng)用上作詳細(xì)的說明,使得很多用戶對此只有一個(gè)模糊的概念。
netlink 套接字的通信依據(jù)是一個(gè)對應(yīng)于進(jìn)程的標(biāo)識,一般定為該進(jìn)程的 ID。當(dāng)通信的一端處于中斷過程時(shí),該標(biāo)識為 0。當(dāng)使用 netlink 套接字進(jìn)行通信,通信的雙方都是用戶態(tài)進(jìn)程,則使用方法類似于消息隊(duì)列。但通信雙方有一端是中斷過程,使用方法則不同。netlink 套接字的最大特點(diǎn)是對中斷過程的支持,它在內(nèi)核空間接收用戶空間數(shù)據(jù)時(shí)不再需要用戶自行啟動(dòng)一個(gè)內(nèi)核線程,而是通過另一個(gè)軟中斷調(diào)用用戶事先指定的接收函數(shù)。工作原理如圖【3】。
圖【3】
很明顯,這里使用了軟中斷而不是內(nèi)核線程來接收數(shù)據(jù),這樣就可以保證數(shù)據(jù)接收的實(shí)時(shí)性。
當(dāng) netlink 套接字用于內(nèi)核空間與用戶空間的通信時(shí),在用戶空間的創(chuàng)建方法和一般套接字使用類似,但內(nèi)核空間的創(chuàng)建方法則不同。圖【4】是 netlink 套接字實(shí)現(xiàn)此類通信時(shí)創(chuàng)建的過程。
圖【4】
以下舉一個(gè) netlink 套接字的應(yīng)用示例。示例實(shí)現(xiàn)了從 netfilter 的 NF_IP_PRE_ROUTING 點(diǎn)截獲的 ICMP 數(shù)據(jù)報(bào),在將數(shù)據(jù)報(bào)的相關(guān)信息傳遞到一個(gè)用戶態(tài)進(jìn)程,由用戶態(tài)進(jìn)程將信息打印在終端上。源碼在文件 imp2.tar.gz中。內(nèi)核模塊代碼(分段詳解):
(一)模塊初始化與卸載
static struct sock *nlfd;struct{ __u32 pid; rwlock_t lock;}user_proc;/*掛接在 netfilter 框架的 NF_IP_PRE_ROUTING 點(diǎn)上的函數(shù)為 get_icmp()*/static struct nf_hook_ops imp2_ops ={ .hook = get_icmp,/*netfilter 鉤子函數(shù)*/ .pf = PF_INET, .hooknum = NF_IP_PRE_ROUTING, .priority = NF_IP_PRI_FILTER -1,};static int __init init(void){ rwlock_init(&user_proc.lock); /*在內(nèi)核創(chuàng)建一個(gè) netlink socket,并注明由 kernel_recieve() 函數(shù)接收數(shù)據(jù) 這里協(xié)議 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 點(diǎn)掛接函數(shù)*/ 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);
?
其實(shí)片斷(一)的工作很簡單,模塊加載階段先在內(nèi)核空間創(chuàng)建一個(gè) netlink 套接字,再將一個(gè)函數(shù)掛接在 netfilter 框架的 NF_IP_PRE_ROUTING 鉤子點(diǎn)上。卸載時(shí)釋放套接字所占的資源并注銷之前在 netfilter 上掛接的函數(shù)。
(二)接收用戶空間的數(shù)據(jù)
DECLARE_MUTEX(receive_sem);01:static void kernel_receive(struct sock *sk, int len)02:{03: do04: {05: struct sk_buff *skb;06: if(down_trylock(&receive_sem))07: return;08:09: while((skb = skb_dequeue(&sk- ? 如果讀者看過 ip_queue.c 或 rtnetlink.c中的源碼會發(fā)現(xiàn)片斷(二)中的 03~18 和 31~38 是 netlink socket 在內(nèi)核空間接收數(shù)據(jù)的框架。在框架中主要是從套接字緩存中取出全部的數(shù)據(jù),然后分析是不是合法的數(shù)據(jù)報(bào),合法的 netlink 數(shù)據(jù)報(bào)必須有nlmsghdr 結(jié)構(gòu)的報(bào)頭。在這里筆者使用了自己定義的消息類型:IMP2_U_PID(消息為用戶空間進(jìn)程的ID),IMP2_CLOSE(用戶空間進(jìn)程關(guān)閉)。因?yàn)榭紤]到 SMP,所以在這里使用了讀寫鎖來避免不同 CPU 訪問臨界區(qū)的問題。kernel_receive() 函數(shù)的運(yùn)行在軟中斷環(huán)境。 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)/*若傳輸層協(xié)議為 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);/*發(fā)送數(shù)據(jù)*/ } 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)); /*開辟一個(gè)新的套接字緩存*/ skb = alloc_skb(size, GFP_ATOMIC); old_tail = skb->tail; /*填寫數(shù)據(jù)報(bào)相關(guān)信息*/ nlh = NLMSG_PUT(skb, 0, 0, IMP2_K_MSG, size-sizeof(*nlh)); packet = NLMSG_DATA(nlh); memset(packet, 0, sizeof(struct packet_info)); /*傳輸?shù)接脩艨臻g的數(shù)據(jù)*/ packet->src = info->src; packet->dest = info->dest; /*計(jì)算經(jīng)過字節(jié)對其后的數(shù)據(jù)實(shí)際長度*/ 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); /*發(fā)送數(shù)據(jù)*/ read_unlock_bh(&user_proc.lock); return ret; nlmsg_failure: /*若發(fā)送失敗,則撤銷套接字緩存*/ if(skb) kfree_skb(skb); return -1;} /*字節(jié)對齊*/#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )/*計(jì)算包含報(bào)頭的數(shù)據(jù)報(bào)長度*/#define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))/*字節(jié)對齊后的數(shù)據(jù)報(bào)長度*/#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))/*填寫相關(guān)報(bào)頭信息,這里使用了nlmsg_failure標(biāo)簽,所以在程序中要定義*/#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;}/*跳過報(bào)頭取實(shí)際數(shù)據(jù)*/#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))/*取 netlink 控制字段*/#define NETLINK_CB(skb)(*(struct netlink_skb_parms*)&((skb)->cb)) ? 4 總結(jié) 本文從內(nèi)核態(tài)代碼的不同運(yùn)行環(huán)境來實(shí)現(xiàn)不同方法的內(nèi)核空間與用戶空間的通信,并分析了它們的實(shí)際效果。最后推薦使用 netlink 套接字實(shí)現(xiàn)中斷環(huán)境與用戶態(tài)進(jìn)程通信,因?yàn)?netlink 套接字是專為此類通信定制的。
(三)截獲 IP 數(shù)據(jù)報(bào)
(四)發(fā)送數(shù)據(jù)
片斷(四)中所使用的宏參考如下:
運(yùn)行示例時(shí),先編譯 imp2_k.c 模塊,然后使用 insmod 將模塊加載入內(nèi)核。再運(yùn)行編譯好的 imp2_u 命令,此時(shí)就會顯示出本機(jī)當(dāng)前接收的 ICMP 數(shù)據(jù)報(bào)的源地址和目的地址。用戶可以使用 Ctrl+C 來終止用戶空間的進(jìn)程,再次啟動(dòng)也不會帶來問題。
評論