用戶空間和內核空間通訊之【Netlink 】

 用戶空間和內核空間通訊之【Netlink 上】 

 Alan Cox在內核1.3版本的開發階段最先引入了Netlink,剛開始時Netlink是以字符驅動接口的方式提供內核與用戶空間的雙向數據通信;隨後,在2.1內核開發過程中,Alexey Kuznetsov將Netlink改寫成一個更加靈活、且易於擴展的基於消息通信接口,並將其應用到高級路由子系統的基礎框架裏。自那時起,Netlink就成了Linux內核子系統和用戶態的應用程序通信的主要手段之一。

       2001年,ForCES IETF委員會正式對Netlink進行了標準化的工作。Jamal Hadi Salim提議將Netlink定義成一種用於網絡設備的路由引擎組件和其控制管理組件之間通信的協議。不過他的建議最終沒有被採納,取而代之的是我們今天所看到的格局:Netlink被設計成一個新的協議域,domain。

       Linux之父託瓦斯曾說過“Linux is evolution, not intelligent design”。什麼意思?就是說,Netlink也同樣遵循了Linux的某些設計理念,即沒有完整的規範文檔,亦沒有設計文檔。只有什麼?你懂得---“Read the f**king source code”。

       當然,本文不是分析Netlink在Linux上的實現機制,而是就“什麼是Netlink”以及“如何用好Netlink”的話題和大家做個分享,只有在遇到問題時才需要去閱讀內核源碼弄清個所以然。


什麼是Netlink

       關於Netlink的理解,需要把握幾個關鍵點:

       1、面向數據報的無連接消息子系統

       2、基於通用的BSD Socket架構而實現

      關於第一點使我們很容易聯想到UDP協議,能想到這一點就非常棒了。按着UDP協議來理解Netlink不是不無道理,只要你能觸類旁通,做到“活學”,善於總結歸納、聯想,最後實現知識遷移這就是學習的本質。Netlink可以實現內核->用戶以及用戶->內核的雙向、異步的數據通信,同時它還支持兩個用戶進程之間、甚至兩個內核子系統之間的數據通信。本文中,對後兩者我們不予考慮,焦點集中在如何實現用戶<->內核之間的數據通信。

      看到第二點腦海中是不是瞬間閃現了下面這張圖片呢?如果是,則說明你確實有慧根;當然,不是也沒關係,慧根可以慢慢長嘛,呵呵。

    在後面實戰Netlink套接字編程時我們主要會用到socket(),bind(),sendmsg()
recvmsg()等系統調用,當然還有socket提供的輪訓(polling)機制。       


Netlink通信類型

      Netlink支持兩種類型的通信方式:單播多播

      單播:經常用於一個用戶進程和一個內核子系統之間1:1的數據通信。用戶空間發送命令到內核,然後從內核接受命令的返回結果。

      多播:經常用於一個內核進程和多個用戶進程之間的1:N的數據通信。內核作爲會話的發起者,用戶空間的應用程序是接收者。爲了實現這個功能,內核空間的程序會創建一個多播組,然後所有用戶空間的對該內核進程發送的消息感興趣的進程都加入到該組即可接收來自內核發送的消息了。如下:
    其中進程A和子系統1之間是單播通信,進程B、C和子系統2是多播通信。上圖還向我們說明了一個信息。從用戶空間傳遞到內核的數據是不需要排隊的,即其操作是同步完成;而從內核空間向用戶空間傳遞數據時需要排隊,是異步的。瞭解了這一點在開發基於Netlink的應用模塊時可以使我們少走很多彎路。假如,你向內核發送了一個消息需要獲取內核中某些信息,比如路由表,或其他信息,如果路由表過於龐大,那麼內核在通過Netlink向你返回數據時,你可以好生琢磨一下如何接收這些數據的問題,畢竟你已經看到了那個輸出隊列了,不能視而不見啊。


Netlink的消息格式

       Netlink消息由兩部分組成:消息頭和有效數據載荷,且整個Netlink消息是4字節對齊,一般按主機字節序進行傳遞。消息頭爲固定的16字節,消息體長度可變:

Netlink的消息頭

      消息頭定義在<include/linux/netlink.h>文件裏,由結構體nlmsghdr表示:

點擊(此處)摺疊或打開

  1. struct nlmsghdr
  2. {
  3.     __u32        nlmsg_len;    /* Length of message including header */
  4.     __u16        nlmsg_type;    /* Message content */
  5.     __u16        nlmsg_flags;    /* Additional flags */
  6.     __u32        nlmsg_seq;    /* Sequence number */
  7.     __u32        nlmsg_pid;    /* Sending process PID */
  8. };

      消息頭中各成員屬性的解釋及說明:

nlmsg_len:整個消息的長度,按字節計算。包括了Netlink消息頭本身。

nlmsg_type:消息的類型,即是數據還是控制消息。目前(內核版本2.6.21)Netlink僅支持四種類型的控制消息,如下:

     NLMSG_NOOP-空消息,什麼也不做;

     NLMSG_ERROR-指明該消息中包含一個錯誤;

     NLMSG_DONE-如果內核通過Netlink隊列返回了多個消息,那麼隊列的最後一條消息的類型爲NLMSG_DONE,其餘所有消息的nlmsg_flags屬性都被設置NLM_F_MULTI位有效。

     NLMSG_OVERRUN-暫時沒用到。

