1. 程式人生 > 其它 >Generic Netlink核心實現分析(一):原理解析

Generic Netlink核心實現分析(一):原理解析

一:Generic Netlink介紹

Generic Netlink 是核心專門為了擴充套件netlink協議簇而設計的“通用netlink協議簇”。

由於netlink協議最多支援32個協議簇,目前Linux4.1的核心中已經使用其中21個,對於使用者需要定製特殊的協議型別略顯不夠,而且使用者還需自行在include/linux/netlink.h中新增簇定義(略顯不妥),為此Linux設計了這種通用Netlink協議簇,使用者可在此之上定義更多型別的子協議

前幾篇博文已經較為詳細的分析了netlink的建立和通訊流程,本文以Generic Netlink為例首先來深入分析一下netlink的訊息結構及建立初始化流程。

(一)Generic Netlink模型框架

Generic Netlink使用NETLINK_GENERIC型別協議簇,同樣基於netlink子系統。具體框架如下:

 

其中Ctrl控制器是一種特殊型別的Genetlink協議族,它用於使用者空間通過Genetlink簇名查詢對應的ID號,後文中會詳細分析。

(二)netlink型別的訊息結構回顧

Generic Netlink的訊息結構基於netlink訊息結構並在此基礎上繼續擴充套件,首先來看一下netlink型別的訊息結構(見include/net/netlink.h): 

/* ========================================================================
 *         Netlink Messages and Attributes Interface (As Seen On TV)
 * ------------------------------------------------------------------------
 *                          Messages Interface
 * ------------------------------------------------------------------------
 *
 * Message Format:
 *    <--- nlmsg_total_size(payload)  --->
 *    <-- nlmsg_msg_size(payload) ->
 *   +----------+- - -+-------------+- - -+-------- - -
 *   | nlmsghdr | Pad |   Payload   | Pad | nlmsghdr
 *   +----------+- - -+-------------+- - -+-------- - -
 *   nlmsg_data(nlh)---^------------^
^ * nlmsg_next(nlh)-----------------------+ * * Payload Format: * <---------------------- nlmsg_len(nlh) ---------------------> * <------ hdrlen ------> <- nlmsg_attrlen(nlh, hdrlen) -> * +----------------------+- - -+--------------------------------+ * | Family Header | Pad | Attributes | * +----------------------+- - -+--------------------------------+ * nlmsg_attrdata(nlh, hdrlen)---^-------------------------------^
* * Attribute Format: * <------- nla_total_size(payload) -------> * <---- nla_attr_size(payload) -----> * +----------+- - -+- - - - - - - - - +- - -+-------- - - * | Header | Pad | Payload | Pad | Header * +----------+- - -+- - - - - - - - - +- - -+-------- - - * <- nla_len(nla) -> ^ * nla_data(nla)----^------------------^ | * nla_next(nla)-----------------------------' * *=========================================================================

首先最上層,一個netlink訊息有netlink訊息頭和netlink訊息載荷組成,它們之間存在記憶體對齊的pad留空空間(這在《Netlink 核心實現分析(1)(2)》中已經看到了,但並未對訊息載荷進行進一步分析);

然後往下一級訊息的實際載荷又可分為family頭級具體的訊息屬性,其中family頭針對不同協議種類的netlink定義各部相同;

到最底層訊息屬性又分為訊息屬性頭和實際的訊息載荷。

(三)Generic Netlink 訊息結構

Generic Netlink訊息基於這個訊息結構型別並定製化為如下結構:

其中family頭對於Genetlink來說就是Generic訊息頭genlmsghdr,接下來是可選的使用者特定訊息頭,最後才是可選的有效載荷,即一個個訊息屬性例項。

Genetlink訊息是命令驅動式的,即每一條訊息的genlmsghdr中都指明瞭當前訊息的cmd訊息命令,這些訊息cmd命令由使用者自行定義。核心在接收到使用者的genl訊息後,首先會對命令cmd做判斷,找到對應的訊息處理結構(可能會執行attr有效性檢查),然後才會去呼叫訊息處理回撥函式從訊息載荷區中讀取並處理其所需要的的attr屬性載荷。

二:Generic Netlink相關結構體

(一)Generic Netlink訊息頭結構:struct genlmsghdr

struct genlmsghdr {
    __u8    cmd;
    __u8    version;
    __u16    reserved;
};

Generic Netlink訊息頭比較簡單,僅包含了兩個欄位。其中cmd表示訊息命令,對於使用者自己定義的每個子協議型別都需要定義特定的訊息命令集,這裡該欄位表示當前訊息的訊息命令;version欄位表示版本控制(可以在在不破壞向後相容性的情況下修改訊息的格式),可以不使用該欄位;最後的reserved欄位保留。

(二)Generic Netlink Family結構:struct genl_family(核心中完成註冊)

struct genl_family {
    unsigned int        id;
    unsigned int        hdrsize;
    char            name[GENL_NAMSIZ];
    unsigned int        version;
    unsigned int        maxattr;
    bool            netnsok;
    bool            parallel_ops;
    int            (*pre_doit)(const struct genl_ops *ops,
                        struct sk_buff *skb,
                        struct genl_info *info);
    void            (*post_doit)(const struct genl_ops *ops,
                         struct sk_buff *skb,
                         struct genl_info *info);
    int            (*mcast_bind)(struct net *net, int group);
    void            (*mcast_unbind)(struct net *net, int group);
    struct nlattr **    attrbuf;    /* private */
    const struct genl_ops *    ops;        /* private */
    const struct genl_multicast_group *mcgrps; /* private */
    unsigned int        n_ops;        /* private */
    unsigned int        n_mcgrps;    /* private */
    unsigned int        mcgrp_offset;    /* private */
    struct list_head    family_list;    /* private */
    struct module        *module;
};

