二 generic netlink

一 基础
由于netlink协议最多支持32个协议簇,目前内核中已经使用其中21个,对于用户需要定制特殊的协议类型略显不够,为此Linux设计了这种通用Netlink协议簇,用户可在此之上定义更多类型的子协议。
Generic Netlink是基于客户端-服务端模型的通信机制。服务端(内核)注册family(family是对genl服务的各项定义的集合)。控制器和客户端都通过已注册的信息与服务端通信。
Generic Netlink使用NETLINK_GENERIC类型协议簇,同样基于netlink子系统。

1 gnel框架:
            
Netlink子系统(3)是所有genl通信的基础。Netlink子系统中收到的所有Generic类型的netlink数据都被送到genl总线(2)上;从内核发出的数据也经由genl总线送至netlink子系统,再打包送至用户空间。
Generic Netlink控制器(1)作为内核的一部分,负责动态地分配genl通道(即genl family id),并管理genl任务。genl控制器是一个特殊的genl内核用户,它负责监听genl bus上的通信通道。genl通信建立在一系列的通信通道的基础上,每个genl family对应多个通道,这些通道由genl控制器动态分配。

2 netlink类型的消息结构:
           
一个netlink消息有netlink消息头和netlink消息载荷组成,它们之间存在内存对齐的pad留空空间;然后往下一级消息的实际载荷又可分为family头及具体的消息属性,其中 family头针对不同协议种类的netlink定义各部相同;到最底层消息属性又分为消息属性头和实际的消息载荷。Genetlink消息基于这个消息 结构类型并定制化为如下结构:
netlink消息头+family头+用户特定消息头+消息属性实例

3 相关结构体:

1)、 通用netlink消息头:
struct genlmsghdr {
    __u8    cmd;  //消息命令
    __u8    version;//版本控制
    __u16   reserved;
};

2)、 Generic Netlink Family结构
内核使用一个哈希表family_ht对已经注册的genl family进行管理,他是对genl服务的各项定义的集合:
struct genl_family {
    unsigned int        id;  //genl family的全局唯一ID号,一般由内核进行分配,取值范围为GENL_MIN_ID~GENL_MAX_ID(16~1023),其中16为控制器。若取0表示由控制器自动分配
    unsigned int        hdrsize; //用户私有报头的长度,即可选的user msg header长度,若没有则为0。
    char                name[GENL_NAMSIZ]; //genl family的名称,必须是独一无二且用户层已知的(用户通过它来向控制查找family id)。
    unsigned int        version;
    unsigned int        maxattr; //消息属性attr最大的类型数, 为0代表不区分所收到的数据的attr
    bool                netnsok; //当前簇是否能够处理网络命名空间;
    bool                parallel_ops;
    int         (*pre_doit)(const struct genl_ops *ops, struct sk_buff *skb, struct genl_info *info); //调用genl_ops结构中处理消息函数doit()前调用的钩子函数,一般用于执行一些前置的当前簇通用化处理,例如对临界区加锁等;
    void            (*post_doit)(const struct genl_ops *ops, struct sk_buff *skb, struct genl_info *info); //调用genl_ops结构中处理消息函数doit()后调用的钩子函数,一般执行pre_doit函数相反的操作
    int         (*mcast_bind)(struct net *net, int group); //在绑定/解绑定socket到一个特定的genl netlink组播组中调用
    void            (*mcast_unbind)(struct net *net, int group);//
    struct nlattr **       attrbuf;     //private 保存拷贝的attr属性缓存
    const struct genl_ops  *ops;        /* private */
    const struct genl_multicast_group *mcgrps;  /* private当前family使用的多播组 */
    unsigned int           n_ops;      /* private */
    unsigned int           n_mcgrps;   /* private */
    unsigned int           mcgrp_offset;   /* private */
    struct list_head       family_list;     //链表结构,用于将当前当前簇链入全局family_ht散列表中;
    struct module          *module;
};

3)、命令处理:
该结构用于注册genl family的用户命令cmd处理函数(对于只向应用层发送消息的簇可以不用实现和注册该结构):
struct genl_ops {
    const struct nla_policy *policy;//属性attr有效性策略,若该字段不为空,在genl执行消息处理函数前会对消息中的attr属性进行校验,否则则不做校验;
    int            (*doit)(struct sk_buff *skb, struct genl_info *info);// 标准命令回调函数,收到数据时触发调用,函数的第一个入参skb中保存了用户下发的消息内容;
    int            (*dumpit)(struct sk_buff *skb, struct netlink_callback *cb);// 转储回调函数,当genl_ops的flag标志被添加了NLM_F_DUMP以后,每次收到gnel消息都会调用该回调函数,这里的第一个入参skb中不再有用户下发消息内容,而是要求函数能够在传入的skb中填入消息载荷并返回填入数据长度;
    int            (*done)(struct netlink_callback *cb);// 转储结束后执行的回调函数;
    u8          cmd;//命令类型,由用户自行根据需要定义
    u8          internal_flags;//私有标识,用于进行一些分支处理,可以不使用;
    u8          flags;//操作标识,有以下四种类型(在genetlink.h中定义):
};

