Generic Netlink詳解

netlink socket是一種用於用戶態進程和內核態進程之間的通信機制。它通過爲內核模塊提供一組特殊的API,併爲用戶程序提供了一組標準的socket接口的方式,實現了全雙工的通訊連接。

Netlink的特點:

  • 雙向傳輸,異步通信
  • 用戶空間中使用標準socket API
  • 內核空間中使用專門的API
  • 支持多播
  • 可由內核端發起通信
  • 支持32種協議類型

netlink僅支持32種協議類型,這在實際應用中可能並不足夠。因此產生了generic netlink(以下簡稱爲genl)。
generic netlink支持1023個子協議號,彌補了netlink協議類型較少的缺陷。支持協議號自動分配。它基於netlink,但是在內核中,generic netlink的接口與netlink並不相同。

1. Generic Netlink框架概述

圖1表示了Generic Netlink框架。Kernel socket API向用戶空間和內核空間分別提供接口。
Netlink子系統(1)是所有genl通信的基礎。Netlink子系統中收到的所有Generic類型的netlink數據都被送到genl總線(2)上;從內核發出的數據也經由genl總線送至netlink子系統,再打包送至用戶空間。
Generic Netlink控制器(4)作爲內核的一部分,負責動態地分配genl通道(即genl family id),並管理genl任務。genl控制器是一個特殊的genl內核用戶,它負責監聽genl bus上的通信通道。genl通信建立在一系列的通信通道的基礎上,每個genl family對應多個通道,這些通道由genl控制器動態分配。

      +---------------------+      +---------------------+
      | (3) application "A" |      | (3) application "B" |
      +------+--------------+      +--------------+------+
             |                                    |
             \                                    /
              \                                  /
               |                                |
       +-------+--------------------------------+-------+
       |        :                               :       |   user-space
  =====+        :   (5)  Kernel socket API      :       +================
       |        :                               :       |   kernel-space
       +--------+-------------------------------+-------+
                |                               |
          +-----+-------------------------------+----+
          |        (1)  Netlink subsystem            |
          +---------------------+--------------------+
                                |
          +---------------------+--------------------+
          |       (2) Generic Netlink bus            |
          +--+--------------------------+-------+----+
             |                          |       |
     +-------+---------+                |       |
     |  (4) Controller |               /         \
     +-----------------+              /           \
                                      |           |
                   +------------------+--+     +--+------------------+
                   | (3) kernel user "X" |     | (3) kernel user "Y" |
                   +---------------------+     +---------------------+
圖1:generic netlink框架

2 Generic Netlink相關結構體
2.1 genl family

Generic Netlink是基於客戶端-服務端模型的通信機制。服務端註冊family(family是對genl服務的各項定義的集合)。控制器和客戶端都通過已註冊的信息與服務端通信。
genl family的結構體如下:

  1. struct genl_family  
  2. {  
  3.       unsigned int            id;  
  4.       unsigned int            hdrsize;  
  5.       char                    name[GENL_NAMSIZ];  
  6.       unsigned int            version;  
  7.       unsigned int            maxattr;  
  8.       struct nlattr **        attrbuf;  
  9.       struct list_head        ops_list;  
  10.       struct list_head        family_list;  
  11. };  

對此結構體元素具體解釋如下:


* id: family id。當新註冊一個family的時候,應該用GENL_ID_GENERATE宏(0x0),表示請控制器自動爲family分配的一個id。0x10保留供genl控制器使用。
* hdrsize: 用戶自定議頭部長度。即圖2中User Msg的長度。如果沒有用戶自定義頭部,這個值被賦爲0。
* version: 版本號,一般填1即可。
* name: family名,要求不同的family使用不同的名字。以便控制器進行正確的查找。
* maxattr:genl使用netlink標準的attr來傳輸數據。此字段定義了最大attr類型數。(注意:不是一次傳輸多少個attr,而是一共有多少種attr,因此,這個值可以被設爲0,爲0代表不區分所收到的數據的attr type)。在接收數據時,可以根據attr type,獲得指定的attr type的數據在整體數據中的位置。

* struct nlattr **attrbuf
* struct list_head ops_list
* struct list_head family_list
以上的三個字段爲私有字段,由系統自動配置,開發者不需要做配置。


