1. 程式人生 > >四、無線資訊傳遞——Generic netlink(二、通訊)

四、無線資訊傳遞——Generic netlink(二、通訊)

系列說明

  仍舊是系列二:

  • 1、無線驅動資訊傳遞框架:說明無線資訊傳遞的步驟流程以及各程式塊之間的聯絡;
  • 2、generic Netlink訊號傳遞機制:hostapd與無線驅動之間的資訊傳遞機制
  • 3、以ssid為例說明使用者將user space中的ssid配置內容傳遞至kernel space的流程:以此係統地瞭解整個無線資訊傳遞流程。

  上一篇文章是對generic netlink的初始化和主要結構體,介面的認識。這一篇文章將主要以一個例子來系統地說明Generic netlink程式碼實現和原理,以便後續實際工作中的應用。
!!!這一篇文章同樣也是參考前一篇文章中的連結內容加上一些自己的理解,需要真正瞭解的可以點選上一章的連結。

這一章的主要說明內容:

  • 1、實現例子功能說明;
  • 2、核心程式碼中Generic netlink的建立;
  • 3、應用層中Generic netlink的程式碼建立;
  • 4、應用層與核心層的通訊程式碼流程說明。

一、例子功能說明

  應用層程式接收使用者的輸入“字串”和“資料”向核心傳送,核心接收後回發應用層,應用層通過終端列印輸出。

二、核心程式碼中Generic netlink的建立示例

  generic netlink在核心程式碼中的實現分為以下幾步:

  • 1、定義Genetlink——主要定義上文中的兩個結構體,分別為genl_family和genl_ops(這裡先對核心為傳送端,封包進行說明;接收後續說明);
  • 2、核心註冊Genetlink——主要對核心中Genetlink的介面呼叫流程說明;(同樣也僅對核心為傳送端,封包的介面說明)

1.2、定義Genetlink結構體

  在前文中已經對主要的4個Genetlink結構體進行了說明,genlmsghdr,genl_family,genl_ops和genl_info。
  因為這裡只說明核心端作為傳送端的結構體定義,所以用於解析genetlink訊息頭的genlmsghdr和用於處理接收訊息的這裡在先不進行說明。主要是對genl_family和genl_ops兩個介面說明。

定義例子使用的family:

enum {
    DEMO_CMD_ATTR_UNSPEC = 0
, DEMO_CMD_ATTR_MESG, /* demo message */ //msg屬性 DEMO_CMD_ATTR_DATA, /* demo data */ //data屬性 __DEMO_CMD_ATTR_MAX, }; #define DEMO_CMD_ATTR_MAX (__DEMO_CMD_ATTR_MAX - 1) static const struct nla_policy demo_cmd_policy[DEMO_CMD_ATTR_MAX+1] = { [DEMO_CMD_ATTR_MESG] = { .type = NLA_STRING }, //定義msg型別為NLA_STRING(字串型別) [DEMO_CMD_ATTR_DATA] = { .type = NLA_S32 }, //定義data型別為NLA_S32(有符號32位數) }; static struct genl_family demo_family = { .id = GENL_ID_GENERATE, //表示由核心統一分配 .name = DEMO_GENL_NAME, //核心的唯一genetlink標誌名 .version = DEMO_GENL_VERSION, //版本號(可以為1,2) .maxattr = DEMO_CMD_ATTR_MAX, //核心由此決定為其分配快取空間大小 };

定義例子使用的genl_ops:

enum {
    DEMO_CMD_UNSPEC = 0, /* Reserved */
    DEMO_CMD_ECHO,   /* user->kernel request/get-response */    //定義顯示cmd的id號
    DEMO_CMD_REPLY,   /* kernel->user event */          //定義返回cmd的id號
    __DEMO_CMD_MAX,
};
#define DEMO_CMD_MAX (__DEMO_CMD_MAX - 1)

static const struct genl_ops demo_ops[] = {
    {
        .cmd  = DEMO_CMD_ECHO,                  //接收到的判斷id(DEMO_CMD_ECHO)
        .doit  = demo_echo_cmd,                 //核心對應命令的執行回撥函式
        .policy  = demo_cmd_policy,             //執行策略,上一模組的demo_cmd_policy中有說明
        .flags  = GENL_ADMIN_PERM,              //定義所需許可權為admin
    },
};