dumpit与doit的区别是:dumpit的第一个参数skb不会携带从客户端发来的数据。相反地,开发者应该在skb中填入需要传给客户端的数据,然后,并skb的数据长度(可以用skb->len)return。skb中携带的数据会被自动送到客户端。只要dumpit的返回值大于0,dumpit函数就会再次被调用,并被要求在skb中填入数据。当服务端没有数据要传给客户端时,dumpit要返回0。如果函数中出错,要求返回一个负值。关于doit和dumpit的触发过程,可以查看源码中的genl_rcv_msg函数

4)、内核消息
内核在接收到用户的genetlink消息后,会对消息解析并封装成genl_info结构,便于命令回校函数进行处理:
struct genl_info {
    u32         snd_seq; // 消息的发送序号(不强制使用)
    u32         snd_portid; // 消息发送端socket所绑定的ID;
    struct nlmsghdr *   nlhdr; // netlink消息头
    struct genlmsghdr * genlhdr; // generic netlink消息头;
    void *          userhdr; // 用户私有报头;
    struct nlattr **attrs; // netlink属性,包含了消息的实际载荷;
    possible_net_t   _net;
    void *           user_ptr[2];
    struct sock*     dst_sk; //目的socket
};


二 使用

1 GNEL服务端(内核)
初始化Generic Netlink的过程分为以下四步:定义family --> 定义operation  --> 注册family --> 注册operation。

下面通过一个简单例子来说明如何完成Generic  Netlink的初始化。
1)、定义Demo genetlick
   
   包括创建 genl_family结构体的实例及其ops函数
    我们在这里定义一个名为"demo_family"的 family  :
/* attribute type 属性类型,用于定义genl_family*/
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)
//定义两种类型的attr属性参数,其中DEMO_CMD_ATTR_MESG表示字符串,DEMO_CMD_ATTR_DATA表示数据。


/* family definition,定义demo_family */
static struct genl_family demo_family = {
        .id            = GENL_ID_GENERATE, //0,表示由内核genl控制器自动分配ID
        .name          = DEMO_GENL_NAME,
        .version       = DEMO_GENL_VERSION,
        .maxattr       = DEMO_CMD_ATTR_MAX, //前文中定义的最大attr属性数,内核将为其分配缓存空间。
};


      至少要创建一个 genl_ops结构体的实例
/*命令指令*/
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)
//定义两种类型的Genetlink cmd指令,其中DEMO_CMD_ECHO用于应用层下发数据,DEMO_CMD_REPLY用于内核向应用层回发数据;


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,一旦本family的genl消息在被总到genl总线上,doit函数会被调用。同时指定有效组策略为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)、内核注册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函数完成创建。


        在完成genl操作后,记对完成对family的注销操作:
                 genl_unregister_family(&doc_exmpl_gnl_family);

3)、消息封装过程及常用接口  
---首先为message分配存储空间:
   struct sk_buff *nlmsg_new(size_t payload, gfp_t flags);
   struct sk_buff *genlmsg_new(size_t payload, gfp_t flags);
       上面两个接口函数创建netlink类型的skb,第一个入参是消息的长度,第二个参数为内存空间分配类型;
       区别是nlmsg_new预留空间大小为sizeof(payload)+NLMSG_HDRLEN, genlmsg_new预留的空间比nlmsg_new多GENL_HDRLEN。

---其次填充header:
   struct nlmsghdr *nlmsg_put(struct sk_buff *skb, u32 portid, u32 seq, int type, int len, int flags);
       nlmsg_put()函数向skb缓冲区中获取消息头空间并且初始化netlink消息头,第5个参数为genetlink消息头和用户私有消息头(这里并未使用)的总空间
   void *genlmsg_put(struct sk_buff *skb, u32 portid, u32 seq, struct genl_family *family, int flags, u8 cmd)
       genlmsg_put()函数初始化netlink消息头和genetlink消息头;

---再次填充实际载荷数据:
   int nla_put(struct sk_buff *skb, int attrtype, int attrlen, const void *data)

