?今天我們來動手演練一下Netlink的用法,看看它到底是如何實現用戶-內核空間的數據通信的。我們依舊是在2.6.21的內核環境下進行開發。
? ? ? 在文件里包含了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協議簇里開發一個新的協議,只要在該文件中定義協議號即可,例如我們定義一種基于Netlink協議簇的、協議號是20的自定義協議,如上所示。同時記得,將內核頭文件目錄中的netlink.h也做對應的修改,在我的系統中它的路徑是:/usr/src/linux-2.6.21/include/linux/netlink.h
? ? ? 接下來我們在用戶空間以及內核空間模塊的開發過程中就可以使用這種協議了,一共分為三個階段。
Stage 1:
? ? ? 我們首先實現的功能是用戶->內核的單向數據通信,即用戶空間發送一個消息給內核,然后內核將其打印輸出,就這么簡單。用戶空間的示例代碼如下【mynlusr.c】
?
點擊(此處)折疊或打開
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#define MAX_PAYLOAD 1024?/*消息最大負載為1024字節*/
int?main(int?argc,?char*?argv[])
{
struct sockaddr_nl dest_addr;
struct nlmsghdr?*nlh?=?NULL;
struct iovec iov;
int?sock_fd=-1;
struct msghdr msg;
if(-1?==?(sock_fd=socket(PF_NETLINK,?SOCK_RAW,NETLINK_TEST))){ //創建套接字
perror("can't create netlink socket!");
return 1;
}
memset(&dest_addr,?0,?sizeof(dest_addr));
dest_addr.nl_family?=?AF_NETLINK;
dest_addr.nl_pid?=?0;?/*我們的消息是發給內核的*/
dest_addr.nl_groups?=?0;?/*在本示例中不存在使用該值的情況*/
//將套接字和Netlink地址結構體進行綁定
if(-1?==?bind(sock_fd,?(struct sockaddr*)&dest_addr,?sizeof(dest_addr))){
perror("can't bind sockfd with sockaddr_nl!");
return 1;
}
if(NULL?==?(nlh=(struct nlmsghdr?*)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
perror("alloc mem failed!");
return 1;
}
memset(nlh,0,MAX_PAYLOAD);
/*?填充Netlink消息頭部?*/
nlh->nlmsg_len?=?NLMSG_SPACE(MAX_PAYLOAD);
nlh->nlmsg_pid?=?0;
nlh->nlmsg_type?=?NLMSG_NOOP;?//指明我們的Netlink是消息負載是一條空消息
nlh->nlmsg_flags?=?0;
/*設置Netlink的消息內容,來自我們命令行輸入的第一個參數*/
strcpy(NLMSG_DATA(nlh),?argv[1]);
/*這個是模板,暫時不用糾結為什么要這樣用。有時間詳細講解socket時再說*/
memset(&iov,?0,?sizeof(iov));
iov.iov_base?=?(void?*)nlh;
iov.iov_len?=?nlh->nlmsg_len;
memset(&msg,?0,?sizeof(msg));
msg.msg_iov?=?&iov;
msg.msg_iovlen?=?1;
sendmsg(sock_fd,?&msg,?0);?//通過Netlink socket向內核發送消息
/*?關閉netlink套接字?*/
close(sock_fd);
free(nlh);
return 0;
}
上面的代碼邏輯已經非常清晰了,都是socket編程的API,唯一不同的是我們這次編程是針對Netlink協議簇的。這里我們提前引入了BSD層的消息結構體struct msghdr{},定義在文件里,以及其數據塊struct iovec{}定義在頭文件里。這里就不展開了,大家先記住這個用法就行。以后有時間再深入到socket的骨子里去轉悠一番。
? ? ? 另外,需要格外注意的就是Netlink的地址結構體和其消息頭結構中pid字段為0的情況,很容易讓人產生混淆,再總結一下:
?0netlink地址結構體.nl_pid1、內核發出的多播報文
2、消息的接收方是內核,即從用戶空間發往內核的消息netlink消息頭體. nlmsg_pid來自內核主動發出的消息
? ? ?這個例子僅是從用戶空間到內核空間的單向數據通信,所以Netlink地址結構體中我們設置了dest_addr.nl_pid = 0,說明我們的報文的目的地是內核空間;在填充Netlink消息頭部時,我們做了nlh->nlmsg_pid = 0這樣的設置。
? ? ?需要注意幾個宏的使用:
? ? ?NLMSG_SPACE(MAX_PAYLOAD),該宏用于返回不小于MAX_PAYLOAD且4字節對齊的最小長度值,一般用于向內存系統申請空間是指定所申請的內存字節數,和NLMSG_LENGTH(len)所不同的是,前者所申請的空間里不包含Netlink消息頭部所占的字節數,后者是消息負載和消息頭加起來的總長度。
? ? ?NLMSG_DATA(nlh),該宏用于返回Netlink消息中數據部分的首地址,在寫入和讀取消息數據部分時會用到它。
? ? ?它們之間的關系如下:
? ? ?內核空間的示例代碼如下【mynlkern.c】:
?
點擊(此處)折疊或打開
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Koorey King");
struct sock?*nl_sk?=?NULL;
static void?nl_data_ready?(struct sock?*sk,?int?len)
{
struct sk_buff?*skb;
struct nlmsghdr?*nlh?=?NULL;
while((skb?=?skb_dequeue(&sk->sk_receive_queue))?!=?NULL)
{
nlh?=?(struct nlmsghdr?*)skb->data;
printk("%s: received netlink message payload: %s \n",?__FUNCTION__,?(char*)NLMSG_DATA(nlh));
kfree_skb(skb);
}
printk("recvied finished!\n");
}
static?int?__init myinit_module()
{
printk("my netlink in\n");
nl_sk?=?netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
return 0;
}
static void __exit mycleanup_module()
{
printk("my netlink out!\n");
sock_release(nl_sk->sk_socket);
}
module_init(myinit_module);
module_exit(mycleanup_module);
? ? ?在內核模塊的初始化函數里我們用
nl_sk =?netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
創建了一個內核態的socket,第一個參數我們擴展的協議號;第二個參數為多播組號,目前我們用不上,將其置為0;第三個參數是個回調函數,即當內核的Netlink socket套接字收到數據時的處理函數;第四個參數就不多說了。
?
? ? ? 在回調函數nl_data_ready()中,我們不斷的從socket的接收隊列去取數據,一旦拿到數據就將其打印輸出。在協議棧的INET層,用于存儲數據的是大名鼎鼎的sk_buff結構,所以我們通過nlh = (struct nlmsghdr *)skb->data;可以拿到netlink的消息體,然后通過NLMSG_DATA(nlh)定位到netlink的消息負載。
?
? ? ? 將上述代碼編譯后測試結果如下:
Stage 2:
? ? ? 我們將上面的代碼稍加改造就可以實現用戶<->內核的雙向數據通信。
? ? ? 首先是改造用戶空間的代碼:
點擊(此處)折疊或打開
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#define MAX_PAYLOAD 1024?/*消息最大負載為1024字節*/
int?main(int?argc,?char*?argv[])
{
struct sockaddr_nl dest_addr;
struct nlmsghdr?*nlh?=?NULL;
struct iovec iov;
int?sock_fd=-1;
struct msghdr msg;
if(-1?==?(sock_fd=socket(PF_NETLINK,?SOCK_RAW,NETLINK_TEST))){
perror("can't create netlink socket!");
return 1;
}
memset(&dest_addr,?0,?sizeof(dest_addr));
dest_addr.nl_family?=?AF_NETLINK;
dest_addr.nl_pid?=?0;?/*我們的消息是發給內核的*/
dest_addr.nl_groups?=?0;?/*在本示例中不存在使用該值的情況*/
if(-1?==?bind(sock_fd,?(struct sockaddr*)&dest_addr,?sizeof(dest_addr))){
perror("can't bind sockfd with sockaddr_nl!");
return 1;
}
if(NULL?==?(nlh=(struct nlmsghdr?*)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
perror("alloc mem failed!");
return 1;
}
memset(nlh,0,MAX_PAYLOAD);
/*?填充Netlink消息頭部?*/
nlh->nlmsg_len?=?NLMSG_SPACE(MAX_PAYLOAD);
nlh->nlmsg_pid = getpid();//我們希望得到內核回應,所以得告訴內核我們ID號
nlh->nlmsg_type?=?NLMSG_NOOP;?//指明我們的Netlink是消息負載是一條空消息
nlh->nlmsg_flags?=?0;
/*設置Netlink的消息內容,來自我們命令行輸入的第一個參數*/
strcpy(NLMSG_DATA(nlh),?argv[1]);
/*這個是模板,暫時不用糾結為什么要這樣用。*/
memset(&iov,?0,?sizeof(iov));
iov.iov_base?=?(void?*)nlh;
iov.iov_len?=?nlh->nlmsg_len;
memset(&msg,?0,?sizeof(msg));
msg.msg_iov?=?&iov;
msg.msg_iovlen?=?1;
sendmsg(sock_fd,?&msg,?0);?//通過Netlink socket向內核發送消息
//接收內核消息的消息
printf("waiting message from kernel!\n");
memset((char*)NLMSG_DATA(nlh),0,1024);
recvmsg(sock_fd,&msg,0);
printf("Got response: %s\n",NLMSG_DATA(nlh));
/*?關閉netlink套接字?*/
close(sock_fd);
free(nlh);
return 0;
}
? ? ? 內核空間的修改如下:
?
點擊(此處)折疊或打開
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?/*該文頭文件里包含了linux/netlink.h,因為我們要用到net/netlink.h中的某些API函數,nlmsg_pug()*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Koorey King");
struct sock?*nl_sk?=?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 = 0; //如果目的組為內核或某一進程,該字段也置0
message[slen] = '\0';
memcpy(NLMSG_DATA(nlh), message, slen+1);
//通過netlink_unicast()將消息發送用戶空間由dstPID所指定了進程號的進程
netlink_unicast(nl_sk,skb,dstPID,0);
printk("send OK!\n");
return;
}
static void nl_data_ready?(struct sock?*sk,?int?len)
{
struct sk_buff?*skb;
struct nlmsghdr?*nlh?=?NULL;
while((skb?=?skb_dequeue(&sk->sk_receive_queue))?!=?NULL)
{
nlh?=?(struct nlmsghdr?*)skb->data;
printk("%s: received netlink message payload: %s \n",?__FUNCTION__,?(char*)NLMSG_DATA(nlh));
kfree_skb(skb);
sendnlmsg("I see you",nlh->nlmsg_pid);?//發送者的進程ID我們已經將其存儲在了netlink消息頭部里的nlmsg_pid字段里,所以這里可以拿來用。
}
printk("recvied finished!\n");
}
static?int?__init myinit_module()
{
printk("my netlink in\n");
nl_sk?=?netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
return 0;
}
static void __exit mycleanup_module()
{
printk("my netlink out!\n");
sock_release(nl_sk->sk_socket);
}
module_init(myinit_module);
module_exit(mycleanup_module);
? ? ? 重新編譯后,測試結果如下:
Stage 3:
? ? ? 前面我們提到過,如果用戶進程希望加入某個多播組時才需要調用bind()函數。前面的示例中我們沒有這個需求,可還是調了bind(),心頭有些不爽。在前幾篇博文里有關于socket編程時幾個常見API的詳細解釋和說明,不明白的童鞋可以回頭去復習一下。
? ? ? 因為Netlink是面向無連接的數據報的套接字,所以我們還可以用sendto()和recvfrom()來實現數據的收發,這次我們不再調用bind()。將Stage 2的例子稍加改造一下,用戶空間的修改如下:
點擊(此處)折疊或打開
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#define MAX_PAYLOAD 1024?/*消息最大負載為1024字節*/
int?main(int?argc,?char*?argv[])
{
struct sockaddr_nl dest_addr;
struct nlmsghdr?*nlh?=?NULL;
//struct iovec iov;
int?sock_fd=-1;
//struct msghdr msg;
if(-1?==?(sock_fd=socket(PF_NETLINK,?SOCK_RAW,NETLINK_TEST))){
perror("can't create netlink socket!");
return 1;
}
memset(&dest_addr,?0,?sizeof(dest_addr));
dest_addr.nl_family?=?AF_NETLINK;
dest_addr.nl_pid?=?0;?/*我們的消息是發給內核的*/
dest_addr.nl_groups?=?0;?/*在本示例中不存在使用該值的情況*/
/*不再調用bind()函數了
if(-1 == bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
perror("can't bind sockfd with sockaddr_nl!");
return 1;
}*/
if(NULL?==?(nlh=(struct nlmsghdr?*)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
perror("alloc mem failed!");
return 1;
}
memset(nlh,0,MAX_PAYLOAD);
/*?填充Netlink消息頭部?*/
nlh->nlmsg_len?=?NLMSG_SPACE(MAX_PAYLOAD);
nlh->nlmsg_pid?=?getpid();//我們希望得到內核回應,所以得告訴內核我們ID號
nlh->nlmsg_type?=?NLMSG_NOOP;?//指明我們的Netlink是消息負載是一條空消息
nlh->nlmsg_flags?=?0;
/*設置Netlink的消息內容,來自我們命令行輸入的第一個參數*/
strcpy(NLMSG_DATA(nlh),?argv[1]);
/*這個模板就用不上了。*/
/*
memset(&iov, 0, sizeof(iov));
iov.iov_base = (void *)nlh;
iov.iov_len = nlh->nlmsg_len;
memset(&msg, 0, sizeof(msg));
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
*/
//sendmsg(sock_fd, &msg, 0); //不再用這種方式發消息到內核
sendto(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),sizeof(dest_addr));?
//接收內核消息的消息
printf("waiting message from kernel!\n");
//memset((char*)NLMSG_DATA(nlh),0,1024);
memset(nlh,0,MAX_PAYLOAD); //清空整個Netlink消息頭包括消息頭和負載
//recvmsg(sock_fd,&msg,0);
recvfrom(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),NULL);
printf("Got response: %s\n",NLMSG_DATA(nlh));?
/*?關閉netlink套接字?*/
close(sock_fd);
free(nlh);
return 0;
}
內核空間的代碼完全不用修改,我們仍然用netlink_unicast()從內核空間發送消息到用戶空間。
? ? ? 重新編譯后,測試結果如下:
? ? ? 和Stage 2中代碼運行效果完全一樣。也就是說,在開發Netlink程序過程中,如果沒牽扯到多播機制,那么用戶空間的socket代碼其實是不用執行bind()系統調用的,但此時就需要用sendto()和recvfrom()完成數據的發送和接收的任務;如果執行了bind()系統調用,當然也可以繼續用sendto()和recvfrom(),但給它們傳遞的參數就有所區別。這時候一般使用sendmsg()和recvmsg()來完成數據的發送和接收。大家根據自己的實際情況靈活選擇。
?
評論
查看更多