  核心註冊中genl_register_family_with_ops為主要入口。

static int __init demo_genetlink_init(void) 
{  
    int ret;  
    pr_info("demo generic netlink module %d init...\n", DEMO_GENL_VERSION);  

    ret = genl_register_family_with_ops(&demo_family, demo_ops);            //註冊family和ops(由於genl_register_family_with_ops在前一文中已經說明,相同的不再贅述,所以這裡主要說明不是組播發送的操作)
    if (ret != 0) {  
        pr_info("failed to init demo generic netlink example module\n");  
        return ret;
    }  
    pr_info("demo generic netlink module init success\n");  
    return 0; 
}  
//其後genl_register_family_with_ops呼叫genl_register_family和genl_register_ops
//而genl_register_family和genl_register_ops中呼叫genl_ctrl_event,這個介面對是不是組播發送會有所區別,在下面的程式碼中進行說明
/*
 * 在傳送組播時:
 *        _genl_register_family_with_ops_grps呼叫genl_ctrl_event的方式是
 * 
 *           genl_ctrl_event(CTRL_CMD_NEWFAMILY, family, NULL, 0);
 *           for (i = 0; i < family->n_mcgrps; i++)
 *                 genl_ctrl_event(CTRL_CMD_NEWMCAST_GRP, family, &family->mcgrps[i], family->mcgrp_offset + i);
 * 傳送指定程式時只調用用一次的genl_ctrl_event()
*/
static int genl_ctrl_event(int event, struct genl_family *family,
      const struct genl_multicast_group *grp,
      int grp_id)
{
    struct sk_buff *msg;
    /* genl is still initialising */
    if (!init_net.genl_sock)                    //判斷是否已經註冊了控制器CTRL簇
        return 0;
    switch (event) {
    case CTRL_CMD_NEWFAMILY:
    case CTRL_CMD_DELFAMILY:
        WARN_ON(grp);
        msg = ctrl_build_family_msg(family, 0, 0, event);   //建立單播控制器family
        break;
    case CTRL_CMD_NEWMCAST_GRP:
    case CTRL_CMD_DELMCAST_GRP:
        BUG_ON(!grp);
        msg = ctrl_build_mcgrp_msg(family, grp, grp_id, 0, 0, event);   //建立組播控制器family
        break;
    default:
        return -EINVAL;

    //下面的genlmsg_multicast_netns和genlmsg_multicast_allns介面,不論哪一種情況,最後都是呼叫nlmsg_multicast()函式。不過這裡有一點需要注意的就是這裡的第三個入參portid為0,這是為了防止向傳送端傳送報文,這也就表明核心控制器簇套接字是不會接受該廣播報文的(核心也不應該接收,否則會讓核心“恐慌”)。
    if (IS_ERR(msg))
        return PTR_ERR(msg);
    if (!family->netnsok) {                     //根據是否支援net名稱空間來選擇傳送的流程
        genlmsg_multicast_netns(&genl_ctrl, &init_net, msg, 0, 0, GFP_KERNEL);  //指定了向init_net傳送
    } else {
        rcu_read_lock();
        genlmsg_multicast_allns(&genl_ctrl, msg, 0, 0, GFP_ATOMIC); //會向所有名稱空間的控制器簇傳送訊息
        rcu_read_unlock();
    }
}
static struct sk_buff *ctrl_build_family_msg(struct genl_family *family,
          u32 portid, int seq, u8 cmd)
{
    struct sk_buff *skb; 
    int err;
    skb = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL); //建立netlink型別的skb,引數分別為訊息長度和記憶體空間分配型別
    if (skb == NULL)
        return ERR_PTR(-ENOBUFS);
    err = ctrl_fill_info(family, portid, seq, 0, skb, cmd); //填充資料包資料
    if (err < 0) {
        nlmsg_free(skb);
        return ERR_PTR(err);
    }
    return skb;
}

//從下面的下層函式呼叫中可以看出:總共預留的空間為NLMSG_ALIGN(NLMSG_HDRLEN+NLMSG_DEFAULT_SIZE)
static inline struct sk_buff *nlmsg_new(size_t payload, gfp_t flags)
{
    return alloc_skb(nlmsg_total_size(payload), flags);
}

static inline int nlmsg_total_size(int payload)
{
    return NLMSG_ALIGN(nlmsg_msg_size(payload));
}

static inline int nlmsg_msg_size(int payload)
{
    return NLMSG_HDRLEN + payload;
}

