引言 Alan Cox在內(nèi)核1.3版本的開發(fā)階段最先引入了Netlink,剛開始時Netlink是以字符驅(qū)動接口的方式提供內(nèi)核與用戶空間的雙向數(shù)據(jù)通信;隨后,在2.1內(nèi)核開發(fā)過程中,Alexey Kuznetsov將Netlink改寫成一個更加靈活、且易于擴(kuò)展的基于消息通信接口,并將其應(yīng)用到高級路由子系統(tǒng)的基礎(chǔ)框架里。自那時起,Netlink就成了Linux內(nèi)核子系統(tǒng)和用戶態(tài)的應(yīng)用程序通信的主要手段之一。
2001年,F(xiàn)orCES IETF委員會正式對Netlink進(jìn)行了標(biāo)準(zhǔn)化的工作。Jamal Hadi Salim提議將Netlink定義成一種用于網(wǎng)絡(luò)設(shè)備的路由引擎組件和其控制管理組件之間通信的協(xié)議。不過他的建議最終沒有被采納,取而代之的是我們今天所看到的格局:Netlink被設(shè)計成一個新的協(xié)議域,domain。
Linux之父托瓦斯曾說過“Linuxis evolution, not intelligent design”。什么意思?就是說,Netlink也同樣遵循了Linux的某些設(shè)計理念,即沒有完整的規(guī)范文檔,亦沒有設(shè)計文檔。只有什么?你懂得---“Read the f**king source code”。
當(dāng)然,本文不是分析Netlink在Linux上的實(shí)現(xiàn)機(jī)制,而是就“什么是Netlink”以及“如何用好Netlink”的話題和大家做個分享,只有在遇到問題時才需要去閱讀內(nèi)核源碼弄清個所以然。
什么是Netlink
關(guān)于Netlink的理解,需要把握幾個關(guān)鍵點(diǎn):
1、面向數(shù)據(jù)報的無連接消息子系統(tǒng)
2、基于通用的BSD Socket架構(gòu)而實(shí)現(xiàn)
關(guān)于第一點(diǎn)使我們很容易聯(lián)想到UDP協(xié)議,能想到這一點(diǎn)就非常棒了。按著UDP協(xié)議來理解Netlink不是不無道理,只要你能觸類旁通,做到“活學(xué)”,善于總結(jié)歸納、聯(lián)想,最后實(shí)現(xiàn)知識遷移這就是學(xué)習(xí)的本質(zhì)。Netlink可以實(shí)現(xiàn)內(nèi)核->用戶以及用戶->內(nèi)核的雙向、異步的數(shù)據(jù)通信,同時它還支持兩個用戶進(jìn)程之間、甚至兩個內(nèi)核子系統(tǒng)之間的數(shù)據(jù)通信。本文中,對后兩者我們不予考慮,焦點(diǎn)集中在如何實(shí)現(xiàn)用戶<->內(nèi)核之間的數(shù)據(jù)通信。
看到第二點(diǎn)腦海中是不是瞬間閃現(xiàn)了下面這張圖片呢?如果是,則說明你確實(shí)有慧根;當(dāng)然,不是也沒關(guān)系,慧根可以慢慢長嘛,呵呵。
在后面實(shí)戰(zhàn)Netlink套接字編程時我們主要會用到socket(),bind(),sendmsg()
和recvmsg()等系統(tǒng)調(diào)用,當(dāng)然還有socket提供的輪訓(xùn)(polling)機(jī)制。
Netlink通信類型
Netlink支持兩種類型的通信方式:單播和多播。
單播:經(jīng)常用于一個用戶進(jìn)程和一個內(nèi)核子系統(tǒng)之間1:1的數(shù)據(jù)通信。用戶空間發(fā)送命令到內(nèi)核,然后從內(nèi)核接受命令的返回結(jié)果。
多播:經(jīng)常用于一個內(nèi)核進(jìn)程和多個用戶進(jìn)程之間的1:N的數(shù)據(jù)通信。內(nèi)核作為會話的發(fā)起者,用戶空間的應(yīng)用程序是接收者。為了實(shí)現(xiàn)這個功能,內(nèi)核空間的程序會創(chuàng)建一個多播組,然后所有用戶空間的對該內(nèi)核進(jìn)程發(fā)送的消息感興趣的進(jìn)程都加入到該組即可接收來自內(nèi)核發(fā)送的消息了。如下:
其中進(jìn)程A和子系統(tǒng)1之間是單播通信,進(jìn)程B、C和子系統(tǒng)2是多播通信。上圖還向我們說明了一個信息。從用戶空間傳遞到內(nèi)核的數(shù)據(jù)是不需要排隊的,即其操作是同步完成;而從內(nèi)核空間向用戶空間傳遞數(shù)據(jù)時需要排隊,是異步的。了解了這一點(diǎn)在開發(fā)基于Netlink的應(yīng)用模塊時可以使我們少走很多彎路。假如,你向內(nèi)核發(fā)送了一個消息需要獲取內(nèi)核中某些信息,比如路由表,或其他信息,如果路由表過于龐大,那么內(nèi)核在通過Netlink向你返回數(shù)據(jù)時,你可以好生琢磨一下如何接收這些數(shù)據(jù)的問題,畢竟你已經(jīng)看到了那個輸出隊列了,不能視而不見啊。
Netlink的消息格式
Netlink消息由兩部分組成:消息頭和有效數(shù)據(jù)載荷,且整個Netlink消息是4字節(jié)對齊,一般按主機(jī)字節(jié)序進(jìn)行傳遞。消息頭為固定的16字節(jié),消息體長度可變:
Netlink的消息頭
消息頭定義在<include/linux/netlink.h>文件里,由結(jié)構(gòu)體nlmsghdr表示:
struct nlmsghdr
{
__u32 nlmsg_len; /* Length of message including header */
__u16 nlmsg_type; /* Message content */
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process PID */
};
消息頭中各成員屬性的解釋及說明:
nlmsg_len:整個消息的長度,按字節(jié)計算。包括了Netlink消息頭本身。
nlmsg_type:消息的類型,即是數(shù)據(jù)還是控制消息。目前(內(nèi)核版本2.6.21)Netlink僅支持四種類型的控制消息,如下:
NLMSG_NOOP-空消息,什么也不做;
NLMSG_ERROR-指明該消息中包含一個錯誤;
NLMSG_DONE-如果內(nèi)核通過Netlink隊列返回了多個消息,那么隊列的最后一條消息的類型為NLMSG_DONE,其余所有消息的nlmsg_flags屬性都被設(shè)置NLM_F_MULTI位有效。
NLMSG_OVERRUN-暫時沒用到。
nlmsg_flags:附加在消息上的額外說明信息,如上面提到的NLM_F_MULTI。摘錄如下: 標(biāo)記
作用及說明
NLM_F_REQUEST
如果消息中有該標(biāo)記位,說明這是一個請求消息。所有從用戶空間到內(nèi)核空間的消息都要設(shè)置該位,否則內(nèi)核將向用戶返回一個EINVAL無效參數(shù)的錯誤
NLM_F_MULTI
消息從用戶->內(nèi)核是同步的立刻完成,而從內(nèi)核->用戶則需要排隊。如果內(nèi)核之前收到過來自用戶的消息中有NLM_F_DUMP位為1的消息,那么內(nèi)核就會向用戶空間發(fā)送一個由多個Netlink消息組成的鏈表。除了最后個消息外,其余每條消息中都設(shè)置了該位有效。
NLM_F_ACK
該消息是內(nèi)核對來自用戶空間的NLM_F_REQUEST消息的響應(yīng)
NLM_F_ECHO
如果從用戶空間發(fā)給內(nèi)核的消息中該標(biāo)記為1,則說明用戶的應(yīng)用進(jìn)程要求內(nèi)核將用戶發(fā)給它的每條消息通過單播的形式再發(fā)送給用戶進(jìn)程。和我們通常說的“回顯”功能類似。
…
…
大家只要知道nlmsg_flags有多種取值就可以,至于每種值的作用和意義,通過谷歌和源代碼一定可以找到答案,這里就不展開了。上一張2.6.21內(nèi)核中所有的取值情況:
nlmsg_seq:消息序列號。因?yàn)镹etlink是面向數(shù)據(jù)報的,所以存在丟失數(shù)據(jù)的風(fēng)險,但是Netlink提供了如何確保消息不丟失的機(jī)制,讓程序開發(fā)人員根據(jù)其實(shí)際需求而實(shí)現(xiàn)。消息序列號一般和NLM_F_ACK類型的消息聯(lián)合使用,如果用戶的應(yīng)用程序需要保證其發(fā)送的每條消息都成功被內(nèi)核收到的話,那么它發(fā)送消息時需要用戶程序自己設(shè)置序號,內(nèi)核收到該消息后對提取其中的序列號,然后在發(fā)送給用戶程序回應(yīng)消息里設(shè)置同樣的序列號。有點(diǎn)類似于TCP的響應(yīng)和確認(rèn)機(jī)制。
注意:當(dāng)內(nèi)核主動向用戶空間發(fā)送廣播消息時,消息中的該字段總是為0。
nlmsg_pid:當(dāng)用戶空間的進(jìn)程和內(nèi)核空間的某個子系統(tǒng)之間通過Netlink建立了數(shù)據(jù)交換的通道后,Netlink會為每個這樣的通道分配一個唯一的數(shù)字標(biāo)識。其主要作用就是將來自用戶空間的請求消息和響應(yīng)消息進(jìn)行關(guān)聯(lián)。說得直白一點(diǎn),假如用戶空間存在多個用戶進(jìn)程,內(nèi)核空間同樣存在多個進(jìn)程,Netlink必須提供一種機(jī)制用于確保每一對“用戶-內(nèi)核”空間通信的進(jìn)程之間的數(shù)據(jù)交互不會發(fā)生紊亂。
即,進(jìn)程A、B通過Netlink向子系統(tǒng)1獲取信息時,子系統(tǒng)1必須確?;厮徒o進(jìn)程A的響應(yīng)數(shù)據(jù)不會發(fā)到進(jìn)程B那里。主要適用于用戶空間的進(jìn)程從內(nèi)核空間獲取數(shù)據(jù)的場景。通常情況下,用戶空間的進(jìn)程在向內(nèi)核發(fā)送消息時一般通過系統(tǒng)調(diào)用getpid()將當(dāng)前進(jìn)程的進(jìn)程號賦給該變量,即用戶空間的進(jìn)程希望得到內(nèi)核的響應(yīng)時才會這么做。從內(nèi)核主動發(fā)送到用戶空間的消息該字段都被設(shè)置為0。
Netlink的消息體
Netlink的消息體采用TLV(Type-Length-Value)格式:
Netlink每個屬性都由<include/linux/netlink.h>文件里的struct nlattr{}來表示:
Netlink提供的錯誤指示消息
當(dāng)用戶空間的應(yīng)用程序和內(nèi)核空間的進(jìn)程之間通過Netlink通信時發(fā)生了錯誤,Netlink必須向用戶空間通報這種錯誤。Netlink對錯誤消息進(jìn)行了單獨(dú)封裝,<include/linux/netlink.h>:
struct nlmsgerr
{
int error; //標(biāo)準(zhǔn)的錯誤碼,定義在errno.h頭文件中??梢杂胮error()來解釋
struct nlmsghdr msg; //指明了哪條消息觸發(fā)了結(jié)構(gòu)體中error這個錯誤值
};
Netlink編程需要注意的問題
基于Netlink的用戶-內(nèi)核通信,有兩種情況可能會導(dǎo)致丟包:
1、內(nèi)存耗盡;
2、用戶空間接收進(jìn)程的緩沖區(qū)溢出。導(dǎo)致緩沖區(qū)溢出的主要原因有可能是:用戶空間的進(jìn)程運(yùn)行太慢;或者接收隊列太短。
如果Netlink不能將消息正確傳遞到用戶空間的接收進(jìn)程,那么用戶空間的接收進(jìn)程在調(diào)用recvmsg()系統(tǒng)調(diào)用時就會返回一個內(nèi)存不足(ENOBUFS)的錯誤,這一點(diǎn)需要注意。換句話說,緩沖區(qū)溢出的情況是不會發(fā)送在從用戶->內(nèi)核的sendmsg()系統(tǒng)調(diào)用里,原因前面我們也說過了,請大家自己思考一下。
當(dāng)然,如果使用的是阻塞型socket通信,也就不存在內(nèi)存耗盡的隱患了,這又是為什么呢?趕緊去谷歌一下,查查什么是阻塞型socket吧。學(xué)而不思則罔,思而不學(xué)則殆嘛。
Netlink的地址結(jié)構(gòu)體
在TCP博文中我們提到過在Internet編程過程中所用到的地址結(jié)構(gòu)體和標(biāo)準(zhǔn)地址結(jié)構(gòu)體,它們和Netlink地址結(jié)構(gòu)體的關(guān)系如下:
struct sockaddr_nl{}的詳細(xì)定義和描述如下:
struct sockaddr_nl
{
sa_family_t nl_family; /*該字段總是為AF_NETLINK */
unsigned short nl_pad; /* 目前未用到,填充為0*/
__u32 nl_pid; /* process pid */
__u32 nl_groups; /* multicast groups mask */
};
nl_pid:該屬性為發(fā)送或接收消息的進(jìn)程ID,前面我們也說過,Netlink不僅可以實(shí)現(xiàn)用戶-內(nèi)核空間的通信還可使現(xiàn)實(shí)用戶空間兩個進(jìn)程之間,或內(nèi)核空間兩個進(jìn)程之間的通信。該屬性為0時一般適用于如下兩種情況:
第一,我們要發(fā)送的目的地是內(nèi)核,即從用戶空間發(fā)往內(nèi)核空間時,我們構(gòu)造的Netlink地址結(jié)構(gòu)體中nl_pid通常情況下都置為0。這里有一點(diǎn)需要跟大家交代一下,在Netlink規(guī)范里,PID全稱是Port-ID(32bits),其主要作用是用于唯一的標(biāo)識一個基于netlink的socket通道。通常情況下nl_pid都設(shè)置為當(dāng)前進(jìn)程的進(jìn)程號。然而,對于一個進(jìn)程的多個線程同時使用netlink socket的情況,nl_pid的設(shè)置一般采用如下這個樣子來實(shí)現(xiàn):
pthread_self() << 16 | getpid();
第二,從內(nèi)核發(fā)出的多播報文到用戶空間時,如果用戶空間的進(jìn)程處在該多播組中,那么其地址結(jié)構(gòu)體中nl_pid也設(shè)置為0,同時還要結(jié)合下面介紹到的另一個屬性。
nl_groups:如果用戶空間的進(jìn)程希望加入某個多播組,則必須執(zhí)行bind()系統(tǒng)調(diào)用。該字段指明了調(diào)用者希望加入的多播組號的掩碼(注意不是組號,后面我們會詳細(xì)講解這個字段)。如果該字段為0則表示調(diào)用者不希望加入任何多播組。對于每個隸屬于Netlink協(xié)議域的協(xié)議,最多可支持32個多播組(因?yàn)閚l_groups的長度為32比特),每個多播組用一個比特來表示。
今天我們來動手演練一下Netlink的用法,看看它到底是如何實(shí)現(xiàn)用戶-內(nèi)核空間的數(shù)據(jù)通信的。我們依舊是在2.6.21的內(nèi)核環(huán)境下進(jìn)行開發(fā)。
在</usr/include/linux/netlink.h>文件里包含了Netlink協(xié)議簇已經(jīng)定義好的一些預(yù)定義協(xié)議:#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 /* 用戶添加的自定義協(xié)議 */
如果我們在Netlink協(xié)議簇里開發(fā)一個新的協(xié)議,只要在該文件中定義協(xié)議號即可,例如我們定義一種基于Netlink協(xié)議簇的、協(xié)議號是20的自定義協(xié)議,如上所示。同時記得,將內(nèi)核頭文件目錄中的netlink.h也做對應(yīng)的修改,在我的系統(tǒng)中它的路徑是:/usr/src/linux-2.6.21/include/linux/netlink.h
接下來我們在用戶空間以及內(nèi)核空間模塊的開發(fā)過程中就可以使用這種協(xié)議了,一共分為三個階段。
Stage 1:
我們首先實(shí)現(xiàn)的功能是用戶->內(nèi)核的單向數(shù)據(jù)通信,即用戶空間發(fā)送一個消息給內(nèi)核,然后內(nèi)核將其打印輸出,就這么簡單。用戶空間的示例代碼如下【mynlusr.c】
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/socket.h>
#define MAX_PAYLOAD 1024 /*消息最大負(fù)載為1024字節(jié)*/
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))){ //創(chuàng)建套接字
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; /*我們的消息是發(fā)給內(nèi)核的*/
dest_addr.nl_groups = 0; /*在本示例中不存在使用該值的情況*/
//將套接字和Netlink地址結(jié)構(gòu)體進(jìn)行綁定
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是消息負(fù)載是一條空消息
nlh->nlmsg_flags = 0;
/*設(shè)置Netlink的消息內(nèi)容,來自我們命令行輸入的第一個參數(shù)*/
strcpy(NLMSG_DATA(nlh), argv[1]);
/*這個是模板,暫時不用糾結(jié)為什么要這樣用。有時間詳細(xì)講解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向內(nèi)核發(fā)送消息
/* 關(guān)閉netlink套接字 */
close(sock_fd);
free(nlh);
return 0;
}
上面的代碼邏輯已經(jīng)非常清晰了,都是socket編程的API,唯一不同的是我們這次編程是針對Netlink協(xié)議簇的。這里我們提前引入了BSD層的消息結(jié)構(gòu)體struct msghdr{},定義在<include/linux/socket.h>文件里,以及其數(shù)據(jù)塊structiovec{}定義在<include/linux/uio.h>頭文件里。這里就不展開了,大家先記住這個用法就行。以后有時間再深入到socket的骨子里去轉(zhuǎn)悠一番。
另外,需要格外注意的就是Netlink的地址結(jié)構(gòu)體和其消息頭結(jié)構(gòu)中pid字段為0的情況,很容易讓人產(chǎn)生混淆,再總結(jié)一下:
0
netlink地址結(jié)構(gòu)體.nl_pid
1、內(nèi)核發(fā)出的多播報文
2、消息的接收方是內(nèi)核,即從用戶空間發(fā)往內(nèi)核的消息
netlink消息頭體. nlmsg_pid
來自內(nèi)核主動發(fā)出的消息
這個例子僅是從用戶空間到內(nèi)核空間的單向數(shù)據(jù)通信,所以Netlink地址結(jié)構(gòu)體中我們設(shè)置了dest_addr.nl_pid = 0,說明我們的報文的目的地是內(nèi)核空間;在填充Netlink消息頭部時,我們做了nlh->nlmsg_pid = 0這樣的設(shè)置。
需要注意幾個宏的使用:
NLMSG_SPACE(MAX_PAYLOAD),該宏用于返回不小于MAX_PAYLOAD且4字節(jié)對齊的最小長度值,一般用于向內(nèi)存系統(tǒng)申請空間是指定所申請的內(nèi)存字節(jié)數(shù),和NLMSG_LENGTH(len)所不同的是,前者所申請的空間里不包含Netlink消息頭部所占的字節(jié)數(shù),后者是消息負(fù)載和消息頭加起來的總長度。
NLMSG_DATA(nlh),該宏用于返回Netlink消息中數(shù)據(jù)部分的首地址,在寫入和讀取消息數(shù)據(jù)部分時會用到它。
它們之間的關(guān)系如下:
內(nèi)核空間的示例代碼如下【mynlkern.c】:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/init.h>
#include <linux/ip.h>
#include <linux/types.h>
#include <linux/sched.h>
#include <net/sock.h>
#include <linux/netlink.h>
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);
在內(nèi)核模塊的初始化函數(shù)里我們用
nl_sk =netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
創(chuàng)建了一個內(nèi)核態(tài)的socket,第一個參數(shù)我們擴(kuò)展的協(xié)議號;第二個參數(shù)為多播組號,目前我們用不上,將其置為0;第三個參數(shù)是個回調(diào)函數(shù),即當(dāng)內(nèi)核的Netlink socket套接字收到數(shù)據(jù)時的處理函數(shù);第四個參數(shù)就不多說了。
在回調(diào)函數(shù)nl_data_ready()中,我們不斷的從socket的接收隊列去取數(shù)據(jù),一旦拿到數(shù)據(jù)就將其打印輸出。在協(xié)議棧的INET層,用于存儲數(shù)據(jù)的是大名鼎鼎的sk_buff結(jié)構(gòu),所以我們通過nlh = (struct nlmsghdr *)skb->data;可以拿到netlink的消息體,然后通過NLMSG_DATA(nlh)定位到netlink的消息負(fù)載。
將上述代碼編譯后測試結(jié)果如下:
Stage 2:
我們將上面的代碼稍加改造就可以實(shí)現(xiàn)用戶<->內(nèi)核的雙向數(shù)據(jù)通信。
首先是改造用戶空間的代碼:#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/socket.h>
#define MAX_PAYLOAD 1024 /*消息最大負(fù)載為1024字節(jié)*/
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; /*我們的消息是發(fā)給內(nèi)核的*/
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();//我們希望得到內(nèi)核回應(yīng),所以得告訴內(nèi)核我們ID號
nlh->nlmsg_type = NLMSG_NOOP; //指明我們的Netlink是消息負(fù)載是一條空消息
nlh->nlmsg_flags = 0;
/*設(shè)置Netlink的消息內(nèi)容,來自我們命令行輸入的第一個參數(shù)*/
strcpy(NLMSG_DATA(nlh), argv[1]);
/*這個是模板,暫時不用糾結(jié)為什么要這樣用。*/
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向內(nèi)核發(fā)送消息
//接收內(nèi)核消息的消息
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));
/* 關(guān)閉netlink套接字 */
close(sock_fd);
free(nlh);
return 0;
}
內(nèi)核空間的修改如下:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/init.h>
#include <linux/ip.h>
#include <linux/types.h>
#include <linux/sched.h>
#include <net/sock.h>
#include <net/netlink.h> /*該文頭文件里包含了linux/netlink.h,因?yàn)槲覀円玫絥et/netlink.h中的某些API函數(shù),nlmsg_pug()*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Koorey King");
struct sock *nl_sk = NULL;
//向用戶空間發(fā)送消息的接口
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()來設(shè)置netlink消息頭部
nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);
// 設(shè)置Netlink的控制塊
NETLINK_CB(skb).pid = 0; // 消息發(fā)送者的id標(biāo)識,如果是內(nèi)核發(fā)的則置0
NETLINK_CB(skb).dst_group = 0; //如果目的組為內(nèi)核或某一進(jìn)程,該字段也置0
message[slen] = '\0';
memcpy(NLMSG_DATA(nlh), message, slen+1);
//通過netlink_unicast()將消息發(fā)送用戶空間由dstPID所指定了進(jìn)程號的進(jìn)程
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); //發(fā)送者的進(jìn)程ID我們已經(jīng)將其存儲在了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);
重新編譯后,測試結(jié)果如下:
Stage 3:
前面我們提到過,如果用戶進(jìn)程希望加入某個多播組時才需要調(diào)用bind()函數(shù)。前面的示例中我們沒有這個需求,可還是調(diào)了bind(),心頭有些不爽。在前幾篇博文里有關(guān)于socket編程時幾個常見API的詳細(xì)解釋和說明,不明白的童鞋可以回頭去復(fù)習(xí)一下。
因?yàn)镹etlink是面向無連接的數(shù)據(jù)報的套接字,所以我們還可以用sendto()和recvfrom()來實(shí)現(xiàn)數(shù)據(jù)的收發(fā),這次我們不再調(diào)用bind()。將Stage 2的例子稍加改造一下,用戶空間的修改如下:
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/socket.h>
#define MAX_PAYLOAD 1024 /*消息最大負(fù)載為1024字節(jié)*/
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; /*我們的消息是發(fā)給內(nèi)核的*/
dest_addr.nl_groups = 0; /*在本示例中不存在使用該值的情況*/
/*不再調(diào)用bind()函數(shù)了
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();//我們希望得到內(nèi)核回應(yīng),所以得告訴內(nèi)核我們ID號
nlh->nlmsg_type = NLMSG_NOOP; //指明我們的Netlink是消息負(fù)載是一條空消息
nlh->nlmsg_flags = 0;
/*設(shè)置Netlink的消息內(nèi)容,來自我們命令行輸入的第一個參數(shù)*/
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); //不再用這種方式發(fā)消息到內(nèi)核
sendto(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),sizeof(dest_addr));
//接收內(nèi)核消息的消息
printf("waiting message from kernel!\n");
//memset((char*)NLMSG_DATA(nlh),0,1024);
memset(nlh,0,MAX_PAYLOAD); //清空整個Netlink消息頭包括消息頭和負(fù)載
//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));
/* 關(guān)閉netlink套接字 */
close(sock_fd);
free(nlh);
return 0;
}
內(nèi)核空間的代碼完全不用修改,我們?nèi)匀挥胣etlink_unicast()從內(nèi)核空間發(fā)送消息到用戶空間。
重新編譯后,測試結(jié)果如下:
和Stage 2中代碼運(yùn)行效果完全一樣。也就是說,在開發(fā)Netlink程序過程中,如果沒牽扯到多播機(jī)制,那么用戶空間的socket代碼其實(shí)是不用執(zhí)行bind()系統(tǒng)調(diào)用的,但此時就需要用sendto()和recvfrom()完成數(shù)據(jù)的發(fā)送和接收的任務(wù);如果執(zhí)行了bind()系統(tǒng)調(diào)用,當(dāng)然也可以繼續(xù)用sendto()和recvfrom(),但給它們傳遞的參數(shù)就有所區(qū)別。這時候一般使用sendmsg()和recvmsg()來完成數(shù)據(jù)的發(fā)送和接收。大家根據(jù)自己的實(shí)際情況靈活選擇。
關(guān)于Netlink多播機(jī)制的用法
在上一篇博文中我們所遇到的情況都是用戶空間作為消息進(jìn)程的發(fā)起者,Netlink還支持內(nèi)核作為消息的發(fā)送方的情況。這一般用于內(nèi)核主動向用戶空間報告一些內(nèi)核狀態(tài),例如我們在用戶空間看到的USB的熱插拔事件的通告就是這樣的應(yīng)用。
先說一下我們的目標(biāo),內(nèi)核線程每個一秒鐘往一個多播組里發(fā)送一條消息,然后用戶空間所以加入了該組的進(jìn)程都會收到這樣的消息,并將消息內(nèi)容打印出來。
Netlink地址結(jié)構(gòu)體中的nl_groups是32位,也就是說每種Netlink協(xié)議最多支持32個多播組。如何理解這里所說的每種Netlink協(xié)議?在</usr/include/linux/netlink.h>里預(yù)定義的如下協(xié)議都是Netlink協(xié)議簇的具體協(xié)議,還有我們添加的NETLINK_TEST也是一種Netlink協(xié)議。
#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 /* 用戶添加的自定義協(xié)議 */
在我們自己添加的NETLINK_TEST協(xié)議里,同樣地,最多允許我們設(shè)置32個多播組,每個多播組用1個比特表示,所以不同的多播組不可能出現(xiàn)重復(fù)。你可以根據(jù)自己的實(shí)際需求,決定哪個多播組是用來做什么的。用戶空間的進(jìn)程如果對某個多播組感興趣,那么它就加入到該組中,當(dāng)內(nèi)核空間的進(jìn)程往該組發(fā)送多播消息時,所有已經(jīng)加入到該多播組的用戶進(jìn)程都會收到該消息。
再回到我們Netlink地址結(jié)構(gòu)體里的nl_groups成員,它是多播組的地址掩碼,注意是掩碼不是多播組的組號。如何根據(jù)多播組號取得多播組號的掩碼呢?在af_netlink.c中有個函數(shù):static u32 netlink_group_mask(u32 group)
{
return group ? 1 << (group - 1) : 0;
}
也就是說,在用戶空間的代碼里,如果我們要加入到多播組1,需要設(shè)置nl_groups設(shè)置為1;多播組2的掩碼為2;多播組3的掩碼為4,依次類推。為0表示我們不希望加入任何多播組。理解這一點(diǎn)很重要。所以我們可以在用戶空間也定義一個類似于netlink_group_mask()的功能函數(shù),完成從多播組號到多播組掩碼的轉(zhuǎn)換。最終用戶空間的代碼如下:
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/socket.h>
#include <errno.h>
#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;
// 創(chuàng)建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; // 表示我們要從內(nèi)核接收多播消息。注意:該字段為0有雙重意義,另一個意義是表示我們發(fā)送的數(shù)據(jù)的目的地址是內(nèi)核。
src_addr.nl_groups = netlink_group_mask(atoi(argv[1])); // 多播組的掩碼,組號來自我們執(zhí)行程序時輸入的第一個參數(shù)
// 因?yàn)槲覀円尤氲揭粋€多播組,所以必須調(diào)用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;
// 從內(nèi)核接收消息
printf("waitinf for...\n");
recvmsg(sock_fd, &msg, 0);
printf("Received message: %s \n", NLMSG_DATA(nlh));
close(sock_fd);
return 0;
}
可以看到,用戶空間的程序基本沒什么變化,唯一需要格外注意的就是Netlink地址結(jié)構(gòu)體中的nl_groups的設(shè)置。由于對它的解釋很少,加之沒有有效的文檔,所以我也是一邊看源碼,一邊在網(wǎng)上搜集資料。有分析不當(dāng)之處,還請大家?guī)臀抑赋觥?div style="height:15px;">
內(nèi)核空間我們添加了內(nèi)核線程和內(nèi)核線程同步方法completion的使用。內(nèi)核空間修改后的代碼如下:#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/init.h>
#include <linux/ip.h>
#include <linux/types.h>
#include <linux/sched.h>
#include <net/sock.h>
#include <net/netlink.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Koorey King");
struct sock *nl_sk = NULL;
static struct task_struct *mythread = NULL; //內(nèi)核線程對象
//向用戶空間發(fā)送消息的接口
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()來設(shè)置netlink消息頭部
nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);
// 設(shè)置Netlink的控制塊里的相關(guān)信息
NETLINK_CB(skb).pid = 0; // 消息發(fā)送者的id標(biāo)識,如果是內(nèi)核發(fā)的則置0
NETLINK_CB(skb).dst_group = 5; //多播組號為5,但置成0好像也可以。
message[slen] = '\0';
memcpy(NLMSG_DATA(nlh), message, slen+1);
//通過netlink_unicast()將消息發(fā)送用戶空間由dstPID所指定了進(jìn)程號的進(jìn)程
//netlink_unicast(nl_sk,skb,dstPID,0);
netlink_broadcast(nl_sk, skb, 0,5, GFP_KERNEL); //發(fā)送多播消息到多播組5,這里我故意沒有用1之類的“常見”值,目的就是為了證明我們上面提到的多播組號和多播組號掩碼之間的對應(yīng)關(guān)系
printk("send OK!\n");
return;
}
//每隔1秒鐘發(fā)送一條“I am from kernel!”消息,共發(fā)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);
關(guān)于內(nèi)核中netlink_kernel_create(int unit, unsigned int groups,…)函數(shù)里的第二個參數(shù)指的是我們內(nèi)核進(jìn)程最多能處理的多播組的個數(shù),如果該值小于32,則默認(rèn)按32處理,所以在調(diào)用netlink_kernel_create()函數(shù)時可以不用糾結(jié)第二個參數(shù),一般將其置為0就可以了。
在skbuff{}結(jié)構(gòu)體中,有個成員叫做"控制塊",源碼對它的解釋如下:
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];
… …
}
當(dāng)內(nèi)核態(tài)的Netlink發(fā)送數(shù)據(jù)到用戶空間時一般需要填充skbuff的控制塊,填充的方式是通過強(qiáng)制類型轉(zhuǎn)換,將其轉(zhuǎn)換成struct netlink_skb_parms{}之后進(jìn)行填充賦值的: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協(xié)議簇里提到的skbuff的cb控制塊里保存的是屬于Netlink的私有信息。怎么講,就是Netlink會用該控制塊里的信息來完成它所提供的一些功能,只是完成Netlink功能所必需的一些私有數(shù)據(jù)。打個比方,以開車為例,開車的時候我們要做的就是打火、控制方向盤、適當(dāng)?shù)乜刂朴烷T和剎車,車就開動了,這就是汽車提供給我們的“功能”。汽車的發(fā)動機(jī),輪胎,傳動軸,以及所用到的螺絲螺栓等都屬于它的“私有”數(shù)據(jù)cb。汽車要運(yùn)行起來這些東西是不可或缺的,但它們之間的協(xié)作和交互對用戶來說又是透明的。就好比我們Netlink的私有控制結(jié)構(gòu)struct netlink_skb_parms{}一樣。
目前我們的例子中,將NETLINK_CB(skb).dst_group設(shè)置為相應(yīng)的多播組號和0效果都是一樣,用戶空間都可以收到該多播消息,原因還不是很清楚,還請Netlink的大蝦們幫我點(diǎn)撥點(diǎn)撥。
編譯后重新運(yùn)行,最后的測試結(jié)果如下:
注意,這里一定要先執(zhí)行insmod加載內(nèi)核模塊,然后再運(yùn)行用戶空間的程序。如果沒有加載mynlkern.ko而直接執(zhí)行./test 5在bind()系統(tǒng)調(diào)用時會報如下的錯誤:
bind failed: No such file or directory
因?yàn)榫W(wǎng)上有寫文章在講老版本Netlink的多播時用法時先執(zhí)行了用戶空間的程序,然后才加載內(nèi)核模塊,現(xiàn)在(2.6.21)已經(jīng)行不通了,這一點(diǎn)請大家注意。
小結(jié):通過這三篇博文我們對Netlink有了初步的認(rèn)識,并且也可以開發(fā)基于Netlink的基本應(yīng)用程序。但這只是冰山一角,要想寫出高質(zhì)量、高效率的軟件模塊還有些差距,特別是對Netlink本質(zhì)的理解還需要提高一個層次,當(dāng)然這其中牽扯到內(nèi)核編程的很多基本功,如臨界資源的互斥、線程安全性保護(hù)、用Netlink傳遞大數(shù)據(jù)時的處理等等都是開發(fā)人員需要考慮的問題。
Linux中與內(nèi)核通信的Netlink機(jī)制(實(shí)例)Netlink在2.6版本的內(nèi)核中變化也是很大的,在最新的2.6.37內(nèi)核中,其定義已經(jīng)改成下面這種形式,傳遞的參數(shù)已經(jīng)達(dá)到6個。其中第一個參數(shù)和mutex參數(shù)都是最新添加的。Mutex也可以為空。這里主要是關(guān)于內(nèi)核空間中的netlink函數(shù)的使用。
extern struct sock *netlink_kernel_create(struct net *net,
int unit,unsigned int groups,
void (*input)(struct sk_buff *skb),
struct mutex *cb_mutex,
struct module *module);
structnet是一個網(wǎng)絡(luò)名字空間namespace,在不同的名字空間里面可以有自己的轉(zhuǎn)發(fā)信息庫,有自己的一套net_device等等。默認(rèn)情況下都是使用init_net這個全局變量,下面是內(nèi)核中調(diào)用netlink_kernel_create()函數(shù)的一個示例。在內(nèi)核中,
audit_sock = netlink_kernel_create(&init_net, NETLINK_AUDIT, 0,
audit_receive, NULL, THIS_MODULE);
模塊調(diào)用函數(shù) netlink_unicast 來發(fā)送單播消息:int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock)
參數(shù)ssk為函數(shù)netlink_kernel_create()返回的socket,參數(shù)skb存放消息,它的data字段指向要發(fā)送的netlink消息結(jié)構(gòu),而skb的控制塊保存了消息的地址信息,前面的宏NETLINK_CB(skb)就用于方便設(shè)置該控制塊,參數(shù)pid為接收消息進(jìn)程的pid,參數(shù)nonblock表示該函數(shù)是否為非阻塞,如果為1,該函數(shù)將在沒有接收緩存可利用時立即返回,而如果為0,該函數(shù)在沒有接收緩存可利用 定時睡眠。
netlink的內(nèi)核實(shí)現(xiàn)在.c文件net/core/af_netlink.c中,內(nèi)核模塊要想使用netlink,也必須包含頭文件linux/netlink.h。內(nèi)核使用netlink需要專門的API,這完全不同于用戶態(tài)應(yīng)用對netlink的使用。如果用戶需要增加新的netlink協(xié)議類型,必須通過修改linux/netlink.h來實(shí)現(xiàn),當(dāng)然,目前的netlink實(shí)現(xiàn)已經(jīng)包含了一個通用的協(xié)議類型NETLINK_GENERIC以方便用戶使用,用戶可以直接使用它而不必增加新的協(xié)議類型。前面講到,為了增加新的netlink協(xié)議類型,用戶僅需增加如下定義到linux/netlink.h就可以:
只要增加這個定義之后,用戶就可以在內(nèi)核的任何地方引用該協(xié)議。
在內(nèi)核中,為了創(chuàng)建一個netlink socket用戶需要調(diào)用如下函數(shù):
extern struct sock *netlink_kernel_create(struct net *net,
int unit,unsigned int groups,
void (*input)(struct sk_buff *skb),
struct mutex *cb_mutex,
struct module *module);
struct net是一個網(wǎng)絡(luò)名字空間namespace,在不同的名字空間里面可以有自己的轉(zhuǎn)發(fā)信息庫,有自己的一套net_device等等。默認(rèn)情況下都是使用init_net這個全局變量
參數(shù)unit表示netlink協(xié)議類型,如NETLINK_MYTEST,參數(shù)input則為內(nèi)核模塊定義的netlink消息處理函數(shù),當(dāng)有消息到達(dá)這個netlinksocket時,該input函數(shù)指針就會被引用。函數(shù)指針input的參數(shù)skb實(shí)際上就是函數(shù)netlink_kernel_create返回的struct sock指針,sock實(shí)際是socket的一個內(nèi)核表示數(shù)據(jù)結(jié)構(gòu),用戶態(tài)應(yīng)用創(chuàng)建的socket在內(nèi)核中也會有一個structsock結(jié)構(gòu)來表示。
函數(shù)input()會在發(fā)送進(jìn)程執(zhí)行sendmsg()時被調(diào)用,這樣處理消息比較及時,但是,如果消息特別長時,這樣處理將增加系統(tǒng)調(diào)用sendmsg()的執(zhí)行時間,也就是說當(dāng)用戶的程序調(diào)用sendmsg()函數(shù)時,如果input()函數(shù)處理時間過長,也就是說input()函數(shù)不執(zhí)行不完,用戶程序調(diào)用的sendmsg()函數(shù)就不會返回。只有當(dāng)內(nèi)核空間中的input()函數(shù)返回時,用戶調(diào)用的sendmsg()函數(shù)才會返回。對于這種情況,可以定義一個內(nèi)核線程專門負(fù)責(zé)消息接收,而函數(shù)input的工作只是喚醒該內(nèi)核線程,這樣sendmsg將很快返回。(這里網(wǎng)上的的說明)不過在查看Linux2.6.37版本的內(nèi)核時并沒有發(fā)現(xiàn)這種處理過程,一般都是按下面的方法進(jìn)行處理。
這里注冊的netlink協(xié)議為NETLINK_XFRM。
nlsk = netlink_kernel_create(net, NETLINK_XFRM, XFRMNLGRP_MAX,
xfrm_netlink_rcv, NULL, THIS_MODULE);
static void xfrm_netlink_rcv(struct sk_buff *skb)
{
mutex_lock(&xfrm_cfg_mutex);
netlink_rcv_skb(skb, &xfrm_user_rcv_msg);
mutex_unlock(&xfrm_cfg_mutex);
}
在netlink_rcv_skb()函數(shù)中進(jìn)行接收處理。
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid,
u32 group, gfp_t allocation)
前面的三個參數(shù)與netlink_unicast相同,參數(shù)group為接收消息的多播組,該參數(shù)的每一個位代表一個多播組,因此如果發(fā)送給多個多播組,就把該參數(shù)設(shè)置為多個多播組組ID的位或。參數(shù)allocation為內(nèi)核內(nèi)存分配類型,一般地為GFP_ATOMIC或GFP_KERNEL,GFP_ATOMIC用于原子的上下文(即不可以睡眠),而GFP_KERNEL用于非原子上下文。
NETLINK_CB(skb).pid = 0;
NETLINK_CB(skb).dst_pid = 0;
NETLINK_CB(skb).dst_group = 1;
字段pid表示消息發(fā)送者進(jìn)程ID,也即源地址,對于內(nèi)核,它為 0, dst_pid 表示消息接收者進(jìn)程 ID,也即目標(biāo)地址,如果目標(biāo)為組或內(nèi)核,它設(shè)置為 0,否則dst_group 表示目標(biāo)組地址,如果它目標(biāo)為某一進(jìn)程或內(nèi)核,dst_group 應(yīng)當(dāng)設(shè)置為 0。
下面是參考網(wǎng)上使用netlink寫的和內(nèi)核通信的兩個程序,一個是用戶空間,一個是內(nèi)核空間。
內(nèi)核空間:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/timer.h>
#include <linux/time.h>
#include <linux/types.h>
#include <net/sock.h>
#include <net/netlink.h>
#define NETLINK_TEST 25
#define MAX_MSGSIZE 1024
int stringlength(char *s);
void sendnlmsg(char * message);
int pid;
int err;
struct sock *nl_sk = NULL;
int flag = 0;
void sendnlmsg(char *message)
{
struct sk_buff *skb_1;
struct nlmsghdr *nlh;
int len = NLMSG_SPACE(MAX_MSGSIZE);
int slen = 0;
if(!message || !nl_sk)
{
return ;
}
skb_1 = alloc_skb(len,GFP_KERNEL);
if(!skb_1)
{
printk(KERN_ERR "my_net_link:alloc_skb_1 error\n");
}
slen = stringlength(message);
nlh = nlmsg_put(skb_1,0,0,0,MAX_MSGSIZE,0);
NETLINK_CB(skb_1).pid = 0;
NETLINK_CB(skb_1).dst_group = 0;
message[slen]= '\0';
memcpy(NLMSG_DATA(nlh),message,slen+1);
printk("my_net_link:send message '%s'.\n",(char *)NLMSG_DATA(nlh));
netlink_unicast(nl_sk,skb_1,pid,MSG_DONTWAIT);
}
int stringlength(char *s)
{
int slen = 0;
for(; *s; s++){
slen++;
}
return slen;
}
void nl_data_ready(struct sk_buff *__skb)
{
struct sk_buff *skb;
struct nlmsghdr *nlh;
char str[100];
struct completion cmpl;
int i=10;
skb = skb_get (__skb);
if(skb->len >= NLMSG_SPACE(0))
{
nlh = nlmsg_hdr(skb);
memcpy(str, NLMSG_DATA(nlh), sizeof(str));
printk("Message received:%s\n",str) ;
pid = nlh->nlmsg_pid;
while(i--)
{
init_completion(&cmpl);
wait_for_completion_timeout(&cmpl,3 * HZ);
sendnlmsg("I am from kernel!");
}
flag = 1;
kfree_skb(skb);
}
}
// Initialize netlink
int netlink_init(void)
{
nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, 1,
nl_data_ready, NULL, THIS_MODULE);
if(!nl_sk){
printk(KERN_ERR "my_net_link: create netlink socket error.\n");
return 1;
}
printk("my_net_link_3: create netlink socket ok.\n");
return 0;
}
static void netlink_exit(void)
{
if(nl_sk != NULL){
sock_release(nl_sk->sk_socket);
}
printk("my_net_link: self module exited\n");
}
module_init(netlink_init);
module_exit(netlink_exit);
MODULE_AUTHOR("frankzfz");
MODULE_LICENSE("GPL");
下面是用戶空間的程序:
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/socket.h>
#include <errno.h>
#define NETLINK_TEST 25
#define MAX_PAYLOAD 1024 // maximum payload size
int main(int argc, char* argv[])
{
int state;
struct sockaddr_nl src_addr, dest_addr;
struct nlmsghdr *nlh = NULL;
struct iovec iov;
struct msghdr msg;
int sock_fd, retval;
int state_smg = 0;
// Create a socket
sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
if(sock_fd == -1){
printf("error getting socket: %s", strerror(errno));
return -1;
}
// To prepare binding
memset(&msg,0,sizeof(msg));
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid(); // self pid
src_addr.nl_groups = 0; // multi cast
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;
}
// To prepare recvmsg
nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
if(!nlh){
printf("malloc nlmsghdr error!\n");
close(sock_fd);
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;
nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
nlh->nlmsg_pid = getpid();
nlh->nlmsg_flags = 0;
strcpy(NLMSG_DATA(nlh),"Hello you!");
iov.iov_base = (void *)nlh;
iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
// iov.iov_len = nlh->nlmsg_len;
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void *)&dest_addr;
msg.msg_namelen = sizeof(dest_addr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
printf("state_smg\n");
state_smg = sendmsg(sock_fd,&msg,0);
if(state_smg == -1)
{
printf("get error sendmsg = %s\n",strerror(errno));
}
memset(nlh,0,NLMSG_SPACE(MAX_PAYLOAD));
printf("waiting received!\n");
// Read message from kernel
while(1){
printf("In while recvmsg\n");
state = recvmsg(sock_fd, &msg, 0);
if(state<0)
{
printf("state<1");
}
printf("In while\n");
printf("Received message: %s\n",(char *) NLMSG_DATA(nlh));
}
close(sock_fd);
return 0;
}
下面是Makefile文件:
obj-m := netlink_k.o
KERNELBUILD := /lib/modules/`uname -r`/build
default:
@echo "BUILE Kmod"
@make -C $(KERNELBUILD) M=$(shell pwd) modules
gcc -o netlink_2 netlink_2.c
clean:
@echo " CLEAN kmod"
@rm -rf *.o
@rm -rf .depend .*.cmd *.ko *.mod.c .tmp_versions *.symvers .*.d
其中,netlink_k.c為內(nèi)核的空間的程序。
先運(yùn)行內(nèi)核代碼netlink_k.ko,也就是在執(zhí)行完makefile文件后,會生成一個netlink_k.ko文件,可以使用下面的命令進(jìn)行安裝,insmod netlink_k.ko,使用lsmod查看,當(dāng)安裝成功后,然后,執(zhí)行./netlink用戶空間程序,可以在另一個終端下執(zhí)行dmesg命令,查看內(nèi)核通信的情況。這里netlink程序向內(nèi)核空間發(fā)送一個hello you!內(nèi)核返回給一個I am from kernel!在這里使用了一個定時器,也就是每3秒中發(fā)送一次I am from kernel!只有內(nèi)核把10個字符串全部發(fā)送完畢后,用戶空間的sendmsg()才會返回,也就是在用戶空間的netlink才會輸出內(nèi)核空間發(fā)送過來的數(shù)據(jù),這里只有一個簡單的程序,并沒有什么實(shí)際的意義,因?yàn)?,正如前面所說的一般情況下不會在回調(diào)函數(shù)中處理太多的東西,以免sendmsg()函數(shù)返回不及時。下面是使用dmesg命令輸出的信息。
[873791.498039] my_net_link_3: create netlink socket ok.
[873810.263676] Message received:Hello
[873813.260848] my_net_link_4:send message 'I am from kernel!'.
[873816.260821] my_net_link_4:send message 'I am from kernel!'.
[873819.260860] my_net_link_4:send message 'I am from kernel!'.
[873822.260762] my_net_link_4:send message 'I am from kernel!'.
[873825.260883] my_net_link_4:send message 'I am from kernel!'.
[873828.260669] my_net_link_4:send message 'I am from kernel!'.
[873831.260714] my_net_link_4:send message 'I am from kernel!'.
[873834.260683] my_net_link_4:send message 'I am from kernel!'.
[873837.260666] my_net_link_4:send message 'I am from kernel!'.
[873840.260632] my_net_link_4:send message 'I am from kernel!'.
參考網(wǎng)址:
http://blog.csdn.net/wangjingfei/archive/2010/02/05/5288460.aspxhttp://blog.csdn.net/liumang_D/archive/2010/03/25/5413042.aspxLinux 系統(tǒng)內(nèi)核空間與用戶空間通信的實(shí)現(xiàn)與分析