nlmsg_flags:附加在消息上的額外說明信息,如上面提到的NLM_F_MULTI。摘錄如下:

標記

作用及說明

NLM_F_REQUEST

如果消息中有該標記位,說明這是一個請求消息。所有從用戶空間到內核空間的消息都要設置該位,否則內核將向用戶返回一個EINVAL無效參數的錯誤

NLM_F_MULTI

消息從用戶->內核是同步的立刻完成,而從內核->用戶則需要排隊。如果內核之前收到過來自用戶的消息中有NLM_F_DUMP位爲1的消息,那麼內核就會向用戶空間發送一個由多個Netlink消息組成的鏈表。除了最後個消息外,其餘每條消息中都設置了該位有效。

NLM_F_ACK

該消息是內核對來自用戶空間的NLM_F_REQUEST消息的響應

NLM_F_ECHO

如果從用戶空間發給內核的消息中該標記爲1,則說明用戶的應用進程要求內核將用戶發給它的每條消息通過單播的形式再發送給用戶進程。和我們通常說的“回顯”功能類似。


    大家只要知道nlmsg_flags有多種取值就可以,至於每種值的作用和意義,通過谷歌和源代碼一定可以找到答案,這裏就不展開了。上一張2.6.21內核中所有的取值情況:

nlmsg_seq:消息序列號。因爲Netlink是面向數據報的,所以存在丟失數據的風險,但是Netlink提供瞭如何確保消息不丟失的機制,讓程序開發人員根據其實際需求而實現。消息序列號一般和NLM_F_ACK類型的消息聯合使用,如果用戶的應用程序需要保證其發送的每條消息都成功被內核收到的話,那麼它發送消息時需要用戶程序自己設置序號,內核收到該消息後對提取其中的序列號,然後在發送給用戶程序迴應消息裏設置同樣的序列號。有點類似於TCP的響應和確認機制。

注意:當內核主動向用戶空間發送廣播消息時,消息中的該字段總是爲0。


nlmsg_pid:當用戶空間的進程和內核空間的某個子系統之間通過Netlink建立了數據交換的通道後,Netlink會爲每個這樣的通道分配一個唯一的數字標識。其主要作用就是將來自用戶空間的請求消息和響應消息進行關聯。說得直白一點,假如用戶空間存在多個用戶進程,內核空間同樣存在多個進程,Netlink必須提供一種機制用於確保每一對“用戶-內核”空間通信的進程之間的數據交互不會發生紊亂。
    即,進程A、B通過Netlink向子系統1獲取信息時,子系統1必須確保回送給進程A的響應數據不會發到進程B那裏。主要適用於用戶空間的進程從內核空間獲取數據的場景。通常情況下,用戶空間的進程在向內核發送消息時一般通過系統調用getpid()將當前進程的進程號賦給該變量,即用戶空間的進程希望得到內核的響應時纔會這麼做。從內核主動發送到用戶空間的消息該字段都被設置爲0。

Netlink的消息體

      Netlink的消息體採用TLV(Type-Length-Value)格式:
      Netlink每個屬性都由<include/linux/netlink.h>文件裏的struct nlattr{}來表示:


Netlink提供的錯誤指示消息

      當用戶空間的應用程序和內核空間的進程之間通過Netlink通信時發生了錯誤,Netlink必須向用戶空間通報這種錯誤。Netlink對錯誤消息進行了單獨封裝,<include/linux/netlink.h>:

點擊(此處)摺疊或打開

  1. struct nlmsgerr
  2. {
  3.     int        error; //標準的錯誤碼,定義在errno.h頭文件中。可以用perror()來解釋
  4.     struct nlmsghdr msg; //指明瞭哪條消息觸發了結構體中error這個錯誤值
  5. };


Netlink編程需要注意的問題

      基於Netlink的用戶-內核通信,有兩種情況可能會導致丟包:

      1、內存耗盡;

      2、用戶空間接收進程的緩衝區溢出。導致緩衝區溢出的主要原因有可能是:用戶空間的進程運行太慢;或者接收隊列太短。

      如果Netlink不能將消息正確傳遞到用戶空間的接收進程,那麼用戶空間的接收進程在調用recvmsg()系統調用時就會返回一個內存不足(ENOBUFS)的錯誤,這一點需要注意。換句話說,緩衝區溢出的情況是不會發送在從用戶->內核的sendmsg()系統調用裏,原因前面我們也說過了,請大家自己思考一下。

      當然,如果使用的是阻塞型socket通信,也就不存在內存耗盡的隱患了,這又是爲什麼呢?趕緊去谷歌一下,查查什麼是阻塞型socket吧。學而不思則罔,思而不學則殆嘛。


Netlink的地址結構體

      在TCP博文中我們提到過在Internet編程過程中所用到的地址結構體和標準地址結構體,它們和Netlink地址結構體的關係如下:

    struct sockaddr_nl{}的詳細定義和描述如下:

點擊(此處)摺疊或打開

  1. struct sockaddr_nl
  2. {
  3.     sa_family_t    nl_family;    /*該字段總是爲AF_NETLINK    */
  4.     unsigned short    nl_pad;        /* 目前未用到,填充爲0*/
  5.     __u32        nl_pid;        /* process pid    */
  6.     __u32        nl_groups;    /* multicast groups mask */
  7. };

