? 關于Netlink多播機制的用法
? ? ? ? 在上一篇博文中我們所遇到的情況都是用戶空間作為消息進程的發起者,Netlink還支持內核作為消息的發送方的情況。這一般用于內核主動向用戶空間報告一些內核狀態,例如我們在用戶空間看到的USB的熱插拔事件的通告就是這樣的應用。
? ? ? ?先說一下我們的目標,內核線程每個一秒鐘往一個多播組里發送一條消息,然后用戶空間所以加入了該組的進程都會收到這樣的消息,并將消息內容打印出來。
?? ? ? ?Netlink地址結構體中的nl_groups是32位,也就是說每種Netlink協議最多支持32個多播組。如何理解這里所說的每種Netlink協議?在里預定義的如下協議都是Netlink協議簇的具體協議,還有我們添加的NETLINK_TEST也是一種Netlink協議。
點擊(此處)折疊或打開
#define NETLINK_ROUTE????????0????/*?Routing/device hook????????????????*/
#define NETLINK_UNUSED????????1????/*?Unused number????????????????*/
#define NETLINK_USERSOCK????2????/*?Reserved?for?user mode socket protocols ????*/
#define NETLINK_FIREWALL????3????/*?Firewalling hook????????????????*/
#define NETLINK_INET_DIAG????4????/*?INET socket monitoring????????????*/
#define NETLINK_NFLOG????????5????/*?netfilter/iptables ULOG?*/
#define NETLINK_XFRM????????6????/*?ipsec?*/
#define NETLINK_SELINUX????????7????/*?SELinux event notifications?*/
#define NETLINK_ISCSI????????8????/*?Open-iSCSI?*/
#define NETLINK_AUDIT????????9????/*?auditing?*/
#define NETLINK_FIB_LOOKUP????10????
#define NETLINK_CONNECTOR????11
#define NETLINK_NETFILTER????12????/*?netfilter subsystem?*/
#define NETLINK_IP6_FW????????13
#define NETLINK_DNRTMSG????????14????/*?DECnet routing messages?*/
#define NETLINK_KOBJECT_UEVENT????15????/*?Kernel messages?to?userspace?*/
#define NETLINK_GENERIC????????16
/*?leave room?for?NETLINK_DM?(DM Events)?*/
#define NETLINK_SCSITRANSPORT????18????/*?SCSI Transports?*/
#define NETLINK_ECRYPTFS????19
#define NETLINK_TEST 20 /* 用戶添加的自定義協議 */
在我們自己添加的NETLINK_TEST協議里,同樣地,最多允許我們設置32個多播組,每個多播組用1個比特表示,所以不同的多播組不可能出現重復。你可以根據自己的實際需求,決定哪個多播組是用來做什么的。用戶空間的進程如果對某個多播組感興趣,那么它就加入到該組中,當內核空間的進程往該組發送多播消息時,所有已經加入到該多播組的用戶進程都會收到該消息。
? ? ? ?再回到我們Netlink地址結構體里的nl_groups成員,它是多播組的地址掩碼,注意是掩碼不是多播組的組號。如何根據多播組號取得多播組號的掩碼呢?在af_netlink.c中有個函數:
點擊(此處)折疊或打開
static u32 netlink_group_mask(u32 group)
{
return group???1?<(group?-?1)?:?0;
}
也就是說,在用戶空間的代碼里,如果我們要加入到多播組1,需要設置nl_groups設置為1;多播組2的掩碼為2;多播組3的掩碼為4,依次類推。為0表示我們不希望加入任何多播組。理解這一點很重要。所以我們可以在用戶空間也定義一個類似于netlink_group_mask()的功能函數,完成從多播組號到多播組掩碼的轉換。最終用戶空間的代碼如下:
?
點擊(此處)折疊或打開
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#define MAX_PAYLOAD 1024?//?Netlink消息的最大載荷的長度
unsigned int netlink_group_mask(unsigned int group)
{
return group ? 1 << (group - 1) : 0;
}
int?main(int?argc,?char*?argv[])
{
struct sockaddr_nl src_addr;
struct nlmsghdr?*nlh?=?NULL;
struct iovec iov;
struct msghdr msg;
int?sock_fd,?retval;
//?創建Socket
sock_fd?=?socket(PF_NETLINK,?SOCK_RAW,?NETLINK_TEST);
if(sock_fd?==?-1){
printf("error getting socket: %s",?strerror(errno));
return?-1;
}
memset(&src_addr,?0,?sizeof(src_addr));
src_addr.nl_family?=?PF_NETLINK;
src_addr.nl_pid = 0;?//?表示我們要從內核接收多播消息。注意:該字段為0有雙重意義,另一個意義是表示我們發送的數據的目的地址是內核。
src_addr.nl_groups?=?netlink_group_mask(atoi(argv[1]));?//?多播組的掩碼,組號來自我們執行程序時輸入的第一個參數
//?因為我們要加入到一個多播組,所以必須調用bind()。
retval?=?bind(sock_fd,?(struct sockaddr*)&src_addr,?sizeof(src_addr));
if(retval?0){
printf("bind failed: %s",?strerror(errno));
close(sock_fd);
return?-1;
}
//?為接收Netlink消息申請存儲空間
nlh?=?(struct nlmsghdr?*)malloc(NLMSG_SPACE(MAX_PAYLOAD));
if(!nlh){
printf("malloc nlmsghdr error!\n");
close(sock_fd);
return?-1;
}
memset(nlh,?0,?NLMSG_SPACE(MAX_PAYLOAD));
iov.iov_base?=?(void?*)nlh;
iov.iov_len?=?NLMSG_SPACE(MAX_PAYLOAD);
memset(&msg,?0,?sizeof(msg));
msg.msg_iov?=?&iov;
msg.msg_iovlen?=?1;
//?從內核接收消息
printf("waitinf for...\n");
recvmsg(sock_fd,?&msg,?0);
printf("Received message: %s \n",?NLMSG_DATA(nlh));
close(sock_fd);
return 0;
}
可以看到,用戶空間的程序基本沒什么變化,唯一需要格外注意的就是Netlink地址結構體中的nl_groups的設置。由于對它的解釋很少,加之沒有有效的文檔,所以我也是一邊看源碼,一邊在網上搜集資料。有分析不當之處,還請大家幫我指出。
? ? ? ?內核空間我們添加了內核線程和內核線程同步方法completion的使用。內核空間修改后的代碼如下:
?
點擊(此處)折疊或打開
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include??
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Koorey King");
struct sock?*nl_sk?=?NULL;
static struct task_struct *mythread = NULL; //內核線程對象
//向用戶空間發送消息的接口
void sendnlmsg(char?*message/*,int?dstPID*/)
{
struct sk_buff?*skb;
struct nlmsghdr?*nlh;
int?len?=?NLMSG_SPACE(MAX_MSGSIZE);
int?slen?=?0;
if(!message?||?!nl_sk){
return;
}
//?為新的 sk_buffer申請空間
skb?=?alloc_skb(len,?GFP_KERNEL);
if(!skb){
printk(KERN_ERR?"my_net_link: alloc_skb Error./n");
return;
}
slen?=?strlen(message)+1;
//用nlmsg_put()來設置netlink消息頭部
nlh?=?nlmsg_put(skb,?0,?0,?0,?MAX_MSGSIZE,?0);
//?設置Netlink的控制塊里的相關信息
NETLINK_CB(skb).pid?=?0;?//?消息發送者的id標識,如果是內核發的則置0
NETLINK_CB(skb).dst_group = 5; //多播組號為5,但置成0好像也可以。
message[slen]?=?'\0';
memcpy(NLMSG_DATA(nlh),?message,?slen+1);
//通過netlink_unicast()將消息發送用戶空間由dstPID所指定了進程號的進程
//netlink_unicast(nl_sk,skb,dstPID,0);
netlink_broadcast(nl_sk, skb, 0,5, GFP_KERNEL);?//發送多播消息到多播組5,這里我故意沒有用1之類的“常見”值,目的就是為了證明我們上面提到的多播組號和多播組號掩碼之間的對應關系
printk("send OK!\n");
return;
}
//每隔1秒鐘發送一條“I am from kernel!”消息,共發10個報文
static int sending_thread(void *data)
{
int i = 10;
struct completion cmpl;
while(i--){
init_completion(&cmpl);
wait_for_completion_timeout(&cmpl, 1 * HZ);
sendnlmsg("I am from kernel!");
}
printk("sending thread exited!");
return 0;
}
static?int?__init myinit_module()
{
printk("my netlink in\n");
nl_sk?=?netlink_kernel_create(NETLINK_TEST,0,NULL,THIS_MODULE);
if(!nl_sk){
printk(KERN_ERR?"my_net_link: create netlink socket error.\n");
return 1;
}
printk("my netlink: create netlink socket ok.\n");
mythread = kthread_run(sending_thread,NULL,"thread_sender");
return 0;
}
static void __exit mycleanup_module()
{
if(nl_sk?!=?NULL){
sock_release(nl_sk->sk_socket);
}
printk("my netlink out!\n");
}
module_init(myinit_module);
module_exit(mycleanup_module);
關于內核中netlink_kernel_create(int unit, unsigned int groups,…)函數里的第二個參數指的是我們內核進程最多能處理的多播組的個數,如果該值小于32,則默認按32處理,所以在調用netlink_kernel_create()函數時可以不用糾結第二個參數,一般將其置為0就可以了。
?
? ? ? ?在skbuff{}結構體中,有個成員叫做"控制塊",源碼對它的解釋如下:
?
點擊(此處)折疊或打開
struct sk_buff?{
/*?These two members must be first.?*/
struct sk_buff????????*next;
struct sk_buff????????*prev;
… …
/*
*?This?is?the control buffer.?It?is?free?to?use?for?every
*?layer.?Please put your?private?variables there.?If?you
*?want?to?keep them across layers you have?to?do?a skb_clone()
*?first.?This?is?owned by whoever has the skb queued ATM.
*/
char????????????cb[48];
… …
}
當內核態的Netlink發送數據到用戶空間時一般需要填充skbuff的控制塊,填充的方式是通過強制類型轉換,將其轉換成struct netlink_skb_parms{}之后進行填充賦值的:
?
點擊(此處)折疊或打開
struct netlink_skb_parms
{
struct ucred????????creds;????????/*?Skb credentials????*/
__u32????????????pid;
__u32????????????dst_group;
kernel_cap_t????????eff_cap;
__u32????????????loginuid;????/*?Login?(audit)?uid?*/
__u32????????????sid;????????/*?SELinux security id?*/
};
填充時的模板代碼如下:
?
點擊(此處)折疊或打開
NETLINK_CB(skb).pid=xx;
NETLINK_CB(skb).dst_group=xx;
這里要注意的是在Netlink協議簇里提到的skbuff的cb控制塊里保存的是屬于Netlink的私有信息。怎么講,就是Netlink會用該控制塊里的信息來完成它所提供的一些功能,只是完成Netlink功能所必需的一些私有數據。打個比方,以開車為例,開車的時候我們要做的就是打火、控制方向盤、適當地控制油門和剎車,車就開動了,這就是汽車提供給我們的“功能”。汽車的發動機,輪胎,傳動軸,以及所用到的螺絲螺栓等都屬于它的“私有”數據cb。汽車要運行起來這些東西是不可或缺的,但它們之間的協作和交互對用戶來說又是透明的。就好比我們Netlink的私有控制結構struct netlink_skb_parms{}一樣。
? ? ? ?目前我們的例子中,將NETLINK_CB(skb).dst_group設置為相應的多播組號和0效果都是一樣,用戶空間都可以收到該多播消息,原因還不是很清楚,還請Netlink的大蝦們幫我點撥點撥。
? ? ? ?編譯后重新運行,最后的測試結果如下:
?
? ? ? ?注意,這里一定要先執行insmod加載內核模塊,然后再運行用戶空間的程序。如果沒有加載mynlkern.ko而直接執行./test 5在bind()系統調用時會報如下的錯誤:
? ? ? ?bind failed: No such file or directory
? ? ? ?因為網上有寫文章在講老版本Netlink的多播時用法時先執行了用戶空間的程序,然后才加載內核模塊,現在(2.6.21)已經行不通了,這一點請大家注意。
? ? ? ?小結:通過這三篇博文我們對Netlink有了初步的認識,并且也可以開發基于Netlink的基本應用程序。但這只是冰山一角,要想寫出高質量、高效率的軟件模塊還有些差距,特別是對Netlink本質的理解還需要提高一個層次,當然這其中牽扯到內核編程的很多基本功,如臨界資源的互斥、線程安全性保護、用Netlink傳遞大數據時的處理等等都是開發人員需要考慮的問題。
?
評論
查看更多