Generic Netlink内核实现分析(二):通信

wifi底层学习之路:二,无线配置管理服务cfg80211 1,什么是cfg80211?wiphy又是什么? 2,cfg80211如何通过netlink与iw进行交互? 3,cfg80211怎么与mac80211进行交互? 阅读详情

前一篇博文中分析了Generic Netlink的消息结构及内核初始化流程,本文中通过一个示例程序来了解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详解》
————————————————
版权声明:本文为优快云博主「luckyapple1028」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/luckyapple1028/article/details/51232582

Linux内核netLink套接字 本章介绍了Netlink套接字。它提供了一种在用户空间和内核间进行通信的机制,被广泛用于网络子系统。在此你见到了一些Netlink套接字使用示例。另外,本章还讨论了Netlink消息以及它们是被如何创建和处理的。... 阅读详情

相关推荐

内核Generic Netlink通信

内核netlink通信 select函数 Netlink 是一种在内核与用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket API 就可以使用 netlink 提供的强大功能, 内核态需要使用专门的内核 API 来使用 netlink。 Netlink 相对于系统调用,ioctl 以及 /proc文件系统而言具有以下优点: 1,netlink使用简单,只需要在includ...

wefang的博客 1536

Linux 通用Netlink Howto文档翻译

原地址:https://wiki.linuxfoundation.org/networking/generic_netlink_howto 通用Netlink HOWTO 本文简要介绍了通用Netlink,一些简单的例子说明了如何使用它,并就如何充分利用通用Netlink通信接口提出了一些建议。虽然本文不要求读者详细了解Netlink是什么以及它是如何工作的,但这里假设读者具有一些基本的Netlink知识。和往常一样,内核源代码是您在这里最好的朋友。 虽然本文从用户空间的角度简要讨论了Generic Net

远见望远的博客 693

四、无线信息传递——Generic netlink(二、通信)

系列说明  仍旧是系列二: 1、无线驱动信息传递框架:说明无线信息传递的步骤流程以及各程序块之间的联系; 2、generic Netlink信号传递机制:hostapd与无线驱动之间的信息传递机制; 3、以ssid为例说明用户将user space中的ssid配置内容传递至kernel space的流程:以此系统地了解整个无线信息传递流程。   上一篇文章是对generic netlink的初始化和

xpbob的博客 3152

Generic+Netlink内核实现分析(二):通信

Generic+Netlink内核实现分析(二):通信 前一篇博文中分析了Generic Netlink的消息结构及内核初始化流程,本文中通过一个示例程序来了解Generic Netlink在内核和应用层之间的单播通信流程。 示例程序:demo_genetlink_kern.c(内核模块)、demo_genetlink_user.c(应用层Demo程序)、demo_genetlink.h 程

sinat_32343037的博客 1021

linux4.4 内核 netlink,wpa_supplicant与内核nl80211通信之Generic Netlink

代码位置:kernel/net/netlink/genetlink.ckernel/include/net/genetlink.hGENL简介netlink仅支持32种协议类型,这在实际应用中可能并不足够。因此产生了generic netlink(以下简称为genl)。generic netlink支持1023(前10个保留不用)个子协议号,弥补了netlink协议类型较少的缺陷。1 架构及工作原...

weixin_39989875的博客 1618

[Android][Linux][Netlink]内核态和用户态使用netlink方法总结

一、Netlink概述 二、用户态如何使用netlink 三、内核态如何使用netlink 四、内核态和用户态之间使用netlink实例(NETD) 五、内核态和用户态之间使用netlink实例(SYN FIREWALL) 一、Netlink概述 (本段摘抄自网络) Netlink 是一种在内核与用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket A

ganjinrui的专栏 3586

GENERIC NETLINK 介绍及使用

之前一篇博文讲述了Netlink如何使用,当时我们是自己定义了一种协议NETLINK_TEST进行用户进程和内核的通信,这篇文章就讲一下如何使用内核为我们提供的NETLINK_GENERIC进行通信。 如果把Netlink协议比作IP层,那么NETLINK_GENERIC可以比作UDP这一层。它们的具体关系如图1。 图中NETLINK_GENERIC协议作为Netlink层的payload,当然NETLINK_GENERIC作为一个上层协议,也拥有自己的头部(图中的Family头部)和pa...