圖2 genl報文與linux中各變量的對應關係


圖3 genl報文格式

2.2 genl_ops 結構體

[html] view plaincopy
  1. struct genl_ops  
  2. {  
  3.       u8                      cmd;  
  4.       unsigned int            flags;  
  5.       struct nla_policy       *policy;  
  6.       int                     (*doit)(struct sk_buff *skb,  
  7.                                       struct genl_info *info);  
  8.       int                     (*dumpit)(struct sk_buff *skb,  
  9.                                           struct netlink_callback *cb);  
  10.       struct list_head        ops_list;  
  11. };  


  •   cmd: 命令名。用於識別各genl_ops
  •   flag: 各種設置屬性,以“或”連接。在需要admin特權級別時,使用GENL_ADMIN_PERM
  •   policy:定義了attr規則。如果此指針非空,genl在觸發事件處理程序之前,會使用這個字段來對幀中的attr做校驗(見nlmsg_parse函數)。該字段可以爲空,表示在觸發事件處理程序之前,不做校驗。

        policy是一個struct nla_policy的數組。struct nla_policy結構體表示如下:

[html] view plaincopy
  1. struct nla_policy  
  2. {  
  3.     u16             type;  
  4.     u16             len;  
  5. };  
        其中,type字段表示attr中的數據類型,可被配置爲:
       NLA_UNSPEC--未定義
       NLA_U8, NLA_U16, NLA_U32, NLA_U64爲8bits, 16bits, 32bits, 64bits的無符號整型
       NLA_STRING--字符串
       NLA_NUL_STRING--空終止符字符串
       NLA_NESTED--attr流

       len字段的意思是:如果在type字段配置的是字符串有關的值,要把len設置爲字符串的最大長度(不包含結尾的'\0')。如果type字段未設置或被設置爲NLA_UNSPEC,那麼這裏要設置爲attr的payload部分的長度。


  • doit:這是一個回調函數。在generic netlink收到數據時觸發,運行在進程上下文。

        doit傳入兩個參數,skb爲觸發此回調函數的socket buffer。第二個參數是一個genl_info結構體,定義如下:

  1. struct genl_info  
  2. {  
  3.      u32                     snd_seq;  
  4.      u32                     snd_pid;  
  5.      struct nlmsghdr *       nlhdr;  
  6.      struct genlmsghdr *     genlhdr;  
  7.      void *                  userhdr;  
  8.      struct nlattr **        attrs;  
  9. };  












                 * snd_seq:發送序號                 

                 * snd_pid:發送客戶端的PID                

                 * nlhdr:netlink header的指針                 

                 * genlmsghdr:genl頭部的指針(即family頭部)                 

                 * userhdr:用戶自定義頭部指針                 

                * attrs:attrs,如果定義了genl_ops->policy,這裏的attrs是被policy過濾以後的結果。

在完成了操作以後,如果執行正確,返回0;否則,返回一個負數。(一定要有返回值,不能不返回)負數的返回值會觸發NLMSG_ERROR消息。當genl_ops的flag標誌被添加了NLMSG_ERROR時,即使doit返回0,也會觸發NLMSG_ERROR消息。

  • dumpit

這是一個回調函數,當genl_ops的flag標誌被添加了NLM_F_DUMP以後,每次收到genl消息即會回觸發這個函數。dumpit與doit的區別是:dumpit的第一個參數skb不會攜帶從客戶端發來的數據。相反地,開發者應該在skb中填入需要傳給客戶端的數據,然後,並skb的數據長度(可以用skb->len)return。skb中攜帶的數據會被自動送到客戶端。只要dumpit的返回值大於0,dumpit函數就會再次被調用,並被要求在skb中填入數據。當服務端沒有數據要傳給客戶端時,dumpit要返回0。如果函數中出錯,要求返回一個負值。關於doit和dumpit的觸發過程,可以查看源碼中的genl_rcv_msg函數。

  • ops_list

爲私有字段,由系統自動配置,開發者不需要做配置。

3 Generic Netlink服務端(內核)初始化