nl_pid:該屬性爲發送或接收消息的進程ID,前面我們也說過,Netlink不僅可以實現用戶-內核空間的通信還可使現實用戶空間兩個進程之間,或內核空間兩個進程之間的通信。該屬性爲0時一般適用於如下兩種情況:

        第一,我們要發送的目的地是內核,即從用戶空間發往內核空間時,我們構造的Netlink地址結構體中nl_pid通常情況下都置爲0。這裏有一點需要跟大家交代一下,在Netlink規範裏,PID全稱是Port-ID(32bits),其主要作用是用於唯一的標識一個基於netlink的socket通道。通常情況下nl_pid都設置爲當前進程的進程號。然而,對於一個進程的多個線程同時使用netlink socket的情況,nl_pid的設置一般採用如下這個樣子來實現:

點擊(此處)摺疊或打開

  1. pthread_self() << 16 | getpid();

       第二,從內核發出的多播報文到用戶空間時,如果用戶空間的進程處在該多播組中,那麼其地址結構體中nl_pid也設置爲0,同時還要結合下面介紹到的另一個屬性。

nl_groups:如果用戶空間的進程希望加入某個多播組,則必須執行bind()系統調用。該字段指明瞭調用者希望加入的多播組號的掩碼(注意不是組號,後面我們會詳細講解這個字段)。如果該字段爲0則表示調用者不希望加入任何多播組。對於每個隸屬於Netlink協議域的協議,最多可支持32個多播組(因爲nl_groups的長度爲32比特),每個多播組用一個比特來表示。 


用戶空間和內核空間通訊之【Netlink 中】 

今天我們來動手演練一下Netlink的用法,看看它到底是如何實現用戶-內核空間的數據通信的。我們依舊是在2.6.21的內核環境下進行開發。

      在</usr/include/linux/netlink.h>文件裏包含了Netlink協議簇已經定義好的一些預定義協議:

點擊(此處)摺疊或打開

  1. #define NETLINK_ROUTE        0    /* Routing/device hook                */
  2. #define NETLINK_UNUSED        1    /* Unused number                */
  3. #define NETLINK_USERSOCK    2    /* Reserved for user mode socket protocols     */
  4. #define NETLINK_FIREWALL    3    /* Firewalling hook                */
  5. #define NETLINK_INET_DIAG    4    /* INET socket monitoring            */
  6. #define NETLINK_NFLOG        5    /* netfilter/iptables ULOG */
  7. #define NETLINK_XFRM        6    /* ipsec */
  8. #define NETLINK_SELINUX        7    /* SELinux event notifications */
  9. #define NETLINK_ISCSI        8    /* Open-iSCSI */
  10. #define NETLINK_AUDIT        9    /* auditing */
  11. #define NETLINK_FIB_LOOKUP    10    
  12. #define NETLINK_CONNECTOR    11
  13. #define NETLINK_NETFILTER    12    /* netfilter subsystem */
  14. #define NETLINK_IP6_FW        13
  15. #define NETLINK_DNRTMSG        14    /* DECnet routing messages */
  16. #define NETLINK_KOBJECT_UEVENT    15    /* Kernel messages to userspace */
  17. #define NETLINK_GENERIC        16
  18. /* leave room for NETLINK_DM (DM Events) */
  19. #define NETLINK_SCSITRANSPORT    18    /* SCSI Transports */
  20. #define NETLINK_ECRYPTFS    19
  21. #define NETLINK_TEST    20 /* 用戶添加的自定義協議 */

      如果我們在Netlink協議簇裏開發一個新的協議,只要在該文件中定義協議號即可,例如我們定義一種基於Netlink協議簇的、協議號是20的自定義協議,如上所示。同時記得,將內核頭文件目錄中的netlink.h也做對應的修改,在我的系統中它的路徑是:/usr/src/linux-2.6.21/include/linux/netlink.h

      接下來我們在用戶空間以及內核空間模塊的開發過程中就可以使用這種協議了,一共分爲三個階段。


Stage 1:

      我們首先實現的功能是用戶->內核單向數據通信,即用戶空間發送一個消息給內核,然後內核將其打印輸出,就這麼簡單。用戶空間的示例代碼如下【mynlusr.c】