  下面進行說明主要的介面ctrl_fill_info進行資料包的填充資料的程式碼分段說明,程式說明時中間會穿插說明呼叫到的介面。最後結合資料包框圖。

/*
 * 引數講解:
 *       family:為新註冊的demo_family;
 *       portid: 表示訊息的傳送端為核心;
 *       seq:  傳送訊息序號為0;
 *       cmd:   命令id號,當前呼叫到的命令號為CTRL_CMD_NEWFAMILY
*/
static int ctrl_fill_info(struct genl_family *family, u32 portid, u32 seq,
     u32 flags, struct sk_buff *skb, u8 cmd)
{
    void *hdr;
    hdr = genlmsg_put(skb, portid, seq, &genl_ctrl, flags, cmd);        //初始化netlink訊息頭和genetlink訊息頭(下部分程式碼說明)
    if (hdr == NULL)
        return -1;

  穿插說明genlmsg_put介面

void *genlmsg_put(struct sk_buff *skb, u32 portid, u32 seq,
    struct genl_family *family, int flags, u8 cmd)
{
    struct nlmsghdr *nlh;
    struct genlmsghdr *hdr;
    nlh = nlmsg_put(skb, portid, seq, family->id, GENL_HDRLEN + family->hdrsize, flags); //向skb緩衝區中獲取訊息頭空間並且初始化netlink訊息頭,第5個引數為genetlink訊息頭和使用者私有訊息頭(這裡並未使用)的總空間,,實際呼叫的函式為__nlmsg_put()
    if (nlh == NULL)
        return NULL;

    hdr = nlmsg_data(nlh); //通過巨集nlmsg_data獲取genetlink訊息頭的地址,前文說過genetlink訊息頭中包含genl_family內容
    hdr->cmd = cmd; //訊息的cmd命令,例子中為CTRL_CMD_NEWFAMILY;
    hdr->version = family->version; //genl_ctrl family簇的version
    hdr->reserved = 0; //0
    return (char *) hdr + GENL_HDRLEN; //返回訊息使用者私有頭(若有)或實際載荷的首地址

}

struct nlmsghdr * __nlmsg_put(struct sk_buff *skb, u32 portid, u32 seq, int type, int len, int flags)
{
    struct nlmsghdr *nlh;
    int size = nlmsg_msg_size(len);
    nlh = (struct nlmsghdr *)skb_put(skb, NLMSG_ALIGN(size)); //分配的空間大小為size = 傳入的len長度 + netlink訊息頭的長度
    nlh->nlmsg_type = type; //例子中:核心genl_ctrl family簇的ID 號GENL_ID_CTRL
    nlh->nlmsg_len = size; //例子中:訊息長度,即genetlink頭+使用者私有頭+netlink頭的長度總和
    nlh->nlmsg_flags = flags; //例子中:0
    nlh->nlmsg_pid = portid; //例子中:傳送端的ID號為0,表示由核心傳送
    nlh->nlmsg_seq = seq; //例子中:0
    if (!__builtin_constant_p(size) || NLMSG_ALIGN(size) - size != 0) //將記憶體對齊用的空白區刷為0
        memset(nlmsg_data(nlh) + len, 0, NLMSG_ALIGN(size) - size);
    return nlh;
}

  至此完成資料包的netlink頭和genetlink頭填充操作,如下圖所示:
這裡寫圖片描述
  接下來進行填充實際資料,仍然是ctrl_fill_info介面接下來的程式碼。

//將新註冊的family結構中的幾個欄位都填充到了訊息中,包括name、id號、版本號、私有頭長度以及maxattr(注意屬性需要一一對應)
if (nla_put_string(skb, CTRL_ATTR_FAMILY_NAME, family->name) || //呼叫的函式nla_put_string、nla_put_u16和nla_put_u32都是nla_put()的封裝
     nla_put_u16(skb, CTRL_ATTR_FAMILY_ID, family->id) || //nla_put實際呼叫的是__nla_put(),下面進行說明
     nla_put_u32(skb, CTRL_ATTR_VERSION, family->version) ||
     nla_put_u32(skb, CTRL_ATTR_HDRSIZE, family->hdrsize) ||
     nla_put_u32(skb, CTRL_ATTR_MAXATTR, family->maxattr))
  goto nla_put_failure;

  穿插說明nla_put介面

int nla_put(struct sk_buff *skb, int attrtype, int attrlen, const void *data)
{
 if (unlikely(skb_tailroom(skb) < nla_total_size(attrlen)))
 return -EMSGSIZE;
 __nla_put(skb, attrtype, attrlen, data);
 return 0;
}

//作用是向skb中新增一個netlink attr屬性
//引數說明:
// 1、skb地址
// 2、要新增的attr屬性型別
// 3、屬性長度
// 4、屬性實際資料
void __nla_put(struct sk_buff *skb, int attrtype, int attrlen,
        const void *data)
{
    struct nlattr *nla;
    nla = __nla_reserve(skb, attrtype, attrlen); //在skb中預留出attr屬性的記憶體空間
    memcpy(nla_data(nla), data, attrlen);
}

//呼叫完此介面後,一個attr屬性就新增完成了
struct nlattr *__nla_reserve(struct sk_buff *skb, int attrtype, int attrlen)
{
    struct nlattr *nla;
    nla = (struct nlattr *) skb_put(skb, nla_total_size(attrlen)); //首先預留空間長度為nla_total_size(attrlen),即attrlen+NLA_HDRLEN(屬性頭長度)+對齊用記憶體空白
    nla->nla_type = attrtype; //attr屬性,即前文中的CTRL_ATTR_FAMILY_NAME等
    nla->nla_len = nla_attr_size(attrlen); //attr屬性長度(attrlen+NLA_HDRLEN);
    memset((unsigned char *) nla + nla->nla_len, 0, nla_padlen(attrlen)); //將attr屬性中的實際資料拷貝到預留測空間中
    return nla;
}

  如此,一個數據包中的attr內容新增完成,如下圖所示:
這裡寫圖片描述
  回到ctrl_fill_info介面中:

    if (family->n_ops) {                        //如果新註冊的family簇也同時註冊了操作介面operations
        struct nlattr *nla_ops;
        int i;
        nla_ops = nla_nest_start(skb, CTRL_ATTR_OPS);       //追加上對應的attr屬性引數,但同前面不同的是,這裡追加的attr引數是“打包”在一起的,使用的屬性為CTRL_ATTR_OPS,接下來沒呼叫nla_put介面新增cmd或者flag資料.由於netlink的attr屬性是支援多級巢狀的,所以這裡的“打包”指的就是新建一級巢狀,呼叫nla_nest_start介面就是這個目的(下面說明)
        if (nla_ops == NULL)
            goto nla_put_failure;
        for (i = 0; i < family->n_ops; i++) {
            struct nlattr *nest;
            const struct genl_ops *ops = &family->ops[i];

            //根據operations中實現的回撥函式封裝flag
            u32 op_flags = ops->flags;
            if (ops->dumpit)
                op_flags |= GENL_CMD_CAP_DUMP;
            if (ops->doit)
                op_flags |= GENL_CMD_CAP_DO;
            if (ops->policy)
                op_flags |= GENL_CMD_CAP_HASPOL;
            nest = nla_nest_start(skb, i + 1);      //與上面的nla_nest_start不同,這裡呼叫nla_nest_start再次建立新的一級巢狀,結合下面呼叫的nla_put_u32可以看出,一個attrtype後面跟一個命令和一個flag
            if (nest == NULL)
                goto nla_put_failure;
            if (nla_put_u32(skb, CTRL_ATTR_OP_ID, ops->cmd) || nla_put_u32(skb, CTRL_ATTR_OP_FLAGS, op_flags))
                goto nla_put_failure;
            nla_nest_end(skb, nest);            //呼叫nla_nest_end結束這一層attr巢狀
        }
        nla_nest_end(skb, nla_ops);             //依舊呼叫nla_nest_end來結束CTRL_ATTR_OPS的那一層attr巢狀
    }

  中間穿插nla_nest_start介面

static inline struct nlattr *nla_nest_start(struct sk_buff *skb, int attrtype)
{
    struct nlattr *start = (struct nlattr *)skb_tail_pointer(skb);
    if (nla_put(skb, attrtype, 0, NULL) < 0)            //依然是nla_put()函式,不過這裡的入參中指定的attr長度為0,然後資料為NULL,這裡其實就是向skb中添加了一段attr屬性頭,然後指定它的屬性nla_type為CTRL_ATTR_OPS,屬性nla_len為0
        return NULL;
    return start;                           //注意函式返回的是新增巢狀attr頭之前的訊息有效資料末尾地址
}
//更新這個巢狀的attr屬性頭的nla_len欄位為本巢狀屬性的實際長度,實現的方式為當前的訊息末尾地址減去建立該級巢狀之前的訊息末尾地址(這就是nla_nest_start()函式要返回start地址的原因了)。
static inline int nla_nest_end(struct sk_buff *skb, struct nlattr *start)
{
    start->nla_len = skb_tail_pointer(skb) - (unsigned char *)start;
    return skb->len;
}

  此時,完成了資料包中的attr資料填充,如下圖所示:
這裡寫圖片描述
  接下來,ctrl_fill_info會再進行判斷family->n_mcgrps欄位,若存在組播組,會同operations一樣增加一級和operations平級的attr巢狀然後新增CTRL_ATTR_MCAST_GROUPS屬性。如下程式碼所示,這裡就不贅述了。

        if (!list_empty(&family->mcast_groups)) {
            struct genl_multicast_group *grp;
            struct nlattr *nla_grps;
            int idx = 1;
            nla_grps = nla_nest_start(skb, CTRL_ATTR_MCAST_GROUPS);
            if (nla_grps == NULL)
                goto nla_put_failure;
            list_for_each_entry(grp, &family->mcast_groups, list) {
                struct nlattr *nest;
                nest = nla_nest_start(skb, idx++);
                if (nest == NULL)
                    goto nla_put_failure;
                NLA_PUT_U32(skb, CTRL_ATTR_MCAST_GRP_ID, grp->id);
                NLA_PUT_STRING(skb, CTRL_ATTR_MCAST_GRP_NAME, grp->name);
                nla_nest_end(skb, nest);
        }
        nla_nest_end(skb, nla_grps);
    }
    return genlmsg_end(skb, hdr); //最後呼叫genlmsg_end完成本次封裝
nla_put_failure:
    genlmsg_cancel(skb, hdr);
    return -EMSGSIZE;
}
static inline void genlmsg_end(struct sk_buff *skb, void *hdr)
{
    nlmsg_end(skb, hdr - GENL_HDRLEN - NLMSG_HDRLEN); //第二個入參為訊息attr載荷的首地址減去2個頭的長度(netlilnk頭和genetlink頭長度),即netlink訊息頭的首地址
}


static inline void nlmsg_end(struct sk_buff *skb, struct nlmsghdr *nlh)
{
    nlh->nlmsg_len = skb_tail_pointer(skb) - (unsigned char *)nlh; //填充nlh->nlmsg_len為整個訊息的長度(包括attr載荷部分和所有的訊息頭部分)
}