初始化Generic Netlink的過程分爲以下四步:定義family,定義operation,註冊family,註冊operation。下面通過一個簡單例子來說明如何完成Generic Netlink的初始化。我們首先創建一個genl_family結構體的實例。我們在這裏定義一個名爲"DOC_EXMPL"的family

  1. /* attribute type */  
  2. enum {  
  3.       DOC_EXMPL_A_UNSPEC,  
  4.       DOC_EXMPL_A_MSG,  
  5.       __DOC_EXMPL_A_MAX,  
  6. };  
  7. #define DOC_EXMPL_A_MAX (__DOC_EXMPL_A_MAX - 1)  
  8.   
  9. /* family definition */  
  10. static struct genl_family doc_exmpl_gnl_family = {  
  11.       .id = GENL_ID_GENERATE,  
  12.       .hdrsize = 0,  
  13.       .name = "DOC_EXMPL",  
  14.       .version = 1,  
  15.       .maxattr = DOC_EXMPL_A_MAX,  
  16.   
  17. };  


以上,我們定義了一個僅有一種attribuste type的family。.id被配置爲GENL_ID_GENERATE,指示genl控制器自動分配一個id。

第二步爲family創建operations。我們至少要創建一個genl_ops結構體的實例。

  1. /* doit handler */  
  2. int <span>genl_recv_doit</span>(struct sk_buff *skb, struct genl_info *info)  
  3. {  
  4.       /* message handling code goes here; return 0 on success, negative 
  5.        * values on failure */  
  6. }  
  7.   
  8.   
  9. /* attribute policy */  
  10. static struct nla_policy doc_exmpl_genl_policy = [DOC_EXMPL_A_MAX + 1] = {  
  11.       [DOC_EXMPL_A_MSG] = { .type = NLA_NUL_STRING },  
  12. }  
  13.   
  14. /* commands */  
  15. enum {  
  16.       DOC_EXMPL_C_UNSPEC,  
  17.       DOC_EXMPL_C_ECHO,  
  18.       __DOC_EXMPL_C_ECHO,  
  19. };  
  20. #define DOC_EXMPL_C_MAX (__DOC_EXMPL_C_MAX - 1)  
  21.   
  22. /* operation definition */  
  23. struct genl_ops doc_exmpl_gnl_ops_echo = {  
  24.       .cmd = DOC_EXMPL_C_ECHO,  
  25.       .flags = 0,  
  26.       .policy = doc_exmpl_genl_policy,  
  27.       .doit = genl_recv_doit,  
  28.       .dumpit = NULL,  
  29. }  


這裏,我們把attribute policy設爲NLA_NUL_STRING,表示attr中數據的屬性爲無NULL結尾的字符串。控制器在收到數據時會自動完成這一類型檢查。

我們定義一個operation,它的id爲DOC_EXMPL_C_ECHO,把上述的policy配置給它。一旦本family的genl消息在被總到genl總線上,doit函數(doc_exmpl_echo)會被調用。

接下來兩步是註冊family和註冊operations。

genl_register_family(&doc_exmpl_gnl_family);

genl_register_ops(&doc_exmpl_gnl_family, &doc_exmpl_gnl_ops_echo);

在完成genl操作後,記對完成對family的註銷操作。

genl_unregister_family(&doc_exmpl_gnl_family);


4 Generic Netlink客戶端(用戶空間)初始化


Generic Netlink在用戶空間的初始化和通常的socket通信一致。大致分爲兩步,創建socket,把socket綁定到地址上(bind)。

下面也通過一個例子簡要說明一下用戶空間genl初始化的過程。

  1. struct sockaddr_nl saddr;  
  2. int                sock;  
  3. sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);  
  4.   
  5. if (sock < 0) {  
  6.     return -1;  
  7. }  
  8.   
  9. memset(&saddr, 0, sizeof(saddr));  
  10. saddr.nl_family = AF_NETLINK;  
  11. saddr.nl_pid = getpid();  
  12. if (bind(sock, (struct sockaddr*)&saddr, sizeof(saddr)) < 0) {  
  13.     printf("bind fail!\n");  
  14.     close(*p_sock);  
  15.     return -1;  
  16. }  


