Generic+Netlink内核实现分析(二):通信
前一篇博文中分析了Generic Netlink的消息结构及内核初始化流程,本文中通过一个示例程序来了解Generic Netlink在内核和应用层之间的单播通信流程。
示例程序:demo_genetlink_kern.c(内核模块)、demo_genetlink_user.c(应用层Demo程序)、demo_genetlink.h
程序主要功能:应用层程序接收用户的输入“字符串”和“数据”向内核发送,内核接收后回发应用层,应用层通过终端打印输出。
代码路径: https://github.com/luckyapple1028/demo-genetlink
Netlink套接字多播发送CTRL_CMD_NEWFAMILY消息,通知应用层有新的family注册了,这样应用层就可以捕获这一消息。详细分析一下:
图1 消息的封装流程(a)(b)(c)
首先确定各个入参的内容:family为新注册的demo_family;portid和seq为0,表示消息的发送端为内核,发送消息序号为0;最后的cmd为CTRL_CMD_NEWFAMILY。函数首先调用genlmsg_put()函数初始化netlink消息头和genetlink消息头;
nlh->nlmsg_type :内核genl_ctrl family簇的ID 号GENL_ID_CTRL;
nlh->nlmsg_len :消息长度,即genetlink头+用户私有头+netlink头的长度总和;
nlh->nlmsg_flags:0;
nlh->nlmsg_pid:发送端的ID号为0,表示又内核发送;
nlh->nlmsg_seq:0;
初始化完成后将内存对齐用的空白区刷为0;然后回到genlmsg_put()函数中继续分析:
hdr->cmd:消息的cmd命令,CTRL_CMD_NEWFAMILY;
hdr->version:genl_ctrl family簇的version;
hdr->reserved:0;
填充完毕后返回消息用户私有头(若有)或实际载荷的首地址,此时的消息skb中的消息填充如图1-a所示。然后再回到ctrl_fill_info()函数中,接下来就要开始填充实际的数据了:
nla->nla_type:attr属性,即前文中的CTRL_ATTR_FAMILY_NAME等;
nla->nla_len:attr属性长度(attrlen+NLA_HDRLEN);
然后再将attr属性中的实际数据拷贝到预留测空间中,如此一个attr属性就添加完成了,此时的消息skb中的消息填充如图1-b所示。再回到ctrl_fill_info()函数中:
然后回到ctrl_fill_info()函数中继续往下是一个for循环,在每个循环中向刚才新创建的一级嵌套attr属性中添加属性。它首先会根据operations中实现的回调函数封装flag,然后依旧是调用nla_nest_start()函数再次创建新的一级嵌套,不过这次的attrtype为函数的序列,随后在消息中追加上回调函数处理的cmd以及flag,最后调用nla_nest_end()函数结束这一层attr嵌套:
ctrl_fill_info()函数接下来会再判断family->n_mcgrps字段,若存在组播组,会同operations一样增加一级和operations平级的attr嵌套然后添加CTRL_ATTR_MCAST_GROUPS属性,这里就不详细分析了。在函数的最后调用nla_nest_end()完成本次消息封装:
至此Demo Genetlink的内核创建流程就全部结束了,此时应用层可以通过Ctrl簇获取它的ID号并向他发送消息了。下面来分析应用层是如何初始化genetlink套接字的。
接下来初始化sockaddr_nl地址结构并进行绑定操作,为了简单起见,这里使用进程ID进行绑定(该值全局唯一),在实际的程序中可自行安排。绑定的流程前博文也已经分析过了,会调用到netlink回调函数netlink_bind(),该函数会将绑定的ID号添加到全局nl_table中。这里有一点需要说明的就是在前一篇博文中已经看到内核genetlink套接字已经指定了bind回调函数为genl_bind,这里如果指定了多播组地址nl_groups,会调用到该回调函数进行多播组的绑定操作。简单看一下:
至此,应用层genetlink套接字初始化完成,下面来分析它是如何发送消息到前文中注册的内核demo genelink套接字的。
另外,在一般的程序中,如果应用层无需向内核发送消息,仅仅需要接收内核发送的消息时,它并不需要通过Ctrl簇获取family id了,仅需要接收内核的genetlink消息并做好cmd和attr类型判断并做出相应的处理即可。
图2 查询Family ID的调用流程
name拷贝到属性attr的payload载荷中,最后更新各个消息头中的长度字段。
消息分装完成后调用sendto系统调用启动发送流程,指定目的地址的地址簇为AF_NETLINK,ID号为0(表示内核)。sendto函数同前博文 《Netlink 内核实现分析(二):通信》中分析的sendmsg()系统调用类似(sendto的msg消息封装过程由内核完成),最后都是调用到sock_sendmsg()函数,具体的中间发送流程前博文中已详细描述,这里不再赘述,直接进入到发送的最后阶段,来看Ctrl簇是如何处理接收到的查询消息的。
在netlink函数调用流程的最后会调用具体协议类型的netlink_rcv()回调函数,其中genetlink的回调函数在前文中已经看到为genl_rcv():
第四个参数为attr属性的长度:nlmsg_attrlen(nlh, hdrlen):
函数最后调用genlmsg_reply()向应用层回发消息:
family的genetlink单播通信过程就大致分析完毕,另外关于多播的通信流程也比较简单,不难掌握,以后有时间再行补充。
参考文献:1、《Linux Kernel Networking Implementation and Theory》;2、《Generic Netlink详解》
示例程序:demo_genetlink_kern.c(内核模块)、demo_genetlink_user.c(应用层Demo程序)、demo_genetlink.h
程序主要功能:应用层程序接收用户的输入“字符串”和“数据”向内核发送,内核接收后回发应用层,应用层通过终端打印输出。
代码路径: https://github.com/luckyapple1028/demo-genetlink
一、创建内核Demo Genetlink
1、定义Demo Genetlink
enum { DEMO_CMD_UNSPEC = 0, /* Reserved */ DEMO_CMD_ECHO, /* user->kernel request/get-response */ DEMO_CMD_REPLY, /* kernel->user event */ __DEMO_CMD_MAX, }; #define DEMO_CMD_MAX (__DEMO_CMD_MAX - 1) enum { DEMO_CMD_ATTR_UNSPEC = 0, DEMO_CMD_ATTR_MESG, /* demo message */ DEMO_CMD_ATTR_DATA, /* demo data */ __DEMO_CMD_ATTR_MAX, }; #define DEMO_CMD_ATTR_MAX (__DEMO_CMD_ATTR_MAX - 1)定义两种类型的Genetlink cmd指令,其中DEMO_CMD_ECHO用于应用层下发数据,DEMO_CMD_REPLY用于内核向应用层回发数据;同时定义两种类型的attr属性参数,其中DEMO_CMD_ATTR_MESG表示字符串,DEMO_CMD_ATTR_DATA表示数据。
static struct genl_family demo_family = { .id = GENL_ID_GENERATE, .name = DEMO_GENL_NAME, .version = DEMO_GENL_VERSION, .maxattr = DEMO_CMD_ATTR_MAX, };定义demo_family,其中ID号为GENL_ID_GENERATE,表示由内核统一分配,maxattr为DEMO_CMD_ATTR_MAX,其前文中定义的最大attr属性数,内核将为其分配缓存空间。
static const struct genl_ops demo_ops[] = { { .cmd = DEMO_CMD_ECHO, .doit = demo_echo_cmd, .policy = demo_cmd_policy, .flags = GENL_ADMIN_PERM, }, };定义操作函数集operations:demo_ops,这里只为DEMO_CMD_ECHO类型的cmd创建消息处理函数接口(因为DEMO_CMD_REPLY类型的cmd用于内核消息,应用层不使用),指定doit消息处理回调函数为demo_echo_cmd,同时指定有效组策略为demo_cmd_policy。
static const struct nla_policy demo_cmd_policy[DEMO_CMD_ATTR_MAX+1] = { [DEMO_CMD_ATTR_MESG] = { .type = NLA_STRING }, [DEMO_CMD_ATTR_DATA] = { .type = NLA_S32 }, };这里限定DEMO_CMD_ATTR_MESG的属性类型为NLA_STRING(字符串类型),限定DEMO_CMD_ATTR_DATA的属性类型为NLA_S32(有符号32位数)。
2、内核注册Demo Genetlink
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); 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()同时注册demo_family及demo_ops,该函数同前面创建CTRL类型的family簇类似,最终都是调用_genl_register_family_with_ops_grps函数完成创建。这个函数已经大致分析过了,此处的注册流程基本一致,主要区别在于最后的send all events,它向所有的应用层加入CTRL控制器簇组播组的Generic
Netlink套接字多播发送CTRL_CMD_NEWFAMILY消息,通知应用层有新的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; /* genl is still initialising */ 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; }函数首先判断是否已经注册了控制器CTRL簇,这里显然已经注册过了,然后主要的工作在ctrl_build_family_msg()中:
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); 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_new()函数创建netlink类型的skb,第一个入参是消息的长度,第二个参数为内存空间分配类型,这里分配的数据空间(包括netlink消息头)一共为一个page。进入nlmsg_new内部:
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; }可以看到总共预留的空间为NLMSG_ALIGN(NLMSG_HDRLEN+NLMSG_DEFAULT_SIZE),这里实际可能用不了这么多的空间,接下来调用ctrl_fill_info()填充消息内容:
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); if (hdr == NULL) return -1;这个函数比较长,这里使用插图的形式来观察消息的封装流程(图中未显示空白Pad区):
图1 消息的封装流程(a)(b)(c)
首先确定各个入参的内容:family为新注册的demo_family;portid和seq为0,表示消息的发送端为内核,发送消息序号为0;最后的cmd为CTRL_CMD_NEWFAMILY。函数首先调用genlmsg_put()函数初始化netlink消息头和genetlink消息头;
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); if (nlh == NULL) return NULL;其中nlmsg_put()函数向skb缓冲区中获取消息头空间并且初始化netlink消息头,入参中的第5个参数为genetlink消息头和用户私有消息头(这里并未使用)的总空间,实际调用的函数为__nlmsg_put():
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)); nlh->nlmsg_type = type; nlh->nlmsg_len = size; nlh->nlmsg_flags = flags; nlh->nlmsg_pid = portid; nlh->nlmsg_seq = seq; if (!__builtin_constant_p(size) || NLMSG_ALIGN(size) - size != 0) memset(nlmsg_data(nlh) + len, 0, NLMSG_ALIGN(size) - size); return nlh; }首先这里的分配的空间大小为size = 传入的len长度 + netlink消息头的长度,然后初始化netlink消息头的各个字段:
nlh->nlmsg_type :内核genl_ctrl family簇的ID 号GENL_ID_CTRL;
nlh->nlmsg_len :消息长度,即genetlink头+用户私有头+netlink头的长度总和;
nlh->nlmsg_flags:0;
nlh->nlmsg_pid:发送端的ID号为0,表示又内核发送;
nlh->nlmsg_seq:0;
初始化完成后将内存对齐用的空白区刷为0;然后回到genlmsg_put()函数中继续分析:
hdr = nlmsg_data(nlh); hdr->cmd = cmd; hdr->version = family->version; hdr->reserved = 0; return (char *) hdr + GENL_HDRLEN; }这里通过宏nlmsg_data获取genetlink消息头的地址,然后开始填充该消息头的各个字段:
hdr->cmd:消息的cmd命令,CTRL_CMD_NEWFAMILY;
hdr->version:genl_ctrl family簇的version;
hdr->reserved:0;
填充完毕后返回消息用户私有头(若有)或实际载荷的首地址,此时的消息skb中的消息填充如图1-a所示。然后再回到ctrl_fill_info()函数中,接下来就要开始填充实际的数据了:
if (nla_put_string(skb, CTRL_ATTR_FAMILY_NAME, family->name) || nla_put_u16(skb, CTRL_ATTR_FAMILY_ID, family->id) || 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;这里将新注册的family结构中的几个字段都填充到了消息中,包括name、id号、版本号、私有头长度以及maxattr(注意属性需要一一对应),调用的函数nla_put_string、nla_put_u16和nla_put_u32都是nla_put()的封装,而nla_put实际调用的是__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; }
void __nla_put(struct sk_buff *skb, int attrtype, int attrlen, const void *data) { struct nlattr *nla; nla = __nla_reserve(skb, attrtype, attrlen); memcpy(nla_data(nla), data, attrlen); }__nla_put()的作用是向skb中添加一个netlink attr属性,入参分别为skb地址、要添加的attr属性类型、属性长度和属性实际数据。首先调用了__nla_reserve在skb中预留出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->nla_type = attrtype; nla->nla_len = nla_attr_size(attrlen); memset((unsigned char *) nla + nla->nla_len, 0, nla_padlen(attrlen)); return nla; }这里首先预留空间长度为nla_total_size(attrlen),即attrlen+NLA_HDRLEN(属性头长度)+对齐用内存空白;然后初始化属性头的两个字段:
nla->nla_type:attr属性,即前文中的CTRL_ATTR_FAMILY_NAME等;
nla->nla_len:attr属性长度(attrlen+NLA_HDRLEN);
然后再将attr属性中的实际数据拷贝到预留测空间中,如此一个attr属性就添加完成了,此时的消息skb中的消息填充如图1-b所示。再回到ctrl_fill_info()函数中:
if (family->n_ops) { struct nlattr *nla_ops; int i; nla_ops = nla_nest_start(skb, CTRL_ATTR_OPS); 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]; 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); 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(skb, nla_ops); }然后如果新注册的family簇也同时注册了操作接口operations,这里会追加上对应的attr属性参数;但同前面不同的是,这里追加的attr参数是“打包”在一起的,使用的属性为CTRL_ATTR_OPS。由于netlink的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) return NULL; return start; }可以看到这里调用的依然是nla_put()函数,不过这里的入参中指定的attr长度为0,然后数据为NULL,那这里其实就是向skb中添加了一段attr属性头,然后指定它的属性nla_type为CTRL_ATTR_OPS,属性nla_len为0,注意函数返回的是添加嵌套attr头之前的消息有效数据末尾地址。
然后回到ctrl_fill_info()函数中继续往下是一个for循环,在每个循环中向刚才新创建的一级嵌套attr属性中添加属性。它首先会根据operations中实现的回调函数封装flag,然后依旧是调用nla_nest_start()函数再次创建新的一级嵌套,不过这次的attrtype为函数的序列,随后在消息中追加上回调函数处理的cmd以及flag,最后调用nla_nest_end()函数结束这一层attr嵌套:
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属性头的nla_len字段为本嵌套属性的实际长度,实现的方式为当前的消息末尾地址减去创建该级嵌套之前的消息末尾地址(这就是nla_nest_start()函数要返回start地址的原因了)。回到ctrl_fill_info()函数中,在for循环结束以后,依旧调用nla_nest_end来结束CTRL_ATTR_OPS的那一层attr嵌套,此时的消息skb中的消息填充如图1-c所示。
ctrl_fill_info()函数接下来会再判断family->n_mcgrps字段,若存在组播组,会同operations一样增加一级和operations平级的attr嵌套然后添加CTRL_ATTR_MCAST_GROUPS属性,这里就不详细分析了。在函数的最后调用nla_nest_end()完成本次消息封装:
static inline void genlmsg_end(struct sk_buff *skb, void *hdr) { nlmsg_end(skb, hdr - GENL_HDRLEN - NLMSG_HDRLEN); }该函数间接调用nlmsg_end()函数,注意第二个入参为消息attr载荷的首地址减去2个头的长度,即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控制器簇发送的消息就已经封装完成了,再回到最上层的genl_ctrl_event()中:
if (IS_ERR(msg)) return PTR_ERR(msg); if (!family->netnsok) { genlmsg_multicast_netns(&genl_ctrl, &init_net, msg, 0, 0, GFP_KERNEL); } else { rcu_read_lock(); genlmsg_multicast_allns(&genl_ctrl, msg, 0, 0, GFP_ATOMIC); rcu_read_unlock(); }这里根据是否支持net命名空间来选择发送的流程,genlmsg_multicast_allns函数从命名中就可以看出会像所有命名空间的控制器簇发送消息,而genlmsg_multicast_netns则指定了向init_net发送,不论哪一种情况,最后都是调用nlmsg_multicast()函数。不过这里有一点需要注意的就是这里的第三个入参portid为0,这是为了防止向发送端发送报文,这也就表明内核控制器簇套接字是不会接受该广播报文的(内核也不应该接收,否则会panic,可参见netlink_data_ready()函数的实现)。
至此Demo Genetlink的内核创建流程就全部结束了,此时应用层可以通过Ctrl簇获取它的ID号并向他发送消息了。下面来分析应用层是如何初始化genetlink套接字的。
二、应用层初始化Genetlink套接字
int main(int argc, char* argv[]) { ...... /* 初始化socket */ nl_fd = demo_create_nl_socket(NETLINK_GENERIC); if (nl_fd < 0) { fprintf(stderr, "failed to create netlink socket\n"); return 0; } ...... }
static int demo_create_nl_socket(int protocol) { int fd; struct sockaddr_nl local; /* 创建socket */ fd = socket(AF_NETLINK, SOCK_RAW, protocol); 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; }应用层依然通过socket系统调用创建AF_NETLINK地址簇的SOCK_RAW套接字,指定协议类型为NETLINK_GENERIC,创建的流程同前博文 《Netlink 内核实现分析(一):创建》中分析的NETLINK_ROUTE类似,这里不再赘述。
接下来初始化sockaddr_nl地址结构并进行绑定操作,为了简单起见,这里使用进程ID进行绑定(该值全局唯一),在实际的程序中可自行安排。绑定的流程前博文也已经分析过了,会调用到netlink回调函数netlink_bind(),该函数会将绑定的ID号添加到全局nl_table中。这里有一点需要说明的就是在前一篇博文中已经看到内核genetlink套接字已经指定了bind回调函数为genl_bind,这里如果指定了多播组地址nl_groups,会调用到该回调函数进行多播组的绑定操作。简单看一下:
if (nlk->netlink_bind && groups) { int group; for (group = 0; group < nlk->ngroups; group++) { if (!test_bit(group, &groups)) continue; err = nlk->netlink_bind(net, group + 1); if (!err) continue; netlink_undo_bind(group, groups, sk); return err; } }这里在genetlink支持的最大组播数中进行轮询,检测用户需要绑定的多播组并将其转换为位序号,然后调用netlink_bind回调函数,这里该函数就是genl_bind():
static int genl_bind(struct net *net, int group) { int i, err = -ENOENT; down_read(&cb_lock); for (i = 0; i < GENL_FAM_TAB_SIZE; i++) { struct genl_family *f; list_for_each_entry(f, genl_family_chain(i), family_list) { if (group >= f->mcgrp_offset && group < f->mcgrp_offset + f->n_mcgrps) { int fam_grp = group - f->mcgrp_offset; if (!f->netnsok && net != &init_net) err = -ENOENT; else if (f->mcast_bind) err = f->mcast_bind(net, fam_grp); else err = 0; break; } } } up_read(&cb_lock); return err; }由于不同family类型的genetlink都共用同一个组播地址空间,所以这里根据用户输入的组播号来查找对应的family,然后会调用该family对应的mcast_bind()回调函数,它需要根据family的需求自行实现,可用于做进一步的特殊需求处理,不实现亦可(目前内核中注册的genl_family均未使用到该接口)。
至此,应用层genetlink套接字初始化完成,下面来分析它是如何发送消息到前文中注册的内核demo genelink套接字的。
三、用户空间和内核空间通信
用户空间想要发送消息到内核的demo genelink套接字,它首先得知道内核分配的demo family的family id号,因为genelink子系统是根据该id号来区分不同family簇的genelink套接字和分发消息的。此时前文中的ctrl就用于该目的,它可以将family name转换为对应的family id,用户空间也通过family name向ctrl簇查询对应的family id。在应用层序获取了family id后它就可以像内核发送消息,该消息分别包含了字符串和数据,同时内核也在接受后进行回发操作。另外,在一般的程序中,如果应用层无需向内核发送消息,仅仅需要接收内核发送的消息时,它并不需要通过Ctrl簇获取family id了,仅需要接收内核的genetlink消息并做好cmd和attr类型判断并做出相应的处理即可。
1、用户查询Demo Family ID
图2 查询Family ID的调用流程
int main(int argc, char* argv[]) { ...... /* 获取family id */ nl_family_id = demo_get_family_id(nl_fd); if (!nl_family_id) { fprintf(stderr, "Error getting family id, errno %d\n", errno); goto out; } PRINTF("family id %d\n", nl_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 */ 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); if (ans.n.nlmsg_type == NLMSG_ERROR || (rep_len < 0) || !NLMSG_OK((&ans.n), rep_len)) return 0; /* 解析family id */ 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); } return id; }该demo_get_family_id()函数比较简单,仅仅是封装查询消息并向内核的ctrl簇发送,然后接收内核的回发结果然后解析出其中的family id,具体的消息发送函数由demo_send_cmd()封装函数来完成,其中入参分别是socket fd、ctrl family id、消息发送端netlink绑定ID号、消息cmd类型、消息attr属性、消息正文内容、消息正文长度。
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) */ 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; msg.g.cmd = genl_cmd; msg.g.version = DEMO_GENL_VERSION; na = (struct nlattr *) GENLMSG_DATA(&msg); na->nla_type = nla_type; na->nla_len = nla_len + 1 + NLA_HDRLEN; memcpy(NLA_DATA(na), nla_data, nla_len); 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) { if (r > 0) { buf += r; buflen -= r; } else if (errno != EAGAIN) return -1; } return 0; }消息的封装过程同内核态消息封装过程类似,需严格按照genelink消息格式进行封装。首先填充netlink消息头,其中nlmsg_type字段不使用netlink定义的标准type,填充为目标family的ID号,其他字段同其他类型的netlink类似;然后填充genetlink消息头,这里设定消息cmd字段为CTRL_CMD_GETFAMILY,version字段为DEMO_GENL_VERSION(同内核保持一致);最后填充一个attr属性,其中属性头的nla_type设定为函数传入的属性type,现该值为CTRL_ATTR_FAMILY_NAME,然后将传入的family
name拷贝到属性attr的payload载荷中,最后更新各个消息头中的长度字段。
消息分装完成后调用sendto系统调用启动发送流程,指定目的地址的地址簇为AF_NETLINK,ID号为0(表示内核)。sendto函数同前博文 《Netlink 内核实现分析(二):通信》中分析的sendmsg()系统调用类似(sendto的msg消息封装过程由内核完成),最后都是调用到sock_sendmsg()函数,具体的中间发送流程前博文中已详细描述,这里不再赘述,直接进入到发送的最后阶段,来看Ctrl簇是如何处理接收到的查询消息的。
在netlink函数调用流程的最后会调用具体协议类型的netlink_rcv()回调函数,其中genetlink的回调函数在前文中已经看到为genl_rcv():
static void genl_rcv(struct sk_buff *skb) { down_read(&cb_lock); netlink_rcv_skb(skb, &genl_rcv_msg); up_read(&cb_lock); }这里netlink_rcv_skb函数的两个入参其中第一个为消息skb,第二个为genl_rcv_msg回调函数;netlink_rcv_skb()函数会对消息进行一些通用性的处理,将用户消息封装成genl_info结构,最后会把消息控制权交给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)) { 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 */ 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; }首先判断消息的长度是否不小于netlink消息头的长度(现在的上下文中显然成立),然后进入while循环开始处理存放在skb中的netlink消息(可能有多个)。循环处理中会首先进行一些基本的数据长度判断,然后根据nlmsg_flags和nlmsg_type字段判断是否跳过消息处理流程、以及是否回发ACK相应。目前由于设定的nlmsg_flags为NLM_F_REQUEST、nlmsg_type为GENL_ID_CTRL(即NLMSG_MIN_TYPE),因此调用genl_rcv_msg()回调函数开始消息处理流程:
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); if (family == NULL) return -ENOENT; if (!family->parallel_ops) genl_lock(); err = genl_family_rcv_msg(family, skb, nlh); if (!family->parallel_ops) genl_unlock(); return err; }该函数首先通过nlmsg_type字段(即family id号)在散列表中查找到对应的注册family,然后如果消息处理不可重入,则这里会上锁,接下来调用genl_family_rcv_msg()函数:
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)) return -ENOENT; hdrlen = GENL_HDRLEN + family->hdrsize; if (nlh->nlmsg_len < nlmsg_msg_size(hdrlen)) return -EINVAL;函数首先判断网络命名空间,若不支持则当前消息的网络空间必须为init_net,然后判断消息的长度。
ops = genl_get_cmd(hdr->cmd, family); if (ops == NULL) return -EOPNOTSUPP;
static const struct genl_ops *genl_get_cmd(u8 cmd, struct genl_family *family) { int i; for (i = 0; i < family->n_ops; i++) if (family->ops[i].cmd == cmd) return &family->ops[i]; return NULL; }这里找到消息cmd命令对应的处理函数并保存早ops变量中,查找的方式是通过cmd字段的匹配类型来找的,这里找到的就是前文中注册的demo_ops结构了。
if ((ops->flags & GENL_ADMIN_PERM) && !netlink_capable(skb, CAP_NET_ADMIN)) return -EPERM;接下来判断权限,这里由于已经在demo_ops中设置了GENL_ADMIN_PERM标识,因此本命令操作需要具有CAP_NET_ADMIN权限。
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; }如果用户设定了NLM_F_DUMP标识,这里就会调用启动dump流程,回填skb消息(这里的skb将不再是用户下发的消息了)。这里不进行详细的分析,继续往下看:
if (family->maxattr && family->parallel_ops) { attrbuf = kmalloc((family->maxattr+1) * sizeof(struct nlattr *), GFP_KERNEL); if (attrbuf == NULL) return -ENOMEM; } else attrbuf = family->attrbuf;这里为attr属性指定接收缓存,在支持重入的情况下这里会另行动态分配内存,否则使用在注册family的__genl_register_family函数中分配的内存空间。需要注意的是这里的内存其实只是一个指针数组,用来存放attr属性的地址,并不会存放实际的属性数据。
if (attrbuf) { err = nlmsg_parse(nlh, hdrlen, attrbuf, family->maxattr, ops->policy); if (err < 0) goto out; }这里将消息的数据拷贝到缓存空间中去,nlmsg_parse()的几个入参分别为netlink消息头,genelink消息头长度(其实也包括了用户私有头,只不过这里为0罢了),数据属性缓存地址,缓存空间大小和属性有效性策略结构。
static inline int nlmsg_parse(const struct nlmsghdr *nlh, int hdrlen, struct nlattr *tb[], int maxtype, const struct nla_policy *policy) { if (nlh->nlmsg_len < nlmsg_msg_size(hdrlen)) return -EINVAL; return nla_parse(tb, maxtype, nlmsg_attrdata(nlh, hdrlen), nlmsg_attrlen(nlh, hdrlen), policy); }该函数间接调用netlink通用的属性拷贝函数,其中将第三个参数为attr参数的首地址:nlmsg_attrdata(nlh, hdrlen):
static inline struct nlattr *nlmsg_attrdata(const struct nlmsghdr *nlh, int hdrlen) { unsigned char *data = nlmsg_data(nlh); return (struct nlattr *) (data + NLMSG_ALIGN(hdrlen)); }这里将指针跳过netlink的头以及genelink头,指向attr的首地址。
第四个参数为attr属性的长度:nlmsg_attrlen(nlh, hdrlen):
static inline int nlmsg_attrlen(const struct nlmsghdr *nlh, int hdrlen) { return nlmsg_len(nlh) - NLMSG_ALIGN(hdrlen); }计算方式为消息除去netlink消息头的剩余长度减去genetlink消息头长度后的长度。
int nla_parse(struct nlattr **tb, int maxtype, const struct nlattr *head, int len, const struct nla_policy *policy) { const struct nlattr *nla; int rem, err; memset(tb, 0, sizeof(struct nlattr *) * (maxtype + 1)); nla_for_each_attr(nla, head, len, rem) { u16 type = nla_type(nla); if (type > 0 && type <= maxtype) { if (policy) { err = validate_nla(nla, maxtype, policy); if (err < 0) goto errout; } tb[type] = (struct nlattr *)nla; } } if (unlikely(rem > 0)) pr_warn_ratelimited("netlink: %d bytes leftover after parsing attributes in process `%s'.\n", rem, current->comm); err = 0; errout: return err; }可以看到该函数会逐一的将属性的地址复制到tb指针数组中去,但是如果传入了有效性策略,那他就会调用validate_nla函数执行有效性判断。对于这里传入的CTRL_ATTR_FAMILY_NAME属性来说,在ctrl_policy中已经定义了有效性限制为NLA_NUL_STRING,最大长度为GENL_NAMSIZ-1:
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 }, };回到genl_family_rcv_msg()函数中继续往下分析:
info.snd_seq = nlh->nlmsg_seq; info.snd_portid = NETLINK_CB(skb).portid; info.nlhdr = nlh; info.genlhdr = nlmsg_data(nlh); info.userhdr = nlmsg_data(nlh) + GENL_HDRLEN; info.attrs = attrbuf; info.dst_sk = skb->sk; genl_info_net_set(&info, net); memset(&info.user_ptr, 0, sizeof(info.user_ptr));这里开始封装genl_info消息结构,填充对应的字段,比较好理解,其中snd_portid填充为发送端的套接字ID号,attrs为前文中分配的attr缓存空间首地址,接下来启动最终的调用处理流程:
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);如果在注册family时指定了pre_doit和post_doit回调函数,将在分别调用ops->doit()函数的前后调用他们,对于Ctrl簇而言并没有定义,这里会直接调用ops->doit()回调函数,对于CTRL_CMD_GETFAMILY来说就是ctrl_getfamily()了:
static int ctrl_getfamily(struct sk_buff *skb, struct genl_info *info) { struct sk_buff *msg; struct genl_family *res = NULL; int err = -EINVAL; if (info->attrs[CTRL_ATTR_FAMILY_ID]) { u16 id = nla_get_u16(info->attrs[CTRL_ATTR_FAMILY_ID]); res = genl_family_find_byid(id); err = -ENOENT; }首先函数匹配CTRL_ATTR_FAMILY_ID,由于并未传入该属性数据,因此这里该属性的地址为NULL,然后接着判断另一个属性类型:
if (info->attrs[CTRL_ATTR_FAMILY_NAME]) { char *name; name = nla_data(info->attrs[CTRL_ATTR_FAMILY_NAME]); res = genl_family_find_byname(name); err = -ENOENT; }这里就开始处理CTRL_CMD_GETFAMILY属性了,只做了一件事,就是通过用户传入的family name获取到对应的family结构。
if (res == NULL) return err; if (!res->netnsok && !net_eq(genl_info_net(info), &init_net)) { /* family doesn't exist here */ return -ENOENT; } msg = ctrl_build_family_msg(res, info->snd_portid, info->snd_seq, CTRL_CMD_NEWFAMILY); if (IS_ERR(msg)) return PTR_ERR(msg); return genlmsg_reply(msg, info);这里依然使用ctrl_build_family_msg()函数封装回发消息(该函数的分析见前文),注意回发消息的cmd为CTRL_CMD_NEWFAMILY(它会将查询结果family的全部内容回传),指定的port_id号为消息查询端的id(并不是内核的id号0),消息的sequence也同查询消息一致。
函数最后调用genlmsg_reply()向应用层回发消息:
static inline int genlmsg_reply(struct sk_buff *skb, struct genl_info *info) { return genlmsg_unicast(genl_info_net(info), skb, info->snd_portid); }可以看到它就是nlmsg_unicast的一个封装而已(nlmsg_unicast的实现分析见 《Netlink 内核实现分析(二):通信》)。至此查询消息的发送和内核的处理流程分析完毕,下面回到示例程序demo_get_family_id()中:
/* 接收内核消息 */ rep_len = recv(sd, &ans, sizeof(ans), 0); if (ans.n.nlmsg_type == NLMSG_ERROR || (rep_len < 0) || !NLMSG_OK((&ans.n), rep_len)) return 0; /* 解析family id */ 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); }这里找到回发消息中的第二个attr(消息结构参见图1-c),然后获取出其中的family id号。至此用户程序成功获取的了demo family的id号,接下来就可以向他发送消息了。
2、向内核Demo Family发送消息
/* 发送字符串消息 */ my_pid = getpid(); string = argv[1]; data = atoi(argv[2]); ret = demo_send_cmd(nl_fd, nl_family_id, my_pid, DEMO_CMD_ECHO, DEMO_CMD_ATTR_MESG, string, strlen(string) + 1); if (ret < 0) { fprintf(stderr, "failed to send echo cmd\n"); goto out; } /* 发送数据消息 */ ret = demo_send_cmd(nl_fd, nl_family_id, my_pid, DEMO_CMD_ECHO, DEMO_CMD_ATTR_DATA, &data, sizeof(data)); if (ret < 0) { fprintf(stderr, "failed to send echo cmd\n"); goto out; }本示例程序比较简单,直接使用程序的入参作为发送的数据。发送依然是调用demo_send_cmd函数实现,但是入参同获取family id时的有所不同,首先发送字符串消息时第二个入参设置为刚刚获取的demo family id,然后发送端套接字ID为当前进程的pid号,然后发送cmd为DEMO_CMD_ECHO,发送的属性依次为DEMO_CMD_ATTR_DATA(其实cmd和attr属性并没有明确的一一对应关系,用户可根据需求自行组合,同时一个cmd消息也可以带很多的attr属性,这点从内核ctrl回发的消息就可以看出),最后发送的消息内容分别为用户输入的字符串和数据。
3、内核Demo Family 回发消息
应用层序向demo family发送DEMO_CMD_ECHO消息后,内核会调用到前文中注册时指定的doit回调函数demo_echo_cmd(具体的数据发送流程同前文中分析的Ctrl查询消息,不再详细分析),来看一下demo_echo_cmd()函数所做的处理。static int demo_echo_cmd(struct sk_buff *skb, struct genl_info *info) { if (info->attrs[DEMO_CMD_ATTR_MESG]) return cmd_attr_echo_message(info); else if (info->attrs[DEMO_CMD_ATTR_DATA]) return cmd_attr_echo_data(info); else return -EINVAL; }该函数会判断接收的属性类型,并做出相应的处理(注意:为了简单起见,该doit回调函数最多一次只能处理一种类型的attr属性),先来看cmd_attr_echo_message()函数
static int cmd_attr_echo_message(struct genl_info *info) { struct nlattr *na; char *msg; struct sk_buff *rep_skb; size_t size; int ret; /* 读取用户下发的消息 */ na = info->attrs[DEMO_CMD_ATTR_MESG]; if (!na) return -EINVAL; msg = (char *)nla_data(na); pr_info("demo generic netlink receive echo mesg %s\n", msg); /* 回发消息 */ size = nla_total_size(strlen(msg)+1); /* 准备构建消息 */ ret = demo_prepare_reply(info, DEMO_CMD_REPLY, &rep_skb, size); if (ret < 0) return ret; /* 填充消息 */ ret = demo_mk_reply(rep_skb, DEMO_CMD_ATTR_MESG, msg, size); if (ret < 0) goto err; /* 完成构建并发送 */ return demo_send_reply(rep_skb, info); err: nlmsg_free(rep_skb); return ret; }这里从DEMO_CMD_ATTR_MESG属性地址处取出用户下发的消息内容,然后调用demo_prepare_reply构建回发消息头。其中入参依次为接收genl_info消息,回发cmd类型,skb指针地址,回发数据长度。
static int demo_prepare_reply(struct genl_info *info, u8 cmd, struct sk_buff **skbp, size_t size) { struct sk_buff *skb; void *reply; /* * If new attributes are added, please revisit this allocation */ skb = genlmsg_new(size, GFP_KERNEL); if (!skb) return -ENOMEM; if (!info) return -EINVAL; /* 构建回发消息头 */ reply = genlmsg_put_reply(skb, info, &demo_family, 0, cmd); if (reply == NULL) { nlmsg_free(skb); return -EINVAL; } *skbp = skb; return 0; }这里依然调用genlmsg_new()函数申请skb套接字缓存空间,然后直接调用genlmsg_put_reply()函数构建回发消息的netlink消息头和genetlink消息头:
static inline void *genlmsg_put_reply(struct sk_buff *skb, struct genl_info *info, struct genl_family *family, int flags, u8 cmd) { return genlmsg_put(skb, info->snd_portid, info->snd_seq, family, flags, cmd); }该函数仅仅是genlmsg_put的一个封装而已,注意入参info->snd_portid为用户层的netlink套接字的id号。回到cmd_attr_echo_message()函数中,接下来填充消息属性:
/* 填充消息 */ ret = demo_mk_reply(rep_skb, DEMO_CMD_ATTR_MESG, msg, size); if (ret < 0) goto err;
static int demo_mk_reply(struct sk_buff *skb, int aggr, void *data, int len) { /* add a netlink attribute to a socket buffer */ return nla_put(skb, aggr, len, data); }这里调用nla_put()函数将字符串消息填充到第一个attr属性中,同时指定attr的属性类型为DEMO_CMD_ATTR_MESG,最后调用demo_send_reply()将消息往应用层发送:
/* 完成构建并发送 */ return demo_send_reply(rep_skb, info);
static int demo_send_reply(struct sk_buff *skb, struct genl_info *info) { struct genlmsghdr *genlhdr = nlmsg_data(nlmsg_hdr(skb)); void *reply = genlmsg_data(genlhdr); genlmsg_end(skb, reply); return genlmsg_reply(skb, info); }首先调用genlmsg_end()更新消息头重的长度字段,然后调用genlmsg_reply启动回发流程:
static inline int genlmsg_reply(struct sk_buff *skb, struct genl_info *info) { return genlmsg_unicast(genl_info_net(info), skb, info->snd_portid); }该函数为genlmsg_unicast()的一个封装。这样demo family的回发字符串息就发送出去了。下面再来简单的看一下cmd_attr_echo_data()函数,它同cmd_attr_echo_message()函数基本类似,唯一的区别就是调用了内核提供的nla_get_s32()和nla_put_s32()这两个封装函数来获取和设置s32类型的attr属性,不做过多的论描。
static int cmd_attr_echo_data(struct genl_info *info) { struct nlattr *na; s32 data; struct sk_buff *rep_skb; size_t size; int ret; /* 读取用户下发的数据 */ na = info->attrs[DEMO_CMD_ATTR_DATA]; if (!na) return -EINVAL; data = nla_get_s32(info->attrs[DEMO_CMD_ATTR_DATA]); pr_info("demo generic netlink receive echo data %d\n", data); /* 回发数据 */ size = nla_total_size(sizeof(s32)); ret = demo_prepare_reply(info, DEMO_CMD_REPLY, &rep_skb, size); if (ret < 0) return ret; /* 为了简单这里直接调用netlink库函数(对于需求的丰富可以自行封装) */ ret = nla_put_s32(rep_skb, DEMO_CMD_ATTR_DATA, data); if (ret < 0) goto err; return demo_send_reply(rep_skb, info); err: nlmsg_free(rep_skb); return ret; }内核消息全部单播发送出去以后,下面来看应用层的接收流程。
4、应用层接收内核Demo Family回发消息
int main(int argc, char* argv[]) { ...... /* 接收用户消息并解析(本示例程序中仅解析2个) */ demo_msg_recv_analysis(nl_fd, argc-1); ...... }
void demo_msg_recv_analysis(int sd, int num) { int rep_len; int len; struct nlattr *na; struct msgtemplate msg; unsigned int data; char *string; while (num--) { /* 接收内核消息回显 */ rep_len = recv(sd, &msg, sizeof(msg), 0); if (rep_len < 0 || demo_msg_check(msg, rep_len) < 0) { fprintf(stderr, "nonfatal reply error: errno %d\n", errno); continue; } PRINTF("received %d bytes\n", rep_len); PRINTF("nlmsghdr size=%zu, nlmsg_len=%d, rep_len=%d\n", sizeof(struct nlmsghdr), msg.n.nlmsg_len, rep_len); rep_len = GENLMSG_PAYLOAD(&msg.n); na = (struct nlattr *) GENLMSG_DATA(&msg); len = 0; /* 一个msg里可能有多个attr,所以这里循环读取 */ while (len < rep_len) { len += NLA_ALIGN(na->nla_len); switch (na->nla_type) { case DEMO_CMD_ATTR_MESG: /* 接收到内核字符串回显 */ string = (char *) NLA_DATA(na); printf("echo reply:%s\n", string); break; case DEMO_CMD_ATTR_DATA: /* 接收到内核数据回显 */ data = *(int *) NLA_DATA(na); printf("echo reply:%u\n", data); break; default: fprintf(stderr, "Unknown nla_type %d\n", na->nla_type); } na = (struct nlattr *) (GENLMSG_DATA(&msg) + len); } } }应用程序调用demo_msg_recv_analysis()函数接收内核消息,其中num表示接收消息的个数。该函数中循环调用recv函数阻塞式的接收内核netlink消息。当有消息接收到以后调用demo_msg_check()函数判断消息的有效性:
int demo_msg_check(struct msgtemplate msg, int rep_len) { if (msg.n.nlmsg_type == NLMSG_ERROR || !NLMSG_OK((&msg.n), rep_len)) { struct nlmsgerr *err = NLMSG_DATA(&msg); fprintf(stderr, "fatal reply error, errno %d\n", err->error); return -1; } return 0; }这里首先判断消息头中的nlmsg_type字段,如果该字段为NLMSG_ERROR表示接收到了错误的消息,应该立即丢弃。如果接收到的消息类型无误,则接下来判断消息的长度是否足够,使用的是NLMSG_OK宏(见netlink.h)。然后接收函数循环读取attr属性并根据属性的attr类型单独进行处理,本示例中仅仅在终端中打印。需要补充的是,本程序中并没有对接收到的消息cmd类型进行判断,其实为了程序的可靠性考虑,最好增加这一方面的判断(虽然netlink的id号保证了不会收到其他id的genetlink消息,但是当某family的cmd类型较多时容易引起混乱)。至此demo
family的genetlink单播通信过程就大致分析完毕,另外关于多播的通信流程也比较简单,不难掌握,以后有时间再行补充。
四、总结
本文和前一篇 《Generic Netlink内核实现分析(一):初始化》分析了内核与用户态Generic netlink的创建、创建及通信流程。Generic netlink作为内核netlink机制的一种特殊应用,更好的实现了内核态与用户态之间的全双工通信的拓展,同时本文还给出了一个简单的示例程序用以分析原理,亦可用作日后扩展的原型程序。参考文献:1、《Linux Kernel Networking Implementation and Theory》;2、《Generic Netlink详解》