Generic Netlink按照family進行管理,使用者需註冊自己定義的genl_family結構,同時核心使用一個雜湊表family_ht對已經註冊的genl family進行管理。各欄位的含義如下:

id:genl family的ID號,一般由核心進行分配,取值範圍為GENL_MIN_ID~GENL_MAX_ID(16~1023),其中GENL_ID_CTRL為控制器的family ID,不可另行分配,該familyID全域性唯一併且在family_ht中的位置也由該值確定;

hdrsize:使用者私有報頭的長度,即可選的user msg header長度,若沒有則為0;
name:genl family的名稱,必須是獨一無二且使用者層已知的(使用者通過它來向控制查詢family id);
version:版本號;
maxattr:訊息屬性attr最大的型別數(即該genl family所支援的最大attr屬性型別的種類個數);
netnsok:指示當前簇是否能夠處理網路名稱空間;
pre_doit:呼叫genl_ops結構中處理訊息函式doit()前呼叫的鉤子函式,一般用於執行一些前置的當前簇通用化處理,例如對臨界區加鎖等;
post_doit:呼叫genl_ops結構中處理訊息函式doit()後呼叫的鉤子函式,一般執行pre_doit函式相反的操作;

mcast_bind/mcast_unbind:在繫結/解繫結socket到一個特定的genl netlink組播組中呼叫(目前核心中沒有相關使用);
attrbuf:儲存拷貝的attr屬性快取;
ops/n_ops:儲存genl family命令處理結構即命令的個數,後面詳細描述;
family_list:連結串列結構,用於將當前當前簇鏈入全域性family_ht散列表中;

mcgrps/n_mcgrps:儲存當前簇使用的組播組及組播地址的個數;

(三)Generic Netlink Family命令處理結構:struct genl_ops(核心中完成註冊)

struct genl_ops {
    const struct nla_policy    *policy;
    int               (*doit)(struct sk_buff *skb,
                       struct genl_info *info);
    int               (*dumpit)(struct sk_buff *skb,
                     struct netlink_callback *cb);
    int               (*done)(struct netlink_callback *cb);
    u8            cmd;
    u8            internal_flags;
    u8            flags;
};

該結構用於註冊genl family的使用者命令cmd處理函式(對於只嚮應用層傳送訊息的簇可以不用實現和註冊該結構),各個欄位的含義如下:
cmd:簇命令型別,由使用者自行根據需要定義;

internal_flags:簇私有標識,用於進行一些分支處理,可以不使用;

