共计 5175 个字符,预计需要花费 13 分钟才能阅读完成。
导读 | 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 的理解,需要把握几个关键点:
1、面向数据报的无连接消息子系统
2、基于通用的 BSD Socket 架构而实现
关于第一点使我们很容易联想到 UDP 协议,能想到这一点就非常棒了。按着 UDP 协议来理解 Netlink 不是不无道理,只要你能触类旁通,做到“活学”,善于总结归纳、联想,最后实现知识迁移这就是学习的本质。Netlink 可以实现内核 -> 用户以及用户 -> 内核的双向、异步的数据通信,同时它还支持两个用户进程之间、甚至两个内核子系统之间的数据通信。本文中,对后两者我们不予考虑,焦点集中在如何实现用户 <-> 内核之间的数据通信。
看到第二点脑海中是不是瞬间闪现了下面这张图片呢?如果是,则说明你确实有慧根;当然,不是也没关系,慧根可以慢慢长嘛,呵呵。
在后面实战 Netlink 套接字编程时我们主要会用到 socket(),bind(),sendmsg()
和 recvmsg()等系统调用,当然还有 socket 提供的轮训 (polling) 机制。
Netlink 支持两种类型的通信方式:单播和多播。
单播:经常用于一个用户进程和一个内核子系统之间 1:1 的数据通信。用户空间发送命令到内核,然后从内核接受命令的返回结果。
多播:经常用于一个内核进程和多个用户进程之间的 1:N 的数据通信。内核作为会话的发起者,用户空间的应用程序是接收者。为了实现这个功能,内核空间的程序会创建一个多播组,然后所有用户空间的对该内核进程发送的消息感兴趣的进程都加入到该组即可接收来自内核发送的消息了。如下:
其中进程 A 和子系统 1 之间是单播通信,进程 B、C 和子系统 2 是多播通信。上图还向我们说明了一个信息。从用户空间传递到内核的数据是不需要排队的,即其操作是同步完成;而从内核空间向用户空间传递数据时需要排队,是异步的。了解了这一点在开发基于 Netlink 的应用模块时可以使我们少走很多弯路。假如,你向内核发送了一个消息需要获取内核中某些信息,比如路由表,或其他信息,如果路由表过于庞大,那么内核在通过 Netlink 向你返回数据时,你可以好生琢磨一下如何接收这些数据的问题,毕竟你已经看到了那个输出队列了,不能视而不见啊。
Netlink 消息由两部分组成:消息头和有效数据载荷,且整个 Netlink 消息是 4 字节对齐,一般按主机字节序进行传递。消息头为固定的 16 字节,消息体长度可变:
消息头定义在 <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 的消息体采用 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 的用户 - 内核通信,有两种情况可能会导致丢包:
1、内存耗尽;
2、用户空间接收进程的缓冲区溢出。导致缓冲区溢出的主要原因有可能是:用户空间的进程运行太慢;或者接收队列太短。
如果 Netlink 不能将消息正确传递到用户空间的接收进程,那么用户空间的接收进程在调用 recvmsg()系统调用时就会返回一个内存不足 (ENOBUFS) 的错误,这一点需要注意。换句话说,缓冲区溢出的情况是不会发送在从用户 -> 内核的 sendmsg()系统调用里,原因前面我们也说过了,请大家自己思考一下。
当然,如果使用的是阻塞型 socket 通信,也就不存在内存耗尽的隐患了,这又是为什么呢?赶紧去谷歌一下,查查什么是阻塞型 socket 吧。学而不思则罔,思而不学则殆嘛。
在 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 剩下的知识点,我们在后面的实战环节有用到时再讨论。
未完,待续…