---最后封装消息:
   void genlmsg_end(struct sk_buff *skb, void *hdr)
   void nlmsg_end(struct sk_buff *skb, struct nlmsghdr *nlh)


4)、发送数据
示例说明:
内核端发送数据的源码。在genl_msg_send_to_user中,调用genl_msg_prepare_usr_msg和 genl_msg_mk_usr_msg来准备socket buffer,为数据加上各种数据头。genlmsg_end把整个数据打包完成,通过genlmsg_unicast完成单播发送。
static inline int genl_msg_mk_usr_msg(struct sk_buff *skb, int type, void *data, int len)
{
    int rc;

    /* add a netlink attribute to a socket buffer */
    if ((rc = nla_put(skb, type, len, data)) != 0) {
        return rc;
    }
    return 0;
}

static inline int genl_msg_prepare_usr_msg(u8 cmd, size_t size, pid_t pid, struct sk_buff **skbp)
{
    struct sk_buff *skb;

    /* create a new netlink msg */
    skb = genlmsg_new(size, GFP_KERNEL);
    if (skb == NULL) {
        return -ENOMEM;
    }

    /* Add a new netlink message to an skb */
    genlmsg_put(skb, pid, 0, &genl_family, 0, cmd);

    *skbp = skb;
    return 0;
}

/**
* genl_msg_send_to_user - 通过generic netlink发送数据到netlink
*
* @data: 发送数据缓存
* @len:  数据长度 单位:byte
* @pid:  发送到的客户端pid
*
* return:
*    0:      成功
*    -1:      失败
*/
int genl_msg_send_to_user(void *data, int len, pid_t pid)
{
    struct sk_buff *skb;
    size_t size;
    void *head;
    int rc;

    size = nla_total_size(len); /* total length of attribute including padding */

    rc = genl_msg_prepare_usr_msg(DOC_EXMPL_C_ECHO, size, pid, &skb);

    if (rc) {
        return rc;
    }

    rc = genl_msg_mk_usr_msg(skb, DOC_EXMPL_A_MSG, data, len);

    if (rc) {
        kfree_skb(skb);
        return rc;
    }

    head = genlmsg_data(nlmsg_data(nlmsg_hdr(skb)));

    rc = genlmsg_end(skb, head);
    if (rc < 0) {
        kfree_skb(skb);
        return rc;
    }

    rc = genlmsg_unicast(&init_net, skb, pid);
    if (rc < 0) {
        return rc;
    }

    return 0;
}


5)接收数据
内核端一旦收到generic  netlink数据,会触发doit函数运行。
doit传入两个参数,skb即是接收到的数据,info包含了Genl消息的一些常用指针。这两个结构体字段详见内核源码。
skb收到的数据还包括了多层的包头,以下程序中的nlmsg_hdr,nlmsg_data,genlmsg_data,nla_data即是把这些包头层层剥开,para->string指向的数据即是用用户空间传来的“纯数据”。

int genl_recv_doit(struct sk_buff *skb, struct genl_info *info)
{
    /* doit 没有运行在中断上下文 */
    static int          kthread_num = 0;
    struct nlmsghdr     *nlhdr;
    struct genlmsghdr   *genlhdr;
    struct nlattr       *nlh;
    struct thread_para  *para;              /* 给线程传递参数的结构体 */

     nlhdr = nlmsg_hdr(skb);
     genlhdr = nlmsg_data(nlhdr);
     nlh = genlmsg_data(genlhdr);
    /* 配置给新开线程所传的参数 */
    /* para 在线程函数thread_string_proc中释放 */
    para = (struct thread_para  *)kmalloc(sizeof(struct thread_para), GFP_KERNEL);
    para->string = nla_data(nlh);
    para->pid = nlhdr->nlmsg_pid;

    /* 每收到一个字符串开辟一个线程 */
    kthread_run(thread_string_proc, (void *)(para), "kthread %d", kthread_num++);

    return 0;
}


2 客户端

Generic  Netlink在用户空间的初始化和通常的socket通信一致。大致分为两步,
1)、创建socket,把socket绑定到地址上(bind)。

下面也通过一个例子简要说明一下用户空间genl初始化的过程。
    struct sockaddr_nl saddr;
    int                sock;
    sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);

    if (sock < 0) {
        return -1;
    }

    memset(&saddr, 0, sizeof(saddr));
    saddr.nl_family = AF_NETLINK;
    saddr.nl_pid = getpid();
    if (bind(sock, (struct sockaddr*)&saddr, sizeof(saddr)) < 0) {
        printf("bind fail!\n");
        close(*p_sock);
        return -1;
    }

