|
引言 Alan Cox在內(nèi)核1.3版本的開發(fā)階段最先引入了Netlink,剛開始時(shí)Netlink是以字符驅(qū)動(dòng)接口的方式提供內(nèi)核與用戶空間的雙向數(shù)據(jù)通信;隨后,在2.1內(nèi)核開發(fā)過程中,Alexey Kuznetsov將Netlink改寫成一個(gè)更加靈活、且易于擴(kuò)展的基于消息通信接口,并將其應(yīng)用到高級(jí)路由子系統(tǒng)的基礎(chǔ)框架里。自那時(shí)起,Netlink就成了Linux內(nèi)核子系統(tǒng)和用戶態(tài)的應(yīng)用程序通信的主要手段之一。 2001年,ForCES IETF委員會(huì)正式對(duì)Netlink進(jìn)行了標(biāo)準(zhǔn)化的工作。Jamal Hadi Salim提議將Netlink定義成一種用于網(wǎng)絡(luò)設(shè)備的路由引擎組件和其控制管理組件之間通信的協(xié)議。不過他的建議最終沒有被采納,取而代之的是我們今天所看到的格局:Netlink被設(shè)計(jì)成一個(gè)新的協(xié)議域,domain。 Linux之父托瓦斯曾說過“Linux
is evolution, not intelligent design”。什么意思?就是說,Netlink也同樣遵循了Linux的某些設(shè)計(jì)理念,即沒有完整的規(guī)范文檔,亦沒有設(shè)計(jì)文檔。只有什么?你懂得---“Read the f**king source code”。 當(dāng)然,本文不是分析Netlink在Linux上的實(shí)現(xiàn)機(jī)制,而是就“什么是Netlink”以及“如何用好Netlink”的話題和大家做個(gè)分享,只有在遇到問題時(shí)才需要去閱讀內(nèi)核源碼弄清個(gè)所以然。
什么是Netlink
關(guān)于Netlink的理解,需要把握幾個(gè)關(guān)鍵點(diǎn):
1、面向數(shù)據(jù)報(bào)的無連接消息子系統(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)知識(shí)遷移這就是學(xué)習(xí)的本質(zhì)。Netlink可以實(shí)現(xiàn)內(nèi)核->用戶以及用戶->內(nèi)核的雙向、異步的數(shù)據(jù)通信,同時(shí)它還支持兩個(gè)用戶進(jìn)程之間、甚至兩個(gè)內(nèi)核子系統(tǒng)之間的數(shù)據(jù)通信。本文中,對(duì)后兩者我們不予考慮,焦點(diǎn)集中在如何實(shí)現(xiàn)用戶<->內(nèi)核之間的數(shù)據(jù)通信。 看到第二點(diǎn)腦海中是不是瞬間閃現(xiàn)了下面這張圖片呢?如果是,則說明你確實(shí)有慧根;當(dāng)然,不是也沒關(guān)系,慧根可以慢慢長(zhǎng)嘛,呵呵。 在后面實(shí)戰(zhàn)Netlink套接字編程時(shí)我們主要會(huì)用到socket(),bind(),sendmsg() 和recvmsg()等系統(tǒng)調(diào)用,當(dāng)然還有socket提供的輪訓(xùn)(polling)機(jī)制。
Netlink通信類型 Netlink支持兩種類型的通信方式:單播和多播。 單播:經(jīng)常用于一個(gè)用戶進(jìn)程和一個(gè)內(nèi)核子系統(tǒng)之間1:1的數(shù)據(jù)通信。用戶空間發(fā)送命令到內(nèi)核,然后從內(nèi)核接受命令的返回結(jié)果。
多播:經(jīng)常用于一個(gè)內(nèi)核進(jìn)程和多個(gè)用戶進(jìn)程之間的1:N的數(shù)據(jù)通信。內(nèi)核作為會(huì)話的發(fā)起者,用戶空間的應(yīng)用程序是接收者。為了實(shí)現(xiàn)這個(gè)功能,內(nèi)核空間的程序會(huì)創(chuàng)建一個(gè)多播組,然后所有用戶空間的對(duì)該內(nèi)核進(jìn)程發(fā)送的消息感興趣的進(jìn)程都加入到該組即可接收來自內(nèi)核發(fā)送的消息了。如下: 其中進(jìn)程A和子系統(tǒng)1之間是單播通信,進(jìn)程B、C和子系統(tǒng)2是多播通信。上圖還向我們說明了一個(gè)信息。從用戶空間傳遞到內(nèi)核的數(shù)據(jù)是不需要排隊(duì)的,即其操作是同步完成;而從內(nèi)核空間向用戶空間傳遞數(shù)據(jù)時(shí)需要排隊(duì),是異步的。了解了這一點(diǎn)在開發(fā)基于Netlink的應(yīng)用模塊時(shí)可以使我們少走很多彎路。假如,你向內(nèi)核發(fā)送了一個(gè)消息需要獲取內(nèi)核中某些信息,比如路由表,或其他信息,如果路由表過于龐大,那么內(nèi)核在通過Netlink向你返回?cái)?shù)據(jù)時(shí),你可以好生琢磨一下如何接收這些數(shù)據(jù)的問題,畢竟你已經(jīng)看到了那個(gè)輸出隊(duì)列了,不能視而不見啊。
Netlink的消息格式
Netlink消息由兩部分組成:消息頭和有效數(shù)據(jù)載荷,且整個(gè)Netlink消息是4字節(jié)對(duì)齊,一般按主機(jī)字節(jié)序進(jìn)行傳遞。消息頭為固定的16字節(jié),消息體長(zhǎng)度可變: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:整個(gè)消息的長(zhǎng)度,按字節(jié)計(jì)算。包括了Netlink消息頭本身。
nlmsg_type:消息的類型,即是數(shù)據(jù)還是控制消息。目前(內(nèi)核版本2.6.21)Netlink僅支持四種類型的控制消息,如下:
NLMSG_NOOP-空消息,什么也不做;
NLMSG_ERROR-指明該消息中包含一個(gè)錯(cuò)誤;
NLMSG_DONE-如果內(nèi)核通過Netlink隊(duì)列返回了多個(gè)消息,那么隊(duì)列的最后一條消息的類型為NLMSG_DONE,其余所有消息的nlmsg_flags屬性都被設(shè)置NLM_F_MULTI位有效。
NLMSG_OVERRUN-暫時(shí)沒用到。
nlmsg_flags:附加在消息上的額外說明信息,如上面提到的NLM_F_MULTI。摘錄如下:
|
標(biāo)記
|
作用及說明
|
|
NLM_F_REQUEST
|
如果消息中有該標(biāo)記位,說明這是一個(gè)請(qǐng)求消息。所有從用戶空間到內(nèi)核空間的消息都要設(shè)置該位,否則內(nèi)核將向用戶返回一個(gè)EINVAL無效參數(shù)的錯(cuò)誤
|
|
NLM_F_MULTI
|
消息從用戶->內(nèi)核是同步的立刻完成,而從內(nèi)核->用戶則需要排隊(duì)。如果內(nèi)核之前收到過來自用戶的消息中有NLM_F_DUMP位為1的消息,那么內(nèi)核就會(huì)向用戶空間發(fā)送一個(gè)由多個(gè)Netlink消息組成的鏈表。除了最后個(gè)消息外,其余每條消息中都設(shè)置了該位有效。
|
|
NLM_F_ACK
|
該消息是內(nèi)核對(duì)來自用戶空間的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:消息序列號(hào)。因?yàn)?span lang="EN-US">Netlink是面向數(shù)據(jù)報(bào)的,所以存在丟失數(shù)據(jù)的風(fēng)險(xiǎn),但是Netlink提供了如何確保消息不丟失的機(jī)制,讓程序開發(fā)人員根據(jù)其實(shí)際需求而實(shí)現(xiàn)。消息序列號(hào)一般和NLM_F_ACK類型的消息聯(lián)合使用,如果用戶的應(yīng)用程序需要保證其發(fā)送的每條消息都成功被內(nèi)核收到的話,那么它發(fā)送消息時(shí)需要用戶程序自己設(shè)置序號(hào),內(nèi)核收到該消息后對(duì)提取其中的序列號(hào),然后在發(fā)送給用戶程序回應(yīng)消息里設(shè)置同樣的序列號(hào)。有點(diǎn)類似于TCP的響應(yīng)和確認(rèn)機(jī)制。
注意:當(dāng)內(nèi)核主動(dòng)向用戶空間發(fā)送廣播消息時(shí),消息中的該字段總是為0。
nlmsg_pid:當(dāng)用戶空間的進(jìn)程和內(nèi)核空間的某個(gè)子系統(tǒng)之間通過Netlink建立了數(shù)據(jù)交換的通道后,Netlink會(huì)為每個(gè)這樣的通道分配一個(gè)唯一的數(shù)字標(biāo)識(shí)。其主要作用就是將來自用戶空間的請(qǐng)求消息和響應(yīng)消息進(jìn)行關(guān)聯(lián)。說得直白一點(diǎn),假如用戶空間存在多個(gè)用戶進(jìn)程,內(nèi)核空間同樣存在多個(gè)進(jìn)程,Netlink必須提供一種機(jī)制用于確保每一對(duì)“用戶-內(nèi)核”空間通信的進(jìn)程之間的數(shù)據(jù)交互不會(huì)發(fā)生紊亂。 即,進(jìn)程A、B通過Netlink向子系統(tǒng)1獲取信息時(shí),子系統(tǒng)1必須確保回送給進(jìn)程A的響應(yīng)數(shù)據(jù)不會(huì)發(fā)到進(jìn)程B那里。主要適用于用戶空間的進(jìn)程從內(nèi)核空間獲取數(shù)據(jù)的場(chǎng)景。通常情況下,用戶空間的進(jìn)程在向內(nèi)核發(fā)送消息時(shí)一般通過系統(tǒng)調(diào)用getpid()將當(dāng)前進(jìn)程的進(jìn)程號(hào)賦給該變量,即用戶空間的進(jìn)程希望得到內(nèi)核的響應(yīng)時(shí)才會(huì)這么做。從內(nèi)核主動(dòng)發(fā)送到用戶空間的消息該字段都被設(shè)置為0。
Netlink的消息體
Netlink的消息體采用TLV(Type-Length-Value)格式: Netlink每個(gè)屬性都由<include/linux/netlink.h>文件里的struct nlattr{}來表示:
Netlink提供的錯(cuò)誤指示消息
當(dāng)用戶空間的應(yīng)用程序和內(nèi)核空間的進(jìn)程之間通過Netlink通信時(shí)發(fā)生了錯(cuò)誤,Netlink必須向用戶空間通報(bào)這種錯(cuò)誤。Netlink對(duì)錯(cuò)誤消息進(jìn)行了單獨(dú)封裝,<include/linux/netlink.h>:- struct nlmsgerr
- {
- int error; //標(biāo)準(zhǔn)的錯(cuò)誤碼,定義在errno.h頭文件中??梢杂胮error()來解釋
- struct nlmsghdr msg; //指明了哪條消息觸發(fā)了結(jié)構(gòu)體中error這個(gè)錯(cuò)誤值
- };
Netlink編程需要注意的問題
基于Netlink的用戶-內(nèi)核通信,有兩種情況可能會(huì)導(dǎo)致丟包:
1、內(nèi)存耗盡;
2、用戶空間接收進(jìn)程的緩沖區(qū)溢出。導(dǎo)致緩沖區(qū)溢出的主要原因有可能是:用戶空間的進(jìn)程運(yùn)行太慢;或者接收隊(duì)列太短。
如果Netlink不能將消息正確傳遞到用戶空間的接收進(jìn)程,那么用戶空間的接收進(jìn)程在調(diào)用recvmsg()系統(tǒng)調(diào)用時(shí)就會(huì)返回一個(gè)內(nèi)存不足(ENOBUFS)的錯(cuò)誤,這一點(diǎn)需要注意。換句話說,緩沖區(qū)溢出的情況是不會(huì)發(fā)送在從用戶->內(nèi)核的sendmsg()系統(tǒng)調(diào)用里,原因前面我們也說過了,請(qǐng)大家自己思考一下。 當(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í)用戶空間兩個(gè)進(jìn)程之間,或內(nèi)核空間兩個(gè)進(jìn)程之間的通信。該屬性為0時(shí)一般適用于如下兩種情況: 第一,我們要發(fā)送的目的地是內(nèi)核,即從用戶空間發(fā)往內(nèi)核空間時(shí),我們構(gòu)造的Netlink地址結(jié)構(gòu)體中nl_pid通常情況下都置為0。這里有一點(diǎn)需要跟大家交代一下,在Netlink規(guī)范里,PID全稱是Port-ID(32bits),其主要作用是用于唯一的標(biāo)識(shí)一個(gè)基于netlink的socket通道。通常情況下nl_pid都設(shè)置為當(dāng)前進(jìn)程的進(jìn)程號(hào)。然而,對(duì)于一個(gè)進(jìn)程的多個(gè)線程同時(shí)使用netlink socket的情況,nl_pid的設(shè)置一般采用如下這個(gè)樣子來實(shí)現(xiàn): - pthread_self() << 16 | getpid();
第二,從內(nèi)核發(fā)出的多播報(bào)文到用戶空間時(shí),如果用戶空間的進(jìn)程處在該多播組中,那么其地址結(jié)構(gòu)體中nl_pid也設(shè)置為0,同時(shí)還要結(jié)合下面介紹到的另一個(gè)屬性。
nl_groups:如果用戶空間的進(jìn)程希望加入某個(gè)多播組,則必須執(zhí)行bind()系統(tǒng)調(diào)用。該字段指明了調(diào)用者希望加入的多播組號(hào)的掩碼(注意不是組號(hào),后面我們會(huì)詳細(xì)講解這個(gè)字段)。如果該字段為0則表示調(diào)用者不希望加入任何多播組。對(duì)于每個(gè)隸屬于Netlink協(xié)議域的協(xié)議,最多可支持32個(gè)多播組(因?yàn)?span lang="EN-US">nl_groups的長(zhǎng)度為32比特),每個(gè)多播組用一個(gè)比特來表示。
今天我們來動(dòng)手演練一下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é)議 */
如果我們?cè)?span lang="EN-US">Netlink協(xié)議簇里開發(fā)一個(gè)新的協(xié)議,只要在該文件中定義協(xié)議號(hào)即可,例如我們定義一種基于Netlink協(xié)議簇的、協(xié)議號(hào)是20的自定義協(xié)議,如上所示。同時(shí)記得,將內(nèi)核頭文件目錄中的netlink.h也做對(duì)應(yīng)的修改,在我的系統(tǒng)中它的路徑是:/usr/src/linux-2.6.21/include/linux/netlink.h 接下來我們?cè)谟脩艨臻g以及內(nèi)核空間模塊的開發(fā)過程中就可以使用這種協(xié)議了,一共分為三個(gè)階段。
Stage 1:
我們首先實(shí)現(xiàn)的功能是用戶->內(nèi)核的單向數(shù)據(jù)通信,即用戶空間發(fā)送一個(gè)消息給內(nèi)核,然后內(nèi)核將其打印輸出,就這么簡(jiǎn)單。用戶空間的示例代碼如下【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)容,來自我們命令行輸入的第一個(gè)參數(shù)*/
- strcpy(NLMSG_DATA(nlh), argv[1]);
- /*這個(gè)是模板,暫時(shí)不用糾結(jié)為什么要這樣用。有時(shí)間詳細(xì)講解socket時(shí)再說*/
- 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,唯一不同的是我們這次編程是針對(duì)Netlink協(xié)議簇的。這里我們提前引入了BSD層的消息結(jié)構(gòu)體struct msghdr{},定義在<include/linux/socket.h>文件里,以及其數(shù)據(jù)塊struct
iovec{}定義在<include/linux/uio.h>頭文件里。這里就不展開了,大家先記住這個(gè)用法就行。以后有時(shí)間再深入到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ā)出的多播報(bào)文
2、消息的接收方是內(nèi)核,即從用戶空間發(fā)往內(nèi)核的消息
|
|
netlink消息頭體. nlmsg_pid
|
來自內(nèi)核主動(dòng)發(fā)出的消息
|
這個(gè)例子僅是從用戶空間到內(nèi)核空間的單向數(shù)據(jù)通信,所以Netlink地址結(jié)構(gòu)體中我們?cè)O(shè)置了dest_addr.nl_pid = 0,說明我們的報(bào)文的目的地是內(nèi)核空間;在填充Netlink消息頭部時(shí),我們做了nlh->nlmsg_pid = 0這樣的設(shè)置。
需要注意幾個(gè)宏的使用:
NLMSG_SPACE(MAX_PAYLOAD),該宏用于返回不小于MAX_PAYLOAD且4字節(jié)對(duì)齊的最小長(zhǎng)度值,一般用于向內(nèi)存系統(tǒng)申請(qǐng)空間是指定所申請(qǐng)的內(nèi)存字節(jié)數(shù),和NLMSG_LENGTH(len)所不同的是,前者所申請(qǐng)的空間里不包含Netlink消息頭部所占的字節(jié)數(shù),后者是消息負(fù)載和消息頭加起來的總長(zhǎng)度。
NLMSG_DATA(nlh),該宏用于返回Netlink消息中數(shù)據(jù)部分的首地址,在寫入和讀取消息數(shù)據(jù)部分時(shí)會(huì)用到它。
它們之間的關(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)建了一個(gè)內(nèi)核態(tài)的socket,第一個(gè)參數(shù)我們擴(kuò)展的協(xié)議號(hào);第二個(gè)參數(shù)為多播組號(hào),目前我們用不上,將其置為0;第三個(gè)參數(shù)是個(gè)回調(diào)函數(shù),即當(dāng)內(nèi)核的Netlink socket套接字收到數(shù)據(jù)時(shí)的處理函數(shù);第四個(gè)參數(shù)就不多說了。
在回調(diào)函數(shù)nl_data_ready()中,我們不斷的從socket的接收隊(duì)列去取數(shù)據(jù),一旦拿到數(shù)據(jù)就將其打印輸出。在協(xié)議棧的INET層,用于存儲(chǔ)數(shù)據(jù)的是大名鼎鼎的sk_buff結(jié)構(gòu),所以我們通過nlh = (struct nlmsghdr *)skb->data;可以拿到netlink的消息體,然后通過NLMSG_DATA(nlh)定位到netlink的消息負(fù)載。
將上述代碼編譯后測(cè)試結(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號(hào)
- nlh->nlmsg_type = NLMSG_NOOP; //指明我們的Netlink是消息負(fù)載是一條空消息
- nlh->nlmsg_flags = 0;
- /*設(shè)置Netlink的消息內(nèi)容,來自我們命令行輸入的第一個(gè)參數(shù)*/
- strcpy(NLMSG_DATA(nlh), argv[1]);
- /*這個(gè)是模板,暫時(shí)不用糾結(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申請(qǐng)空間
- 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)識(shí),如果是內(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)程號(hào)的進(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)將其存儲(chǔ)在了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);
重新編譯后,測(cè)試結(jié)果如下: 
Stage 3:
前面我們提到過,如果用戶進(jìn)程希望加入某個(gè)多播組時(shí)才需要調(diào)用bind()函數(shù)。前面的示例中我們沒有這個(gè)需求,可還是調(diào)了bind(),心頭有些不爽。在前幾篇博文里有關(guān)于socket編程時(shí)幾個(gè)常見API的詳細(xì)解釋和說明,不明白的童鞋可以回頭去復(fù)習(xí)一下。
因?yàn)?span lang="EN-US">Netlink是面向無連接的數(shù)據(jù)報(bào)的套接字,所以我們還可以用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號(hào)
- nlh->nlmsg_type = NLMSG_NOOP; //指明我們的Netlink是消息負(fù)載是一條空消息
- nlh->nlmsg_flags = 0;
- /*設(shè)置Netlink的消息內(nèi)容,來自我們命令行輸入的第一個(gè)參數(shù)*/
- strcpy(NLMSG_DATA(nlh), argv[1]);
- /*這個(gè)模板就用不上了。*/
- /*
- 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); //清空整個(gè)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)匀挥?span lang="EN-US">netlink_unicast()從內(nèi)核空間發(fā)送消息到用戶空間。
重新編譯后,測(cè)試結(jié)果如下: 和Stage 2中代碼運(yùn)行效果完全一樣。也就是說,在開發(fā)Netlink程序過程中,如果沒牽扯到多播機(jī)制,那么用戶空間的socket代碼其實(shí)是不用執(zhí)行bind()系統(tǒng)調(diào)用的,但此時(shí)就需要用sendto()和recvfrom()完成數(shù)據(jù)的發(fā)送和接收的任務(wù);如果執(zhí)行了bind()系統(tǒng)調(diào)用,當(dāng)然也可以繼續(xù)用sendto()和recvfrom(),但給它們傳遞的參數(shù)就有所區(qū)別。這時(shí)候一般使用sendmsg()和recvmsg()來完成數(shù)據(jù)的發(fā)送和接收。大家根據(jù)自己的實(shí)際情況靈活選擇。
關(guān)于Netlink多播機(jī)制的用法
在上一篇博文中我們所遇到的情況都是用戶空間作為消息進(jìn)程的發(fā)起者,Netlink還支持內(nèi)核作為消息的發(fā)送方的情況。這一般用于內(nèi)核主動(dòng)向用戶空間報(bào)告一些內(nèi)核狀態(tài),例如我們?cè)谟脩艨臻g看到的USB的熱插拔事件的通告就是這樣的應(yīng)用。
先說一下我們的目標(biāo),內(nèi)核線程每個(gè)一秒鐘往一個(gè)多播組里發(fā)送一條消息,然后用戶空間所以加入了該組的進(jìn)程都會(huì)收到這樣的消息,并將消息內(nèi)容打印出來。
Netlink地址結(jié)構(gòu)體中的nl_groups是32位,也就是說每種Netlink協(xié)議最多支持32個(gè)多播組。如何理解這里所說的每種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é)議里,同樣地,最多允許我們?cè)O(shè)置32個(gè)多播組,每個(gè)多播組用1個(gè)比特表示,所以不同的多播組不可能出現(xiàn)重復(fù)。你可以根據(jù)自己的實(shí)際需求,決定哪個(gè)多播組是用來做什么的。用戶空間的進(jìn)程如果對(duì)某個(gè)多播組感興趣,那么它就加入到該組中,當(dāng)內(nèi)核空間的進(jìn)程往該組發(fā)送多播消息時(shí),所有已經(jīng)加入到該多播組的用戶進(jìn)程都會(huì)收到該消息。
再回到我們Netlink地址結(jié)構(gòu)體里的nl_groups成員,它是多播組的地址掩碼,注意是掩碼不是多播組的組號(hào)。如何根據(jù)多播組號(hào)取得多播組號(hào)的掩碼呢?在af_netlink.c中有個(gè)函數(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)很重要。所以我們可以在用戶空間也定義一個(gè)類似于netlink_group_mask()的功能函數(shù),完成從多播組號(hào)到多播組掩碼的轉(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消息的最大載荷的長(zhǎng)度
- 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有雙重意義,另一個(gè)意義是表示我們發(fā)送的數(shù)據(jù)的目的地址是內(nèi)核。
- src_addr.nl_groups = netlink_group_mask(atoi(argv[1])); // 多播組的掩碼,組號(hào)來自我們執(zhí)行程序時(shí)輸入的第一個(gè)參數(shù)
- // 因?yàn)槲覀円尤氲揭粋€(gè)多播組,所以必須調(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消息申請(qǐng)存儲(chǔ)空間
- 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è)置。由于對(duì)它的解釋很少,加之沒有有效的文檔,所以我也是一邊看源碼,一邊在網(wǎng)上搜集資料。有分析不當(dāng)之處,還請(qǐng)大家?guī)臀抑赋觥?/font>
內(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)核線程對(duì)象
- //向用戶空間發(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申請(qǐng)空間
- 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)識(shí),如果是內(nèi)核發(fā)的則置0
- NETLINK_CB(skb).dst_group = 5; //多播組號(hào)為5,但置成0好像也可以。
- message[slen] = '\0';
- memcpy(NLMSG_DATA(nlh), message, slen+1);
- //通過netlink_unicast()將消息發(fā)送用戶空間由dstPID所指定了進(jìn)程號(hào)的進(jìn)程
- //netlink_unicast(nl_sk,skb,dstPID,0);
- netlink_broadcast(nl_sk, skb, 0,5, GFP_KERNEL); //發(fā)送多播消息到多播組5,這里我故意沒有用1之類的“常見”值,目的就是為了證明我們上面提到的多播組號(hào)和多播組號(hào)掩碼之間的對(duì)應(yīng)關(guān)系
- printk("send OK!\n");
- return;
- }
- //每隔1秒鐘發(fā)送一條“I am from kernel!”消息,共發(fā)10個(gè)報(bào)文
- 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ù)里的第二個(gè)參數(shù)指的是我們內(nèi)核進(jìn)程最多能處理的多播組的個(gè)數(shù),如果該值小于32,則默認(rèn)按32處理,所以在調(diào)用netlink_kernel_create()函數(shù)時(shí)可以不用糾結(jié)第二個(gè)參數(shù),一般將其置為0就可以了。
在skbuff{}結(jié)構(gòu)體中,有個(gè)成員叫做"控制塊",源碼對(duì)它的解釋如下: - 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ù)到用戶空間時(shí)一般需要填充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 */
- };
填充時(shí)的模板代碼如下: - NETLINK_CB(skb).pid=xx;
- NETLINK_CB(skb).dst_group=xx;
這里要注意的是在Netlink協(xié)議簇里提到的skbuff的cb控制塊里保存的是屬于Netlink的私有信息。怎么講,就是Netlink會(huì)用該控制塊里的信息來完成它所提供的一些功能,只是完成Netlink功能所必需的一些私有數(shù)據(jù)。打個(gè)比方,以開車為例,開車的時(shí)候我們要做的就是打火、控制方向盤、適當(dāng)?shù)乜刂朴烷T和剎車,車就開動(dòng)了,這就是汽車提供給我們的“功能”。汽車的發(fā)動(dòng)機(jī),輪胎,傳動(dòng)軸,以及所用到的螺絲螺栓等都屬于它的“私有”數(shù)據(jù)cb。汽車要運(yùn)行起來這些東西是不可或缺的,但它們之間的協(xié)作和交互對(duì)用戶來說又是透明的。就好比我們Netlink的私有控制結(jié)構(gòu)struct netlink_skb_parms{}一樣。
目前我們的例子中,將NETLINK_CB(skb).dst_group設(shè)置為相應(yīng)的多播組號(hào)和0效果都是一樣,用戶空間都可以收到該多播消息,原因還不是很清楚,還請(qǐng)Netlink的大蝦們幫我點(diǎn)撥點(diǎn)撥。
編譯后重新運(yùn)行,最后的測(cè)試結(jié)果如下:
注意,這里一定要先執(zhí)行insmod加載內(nèi)核模塊,然后再運(yùn)行用戶空間的程序。如果沒有加載mynlkern.ko而直接執(zhí)行./test 5在bind()系統(tǒng)調(diào)用時(shí)會(huì)報(bào)如下的錯(cuò)誤:
bind failed: No such file or directory
因?yàn)榫W(wǎng)上有寫文章在講老版本Netlink的多播時(shí)用法時(shí)先執(zhí)行了用戶空間的程序,然后才加載內(nèi)核模塊,現(xiàn)在(2.6.21)已經(jīng)行不通了,這一點(diǎn)請(qǐng)大家注意。
小結(jié):通過這三篇博文我們對(duì)Netlink有了初步的認(rèn)識(shí),并且也可以開發(fā)基于Netlink的基本應(yīng)用程序。但這只是冰山一角,要想寫出高質(zhì)量、高效率的軟件模塊還有些差距,特別是對(duì)Netlink本質(zhì)的理解還需要提高一個(gè)層次,當(dāng)然這其中牽扯到內(nèi)核編程的很多基本功,如臨界資源的互斥、線程安全性保護(hù)、用Netlink傳遞大數(shù)據(jù)時(shí)的處理等等都是開發(fā)人員需要考慮的問題。
Linux中與內(nèi)核通信的Netlink機(jī)制(實(shí)例)
Netlink在2.6版本的內(nèi)核中變化也是很大的,在最新的2.6.37內(nèi)核中,其定義已經(jīng)改成下面這種形式,傳遞的參數(shù)已經(jīng)達(dá)到6個(gè)。其中第一個(gè)參數(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);
| struct
net是一個(gè)網(wǎng)絡(luò)名字空間namespace,在不同的名字空間里面可以有自己的轉(zhuǎn)發(fā)信息庫(kù),有自己的一套net_device等等。默認(rèn)情況下都是使用
init_net這個(gè)全局變量,下面是內(nèi)核中調(diào)用netlink_kernel_create()函數(shù)的一個(gè)示例。在內(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ù)將在沒有接收緩存可利用時(shí)立即返回,而如果為0,該函
數(shù)在沒有接收緩存可利用 定時(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)用對(duì)netlink的使用。如果用戶需要增加新的netlink協(xié)議類型,必須通過修改
linux/netlink.h來實(shí)現(xiàn),當(dāng)然,目前的netlink實(shí)現(xiàn)已經(jīng)包含了一個(gè)通用的協(xié)議類型NETLINK_GENERIC以方便用戶使用,用
戶可以直接使用它而不必增加新的協(xié)議類型。前面講到,為了增加新的netlink協(xié)議類型,用戶僅需增加如下定義到linux/netlink.h就可
以:
只要增加這個(gè)定義之后,用戶就可以在內(nèi)核的任何地方引用該協(xié)議。
在內(nèi)核中,為了創(chuàng)建一個(gè)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是一個(gè)網(wǎng)絡(luò)名字空間namespace,在不同的名字空間里面可以有自己的轉(zhuǎn)發(fā)信息庫(kù),有自己的一套net_device等等。默認(rèn)情況下都是使用init_net這個(gè)全局變量
參數(shù)unit表示netlink協(xié)議類型,如
NETLINK_MYTEST,參數(shù)input則為內(nèi)核模塊定義的netlink消息處理函數(shù),當(dāng)有消息到達(dá)這個(gè)netlink
socket時(shí),該input函數(shù)指針就會(huì)被引用。函數(shù)指針input的參數(shù)skb實(shí)際上就是函數(shù)netlink_kernel_create返回的
struct sock指針,sock實(shí)際是socket的一個(gè)內(nèi)核表示數(shù)據(jù)結(jié)構(gòu),用戶態(tài)應(yīng)用創(chuàng)建的socket在內(nèi)核中也會(huì)有一個(gè)struct
sock結(jié)構(gòu)來表示。
函數(shù)input()會(huì)在發(fā)送進(jìn)程執(zhí)行sendmsg()時(shí)
被調(diào)用,這樣處理消息比較及時(shí),但是,如果消息特別長(zhǎng)時(shí),這樣處理將增加系統(tǒng)調(diào)用sendmsg()的執(zhí)行時(shí)間,也就是說當(dāng)用戶的程序調(diào)用sendmsg
()函數(shù)時(shí),如果input()函數(shù)處理時(shí)間過長(zhǎng),也就是說input()函數(shù)不執(zhí)行不完,用戶程序調(diào)用的sendmsg()函數(shù)就不會(huì)返回。只有當(dāng)內(nèi)核
空間中的input()函數(shù)返回時(shí),用戶調(diào)用的sendmsg()函數(shù)才會(huì)返回。對(duì)于這種情況,可以定義一個(gè)內(nèi)核線程專門負(fù)責(zé)消息接收,而函數(shù)input
的工作只是喚醒該內(nèi)核線程,這樣sendmsg將很快返回。(這里網(wǎng)上的的說明)不過在查看Linux2.6.37版本的內(nèi)核時(shí)并沒有發(fā)現(xiàn)這種處理過程,
一般都是按下面的方法進(jìn)行處理。
這里注冊(cè)的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)
|
前面的三個(gè)參數(shù)與
netlink_unicast相同,參數(shù)group為接收消息的多播組,該參數(shù)的每一個(gè)位代表一個(gè)多播組,因此如果發(fā)送給多個(gè)多播組,就把該參數(shù)設(shè)置為
多個(gè)多播組組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,也即源地址,對(duì)于內(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)核通信的兩個(gè)程序,一個(gè)是用戶空間,一個(gè)是內(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文件后,會(huì)生成一個(gè)netlink_k.ko文件,可以使用下面的命令進(jìn)行安
裝,insmod
netlink_k.ko,使用lsmod查看,當(dāng)安裝成功后,然后,執(zhí)行./netlink用戶空間程序,可以在另一個(gè)終端下執(zhí)行dmesg命令,查看
內(nèi)核通信的情況。這里netlink程序向內(nèi)核空間發(fā)送一個(gè)hello you!內(nèi)核返回給一個(gè)I am from
kernel!在這里使用了一個(gè)定時(shí)器,也就是每3秒中發(fā)送一次I am from
kernel!只有內(nèi)核把10個(gè)字符串全部發(fā)送完畢后,用戶空間的sendmsg()才會(huì)返回,也就是在用戶空間的netlink才會(huì)輸出內(nèi)核空間發(fā)送過
來的數(shù)據(jù),這里只有一個(gè)簡(jiǎn)單的程序,并沒有什么實(shí)際的意義,因?yàn)?,正如前面所說的一般情況下不會(huì)在回調(diào)函數(shù)中處理太多的東西,以免sendmsg()函數(shù)
返回不及時(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.aspx
http://blog.csdn.net/liumang_D/archive/2010/03/25/5413042.aspx
Linux 系統(tǒng)內(nèi)核空間與用戶空間通信的實(shí)現(xiàn)與分析
|