  至此,向CTRL控制器簇傳送的訊息就已經封裝完成。

2、應用層中Generic netlink的程式碼建立

  應用層相對於核心的建立會比較簡單一點。示例程式碼如下:

/*<----------main函式------------->*/
int main(int argc, char* argv[]) 
{

 ......

 /* 初始化socket */ 
    nl_fd = demo_create_nl_socket(NETLINK_GENERIC); //指定協議型別為NETLINK_GENERIC
    if (nl_fd < 0) {
        fprintf(stderr, "failed to create netlink socket\n");
        return 0;  
    }

 ......

}
/*<------------demo_create_nl_socket函式---------------->*/
static int demo_create_nl_socket(int protocol)
{
    int fd;
    struct sockaddr_nl local;
    /* 建立socket */
    fd = socket(AF_NETLINK, SOCK_RAW, protocol); //通過socket系統呼叫建立AF_NETLINK地址簇的SOCK_RAW套接字,指定協議型別為NETLINK_GENERIC
    if (fd < 0)
        return -1;
    memset(&local, 0, sizeof(local));
    local.nl_family = AF_NETLINK;
    local.nl_pid = getpid();
    /* 使用本程序的pid進行繫結 */
    if (bind(fd, (struct sockaddr *) &local, sizeof(local)) < 0)
        goto error;
    return fd;

error:
    close(fd);
    return -1;
}

3、應用層與核心層的通訊程式碼流程說明

