netlink socket是一种用于用户态进程和内核态进程之间的通信机制。它通过为内核模块提供一组特殊的API,并为用户程序提供了一组标准的socket接口的方式,实现了全双工的通讯连接。

Netlink的特点:

  • 双向传输,异步通信
  • 用户空间中使用标准socket API
  • 内核空间中使用专门的API
  • 支持多播
  • 可由内核端发起通信
  • 支持32种协议类型

netlink仅支持32种协议类型,这在实际应用中可能并不足够。因此产生了generic netlink(以下简称为genl)。

generic netlink支持1023个子协议号,弥补了netlink协议类型较少的缺陷。支持协议号自动分配。它基于netlink,但是在内核中,generic netlink的接口与netlink并不相同。

1. Generic Netlink框架概述

图1表示了Generic Netlink框架。Kernel socket API向用户空间和内核空间分别提供接口。

Netlink子系统(1)是所有genl通信的基础。Netlink子系统中收到的所有Generic类型的netlink数据都被送到genl总线(2)上;从内核发出的数据也经由genl总线送至netlink子系统,再打包送至用户空间。

Generic Netlink控制器(4)作为内核的一部分,负责动态地分配genl通道(即genl family
id),并管理genl任务。genl控制器是一个特殊的genl内核用户,它负责监听genl
bus上的通信通道。genl通信建立在一系列的通信通道的基础上,每个genl family对应多个通道,这些通道由genl控制器动态分配。

      +---------------------+      +---------------------+
| (3) application "A" | | (3) application "B" |
+------+--------------+ +--------------+------+
| |
\ /
\ /
| |
+-------+--------------------------------+-------+
| : : | user-space
=====+ : (5) Kernel socket API : +================
| : : | kernel-space
+--------+-------------------------------+-------+
| |
+-----+-------------------------------+----+
| (1) Netlink subsystem |
+---------------------+--------------------+
|
+---------------------+--------------------+
| (2) Generic Netlink bus |
+--+--------------------------+-------+----+
| | |
+-------+---------+ | |
| (4) Controller | / \
+-----------------+ / \
| |
+------------------+--+ +--+------------------+
| (3) kernel user "X" | | (3) kernel user "Y" |
+---------------------+ +---------------------+
图1:generic netlink框架

2 Generic Netlink相关结构体

2.1 genl family



Generic Netlink是基于客户端-服务端模型的通信机制。服务端注册family(family是对genl服务的各项定义的集合)。控制器和客户端都通过已注册的信息与服务端通信。

genl family的结构体如下:

struct genl_family
{
unsigned int id;
unsigned int hdrsize;
char name[GENL_NAMSIZ];
unsigned int version;
unsigned int maxattr;
struct nlattr ** attrbuf;
struct list_head ops_list;
struct list_head family_list;
};

对此结构体元素具体解释如下:

* id: family id。当新注册一个family的时候,应该用GENL_ID_GENERATE宏(0x0),表示请控制器自动为family分配的一个id。0x10保留供genl控制器使用。

* hdrsize: 用户自定议头部长度。即图2中User Msg的长度。如果没有用户自定义头部,这个值被赋为0。

* version: 版本号,一般填1即可。

* name: family名,要求不同的family使用不同的名字。以便控制器进行正确的查找。

*
maxattr:genl使用netlink标准的attr来传输数据。此字段定义了最大attr类型数。(注意:不是一次传输多少个attr,而是一共
有多少种attr,因此,这个值可以被设为0,为0代表不区分所收到的数据的attr type)。在接收数据时,可以根据attr
type,获得指定的attr type的数据在整体数据中的位置。

* struct nlattr **attrbuf

* struct list_head ops_list

* struct list_head family_list

以上的三个字段为私有字段,由系统自动配置,开发者不需要做配置。

图2 genl报文与linux中各变量的对应关系

图3 genl报文格式

2.2 genl_ops 结构体