weixin_39094034的博客 5119

三、无线信息传递——Generic Netlink(一、初始化)

系列说明  上一篇说明了无线信息的user space至kernel space的大致传递流程,这一主要针对以下3点进行一个顺序的描述: 1、无线驱动信息传递框架:说明无线信息传递的步骤流程以及各程序块之间的联系; 2、generic Netlink信号传递机制:hostapd与无线驱动之间的信息传递机制; 3、以ssid为例说明用户将user space中的ssid配置内容传递至kernel sp

xpbob的博客 3130

linux 内核 :Netlink 原理分析

转:https://blog.youkuaiyun.com/luckyapple1028/article/details/50839395 Netlink 是一种IPC(Inter Process Commumicate)机制,它是一种用于内核与用户空间通信的机制,同时它也以用于进程间通信(Netlink 更多用于内核通信,进程之间通信更多使用Unix域套接字)。在一般情况下,用户态和内核态通信会使用传统的I...

老鹰不捉小鸡 4118

linux generic netlink实现机制:通信实例

目录 1 demo 概述 2 创建内核Demo Genetlink 2.1 定义Demo Genetlink 3 内核注册Demo Genetlink 3 应用层初始化Genetlink套接字 4 用户空间和内核空间通信 4.1 用户查询Demo Family ID 4.2 向内核Demo Family发送消息 4.3 内核Demo Family 回发消息 4.4 应用层接收内核Demo Family回发消息 1 demo 概述 示例程序:demo_genetlink_kern.c(

2598

linux generic netlink实现机制:注册、创建

目录 1Generic Netlink 概述 2 Generic Netlink相关结构体 2.1 Generic Netlink消息头结构:struct genlmsghdr 2.2 Generic Netlink Family结构:struct genl_family(内核中完成注册) 2.3 Generic Netlink Family命令处理结构:struct genl_ops(内核中完成注册) 2.4 Generic Netlink Family内核接收消息结构:struct gen.

3484

Generic Netlink详解

netlink socket是一种用于用户态进程和内核态进程之间的通信机制。它通过为内核模块提供一组特殊的API,并为用户程序提供了一组标准的socket接口的方式,实现了全双工的通讯连接。 Netlink的特点: 双向传输,异步通信用户空间中使用标准socket API内核空间中使用专门的API支持多播可由内核端发起通信支持32种协议类型 netlink仅支持32种协议类型

工匠重生 3912

Replay音频转换时samplerate缺失的解决方案指南

Replay音频转换时提示"No module named 'samplerate' This error is lazily reported, having originally occured in File"的解决方案

weixin_47445543的博客 2130

内核与用户空间的通信实现—netlink

netlink的简单驱动和应用程序实现

ljl113的博客 3262

ADS 2013安装教程

1.2013和2014安装过程是一样的,运行ads2013_06_shp_windows文件进行安装 2.点击“NEXT”。 3.选择I accept,然后点击“NEXT”。 4.选择“complete”,然后点击“NEXT”。 5.选择安装路径,最好默认安装路径,安装路径不要有中文,然后点击“NEXT”。 6.点击“NEXT”. 7.如下路径默认安装,然后点击“NEXT”。 8.点击“NEXT”。 ...

谢谢关注“小鱼教你模数电”,各平台同号 6008

OSMDroid(1)——OSM简介,基础例子搭建

1.OSMDroid简介 Osmdroid是一款完全开源的地图基本操作SDK,相比于现在普遍使用的百度地图,高德地图,谷歌地图等最大的好处就是开源。 最近在做一个军方项目,要求完全不能联网,从开发到应用整个阶段都是在局域网进行,相比之下采用了OSMDroid这个SDK作为解决方案。 先来看OSMDroid,便是(OpenStreetMapForAndroid),后面都简称OSM...

robotskly的博客 6178

vswitch 与内核datapath通信机制 Generic Netlink介绍

1.netlink介绍 netlink socket是一种用于用户态进程和内核态进程之间的通信机制。它通过为内核模块提供一组特殊的API,并为用户程序提供了一组标准的socket接口的方式,实现了全双工的通讯连接。 netlink仅支持32种协议类型,目前内核中已经使用其中21个,对于用户需要定制特殊的协议类型略显不够,为此Linux设计了这种通用Netlink协议簇,用户可在此之上定义更多类型的子协议。 #define NETLINK_ROUTE 0 /* Routing/device hook

魏言华的博客 726

操作系统期末大题类型题解

额,OS主要这回考程序分析题,PV操作、生产者消费者啊,哲学家进餐问题,死锁问题啊,如何避免死锁,经典银行家算法,页面置换算法啥的,磁盘调度......总计还有很多,就不一一说了 PV操作 谈到PV操作,首先要了解什么是信号量,其实就是用来解决互斥与同步问题的,分为原语wait(S)和signal(S),也就是我们常说的PV操作,在这里给出王道上的概念 wait(S){ while(S>0) S=S-1; } signal(S){ S=S+1; } 记录型信号量

ZXxxiaoxuan的博客 792

Netlink 内核实现分析(一):创建

Netlink 是一种用于内核与用户空间通信的IPC(Inter Process Commumicate)机制,本文主要讲解netlink子系统的初始化、内核套接字的创建、用户层套接字的创建及绑定。

My Linux Journey 2万+

计算机视觉(角点检测)- 1 - Harris角点检测

计算机视觉(角点检测)-1.1 Harris角点检测学习前言 学习前言   今天更新一下计算机视觉的东西吧,哈哈哈o( ̄︶ ̄)o   关于角点检测呢,就是检测图像中角点的算法,常见的有四种常见的算法:Moravec角点检测算法、 Harris角点检测算法、Shi-Tomasi 算法、FAST角点检测算法,不过常用的就两种吧——Harris角点检测算法、Shi-Tomasi 算法(反正我基本上就只用到这两种,先着重介绍前面两种,后面再花世界介绍吧)(~ ̄▽ ̄)~ ...

qq_40374812的博客 1万+

Palantir 凭什么两年涨 20 倍?深度解析解决 AI 幻觉的 “本体论“ 核心原理

本文深度解析了Palantir本体(Ontology)的核心原理及其在企业AI应用中的革命性价值。本体通过构建结构化的语义层与动态执行层,将企业分散的数字资产(数据集、虚拟表、模型)映射为现实世界的数字孪生,使大模型能够在**严格的业务规则与数据边界内**进行推理与决策,从而从架构层面彻底解决AI幻觉问题。这一技术架构不仅帮助Palantir实现股价两年增长20倍、市值突破4000亿美金的商业成功,更成为SAP、Salesforce等ERP巨头争相模仿的下一代企业智能标准范式。通过航空业飞机故障应急处理等实

program哲学 224

Netlink 通信机制

Netlink套接字是用以实现用户进程与内核进程通信的一种特殊的进程间通信(IPC) ,也是网络应用程序与内核通信的最常用的接口。在Linux 内核中,使用netlink 进行应用与内核通信的应用有很多,如路由 daemon(NETLINK_ROUTE)用户态 socket 协议(NETLINK_USERSOCK)防火墙(NETLINK_FIREWALL)netfilter 子系统(NETLINK_NETFILTER)内核事件向用户态通知(NETLINK_KOBJECT_UEVENT)

chi's blog 1444

手把手教你学Simulink——四开关 Buck‑Boost(FSBB)变换器的无缝模式切换仿真

本文详细介绍了四开关Buck-Boost(FSBB)变换器的Simulink仿真方法,重点解决模式切换时的直通风险和输出电压波动问题。通过统一电压PI控制、迟滞比较模式仲裁(Buck/Boost/Pass-Through)和200ns死区设计,实现了Vin从26V到22V变化时Vout稳定在12V±0.15V。关键步骤包括:搭建功率级电路、Stateflow模式判断、PI控制器输出占空比映射(Buck模式D≈Vo/Vin,Boost模式D≈1-Vo/Vin),以及带死区的PWM信号分配。仿真验证了切换过程中

MHD0815的博客 115
上一篇: Netlink 内核实现分析(二):通信
下一篇: generic_netlink_howto
废言Pro
博客等级 码龄16年 1093粉丝 142原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值