flags:操作標識,有以下四種類型(在genetlink.h中定義):

#define GENL_ADMIN_PERM        0x01    /* 當設定該標識時表示本命令操作需要具有CAP_NET_ADMIN許可權 */
#define GENL_CMD_CAP_DO        0x02    /* 當genl_ops結構中實現了doit()回撥函式則設定該標識 */
#define GENL_CMD_CAP_DUMP    0x04    /* 當genl_ops結構中實現了dumpit()回撥函式則設定該標識 */
#define GENL_CMD_CAP_HASPOL    0x08    /* 當genl_ops結構中定義了屬性有效性策略(nla_policy)則設定該標識 */

policy:屬性attr有效性策略,該結構定義在《Netlink核心實現分析(一)》種已經見過了,若該欄位不為空,在genl執行訊息處理函式前會對訊息中的attr屬性進行校驗,否則則不做校驗;
doit:標準命令回撥函式,在當前族中收到資料時觸發呼叫,函式的第一個入參skb中儲存了使用者下發的訊息內容;

dumpit:轉儲回撥函式,當genl_ops的flag標誌被添加了NLM_F_DUMP以後會呼叫該回調函式,這裡的第一個入參skb中不再有使用者下發訊息內容,而是要求函式能夠在傳入的skb中填入訊息載荷並返回填入資料長度;

done:轉儲結束後執行的回撥函式;

(四)Generic Netlink Family核心接收訊息結構:struct genl_info

struct genl_info {
    u32            snd_seq;
    u32            snd_portid;
    struct nlmsghdr *    nlhdr;
    struct genlmsghdr *    genlhdr;
    void *            userhdr;
    struct nlattr **    attrs;
    possible_net_t        _net;
    void *            user_ptr[2];
    struct sock *        dst_sk;
};

核心在接收到使用者的genetlink訊息後,會對訊息解析並封裝成genl_info結構,便於命令回校函式進行處理,其中各欄位含義如下:
snd_seq:訊息的傳送序號(不強制使用);

snd_portid:訊息傳送端socket所繫結的ID;

nlhdr:netlink訊息頭;

genlhdr:generic netlink訊息頭;

userhdr:使用者私有報頭;

attrs:netlink屬性,包含了訊息的實際載荷;

dst_sk:目的socket;

三:Generic Netlink初始化

Generic Netlink只是中特殊型別的Netlink,它本質上還是依賴於netlink的核心機制,相關的函式在genetlink.c中,由genl_init()啟動初始化流程:

(一)genl_init初始化

static int __init genl_init(void)
{
    int i, err;
 
    for (i = 0; i < GENL_FAM_TAB_SIZE; i++)
        INIT_LIST_HEAD(&family_ht[i]);      //初始化用於儲存和維護Generic netlink family的散列表family_ht陣列
 
    err = genl_register_family_with_ops_groups(&genl_ctrl, genl_ctrl_ops,
                           genl_ctrl_groups);  //向核心Generic netlink子系統註冊控制器簇型別的Genetlink Family --- 詳解1
    if (err < 0)
        goto problem;
 
    err = register_pernet_subsys(&genl_pernet_ops);
    if (err)
        goto problem;
 
    return 0;
 
problem:
    panic("GENL: Cannot register controller: %d\n", err);
}

首先初始化用於儲存和維護Generic netlink family的散列表family_ht陣列,然後呼叫genl_register_family_with_ops_groups向核心Generic netlink子系統註冊控制器簇型別的Genetlink Family.

1.詳解1 --- genl_register_family_with_ops_groups(&genl_ctrl, genl_ctrl_ops, genl_ctrl_groups);

首先來看一下genl_ctrl的定義: 

static struct genl_family genl_ctrl = {
    .id = GENL_ID_CTRL,
    .name = "nlctrl",
    .version = 0x2,
    .maxattr = CTRL_ATTR_MAX,
    .netnsok = true,          //netnsok欄位為true表示支援net名稱空間。
};

這裡的ID為GENL_ID_CTRL(16),即分配區間的最小值,maxattr定義為支援的attr屬性最大個數CTRL_ATTR_MAX,該值定義如下:

enum {
    CTRL_ATTR_UNSPEC,
    CTRL_ATTR_FAMILY_ID,
    CTRL_ATTR_FAMILY_NAME,
    CTRL_ATTR_VERSION,
    CTRL_ATTR_HDRSIZE,
    CTRL_ATTR_MAXATTR,
    CTRL_ATTR_OPS,
    CTRL_ATTR_MCAST_GROUPS,
    __CTRL_ATTR_MAX,
};
 
#define CTRL_ATTR_MAX(__CTRL_ATTR_MAX - 1)

這裡為genetlink控制器定義了以CTRL_ATTR_UNSPEC為開頭到最後的__CTRL_ATTR_MAX中的一共7個attr屬性型別,後文再進行分析;

繼續回到genl_ctrl中,最後netnsok欄位為true表示支援net名稱空間。

再來看一下genl_ctrl_ops的定義:

static struct genl_ops genl_ctrl_ops[] = {
    {
        .cmd        = CTRL_CMD_GETFAMILY,
        .doit        = ctrl_getfamily,
        .dumpit        = ctrl_dumpfamily,
        .policy        = ctrl_policy,
    },
};

這裡為控制器型別的genetlink family只定義了一種cmd型別的核心操作介面,即CTRL_CMD_GETFAMILY,它用於應用空間從核心中獲取指定family名稱的ID號

因為該ID號在核心註冊family時由核心進行分配,應用空間一般只知道需要通訊的family name,但是要發起通訊就必須知道該ID號,所以核心設計了控制器型別的family並定義了CTRL_CMD_GETFAMILY命令的處理介面用於應用程式查詢ID號
然後指明doit和dumpit回撥函式為ctrl_getfamily和ctrl_dumpfamily,最後指定attr有效性策略為ctrl_policy:

static const struct nla_policy ctrl_policy[CTRL_ATTR_MAX+1] = {
    [CTRL_ATTR_FAMILY_ID]    = { .type = NLA_U16 },
    [CTRL_ATTR_FAMILY_NAME]    = { .type = NLA_NUL_STRING,
                    .len = GENL_NAMSIZ - 1 },
};

這裡為CTRL_ATTR_FAMILY_ID屬性限定型別為16位無符號數,為CTRL_ATTR_FAMILY_NAME屬性限定為空結尾的字串型別並限定了長度。

最後來看一下注冊的組播組genl_ctrl_groups

static struct genl_multicast_group genl_ctrl_groups[] = {
    { .name = "notify", },
};

這裡添加了name為”notify“的組播組。然後進入genl_register_family_with_ops_groups內部來分析一下核心是如何註冊這個family簇的:

#define genl_register_family_with_ops_groups(family, ops, grps)    \
    _genl_register_family_with_ops_grps((family),            \
                        (ops), ARRAY_SIZE(ops),    \
                        (grps), ARRAY_SIZE(grps))
static inline int
_genl_register_family_with_ops_grps(struct genl_family *family,
                    const struct genl_ops *ops, size_t n_ops,
                    const struct genl_multicast_group *mcgrps,
                    size_t n_mcgrps)
{
    family->module = THIS_MODULE;
    family->ops = ops;
    family->n_ops = n_ops;
    family->mcgrps = mcgrps;
    family->n_mcgrps = n_mcgrps;
    return __genl_register_family(family);
}

這裡根據入參初始化了family的ops等欄位,然後呼叫__genl_register_family()繼續進行註冊:

int __genl_register_family(struct genl_family *family)
{
    int err = -EINVAL, i;
 
    if (family->id && family->id < GENL_MIN_ID)
        goto errout;
 
    if (family->id > GENL_MAX_ID)
        goto errout;

首先對入參的ID號進行判斷,一般來說,為了保證ID號的全域性唯一性,程式中一般都設定為GENL_ID_GENERATE,由核心統一分配(當然這裡註冊控制器family除外了)。

    err = genl_validate_ops(family);
    if (err)
        return err;
 
    genl_lock_all();
 
    if (genl_family_find_byname(family->name)) {
        err = -EEXIST;
        goto errout_locked;
    }

接下來呼叫genl_validate_ops對ops函式集做校驗,對於每一個註冊的genl_ops結構,其中doit和dumpit回撥函式必須至少實現一個,然後其針對的cmd命令不可以出現重複,否則返回錯誤,註冊失敗。

然後上鎖開始啟動連結串列操作,首先需要確保的是family name的全域性唯一性,因此這裡會查詢是否有同名的簇已經註冊了,若有就不能再註冊了。

    if (family->id == GENL_ID_GENERATE) {
        u16 newid = genl_generate_id();
 
        if (!newid) {
            err = -ENOMEM;
            goto errout_locked;
        }
 
        family->id = newid;
    } else if (genl_family_find_byid(family->id)) {
        err = -EEXIST;
        goto errout_locked;
    }

然後判斷傳入的ID號是否為GENL_ID_GENERATE,若是則由核心分配一個空閒的ID號,否則得確保程式指定的ID號沒有被使用過。 

    if (family->maxattr && !family->parallel_ops) {
        family->attrbuf = kmalloc((family->maxattr+1) *
                    sizeof(struct nlattr *), GFP_KERNEL);
        if (family->attrbuf == NULL) {
            err = -ENOMEM;
            goto errout_locked;
        }
    } else
        family->attrbuf = NULL;

接著根據註冊的最大attr引數maxattr分配空間,這裡對於genl_ctrl來說一共分配了CTRL_ATTR_MAX+1個指標記憶體空間,以後用於快取attr屬性( 注意僅僅是儲存屬性的地址而非內容)。

    err = genl_validate_assign_mc_groups(family);
    if (err)
        goto errout_locked;

然後呼叫genl_validate_assign_mc_groups()函式判斷新增組播地址空間,該函式一共做了3件事:

(1)判斷註冊family的group組播名的有效性;

(2)為該family分配組播地址位元位並將bit偏移儲存到family->mcgrp_offset變數中(由於generic netlink中不同型別的family簇共用NETLINK_GENERIC協議型別的group組播地址空間,因此核心特別維護了幾個全域性變數mc_groups_longs、mc_groups和mc_group_start用以維護組播地址的位元位,另外對於幾種特殊的family是已經分配了的。無需再行分配,例如這裡的crtl控制器);

(3)更新全域性nl_table對應的NETLINK_GENERIC協議型別netlink的groups標識。
繼續回到中__genl_register_family()函式中:

    list_add_tail(&family->family_list, genl_family_chain(family->id));
    genl_unlock_all();
 
    /* send all events */
    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);
 
    return 0;
 
errout_locked:
    genl_unlock_all();
errout:
    return err;
}
EXPORT_SYMBOL(__genl_register_family);

接下來將family註冊到連結串列中,最後呼叫genl_ctrl_event()函式向核心的控制器family傳送CTRL_CMD_NEWFAMILY和CTRL_CMD_NEWMCAST_GRP命令訊息,當然這裡本身就是在建立ctrl控制器family,所以該函式不會做任何的事情,對於註冊其他通用family的情況後續在分析,這樣ctrl familu就成功建立完成了。

static int __net_init genl_pernet_init(struct net *net)
{
    struct netlink_kernel_cfg cfg = {
        .input        = genl_rcv,
        .flags        = NL_CFG_F_NONROOT_RECV,
        .bind        = genl_bind,
        .unbind        = genl_unbind,
    };
 
    /* we'll bump the group number right afterwards */
    net->genl_sock = netlink_kernel_create(net, NETLINK_GENERIC, &cfg);
 
    if (!net->genl_sock && net_eq(net, &init_net))
        panic("GENL: Cannot initialize generic netlink\n");
 
    if (!net->genl_sock)
        return -ENOMEM;
 
    return 0;
}

這裡定義了genetlink核心套接字的配置,並指定了訊息處理函式為genl_rcv(),套接字繫結和解繫結函式為genl_bind()和genl_unbind()(這點需要注意,和NETLINK_ROUTE不同),隨後呼叫netlink_kernel_create()函式完成核心套接字的註冊(netlink_kernel_create函式在前篇博文中已經詳細分析過了),並將生成的套接字賦值到網路名稱空間net的genl_sock中,以後就可以通過net->genl_sock來找到genetlink核心套接字了。