struct genl_ops
{
u8 cmd;
unsigned int flags;
struct nla_policy *policy;
int (*doit)(struct sk_buff *skb,
struct genl_info *info);
int (*dumpit)(struct sk_buff *skb,
struct netlink_callback *cb);
struct list_head ops_list;
};
  • cmd: 命令名。用于识别各genl_ops
  • flag: 各种设置属性,以“或”连接。在需要admin特权级别时,使用GENL_ADMIN_PERM
  • policy:定义了attr规则。如果此指针非空,genl在触发事件处理程序之前,会使用这个字段来对帧中的attr做校验(见nlmsg_parse函数)。该字段可以为空,表示在触发事件处理程序之前,不做校验。

policy是一个struct nla_policy的数组。struct nla_policy结构体表示如下:

struct nla_policy
{
u16 type;
u16 len;
};

其中,type字段表示attr中的数据类型,可被配置为:
       NLA_UNSPEC--未定义

       NLA_U8, NLA_U16, NLA_U32, NLA_U64为8bits, 16bits, 32bits, 64bits的无符号整型

       NLA_STRING--字符串

       NLA_NUL_STRING--空终止符字符串

       NLA_NESTED--attr流

len字段的意思是:如果在type字段配置的是字符串有关的值,要把len设置为字符串的最大长度(不包含结尾的'\0')。如果type字段未设置或被设置为NLA_UNSPEC,那么这里要设置为attr的payload部分的长度。

  • doit:这是一个回调函数。在generic netlink收到数据时触发,运行在进程上下文。

doit传入两个参数,skb为触发此回调函数的socket buffer。第二个参数是一个genl_info结构体,定义如下:

struct genl_info
{
u32 snd_seq;
u32 snd_pid;
struct nlmsghdr * nlhdr;
struct genlmsghdr * genlhdr;
void * userhdr;
struct nlattr ** attrs;
};

* snd_seq:发送序号

* snd_pid:发送客户端的PID

* nlhdr:netlink header的指针

* genlmsghdr:genl头部的指针(即family头部)

* userhdr:用户自定义头部指针

*
attrs:attrs,如果定义了genl_ops->policy,这里的attrs是被policy过滤以后的结果。在完成了操作以后,如果
执行正确,返回0;否则,返回一个负数。负数的返回值会触发NLMSG_ERROR消息。当genl_ops的flag标志被添加了
NLMSG_ERROR时,即使doit返回0,也会触发NLMSG_ERROR消息。

  • dumpit

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

  • ops_list

为私有字段,由系统自动配置,开发者不需要做配置。

3 Generic Netlink服务端(内核)初始化

初始化Generic
Netlink的过程分为以下四步:定义family,定义operation,注册family,注册operation。下面通过一个简单例子来说明
如何完成Generic
Netlink的初始化。我们首先创建一个genl_family结构体的实例。我们在这里定义一个名为"DOC_EXMPL"的family

/* attribute type */
enum {
DOC_EXMPL_A_UNSPEC,
DOC_EXMPL_A_MSG,
__DOC_EXMPL_A_MAX,
};
#define DOC_EXMPL_A_MAX (__DOC_EXMPL_A_MAX - 1) /* family definition */
static struct genl_family doc_exmpl_gnl_family = {
.id = GENL_ID_GENERATE,
.hdrsize = 0,
.name = "DOC_EXMPL",
.version = 1,
.maxattr = DOC_EXMPL_A_MAX, };

以上,我们定义了一个仅有一种attribuste type的family。.id被配置为GENL_ID_GENERATE,指示genl控制器自动分配一个id。

第二步为family创建operations。我们至少要创建一个genl_ops结构体的实例。

/* doit handler */
int doc_exmpl_echo(struct sk_buff *skb, struct genl_info *info)
{
/* message handling code goes here; return 0 on success, negative
* values on failure */
} /* attribute policy */
static struct nla_policy doc_exmpl_genl_policy = [DOC_EXMPL_A_MAX + 1] = {
[DOC_EXMPL_A_MSG] = { .type = NLA_NUL_STRING },
} /* commands */
enum {
DOC_EXMPL_C_UNSPEC,
DOC_EXMPL_C_ECHO,
__DOC_EXMPL_C_ECHO,
};
#define DOC_EXMPL_C_MAX (__DOC_EXMPL_C_MAX - 1) /* operation definition */
struct genl_ops doc_exmpl_gnl_ops_echo = {
.cmd = DOC_EXMPL_C_ECHO,
.flags = 0,
.policy = doc_exmpl_genl_policy,
.doit = genl_recv_doit,
.dumpit = NULL,
}