上述代碼中,我們先創建一個socket,注意,第一個參數必須爲AF_NETLINK 或 PF_NETLINK,表示創建netlink socket,第二個參數必須是SOCK_RAW或SOCK_DGRAM, 第三個參數指定netlink協議類型,我們要使用generic netlink,那麼就要將其設置爲:NETLINK_GENERIC。

接下來,對於genl不可缺少的一步就是獲取family id。family id是服務端註冊family時,由控制器自動分配的。此時客戶端尚不知道family id爲多少,因此需要向客戶端請求family id。

下面是一段獲取family id的函數

  1. static int genl_get_family_id(int sd, char *family_name)  
  2. {  
  3.     msgtemplate_t ans;  
  4.     int id, rc;  
  5.     struct nlattr *na;  
  6.     int rep_len;  
  7.   
  8.     rc = genl_send_msg(sd, GENL_ID_CTRL, 0, CTRL_CMD_GETFAMILY, 1,  
  9.                     CTRL_ATTR_FAMILY_NAME, (void *)family_name,  
  10.                     strlen(family_name)+1);  
  11.   
  12.     rep_len = recv(sd, &ans, sizeof(ans), 0);  
  13.     if (rep_len < 0) {  
  14.         return 0;  
  15.     }  
  16.     if (ans.n.nlmsg_type == NLMSG_ERROR || !NLMSG_OK((&ans.n), rep_len)) {  
  17.         return 0;  
  18.     }  
  19.   
  20.     na = (struct nlattr *) GENLMSG_DATA(&ans);  
  21.     na = (struct nlattr *) ((char *) na + NLA_ALIGN(na->nla_len));  
  22.     if (na->nla_type == CTRL_ATTR_FAMILY_ID) {  
  23.         id = *(__u16 *) NLA_DATA(na);  
  24.     } else {  
  25.         id = 0;  
  26.     }  
  27.   
  28.     return id;  
  29. }  


在這個函數中,調用genl_send_msg(這個函數會在下文中介紹並給出源碼)發送請求family id的消息,並調用recv接收服務端的反饋消息。這個消息中即包含了family id。

這個函數的第一個參數是已創建好的socket。第二個參數是family name,注意這裏family name需要與服務端註冊famile時的name字段一致。該函數返回值即是family id以下是一個調用示例。
int fid = genl_get_family_id(sock, "DOC_EXMPL");

5 Generic Netlink通信
這一節對如何使用Generic Netlink完成內核空間與用戶空間的通信做介紹。並把我的示例代碼貢獻出來。

示例代碼呈現了內核(服務端)和用戶空間(客戶端)收發數據的過程。

5.1 內核發送數據