  使用者與核心空間之間的通訊包括:

  • 1、使用者查詢Demo Family ID;
  • 2、向核心Demo Family傳送訊息;
  • 3、核心Demo Family 回發訊息;
  • 4、應用層接收核心Demo Family回發訊息。

3.1、使用者查詢Demo Family ID

使用者空間如果向核心傳送資料的話,使用者空間是通過什麼來識別出核心中對應處理部分的唯一性呢?
  通過核心分配的demo family的family id號。使用者空間想要傳送訊息到核心的demo genelink套接字,它首先得知道核心分配的demo family的family id號,因為genelink子系統是根據該id號來區分不同family簇的genelink套接字和分發訊息的。

怎麼去獲取這個唯一的family id呢?
  前文中的ctrl就用於使用者空間獲取family id的目的,它可以將family name轉換為對應的family id,使用者空間也通過family name向ctrl簇查詢對應的family id。在應用層序獲取了family id後它就可以像核心傳送訊息,該訊息分別包含了字串和資料,同時核心也在接受後進行回發操作。

  family id的獲取程式流程圖如下所示:
這裡寫圖片描述

  從使用者空間的main函式開始,介紹使用者空間下怎麼獲取family id。

/*<-------------main函式-------------->*/
int main(int argc, char* argv[]) 
{
    ......

    /* 獲取family id */
    nl_family_id = demo_get_family_id(nl_fd); //獲取family id介面
    if (!nl_family_id) {
        fprintf(stderr, "Error getting family id, errno %d\n", errno);
        goto out;
    }
    PRINTF("family id %d\n", nl_family_id);
    ......
}
/*<----------------demo_get_family_id函式------------->*/
static int demo_get_family_id(int sd)
{
    struct msgtemplate ans;

    char name[100];
    int id = 0, ret;
    struct nlattr *na;
    int rep_len;
    /* 根據gen family name查詢family id */
    //封裝查詢訊息並向核心的ctrl簇傳送
    strcpy(name, DEMO_GENL_NAME);
    ret = demo_send_cmd(sd, GENL_ID_CTRL, getpid(), CTRL_CMD_GETFAMILY,         //傳送,程式碼如下說明
                 CTRL_ATTR_FAMILY_NAME, (void *)name, strlen(DEMO_GENL_NAME)+1);
    if (ret < 0)
        return 0; 

    /* 接收核心訊息 */
    rep_len = recv(sd, &ans, sizeof(ans), 0);       //接收核心的回髮結果然後解析出其中的family id
    if (ans.n.nlmsg_type == NLMSG_ERROR || (rep_len < 0) || !NLMSG_OK((&ans.n), rep_len))
        return 0;
    /* 解析family id */
    //找到回發訊息中的第二個attr
    na = (struct nlattr *) GENLMSG_DATA(&ans);
    na = (struct nlattr *) ((char *) na + NLA_ALIGN(na->nla_len));
    if (na->nla_type == CTRL_ATTR_FAMILY_ID) {
        id = *(__u16 *) NLA_DATA(na);           //獲取出其中的family id號
    }

    return id;                      //至此使用者程式成功獲取的了demo family的id號,接下來就可以向他傳送訊息了
} 
//訊息的封裝過程同核心態訊息封裝過程類似,需嚴格按照genelink訊息格式進行封裝
static int demo_send_cmd(int sd, __u16 nlmsg_type, __u32 nlmsg_pid,
      __u8 genl_cmd, __u16 nla_type,
      void *nla_data, int nla_len)
{
    struct nlattr *na;
    struct sockaddr_nl nladdr;
    int r, buflen;
    char *buf;
    struct msgtemplate msg;
    /* 填充msg (本函式傳送的msg只填充一個attr) */
    //首先填充netlink訊息頭
    msg.n.nlmsg_len = NLMSG_LENGTH(GENL_HDRLEN);
    msg.n.nlmsg_type = nlmsg_type;
    msg.n.nlmsg_flags = NLM_F_REQUEST;
    msg.n.nlmsg_seq = 0;
    msg.n.nlmsg_pid = nlmsg_pid;

    //然後填充genetlink訊息頭

    msg.g.cmd = genl_cmd; //訊息cmd欄位為CTRL_CMD_GETFAMILY
    msg.g.version = DEMO_GENL_VERSION; //version欄位為DEMO_GENL_VERSION(同核心保持一致)


    //填充一個attr屬性
    na = (struct nlattr *) GENLMSG_DATA(&msg);
    na->nla_type = nla_type;                                                                                  //例子中為CTRL_ATTR_FAMILY_NAME
    na->nla_len = nla_len + 1 + NLA_HDRLEN;
    memcpy(NLA_DATA(na), nla_data, nla_len);                                                  //將傳入的family name拷貝到屬性attr的payload載荷中
    msg.n.nlmsg_len += NLMSG_ALIGN(na->nla_len);                                       //更新各個訊息頭中的長度欄位 
    buf = (char *) &msg;
    buflen = msg.n.nlmsg_len;
    memset(&nladdr, 0, sizeof(nladdr));
    nladdr.nl_family = AF_NETLINK;
    /* 迴圈傳送直到傳送完成 */
    while ((r = sendto(sd, buf, buflen, 0, (struct sockaddr *) &nladdr, sizeof(nladdr))) < buflen) { //啟動傳送流程,指定目的地址的地址簇為AF_NETLINK,ID號為0(表示核心)。
        if (r > 0) {
            buf += r;
            buflen -= r;
        } else if (errno != EAGAIN)
            return -1;
    }

    return 0;
}