上述代码中,我们先创建一个socket,注意,第一个参数必须为AF_NETLINK 或 PF_NETLINK,表示创建netlink socket,第二个参数必须是SOCK_RAW或SOCK_DGRAM, 第三个参数指定netlink协议类型,我们要使用generic netlink,那么就要将其设置为:NETLINK_GENERIC。

 2)、获取family id
family id是服务端注册family时,由控制器自动分配的。此时客户端尚不知道family id为多少,因此需要向客户端请求family id。

static int genl_get_family_id(int sd, char *family_name)
{
    msgtemplate_t ans;
    int id, rc;
    struct nlattr *na;
    int rep_len;

    rc = genl_send_msg(sd, GENL_ID_CTRL, 0, CTRL_CMD_GETFAMILY, 1,
                    CTRL_ATTR_FAMILY_NAME, (void *)family_name,
                    strlen(family_name)+1);

    rep_len = recv(sd, &ans, sizeof(ans), 0);
    if (rep_len < 0) {
        return 0;
    }
    if (ans.n.nlmsg_type == NLMSG_ERROR || !NLMSG_OK((&ans.n), rep_len)) {
        return 0;
    }

    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);
    } else {
        id = 0;
    }

    return id;
}
在这个函数中,调用genl_send_msg(这个函数会在下文中介绍并给出源码)发送请求family id的消息,并调用recv接收服务端的反馈消息。这个消息中即包含了family id。
这个函数的第一个参数是已创建好的socket。第二个参数是family name,注意这里family name需要与服务端注册famile时的name字段一致。该函数返回值即是family id,以下是一个调用示例。
int fid = genl_get_family_id(sock, "DOC_EXMPL");

3)空间接收数据
客户端调用通用的recv函数即可完成从内核来的数据的接收。需要注意的是,接收到的数据包含几级的header,我们需要准确地定位到我们所需数据的位置。
当没有用户自定义头部(在注册family时把hdrsize置0)时,可以构建这样的数据结构用于接收数据。这样,收到的数据中的netlink header和genl header就被很容易地剥离开来。
typedef struct msgtemplate {
    struct nlmsghdr n;
    struct genlmsghdr g;
    char data[MAX_MSG_SIZE];
} msgtemplate_t;

下面是客户端接收数据函数的源码:
#define NLMSG_DATA(nlh)         ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
#define GENLMSG_DATA(glh)       ((void *)(NLMSG_DATA(glh) + GENL_HDRLEN))
#define NLA_DATA(na)            ((void *)((char *)(na) + NLA_HDRLEN))

void genl_rcv_msg(int fid, int sock, char **string)
{
    int ret;
    struct msgtemplate msg;
    struct nlattr *na;

    ret = recv(sock, &msg, sizeof(msg), 0);
    if (ret < 0) {
        return;
    }

    if (msg.n.nlmsg_type == NLMSG_ERROR || !NLMSG_OK((&msg.n), ret)) {
        return;
    }
    if (msg.n.nlmsg_type == fid && fid != 0) {
        na = (struct nlattr *) GENLMSG_DATA(&msg);
        *string = (char *)NLA_DATA(na);
    }
}

以上函数中,第一个参数为family id,第二个参数为socket,第三个参数为待接收数据的buffer。

4)、用户空间发送数据
客户端发送数据简单地说就是调用通用的socket API---sendto来发送数据

/**
* genl_send_msg - 通过generic netlink给内核发送数据
*
* @sd: 客户端socket
* @nlmsg_type: family_id
* @nlmsg_pid: 客户端pid
* @genl_cmd: 命令类型
* @genl_version: genl版本号
* @nla_type: netlink attr类型
* @nla_data: 发送的数据
* @nla_len: 发送数据长度
*
* return: 
*    0:       成功 
*    -1:      失败
*/

int genl_send_msg(int sd, u_int16_t nlmsg_type, u_int32_t nlmsg_pid,
        u_int8_t genl_cmd, u_int8_t genl_version, u_int16_t nla_type,
        void *nla_data, int nla_len)
{
    struct nlattr *na;
    struct sockaddr_nl nladdr;
    int r, buflen;
    char *buf;
    msgtemplate_t msg;

    if (nlmsg_type == 0) {
        return 0;
    }

    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;
    /*
     * nlmsg_pid是发送进程的端口号。
     * Linux内核不关心这个字段,仅用于跟踪消息。
     */
    msg.n.nlmsg_pid = nlmsg_pid;
    msg.g.cmd = genl_cmd;
    msg.g.version = 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;
}

现在用户空间使用统一库libnl,提供基于Linux内核的netlink协议的API。
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值