熊偉++丁涵++羅云鋒
摘要:Netlink是Linux操作系統內核空間與用戶空間最流行的進程間通信機制之一,但目前在多線程程序中的使用還存在一些問題。介紹了Netlink相對于Linux其它傳統通信手段的優點,闡述了使用Netlink進行用戶程序與內核模塊通信的實現方法,分析了目前公開資料上Netlink線程并發支持機制存在的問題,并給出了支持多線程并發與消息異步處理的正確方法,最后在真實機器上進行了驗證。結果顯示,該方法能有效支持在多線程Linux應用中使用Netlink進行用戶態與內核態通信。
關鍵詞:Linux;nelink;進程間通信;多線程并發;異步處理
DOIDOI:10.11907/rjdk.172576
中圖分類號:TP319文獻標識碼:A文章編號:16727800(2017)010009905
0引言
Linux是當今應用最廣泛的操作系統之一,其兼容性好,能適應從嵌入式設備、個人用戶終端到高性能服務器的不同硬件平臺,具有多任務與多用戶能力。Linux符合POSIX標準,在GNU公共許可權限下可免費獲得其內核源代碼,同時還具備完整的軟件生態鏈,包含各種開發工具及第三方軟件庫,非常方便用戶開發定制自己的應用。
Linux采用模塊化單內核架構,支持內核模塊動態加載與卸載,其系統地址空間結構如圖1所示。Linux所有
圖1Linux操作系統結構
內核代碼可看作一個整體,運行在一個獨立的地址空間中,通常被稱為內核空間[1]。運行于內核空間的代碼不受任何限制,能夠自由地訪問任何有效地址以及直接進行設備訪問。而用戶應用運行在內核之上,其不能隨意占用系統資源與修改系統配置,從而確保系統安全性與穩定性。
在日常應用中,應用程序通常包括上層用戶界面程序與底層內核驅動模塊兩部分,用戶界面負責接收用戶輸入及顯示最終處理結果,內核驅動則負責調用內核處理用戶請求。因此內核空間與用戶空間進行通信的方法非常重要。
目前,Linux常用的內核用戶通信機制有以下幾種[2]:
(1)設備驅動接口。設備節點位于/dev目錄下。設備驅動接口允許用戶訪問設備節點[3],利用copy_from_user()與copy_to_user()函數,在用戶態與內核態間拷貝數據。但是這兩個函數只支持阻塞式調用,不能在中斷中使用,而且只支持用戶程序主動進行通信,通常用在硬件驅動中。
(2)Proc與sysfs文件系統。Proc與sysfs是虛擬文件系統,用于顯示進程、處理器、內存、中斷等信息[4,5]。用戶可以通過讀寫這兩種文件系統與內核進行通信。其最大缺點是不支持基于事件的信號機制,數據傳輸大小也不能超過一個內存頁,可擴展性較差。
(3)內存映射。/dev/mem是Linux系統中一種特殊的字符設備文件[6],應用程序通過這個節點,可以在內核地址空間與用戶地址空間進行映射,然后訪問映射后的內存區域,實現用戶空間與內核空間的通信。但是對內核地址的誤操作將引起嚴重后果,導致系統崩潰。
(4)Netlink套接字。Netlink是一種面向數據報的消息系統,目前在Linux內核中有非常多應用可用于通信,包括路由、IPSEC、防火墻、netfilter日志等[710]。Netlink具有以下特點:消息具有較強的擴展能力,用戶可以自定義消息格式,且提供了基于事件的信號機制,允許大數據傳輸;支持全雙工傳輸,允許內核主動發起傳輸通信;支持單播與組播兩種通信方式[11]。
如上所述,目前在Linux系統內核空間與用戶空間通信方式中:設備節點適合于驅動程序開發,但只支持單工傳輸;Proc與sysfs文件使用方便,但不支持傳輸大數據;內存映射傳輸效率最高,但誤操作時會對系統造成嚴重破壞;Netlink則使用很靈活,能滿足大多數用戶需求。
本文首先介紹Netlink套接字基本使用方法與通信流程,然后詳細闡述使用Netlink如何實現多線程并發與消息異步處理通信,最后在真實硬件平臺上對該機制進行驗證。
1Netlink機制概述
Netlink機制包含用戶態接口與內核態接口,其中用戶態沿用標準的socket接口,內核態則提供了專用接口。
1.1用戶態Netlink接口
Netlink用戶態接口與BSD套接字接口基本一致,包括:socket()、bind()、sendmsg()、recvmsg()、close等常用接口。
1.1.1Netlink套接字創建
int socket(int domain, int type, int protocol)
其中,domain參數為AF_NETLINK或PF_NETLINK,表示使用Netlink協議,type參數是SOCK_RAW或SOCK_DGRAM,代表Netlink面向數據報,最后一個參數指定Netlink協議類型,除了內核中已經定義的類型,用戶還可以定義自己的協議類型。
1.1.2套接字地址綁定
int bind(fd, (struct sockaddr*)&nladdr, sizeof(struct sockaddr_nl))
函數bind()用于Netlink套接字句柄與Netlink源地址綁定。第一個參數為創建Netlink套接字時獲取的描述符,第二個為Netlink源地址結構指針,最后一個參數為Netlink源地址結構大小。
Netlink socket地址定義如下:
struct sockaddr_nl {
_kernel_sa_family_t nl_family;/*AF_NETLINK*/endprint
unsigned short nl_pad; /* zero */
_u32 nl_pid; /* port ID */
_u32 nl_groups; /* multicast groups mask */
};
其中:nl_family代表協議類型,設置為AF_NETLINK或者PF_NETLINK;字段nl_pad保留,默認設置為0;nl_pid代表Netlink socket的本地地址,為確保消息發送準確性,其設置非常關鍵,必須確保唯一性;字段nl_groups用于設置多播組,如果設置為0,表示進行單播。
1.1.3Netlink消息發送
int sendmsg(int sock, struct msghdr *msg, int flags)
Netlink發送消息前需要填充信息,信息由消息頭與數據部分組成,其結構如圖2所示。
圖2Netlink消息頭
消息長度代表Netlink消息的總長度,包括消息頭長度與數據部分長度;應用內部定義消息類型,大部分情況下設置為0;標志用于設置消息標志,內核讀取與修改這類標志,通常不需修改,默認為0,在一些高級應用(如路由daemon)中需要設置它進行特殊操作;字段序列號與消息端口號用于應用追蹤消息來源,分別表示消息發送順序號與來源端口號。
1.1.4Netlink消息接收
int recvmsg(int sock, struct msghdr *msg, int flags)
應用接收消息時,首先需要為消息頭與數據部分分配足夠空間,然后填充消息頭。
1.1.5關閉Netlink套接字
Close函數用于關閉套接字,釋放資源。
1.2內核態Netlink接口
Linux內核包含一套專門的接口函數,接收用戶程序發送的數據以及將處理完數據發送回用戶程序。
1.2.1內核態Netlink套接字創建
struct sock*netlink_kernel_create(struct net*net, int unit, struct netlink_kernel_cfg*cfg)
netlink_kernel_create函數在不同內核版本間變化非常大,在使用過程中,需要查找與內核匹配的函數定義。在3.0以上版本中,該函數包含3個參數:第一個參數指定網絡名字空間,默認為init_net全局變量;第二個參數設置Netlink協議類型,需要與用戶態定義一致;第三個參數用于指定內核態Netlink配置信息。其結構為:
struct netlink_kernel_cfg {
unsigned int groups;
unsigned int flags;
void (*input)(struct sk_buff *skb);
struct mutex *cb_mutex;
int (*bind)(struct net *net, int group);
void (*unbind)(struct net *net, int group);
bool (*compare)(struct net *net, struct sock *sk); }
通常用戶只需設置groups與input字段:Groups用于設置單播還是組播;input用于注冊消息回調處理函數,當內核接收到用戶發來的Netlink信息后會自動調用它。
1.2.2從內核態向用戶態發送數據
Netlink支持單播與組播,因此內核態信息發送函數包括兩個。
單播:
int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock)
第一個參數為內核Netlink套接字句柄;第二個參數存放消息結構,數據字段為發送的Netlink消息;控制塊則包含消息地址信息;第三個參數portid為接收對象的Netlink地址,最后一個用于設置阻塞屬性。
組播:
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, __u32 portid,__u32 group, gfp_t allocation)
前3個參數與單播一樣,第四個參數用于指定接收消息的多播組,最后一個參數則為內核內存分配類型。
1.3Netlink內核態與用戶態通信流程
用戶程序通過Netlink機制與內核進行通信,流程如圖3所示。
圖3Netlink用戶態與內核態交互流程
用戶態Netlink使用流程與常用BSD Socket一樣,首先使用socket函數創建套接字,然后使用bind函數綁定地址,封裝并使用sendmsg向內核發送消息,接著使用recvmsg接收消息,最后通過close函數關閉套接字,釋放資源。內核態處理過程類似,發送消息時還可以根據需要選擇單播或者多播。
2機制設計
Netlink作為socket的一個變種,本身就支持并發與異步處理,但是需要針對線程中Netlink socket本地地址設置與消息接收處理松耦合進行特殊設計。
2.1線程Netlink本地地址生成方法
Netlink并發實現的關鍵點是套接字創建時地址設置的唯一性。Bind函數負責給Netlink套接字命名,將本地地址與其相關聯。上文已經介紹了結構體sockaddr_nl中nl_pid字段用于代表32位本地地址,其在填充時必須保證唯一性才能確保收發消息的準確性。endprint
如果用戶程序實現的是進程級并發,可以采用進程號作為nl_pid的值,進程號在系統中的唯一性確保了Netlink本地地址的唯一性。
但是如果進程中多個線程需要創建各自獨立的Netlink socket,基于線程共享進程號的原因,進程號就不能用于區分線程創建的Netlink套接字地址。目前流傳最廣泛的線程Netlink中nl_pid創建方法為[12]:
pthread_self() 16 | getpid()
其中,nl_pid由線程自身ID后半部與其所屬進程pid拼接而成,期望由pid在系統的唯一性與pthread_self在進程中的唯一性,保證nl_pid在全系統的唯一性。但在實際使用中,生成的值并不唯一。圖4是在ubuntu 14.04中創建10個線程生成的pid、tid(pthread_self返回值)與nl_pid結果。如圖4所示,10個樣本中就出現了重復。
圖4線程地址示例圖
從圖4中可以發現,pthread_self低16位有12位重復,pthread_self()16后只有高4位發生變化,由于pid一般小于65 536,因此進程中創建n個線程,使用pthread_self()16 | getpid()出現重復數據的概率為:
1-15!(16-n)!×16n-1(1 運行10次出現重復的概率約為85%,而創建17個以上線程nl_pid重復概率就已經為1。 分析pthread_self實現,可以發現其實際獲取的是線程TCB地址相對于進程數據段的偏移,所以低地址一致,造成按上述方法生成nl_pid出現重復。 因此,為了保證線程中Netlink套接字正常使用,需要重新設計nl_pid生成公式??疾靝thread_self的實現,它在進程內唯一而且后半部基本一致,因此可以考慮取其前半部與線程pid進行拼接,從而確保生成nl_pid在全系統的唯一性。新生成方法如下: pthread_self()16 | getpid()16 一個支持多線程并發的Netlink用戶態示例用例如下,所有nl_pid設置都使用新方法: struct sockaddr_nl src_addr, dest_addr; struct nlmsghdr* nlh = NULL; struct iovec iov; struct msghdr msg; /**建立用戶空間netlink套接字*/ sock_nl=socket(AF_NETLINK, SOCK_RAW, LinuxV_NL_P_TYPE); /**填充SRC_ADDR并進行端口綁定*/ memset(&src_addr, 0, sizeof(struct sockaddr_nl)); src_addr.nl_family=AF_NETLINK; /**按照新公式定義本地地址*/ src_addr.nl_pid=pthread_self()16 | getpid()16; src_addr.nl_groups = 0; /**綁定netlink套接字,在nl_pid端口監聽*/ bind(sock_nl, (struct sockaddr*)&src_addr, sizeof(struct sockaddr_nl); /**封裝Netlink消息*/ dest_addr.nl_family=AF_NETLINK; dest_addr.nl_pid=0; /**to Linux kernel*/ dest_addr.nl_groups=0; /**填充netlink命令包頭*/ nlh=(struct nlmsghdr*)malloc(NLMSG_LENGTH(MAX_PAYLOAD)); nlh->nlmsg_type=MY_TYPE_0; /**注意保持和初始化時nl_pid一致,用于確定消息來源*/ nlh->nlmsg_pid=pthread_self()16 | getpid()16; /**填充發送命令報內容*/ memcpy(NLMSG_DATA(nlh), buf, buflen); /**構造Netlink消息包*/ iov.iov_base=(void*)nlh; msg.msg_name=(void*)&dest_addr; msg.msg_iov=&iov; /**發送netlink包*/ sendmsg(sock_nl, &msg, 0); /**接收netlink包*/ recvmsg(sock_nl, &msg, 0); close(sock_nl) 2.2內核Netlink消息異步處理機制 Netlink是BSD套接字的一種,繼承了其異步處理特性,用戶程序發送消息并把消息保存到接收者的接收隊列后,不需要一直等待內核處理完消息。 為了提高異步處理效率,在內核態可以將Netlink信息的接收與處理過程松耦合,這樣內核收到用戶發來的消息后只負責喚醒處理內核線程,然后就返回。所有的消息處理工作由處理線程完成,從而可以實現用戶程序持續快速發送Netlink消息到內核,提高吞吐率。特別是如果應用程序發送信息處理流程不同,就可以創建多個內核線程進行并發處理。在內核3.12.11上內核態Netlink創建與消息異步收發實現代碼如下:
struct netlink_kernel_cfg cfg={
.groups=0,
.input=receive_us_msg,
};
nl_sk=netlink_kernel_create(&init_net,LinuxV_NL_P_TYPE, &cfg);
/**創建netlink消息處理內核線程*/
nl_msg_thread=kthread_run(nlmsg_process_thread, NULL, "process");
void receive_us_msg(struct sk_buff* skb)
{
struct sk_buff* nl_skb=NULL;
nl_skb=skb_copy(skb,GFP_ATOMIC);
if(nl_skb)
skb_queue_tail(&(nl_sk->sk_receive_queue),nl_skb);
wake_up_interruptible(sk_sleep(nl_sk));
}
/*nlmsg_process_thread函數負責處理Netlink消息并回傳用戶程序*/
3實驗平臺與測試方法
為了驗證本文Netlink多線程并發機制的穩定性與擴展性,測試將在32位與64位Linux系統上進行。測試機具體配置如表1、表2所示。
在測試中,檢驗使用上述方法進程能創建包含Netlink連接的線程數量。如果進程中不同線程生成的nl_pid一致,Netlink將創建失敗。在32位系統上,進程可以使用的虛擬用戶地址空間為3GB,其創建線程分配的線程空間大小總和不能超過這個限制,因此系統中進程能創建的線程數量取決于線程堆棧大小,在實驗中通過ulimit命令設置不同的線程堆棧大小,然后記錄進程能創建的最大線程數量,看其是否與理論值相符。在64位機器上,由于虛擬地址空間可以達到TB級,因此測試時首先設置系統最大可創建線程數,然后通過測試程序記錄實際能創建的最大線程數量,看其是否與設置值相符。
4實驗結果與分析
在32位操作系統上測試結果如表3所示。
試驗顯示,在32位操作系統上,線程堆棧為2M時,創建線程數量為1 449,這個值與理論最大值相符,因為在32位Linux操作系統下進程用戶空間大小為3G(3 072M),用3 072M除以2M得1 536,但實際測試用例中代碼段與數據段等占用大概1KB,這個值應該為1 400多。同理,內核堆棧為4M、8M、16M時,線程數量與理論最大值相符合。
在64位操作系統上測試結果如表4所示。
系統中最大線程數量值設置為32 768,測試結果顯示,不同大小線程堆棧情況下創建的線程數量與系統線程最大值相差不大,符合預期,完全能夠滿足實際應用需求。
5結語
Netlink socket是Linux系統中用戶程序與內核模塊之間一種很靈活的通信方式,它使用方便,提供了全雙工、緩沖I/O、多點傳送及異步通訊等高級特性,應用極其廣泛。但是,Netlink在內核不同版本中變化非常大,目前公開資料上提供的線程并發與消息接收處理松耦合方法存在錯誤,也不適應當前內核版本。本文分析了Netlink socket本地地址nl_pid流行計算公式的錯誤原因,設計了新的計算公式,并在真實機器上進行了驗證,測試結果顯示新計算公式能真正支持線程中Netlink的使用。同時,還針對當前內核版本,設計了切實可用的消息接收與處理松耦合流程,實現了上層應用程序的快速響應。
但是,由于Netlink是基于BSD socket實現的,其通信過程耗時非常大,傳輸效率不高,在今后工作中可以考慮結合其它傳輸機制,實現快捷高效的內核態與用戶態數據通信。
參考文獻參考文獻:
[1]ROBERT L. Linux內核設計與實現[M].陳莉君,康華,張波,譯.北京:機械工業出版社,2006.
[2]NEIRA-AYUSO P, GASCA R M, LEFEVRE L. Communicating between the kernel and userspace in Linux using Netlink sockets[J]. Software Practice & Experience,2013,40(9):797–810.
[3]CORBET J, RUBINI A, KROAHHARTMAN G. Linux device drivers[M]. Cambridge :O'Reilly Media, Inc.2005.
[4]郭松,謝維波.Linux視域下Proc文件系統的編程剖析[J].華僑大學學報:自然科學版,2010,31(5):515520.
[5]MOCHEL P. The sysfs filesystem[J]. Proceedings of Annual Linux Symposium,2005(1):313326.
[6]STEVENS W R,RAGO S A. UNIX環境高級編程[M].尤晉元,譯.北京:人民郵電出版社,2009.
[7]SALIM J, KHOSRAVI H, KLEEN A, et al. Linux Netlink as an IP services protocol[J]. International Journal of Developmental Neuroscience,2003,28(8):94.
[8]KENT S, SEO K. RFC4301: security architecture for internet protocol (IPSec)[M]. Los Angeles: RFC Editor,2005.
[9]PURDY G N. Linux iptablespocket reference: firewalls, nat and accounting[M]. Sebastopol, CA: OReilly Media, Inc,2004.
[10]ROSEN. Netfilter[M]. Berkeley, CA: Linux Kernel Networking Apress,2014.
[11]劉斌,朱程榮.Linux內核與用戶空間通信機制研究[J].電腦知識與技術,2012,8(16):38163817.
[12]HE K K. Why and how to use Netlink socket[J]. Linux Journal,2005,11(130):1419.
責任編輯(責任編輯:何麗)endprint