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的消息头
消息头定义在文件里,由结构体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每个属性都由文件里的struct nlattr{}来表示:
Netlink提供的错误指示消息
内容
当用户空间的应用程序和内核空间的进程之间通过Netlink通信时发生了错误,Netlink必须向用户空间通报这种错误。Netlink对错误消息进行了单独封装,:
点击(此处)折叠或打开
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剩下的知识点,我们在后面的实战环节有用到时再讨论。