點擊(此處)摺疊或打開

  1. #include <sys/stat.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <sys/socket.h>
  6. #include <sys/types.h>
  7. #include <string.h>
  8. #include <asm/types.h>
  9. #include <linux/netlink.h>
  10. #include <linux/socket.h>

  11. #define MAX_PAYLOAD 1024 /*消息最大負載爲1024字節*/

  12. int main(int argc, char* argv[])
  13. {
  14.     struct sockaddr_nl dest_addr;
  15.     struct nlmsghdr *nlh = NULL;
  16.     struct iovec iov;
  17.     int sock_fd=-1;
  18.     struct msghdr msg;
  19.         
  1.     if(-== (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){ //創建套接字
  2.             perror("can't create netlink socket!");
  3.             return 1;
  4.     }
  5.     memset(&dest_addr, 0, sizeof(dest_addr));
  6.     dest_addr.nl_family = AF_NETLINK;
  7.     dest_addr.nl_pid = 0; /*我們的消息是發給內核的*/
  8.     dest_addr.nl_groups = 0; /*在本示例中不存在使用該值的情況*/
  9.         
  10.     //將套接字和Netlink地址結構體進行綁定
  1.     if(-== bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
  2.           perror("can't bind sockfd with sockaddr_nl!");
  3.           return 1;
  4.     }

  5.     if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
  6.           perror("alloc mem failed!");
  7.           return 1;
  8.     }

  9.     memset(nlh,0,MAX_PAYLOAD);
  10.     /* 填充Netlink消息頭部 */
  11.     nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
  12.     nlh->nlmsg_pid = 0;
  13.     nlh->nlmsg_type = NLMSG_NOOP; //指明我們的Netlink是消息負載是一條空消息
  14.     nlh->nlmsg_flags = 0;

  15.     /*設置Netlink的消息內容,來自我們命令行輸入的第一個參數*/
  16.     strcpy(NLMSG_DATA(nlh), argv[1]);

  17.     /*這個是模板,暫時不用糾結爲什麼要這樣用。有時間詳細講解socket時再說*/
  18.     memset(&iov, 0, sizeof(iov));
  19.     iov.iov_base = (void *)nlh;
  20.     iov.iov_len = nlh->nlmsg_len;
  21.     memset(&msg, 0, sizeof(msg));
  22.     msg.msg_iov = &iov;
  23.     msg.msg_iovlen = 1;

  24.     sendmsg(sock_fd, &msg, 0); //通過Netlink socket向內核發送消息

  25.     /* 關閉netlink套接字 */
  26.     close(sock_fd);
  27.     free(nlh);
  28.     return 0;
  29. }

      上面的代碼邏輯已經非常清晰了,都是socket編程的API,唯一不同的是我們這次編程是針對Netlink協議簇的。這裏我們提前引入了BSD層的消息結構體struct msghdr{},定義在<include/linux/socket.h>文件裏,以及其數據塊struct iovec{}定義在<include/linux/uio.h>頭文件裏。這裏就不展開了,大家先記住這個用法就行。以後有時間再深入到socket的骨子裏去轉悠一番。

      另外,需要格外注意的就是Netlink的地址結構體和其消息頭結構中pid字段爲0的情況,很容易讓人產生混淆,再總結一下:

 

0

netlink地址結構體.nl_pid

1、內核發出的多播報文

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】:

點擊(此處)摺疊或打開

  1. #include <linux/kernel.h>
  2. #include <linux/module.h>
  3. #include <linux/skbuff.h>
  4. #include <linux/init.h>
  5. #include <linux/ip.h>
  6. #include <linux/types.h>
  7. #include <linux/sched.h>
  8. #include <net/sock.h>
  9. #include <linux/netlink.h>

  10. MODULE_LICENSE("GPL");
  11. MODULE_AUTHOR("Koorey King");

  12. struct sock *nl_sk = NULL;
  13. static void nl_data_ready (struct sock *sk, int len)
  14. {
  15.     struct sk_buff *skb;
  16.     struct nlmsghdr *nlh = NULL;

  17.     while((skb = skb_dequeue(&sk->sk_receive_queue)) != NULL)
  18.     {
  19.           nlh = (struct nlmsghdr *)skb->data;
  20.           printk("%s: received netlink message payload: %s \n", __FUNCTION__, (char*)NLMSG_DATA(nlh));
  21.           kfree_skb(skb);
  22.     }
  23.     printk("recvied finished!\n");
  24. }

  25. static int __init myinit_module()
  26. {
  27.     printk("my netlink in\n");
  28.     nl_sk = netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
  29.     return 0;
  30. }

  31. static void __exit mycleanup_module()
  32. {
  33.     printk("my netlink out!\n");
  34.     sock_release(nl_sk->sk_socket);
  35. }

  36. module_init(myinit_module);
  37. 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:

      我們將上面的代碼稍加改造就可以實現用戶<->內核雙向數據通信。

      首先是改造用戶空間的代碼:

點擊(此處)摺疊或打開

  1. #include <sys/stat.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <sys/socket.h>
  6. #include <sys/types.h>
  7. #include <string.h>
  8. #include <asm/types.h>
  9. #include <linux/netlink.h>
  10. #include <linux/socket.h>

  11. #define MAX_PAYLOAD 1024 /*消息最大負載爲1024字節*/

  12. int main(int argc, char* argv[])
  13. {
  14.     struct sockaddr_nl dest_addr;
  15.     struct nlmsghdr *nlh = NULL;
  16.     struct iovec iov;
  17.     int sock_fd=-1;
  18.     struct msghdr msg;

  19.     if(-== (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
  20.           perror("can't create netlink socket!");
  21.           return 1;
  22.     }
  23.     memset(&dest_addr, 0, sizeof(dest_addr));
  24.     dest_addr.nl_family = AF_NETLINK;
  25.     dest_addr.nl_pid = 0; /*我們的消息是發給內核的*/
  26.     dest_addr.nl_groups = 0; /*在本示例中不存在使用該值的情況*/

  27.     if(-== bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
  28.           perror("can't bind sockfd with sockaddr_nl!");
  29.           return 1;
  30.     }
  31.     if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
  32.           perror("alloc mem failed!");
  33.           return 1;
  34.     }
  35.     
  36.     memset(nlh,0,MAX_PAYLOAD);
  37.     /* 填充Netlink消息頭部 */
  38.     nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
  39.     nlh->nlmsg_pid = getpid();//我們希望得到內核迴應,所以得告訴內核我們ID號
  40.     nlh->nlmsg_type = NLMSG_NOOP; //指明我們的Netlink是消息負載是一條空消息
  41.     nlh->nlmsg_flags = 0;

  42.     /*設置Netlink的消息內容,來自我們命令行輸入的第一個參數*/
  43.     strcpy(NLMSG_DATA(nlh), argv[1]);

  44.     /*這個是模板,暫時不用糾結爲什麼要這樣用。*/
  45.     memset(&iov, 0, sizeof(iov));
  46.     iov.iov_base = (void *)nlh;
  47.     iov.iov_len = nlh->nlmsg_len;
  48.     memset(&msg, 0, sizeof(msg));
  49.     msg.msg_iov = &iov;
  50.     msg.msg_iovlen = 1;

  51.     sendmsg(sock_fd, &msg, 0); //通過Netlink socket向內核發送消息

  52.     //接收內核消息的消息
  53.     printf("waiting message from kernel!\n");
  54.     memset((char*)NLMSG_DATA(nlh),0,1024);
  55.     recvmsg(sock_fd,&msg,0);
  56.     printf("Got response: %s\n",NLMSG_DATA(nlh));

  57.     /* 關閉netlink套接字 */
  58.     close(sock_fd);
  59.     free(nlh);
  60.     return 0;
  61. }

      內核空間的修改如下:

