Generic Netlink內核實現分析(一):初始化

Generic Netlink 是內核專門爲了擴展netlink協議簇而設計的“通用netlink協議簇”。由於netlink協議最多支持32個協議簇,目前Linux4.1的內核中已經使用其中21個,對於用戶需要定製特殊的協議類型略顯不夠,而且用戶還需自行在include/linux/netlink.h中添加簇定義(略顯不妥),爲此Linux設計了這種通用Netlink協議簇,用戶可在此之上定義更多類型的子協議。前兩篇博文已經較爲詳細的分析了netlink的創建和通信流程,本文以Generic Netlink爲例首先來深入分析一下netlink的消息結構及創建初始化流程。

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


圖1 Generic Netlink模型框架

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

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定義各部相同;到最底層消息屬性又分爲消息屬性頭和實際的消息載荷。Genetlink消息基於這個消息結構類型並定製化爲如下結構:

圖2 Generic Netlink消息結構

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

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

一、Generic Netlink相關結構體

1、Generic Netlink消息頭結構:struct genlmsghdr

struct genlmsghdr {
	__u8	cmd;
	__u8	version;
	__u16	reserved;
};
Generic Netlink消息頭比較簡單,僅包含了兩個字段。其中cmd表示消息命令,對於用戶自己定義的每個子協議類型都需要定義特定的消息命令集,這裏該字段表示當前消息的消息命令;version字段表示版本控制(可以在在不破壞向後兼容性的情況下修改消息的格式),可以不使用該字段;最後的reserved字段保留。

2、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:保存當前簇使用的組播組及組播地址的個數;

3、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:轉儲結束後執行的回調函數;

4、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()啓動初始化流程:


圖3 Genetic Netlink初始化流程

static int __init genl_init(void)
{
	int i, err;

	for (i = 0; i < GENL_FAM_TAB_SIZE; i++)
		INIT_LIST_HEAD(&family_ht[i]);

	err = genl_register_family_with_ops_groups(&genl_ctrl, genl_ctrl_ops,
						   genl_ctrl_groups);
	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,首先來看一下genl_ctrl的定義:

static struct genl_family genl_ctrl = {
	.id = GENL_ID_CTRL,
	.name = "nlctrl",
	.version = 0x2,
	.maxattr = CTRL_ATTR_MAX,
	.netnsok = true,
};
這裏的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屬性限定爲空結尾的字符串類型並限定了長度。

最後來看一下注冊的組播組:
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就成功創建完成了。

最後再回到genl_init()中,接下來使用了register_pernet_subsys方法爲當前系統中的網絡命名空間創建Generic Netlink套接字:

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內核套接字了。

本文分析了內核Generic Netlink的消息結構以及創建初始化流程,下一篇來根據一個具體的示例domo程序跟蹤分析Generic netlink消息在內核中的通信及處理流程。


參考文獻:1、《Linux Kernel Networking Implementation and Theory》;2、《Generic Netlink詳解》


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