以下是內核端發送數據的源碼。在genl_msg_send_to_user中,調用genl_msg_prepare_usr_msg和genl_msg_mk_usr_msg來準備socket buffer,爲數據加上各種數據頭(參考圖2)。genlmsg_end把整個數據打包完成,通過genlmsg_unicast完成單播發送。

  1. /**  
  2. * genl_msg_send_to_user - 通過generic netlink發送數據到netlink  
  3. * 
  4. * @data: 發送數據緩存 
  5. * @len:  數據長度 單位:byte 
  6. * @pid:  發送到的客戶端pid <span style="color:#FF0000;"><strong>這個pid要從用戶空間發來數據觸發的doit中的info->snd_pid參數獲得</strong></span> 
  7. * 
  8. * return:  
  9. *    0:       成功  
  10. *    -1:      失敗 
  11. */  
  12. int genl_msg_send_to_user(void *data, int len, pid_t pid)  
  13. {  
  14.     struct sk_buff *skb;  
  15.     size_t size;  
  16.     void *head;  
  17.     int rc;  
  18.   
  19.     size = nla_total_size(len); /* total length of attribute including padding */  
  20.   
  21.     rc = genl_msg_prepare_usr_msg(DOC_EXMPL_C_ECHO, size, pid, &skb);  
  22.   
  23.     if (rc) {  
  24.         return rc;  
  25.     }  
  26.   
  27.     rc = genl_msg_mk_usr_msg(skb, DOC_EXMPL_A_MSG, data, len);  
  28.   
  29.     if (rc) {  
  30.         kfree_skb(skb);  
  31.         return rc;  
  32.     }  
  33.   
  34.     head = genlmsg_data(nlmsg_data(nlmsg_hdr(skb)));  
  35.   
  36.     rc = genlmsg_end(skb, head);  
  37.     if (rc < 0) {  
  38.         kfree_skb(skb);  
  39.         return rc;  
  40.     }  
  41.   
  42.     rc = genlmsg_unicast(&init_net, skb, pid);  
  43.     if (rc < 0) {  
  44.         return rc;  
  45.     }  
  46.   
  47.     return 0;  
  48. }  
  49.   
  50.   
  51. static inline int genl_msg_mk_usr_msg(struct sk_buff *skb, int type, void *data, int len)  
  52. {  
  53.     int rc;  
  54.   
  55.     /* add a netlink attribute to a socket buffer */  
  56.     if ((rc = nla_put(skb, type, len, data)) != 0) {  
  57.         return rc;  
  58.     }  
  59.     return 0;  
  60. }  
  61.   
  62. static inline int genl_msg_prepare_usr_msg(u8 cmd, size_t size, pid_t pid, struct sk_buff **skbp)  
  63. {  
  64.     struct sk_buff *skb;  
  65.   
  66.     /* create a new netlink msg */  
  67.     skb = genlmsg_new(size, GFP_KERNEL);  
  68.     if (skb == NULL) {  
  69.         return -ENOMEM;  
  70.     }  
  71.   
  72.     /* Add a new netlink message to an skb */  
  73.     genlmsg_put(skb, pid, 0, &genl_family, 0, cmd);  
  74.   
  75.     *skbp = skb;  
  76.     return 0;  
  77. }  

5.2 用戶空間接收數據
客戶端調用通用的recv函數即可完成從內核來的數據的接收。需要注意的是,接收到的數據包含幾級的header(圖3),我們需要準確地定位到我們所需數據的位置。

當沒有用戶自定義頭部(即圖3中的User Msg,在註冊family時把hdrsize置0)時,可以構建這樣的數據結構用於接收數據。這樣,收到的數據中的netlink header和genl header就被很容易地剝離開來。

[html] view plaincopy
  1. typedef struct msgtemplate {  
  2.     struct nlmsghdr n;  
  3.     struct genlmsghdr g;  
  4.     char data[MAX_MSG_SIZE];  
  5. } msgtemplate_t;  


下面是客戶端接收數據函數的源碼:

  1. #define GENLMSG_DATA(glh)       ((void *)(NLMSG_DATA(glh) + GENL_HDRLEN))  
  2. #define NLA_DATA(na)            ((void *)((char *)(na) + NLA_HDRLEN))  
  3.   
  4. void genl_rcv_msg(int fid, int sock, char **string)  
  5. {  
  6.     int ret;  
  7.     struct msgtemplate msg;  
  8.     struct nlattr *na;  
  9.   
  10.     ret = recv(sock, &msg, sizeof(msg), 0);  
  11.     if (ret < 0) {  
  12.         return;  
  13.     }  
  14.     //printf("received length %d\n", ret);  
  15.     if (msg.n.nlmsg_type == NLMSG_ERROR || !NLMSG_OK((&msg.n), ret)) {  
  16.         return;  
  17.     }  
  18.     if (msg.n.nlmsg_type == fid && fid != 0) {  
  19.         na = (struct nlattr *) GENLMSG_DATA(&msg);  
  20.         *string = (char *)NLA_DATA(na);  
  21.     }   
  22. }  


以上函數中,第一個參數爲family id,第二個參數爲socket,第三個參數爲待接收數據的buffer。