这里,我们把attribute policy设为NLA_NUL_STRING,表示attr中数据的属性为无NULL结尾的字符串。控制器在收到数据时会自动完成这一类型检查。

我们定义一个operation,它的id为DOC_EXMPL_C_ECHO,把上述的policy配置给它。一旦本family的genl消息在被总到genl总线上,doit函数(doc_exmpl_echo)会被调用。

接下来两步是注册family和注册operations。

genl_register_family(&doc_exmpl_gnl_family);

genl_register_ops(&doc_exmpl_gnl_family, &doc_exmpl_gnl_ops_echo);

在完成genl操作后,记对完成对family的注销操作。

genl_unregister_family(&doc_exmpl_gnl_family);

4 Generic Netlink客户端(用户空间)初始化

Generic Netlink在用户空间的初始化和通常的socket通信一致。大致分为两步,创建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。

接下来,对于genl不可缺少的一步就是获取family id。family id是服务端注册family时,由控制器自动分配的。此时客户端尚不知道family id为多少,因此需要向客户端请求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");


5 Generic Netlink通信


这一节对如何使用Generic Netlink完成内核空间与用户空间的通信做介绍。并把我的示例代码贡献出来。

示例代码呈现了内核(服务端)和用户空间(客户端)收发数据的过程。

5.1 内核发送数据

以下是内核端发送数据的源码。在genl_msg_send_to_user中,调用genl_msg_prepare_usr_msg和
genl_msg_mk_usr_msg来准备socket
buffer,为数据加上各种数据头(参考图2)。genlmsg_end把整个数据打包完成,通过genlmsg_unicast完成单播发送。

