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); //为当前系统中的网络命名空间创建Generic Netlink套接字。 --- 详解2 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)) { //确保的是family name的全局唯一性 err = -EEXIST; goto errout_locked; }
接下来调用genl_validate_ops对ops函数集做校验,对于每一个注册的genl_ops结构,其中doit和dumpit回调函数必须至少实现一个,然后其针对的cmd命令不可以出现重复,否则返回错误,注册失败。
然后上锁开始启动链表操作,首先需要确保的是family name的全局唯一性,因此这里会查找是否有同名的簇已经注册了,若有就不能再注册了。
if (family->id == GENL_ID_GENERATE) { //判断传入的ID号是否为GENL_ID_GENERATE u16 newid = genl_generate_id(); //若是则由内核分配一个空闲的ID号 if (!newid) { err = -ENOMEM; goto errout_locked; } family->id = newid; } else if (genl_family_find_byid(family->id)) { //否则得确保程序指定的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)); //将family注册到链表中 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,所以该函数不会做任何的事情(会直接判断init_net.genl_sock字段,然后return,所以不会做其他事情),对于注册其他通用family的情况后续在分析,这样ctrl family就成功创建完成了。
static int genl_ctrl_event(int event, struct genl_family *family, const struct genl_multicast_group *grp, int grp_id) { struct sk_buff *msg; /* 是否已经注册了控制器CTRL簇,这里显然还未注册,所以会return */ if (!init_net.genl_sock) //在这里就直接返回了!!!,所以暂时不用看后面的部分,下一篇博客中讲到 return 0; switch (event) { case CTRL_CMD_NEWFAMILY: case CTRL_CMD_DELFAMILY: WARN_ON(grp); msg = ctrl_build_family_msg(family, 0, 0, event); 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); break; default: return -EINVAL; }
注意:对于用户创建的Generic Netlink套接字,genl_ctrl_event方法向所有的应用层(加入CTRL控制器簇组播组的Generic Netlink套接字)多播发送CTRL_CMD_NEWFAMILY消息,通知应用层有新的family注册了,这样应用层就可以捕获这一消息!!!
2.详解2 --- err = register_pernet_subsys(&genl_pernet_ops);
为当前系统中的网络命名空间创建Generic Netlink套接字。
int register_pernet_subsys(struct pernet_operations *ops) { int error; mutex_lock(&net_mutex); error = register_pernet_operations(first_device, ops); mutex_unlock(&net_mutex); return error; }
其中genl_pernet_ops定义如下:
static struct pernet_operations genl_pernet_ops = { .init = genl_pernet_init, .exit = genl_pernet_exit, }; static int __net_init genl_pernet_init(struct net *net) { struct netlink_kernel_cfg cfg = { .input = genl_rcv,//指定了消息处理函数为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);//每一种套接字都有自己的名字,比如net->rtnl表示route netlink 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内核套接字了。
(二)整体流程
作者:山上有风景
欢迎任何形式的转载,但请务必注明出处。
限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
2019-05-10 Arduino---HC-05 蓝牙模块
2018-05-10 python---基础知识回顾(十)进程和线程(py2中自定义线程池和py3中的线程池使用)
2018-05-10 python---函数补充(变量传递),语句执行顺序(入栈顺序)