5.3 用戶空間發送數據
客戶端發送數據簡單地說就是調用通用的socket API---sendto來發送數據

  1. /**  
  2. * genl_send_msg - 通過generic netlink給內核發送數據  
  3. * 
  4. * @sd: 客戶端socket  
  5. * @nlmsg_type: family_id 
  6. * @nlmsg_pid: 客戶端pid 
  7. * @genl_cmd: 命令類型 
  8. * @genl_version: genl版本號 
  9. * @nla_type: netlink attr類型 
  10. * @nla_data: 發送的數據 
  11. * @nla_len: 發送數據長度 
  12. * 
  13. * return:  
  14. *    0:       成功  
  15. *    -1:      失敗 
  16. */  
  17. int genl_send_msg(int sd, u_int16_t nlmsg_type, u_int32_t nlmsg_pid,  
  18.         u_int8_t genl_cmd, u_int8_t genl_version, u_int16_t nla_type,  
  19.         void *nla_data, int nla_len)  
  20. {  
  21.     struct nlattr *na;  
  22.     struct sockaddr_nl nladdr;  
  23.     int r, buflen;  
  24.     char *buf;  
  25.     msgtemplate_t msg;  
  26.   
  27.   
  28.     if (nlmsg_type == 0) {  
  29.         return 0;  
  30.     }  
  31.   
  32.     msg.n.nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);  
  33.     msg.n.nlmsg_type = nlmsg_type;  
  34.     msg.n.nlmsg_flags = NLM_F_REQUEST;  
  35.     msg.n.nlmsg_seq = 0;  
  36.     /* 
  37.      * nlmsg_pid是發送進程的端口號。 
  38.      * Linux內核不關心這個字段,僅用於跟蹤消息。 
  39.      */  
  40.     msg.n.nlmsg_pid = nlmsg_pid;  
  41.     msg.g.cmd = genl_cmd;  
  42.     msg.g.version = genl_version;  
  43.     na = (struct nlattr *) GENLMSG_DATA(&msg);  
  44.     na->nla_type = nla_type;  
  45.     na->nla_len = nla_len + 1 + NLA_HDRLEN;  
  46.     memcpy(NLA_DATA(na), nla_data, nla_len);  
  47.     msg.n.nlmsg_len += NLMSG_ALIGN(na->nla_len);  
  48.   
  49.     buf = (char *) &msg;  
  50.     buflen = msg.n.nlmsg_len ;  
  51.     memset(&nladdr, 0, sizeof(nladdr));  
  52.     nladdr.nl_family = AF_NETLINK;  
  53.     while ((r = sendto(sd, buf, buflen, 0, (struct sockaddr *) &nladdr  
  54.             , sizeof(nladdr))) < buflen) {  
  55.         if (r > 0) {  
  56.             buf += r;  
  57.             buflen -= r;  
  58.         } else if (errno != EAGAIN) {  
  59.             return -1;  
  60.         }  
  61.     }  
  62.     return 0;  
  63. }  


5.4 內核接收數據


內核端一旦收到generic netlink數據,會觸發doit函數運行(上文第3節有提及doit的初始化方法)。

doit傳入兩個參數,skb即是接收到的數據,info包含了Genl消息的一些常用指針。這兩個結構體字段詳見內核源碼。

skb收到的數據還包括了多層的包頭,以下程序中的nlmsg_hdr,nlmsg_data,genlmsg_data,nla_data即是把這些包頭層層剝開,para->string指向的數據即是用用戶空間傳來的“純數據”。

  1. int genl_recv_doit(struct sk_buff *skb, struct genl_info *info)  
  2. {  
  3.     /* doit 沒有運行在中斷上下文 */  
  4.     static int          kthread_num = 0;   
  5.     struct nlmsghdr     *nlhdr;  
  6.     struct genlmsghdr   *genlhdr;  
  7.     struct nlattr       *nlh;  
  8.     struct thread_para  *para;              /* 給線程傳遞參數的結構體 */  
  9.       
  10.      nlhdr = nlmsg_hdr(skb);  
  11.      genlhdr = nlmsg_data(nlhdr);  
  12.      nlh = genlmsg_data(genlhdr);  
  13.     /* 配置給新開線程所傳的參數 */  
  14.     /* para 在線程函數thread_string_proc中釋放 */  
  15.     para = (struct thread_para  *)kmalloc(sizeof(struct thread_para), GFP_KERNEL);  
  16.     para->string = nla_data(nlh);  
  17.     para->pid = nlhdr->nlmsg_pid;  
  18.       
  19.     /* 每收到一個字符串開闢一個線程 */  
  20.     kthread_run(thread_string_proc, (void *)(para), "kthread %d", kthread_num++);   
  21.       
  22.     return 0;  
  23. }  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章