  瞭解了使用者端的獲取程式碼流程,接下來看核心端的Ctrl簇是如何處理接收到的查詢訊息的。
  在netlink函式呼叫流程的最後會呼叫具體協議型別的netlink_rcv()回撥函式,其中genetlink的回撥函式在前文中已經看到為genl_rcv():

static void genl_rcv(struct sk_buff *skb)
{
    down_read(&cb_lock);
    //netlink_rcv_skb()函式會對訊息進行一些通用性的處理,將使用者訊息封裝成genl_info結構,最後會把訊息控制權交給genl_rcv_msg()回撥函式
    netlink_rcv_skb(skb, &genl_rcv_msg); //核心端接收,引數中第一個為訊息skb,第二個為genl_rcv_msg回撥函式。
    up_read(&cb_lock);
}

  這裡出現了又一個主要的結構體:genl_info。

//目前由於設定的nlmsg_flags為NLM_F_REQUEST、nlmsg_type為GENL_ID_CTRL(即NLMSG_MIN_TYPE),因此呼叫genl_rcv_msg()回撥函式開始訊息處理流程
int netlink_rcv_skb(struct sk_buff *skb, int (*cb)(struct sk_buff *,
           struct nlmsghdr *))
{
    struct nlmsghdr *nlh;
    int err;
    while (skb->len >= nlmsg_total_size(0)) { //判斷訊息的長度是否不小於netlink訊息頭的長度
        int msglen;
        nlh = nlmsg_hdr(skb);
        err = 0;

        //進行一些基本的資料長度判斷
        if (nlh->nlmsg_len < NLMSG_HDRLEN || skb->len < nlh->nlmsg_len)
            return 0;
        /* Only requests are handled by the kernel */
        //根據nlmsg_flags和nlmsg_type欄位判斷是否跳過訊息處理流程、以及是否回發ACK相應
        if (!(nlh->nlmsg_flags & NLM_F_REQUEST))
            goto ack;
        /* Skip control messages */
        if (nlh->nlmsg_type < NLMSG_MIN_TYPE)
            goto ack;
        err = cb(skb, nlh);
        if (err == -EINTR)
            goto skip;
ack:
        if (nlh->nlmsg_flags & NLM_F_ACK || err)
            netlink_ack(skb, nlh, err);
skip:
        msglen = NLMSG_ALIGN(nlh->nlmsg_len);
        if (msglen > skb->len)
            msglen = skb->len;
        skb_pull(skb, msglen);
    }
    return 0;
}
static int genl_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
{
    struct genl_family *family;
    int err;
    family = genl_family_find_byid(nlh->nlmsg_type); //通過nlmsg_type欄位(即family id號)在散列表中查詢到對應的註冊family
    if (family == NULL)
        return -ENOENT;
    if (!family->parallel_ops) //如果訊息處理不可重入,則這裡會上鎖
        genl_lock();
    err = genl_family_rcv_msg(family, skb, nlh); //呼叫genl_family_rcv_msg()函式(下面程式碼說明)
    if (!family->parallel_ops)
        genl_unlock();
    return err;
}

static int genl_family_rcv_msg(struct genl_family *family,
          struct sk_buff *skb,
          struct nlmsghdr *nlh)
{
    const struct genl_ops *ops;
    struct net *net = sock_net(skb->sk);
    struct genl_info info;
    struct genlmsghdr *hdr = nlmsg_data(nlh);
    struct nlattr **attrbuf;
    int hdrlen, err;
    /* this family doesn't exist in this netns */
    if (!family->netnsok && !net_eq(net, &init_net)) //函式首先判斷網路名稱空間,若不支援則當前訊息的網路空間必須為init_net
        return -ENOENT;
    hdrlen = GENL_HDRLEN + family->hdrsize; //判斷訊息的長度
    if (nlh->nlmsg_len < nlmsg_msg_size(hdrlen))
        return -EINVAL;

