欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

深入解析Generic Netlink内核实现(二):通信的详细分析

最编程 2024-01-17 11:20:17
...

一:创建内核Demo Genetlink

(一)定义Demo Genetlink

定义两种类型的Genetlink cmd指令:

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)
 

其中DEMO_CMD_ECHO用于应用层下发数据,DEMO_CMD_REPLY用于内核向应用层回发数据;

同时定义两种类型的attr属性参数:

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)

其中DEMO_CMD_ATTR_MESG表示字符串,DEMO_CMD_ATTR_DATA表示数据。(分别发送两种类型,这里没有一块发送)

定义demo_family:

static struct genl_family demo_family = {
    .id         = GENL_ID_GENERATE,
    .name       = DEMO_GENL_NAME,
    .version    = DEMO_GENL_VERSION,
    .maxattr    = DEMO_CMD_ATTR_MAX,
};

其中ID号为GENL_ID_GENERATE,表示由内核统一分配;maxattr为DEMO_CMD_ATTR_MAX,是前文中定义的最大attr属性数,内核将为其分配缓存空间。

定义操作函数集operations:demo_ops

static const struct genl_ops demo_ops[] = {
    {
        .cmd        = DEMO_CMD_ECHO,
        .doit       = demo_echo_cmd,
        .policy     = demo_cmd_policy,
        .flags      = GENL_ADMIN_PERM,
    },
};

这里只为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位数)。

(二)内核注册Demo Genetlink(内核态实现)

module_init(demo_genetlink_init);  
module_exit(demo_genetlink_exit);  
MODULE_LICENSE("GPL"); 

模块初始化demo_genetlink_init函数中实现内核注册:

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);  //前一篇博客详解1中详细介绍了!!
    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
        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区):

首先确定genlmsg_put函数中各个入参的内容: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()函数中,接下来就要开始填充实际的数据了(如下代码,会添加多个attr,在nla_put中每次添加一个)

    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类型判断并做出相应的处理即可。

(一)用户查询Demo 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 **
						

上一篇: 中华文明延续五千年的历史背景

下一篇: 深入解析Java中lombok @Builder注解的使用