Linux generic netlink 自定义内核通信
前文说过将32个netlink协议号中未使用的号码用于自定义内核模块通信并不是一个好主意,更规范的做法是使用generic netlink的方式达成同样的目的。generic netlink是一种netlink协议,被设计为一个通用的协议,用于承载运行于其上的各种用户自定义协议。这里介绍generic netlink的使用方式及其实现原理。
generic netlink 使用
数据结构
例子代码之前先看几个需要了解的数据结构。
1 | struct genlmsghdr { |
genlmsghdr是generic netlink的消息头,发送给内核的generic netlink消息中该结构必须存在,因为内核需要根据这个协议头定位该cmd对应的ops。
1 | struct genl_family { |
genl_family用于注册到generic netlink中,需要使用静态数据,因为后续处理需要该结构。
- id
family注册后最终确定的id,内核保留字段,自定义协议注册该字段保留0。 - hdrsize
自定义协议中自定义头的长度,会影响内核对消息体中nlattr的定位及预处理。 - name
family的名字,不能注册重复名字,长度在15以内(不包含’\0’结尾)。 - maxattr
支持的nla_type的最大值,如果在消息中出现超出该值的nlattr,将不会被解析填充到genl_info中,也不会被policy校验。 - netnsok
是否支持接收所有net namespace的请求。 - parallel_ops
是否支持并行操作,如果不支持并行操作,内核将使用一个全局锁将不支持并行操作的family的所有请求串行处理。 - pre_doid
如果非NULL,将在genl_ops的doit函数指针前执行。 - post_doit
如果非NULL,将在genl_ops的doit函数指针执行。 - attrbuf
内核保留字段,如果不支持并行操作,将用作genl_info字段attrs共用的缓存。 - ops
支持的genl_ops数组。 - mcgrps
family需要的组播组名字数组。 - n_ops
ops数组长度。 - n_mcgrps
mcgrps数组长度 - mcgrp_offset
内核保留字段,最终确定的组播组连续id的首个id。
1 | struct genl_ops { |
注册时嵌入到genl_family结构中,同样需要使用静态数据
- policy
用户校验该cmd下nlattr基本内容,具体看include/net/netlink.h
和源码吧。 - doit
该cmd的处理函数。 - dumpit
该cmd的请求nlmsg_flags中如果包含NLM_F_DUMP,将由该处理函数处理。这里的处理逻辑支持对于一个dump请求多次调用dumpit返回响应,由dumpit的返回值决定。下面的NLM_F_DUMP 请求处理部分可以看到更多介绍。单播数据包分发流程可以看到其生效位置。 - done
与dumpit对应,在dumpit的一个完整逻辑完成后将会调用该处理函数。 - cmd
该ops对应处理的cmd,对应genlmsghdr中的cmd。 - internal_flags
family自定义使用,内核不关心该字段。 - flags
标明该ops中cmd对应的请求,需要进程拥有的权限,单播数据包分发流程可以看到有对其的检查。
1 | struct genl_multicast_group { |
1 | struct genl_info { |
genl_info由内核generic netlink子系统填充并传递给用户自定义的处理函数,简化用户的解析操作。
- snd_seq
发送方nlmsg_seq。 - snd_portid
发送方portid。 - nlhdr
nlmsghdr结构指针。 - genlhdr
genlmsghdr结构指针。 - userhdr
genl_family注册时标明长度的自定义头的地址。 - attrs
内核解析的nlattr指针数组,索引为nla_type,只解析最外层的nlattr。如果相同nla_type的nlattr,后面的会覆盖前面的填充。 - _net
skb所属sk的net namespace,取对应的net namespace使用函数genl_info_net
。 - user_ptr
这个东西应该是给用户自己使用的,内核不关心。
例子代码
1 |
|
1 |
|
1 |
|
1 | obj-m := genl_kern.o |
1 | my pid: 26758 |
1 | [985691.316473] my init. |
generic netlink 实现
generic netlink的实现代码位于/net/netlink/genetlink
实现原理上可以简单描述如下:
- 组播依赖netlink的原生组播功能,generic netlink负责对组播组id进行分配管理。
- 单播由netlink传递到generic netlink消息总线(这个消息总线就是一个统一的处理入口函数),通过family id定位到genl_family,进一步通过cmd定位到genl_ops进行处理。
- controller服务负责对family name和family id进行名字翻译处理,controller的family id是固定的
GENL_ID_CTRL
。
下面是generic netlink howto(文章中数据结构已经比较旧)中描述的基本架构图。
1 | +---------------------+ +---------------------+ |
初始化
generic netlink的初始化工作由函数genl_init
完成。
1 | static int __init genl_init(void) |
这个函数内容很少,两个功能。
- 通过函数genl_register_family在generic netlink协议下注册了一个controller名字服务。
- genl_register_family的实现包含了generic netlink如果通过自定义协议名承载·
- controller与用户的自定义协议使用同样的api注册,实现了通过自定义协议名查询相关信息的功能(比如id和广播组)。
- 为每个net namespace注册了函数调用。
这个函数会对每个net namespace(包括未来创建的新的net namespace)执行,用于在netlink协议族上注册generic netlink协议。
这两个工作完成的先后并没有严格的依赖关系。先看对每个net namespace注册的函数。
1 | static struct pernet_operations genl_pernet_ops = { |
1 | static int __net_init genl_pernet_init(struct net *net) |
到函数genl_pernet_init
就很好理解了,结合之前netlink的介绍,就是在netlink协议族上注册协议号为NETLINK_GENERIC
的协议,允许非特权用户接收广播,数据包在内核的接收处理函数为genl_rcv
,每个net namespace中创建的sk赋值给net->genl_sock
用于内核发送generic netlink数据包时使用。用户态发送给内核的所有generic netlink数据包,不区分协议名,入口处理函数都是genl_rcv
,这个函数我们晚一点再看,先看一下genl_register_family
如何注册一个自定义协议到generaic netlink上,然后再看genl_rcv
如何将数据包分发给不同的自定义协议。
自定义协议注册
genl_register_family
主要完成两个功能:
- 为注册的genl_family分配一个唯一id,并将指针保存起来后续将使用
- 如果注册有组播,还会为组播分配连续id的组播组,其实组播组id记录在mcgrp_offset字段中
1 |
|
单播数据包分发
通过前面的注册流程可以看到generic netlink组播依赖netlink的组播机制,这里继续看单播数据包的是如何分发的。之前分析过netlink内核收包流程,这里直接分析generic netlink在内核的收包处理函数genl_rcv
1 | +--------+ |
NLM_F_DUMP 请求处理
dump的处理涉及一个结构体struct netlink_callback
1 | struct netlink_callback { |
在dumpit
和done
函数中作为参数传递的struct netlink_callback
类型指针指向的实际是struct netlink_sock
中成员cb
,同时nlk成员cb_running
标记了一次dump正在运行中,如果在一次dump未完成时有另外的dump请求到来将返回-EBUSY
,注意这里仅仅阻止同一个socket的并发dump,不同socket间并不影响。
dump部分的调用栈为
genl_family_rcv_msg
-> __netlink_dump_start
-> netlink_dump
__netlink_dump_start
负责检查nlk的cb_running
判断是否有dump未完成,填充cb各成员,并调用netlink_dump
netlink_dump
分配了一个skb准备交给dumpit
函数填充dump内容,调用cb->dump
也就是dumpit
。这里对dumpit
返回值的判断值得注意:- 返回值大于0
含义为当次dump未结束,需要后续继续调用dumpit
,因此直接发送skb,并不会修改cb_running
状态。 - 返回值小于等于0
含义为当次dump已经完成,0为正常完成,负数为错误码的负值。由于当次dump已经完成,因此会在skb中添加一个类型为NLMSG_DONE的netlink消息用于向用户程序标记当次dump已经完成,并修改cb_running
为false,释放cb保存的相关资源。
- 返回值大于0
通过上面的分析,可以看到一次完整的dump可能不会在该次请求的上下文中完成,因此会有其他位置继续完成该次dump过程。这个位置是函数netlink_recvmsg
,也就是用户程序后续接收消息时将会继续dump过程,该调用栈为:
netlink_recvmsg
-> netlink_dump
如果一个dump未完成,而对应的socket已经关闭,则不会继续调用dumpit
,而是在close时调用一次done
函数通知内核该次dump到此为止。该调用栈为:
netlink_release
中调用call_rcu
传入deferred_put_nlk_sk
。
deferred_put_nlk_sk
-> sock_put
-> sk_free
-> __sk_free
-> sk_destruct
-> __sk_destruct
-> netlink_sock_destruct
其中netlink_sock_destruct
是由函数指针sk->sk_destruct
调用的。该函数指针在__netlink_create
中被设置,参考netlink相关部分。
通过dump部分内核代码分析,及参考controller中ctrl_dumpfamily
的实现,可以推荐dumpit
函数实现方式。
- 使用
cb->args
数组做状态保存。- 如果对dump存在
cb->args
之外的资源分配和状态保存,需要在done
函数中做清理。 - 如果对dump没有
cb->args
之外的资源分配和状态保存,则可以不使用done
函数。
- 如果对dump存在
- 如果有数据需要dump,填充到skb中。
- 如果skb空间不足,则填充一部分数据,并返回skb中数据长度。
- 如果skb空间充足,则填充数据,同样返回skb中数据长度。
- 如果不再需要填充数据,返回0。
这种实现方式的好处是,最后一次调用dumpit
时不会填充skb,因此会保留足够的空间给NLMSG_DONE
类型的netlink消息使用,不会因为空间不足而导致失败,内核对NLMSG_DONE
类型netlink消息空间不足的处理并不友好。
由于此小节是后续补充的内容,因此代码片段单独列出供参考。
1 | static int add_dumpit(struct sk_buff *skb, struct netlink_callback *cb) { |
1 | int test_dump(unsigned int family_id, int fd) { |
controller服务
controller的工作是提供名字服务,提交genl_family的name,返回相关信息,最主要的就是family的id。
从下面的数据结构可以看到controller服务仅接受一个cmd,也就是CTRL_CMD_GETFAMILY,与用户自定义协议不同之处在于这里id是固定的,因为controller服务是首个服务,也只有固定id,才能让后面的查询请求可以准确发送到controller,才能完成名字查询服务。具体流程这里不描述了,返回内容格式可以参考例子代码中用户态解析。
1 | static struct genl_ops genl_ctrl_ops[] = { |