/**
* 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;
} 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;
}

5.2 用户空间接收数据

客户端调用通用的recv函数即可完成从内核来的数据的接收。需要注意的是,接收到的数据包含几级的header(图3),我们需要准确地定位到我们所需数据的位置。

当没有用户自定义头部(即图3中的User Msg,在注册family时把hdrsize置0)时,可以构建这样的数据结构用于接收数据。这样,收到的数据中的netlink header和genl header就被很容易地剥离开来。

typedef struct msgtemplate {
struct nlmsghdr n;
struct genlmsghdr g;
char data[MAX_MSG_SIZE];
} msgtemplate_t;

下面是客户端接收数据函数的源码:

#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;
}
//printf("received length %d\n", ret);
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。

5.3 用户空间发送数据

客户端发送数据简单地说就是调用通用的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;
}


5.4 内核接收数据


内核端一旦收到generic netlink数据,会触发doit函数运行(上文第3节有提及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;
}
 
http://www.tuicool.com/articles/jE7nim

Generic Netlink详解的更多相关文章

  1. 使用 /proc 文件系统来访问 linux操作系统 内核的内容 && 虚拟文件系统vfs及proc详解

    http://blog.163.com/he_junwei/blog/static/19793764620152743325659/ http://www.01yun.com/other/201304 ...

  2. suricata.yaml (一款高性能的网络IDS、IPS和网络安全监控引擎)默认配置文件(图文详解)

    不多说,直接上干货! 前期博客 基于CentOS6.5下Suricata(一款高性能的网络IDS.IPS和网络安全监控引擎)的搭建(图文详解)(博主推荐) 或者 基于Ubuntu14.04下Suric ...

  3. [转]keil使用详解

    第一节 系统概述 Keil C51是美国Keil Software公司出品的51系列兼容单片机C语言软件开发系统,与汇编相比,C语言在功能上.结构性.可读性.可维护性上有明显的优势,因而易学易用.用过 ...

  4. Elasticsearch配置详解、文档元数据

    目录 返回目录:http://www.cnblogs.com/hanyinglong/p/5464604.html 1.Elasticsearch配置文件详解 a. 在上面博客中,我们已经安装并且成功 ...

  5. redis配置详解

    ##redis配置详解 # Redis configuration file example. # # Note that in order to read the configuration fil ...

  6. Linux /dev目录详解和Linux系统各个目录的作用

    Linux /dev目录详解(转http://blog.csdn.net/maopig/article/details/7195048) 在linux下,/dev目录是很重要的,各种设备都在下面.下面 ...

  7. C# LINQ详解(转)

    C# LINQ详解(一)   原文标题:How does it work in C#?-Part 3 (C# LINQ in detail),作者:Mohammand A Rahman. 目录 LIN ...

  8. 【ORM】--FluentNHibernate之基本映射详解

           最近在做项目的时候用到了NHibernate,使用它并不困难,但是很麻烦.如果我的数据库有几百张表如果想要一个个的映射岂不是很麻烦,所以这种情况下使用NHibernate就会很笨重,虽然 ...

  9. Spring MVC 学习总结(二)——控制器定义与@RequestMapping详解

    一.控制器定义 控制器提供访问应用程序的行为,通常通过服务接口定义或注解定义两种方法实现. 控制器解析用户的请求并将其转换为一个模型.在Spring MVC中一个控制器可以包含多个Action(动作. ...

随机推荐

  1. [原创]VM虚拟机安装centos6.4详细图文教程

    1.启动虚拟机,新建虚拟机.   2.选择从镜像安装,选择centos6.4的路径. 3.设置用户名和密码.注:只能用小写字母. 4.选择安装路径. 5.配置磁盘大小. 6.准备创建. 如果需要自定义 ...

  2. 如何在Hadoop的MapReduce程序中处理JSON文件

    简介: 最近在写MapReduce程序处理日志时,需要解析JSON配置文件,简化Java程序和处理逻辑.但是Hadoop本身似乎没有内置对JSON文件的解析功能,我们不得不求助于第三方JSON工具包. ...

  3. 搞懂offsetY、offsetTop、scrollTop、offsetHeight、scrollHeight

    先搞offsetTop,最难懂的就是它了 官方解释:返回当前元素的上边界到它的包含元素的上边界的偏移量,以像素为单位.这真TM坑爹啊!有木有!经过仔细研究查找得出结论:offsetTop是相对于离它最 ...

  4. 服务器端与客户端TCP连接入门(一)

    Java中使用Socket(即套接字)完成TCP程序的开发 服务器端使用ServerSocket接收客户端的连接请求,每一个客户端都使用一个Socket对象表示 在服务器端每次运行时都要使用accep ...

  5. VS2013的项目转到VS2010需要修改的

    Visual Studio2013: 用的是.net FrameWork 4.5版本,自带Nuget(在里面可以搜索到各种引用插件,不用再自己一个个百度,就像X60软件管家) Visual Studi ...

  6. redis OK

    http://redis.readthedocs.org/en/2.4/set.html1, client.end();redis.expire(key,10) ,lsize,llen APPEND ...

  7. Android学习参考教程和工具及常见问题解决

    参考教程: 1.菜鸟教程:http://www.runoob.com/w3cnote/android-tutorial-intro.html 2.Android初學特訓班(第五版) 使用工具: 1.A ...

  8. CSS Hack及常用的技巧

    何谓CSS Hack? 不同的浏览器,比如Internet Explorer 6.Internet Explorer 7. Mozilla Firefox对CSS的解析认识不一样,因此会导致生成的页面 ...

  9. Ubuntu根目录下各文件的功能介绍

    http://jingyan.baidu.com/article/afd8f4de55189c34e286e9e6.html

  10. XCode 自动化打包总结

    最近一个礼拜折腾xcode 中ipa 自动化打包,对我来说也说是磕磕碰碰.毕竟对mac下的命令行模式完全不熟悉.而且我们的项目是基于cordova的一个项目. 之前我自己对cordova 项目的命令行 ...