    ops = genl_get_cmd(hdr->cmd, family); //這裡找到訊息cmd命令對應的處理函式並保存於ops變數中,查詢的方式是通過cmd欄位的匹配型別來找的,這裡找到的就是前文中註冊的demo_ops結構了
    if (ops == NULL)
        return -EOPNOTSUPP;

    if ((ops->flags & GENL_ADMIN_PERM) && !netlink_capable(skb, CAP_NET_ADMIN)) //判斷許可權,這裡由於已經在demo_ops中設定了GENL_ADMIN_PERM標識,因此本命令操作需要具有CAP_NET_ADMIN許可權
        return -EPERM;

    //如果使用者設定了NLM_F_DUMP標識,這裡就會呼叫啟動dump流程,回填skb訊息(這裡的skb將不再是使用者下發的訊息了)。
    if ((nlh->nlmsg_flags & NLM_F_DUMP) == NLM_F_DUMP) {
        int rc;
        if (ops->dumpit == NULL)
            return -EOPNOTSUPP;
        if (!family->parallel_ops) {
            struct netlink_dump_control c = {
                .module = family->module,
                /* we have const, but the netlink API doesn't */
                .data = (void *)ops,
                .dump = genl_lock_dumpit,
                .done = genl_lock_done,
            };
            genl_unlock();
            rc = __netlink_dump_start(net->genl_sock, skb, nlh, &c);
            genl_lock();
        } else {
            struct netlink_dump_control c = {
                .module = family->module,
                .dump = ops->dumpit,
                .done = ops->done,
            };
            rc = __netlink_dump_start(net->genl_sock, skb, nlh, &c);
        }
        return rc;
    }

    //為attr屬性指定接收快取
    //!!!需要注意的是這裡的記憶體其實只是一個指標陣列,用來存放attr屬性的地址,並不會存放實際的屬性資料。
    if (family->maxattr && family->parallel_ops) { //在支援重入的情況下這裡會另行動態分配記憶體,否則使用在註冊family的__genl_register_family函式中分配的記憶體空間
    attrbuf = kmalloc((family->maxattr+1) * sizeof(struct nlattr *), GFP_KERNEL);
    if (attrbuf == NULL)
        return -ENOMEM;
    } else
        attrbuf = family->attrbuf;

    if (attrbuf) {
        err = nlmsg_parse(nlh, hdrlen, attrbuf, family->maxattr, ops->policy); //將訊息的資料拷貝到快取空間中去(在下面程式碼中進行具體說明)
        //引數:
        // nlh:netlink訊息頭
        // hdrlen:genelink訊息頭長度(其實也包括了使用者私有頭,只不過這裡為0罷了)
        // attrbuf:資料屬性快取地址
        // family->maxattr:快取空間大小
        // ops->policy:屬性有效性策略結構
        if (err < 0)
            goto out;
    }