點擊(此處)摺疊或打開

  1. #include <linux/kernel.h>
  2. #include <linux/module.h>
  3. #include <linux/skbuff.h>
  4. #include <linux/init.h>
  5. #include <linux/ip.h>
  6. #include <linux/types.h>
  7. #include <linux/sched.h>
  8. #include <net/sock.h>
  9. #include <net/netlink.h> /*該文頭文件裏包含了linux/netlink.h,因爲我們要用到net/netlink.h中的某些API函數,nlmsg_pug()*/

  10. MODULE_LICENSE("GPL");
  11. MODULE_AUTHOR("Koorey King");

  12. struct sock *nl_sk = NULL;
  13. //向用戶空間發送消息的接口
  14. void sendnlmsg(char *message,int dstPID)
  15. {
  16.     struct sk_buff *skb;
  17.     struct nlmsghdr *nlh;
  18.     int len = NLMSG_SPACE(MAX_MSGSIZE);
  19.     int slen = 0;

  20.     if(!message || !nl_sk){
  21.         return;
  22.     }

  23.     // 爲新的 sk_buffer申請空間
  24.     skb = alloc_skb(len, GFP_KERNEL);
  25.     if(!skb){
  26.         printk(KERN_ERR "my_net_link: alloc_skb Error./n");
  27.         return;
  28.     }

  29.     slen = strlen(message)+1;

  30.     //用nlmsg_put()來設置netlink消息頭部
  31.     nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);

  32.     // 設置Netlink的控制塊
  33.     NETLINK_CB(skb).pid = 0; // 消息發送者的id標識,如果是內核發的則置0
  34.     NETLINK_CB(skb).dst_group = 0; //如果目的組爲內核或某一進程,該字段也置0

  35.     message[slen] = '\0';
  36.     memcpy(NLMSG_DATA(nlh), message, slen+1);

  37.     //通過netlink_unicast()將消息發送用戶空間由dstPID所指定了進程號的進程
  38.     netlink_unicast(nl_sk,skb,dstPID,0);
  39.     printk("send OK!\n");
  40.     return;
  41. }

  42. static void nl_data_ready (struct sock *sk, int len)
  43. {
  44.     struct sk_buff *skb;
  45.     struct nlmsghdr *nlh = NULL;

  46.     while((skb = skb_dequeue(&sk->sk_receive_queue)) != NULL)
  47.     {
  48.         nlh = (struct nlmsghdr *)skb->data;
  49.         printk("%s: received netlink message payload: %s \n", __FUNCTION__, (char*)NLMSG_DATA(nlh));
  50.         kfree_skb(skb);
  51.         sendnlmsg("I see you",nlh->nlmsg_pid); //發送者的進程ID我們已經將其存儲在了netlink消息頭部裏的nlmsg_pid字段裏,所以這裏可以拿來用。
  52.     }
  53.     printk("recvied finished!\n");
  54. }

  55. static int __init myinit_module()
  56. {
  57.     printk("my netlink in\n");
  58.     nl_sk = netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
  59.     return 0;
  60. }

  61. static void __exit mycleanup_module()
  62. {
  63.     printk("my netlink out!\n");
  64.     sock_release(nl_sk->sk_socket);
  65. }

  66. module_init(myinit_module);
  67. module_exit(mycleanup_module);

      重新編譯後,測試結果如下:


Stage 3:

      前面我們提到過,如果用戶進程希望加入某個多播組時才需要調用bind()函數。前面的示例中我們沒有這個需求,可還是調了bind(),心頭有些不爽。在前幾篇博文裏有關於socket編程時幾個常見API的詳細解釋和說明,不明白的童鞋可以回頭去複習一下。

      因爲Netlink是面向無連接的數據報的套接字,所以我們還可以用sendto()和recvfrom()來實現數據的收發,這次我們不再調用bind()。將Stage 2的例子稍加改造一下,用戶空間的修改如下:

點擊(此處)摺疊或打開

  1. #include <sys/stat.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <sys/socket.h>
  6. #include <sys/types.h>
  7. #include <string.h>
  8. #include <asm/types.h>
  9. #include <linux/netlink.h>
  10. #include <linux/socket.h>

  11. #define MAX_PAYLOAD 1024 /*消息最大負載爲1024字節*/

  12. int main(int argc, char* argv[])
  13. {
  14.     struct sockaddr_nl dest_addr;
  15.     struct nlmsghdr *nlh = NULL;
  16.     //struct iovec iov;
  17.     int sock_fd=-1;
  18.     //struct msghdr msg;

  19.     if(-== (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
  20.           perror("can't create netlink socket!");
  21.           return 1;
  22.     }
  23.     memset(&dest_addr, 0, sizeof(dest_addr));
  24.     dest_addr.nl_family = AF_NETLINK;
  25.     dest_addr.nl_pid = 0; /*我們的消息是發給內核的*/
  26.     dest_addr.nl_groups = 0; /*在本示例中不存在使用該值的情況*/

  27.    /*不再調用bind()函數了
  28.    if(-1 == bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
  29.           perror("can't bind sockfd with sockaddr_nl!");
  30.           return 1;
  31.    }*/

  32.    if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
  33.           perror("alloc mem failed!");
  34.           return 1;
  35.    }
  36.    memset(nlh,0,MAX_PAYLOAD);
  37.    /* 填充Netlink消息頭部 */
  38.    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
  39.    nlh->nlmsg_pid = getpid();//我們希望得到內核迴應,所以得告訴內核我們ID號
  40.    nlh->nlmsg_type = NLMSG_NOOP; //指明我們的Netlink是消息負載是一條空消息
  41.    nlh->nlmsg_flags = 0;

  42.    /*設置Netlink的消息內容,來自我們命令行輸入的第一個參數*/
  43.    strcpy(NLMSG_DATA(nlh), argv[1]);

  44.    /*這個模板就用不上了。*/
  45.    /*
  46.    memset(&iov, 0, sizeof(iov));
  47.    iov.iov_base = (void *)nlh;
  48.    iov.iov_len = nlh->nlmsg_len;
  49.    memset(&msg, 0, sizeof(msg));
  50.    msg.msg_iov = &iov;
  51.    msg.msg_iovlen = 1;
  52.    */

  53.    //sendmsg(sock_fd, &msg, 0); //不再用這種方式發消息到內核
  54.    sendto(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),sizeof(dest_addr)); 
  55.         
  56.    //接收內核消息的消息
  57.    printf("waiting message from kernel!\n");
  58.    //memset((char*)NLMSG_DATA(nlh),0,1024);
  59.    memset(nlh,0,MAX_PAYLOAD); //清空整個Netlink消息頭包括消息頭和負載
  60.    //recvmsg(sock_fd,&msg,0);
  61.    recvfrom(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),NULL);
  62.    printf("Got response: %s\n",NLMSG_DATA(nlh)); 

  63.    /* 關閉netlink套接字 */
  64.    close(sock_fd);
  65.    free(nlh);
  66.    return 0;
  67. }

      內核空間的代碼完全不用修改,我們仍然用netlink_unicast()從內核空間發送消息到用戶空間。

      重新編譯後,測試結果如下:

      和Stage 2中代碼運行效果完全一樣。也就是說,在開發Netlink程序過程中,如果沒牽扯到多播機制,那麼用戶空間的socket代碼其實是不用執行bind()系統調用的,但此時就需要用sendto()和recvfrom()完成數據的發送和接收的任務;如果執行了bind()系統調用,當然也可以繼續用sendto()和recvfrom(),但給它們傳遞的參數就有所區別。這時候一般使用sendmsg()和recvmsg()來完成數據的發送和接收。大家根據自己的實際情況靈活選擇。



 用戶空間和內核空間通訊之【Netlink 下】

關於Netlink多播機制的用法

        在上一篇博文中我們所遇到的情況都是用戶空間作爲消息進程的發起者,Netlink還支持內核作爲消息的發送方的情況。這一般用於內核主動向用戶空間報告一些內核狀態,例如我們在用戶空間看到的USB的熱插拔事件的通告就是這樣的應用。

       先說一下我們的目標,內核線程每個一秒鐘往一個多播組裏發送一條消息,然後用戶空間所以加入了該組的進程都會收到這樣的消息,並將消息內容打印出來。

        Netlink地址結構體中的nl_groups是32位,也就是說每種Netlink協議最多支持32個多播組。如何理解這裏所說的每種Netlink協議?在裏預定義的如下協議都是Netlink協議簇的具體協議,還有我們添加的NETLINK_TEST也是一種Netlink協議。

