本文主要是介绍switch_set_state/netlink/kmod,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
许多代码调用接口 switch_set_state 向用户态传递消息,在 switch_set_state 中:
switch_set_state 调用接口 kobject_uevent_env
在 kobject_uevent_env 中,如果定义了 CONFIG_NET ,则会通过netlink向用户态发送消息;
在kernel3.4上,不判断,直接调用call_usermodehelper通过kmod向用户态发送消息。
在kernel4.4上,如果定义了 CONFIG_UEVENT_HELPER ,则会调用call_usermodehelper_exec通过kmod向用户态发送消息;如果没定义 CONFIG_UEVENT_HELPER ,则不会通过kmod向用户态发送消息。
在kernel3.4上,switch_set_state 通过kmod向用户态发送消息时,会调用 call_usermodehelper 来进行发送,该函数 call_usermodehelper 的第一个参数 uevent_helper 的路径来自 CONFIG_UEVENT_HELPER_PATH 。我们可在此配置uevent_helper的名字(含路径)
char uevent_helper[UEVENT_HELPER_PATH_LEN] = CONFIG_UEVENT_HELPER_PATH;
...
if (uevent_helper[0] && !kobj_usermode_filter(kobj)) {
char *argv [3];
argv [0] = uevent_helper;
argv [1] = (char *)subsystem;
argv [2] = NULL;
retval = add_uevent_var(env, "HOME=/");
if (retval)
goto exit;
retval = add_uevent_var(env,
"PATH=/sbin:/bin:/usr/sbin:/usr/bin");
if (retval)
goto exit;
retval = call_usermodehelper(argv[0], argv,
env->envp, UMH_WAIT_EXEC);
}
内核中也有多处代码直接调用 call_usermodehelper ,第一个参数在模块代码中指定,例如在 drivers\net\hamradio\baycom_epp.c
static char eppconfig_path[256] = "/usr/sbin/eppfpga";
...
return call_usermodehelper(eppconfig_path, argv, envp, UMH_WAIT_PROC);
drivers\macintosh\therm_pm72.c 中:
static char * critical_overtemp_path = "/sbin/critical_overtemp";
...
static int call_critical_overtemp(void)
{
char *argv[] = { critical_overtemp_path, NULL };
static char *envp[] = { "HOME=/",
"TERM=linux",
"PATH=/sbin:/usr/sbin:/bin:/usr/bin",
NULL };
return call_usermodehelper(critical_overtemp_path,
argv, envp, UMH_WAIT_EXEC);
}
转几篇文章:
http://blog.csdn.net/sunweizhong1024/article/details/7928530
Uevent 上报event事件给上层的详细讲解
本文章讲解插入headphone的时候,向上层上报event函数的整个过程
headphone_event(wm8903->hp_state);
当有headphone 插入的时候,那么就将hp_state的状态设置为1
#ifdef CONFIG_I_LOVE_PBJ30
void headphone_event(int state)
{
switch_set_state(&wired_switch_dev, state);
}
EXPORT_SYMBOL_GPL(headphone_event);
#endif
headphone_event 函数会调用switch_set_state函数进行上报事件
staticstruct switch_dev wired_switch_dev = {
.name = "h2w",
};
void switch_set_state(struct switch_dev *sdev, int state)
{
char name_buf[120];
char state_buf[120];
char *prop_buf;
char *envp[3];
int env_offset = 0;
int length;
if (sdev->state != state) {
sdev->state = state;
prop_buf = (char *)get_zeroed_page(GFP_KERNEL);
if (prop_buf) {
length = name_show(sdev->dev, NULL, prop_buf);
if (length > 0) {
if (prop_buf[length - 1] == '\n')
prop_buf[length - 1] = 0;
snprintf(name_buf, sizeof(name_buf),
"SWITCH_NAME=%s", prop_buf);
envp[env_offset++] = name_buf;
}
length = state_show(sdev->dev, NULL, prop_buf);
if (length > 0) {
if (prop_buf[length - 1] == '\n')
prop_buf[length - 1] = 0;
snprintf(state_buf, sizeof(state_buf),
"SWITCH_STATE=%s", prop_buf);
envp[env_offset++] = state_buf;
}
envp[env_offset] = NULL;
kobject_uevent_env(&sdev->dev->kobj, KOBJ_CHANGE, envp);
free_page((unsignedlong)prop_buf);
} else {
printk(KERN_ERR "out of memory in switch_set_state\n");
kobject_uevent(&sdev->dev->kobj, KOBJ_CHANGE);
}
}
}
EXPORT_SYMBOL_GPL(switch_set_state);
讲解下上的switch_set_state函数
我们首先会判断sdev里面的状态和我们需要设置的状态是否一样,如果一样的话,那么就不需要去管它,如果不一样的话,那么就将state的值修改
上面主要会调用以下几个函数:
name_show:
state_show:
kobject_uevent_env:
在switch_set_state函数里面,会申请一个buffer,然后调用name_show函数
static ssize_t name_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
struct switch_dev *sdev = (struct switch_dev *)
dev_get_drvdata(dev);
if (sdev->print_name) {
int ret = sdev->print_name(sdev, buf);
if (ret >= 0)
return ret;
}
return sprintf(buf, "%s\n", sdev->name);
}
name_show函数会判断sdev里面的print_name函数有没有被实现,如果没有实现的话,那么就将sdev->name的值输入到buf中去
我们的sdev里面实现的如下:
staticstruct switch_dev wired_switch_dev = {
.name = "h2w",
};
所以就直接将name 输入到buf中去
然后将输入到buf里面的值赋值到envp数组里面去:envp[env_offset++] = name_buf;
接下来的state_show函数的执行和上面的name_show函数是一样的,只是一个输出的是name,另外一个是state
也是将state的值赋值到envp 的数组里面去
接下来会调用kobject_uevent_env函数进行上报事件
kobject_uevent_env(&sdev->dev->kobj, KOBJ_CHANGE, envp);
kobject_uevent_env函数会首先判断 我这个kobject是属于哪个kset里面的,然后找出这个kset里面的uevent_ops
kobject_uevent_env函数的实现全部过程如下:
int kobject_uevent_env(struct kobject *kobj, enum kobject_action action,
char *envp_ext[])
{
struct kobj_uevent_env *env;
constchar *action_string = kobject_actions[action];
constchar *devpath = NULL;
constchar *subsystem;
struct kobject *top_kobj;
struct kset *kset;
conststruct kset_uevent_ops *uevent_ops;
u64 seq;
int i = 0;
int retval = 0;
#ifdef CONFIG_NET
struct uevent_sock *ue_sk;
#endif
pr_debug("kobject: '%s' (%p): %s\n",
kobject_name(kobj), kobj, __func__);
/* search the kset we belong to */
top_kobj = kobj;
while (!top_kobj->kset && top_kobj->parent)//取出这个kobject属于哪一个kset
top_kobj = top_kobj->parent;
if (!top_kobj->kset) {
pr_debug("kobject: '%s' (%p): %s: attempted to send uevent "
"without kset!\n", kobject_name(kobj), kobj,
__func__);
return -EINVAL;
}
kset = top_kobj->kset;//找到这个kobject的kset成员
uevent_ops = kset->uevent_ops; //找到这个kset创建的时候,kset里面的uevent_ops成员
/* skip the event, if uevent_suppress is set*/
if (kobj->uevent_suppress) {//判断kobject里面有没有uvent_supress,如果有那么就代表这个
event是忽略的
pr_debug("kobject: '%s' (%p): %s: uevent_suppress "
"caused the event to drop!\n",
kobject_name(kobj), kobj, __func__);
return 0;
}
/* skip the event, if the filter returns zero. */
if (uevent_ops && uevent_ops->filter)
if (!uevent_ops->filter(kset, kobj)) {//kset里面的uevent_ops里面的filter成员决定是否将事件传递到用户空间去,如果返回0的话,那么就直接忽略,通过代码看的出来
pr_debug("kobject: '%s' (%p): %s: filter function "
"caused the event to drop!\n",
kobject_name(kobj), kobj, __func__);
return 0;
}
/* originating subsystem */
if (uevent_ops && uevent_ops->name)
subsystem = uevent_ops->name(kset, kobj);//调用uevent_ops里面的name成员将字符串传递给用户空间的热插拔程序
else
subsystem = kobject_name(&kset->kobj);
if (!subsystem) {
pr_debug("kobject: '%s' (%p): %s: unset subsystem caused the "
"event to drop!\n", kobject_name(kobj), kobj,
__func__);
return 0;
}
/* environment buffer */
env = kzalloc(sizeof(struct kobj_uevent_env), GFP_KERNEL);
if (!env)
return -ENOMEM;
/* complete object path */
devpath = kobject_get_path(kobj, GFP_KERNEL);
if (!devpath) {
retval = -ENOENT;
goto exit;
}
/* default keys */
retval = add_uevent_var(env, "ACTION=%s", action_string);
if (retval)
goto exit;
retval = add_uevent_var(env, "DEVPATH=%s", devpath);
if (retval)
goto exit;
retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);
if (retval)
goto exit;
/* keys passed in from the caller */
if (envp_ext) {
for (i = 0; envp_ext[i]; i++) {
retval = add_uevent_var(env, "%s", envp_ext[i]);//将我们设置的参数,添加到环境变量中去
if (retval)
goto exit;
}
}
/* let the kset specific function add its stuff */
if (uevent_ops && uevent_ops->uevent) {
retval = uevent_ops->uevent(kset, kobj, env);
if (retval) {
pr_debug("kobject: '%s' (%p): %s: uevent() returned "
"%d\n", kobject_name(kobj), kobj,
__func__, retval);
goto exit;
}
}
/*
* Mark "add" and "remove" events in the object to ensure proper
* events to userspace during automatic cleanup. If the object did
* send an "add" event, "remove" will automatically generated by
* the core, if not already done by the caller.
*/
if (action == KOBJ_ADD)
kobj->state_add_uevent_sent = 1;
elseif (action == KOBJ_REMOVE)
kobj->state_remove_uevent_sent = 1;
/* we will send an event, so request a new sequence number */
spin_lock(&sequence_lock);
seq = ++uevent_seqnum;
spin_unlock(&sequence_lock);
retval = add_uevent_var(env, "SEQNUM=%llu", (unsignedlonglong)seq);
if (retval)
goto exit;
#if defined(CONFIG_NET)
/* send netlink message */
mutex_lock(&uevent_sock_mutex);
list_for_each_entry(ue_sk, &uevent_sock_list, list) {
struct sock *uevent_sock = ue_sk->sk;
struct sk_buff *skb;
size_t len;
/* allocate message with the maximum possible size */
len = strlen(action_string) + strlen(devpath) + 2;
skb = alloc_skb(len + env->buflen, GFP_KERNEL);
if (skb) {
char *scratch;
/* add header */
scratch = skb_put(skb, len);
sprintf(scratch, "%s@%s", action_string, devpath);
/* copy keys to our continuous event payload buffer */
for (i = 0; i < env->envp_idx; i++) {
len = strlen(env->envp[i]) + 1;
scratch = skb_put(skb, len);
strcpy(scratch, env->envp[i]);
}
NETLINK_CB(skb).dst_group = 1;
retval = netlink_broadcast_filtered(uevent_sock, skb,
0, 1, GFP_KERNEL,
kobj_bcast_filter,
kobj);
/* ENOBUFS should be handled in userspace */
if (retval == -ENOBUFS)
retval = 0;
} else
retval = -ENOMEM;
}
mutex_unlock(&uevent_sock_mutex);
#endif
/* call uevent_helper, usually only enabled during early boot */
if (uevent_helper[0] && !kobj_usermode_filter(kobj)) {
char *argv [3];
argv [0] = uevent_helper;
argv [1] = (char *)subsystem;
argv [2] = NULL;
retval = add_uevent_var(env, "HOME=/");
if (retval)
goto exit;
retval = add_uevent_var(env,
"PATH=/sbin:/bin:/usr/sbin:/usr/bin");
if (retval)
goto exit;
retval = call_usermodehelper(argv[0], argv,
env->envp, UMH_WAIT_EXEC);
}
exit:
kfree(devpath);
kfree(env);
return retval;
}
EXPORT_SYMBOL_GPL(kobject_uevent_env);
看下如何将用户空间需要的参数添加到环境变量中去的??
传进去需要的参数
add_uevent_var(env, "%s", envp_ext[i]);
**
* add_uevent_var - add key value string to the environment buffer
* @env: environment buffer structure
* @format: printf format for the key=value pair
*
* Returns 0 if environment variable was added successfully or -ENOMEM
* if no space was available.
*/
int add_uevent_var(struct kobj_uevent_env *env, constchar *format, ...)
{
va_list args;
int len;
if (env->envp_idx >= ARRAY_SIZE(env->envp)) {
WARN(1, KERN_ERR "add_uevent_var: too many keys\n");
return -ENOMEM;
}
va_start(args, format);
len = vsnprintf(&env->buf[env->buflen],
sizeof(env->buf) - env->buflen,
format, args);
va_end(args);
if (len >= (sizeof(env->buf) - env->buflen)) {
WARN(1, KERN_ERR "add_uevent_var: buffer size too small\n");
return -ENOMEM;
}
env->envp[env->envp_idx++] = &env->buf[env->buflen];
env->buflen += len + 1;
return 0;
}
EXPORT_SYMBOL_GPL(add_uevent_var);
上面的函数在将参数添加到环境变量中去,如果添加添加成功的话,那么就返回0
用户空间和内核空间通讯之Netlink
http://www.360doc.com/content/13/0912/17/496343_314005658.shtml
引言 Alan Cox在内核1.3版本的开发阶段最先引入了Netlink,刚开始时Netlink是以字符驱动接口的方式提供内核与用户空间的双向数据通信;随后,在2.1内核开发过程中,Alexey Kuznetsov将Netlink改写成一个更加灵活、且易于扩展的基于消息通信接口,并将其应用到高级路由子系统的基础框架里。自那时起,Netlink就成了Linux内核子系统和用户态的应用程序通信的主要手段之一。 2001年,ForCES IETF委员会正式对Netlink进行了标准化的工作。Jamal Hadi Salim提议将Netlink定义成一种用于网络设备的路由引擎组件和其控制管理组件之间通信的协议。不过他的建议最终没有被采纳,取而代之的是我们今天所看到的格局:Netlink被设计成一个新的协议域,domain。 Linux之父托瓦斯曾说过“Linux is evolution, not intelligent design”。什么意思?就是说,Netlink也同样遵循了Linux的某些设计理念,即没有完整的规范文档,亦没有设计文档。只有什么?你懂得---“Read the f**king source code”。 当然,本文不是分析Netlink在Linux上的实现机制,而是就“什么是Netlink”以及“如何用好Netlink”的话题和大家做个分享,只有在遇到问题时才需要去阅读内核源码弄清个所以然。 什么是Netlink 关于Netlink的理解,需要把握几个关键点: 1、面向数据报的无连接消息子系统 2、基于通用的BSD Socket架构而实现 关于第一点使我们很容易联想到UDP协议,能想到这一点就非常棒了。按着UDP协议来理解Netlink不是不无道理,只要你能触类旁通,做到“活学”,善于总结归纳、联想,最后实现知识迁移这就是学习的本质。Netlink可以实现内核->用户以及用户->内核的双向、异步的数据通信,同时它还支持两个用户进程之间、甚至两个内核子系统之间的数据通信。本文中,对后两者我们不予考虑,焦点集中在如何实现用户<->内核之间的数据通信。 看到第二点脑海中是不是瞬间闪现了下面这张图片呢?如果是,则说明你确实有慧根;当然,不是也没关系,慧根可以慢慢长嘛,呵呵。 在后面实战Netlink套接字编程时我们主要会用到socket(),bind(),sendmsg() 和 recvmsg()等系统调用,当然还有socket提供的轮训(polling)机制。 Netlink通信类型 Netlink支持两种类型的通信方式:单播和多播。 单播:经常用于一个用户进程和一个内核子系统之间1:1的数据通信。用户空间发送命令到内核,然后从内核接受命令的返回结果。 多播:经常用于一个内核进程和多个用户进程之间的1:N的数据通信。内核作为会话的发起者,用户空间的应用程序是接收者。为了实现这个功能,内核空间的程序会创建一个多播组,然后所有用户空间的对该内核进程发送的消息感兴趣的进程都加入到该组即可接收来自内核发送的消息了。如下: 其中进程A和子系统1之间是单播通信,进程B、C和子系统2是多播通信。上图还向我们说明了一个信息。从用户空间传递到内核的数据是不需要排队的,即其操作是同步完成;而从内核空间向用户空间传递数据时需要排队,是异步的。了解了这一点在开发基于Netlink的应用模块时可以使我们少走很多弯路。假如,你向内核发送了一个消息需要获取内核中某些信息,比如路由表,或其他信息,如果路由表过于庞大,那么内核在通过Netlink向你返回数据时,你可以好生琢磨一下如何接收这些数据的问题,毕竟你已经看到了那个输出队列了,不能视而不见啊。 Netlink的消息格式 Netlink消息由两部分组成:消息头和有效数据载荷,且整个Netlink消息是4字节对齐,一般按主机字节序进行传递。消息头为固定的16字节,消息体长度可变: Netlink的消息头 消息头定义在<include/linux/netlink.h>文件里,由结构体nlmsghdr表示: - struct nlmsghdr
- {
- __u32 nlmsg_len; /* Length of message including header */
- __u16 nlmsg_type; /* Message content */
- __u16 nlmsg_flags; /* Additional flags */
- __u32 nlmsg_seq; /* Sequence number */
- __u32 nlmsg_pid; /* Sending process PID */
- };
消息头中各成员属性的解释及说明: nlmsg_len:整个消息的长度,按字节计算。包括了Netlink消息头本身。 nlmsg_type:消息的类型,即是数据还是控制消息。目前(内核版本2.6.21)Netlink仅支持四种类型的控制消息,如下: NLMSG_NOOP-空消息,什么也不做; NLMSG_ERROR-指明该消息中包含一个错误; NLMSG_DONE-如果内核通过Netlink队列返回了多个消息,那么队列的最后一条消息的类型为NLMSG_DONE,其余所有消息的nlmsg_flags属性都被设置NLM_F_MULTI位有效。 NLMSG_OVERRUN-暂时没用到。 nlmsg_flags:附加在消息上的额外说明信息,如上面提到的NLM_F_MULTI。摘录如下: 标记 | 作用及说明 | NLM_F_REQUEST | 如果消息中有该标记位,说明这是一个请求消息。所有从用户空间到内核空间的消息都要设置该位,否则内核将向用户返回一个EINVAL无效参数的错误 | NLM_F_MULTI | 消息从用户->内核是同步的立刻完成,而从内核->用户则需要排队。如果内核之前收到过来自用户的消息中有NLM_F_DUMP位为1的消息,那么内核就会向用户空间发送一个由多个Netlink消息组成的链表。除了最后个消息外,其余每条消息中都设置了该位有效。 | NLM_F_ACK | 该消息是内核对来自用户空间的NLM_F_REQUEST消息的响应 | NLM_F_ECHO | 如果从用户空间发给内核的消息中该标记为1,则说明用户的应用进程要求内核将用户发给它的每条消息通过单播的形式再发送给用户进程。和我们通常说的“回显”功能类似。 | … | … | 大家只要知道nlmsg_flags有多种取值就可以,至于每种值的作用和意义,通过谷歌和源代码一定可以找到答案,这里就不展开了。上一张2.6.21内核中所有的取值情况: nlmsg_seq:消息序列号。因为Netlink是面向数据报的,所以存在丢失数据的风险,但是Netlink提供了如何确保消息不丢失的机制,让程序开发人员根据其实际需求而实现。消息序列号一般和NLM_F_ACK类型的消息联合使用,如果用户的应用程序需要保证其发送的每条消息都成功被内核收到的话,那么它发送消息时需要用户程序自己设置序号,内核收到该消息后对提取其中的序列号,然后在发送给用户程序回应消息里设置同样的序列号。有点类似于TCP的响应和确认机制。 注意:当内核主动向用户空间发送广播消息时,消息中的该字段总是为0。 nlmsg_pid :当用户空间的进程和内核空间的某个子系统之间通过Netlink建立了数据交换的通道后,Netlink会为每个这样的通道分配一个唯一的数字标识。其主要作用就是将来自用户空间的请求消息和响应消息进行关联。说得直白一点,假如用户空间存在多个用户进程,内核空间同样存在多个进程,Netlink必须提供一种机制用于确保每一对“用户-内核”空间通信的进程之间的数据交互不会发生紊乱。 即,进程A、B通过Netlink向子系统1获取信息时,子系统1必须确保回送给进程A的响应数据不会发到进程B那里。主要适用于用户空间的进程从内核空间获取数据的场景。通常情况下,用户空间的进程在向内核发送消息时一般通过系统调用getpid()将当前进程的进程号赋给该变量,即用户空间的进程希望得到内核的响应时才会这么做。从内核主动发送到用户空间的消息该字段都被设置为0。 Netlink的消息体 Netlink的消息体采用TLV(Type-Length-Value)格式: Netlink每个属性都由<include/linux/netlink.h>文件里的struct nlattr{}来表示: Netlink提供的错误指示消息 当用户空间的应用程序和内核空间的进程之间通过Netlink通信时发生了错误,Netlink必须向用户空间通报这种错误。Netlink对错误消息进行了单独封装,<include/linux/netlink.h>: - struct nlmsgerr
- {
- int error; //标准的错误码,定义在errno.h头文件中。可以用perror()来解释
- struct nlmsghdr msg; //指明了哪条消息触发了结构体中error这个错误值
- };
Netlink编程需要注意的问题 基于Netlink的用户-内核通信,有两种情况可能会导致丢包: 1、内存耗尽; 2、用户空间接收进程的缓冲区溢出。导致缓冲区溢出的主要原因有可能是:用户空间的进程运行太慢;或者接收队列太短。 如果Netlink不能将消息正确传递到用户空间的接收进程,那么用户空间的接收进程在调用recvmsg()系统调用时就会返回一个内存不足(ENOBUFS)的错误,这一点需要注意。换句话说,缓冲区溢出的情况是不会发送在从用户->内核的sendmsg()系统调用里,原因前面我们也说过了,请大家自己思考一下。 当然,如果使用的是阻塞型socket通信,也就不存在内存耗尽的隐患了,这又是为什么呢?赶紧去谷歌一下,查查什么是阻塞型socket吧。学而不思则罔,思而不学则殆嘛。 Netlink的地址结构体 在TCP博文中我们提到过在Internet编程过程中所用到的地址结构体和标准地址结构体,它们和Netlink地址结构体的关系如下: struct sockaddr_nl{}的详细定义和描述如下: - struct sockaddr_nl
- {
- sa_family_t nl_family; /*该字段总是为AF_NETLINK */
- unsigned short nl_pad; /* 目前未用到,填充为0*/
- __u32 nl_pid; /* process pid */
- __u32 nl_groups; /* multicast groups mask */
- };
nl_pid:该属性为发送或接收消息的进程ID,前面我们也说过,Netlink不仅可以实现用户-内核空间的通信还可使现实用户空间两个进程之间,或内核空间两个进程之间的通信。该属性为0时一般适用于如下两种情况: 第一,我们要发送的目的地是内核,即从用户空间发往内核空间时,我们构造的Netlink地址结构体中nl_pid通常情况下都置为0。这里有一点需要跟大家交代一下,在Netlink规范里,PID全称是Port-ID(32bits),其主要作用是用于唯一的标识一个基于netlink的socket通道。通常情况下nl_pid都设置为当前进程的进程号。然而,对于一个进程的多个线程同时使用netlink socket的情况,nl_pid的设置一般采用如下这个样子来实现: - pthread_self() << 16 | getpid();
第二,从内核发出的多播报文到用户空间时,如果用户空间的进程处在该多播组中,那么其地址结构体中nl_pid也设置为0,同时还要结合下面介绍到的另一个属性。 nl_groups:如果用户空间的进程希望加入某个多播组,则必须执行bind()系统调用。该字段指明了调用者希望加入的多播组号的掩码(注意不是组号,后面我们会详细讲解这个字段)。如果该字段为0则表示调用者不希望加入任何多播组。对于每个隶属于Netlink协议域的协议,最多可支持32个多播组(因为nl_groups的长度为32比特),每个多播组用一个比特来表示。 今天我们来动手演练一下Netlink的用法,看看它到底是如何实现用户-内核空间的数据通信的。我们依旧是在2.6.21的内核环境下进行开发。 在</usr/include/linux/netlink.h>文件里包含了Netlink协议簇已经定义好的一些预定义协议: - #define NETLINK_ROUTE 0 /* Routing/device hook */
- #define NETLINK_UNUSED 1 /* Unused number */
- #define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */
- #define NETLINK_FIREWALL 3 /* Firewalling hook */
- #define NETLINK_INET_DIAG 4 /* INET socket monitoring */
- #define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */
- #define NETLINK_XFRM 6 /* ipsec */
- #define NETLINK_SELINUX 7 /* SELinux event notifications */
- #define NETLINK_ISCSI 8 /* Open-iSCSI */
- #define NETLINK_AUDIT 9 /* auditing */
- #define NETLINK_FIB_LOOKUP 10
- #define NETLINK_CONNECTOR 11
- #define NETLINK_NETFILTER 12 /* netfilter subsystem */
- #define NETLINK_IP6_FW 13
- #define NETLINK_DNRTMSG 14 /* DECnet routing messages */
- #define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */
- #define NETLINK_GENERIC 16
- /* leave room for NETLINK_DM (DM Events) */
- #define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */
- #define NETLINK_ECRYPTFS 19
- #define NETLINK_TEST 20 /* 用户添加的自定义协议 */
如果我们在Netlink协议簇里开发一个新的协议,只要在该文件中定义协议号即可,例如我们定义一种基于Netlink协议簇的、协议号是20的自定义协议,如上所示。同时记得,将内核头文件目录中的netlink.h也做对应的修改,在我的系统中它的路径是:/usr/src/linux-2.6.21/include/linux/netlink.h 接下来我们在用户空间以及内核空间模块的开发过程中就可以使用这种协议了,一共分为三个阶段。 Stage 1: 我们首先实现的功能是用户->内核的单向数据通信,即用户空间发送一个消息给内核,然后内核将其打印输出,就这么简单。用户空间的示例代码如下【mynlusr.c】 - #include <sys/stat.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <sys/socket.h>
- #include <sys/types.h>
- #include <string.h>
- #include <asm/types.h>
- #include <linux/netlink.h>
- #include <linux/socket.h>
- #define MAX_PAYLOAD 1024 /*消息最大负载为1024字节*/
- int main(int argc, char* argv[])
- {
- struct sockaddr_nl dest_addr;
- struct nlmsghdr *nlh = NULL;
- struct iovec iov;
- int sock_fd=-1;
- struct msghdr msg;
-
- if(-1 == (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){ //创建套接字
- perror("can't create netlink socket!");
- return 1;
- }
- memset(&dest_addr, 0, sizeof(dest_addr));
- dest_addr.nl_family = AF_NETLINK;
- dest_addr.nl_pid = 0; /*我们的消息是发给内核的*/
- dest_addr.nl_groups = 0; /*在本示例中不存在使用该值的情况*/
-
- //将套接字和Netlink地址结构体进行绑定
- if(-1 == bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
- perror("can't bind sockfd with sockaddr_nl!");
- return 1;
- }
- if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
- perror("alloc mem failed!");
- return 1;
- }
- memset(nlh,0,MAX_PAYLOAD);
- /* 填充Netlink消息头部 */
- nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
- nlh->nlmsg_pid = 0;
- nlh->nlmsg_type = NLMSG_NOOP; //指明我们的Netlink是消息负载是一条空消息
- nlh->nlmsg_flags = 0;
- /*设置Netlink的消息内容,来自我们命令行输入的第一个参数*/
- strcpy(NLMSG_DATA(nlh), argv[1]);
- /*这个是模板,暂时不用纠结为什么要这样用。有时间详细讲解socket时再说*/
- memset(&iov, 0, sizeof(iov));
- iov.iov_base = (void *)nlh;
- iov.iov_len = nlh->nlmsg_len;
- memset(&msg, 0, sizeof(msg));
- msg.msg_iov = &iov;
- msg.msg_iovlen = 1;
- sendmsg(sock_fd, &msg, 0); //通过Netlink socket向内核发送消息
- /* 关闭netlink套接字 */
- close(sock_fd);
- free(nlh);
- return 0;
- }
上面的代码逻辑已经非常清晰了,都是socket编程的API,唯一不同的是我们这次编程是针对Netlink协议簇的。这里我们提前引入了BSD层的消息结构体struct msghdr{},定义在<include/linux/socket.h>文件里,以及其数据块struct iovec{}定义在<include/linux/uio.h>头文件里。这里就不展开了,大家先记住这个用法就行。以后有时间再深入到socket的骨子里去转悠一番。 另外,需要格外注意的就是Netlink的地址结构体和其消息头结构中pid字段为0的情况,很容易让人产生混淆,再总结一下: | 0 | netlink地址结构体.nl_pid | 1、内核发出的多播报文 2、消息的接收方是内核,即从用户空间发往内核的消息 | netlink消息头体. nlmsg_pid | 来自内核主动发出的消息 | 这个例子仅是从用户空间到内核空间的单向数据通信,所以Netlink地址结构体中我们设置了dest_addr.nl_pid = 0,说明我们的报文的目的地是内核空间;在填充Netlink消息头部时,我们做了nlh->nlmsg_pid = 0这样的设置。 需要注意几个宏的使用: NLMSG_SPACE(MAX_PAYLOAD),该宏用于返回不小于MAX_PAYLOAD且4字节对齐的最小长度值,一般用于向内存系统申请空间是指定所申请的内存字节数,和NLMSG_LENGTH(len)所不同的是,前者所申请的空间里不包含Netlink消息头部所占的字节数,后者是消息负载和消息头加起来的总长度。 NLMSG_DATA(nlh),该宏用于返回Netlink消息中数据部分的首地址,在写入和读取消息数据部分时会用到它。 它们之间的关系如下: 内核空间的示例代码如下【mynlkern.c】: - #include <linux/kernel.h>
- #include <linux/module.h>
- #include <linux/skbuff.h>
- #include <linux/init.h>
- #include <linux/ip.h>
- #include <linux/types.h>
- #include <linux/sched.h>
- #include <net/sock.h>
- #include <linux/netlink.h>
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("Koorey King");
- struct sock *nl_sk = NULL;
- static void nl_data_ready (struct sock *sk, int len)
- {
- struct sk_buff *skb;
- struct nlmsghdr *nlh = NULL;
- while((skb = skb_dequeue(&sk->sk_receive_queue)) != NULL)
- {
- nlh = (struct nlmsghdr *)skb->data;
- printk("%s: received netlink message payload: %s \n", __FUNCTION__,(char*)NLMSG_DATA(nlh));
- kfree_skb(skb);
- }
- printk("recvied finished!\n");
- }
- static int __init myinit_module()
- {
- printk("my netlink in\n");
- nl_sk =netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
- return 0;
- }
- static void __exit mycleanup_module()
- {
- printk("my netlink out!\n");
- sock_release(nl_sk->sk_socket);
- }
- module_init(myinit_module);
- module_exit(mycleanup_module);
在内核模块的初始化函数里我们用 nl_sk = netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE); 创建了一个内核态的socket,第一个参数我们扩展的协议号;第二个参数为多播组号,目前我们用不上,将其置为0;第三个参数是个回调函数,即当内核的Netlink socket套接字收到数据时的处理函数;第四个参数就不多说了。 在回调函数nl_data_ready()中,我们不断的从socket的接收队列去取数据,一旦拿到数据就将其打印输出。在协议栈的INET层,用于存储数据的是大名鼎鼎的sk_buff结构,所以我们通过nlh = (struct nlmsghdr *)skb->data;可以拿到netlink的消息体,然后通过NLMSG_DATA(nlh)定位到netlink的消息负载。 将上述代码编译后测试结果如下: Stage 2: 我们将上面的代码稍加改造就可以实现用户<->内核的双向数据通信。 首先是改造用户空间的代码: - #include <sys/stat.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <sys/socket.h>
- #include <sys/types.h>
- #include <string.h>
- #include <asm/types.h>
- #include <linux/netlink.h>
- #include <linux/socket.h>
- #define MAX_PAYLOAD 1024 /*消息最大负载为1024字节*/
- int main(int argc, char* argv[])
- {
- struct sockaddr_nl dest_addr;
- struct nlmsghdr *nlh = NULL;
- struct iovec iov;
- int sock_fd=-1;
- struct msghdr msg;
- if(-1 == (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
- perror("can't create netlink socket!");
- return 1;
- }
- memset(&dest_addr, 0, sizeof(dest_addr));
- dest_addr.nl_family = AF_NETLINK;
- dest_addr.nl_pid = 0; /*我们的消息是发给内核的*/
- dest_addr.nl_groups = 0; /*在本示例中不存在使用该值的情况*/
- if(-1 == bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
- perror("can't bind sockfd with sockaddr_nl!");
- return 1;
- }
- if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
- perror("alloc mem failed!");
- return 1;
- }
-
- memset(nlh,0,MAX_PAYLOAD);
- /* 填充Netlink消息头部 */
- nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
- nlh->nlmsg_pid = getpid();//我们希望得到内核回应,所以得告诉内核我们ID号
- nlh->nlmsg_type = NLMSG_NOOP; //指明我们的Netlink是消息负载是一条空消息
- nlh->nlmsg_flags = 0;
- /*设置Netlink的消息内容,来自我们命令行输入的第一个参数*/
- strcpy(NLMSG_DATA(nlh), argv[1]);
- /*这个是模板,暂时不用纠结为什么要这样用。*/
- memset(&iov, 0, sizeof(iov));
- iov.iov_base = (void *)nlh;
- iov.iov_len = nlh->nlmsg_len;
- memset(&msg, 0, sizeof(msg));
- msg.msg_iov = &iov;
- msg.msg_iovlen = 1;
- sendmsg(sock_fd, &msg, 0); //通过Netlink socket向内核发送消息
- //接收内核消息的消息
- printf("waiting message from kernel!\n");
- memset((char*)NLMSG_DATA(nlh),0,1024);
- recvmsg(sock_fd,&msg,0);
- printf("Got response: %s\n",NLMSG_DATA(nlh));
- /* 关闭netlink套接字 */
- close(sock_fd);
- free(nlh);
- return 0;
- }
内核空间的修改如下: - #include <linux/kernel.h>
- #include <linux/module.h>
- #include <linux/skbuff.h>
- #include <linux/init.h>
- #include <linux/ip.h>
- #include <linux/types.h>
- #include <linux/sched.h>
- #include <net/sock.h>
- #include <net/netlink.h> /*该文头文件里包含了linux/netlink.h,因为我们要用到net/netlink.h中的某些API函数,nlmsg_pug()*/
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("Koorey King");
- struct sock *nl_sk = NULL;
- //向用户空间发送消息的接口
- void sendnlmsg(char *message,int dstPID)
- {
- struct sk_buff *skb;
- struct nlmsghdr *nlh;
- int len = NLMSG_SPACE(MAX_MSGSIZE);
- int slen = 0;
- if(!message || !nl_sk){
- return;
- }
- // 为新的 sk_buffer申请空间
- skb = alloc_skb(len, GFP_KERNEL);
- if(!skb){
- printk(KERN_ERR "my_net_link: alloc_skb Error./n");
- return;
- }
- slen = strlen(message)+1;
- //用nlmsg_put()来设置netlink消息头部
- nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);
- // 设置Netlink的控制块
- NETLINK_CB(skb).pid = 0; // 消息发送者的id标识,如果是内核发的则置0
- NETLINK_CB(skb).dst_group = 0; //如果目的组为内核或某一进程,该字段也置0
- message[slen] = '\0';
- memcpy(NLMSG_DATA(nlh), message, slen+1);
- //通过netlink_unicast()将消息发送用户空间由dstPID所指定了进程号的进程
- netlink_unicast(nl_sk,skb,dstPID,0);
- printk("send OK!\n");
- return;
- }
- static void nl_data_ready (struct sock *sk, int len)
- {
- struct sk_buff *skb;
- struct nlmsghdr *nlh = NULL;
- while((skb = skb_dequeue(&sk->sk_receive_queue)) != NULL)
- {
- nlh = (struct nlmsghdr *)skb->data;
- printk("%s: received netlink message payload: %s \n", __FUNCTION__,(char*)NLMSG_DATA(nlh));
- kfree_skb(skb);
- sendnlmsg("I see you",nlh->nlmsg_pid); //发送者的进程ID我们已经将其存储在了netlink消息头部里的nlmsg_pid字段里,所以这里可以拿来用。
- }
- printk("recvied finished!\n");
- }
- static int __init myinit_module()
- {
- printk("my netlink in\n");
- nl_sk = netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
- return 0;
- }
- static void __exit mycleanup_module()
- {
- printk("my netlink out!\n");
- sock_release(nl_sk->sk_socket);
- }
- module_init(myinit_module);
- module_exit(mycleanup_module);
重新编译后,测试结果如下: Stage 3: 前面我们提到过,如果用户进程希望加入某个多播组时才需要调用bind()函数。前面的示例中我们没有这个需求,可还是调了bind(),心头有些不爽。在前几篇博文里有关于socket编程时几个常见API的详细解释和说明,不明白的童鞋可以回头去复习一下。 因为Netlink是面向无连接的数据报的套接字,所以我们还可以用sendto()和recvfrom()来实现数据的收发,这次我们不再调用bind()。将Stage 2的例子稍加改造一下,用户空间的修改如下: - #include <sys/stat.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <sys/socket.h>
- #include <sys/types.h>
- #include <string.h>
- #include <asm/types.h>
- #include <linux/netlink.h>
- #include <linux/socket.h>
- #define MAX_PAYLOAD 1024 /*消息最大负载为1024字节*/
- int main(int argc, char* argv[])
- {
- struct sockaddr_nl dest_addr;
- struct nlmsghdr *nlh = NULL;
- //struct iovec iov;
- int sock_fd=-1;
- //struct msghdr msg;
- if(-1 == (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
- perror("can't create netlink socket!");
- return 1;
- }
- memset(&dest_addr, 0, sizeof(dest_addr));
- dest_addr.nl_family = AF_NETLINK;
- dest_addr.nl_pid = 0; /*我们的消息是发给内核的*/
- dest_addr.nl_groups = 0; /*在本示例中不存在使用该值的情况*/
- /*不再调用bind()函数了
- if(-1 == bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
- perror("can't bind sockfd with sockaddr_nl!");
- return 1;
- }*/
- if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
- perror("alloc mem failed!");
- return 1;
- }
- memset(nlh,0,MAX_PAYLOAD);
- /* 填充Netlink消息头部 */
- nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
- nlh->nlmsg_pid = getpid();//我们希望得到内核回应,所以得告诉内核我们ID号
- nlh->nlmsg_type = NLMSG_NOOP; //指明我们的Netlink是消息负载是一条空消息
- nlh->nlmsg_flags = 0;
- /*设置Netlink的消息内容,来自我们命令行输入的第一个参数*/
- strcpy(NLMSG_DATA(nlh), argv[1]);
- /*这个模板就用不上了。*/
- /*
- memset(&iov, 0, sizeof(iov));
- iov.iov_base = (void *)nlh;
- iov.iov_len = nlh->nlmsg_len;
- memset(&msg, 0, sizeof(msg));
- msg.msg_iov = &iov;
- msg.msg_iovlen = 1;
- */
- //sendmsg(sock_fd, &msg, 0); //不再用这种方式发消息到内核
- sendto(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),sizeof(dest_addr));
-
- //接收内核消息的消息
- printf("waiting message from kernel!\n");
- //memset((char*)NLMSG_DATA(nlh),0,1024);
- memset(nlh,0,MAX_PAYLOAD); //清空整个Netlink消息头包括消息头和负载
- //recvmsg(sock_fd,&msg,0);
- recvfrom(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),NULL);
- printf("Got response: %s\n",NLMSG_DATA(nlh));
- /* 关闭netlink套接字 */
- close(sock_fd);
- free(nlh);
- return 0;
- }
内核空间的代码完全不用修改,我们仍然用netlink_unicast()从内核空间发送消息到用户空间。 重新编译后,测试结果如下: 和Stage 2中代码运行效果完全一样。也就是说,在开发Netlink程序过程中,如果没牵扯到多播机制,那么用户空间的socket代码其实是不用执行bind()系统调用的,但此时就需要用sendto()和recvfrom()完成数据的发送和接收的任务;如果执行了bind()系统调用,当然也可以继续用sendto()和recvfrom(),但给它们传递的参数就有所区别。这时候一般使用sendmsg()和recvmsg()来完成数据的发送和接收。大家根据自己的实际情况灵活选择。 关于Netlink多播机制的用法 在上一篇博文中我们所遇到的情况都是用户空间作为消息进程的发起者,Netlink还支持内核作为消息的发送方的情况。这一般用于内核主动向用户空间报告一些内核状态,例如我们在用户空间看到的USB的热插拔事件的通告就是这样的应用。 先说一下我们的目标,内核线程每个一秒钟往一个多播组里发送一条消息,然后用户空间所以加入了该组的进程都会收到这样的消息,并将消息内容打印出来。 Netlink地址结构体中的nl_groups是32位,也就是说每种Netlink协议最多支持32个多播组。如何理解这里所说的每种Netlink协议?在</usr/include/linux/netlink.h>里预定义的如下协议都是Netlink协议簇的具体协议,还有我们添加的NETLINK_TEST也是一种Netlink协议。 - #define NETLINK_ROUTE 0 /* Routing/device hook */
- #define NETLINK_UNUSED 1 /* Unused number */
- #define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */
- #define NETLINK_FIREWALL 3 /* Firewalling hook */
- #define NETLINK_INET_DIAG 4 /* INET socket monitoring */
- #define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */
- #define NETLINK_XFRM 6 /* ipsec */
- #define NETLINK_SELINUX 7 /* SELinux event notifications */
- #define NETLINK_ISCSI 8 /* Open-iSCSI */
- #define NETLINK_AUDIT 9 /* auditing */
- #define NETLINK_FIB_LOOKUP 10
- #define NETLINK_CONNECTOR 11
- #define NETLINK_NETFILTER 12 /* netfilter subsystem */
- #define NETLINK_IP6_FW 13
- #define NETLINK_DNRTMSG 14 /* DECnet routing messages */
- #define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */
- #define NETLINK_GENERIC 16
- /* leave room for NETLINK_DM (DM Events) */
- #define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */
- #define NETLINK_ECRYPTFS 19
- #define NETLINK_TEST 20 /* 用户添加的自定义协议 */
在我们自己添加的NETLINK_TEST协议里,同样地,最多允许我们设置32个多播组,每个多播组用1个比特表示,所以不同的多播组不可能出现重复。你可以根据自己的实际需求,决定哪个多播组是用来做什么的。用户空间的进程如果对某个多播组感兴趣,那么它就加入到该组中,当内核空间的进程往该组发送多播消息时,所有已经加入到该多播组的用户进程都会收到该消息。 再回到我们Netlink地址结构体里的nl_groups成员,它是多播组的地址掩码,注意是掩码不是多播组的组号。如何根据多播组号取得多播组号的掩码呢?在af_netlink.c中有个函数: - static u32 netlink_group_mask(u32 group)
- {
- return group ? 1 << (group - 1) : 0;
- }
也就是说,在用户空间的代码里,如果我们要加入到多播组1,需要设置nl_groups设置为1;多播组2的掩码为2;多播组3的掩码为4,依次类推。为0表示我们不希望加入任何多播组。理解这一点很重要。所以我们可以在用户空间也定义一个类似于netlink_group_mask()的功能函数,完成从多播组号到多播组掩码的转换。最终用户空间的代码如下: - #include <sys/stat.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <sys/socket.h>
- #include <sys/types.h>
- #include <string.h>
- #include <asm/types.h>
- #include <linux/netlink.h>
- #include <linux/socket.h>
- #include <errno.h>
- #define MAX_PAYLOAD 1024 // Netlink消息的最大载荷的长度
- unsigned int netlink_group_mask(unsigned int group)
- {
- return group ? 1 << (group - 1) : 0;
- }
- int main(int argc, char* argv[])
- {
- struct sockaddr_nl src_addr;
- struct nlmsghdr *nlh = NULL;
- struct iovec iov;
- struct msghdr msg;
- int sock_fd, retval;
- // 创建Socket
- sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
- if(sock_fd == -1){
- printf("error getting socket: %s", strerror(errno));
- return -1;
- }
- memset(&src_addr, 0, sizeof(src_addr));
- src_addr.nl_family = PF_NETLINK;
- src_addr.nl_pid = 0; // 表示我们要从内核接收多播消息。注意:该字段为0有双重意义,另一个意义是表示我们发送的数据的目的地址是内核。
- src_addr.nl_groups = netlink_group_mask(atoi(argv[1])); // 多播组的掩码,组号来自我们执行程序时输入的第一个参数
- // 因为我们要加入到一个多播组,所以必须调用bind()。
- retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
- if(retval < 0){
- printf("bind failed: %s", strerror(errno));
- close(sock_fd);
- return -1;
- }
- // 为接收Netlink消息申请存储空间
- nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
- if(!nlh){
- printf("malloc nlmsghdr error!\n");
- close(sock_fd);
- return -1;
- }
- memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
- iov.iov_base = (void *)nlh;
- iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
- memset(&msg, 0, sizeof(msg));
- msg.msg_iov = &iov;
- msg.msg_iovlen = 1;
- // 从内核接收消息
- printf("waitinf for...\n");
- recvmsg(sock_fd, &msg, 0);
- printf("Received message: %s \n", NLMSG_DATA(nlh));
-
- close(sock_fd);
- return 0;
- }
可以看到,用户空间的程序基本没什么变化,唯一需要格外注意的就是Netlink地址结构体中的nl_groups的设置。由于对它的解释很少,加之没有有效的文档,所以我也是一边看源码,一边在网上搜集资料。有分析不当之处,还请大家帮我指出。 内核空间我们添加了内核线程和内核线程同步方法completion的使用。内核空间修改后的代码如下: - #include <linux/kernel.h>
- #include <linux/module.h>
- #include <linux/skbuff.h>
- #include <linux/init.h>
- #include <linux/ip.h>
- #include <linux/types.h>
- #include <linux/sched.h>
- #include <net/sock.h>
- #include <net/netlink.h>
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("Koorey King");
- struct sock *nl_sk = NULL;
- static struct task_struct *mythread = NULL; //内核线程对象
- //向用户空间发送消息的接口
- void sendnlmsg(char *message/*,int dstPID*/)
- {
- struct sk_buff *skb;
- struct nlmsghdr *nlh;
- int len = NLMSG_SPACE(MAX_MSGSIZE);
- int slen = 0;
- if(!message || !nl_sk){
- return;
- }
- // 为新的 sk_buffer申请空间
- skb = alloc_skb(len, GFP_KERNEL);
- if(!skb){
- printk(KERN_ERR "my_net_link: alloc_skb Error./n");
- return;
- }
- slen = strlen(message)+1;
- //用nlmsg_put()来设置netlink消息头部
- nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);
- // 设置Netlink的控制块里的相关信息
- NETLINK_CB(skb).pid = 0; // 消息发送者的id标识,如果是内核发的则置0
- NETLINK_CB(skb).dst_group = 5; //多播组号为5,但置成0好像也可以。
- message[slen] = '\0';
- memcpy(NLMSG_DATA(nlh), message, slen+1);
- //通过netlink_unicast()将消息发送用户空间由dstPID所指定了进程号的进程
- //netlink_unicast(nl_sk,skb,dstPID,0);
- netlink_broadcast(nl_sk, skb, 0,5, GFP_KERNEL); //发送多播消息到多播组5,这里我故意没有用1之类的“常见”值,目的就是为了证明我们上面提到的多播组号和多播组号掩码之间的对应关系
- printk("send OK!\n");
- return;
- }
- //每隔1秒钟发送一条“I am from kernel!”消息,共发10个报文
- static int sending_thread(void *data)
- {
- int i = 10;
- struct completion cmpl;
- while(i--){
- init_completion(&cmpl);
- wait_for_completion_timeout(&cmpl, 1 * HZ);
- sendnlmsg("I am from kernel!");
- }
- printk("sending thread exited!");
- return 0;
- }
- static int __init myinit_module()
- {
- printk("my netlink in\n");
- nl_sk = netlink_kernel_create(NETLINK_TEST,0,NULL,THIS_MODULE);
- if(!nl_sk){
- printk(KERN_ERR "my_net_link: create netlink socket error.\n");
- return 1;
- }
- printk("my netlink: create netlink socket ok.\n");
- mythread = kthread_run(sending_thread,NULL,"thread_sender");
- return 0;
- }
- static void __exit mycleanup_module()
- {
- if(nl_sk != NULL){
- sock_release(nl_sk->sk_socket);
- }
- printk("my netlink out!\n");
- }
- module_init(myinit_module);
- module_exit(mycleanup_module);
关于内核中netlink_kernel_create(int unit, unsigned int groups,…)函数里的第二个参数指的是我们内核进程最多能处理的多播组的个数,如果该值小于32,则默认按32处理,所以在调用netlink_kernel_create()函数时可以不用纠结第二个参数,一般将其置为0就可以了。 在skbuff{}结构体中,有个成员叫做"控制块",源码对它的解释如下: - struct sk_buff {
- /* These two members must be first. */
- struct sk_buff *next;
- struct sk_buff *prev;
- … …
- /*
- * This is the control buffer. It is free to use for every
- * layer. Please put your private variables there. If you
- * want to keep them across layers you have to do a skb_clone()
- * first. This is owned by whoever has the skb queued ATM.
- */
- char cb[48];
- … …
- }
当内核态的Netlink发送数据到用户空间时一般需要填充skbuff的控制块,填充的方式是通过强制类型转换,将其转换成struct netlink_skb_parms{}之后进行填充赋值的: - struct netlink_skb_parms
- {
- struct ucred creds; /* Skb credentials */
- __u32 pid;
- __u32 dst_group;
- kernel_cap_t eff_cap;
- __u32 loginuid; /* Login (audit) uid */
- __u32 sid; /* SELinux security id */
- };
填充时的模板代码如下: - NETLINK_CB(skb).pid=xx;
- NETLINK_CB(skb).dst_group=xx;
这里要注意的是在Netlink协议簇里提到的skbuff的cb控制块里保存的是属于Netlink的私有信息。怎么讲,就是Netlink会用该控制块里的信息来完成它所提供的一些功能,只是完成Netlink功能所必需的一些私有数据。打个比方,以开车为例,开车的时候我们要做的就是打火、控制方向盘、适当地控制油门和刹车,车就开动了,这就是汽车提供给我们的“功能”。汽车的发动机,轮胎,传动轴,以及所用到的螺丝螺栓等都属于它的“私有”数据cb。汽车要运行起来这些东西是不可或缺的,但它们之间的协作和交互对用户来说又是透明的。就好比我们Netlink的私有控制结构struct netlink_skb_parms{}一样。 目前我们的例子中,将NETLINK_CB(skb).dst_group设置为相应的多播组号和0效果都是一样,用户空间都可以收到该多播消息,原因还不是很清楚,还请Netlink的大虾们帮我点拨点拨。 编译后重新运行,最后的测试结果如下: 注意,这里一定要先执行insmod加载内核模块,然后再运行用户空间的程序。如果没有加载mynlkern.ko而直接执行./test 5在bind()系统调用时会报如下的错误: bind failed: No such file or directory 因为网上有写文章在讲老版本Netlink的多播时用法时先执行了用户空间的程序,然后才加载内核模块,现在(2.6.21)已经行不通了,这一点请大家注意。 小结:通过这三篇博文我们对Netlink有了初步的认识,并且也可以开发基于Netlink的基本应用程序。但这只是冰山一角,要想写出高质量、高效率的软件模块还有些差距,特别是对Netlink本质的理解还需要提高一个层次,当然这其中牵扯到内核编程的很多基本功,如临界资源的互斥、线程安全性保护、用Netlink传递大数据时的处理等等都是开发人员需要考虑的问题。 Linux中与内核通信的Netlink机制(实例) Netlink在2.6版本的内核中变化也是很大的,在最新的2.6.37内核中,其定义已经改成下面这种形式,传递的参数已经达到6个。其中第一个参数 和mutex参数都是最新添加的。Mutex也可以为空。这里主要是关于内核空间中的netlink函数的使用。 extern struct sock *netlink_kernel_create(struct net *net, int unit,unsigned int groups, void (*input)(struct sk_buff *skb), struct mutex *cb_mutex, struct module *module); | struct net是一个网络名字空间namespace,在不同的名字空间里面可以有自己的转发信息库,有自己的一套net_device等等。默认情况下都是使用 init_net这个全局变量,下面是内核中调用netlink_kernel_create()函数的一个示例。 在内核中, audit_sock = netlink_kernel_create(&init_net, NETLINK_AUDIT, 0, audit_receive, NULL, THIS_MODULE); | 模块调用函数 netlink_unicast 来发送单播消息: int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock) | 参数ssk为函数 netlink_kernel_create()返回的socket,参数skb存放消息,它的data字段指向要发送的netlink消息结构,而 skb的控制块保存了消息的地址信息,前面的宏NETLINK_CB(skb)就用于方便设置该控制块, 参数pid为接收消息进程的pid,参数nonblock表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函 数在没有接收缓存可利用 定时睡眠。 netlink的内核实现在.c文件 net/core/af_netlink.c中,内核模块要想使用netlink,也必须包含头文件linux/netlink.h。内核使用 netlink需要专门的API,这完全不同于用户态应用对netlink的使用。如果用户需要增加新的netlink协议类型,必须通过修改 linux/netlink.h来实现,当然,目前的netlink实现已经包含了一个通用的协议类型NETLINK_GENERIC以方便用户使用,用 户可以直接使用它而不必增加新的协议类型。前面讲到,为了增加新的netlink协议类型,用户仅需增加如下定义到linux/netlink.h就可 以: 只要增加这个定义之后,用户就可以在内核的任何地方引用该协议。 在内核中,为了创建一个netlink socket用户需要调用如下函数: extern struct sock *netlink_kernel_create(struct net *net, int unit,unsigned int groups, void (*input)(struct sk_buff *skb), struct mutex *cb_mutex, struct module *module); | struct net是一个网络名字空间namespace,在不同的名字空间里面可以有自己的转发信息库,有自己的一套net_device等等。默认情况下都是使用init_net这个全局变量 参数unit表示netlink协议类型,如 NETLINK_MYTEST,参数input则为内核模块定义的netlink消息处理函数,当有消息到达这个netlink socket时,该input函数指针就会被引用。函数指针input的参数skb实际上就是函数netlink_kernel_create返回的 struct sock指针,sock实际是socket的一个内核表示数据结构,用户态应用创建的socket在内核中也会有一个struct sock结构来表示。 函数input()会在发送进程执行sendmsg()时 被调用,这样处理消息比较及时,但是,如果消息特别长时,这样处理将增加系统调用sendmsg()的执行时间,也就是说当用户的程序调用sendmsg ()函数时,如果input()函数处理时间过长,也就是说input()函数不执行不完,用户程序调用的sendmsg()函数就不会返回。只有当内核 空间中的input()函数返回时,用户调用的sendmsg()函数才会返回。对于这种情况,可以定义一个内核线程专门负责消息接收,而函数input 的工作只是唤醒该内核线程,这样sendmsg将很快返回。(这里网上的的说明)不过在查看Linux2.6.37版本的内核时并没有发现这种处理过程, 一般都是按下面的方法进行处理。 这里注册的netlink协议为NETLINK_XFRM。 nlsk = netlink_kernel_create(net, NETLINK_XFRM, XFRMNLGRP_MAX, xfrm_netlink_rcv, NULL, THIS_MODULE); static void xfrm_netlink_rcv(struct sk_buff *skb) { mutex_lock(&xfrm_cfg_mutex); netlink_rcv_skb(skb, &xfrm_user_rcv_msg); mutex_unlock(&xfrm_cfg_mutex); } 在netlink_rcv_skb()函数中进行接收处理。 int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid, u32 group, gfp_t allocation) | 前面的三个参数与 netlink_unicast相同,参数group为接收消息的多播组,该参数的每一个位代表一个多播组,因此如果发送给多个多播组,就把该参数设置为 多个多播组组ID的位或。参数allocation为内核内存分配类型,一般地为GFP_ATOMIC或GFP_KERNEL,GFP_ATOMIC用于 原子的上下文(即不可以睡眠),而GFP_KERNEL用于非原子上下文。 NETLINK_CB(skb).pid = 0; NETLINK_CB(skb).dst_pid = 0; NETLINK_CB(skb).dst_group = 1; | 字段pid表示消息发送者进程 ID,也即源地址,对于内核,它为 0, dst_pid 表示消息接收者进程 ID,也即目标地址,如果目标为组或内核,它设置为 0,否则 dst_group 表示目标组地址,如果它目标为某一进程或内核,dst_group 应当设置为 0。 下面是参考网上使用netlink写的和内核通信的两个程序,一个是用户空间,一个是内核空间。 内核空间: #include <linux/init.h> #include <linux/module.h> #include <linux/timer.h> #include <linux/time.h> #include <linux/types.h> #include <net/sock.h> #include <net/netlink.h> #define NETLINK_TEST 25 #define MAX_MSGSIZE 1024 int stringlength(char *s); void sendnlmsg(char * message); int pid; int err; struct sock *nl_sk = NULL; int flag = 0; void sendnlmsg(char *message) { struct sk_buff *skb_1; struct nlmsghdr *nlh; int len = NLMSG_SPACE(MAX_MSGSIZE); int slen = 0; if(!message || !nl_sk) { return ; } skb_1 = alloc_skb(len,GFP_KERNEL); if(!skb_1) { printk(KERN_ERR "my_net_link:alloc_skb_1 error\n"); } slen = stringlength(message); nlh = nlmsg_put(skb_1,0,0,0,MAX_MSGSIZE,0); NETLINK_CB(skb_1).pid = 0; NETLINK_CB(skb_1).dst_group = 0; message[slen]= '\0'; memcpy(NLMSG_DATA(nlh),message,slen+1); printk("my_net_link:send message '%s'.\n",(char *)NLMSG_DATA(nlh)); netlink_unicast(nl_sk,skb_1,pid,MSG_DONTWAIT); } int stringlength(char *s) { int slen = 0; for(; *s; s++){ slen++; } return slen; } void nl_data_ready(struct sk_buff *__skb) { struct sk_buff *skb; struct nlmsghdr *nlh; char str[100]; struct completion cmpl; int i=10; skb = skb_get (__skb); if(skb->len >= NLMSG_SPACE(0)) { nlh = nlmsg_hdr(skb); memcpy(str, NLMSG_DATA(nlh), sizeof(str)); printk("Message received:%s\n",str) ; pid = nlh->nlmsg_pid; while(i--) { init_completion(&cmpl); wait_for_completion_timeout(&cmpl,3 * HZ); sendnlmsg("I am from kernel!"); } flag = 1; kfree_skb(skb); } } // Initialize netlink int netlink_init(void) { nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, 1, nl_data_ready, NULL, THIS_MODULE); if(!nl_sk){ printk(KERN_ERR "my_net_link: create netlink socket error.\n"); return 1; } printk("my_net_link_3: create netlink socket ok.\n"); return 0; } static void netlink_exit(void) { if(nl_sk != NULL){ sock_release(nl_sk->sk_socket); } printk("my_net_link: self module exited\n"); } module_init(netlink_init); module_exit(netlink_exit); MODULE_AUTHOR("frankzfz"); MODULE_LICENSE("GPL"); | 下面是用户空间的程序: #include <sys/stat.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <sys/types.h> #include <string.h> #include <asm/types.h> #include <linux/netlink.h> #include <linux/socket.h> #include <errno.h> #define NETLINK_TEST 25 #define MAX_PAYLOAD 1024 // maximum payload size int main(int argc, char* argv[]) { int state; struct sockaddr_nl src_addr, dest_addr; struct nlmsghdr *nlh = NULL; struct iovec iov; struct msghdr msg; int sock_fd, retval; int state_smg = 0; // Create a socket sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST); if(sock_fd == -1){ printf("error getting socket: %s", strerror(errno)); return -1; } // To prepare binding memset(&msg,0,sizeof(msg)); memset(&src_addr, 0, sizeof(src_addr)); src_addr.nl_family = AF_NETLINK; src_addr.nl_pid = getpid(); // self pid src_addr.nl_groups = 0; // multi cast retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr)); if(retval < 0){ printf("bind failed: %s", strerror(errno)); close(sock_fd); return -1; } // To prepare recvmsg nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)); if(!nlh){ printf("malloc nlmsghdr error!\n"); close(sock_fd); return -1; } memset(&dest_addr,0,sizeof(dest_addr)); dest_addr.nl_family = AF_NETLINK; dest_addr.nl_pid = 0; dest_addr.nl_groups = 0; nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD); nlh->nlmsg_pid = getpid(); nlh->nlmsg_flags = 0; strcpy(NLMSG_DATA(nlh),"Hello you!"); iov.iov_base = (void *)nlh; iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD); // iov.iov_len = nlh->nlmsg_len; memset(&msg, 0, sizeof(msg)); msg.msg_name = (void *)&dest_addr; msg.msg_namelen = sizeof(dest_addr); msg.msg_iov = &iov; msg.msg_iovlen = 1; printf("state_smg\n"); state_smg = sendmsg(sock_fd,&msg,0); if(state_smg == -1) { printf("get error sendmsg = %s\n",strerror(errno)); } memset(nlh,0,NLMSG_SPACE(MAX_PAYLOAD)); printf("waiting received!\n"); // Read message from kernel while(1){ printf("In while recvmsg\n"); state = recvmsg(sock_fd, &msg, 0); if(state<0) { printf("state<1"); } printf("In while\n"); printf("Received message: %s\n",(char *) NLMSG_DATA(nlh)); } close(sock_fd); return 0; } | 下面是Makefile文件: obj-m := netlink_k.o KERNELBUILD := /lib/modules/`uname -r`/build default: @echo "BUILE Kmod" @make -C $(KERNELBUILD) M=$(shell pwd) modules gcc -o netlink_2 netlink_2.c clean: @echo " CLEAN kmod" @rm -rf *.o @rm -rf .depend .*.cmd *.ko *.mod.c .tmp_versions *.symvers .*.d | 其中,netlink_k.c为内核的空间的程序。 先运行内核代码netlink_k.ko,也就是在执行完makefile文件后,会生成一个netlink_k.ko文件,可以使用下面的命令进行安 装,insmod netlink_k.ko,使用lsmod查看,当安装成功后,然后,执行./netlink用户空间程序,可以在另一个终端下执行dmesg命令,查看 内核通信的情况。这里netlink程序向内核空间发送一个hello you!内核返回给一个I am from kernel!在这里使用了一个定时器,也就是每3秒中发送一次I am from kernel!只有内核把10个字符串全部发送完毕后,用户空间的sendmsg()才会返回,也就是在用户空间的netlink才会输出内核空间发送过 来的数据,这里只有一个简单的程序,并没有什么实际的意义,因为,正如前面所说的一般情况下不会在回调函数中处理太多的东西,以免sendmsg()函数 返回不及时。下面是使用dmesg命令输出的信息。 [873791.498039] my_net_link_3: create netlink socket ok. [873810.263676] Message received:Hello [873813.260848] my_net_link_4:send message 'I am from kernel!'. [873816.260821] my_net_link_4:send message 'I am from kernel!'. [873819.260860] my_net_link_4:send message 'I am from kernel!'. [873822.260762] my_net_link_4:send message 'I am from kernel!'. [873825.260883] my_net_link_4:send message 'I am from kernel!'. [873828.260669] my_net_link_4:send message 'I am from kernel!'. [873831.260714] my_net_link_4:send message 'I am from kernel!'. [873834.260683] my_net_link_4:send message 'I am from kernel!'. [873837.260666] my_net_link_4:send message 'I am from kernel!'. [873840.260632] my_net_link_4:send message 'I am from kernel!'. | 参考网址: http://blog.csdn.net/wangjingfei/archive/2010/02/05/5288460.aspx http://blog.csdn.net/liumang_D/archive/2010/03/25/5413042.aspx Linux 系统内核空间与用户空间通信的实现与分析 |
引言 Alan Cox在内核1.3版本的开发阶段最先引入了Netlink,刚开始时Netlink是以字符驱动接口的方式提供内核与用户空间的双向数据通信;随后,在2.1内核开发过程中,Alexey Kuznetsov将Netlink改写成一个更加灵活、且易于扩展的基于消息通信接口,并将其应用到高级路由子系统的基础框架里。自那时起,Netlink就成了Linux内核子系统和用户态的应用程序通信的主要手段之一。 2001年,ForCES IETF委员会正式对Netlink进行了标准化的工作。Jamal Hadi Salim提议将Netlink定义成一种用于网络设备的路由引擎组件和其控制管理组件之间通信的协议。不过他的建议最终没有被采纳,取而代之的是我们今天所看到的格局:Netlink被设计成一个新的协议域,domain。 Linux之父托瓦斯曾说过“Linux is evolution, not intelligent design”。什么意思?就是说,Netlink也同样遵循了Linux的某些设计理念,即没有完整的规范文档,亦没有设计文档。只有什么?你懂得---“Read the f**king source code”。 当然,本文不是分析Netlink在Linux上的实现机制,而是就“什么是Netlink”以及“如何用好Netlink”的话题和大家做个分享,只有在遇到问题时才需要去阅读内核源码弄清个所以然。 什么是Netlink 关于Netlink的理解,需要把握几个关键点: 1、面向数据报的无连接消息子系统 2、基于通用的BSD Socket架构而实现 关于第一点使我们很容易联想到UDP协议,能想到这一点就非常棒了。按着UDP协议来理解Netlink不是不无道理,只要你能触类旁通,做到“活学”,善于总结归纳、联想,最后实现知识迁移这就是学习的本质。Netlink可以实现内核->用户以及用户->内核的双向、异步的数据通信,同时它还支持两个用户进程之间、甚至两个内核子系统之间的数据通信。本文中,对后两者我们不予考虑,焦点集中在如何实现用户<->内核之间的数据通信。 看到第二点脑海中是不是瞬间闪现了下面这张图片呢?如果是,则说明你确实有慧根;当然,不是也没关系,慧根可以慢慢长嘛,呵呵。 在后面实战Netlink套接字编程时我们主要会用到socket(),bind(),sendmsg() 和 recvmsg()等系统调用,当然还有socket提供的轮训(polling)机制。 Netlink通信类型 Netlink支持两种类型的通信方式:单播和多播。 单播:经常用于一个用户进程和一个内核子系统之间1:1的数据通信。用户空间发送命令到内核,然后从内核接受命令的返回结果。 多播:经常用于一个内核进程和多个用户进程之间的1:N的数据通信。内核作为会话的发起者,用户空间的应用程序是接收者。为了实现这个功能,内核空间的程序会创建一个多播组,然后所有用户空间的对该内核进程发送的消息感兴趣的进程都加入到该组即可接收来自内核发送的消息了。如下: 其中进程A和子系统1之间是单播通信,进程B、C和子系统2是多播通信。上图还向我们说明了一个信息。从用户空间传递到内核的数据是不需要排队的,即其操作是同步完成;而从内核空间向用户空间传递数据时需要排队,是异步的。了解了这一点在开发基于Netlink的应用模块时可以使我们少走很多弯路。假如,你向内核发送了一个消息需要获取内核中某些信息,比如路由表,或其他信息,如果路由表过于庞大,那么内核在通过Netlink向你返回数据时,你可以好生琢磨一下如何接收这些数据的问题,毕竟你已经看到了那个输出队列了,不能视而不见啊。 Netlink的消息格式 Netlink消息由两部分组成:消息头和有效数据载荷,且整个Netlink消息是4字节对齐,一般按主机字节序进行传递。消息头为固定的16字节,消息体长度可变: Netlink的消息头 消息头定义在<include/linux/netlink.h>文件里,由结构体nlmsghdr表示: - struct nlmsghdr
- {
- __u32 nlmsg_len; /* Length of message including header */
- __u16 nlmsg_type; /* Message content */
- __u16 nlmsg_flags; /* Additional flags */
- __u32 nlmsg_seq; /* Sequence number */
- __u32 nlmsg_pid; /* Sending process PID */
- };
消息头中各成员属性的解释及说明: nlmsg_len:整个消息的长度,按字节计算。包括了Netlink消息头本身。 nlmsg_type:消息的类型,即是数据还是控制消息。目前(内核版本2.6.21)Netlink仅支持四种类型的控制消息,如下: NLMSG_NOOP-空消息,什么也不做; NLMSG_ERROR-指明该消息中包含一个错误; NLMSG_DONE-如果内核通过Netlink队列返回了多个消息,那么队列的最后一条消息的类型为NLMSG_DONE,其余所有消息的nlmsg_flags属性都被设置NLM_F_MULTI位有效。 NLMSG_OVERRUN-暂时没用到。 nlmsg_flags:附加在消息上的额外说明信息,如上面提到的NLM_F_MULTI。摘录如下: 标记 | 作用及说明 | NLM_F_REQUEST | 如果消息中有该标记位,说明这是一个请求消息。所有从用户空间到内核空间的消息都要设置该位,否则内核将向用户返回一个EINVAL无效参数的错误 | NLM_F_MULTI | 消息从用户->内核是同步的立刻完成,而从内核->用户则需要排队。如果内核之前收到过来自用户的消息中有NLM_F_DUMP位为1的消息,那么内核就会向用户空间发送一个由多个Netlink消息组成的链表。除了最后个消息外,其余每条消息中都设置了该位有效。 | NLM_F_ACK | 该消息是内核对来自用户空间的NLM_F_REQUEST消息的响应 | NLM_F_ECHO | 如果从用户空间发给内核的消息中该标记为1,则说明用户的应用进程要求内核将用户发给它的每条消息通过单播的形式再发送给用户进程。和我们通常说的“回显”功能类似。 | … | … | 大家只要知道nlmsg_flags有多种取值就可以,至于每种值的作用和意义,通过谷歌和源代码一定可以找到答案,这里就不展开了。上一张2.6.21内核中所有的取值情况: nlmsg_seq:消息序列号。因为Netlink是面向数据报的,所以存在丢失数据的风险,但是Netlink提供了如何确保消息不丢失的机制,让程序开发人员根据其实际需求而实现。消息序列号一般和NLM_F_ACK类型的消息联合使用,如果用户的应用程序需要保证其发送的每条消息都成功被内核收到的话,那么它发送消息时需要用户程序自己设置序号,内核收到该消息后对提取其中的序列号,然后在发送给用户程序回应消息里设置同样的序列号。有点类似于TCP的响应和确认机制。 注意:当内核主动向用户空间发送广播消息时,消息中的该字段总是为0。 nlmsg_pid :当用户空间的进程和内核空间的某个子系统之间通过Netlink建立了数据交换的通道后,Netlink会为每个这样的通道分配一个唯一的数字标识。其主要作用就是将来自用户空间的请求消息和响应消息进行关联。说得直白一点,假如用户空间存在多个用户进程,内核空间同样存在多个进程,Netlink必须提供一种机制用于确保每一对“用户-内核”空间通信的进程之间的数据交互不会发生紊乱。 即,进程A、B通过Netlink向子系统1获取信息时,子系统1必须确保回送给进程A的响应数据不会发到进程B那里。主要适用于用户空间的进程从内核空间获取数据的场景。通常情况下,用户空间的进程在向内核发送消息时一般通过系统调用getpid()将当前进程的进程号赋给该变量,即用户空间的进程希望得到内核的响应时才会这么做。从内核主动发送到用户空间的消息该字段都被设置为0。 Netlink的消息体 Netlink的消息体采用TLV(Type-Length-Value)格式: Netlink每个属性都由<include/linux/netlink.h>文件里的struct nlattr{}来表示: Netlink提供的错误指示消息 当用户空间的应用程序和内核空间的进程之间通过Netlink通信时发生了错误,Netlink必须向用户空间通报这种错误。Netlink对错误消息进行了单独封装,<include/linux/netlink.h>: - struct nlmsgerr
- {
- int error; //标准的错误码,定义在errno.h头文件中。可以用perror()来解释
- struct nlmsghdr msg; //指明了哪条消息触发了结构体中error这个错误值
- };
Netlink编程需要注意的问题 基于Netlink的用户-内核通信,有两种情况可能会导致丢包: 1、内存耗尽; 2、用户空间接收进程的缓冲区溢出。导致缓冲区溢出的主要原因有可能是:用户空间的进程运行太慢;或者接收队列太短。 如果Netlink不能将消息正确传递到用户空间的接收进程,那么用户空间的接收进程在调用recvmsg()系统调用时就会返回一个内存不足(ENOBUFS)的错误,这一点需要注意。换句话说,缓冲区溢出的情况是不会发送在从用户->内核的sendmsg()系统调用里,原因前面我们也说过了,请大家自己思考一下。 当然,如果使用的是阻塞型socket通信,也就不存在内存耗尽的隐患了,这又是为什么呢?赶紧去谷歌一下,查查什么是阻塞型socket吧。学而不思则罔,思而不学则殆嘛。 Netlink的地址结构体 在TCP博文中我们提到过在Internet编程过程中所用到的地址结构体和标准地址结构体,它们和Netlink地址结构体的关系如下: struct sockaddr_nl{}的详细定义和描述如下: - struct sockaddr_nl
- {
- sa_family_t nl_family; /*该字段总是为AF_NETLINK */
- unsigned short nl_pad; /* 目前未用到,填充为0*/
- __u32 nl_pid; /* process pid */
- __u32 nl_groups; /* multicast groups mask */
- };
nl_pid:该属性为发送或接收消息的进程ID,前面我们也说过,Netlink不仅可以实现用户-内核空间的通信还可使现实用户空间两个进程之间,或内核空间两个进程之间的通信。该属性为0时一般适用于如下两种情况: 第一,我们要发送的目的地是内核,即从用户空间发往内核空间时,我们构造的Netlink地址结构体中nl_pid通常情况下都置为0。这里有一点需要跟大家交代一下,在Netlink规范里,PID全称是Port-ID(32bits),其主要作用是用于唯一的标识一个基于netlink的socket通道。通常情况下nl_pid都设置为当前进程的进程号。然而,对于一个进程的多个线程同时使用netlink socket的情况,nl_pid的设置一般采用如下这个样子来实现: - pthread_self() << 16 | getpid();
第二,从内核发出的多播报文到用户空间时,如果用户空间的进程处在该多播组中,那么其地址结构体中nl_pid也设置为0,同时还要结合下面介绍到的另一个属性。 nl_groups:如果用户空间的进程希望加入某个多播组,则必须执行bind()系统调用。该字段指明了调用者希望加入的多播组号的掩码(注意不是组号,后面我们会详细讲解这个字段)。如果该字段为0则表示调用者不希望加入任何多播组。对于每个隶属于Netlink协议域的协议,最多可支持32个多播组(因为nl_groups的长度为32比特),每个多播组用一个比特来表示。 今天我们来动手演练一下Netlink的用法,看看它到底是如何实现用户-内核空间的数据通信的。我们依旧是在2.6.21的内核环境下进行开发。 在</usr/include/linux/netlink.h>文件里包含了Netlink协议簇已经定义好的一些预定义协议: - #define NETLINK_ROUTE 0 /* Routing/device hook */
- #define NETLINK_UNUSED 1 /* Unused number */
- #define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */
- #define NETLINK_FIREWALL 3 /* Firewalling hook */
- #define NETLINK_INET_DIAG 4 /* INET socket monitoring */
- #define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */
- #define NETLINK_XFRM 6 /* ipsec */
- #define NETLINK_SELINUX 7 /* SELinux event notifications */
- #define NETLINK_ISCSI 8 /* Open-iSCSI */
- #define NETLINK_AUDIT 9 /* auditing */
- #define NETLINK_FIB_LOOKUP 10
- #define NETLINK_CONNECTOR 11
- #define NETLINK_NETFILTER 12 /* netfilter subsystem */
- #define NETLINK_IP6_FW 13
- #define NETLINK_DNRTMSG 14 /* DECnet routing messages */
- #define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */
- #define NETLINK_GENERIC 16
- /* leave room for NETLINK_DM (DM Events) */
- #define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */
- #define NETLINK_ECRYPTFS 19
- #define NETLINK_TEST 20 /* 用户添加的自定义协议 */
如果我们在Netlink协议簇里开发一个新的协议,只要在该文件中定义协议号即可,例如我们定义一种基于Netlink协议簇的、协议号是20的自定义协议,如上所示。同时记得,将内核头文件目录中的netlink.h也做对应的修改,在我的系统中它的路径是:/usr/src/linux-2.6.21/include/linux/netlink.h 接下来我们在用户空间以及内核空间模块的开发过程中就可以使用这种协议了,一共分为三个阶段。 Stage 1: 我们首先实现的功能是用户->内核的单向数据通信,即用户空间发送一个消息给内核,然后内核将其打印输出,就这么简单。用户空间的示例代码如下【mynlusr.c】 - #include <sys/stat.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <sys/socket.h>
- #include <sys/types.h>
- #include <string.h>
- #include <asm/types.h>
- #include <linux/netlink.h>
- #include <linux/socket.h>
- #define MAX_PAYLOAD 1024 /*消息最大负载为1024字节*/
- int main(int argc, char* argv[])
- {
- struct sockaddr_nl dest_addr;
- struct nlmsghdr *nlh = NULL;
- struct iovec iov;
- int sock_fd=-1;
- struct msghdr msg;
-
- if(-1 == (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){ //创建套接字
- perror("can't create netlink socket!");
- return 1;
- }
- memset(&dest_addr, 0, sizeof(dest_addr));
- dest_addr.nl_family = AF_NETLINK;
- dest_addr.nl_pid = 0; /*我们的消息是发给内核的*/
- dest_addr.nl_groups = 0; /*在本示例中不存在使用该值的情况*/
-
- //将套接字和Netlink地址结构体进行绑定
- if(-1 == bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
- perror("can't bind sockfd with sockaddr_nl!");
- return 1;
- }
- if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
- perror("alloc mem failed!");
- return 1;
- }
- memset(nlh,0,MAX_PAYLOAD);
- /* 填充Netlink消息头部 */
- nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
- nlh->nlmsg_pid = 0;
- nlh->nlmsg_type = NLMSG_NOOP; //指明我们的Netlink是消息负载是一条空消息
- nlh->nlmsg_flags = 0;
- /*设置Netlink的消息内容,来自我们命令行输入的第一个参数*/
- strcpy(NLMSG_DATA(nlh), argv[1]);
- /*这个是模板,暂时不用纠结为什么要这样用。有时间详细讲解socket时再说*/
- memset(&iov, 0, sizeof(iov));
- iov.iov_base = (void *)nlh;
- iov.iov_len = nlh->nlmsg_len;
- memset(&msg, 0, sizeof(msg));
- msg.msg_iov = &iov;
- msg.msg_iovlen = 1;
- sendmsg(sock_fd, &msg, 0); //通过Netlink socket向内核发送消息
- /* 关闭netlink套接字 */
- close(sock_fd);
- free(nlh);
- return 0;
- }
上面的代码逻辑已经非常清晰了,都是socket编程的API,唯一不同的是我们这次编程是针对Netlink协议簇的。这里我们提前引入了BSD层的消息结构体struct msghdr{},定义在<include/linux/socket.h>文件里,以及其数据块struct iovec{}定义在<include/linux/uio.h>头文件里。这里就不展开了,大家先记住这个用法就行。以后有时间再深入到socket的骨子里去转悠一番。 另外,需要格外注意的就是Netlink的地址结构体和其消息头结构中pid字段为0的情况,很容易让人产生混淆,再总结一下: | 0 | netlink地址结构体.nl_pid | 1、内核发出的多播报文 2、消息的接收方是内核,即从用户空间发往内核的消息 | netlink消息头体. nlmsg_pid | 来自内核主动发出的消息 | 这个例子仅是从用户空间到内核空间的单向数据通信,所以Netlink地址结构体中我们设置了dest_addr.nl_pid = 0,说明我们的报文的目的地是内核空间;在填充Netlink消息头部时,我们做了nlh->nlmsg_pid = 0这样的设置。 需要注意几个宏的使用: NLMSG_SPACE(MAX_PAYLOAD),该宏用于返回不小于MAX_PAYLOAD且4字节对齐的最小长度值,一般用于向内存系统申请空间是指定所申请的内存字节数,和NLMSG_LENGTH(len)所不同的是,前者所申请的空间里不包含Netlink消息头部所占的字节数,后者是消息负载和消息头加起来的总长度。 NLMSG_DATA(nlh),该宏用于返回Netlink消息中数据部分的首地址,在写入和读取消息数据部分时会用到它。 它们之间的关系如下: 内核空间的示例代码如下【mynlkern.c】: - #include <linux/kernel.h>
- #include <linux/module.h>
- #include <linux/skbuff.h>
- #include <linux/init.h>
- #include <linux/ip.h>
- #include <linux/types.h>
- #include <linux/sched.h>
- #include <net/sock.h>
- #include <linux/netlink.h>
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("Koorey King");
- struct sock *nl_sk = NULL;
- static void nl_data_ready (struct sock *sk, int len)
- {
- struct sk_buff *skb;
- struct nlmsghdr *nlh = NULL;
- while((skb = skb_dequeue(&sk->sk_receive_queue)) != NULL)
- {
- nlh = (struct nlmsghdr *)skb->data;
- printk("%s: received netlink message payload: %s \n", __FUNCTION__,(char*)NLMSG_DATA(nlh));
- kfree_skb(skb);
- }
- printk("recvied finished!\n");
- }
- static int __init myinit_module()
- {
- printk("my netlink in\n");
- nl_sk =netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
- return 0;
- }
- static void __exit mycleanup_module()
- {
- printk("my netlink out!\n");
- sock_release(nl_sk->sk_socket);
- }
- module_init(myinit_module);
- module_exit(mycleanup_module);
在内核模块的初始化函数里我们用 nl_sk = netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE); 创建了一个内核态的socket,第一个参数我们扩展的协议号;第二个参数为多播组号,目前我们用不上,将其置为0;第三个参数是个回调函数,即当内核的Netlink socket套接字收到数据时的处理函数;第四个参数就不多说了。 在回调函数nl_data_ready()中,我们不断的从socket的接收队列去取数据,一旦拿到数据就将其打印输出。在协议栈的INET层,用于存储数据的是大名鼎鼎的sk_buff结构,所以我们通过nlh = (struct nlmsghdr *)skb->data;可以拿到netlink的消息体,然后通过NLMSG_DATA(nlh)定位到netlink的消息负载。 将上述代码编译后测试结果如下: Stage 2: 我们将上面的代码稍加改造就可以实现用户<->内核的双向数据通信。 首先是改造用户空间的代码: - #include <sys/stat.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <sys/socket.h>
- #include <sys/types.h>
- #include <string.h>
- #include <asm/types.h>
- #include <linux/netlink.h>
- #include <linux/socket.h>
- #define MAX_PAYLOAD 1024 /*消息最大负载为1024字节*/
- int main(int argc, char* argv[])
- {
- struct sockaddr_nl dest_addr;
- struct nlmsghdr *nlh = NULL;
- struct iovec iov;
- int sock_fd=-1;
- struct msghdr msg;
- if(-1 == (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
- perror("can't create netlink socket!");
- return 1;
- }
- memset(&dest_addr, 0, sizeof(dest_addr));
- dest_addr.nl_family = AF_NETLINK;
- dest_addr.nl_pid = 0; /*我们的消息是发给内核的*/
- dest_addr.nl_groups = 0; /*在本示例中不存在使用该值的情况*/
- if(-1 == bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
- perror("can't bind sockfd with sockaddr_nl!");
- return 1;
- }
- if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
- perror("alloc mem failed!");
- return 1;
- }
-
- memset(nlh,0,MAX_PAYLOAD);
- /* 填充Netlink消息头部 */
- nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
- nlh->nlmsg_pid = getpid();//我们希望得到内核回应,所以得告诉内核我们ID号
- nlh->nlmsg_type = NLMSG_NOOP; //指明我们的Netlink是消息负载是一条空消息
- nlh->nlmsg_flags = 0;
- /*设置Netlink的消息内容,来自我们命令行输入的第一个参数*/
- strcpy(NLMSG_DATA(nlh), argv[1]);
- /*这个是模板,暂时不用纠结为什么要这样用。*/
- memset(&iov, 0, sizeof(iov));
- iov.iov_base = (void *)nlh;
- iov.iov_len = nlh->nlmsg_len;
- memset(&msg, 0, sizeof(msg));
- msg.msg_iov = &iov;
- msg.msg_iovlen = 1;
- sendmsg(sock_fd, &msg, 0); //通过Netlink socket向内核发送消息
- //接收内核消息的消息
- printf("waiting message from kernel!\n");
- memset((char*)NLMSG_DATA(nlh),0,1024);
- recvmsg(sock_fd,&msg,0);
- printf("Got response: %s\n",NLMSG_DATA(nlh));
- /* 关闭netlink套接字 */
- close(sock_fd);
- free(nlh);
- return 0;
- }
内核空间的修改如下: - #include <linux/kernel.h>
- #include <linux/module.h>
- #include <linux/skbuff.h>
- #include <linux/init.h>
- #include <linux/ip.h>
- #include <linux/types.h>
- #include <linux/sched.h>
- #include <net/sock.h>
- #include <net/netlink.h> /*该文头文件里包含了linux/netlink.h,因为我们要用到net/netlink.h中的某些API函数,nlmsg_pug()*/
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("Koorey King");
- struct sock *nl_sk = NULL;
- //向用户空间发送消息的接口
- void sendnlmsg(char *message,int dstPID)
- {
- struct sk_buff *skb;
- struct nlmsghdr *nlh;
- int len = NLMSG_SPACE(MAX_MSGSIZE);
- int slen = 0;
- if(!message || !nl_sk){
- return;
- }
- // 为新的 sk_buffer申请空间
- skb = alloc_skb(len, GFP_KERNEL);
- if(!skb){
- printk(KERN_ERR "my_net_link: alloc_skb Error./n");
- return;
- }
- slen = strlen(message)+1;
- //用nlmsg_put()来设置netlink消息头部
- nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);
- // 设置Netlink的控制块
- NETLINK_CB(skb).pid = 0; // 消息发送者的id标识,如果是内核发的则置0
- NETLINK_CB(skb).dst_group = 0; //如果目的组为内核或某一进程,该字段也置0
- message[slen] = '\0';
- memcpy(NLMSG_DATA(nlh), message, slen+1);
- //通过netlink_unicast()将消息发送用户空间由dstPID所指定了进程号的进程
- netlink_unicast(nl_sk,skb,dstPID,0);
- printk("send OK!\n");
- return;
- }
- static void nl_data_ready (struct sock *sk, int len)
- {
- struct sk_buff *skb;
- struct nlmsghdr *nlh = NULL;
- while((skb = skb_dequeue(&sk->sk_receive_queue)) != NULL)
- {
- nlh = (struct nlmsghdr *)skb->data;
- printk("%s: received netlink message payload: %s \n", __FUNCTION__,(char*)NLMSG_DATA(nlh));
- kfree_skb(skb);
- sendnlmsg("I see you",nlh->nlmsg_pid); //发送者的进程ID我们已经将其存储在了netlink消息头部里的nlmsg_pid字段里,所以这里可以拿来用。
- }
- printk("recvied finished!\n");
- }
- static int __init myinit_module()
- {
- printk("my netlink in\n");
- nl_sk = netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
- return 0;
- }
- static void __exit mycleanup_module()
- {
- printk("my netlink out!\n");
- sock_release(nl_sk->sk_socket);
- }
- module_init(myinit_module);
- module_exit(mycleanup_module);
重新编译后,测试结果如下: Stage 3: 前面我们提到过,如果用户进程希望加入某个多播组时才需要调用bind()函数。前面的示例中我们没有这个需求,可还是调了bind(),心头有些不爽。在前几篇博文里有关于socket编程时几个常见API的详细解释和说明,不明白的童鞋可以回头去复习一下。 因为Netlink是面向无连接的数据报的套接字,所以我们还可以用sendto()和recvfrom()来实现数据的收发,这次我们不再调用bind()。将Stage 2的例子稍加改造一下,用户空间的修改如下: - #include <sys/stat.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <sys/socket.h>
- #include <sys/types.h>
- #include <string.h>
- #include <asm/types.h>
- #include <linux/netlink.h>
- #include <linux/socket.h>
- #define MAX_PAYLOAD 1024 /*消息最大负载为1024字节*/
- int main(int argc, char* argv[])
- {
- struct sockaddr_nl dest_addr;
- struct nlmsghdr *nlh = NULL;
- //struct iovec iov;
- int sock_fd=-1;
- //struct msghdr msg;
- if(-1 == (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
- perror("can't create netlink socket!");
- return 1;
- }
- memset(&dest_addr, 0, sizeof(dest_addr));
- dest_addr.nl_family = AF_NETLINK;
- dest_addr.nl_pid = 0; /*我们的消息是发给内核的*/
- dest_addr.nl_groups = 0; /*在本示例中不存在使用该值的情况*/
- /*不再调用bind()函数了
- if(-1 == bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
- perror("can't bind sockfd with sockaddr_nl!");
- return 1;
- }*/
- if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
- perror("alloc mem failed!");
- return 1;
- }
- memset(nlh,0,MAX_PAYLOAD);
- /* 填充Netlink消息头部 */
- nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
- nlh->nlmsg_pid = getpid();//我们希望得到内核回应,所以得告诉内核我们ID号
- nlh->nlmsg_type = NLMSG_NOOP; //指明我们的Netlink是消息负载是一条空消息
- nlh->nlmsg_flags = 0;
- /*设置Netlink的消息内容,来自我们命令行输入的第一个参数*/
- strcpy(NLMSG_DATA(nlh), argv[1]);
- /*这个模板就用不上了。*/
- /*
- memset(&iov, 0, sizeof(iov));
- iov.iov_base = (void *)nlh;
- iov.iov_len = nlh->nlmsg_len;
- memset(&msg, 0, sizeof(msg));
- msg.msg_iov = &iov;
- msg.msg_iovlen = 1;
- */
- //sendmsg(sock_fd, &msg, 0); //不再用这种方式发消息到内核
- sendto(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),sizeof(dest_addr));
-
- //接收内核消息的消息
- printf("waiting message from kernel!\n");
- //memset((char*)NLMSG_DATA(nlh),0,1024);
- memset(nlh,0,MAX_PAYLOAD); //清空整个Netlink消息头包括消息头和负载
- //recvmsg(sock_fd,&msg,0);
- recvfrom(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),NULL);
- printf("Got response: %s\n",NLMSG_DATA(nlh));
- /* 关闭netlink套接字 */
- close(sock_fd);
- free(nlh);
- return 0;
- }
内核空间的代码完全不用修改,我们仍然用netlink_unicast()从内核空间发送消息到用户空间。 重新编译后,测试结果如下: 和Stage 2中代码运行效果完全一样。也就是说,在开发Netlink程序过程中,如果没牵扯到多播机制,那么用户空间的socket代码其实是不用执行bind()系统调用的,但此时就需要用sendto()和recvfrom()完成数据的发送和接收的任务;如果执行了bind()系统调用,当然也可以继续用sendto()和recvfrom(),但给它们传递的参数就有所区别。这时候一般使用sendmsg()和recvmsg()来完成数据的发送和接收。大家根据自己的实际情况灵活选择。 关于Netlink多播机制的用法 在上一篇博文中我们所遇到的情况都是用户空间作为消息进程的发起者,Netlink还支持内核作为消息的发送方的情况。这一般用于内核主动向用户空间报告一些内核状态,例如我们在用户空间看到的USB的热插拔事件的通告就是这样的应用。 先说一下我们的目标,内核线程每个一秒钟往一个多播组里发送一条消息,然后用户空间所以加入了该组的进程都会收到这样的消息,并将消息内容打印出来。 Netlink地址结构体中的nl_groups是32位,也就是说每种Netlink协议最多支持32个多播组。如何理解这里所说的每种Netlink协议?在</usr/include/linux/netlink.h>里预定义的如下协议都是Netlink协议簇的具体协议,还有我们添加的NETLINK_TEST也是一种Netlink协议。 - #define NETLINK_ROUTE 0 /* Routing/device hook */
- #define NETLINK_UNUSED 1 /* Unused number */
- #define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */
- #define NETLINK_FIREWALL 3 /* Firewalling hook */
- #define NETLINK_INET_DIAG 4 /* INET socket monitoring */
- #define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */
- #define NETLINK_XFRM 6 /* ipsec */
- #define NETLINK_SELINUX 7 /* SELinux event notifications */
- #define NETLINK_ISCSI 8 /* Open-iSCSI */
- #define NETLINK_AUDIT 9 /* auditing */
- #define NETLINK_FIB_LOOKUP 10
- #define NETLINK_CONNECTOR 11
- #define NETLINK_NETFILTER 12 /* netfilter subsystem */
- #define NETLINK_IP6_FW 13
- #define NETLINK_DNRTMSG 14 /* DECnet routing messages */
- #define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */
- #define NETLINK_GENERIC 16
- /* leave room for NETLINK_DM (DM Events) */
- #define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */
- #define NETLINK_ECRYPTFS 19
- #define NETLINK_TEST 20 /* 用户添加的自定义协议 */
在我们自己添加的NETLINK_TEST协议里,同样地,最多允许我们设置32个多播组,每个多播组用1个比特表示,所以不同的多播组不可能出现重复。你可以根据自己的实际需求,决定哪个多播组是用来做什么的。用户空间的进程如果对某个多播组感兴趣,那么它就加入到该组中,当内核空间的进程往该组发送多播消息时,所有已经加入到该多播组的用户进程都会收到该消息。 再回到我们Netlink地址结构体里的nl_groups成员,它是多播组的地址掩码,注意是掩码不是多播组的组号。如何根据多播组号取得多播组号的掩码呢?在af_netlink.c中有个函数: - static u32 netlink_group_mask(u32 group)
- {
- return group ? 1 << (group - 1) : 0;
- }
也就是说,在用户空间的代码里,如果我们要加入到多播组1,需要设置nl_groups设置为1;多播组2的掩码为2;多播组3的掩码为4,依次类推。为0表示我们不希望加入任何多播组。理解这一点很重要。所以我们可以在用户空间也定义一个类似于netlink_group_mask()的功能函数,完成从多播组号到多播组掩码的转换。最终用户空间的代码如下: - #include <sys/stat.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <sys/socket.h>
- #include <sys/types.h>
- #include <string.h>
- #include <asm/types.h>
- #include <linux/netlink.h>
- #include <linux/socket.h>
- #include <errno.h>
- #define MAX_PAYLOAD 1024 // Netlink消息的最大载荷的长度
- unsigned int netlink_group_mask(unsigned int group)
- {
- return group ? 1 << (group - 1) : 0;
- }
- int main(int argc, char* argv[])
- {
- struct sockaddr_nl src_addr;
- struct nlmsghdr *nlh = NULL;
- struct iovec iov;
- struct msghdr msg;
- int sock_fd, retval;
- // 创建Socket
- sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
- if(sock_fd == -1){
- printf("error getting socket: %s", strerror(errno));
- return -1;
- }
- memset(&src_addr, 0, sizeof(src_addr));
- src_addr.nl_family = PF_NETLINK;
- src_addr.nl_pid = 0; // 表示我们要从内核接收多播消息。注意:该字段为0有双重意义,另一个意义是表示我们发送的数据的目的地址是内核。
- src_addr.nl_groups = netlink_group_mask(atoi(argv[1])); // 多播组的掩码,组号来自我们执行程序时输入的第一个参数
- // 因为我们要加入到一个多播组,所以必须调用bind()。
- retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
- if(retval < 0){
- printf("bind failed: %s", strerror(errno));
- close(sock_fd);
- return -1;
- }
- // 为接收Netlink消息申请存储空间
- nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
- if(!nlh){
- printf("malloc nlmsghdr error!\n");
- close(sock_fd);
- return -1;
- }
- memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
- iov.iov_base = (void *)nlh;
- iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
- memset(&msg, 0, sizeof(msg));
- msg.msg_iov = &iov;
- msg.msg_iovlen = 1;
- // 从内核接收消息
- printf("waitinf for...\n");
- recvmsg(sock_fd, &msg, 0);
- printf("Received message: %s \n", NLMSG_DATA(nlh));
-
- close(sock_fd);
- return 0;
- }
可以看到,用户空间的程序基本没什么变化,唯一需要格外注意的就是Netlink地址结构体中的nl_groups的设置。由于对它的解释很少,加之没有有效的文档,所以我也是一边看源码,一边在网上搜集资料。有分析不当之处,还请大家帮我指出。 内核空间我们添加了内核线程和内核线程同步方法completion的使用。内核空间修改后的代码如下: - #include <linux/kernel.h>
- #include <linux/module.h>
- #include <linux/skbuff.h>
- #include <linux/init.h>
- #include <linux/ip.h>
- #include <linux/types.h>
- #include <linux/sched.h>
- #include <net/sock.h>
- #include <net/netlink.h>
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("Koorey King");
- struct sock *nl_sk = NULL;
- static struct task_struct *mythread = NULL; //内核线程对象
- //向用户空间发送消息的接口
- void sendnlmsg(char *message/*,int dstPID*/)
- {
- struct sk_buff *skb;
- struct nlmsghdr *nlh;
- int len = NLMSG_SPACE(MAX_MSGSIZE);
- int slen = 0;
- if(!message || !nl_sk){
- return;
- }
- // 为新的 sk_buffer申请空间
- skb = alloc_skb(len, GFP_KERNEL);
- if(!skb){
- printk(KERN_ERR "my_net_link: alloc_skb Error./n");
- return;
- }
- slen = strlen(message)+1;
- //用nlmsg_put()来设置netlink消息头部
- nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);
- // 设置Netlink的控制块里的相关信息
- NETLINK_CB(skb).pid = 0; // 消息发送者的id标识,如果是内核发的则置0
- NETLINK_CB(skb).dst_group = 5; //多播组号为5,但置成0好像也可以。
- message[slen] = '\0';
- memcpy(NLMSG_DATA(nlh), message, slen+1);
- //通过netlink_unicast()将消息发送用户空间由dstPID所指定了进程号的进程
- //netlink_unicast(nl_sk,skb,dstPID,0);
- netlink_broadcast(nl_sk, skb, 0,5, GFP_KERNEL); //发送多播消息到多播组5,这里我故意没有用1之类的“常见”值,目的就是为了证明我们上面提到的多播组号和多播组号掩码之间的对应关系
- printk("send OK!\n");
- return;
- }
- //每隔1秒钟发送一条“I am from kernel!”消息,共发10个报文
- static int sending_thread(void *data)
- {
- int i = 10;
- struct completion cmpl;
- while(i--){
- init_completion(&cmpl);
- wait_for_completion_timeout(&cmpl, 1 * HZ);
- sendnlmsg("I am from kernel!");
- }
- printk("sending thread exited!");
- return 0;
- }
- static int __init myinit_module()
- {
- printk("my netlink in\n");
- nl_sk = netlink_kernel_create(NETLINK_TEST,0,NULL,THIS_MODULE);
- if(!nl_sk){
- printk(KERN_ERR "my_net_link: create netlink socket error.\n");
- return 1;
- }
- printk("my netlink: create netlink socket ok.\n");
- mythread = kthread_run(sending_thread,NULL,"thread_sender");
- return 0;
- }
- static void __exit mycleanup_module()
- {
- if(nl_sk != NULL){
- sock_release(nl_sk->sk_socket);
- }
- printk("my netlink out!\n");
- }
- module_init(myinit_module);
- module_exit(mycleanup_module);
关于内核中netlink_kernel_create(int unit, unsigned int groups,…)函数里的第二个参数指的是我们内核进程最多能处理的多播组的个数,如果该值小于32,则默认按32处理,所以在调用netlink_kernel_create()函数时可以不用纠结第二个参数,一般将其置为0就可以了。 在skbuff{}结构体中,有个成员叫做"控制块",源码对它的解释如下: - struct sk_buff {
- /* These two members must be first. */
- struct sk_buff *next;
- struct sk_buff *prev;
- … …
- /*
- * This is the control buffer. It is free to use for every
- * layer. Please put your private variables there. If you
- * want to keep them across layers you have to do a skb_clone()
- * first. This is owned by whoever has the skb queued ATM.
- */
- char cb[48];
- … …
- }
当内核态的Netlink发送数据到用户空间时一般需要填充skbuff的控制块,填充的方式是通过强制类型转换,将其转换成struct netlink_skb_parms{}之后进行填充赋值的: - struct netlink_skb_parms
- {
- struct ucred creds; /* Skb credentials */
- __u32 pid;
- __u32 dst_group;
- kernel_cap_t eff_cap;
- __u32 loginuid; /* Login (audit) uid */
- __u32 sid; /* SELinux security id */
- };
填充时的模板代码如下: - NETLINK_CB(skb).pid=xx;
- NETLINK_CB(skb).dst_group=xx;
这里要注意的是在Netlink协议簇里提到的skbuff的cb控制块里保存的是属于Netlink的私有信息。怎么讲,就是Netlink会用该控制块里的信息来完成它所提供的一些功能,只是完成Netlink功能所必需的一些私有数据。打个比方,以开车为例,开车的时候我们要做的就是打火、控制方向盘、适当地控制油门和刹车,车就开动了,这就是汽车提供给我们的“功能”。汽车的发动机,轮胎,传动轴,以及所用到的螺丝螺栓等都属于它的“私有”数据cb。汽车要运行起来这些东西是不可或缺的,但它们之间的协作和交互对用户来说又是透明的。就好比我们Netlink的私有控制结构struct netlink_skb_parms{}一样。 目前我们的例子中,将NETLINK_CB(skb).dst_group设置为相应的多播组号和0效果都是一样,用户空间都可以收到该多播消息,原因还不是很清楚,还请Netlink的大虾们帮我点拨点拨。 编译后重新运行,最后的测试结果如下: 注意,这里一定要先执行insmod加载内核模块,然后再运行用户空间的程序。如果没有加载mynlkern.ko而直接执行./test 5在bind()系统调用时会报如下的错误: bind failed: No such file or directory 因为网上有写文章在讲老版本Netlink的多播时用法时先执行了用户空间的程序,然后才加载内核模块,现在(2.6.21)已经行不通了,这一点请大家注意。 小结:通过这三篇博文我们对Netlink有了初步的认识,并且也可以开发基于Netlink的基本应用程序。但这只是冰山一角,要想写出高质量、高效率的软件模块还有些差距,特别是对Netlink本质的理解还需要提高一个层次,当然这其中牵扯到内核编程的很多基本功,如临界资源的互斥、线程安全性保护、用Netlink传递大数据时的处理等等都是开发人员需要考虑的问题。 Linux中与内核通信的Netlink机制(实例) Netlink在2.6版本的内核中变化也是很大的,在最新的2.6.37内核中,其定义已经改成下面这种形式,传递的参数已经达到6个。其中第一个参数 和mutex参数都是最新添加的。Mutex也可以为空。这里主要是关于内核空间中的netlink函数的使用。 extern struct sock *netlink_kernel_create(struct net *net, int unit,unsigned int groups, void (*input)(struct sk_buff *skb), struct mutex *cb_mutex, struct module *module); | struct net是一个网络名字空间namespace,在不同的名字空间里面可以有自己的转发信息库,有自己的一套net_device等等。默认情况下都是使用 init_net这个全局变量,下面是内核中调用netlink_kernel_create()函数的一个示例。 在内核中, audit_sock = netlink_kernel_create(&init_net, NETLINK_AUDIT, 0, audit_receive, NULL, THIS_MODULE); | 模块调用函数 netlink_unicast 来发送单播消息: int netlink_unicast(struct sock *ssk, struct sk_buff *skb, u32 pid, int nonblock) | 参数ssk为函数 netlink_kernel_create()返回的socket,参数skb存放消息,它的data字段指向要发送的netlink消息结构,而 skb的控制块保存了消息的地址信息,前面的宏NETLINK_CB(skb)就用于方便设置该控制块, 参数pid为接收消息进程的pid,参数nonblock表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函 数在没有接收缓存可利用 定时睡眠。 netlink的内核实现在.c文件 net/core/af_netlink.c中,内核模块要想使用netlink,也必须包含头文件linux/netlink.h。内核使用 netlink需要专门的API,这完全不同于用户态应用对netlink的使用。如果用户需要增加新的netlink协议类型,必须通过修改 linux/netlink.h来实现,当然,目前的netlink实现已经包含了一个通用的协议类型NETLINK_GENERIC以方便用户使用,用 户可以直接使用它而不必增加新的协议类型。前面讲到,为了增加新的netlink协议类型,用户仅需增加如下定义到linux/netlink.h就可 以: 只要增加这个定义之后,用户就可以在内核的任何地方引用该协议。 在内核中,为了创建一个netlink socket用户需要调用如下函数: extern struct sock *netlink_kernel_create(struct net *net, int unit,unsigned int groups, void (*input)(struct sk_buff *skb), struct mutex *cb_mutex, struct module *module); | struct net是一个网络名字空间namespace,在不同的名字空间里面可以有自己的转发信息库,有自己的一套net_device等等。默认情况下都是使用init_net这个全局变量 参数unit表示netlink协议类型,如 NETLINK_MYTEST,参数input则为内核模块定义的netlink消息处理函数,当有消息到达这个netlink socket时,该input函数指针就会被引用。函数指针input的参数skb实际上就是函数netlink_kernel_create返回的 struct sock指针,sock实际是socket的一个内核表示数据结构,用户态应用创建的socket在内核中也会有一个struct sock结构来表示。 函数input()会在发送进程执行sendmsg()时 被调用,这样处理消息比较及时,但是,如果消息特别长时,这样处理将增加系统调用sendmsg()的执行时间,也就是说当用户的程序调用sendmsg ()函数时,如果input()函数处理时间过长,也就是说input()函数不执行不完,用户程序调用的sendmsg()函数就不会返回。只有当内核 空间中的input()函数返回时,用户调用的sendmsg()函数才会返回。对于这种情况,可以定义一个内核线程专门负责消息接收,而函数input 的工作只是唤醒该内核线程,这样sendmsg将很快返回。(这里网上的的说明)不过在查看Linux2.6.37版本的内核时并没有发现这种处理过程, 一般都是按下面的方法进行处理。 这里注册的netlink协议为NETLINK_XFRM。 nlsk = netlink_kernel_create(net, NETLINK_XFRM, XFRMNLGRP_MAX, xfrm_netlink_rcv, NULL, THIS_MODULE); static void xfrm_netlink_rcv(struct sk_buff *skb) { mutex_lock(&xfrm_cfg_mutex); netlink_rcv_skb(skb, &xfrm_user_rcv_msg); mutex_unlock(&xfrm_cfg_mutex); } 在netlink_rcv_skb()函数中进行接收处理。 int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 pid, u32 group, gfp_t allocation) | 前面的三个参数与 netlink_unicast相同,参数group为接收消息的多播组,该参数的每一个位代表一个多播组,因此如果发送给多个多播组,就把该参数设置为 多个多播组组ID的位或。参数allocation为内核内存分配类型,一般地为GFP_ATOMIC或GFP_KERNEL,GFP_ATOMIC用于 原子的上下文(即不可以睡眠),而GFP_KERNEL用于非原子上下文。 NETLINK_CB(skb).pid = 0; NETLINK_CB(skb).dst_pid = 0; NETLINK_CB(skb).dst_group = 1; | 字段pid表示消息发送者进程 ID,也即源地址,对于内核,它为 0, dst_pid 表示消息接收者进程 ID,也即目标地址,如果目标为组或内核,它设置为 0,否则 dst_group 表示目标组地址,如果它目标为某一进程或内核,dst_group 应当设置为 0。 下面是参考网上使用netlink写的和内核通信的两个程序,一个是用户空间,一个是内核空间。 内核空间: #include <linux/init.h> #include <linux/module.h> #include <linux/timer.h> #include <linux/time.h> #include <linux/types.h> #include <net/sock.h> #include <net/netlink.h> #define NETLINK_TEST 25 #define MAX_MSGSIZE 1024 int stringlength(char *s); void sendnlmsg(char * message); int pid; int err; struct sock *nl_sk = NULL; int flag = 0; void sendnlmsg(char *message) { struct sk_buff *skb_1; struct nlmsghdr *nlh; int len = NLMSG_SPACE(MAX_MSGSIZE); int slen = 0; if(!message || !nl_sk) { return ; } skb_1 = alloc_skb(len,GFP_KERNEL); if(!skb_1) { printk(KERN_ERR "my_net_link:alloc_skb_1 error\n"); } slen = stringlength(message); nlh = nlmsg_put(skb_1,0,0,0,MAX_MSGSIZE,0); NETLINK_CB(skb_1).pid = 0; NETLINK_CB(skb_1).dst_group = 0; message[slen]= '\0'; memcpy(NLMSG_DATA(nlh),message,slen+1); printk("my_net_link:send message '%s'.\n",(char *)NLMSG_DATA(nlh)); netlink_unicast(nl_sk,skb_1,pid,MSG_DONTWAIT); } int stringlength(char *s) { int slen = 0; for(; *s; s++){ slen++; } return slen; } void nl_data_ready(struct sk_buff *__skb) { struct sk_buff *skb; struct nlmsghdr *nlh; char str[100]; struct completion cmpl; int i=10; skb = skb_get (__skb); if(skb->len >= NLMSG_SPACE(0)) { nlh = nlmsg_hdr(skb); memcpy(str, NLMSG_DATA(nlh), sizeof(str)); printk("Message received:%s\n",str) ; pid = nlh->nlmsg_pid; while(i--) { init_completion(&cmpl); wait_for_completion_timeout(&cmpl,3 * HZ); sendnlmsg("I am from kernel!"); } flag = 1; kfree_skb(skb); } } // Initialize netlink int netlink_init(void) { nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, 1, nl_data_ready, NULL, THIS_MODULE); if(!nl_sk){ printk(KERN_ERR "my_net_link: create netlink socket error.\n"); return 1; } printk("my_net_link_3: create netlink socket ok.\n"); return 0; } static void netlink_exit(void) { if(nl_sk != NULL){ sock_release(nl_sk->sk_socket); } printk("my_net_link: self module exited\n"); } module_init(netlink_init); module_exit(netlink_exit); MODULE_AUTHOR("frankzfz"); MODULE_LICENSE("GPL"); | 下面是用户空间的程序: #include <sys/stat.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <sys/types.h> #include <string.h> #include <asm/types.h> #include <linux/netlink.h> #include <linux/socket.h> #include <errno.h> #define NETLINK_TEST 25 #define MAX_PAYLOAD 1024 // maximum payload size int main(int argc, char* argv[]) { int state; struct sockaddr_nl src_addr, dest_addr; struct nlmsghdr *nlh = NULL; struct iovec iov; struct msghdr msg; int sock_fd, retval; int state_smg = 0; // Create a socket sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST); if(sock_fd == -1){ printf("error getting socket: %s", strerror(errno)); return -1; } // To prepare binding memset(&msg,0,sizeof(msg)); memset(&src_addr, 0, sizeof(src_addr)); src_addr.nl_family = AF_NETLINK; src_addr.nl_pid = getpid(); // self pid src_addr.nl_groups = 0; // multi cast retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr)); if(retval < 0){ printf("bind failed: %s", strerror(errno)); close(sock_fd); return -1; } // To prepare recvmsg nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)); if(!nlh){ printf("malloc nlmsghdr error!\n"); close(sock_fd); return -1; } memset(&dest_addr,0,sizeof(dest_addr)); dest_addr.nl_family = AF_NETLINK; dest_addr.nl_pid = 0; dest_addr.nl_groups = 0; nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD); nlh->nlmsg_pid = getpid(); nlh->nlmsg_flags = 0; strcpy(NLMSG_DATA(nlh),"Hello you!"); iov.iov_base = (void *)nlh; iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD); // iov.iov_len = nlh->nlmsg_len; memset(&msg, 0, sizeof(msg)); msg.msg_name = (void *)&dest_addr; msg.msg_namelen = sizeof(dest_addr); msg.msg_iov = &iov; msg.msg_iovlen = 1; printf("state_smg\n"); state_smg = sendmsg(sock_fd,&msg,0); if(state_smg == -1) { printf("get error sendmsg = %s\n",strerror(errno)); } memset(nlh,0,NLMSG_SPACE(MAX_PAYLOAD)); printf("waiting received!\n"); // Read message from kernel while(1){ printf("In while recvmsg\n"); state = recvmsg(sock_fd, &msg, 0); if(state<0) { printf("state<1"); } printf("In while\n"); printf("Received message: %s\n",(char *) NLMSG_DATA(nlh)); } close(sock_fd); return 0; } | 下面是Makefile文件: obj-m := netlink_k.o KERNELBUILD := /lib/modules/`uname -r`/build default: @echo "BUILE Kmod" @make -C $(KERNELBUILD) M=$(shell pwd) modules gcc -o netlink_2 netlink_2.c clean: @echo " CLEAN kmod" @rm -rf *.o @rm -rf .depend .*.cmd *.ko *.mod.c .tmp_versions *.symvers .*.d | 其中,netlink_k.c为内核的空间的程序。 先运行内核代码netlink_k.ko,也就是在执行完makefile文件后,会生成一个netlink_k.ko文件,可以使用下面的命令进行安 装,insmod netlink_k.ko,使用lsmod查看,当安装成功后,然后,执行./netlink用户空间程序,可以在另一个终端下执行dmesg命令,查看 内核通信的情况。这里netlink程序向内核空间发送一个hello you!内核返回给一个I am from kernel!在这里使用了一个定时器,也就是每3秒中发送一次I am from kernel!只有内核把10个字符串全部发送完毕后,用户空间的sendmsg()才会返回,也就是在用户空间的netlink才会输出内核空间发送过 来的数据,这里只有一个简单的程序,并没有什么实际的意义,因为,正如前面所说的一般情况下不会在回调函数中处理太多的东西,以免sendmsg()函数 返回不及时。下面是使用dmesg命令输出的信息。 [873791.498039] my_net_link_3: create netlink socket ok. [873810.263676] Message received:Hello [873813.260848] my_net_link_4:send message 'I am from kernel!'. [873816.260821] my_net_link_4:send message 'I am from kernel!'. [873819.260860] my_net_link_4:send message 'I am from kernel!'. [873822.260762] my_net_link_4:send message 'I am from kernel!'. [873825.260883] my_net_link_4:send message 'I am from kernel!'. [873828.260669] my_net_link_4:send message 'I am from kernel!'. [873831.260714] my_net_link_4:send message 'I am from kernel!'. [873834.260683] my_net_link_4:send message 'I am from kernel!'. [873837.260666] my_net_link_4:send message 'I am from kernel!'. [873840.260632] my_net_link_4:send message 'I am from kernel!'. | 参考网址: http://blog.csdn.net/wangjingfei/archive/2010/02/05/5288460.aspx http://blog.csdn.net/liumang_D/archive/2010/03/25/5413042.aspx Linux 系统内核空间与用户空间通信的实现与分析 |
这篇关于switch_set_state/netlink/kmod的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!