    //開始封裝genl_info訊息結構,填充對應的欄位
    info.snd_seq = nlh->nlmsg_seq;
    info.snd_portid = NETLINK_CB(skb).portid; //傳送端的套接字ID號
    info.nlhdr = nlh;
    info.genlhdr = nlmsg_data(nlh);
    info.userhdr = nlmsg_data(nlh) + GENL_HDRLEN;
    info.attrs = attrbuf; //為前文中分配的attr快取空間首地址
    info.dst_sk = skb->sk;
    genl_info_net_set(&info, net);
    memset(&info.user_ptr, 0, sizeof(info.user_ptr));

    //如果在註冊family時指定了pre_doit和post_doit回撥函式,將在分別呼叫ops->doit()函式的前後呼叫他們。
    //在當前獲取family id的操作中,對於Ctrl簇而言並沒有定義,這裡會直接呼叫ops->doit()回撥函式,對於CTRL_CMD_GETFAMILY來說就是ctrl_getfamily()(在下面程式碼中具體說明)
    if (family->pre_doit) {
        err = family->pre_doit(ops, skb, &info);
        if (err)
            goto out;
    }
    err = ops->doit(skb, &info);
    if (family->post_doit)
        family->post_doit(ops, skb, &info);
    ...
}
static int genl_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
{
    struct genl_family *family;
    int err;
    family = genl_family_find_byid(nlh->nlmsg_type); //通過nlmsg_type欄位(即family id號)在散列表中查詢到對應的註冊family
    if (family == NULL)
        return -ENOENT;
    if (!family->parallel_ops) //如果訊息處理不可重入,則這裡會上鎖
        genl_lock();
    err = genl_family_rcv_msg(family, skb, nlh); //呼叫genl_family_rcv_msg()函式(下面程式碼說明)
    if (!family->parallel_ops)
        genl_unlock();
    return err;
}

static int genl_family_rcv_msg(struct genl_family *family,
          struct sk_buff *skb,
          struct nlmsghdr *nlh)
{
    const struct genl_ops *ops;
    struct net *net = sock_net(skb->sk);
    struct genl_info info;
    struct genlmsghdr *hdr = nlmsg_data(nlh);
    struct nlattr **attrbuf;
    int hdrlen, err;
    /* this family doesn't exist in this netns */
    if (!family->netnsok && !net_eq(net, &init_net)) //函式首先判斷網路名稱空間,若不支援則當前訊息的網路空間必須為init_net
        return -ENOENT;
    hdrlen = GENL_HDRLEN + family->hdrsize; //判斷訊息的長度
    if (nlh->nlmsg_len < nlmsg_msg_size(hdrlen))
        return -EINVAL;

    ops = genl_get_cmd(hdr->cmd, family); //這裡找到訊息cmd命令對應的處理函式並保存於ops變數中,查詢的方式是通過cmd欄位的匹配型別來找的,這裡找到的就是前文中註冊的demo_ops結構了
    if (ops == NULL)
        return -EOPNOTSUPP;

    if ((ops->flags & GENL_ADMIN_PERM) && !netlink_capable(skb, CAP_NET_ADMIN)) //判斷許可權,這裡由於已經在demo_ops中設定了GENL_ADMIN_PERM標識,因此本命令操作需要具有CAP_NET_ADMIN許可權
        return -EPERM;

    //如果使用者設定了NLM_F_DUMP標識,這裡就會呼叫啟動dump流程,回填skb訊息(這裡的skb將不再是使用者下發的訊息了)。
    if ((nlh->nlmsg_flags & NLM_F_DUMP) == NLM_F_DUMP) {
        int rc;
        if (ops->dumpit == NULL)
            return -EOPNOTSUPP;
        if (!family->parallel_ops) {
            struct netlink_dump_control c = {
                .module = family->module,
            /* we have const, but the netlink API doesn't */
                .data = (void *)ops,
                .dump = genl_lock_dumpit,
                .done = genl_lock_done,
            };
            genl_unlock();
            rc = __netlink_dump_start(net->genl_sock, skb, nlh, &c);
            genl_lock();
        } else {
            struct netlink_dump_control c = {
                .module = family->module,
                .dump = ops->dumpit,
                .done = ops->done,
            };
            rc = __netlink_dump_start(net->genl_sock, skb, nlh, &c);
        }
        return rc;
    }


    //為attr屬性指定接收快取
    //!!!需要注意的是這裡的記憶體其實只是一個指標陣列,用來存放attr屬性的地址,並不會存放實際的屬性資料。
    if (family->maxattr && family->parallel_ops) { //在支援重入的情況下這裡會另行動態分配記憶體,否則使用在註冊family的__genl_register_family函式中分配的記憶體空間
        attrbuf = kmalloc((family->maxattr+1) * sizeof(struct nlattr *), GFP_KERNEL);
        if (attrbuf == NULL