點擊(此處)摺疊或打開

  1. #define NETLINK_ROUTE        0    /* Routing/device hook                */
  2. #define NETLINK_UNUSED        1    /* Unused number                */
  3. #define NETLINK_USERSOCK    2    /* Reserved for user mode socket protocols     */
  4. #define NETLINK_FIREWALL    3    /* Firewalling hook                */
  5. #define NETLINK_INET_DIAG    4    /* INET socket monitoring            */
  6. #define NETLINK_NFLOG        5    /* netfilter/iptables ULOG */
  7. #define NETLINK_XFRM        6    /* ipsec */
  8. #define NETLINK_SELINUX        7    /* SELinux event notifications */
  9. #define NETLINK_ISCSI        8    /* Open-iSCSI */
  10. #define NETLINK_AUDIT        9    /* auditing */
  11. #define NETLINK_FIB_LOOKUP    10    
  12. #define NETLINK_CONNECTOR    11
  13. #define NETLINK_NETFILTER    12    /* netfilter subsystem */
  14. #define NETLINK_IP6_FW        13
  15. #define NETLINK_DNRTMSG        14    /* DECnet routing messages */
  16. #define NETLINK_KOBJECT_UEVENT    15    /* Kernel messages to userspace */
  17. #define NETLINK_GENERIC        16
  18. /* leave room for NETLINK_DM (DM Events) */
  19. #define NETLINK_SCSITRANSPORT    18    /* SCSI Transports */
  20. #define NETLINK_ECRYPTFS    19
  21. #define NETLINK_TEST 20 /* 用戶添加的自定義協議 */

       在我們自己添加的NETLINK_TEST協議裏,同樣地,最多允許我們設置32個多播組,每個多播組用1個比特表示,所以不同的多播組不可能出現重複。你可以根據自己的實際需求,決定哪個多播組是用來做什麼的。用戶空間的進程如果對某個多播組感興趣,那麼它就加入到該組中,當內核空間的進程往該組發送多播消息時,所有已經加入到該多播組的用戶進程都會收到該消息。

       再回到我們Netlink地址結構體裏的nl_groups成員,它是多播組的地址掩碼,注意是掩碼不是多播組的組號。如何根據多播組號取得多播組號的掩碼呢?在af_netlink.c中有個函數:

點擊(此處)摺疊或打開

  1. static u32 netlink_group_mask(u32 group)
  2. {
  3.     return group ? 1 << (group - 1) : 0;
  4. }

       也就是說,在用戶空間的代碼裏,如果我們要加入到多播組1,需要設置nl_groups設置爲1;多播組2的掩碼爲2;多播組3的掩碼爲4,依次類推。爲0表示我們不希望加入任何多播組。理解這一點很重要。所以我們可以在用戶空間也定義一個類似於netlink_group_mask()的功能函數,完成從多播組號到多播組掩碼的轉換。最終用戶空間的代碼如下:


點擊(此處)摺疊或打開

  1. #include <sys/stat.h>
  2. #include <unistd.h>
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <sys/socket.h>
  6. #include <sys/types.h>
  7. #include <string.h>
  8. #include <asm/types.h>
  9. #include <linux/netlink.h>
  10. #include <linux/socket.h>
  11. #include <errno.h>

  12. #define MAX_PAYLOAD 1024 // Netlink消息的最大載荷的長度

  13. unsigned int netlink_group_mask(unsigned int group)
  14. {
  15.     return group ? 1 << (group - 1) : 0;
  16. }

  17. int main(int argc, char* argv[])
  18. {
  19.     struct sockaddr_nl src_addr;
  20.     struct nlmsghdr *nlh = NULL;
  21.     struct iovec iov;
  22.     struct msghdr msg;
  23.     int sock_fd, retval;

  24.     // 創建Socket
  25.     sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
  26.     if(sock_fd == -1){
  27.         printf("error getting socket: %s", strerror(errno));
  28.         return -1;
  29.     }

  30.     memset(&src_addr, 0, sizeof(src_addr));
  31.     src_addr.nl_family = PF_NETLINK;
  32.     src_addr.nl_pid = 0; // 表示我們要從內核接收多播消息。注意:該字段爲0有雙重意義,另一個意義是表示我們發送的數據的目的地址是內核。
  33.     src_addr.nl_groups = netlink_group_mask(atoi(argv[1])); // 多播組的掩碼,組號來自我們執行程序時輸入的第一個參數

  34.     // 因爲我們要加入到一個多播組,所以必須調用bind()
  35.     retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
  36.     if(retval < 0){
  37.         printf("bind failed: %s", strerror(errno));
  38.         close(sock_fd);
  39.         return -1;
  40.     }

  41.     // 爲接收Netlink消息申請存儲空間
  42.     nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
  43.     if(!nlh){
  44.         printf("malloc nlmsghdr error!\n");
  45.         close(sock_fd);
  46.         return -1;
  47.     }

  48.     memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
  49.     iov.iov_base = (void *)nlh;
  50.     iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);

  51.     memset(&msg, 0, sizeof(msg));
  52.     msg.msg_iov = &iov;
  53.     msg.msg_iovlen = 1;

  54.     // 從內核接收消息
  55.     printf("waitinf for...\n");
  56.     recvmsg(sock_fd, &msg, 0);
  57.     printf("Received message: %s \n", NLMSG_DATA(nlh));
  58.     
  59.     close(sock_fd);

  60.     return 0;
  61. }

       可以看到,用戶空間的程序基本沒什麼變化,唯一需要格外注意的就是Netlink地址結構體中的nl_groups的設置。由於對它的解釋很少,加之沒有有效的文檔,所以我也是一邊看源碼,一邊在網上搜集資料。有分析不當之處,還請大家幫我指出。

       內核空間我們添加了內核線程和內核線程同步方法completion的使用。內核空間修改後的代碼如下:



點擊(此處)摺疊或打開

  1. #include <linux/kernel.h>
  2. #include <linux/module.h>
  3. #include <linux/skbuff.h>
  4. #include <linux/init.h>
  5. #include <linux/ip.h>
  6. #include <linux/types.h>
  7. #include <linux/sched.h>
  8. #include <net/sock.h>
  9. #include <net/netlink.h> 

  10. MODULE_LICENSE("GPL");
  11. MODULE_AUTHOR("Koorey King");

  12. struct sock *nl_sk = NULL;
  13. static struct task_struct *mythread = NULL; //內核線程對象

  14. //向用戶空間發送消息的接口
  15. void sendnlmsg(char *message/*,int dstPID*/)
  16. {
  17.     struct sk_buff *skb;
  18.     struct nlmsghdr *nlh;
  19.     int len = NLMSG_SPACE(MAX_MSGSIZE);
  20.     int slen = 0;

  21.     if(!message || !nl_sk){
  22.         return;
  23.     }

  24.     // 爲新的 sk_buffer申請空間
  25.     skb = alloc_skb(len, GFP_KERNEL);
  26.     if(!skb){
  27.         printk(KERN_ERR "my_net_link: alloc_skb Error./n");
  28.         return;
  29.     }

  30.     slen = strlen(message)+1;

  31.     //用nlmsg_put()來設置netlink消息頭部
  32.     nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);

  33.     // 設置Netlink的控制塊裏的相關信息
  34.     NETLINK_CB(skb).pid = 0; // 消息發送者的id標識,如果是內核發的則置0
  35.     NETLINK_CB(skb).dst_group = 5; //多播組號爲5,但置成0好像也可以。

  36.     message[slen] = '\0';
  37.     memcpy(NLMSG_DATA(nlh), message, slen+1);

  38.     //通過netlink_unicast()將消息發送用戶空間由dstPID所指定了進程號的進程
  39.     //netlink_unicast(nl_sk,skb,dstPID,0);
  40.     netlink_broadcast(nl_sk, skb, 0,5, GFP_KERNEL); //發送多播消息到多播組5,這裏我故意沒有用1之類的“常見”值,目的就是爲了證明我們上面提到的多播組號和多播組號掩碼之間的對應關係
  41.     printk("send OK!\n");
  42.     return;
  43. }

  44. //每隔1秒鐘發送一條“I am from kernel!”消息,共發10個報文
  45. static int sending_thread(void *data)
  46. {
  47.      int i = 10;
  48.      struct completion cmpl;
  49.      while(i--){
  50.             init_completion(&cmpl);
  51.             wait_for_completion_timeout(&cmpl, 1 * HZ);
  52.             sendnlmsg("I am from kernel!");
  53.      }
  54.      printk("sending thread exited!");
  55.      return 0;
  56. }

  57. static int __init myinit_module()
  58. {
  59.     printk("my netlink in\n");
  60.     nl_sk = netlink_kernel_create(NETLINK_TEST,0,NULL,THIS_MODULE);

  61.     if(!nl_sk){
  62.         printk(KERN_ERR "my_net_link: create netlink socket error.\n");
  63.         return 1;
  64.     }

  65.     printk("my netlink: create netlink socket ok.\n");
  66.     mythread = kthread_run(sending_thread,NULL,"thread_sender");
  67.     return 0;
  68. }

  69. static void __exit mycleanup_module()
  70. {
  71.     if(nl_sk != NULL){
  72.         sock_release(nl_sk->sk_socket);
  73. }
  74. printk("my netlink out!\n");
  75. }

  76. module_init(myinit_module);
  77. module_exit(mycleanup_module);

       關於內核中netlink_kernel_create(int unit, unsigned int groups,…)函數裏的第二個參數指的是我們內核進程最多能處理的多播組的個數,如果該值小於32,則默認按32處理,所以在調用netlink_kernel_create()函數時可以不用糾結第二個參數,一般將其置爲0就可以了。

 

       在skbuff{}結構體中,有個成員叫做"控制塊",源碼對它的解釋如下:


點擊(此處)摺疊或打開

  1. struct sk_buff {
  2.     /* These two members must be first. */
  3.     struct sk_buff        *next;
  4.     struct sk_buff        *prev;
  5.     … …
  6.     /*
  7.      * This is the control buffer. It is free to use for every
  8.      * layer. Please put your private variables there. If you
  9.      * want to keep them across layers you have to do a skb_clone()
  10.      * first. This is owned by whoever has the skb queued ATM.
  11.      */
  12.     char            cb[48];

  13.     … …
  14. }
       當內核態的Netlink發送數據到用戶空間時一般需要填充skbuff的控制塊,填充的方式是通過強制類型轉換,將其轉換成struct netlink_skb_parms{}之後進行填充賦值的:



點擊(此處)摺疊或打開

  1. struct netlink_skb_parms
  2. {
  3.     struct ucred        creds;        /* Skb credentials    */
  4.     __u32            pid;
  5.     __u32            dst_group;
  6.     kernel_cap_t        eff_cap;
  7.     __u32            loginuid;    /* Login (audit) uid */
  8.     __u32            sid;        /* SELinux security id */
  9. };

       填充時的模板代碼如下:


點擊(此處)摺疊或打開

  1. NETLINK_CB(skb).pid=xx;
  2. 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傳遞大數據時的處理等等都是開發人員需要考慮的問題。


       完。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章