主要参考了《深入Linux内核架构》和《精通Linux内核网络》相关章节
netlink是一种基于网络的机制,允许在内核内部以及内核与用户层之间进行通信。最早在内核2.2引入,旨在替代笨拙的IOCTL,IOCTL不能从内核向用户空间发送异步消息,而且必须定义IOCTL号。
Netlink协议定义在RFC3549中。以前是可以编译成模块,现在直接集成到内核了。与profs和sysfs相比,有一些优势如下:
/proc/net/netlink文件中包含了当前活动的netlink连接信息。
代码位于net/netlink中:af_netlink.c af_netlink.h diag.c genetlink.c
除这些文件外,还有几个头文件。事实上,最常用的是af_netlink模块。它提供了Netlink内核套接字API,而genetlink模块提供了新的通用Netlink APl。使用它创建Netlink消息更容易。监视接模块diag ( diag.c)提供的API用于读写有关Netlink套接字的信息。
要在用户空间中创建Netlink套接字,可使用系统调用socket()。Netlink套接字可以是SOCK_RAW套接字,也可以是SOCK_DGRAM套接字。
它表示Netlink套接字的地址。与socket绑定
struct sockaddr_nl {
__kernel_sa_family_t nl_family; /* 始终为AF_NETLINK */
unsigned short nl_pad; /* zero */
__u32 nl_pid; /* 端口号 */
__u32 nl_groups; /* 组播组掩码 */
};
nl_pid: Netlink套接字的单播地址。对于内核Netlink套接字,该值应为0。
用户空间应用程序有时会将nl_pid设置为其进程ID ( pid )。在用户空间应用程序中,如果你显式地将nl_pid设置为0或根本不设置它,之后再调用bind(),则内核方法netlink_autobind()将给nl_pid赋值,会尝试将其设置为当前线程的进程ID。
如果你要在用户空间中创建两个套接字,且没有调用bind(),就得确保nl_pid是唯一的。Netlink套接字不仅用于网络子系统,还可用于其他子系统,如SELinux , audit 、uevent等。rtnelink套接字是专用于联网的Netlink套接字,用于路由消息、邻接消息、链路消息和其他网络子系统消息。
iproute2包基于Netlink套接字,在2.1.7节中,提供了一个在iproute2中使用Netlink套接字的示例。net-tools包投入使用的时间较为久远,未来可能会被摒弃。这里之所以提及它,旨在说明,它虽然可以代替iproute2,但功能和威力都远不及后者。
有两个用于控制TCP/IP联网和处理网络设备的包:net-tools和iproute2。
iproute2包包含如下命令
iproute2包主要基于通过netlink套接字从用户空间向内核发送请示并获取应答。
net-tools包是基于IOCTL的,也有些主要命令:
在内核网络栈中,可创建多种Netlink套接字。每种内核套接字都可处理不同类型的消息。
当创建socket时, 协议类型参数选择的是NETLINK_ROUTE, 得到的socket是rtnetlink_socket,
rtnelink套接字是专用于link net的Netlink套接字,用于路由消息、邻接消息、链路消息和其他网络子系统消息。
请注意,rtnetlink套接字支持网络命名空间。网络命名空间对象(结构net)包含一个名为rtnl的成员( rtnetlink套接字)。在方法rtnetlink_net_init()中,调用netlink_kernel_create()创建rtnetlink套接字后,将其赋给了相应网络命名空间对象的rtnl指针。
来看看netlink_kernel_create()的原型。
static inline struct sock *
netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
{
return __netlink_kernel_create(net, unit, THIS_MODULE, cfg);
}
第1个参数net 为网络命名空间。
第2个参数为Netlink协议。
(例如,NETLINK_ROUTE表示rtnetlink消息,NETLINK_XFRM表示IPsec子系统,NETLINK_AUDIT表示审计子系统)
有20多种Netlink协议,但总数不能超过32(MAX_LINKS ),这是开发通用Netlink协议的原因之一。Netlink协议的完整清单可在include/uapi/linux/netlink.h中找到。
第3个参数是一个netlink_kernel_cfg引用。这个结构包含用于创建Netlink套接字的可选参数
/* optional Netlink kernel configuration parameters */
struct netlink_kernel_cfg {
unsigned int groups;
unsigned int flags;
void (*input)(struct sk_buff *skb);
struct mutex *cb_mutex;
int (*bind)(struct net *net, int group);
void (*unbind)(struct net *net, int group);
bool (*compare)(struct net *net, struct sock *sk);
};
成员input用于指定回调函数。如果netlink_kernel_cfg的成员input为NULL,内核套接字将无法接收来自用户空间的数据(虽然能够从内核向用户空间发送数据)。对于rtnetlink内核套接字,方法rtnetlink_rcv()将被指定为input回调函数。这样,通过rtnelink套接字从用户空间发送的数据将由rtnetlink_rcv()进行处理。
方法netlink_kernel_create()调用方法netlink_insert(),在nl_table表中创建一个条目。对nl_table表的访问由读写锁nl_table_lock进行保护。要在这个表中进行查找,可调用方法netlink_lookup(),并指定协议和端口号。
**要为特定消息类型注册回调函数,可调用rtnl_register()。**在网络内核代码的多个地方,都注册了这样的回调函数。
使用Netlink内核套接字,首先需要注册它,具体内核源码如下
net\core\rtnetlink.c
void rtnl_register(int protocol, int msgtype,
rtnl_doit_func doit, rtnl_dumpit_func dumpit,
rtnl_calcit_func calcit)
{
if (__rtnl_register(protocol, msgtype, doit, dumpit, calcit) < 0)
panic("Unable to register rtnetlink message handler, "
"protocol = %d, message type = %d\n",
protocol, msgtype);
}
EXPORT_SYMBOL_GPL(rtnl_register);
回调函数doit用于指定添加、删除、修改等操作,回调函数dumpit用于检索信息,回调函数calcit用于计算缓冲区大小。rtnetlink模块中有一个名为rtnl_msg_handlers的表。这个表将协议号用作索引。其中的每个条目本身都是一个表,并将消息类型作为其索引。表中的每个元素都是一个rtnl_link实例——由指向上述3个回调函数的指针组成的结构。使用rtnl_register()注册回调函数时,指定的回调函数将被加人到这个表中。
rtnelink消息是使用rtmsg_ifinfo()发送的。例如,在dev_open()中,使用下面的代码创建一条新链路: rtmsg_ifinfo(RTM_NEWLINK,dev,IFF_UP|IFF_RUNNING)。在方法rtmsg_ifinfo()中,首先调用方法nlmsg_new()来分配一个大小合适的sk_buff。然后创建两个对象:Netlink消息报头( nlmsghdr )和ifinfomsg对象。后者紧跟在Netlink消息报头后面。这两个对象由方法rtnl_fill_ifinfo()初始化。接下来,调用rtnl_notify()来发送数据包。数据包的实际发送工作是由通用Netlink方法nlmsg_notify()完成的。
Netlink消息必须采用特定的格式,此格式是在RFC 3549中进行规定的。Netlink消息的开头是长度固定的Netlink报头,其后是有效载荷。

struct nlmsghdr {
__u32 nlmsg_len; /* Length of message including header */
__u16 nlmsg_type; /* 消息类型 */
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; // 序列号,用于排列消息。与有些第4层传输协议不同,Netlink并未要求必须使用序列号。
// 发送端口的ID。对于从内核发送的消息,nlmsg_pid为0;对于从用户空间发送的消息,可将nlmsg_pid设置为发送消息的用户空间应用程序的进程ID。
__u32 nlmsg_pid;
};
nlmsg_type:消息类型。有如下4种基本的Netlink消息类型。
nlmsg_flags字段的可能取值如下。
下面的标志是针对条目创建的说明符。
紧跟在报头后面的是有效载荷。
/*
* <------- NLA_HDRLEN ------> <-- NLA_ALIGN(payload)-->
* +---------------------+- - -+- - - - - - - - - -+- - -+
* | Header | Pad | Payload | Pad |
* | (struct nlattr) | ing | | ing |
* +---------------------+- - -+- - - - - - - - - -+- - -+
* <-------------- nlattr->nla_len -------------->
*/
struct nlattr {
__u16 nla_len; // 长度
__u16 nla_type;
};
nla_type:属性的类型。
nla_type的可能取值包括
所有的Netlink属性都必须与4字节边界( NLA_ALIGNTO)对齐。
每个协议簇都可定义属性有效性策略,即对收到的属性的期望。这种有效性策略用nla_policy对象表示。事实上,结构nla_policy内容与结构nlattr完全相同。
struct nla_policy { u16 type; u16 len; };
- 1
- 2
- 3
- 4
属性有效性策略是一个nla_policy对象数组。这个数组将属性号用作索引。对于每个属性(定长属性除外),如果nla_policy对象的len值为0,就不执行有效性检查。如果属性的类型为字符串(如NLA_STRING),则len值应为字符串的最大长度(不包括末尾的NULL字节);如果属性的类型为NLA_UNSPEC(或未知),应将len设置为属性有效载荷的长度;如果属性的类型为NLA_FLAG,将不使用len(其中的原因在于,属性存在就表示true,属性不存在就表示false )。
rtnetlink (NETLINK_ROUTE)消息(协议)并非仅限于网络路由选择子系统消息,还包括邻接子系统消息﹑接口设置消息、防火墙消息.Netlink排队消息、策略路由消息以及众多其他类型的rtnetlink消息。
NETLINK_ROUTE消息分为多个消息簇:
每个消息簇都分为3类,分别用于创建、删除和检索信息。因此,路由选择消息包含用于创·建路由的消息类型RTM_NEWROUTE、用于删除路由的消息类型RTM_DELROUTE以及用于检索路由的消息类型RTM_GETROUTE。对于LINK消息簇,除用于创建、删除和信息检索的消息类型外,还有用于修改链路的消息类型:RTM_SETLINK。
下面从Netlink协议的角度出发,看看添加和删除路由选择条目时在内核中发生的情况。
要在路由选择表中添加路由选择条目,可运行类似于下面的命令。
ip route add 192.168.2.11 via 192.168.2.20
要删除前面添加的路由选择条目,可执行如下命令。
ip route del 192.168.2.11
可像下面这样使用iproute2命令ip来监视网络事件。ip monitor route
执行命令ip monitor route时,将启动一个守护程序,它将打开一个Netlink套接字,**并加入RTNLGRP_IPV4_ROUTE组播组。**这样,像前面的示例那样添加/删除路由时,将导致如下结果:使用rtnl_notify()发送的消息将被守护程序接收,并显示在终端窗口中。
可以以这种方式加人其他组播组。
至此,你知道了Netlink消息是什么以及如何创建和处理它们,还知道如何处理Netlink套接字。接下来,你将学习开发通用Netlink簇(在内核2.6.15中首次引入)的原因及其Linux实现。
Netlink协议的一个缺点是,协议簇数不能超过32(MAX_LINKS)个。这是开发通用Netlink簇的主要原因之一——旨在支持添加更多的协议簇。它就像是Netlink多路复用器,使用单个Netlink协议簇(NETLINK_GENERIC)。通用Netlink协议以Netlink协议为基础,并使用其API。
通用Netlink消息
通用Netlink消息的开头是一个Netlink报头,接下来是通用Netlink消息报头以及可选的用户特定报头,然后才是可选的有效载荷,如下图所示。
struct genlmsghdr { __u8 cmd; __u8 version; __u16 reserved; };
- 1
- 2
- 3
- 4
- 5
cmd:通用Netlink消息类型。你注册的每个通用簇都将添加自己的命令。
version:可用于提供版本控制支持。版本号的作用是:能够在不破坏向后兼容性的情况下修改消息的格式。
reserved:保留,供以后使用。
为通用Netlink消息分配缓冲区的工作由下面的方法完成。sk_buff *genlmsg_new(size_t payload,gfp_t flags)
它实际上是一个nlmsg_new()包装器。使用genlmsg_new()分配缓冲区后,调用genlmsg_put()来创建通用Netlink报头。它是一个genlmsghdr实例。单播通用Netlink消息是使用genlmsg_unicast()发送的。它实际上是一个nlmsg_unicast()包装器。发送组播通用Netlink消息的方式有如下两种。
genlmsg_multicast(): 此方法将消息发送到默认网络命名空间net_init()。
qenlmsg multicast allns():此方法将消息发送到所有网络命名空间。
CRIU(Checkpoint/Restore In Userspace)运行在linux操作系统上的一个软件工具,其功能是在用户空间实现Checkpoint/Restore功能。使用这个工具,你可以冻结一个正在运行的程序,并且checkpoint它到一系列的文件,然后你就可以使用这些文件在任何主机重新恢复这个程序到被冻结的那个点(白话就是实现对已运行程序的备份和恢复)。所以criu通常被用在程序或者容器的热迁移、快照、远程调试等。CRIU 起初是Virtuozzo的一个项目,随着开源社区的帮助,现在也被整合到OpenVZ(它是 Virtuozzo 的开源版本), LXC/LXD, Docker, Podman等软件项目里。
Netlink套接字sock_diag提供了一个基于Netlink的子系统,可用于获取有关套接字的信息。在内核中添加它旨在在Linux用户空间中支持检查点/恢复功能(CRIU)。要支持这项功能,还需要有关套接字的其他数据。 例如,/procfs并没有指出UNIX域套接字(AF_UNIX)的对等体。然而,若要支持检查点/恢复,则必须有这种信息。通过/proc无法导出这种额外信息,而修改procfs条目也并非总是可行的选择,因为这样做可能破坏用户空间应用程序。
Netlink套接字sock_diag提供了一个API,让你能够访问这种额外数据。 CRIU项目和实用工具ss都使用了这个API。为进程建立检查点(将进程的状态存储到文件系统)后,如果不使用sock_diag,将无法重建其UNIX域套接字,因为你不知道对等体都有谁。
**为支持工具ss使用的监视接口,开发了一种基于Netlink 的内核套接字——NETLINK_SOCK_DIAG。**iproute2包中的工具ss让你能够获取套接字的统计信息,其方式类似于Netstat。相比于其他工具,它显示的TCP和状态信息更多。
创建Netlink内核套接字sock_diag的方式如下。
static int __net_init diag_net_init(struct net *net)
{
struct netlink_kernel_cfg cfg = {
.groups = SKNLGRP_MAX,
.input = sock_diag_rcv,
.bind = sock_diag_bind,
.flags = NL_CFG_F_NONROOT_RECV,
};
net->diag_nlsk = netlink_kernel_create(net, NETLINK_SOCK_DIAG, &cfg);
return net->diag_nlsk == NULL ? -ENOMEM : 0;
}
sock_diag模块包含一个名为sock_diag_handlers的表,其中包含一系列sock_diag_handler对象。这个表将协议号用作索引(完整的协议号清单请参阅include/linux/socket.h)。
每个需要在此表中添加套接字监视接口条目的协议都会首先定义一个处理程序,然后再调用sock_diag_register(),并指定其处理程序。
本章介绍了Netlink套接字。它提供了一种在用户空间和内核间进行通信的机制,被广泛用于网络子系统。在此你见到了一些Netlink套接字使用示例。另外,本章还讨论了Netlink消息以及它们是被如何创建和处理的。本章探讨的另一个重要主题是通用Netlink套接字,介绍了其优点和用途。
#include
#include
#include
#include
#include
#define NETLINK_TEST 30 // 未被使用的协议号
#define MSG_LEN 125
#define USER_PORT 100
MODULE_LICENSE("GPL");
MODULE_AUTHOR("vico");
MODULE_DESCRIPTION("netlink protocol example");
struct sock *nlsk = NULL;
extern struct net init_net;
int send_usrmsg(char *pbuf, uint16_t len)
{
struct sk_buff *nl_skb;
struct nlmsghdr *nlh;
int ret;
/* 创建sk_buff 空间 */
nl_skb = nlmsg_new(len, GFP_ATOMIC);
if(!nl_skb)
{
printk("\nError:netlink alloc failure.\n\n");
return -1;
}
/* 设置netlink消息头部 */
nlh = nlmsg_put(nl_skb, 0, 0, NETLINK_TEST, len, 0);
if(nlh == NULL)
{
printk("\nError:nlmsg_put failaure. \n\n");
nlmsg_free(nl_skb);
return -1;
}
/* 拷贝数据发送 */
memcpy(nlmsg_data(nlh), pbuf, len);
ret = netlink_unicast(nlsk, nl_skb, USER_PORT, MSG_DONTWAIT);
return ret;
}
static void netlink_rcv_msg(struct sk_buff *skb)
{
struct nlmsghdr *nlh = NULL;
char *umsg = NULL;
char *kmsg = "Hello World!";
if(skb->len >= nlmsg_total_size(0))
{
nlh = nlmsg_hdr(skb);
umsg = NLMSG_DATA(nlh);
if(umsg)
{
printk("kernel recv from user: %s\n", umsg);
send_usrmsg(kmsg, strlen(kmsg));
}
}
}
struct netlink_kernel_cfg cfg = {
.input = netlink_rcv_msg, /* set recv callback */
};
int test_netlink_init(void)
{
/* create netlink socket */
nlsk = (struct sock *)netlink_kernel_create(&init_net, NETLINK_TEST, &cfg);
if(nlsk == NULL)
{
printk("\nError:netlink_kernel_create error !\n");
return -1;
}
printk("\ntest_netlink_init\n");
return 0;
}
void test_netlink_exit(void)
{
if (nlsk){
netlink_kernel_release(nlsk); /* release ..*/
nlsk = NULL;
}
printk("test_netlink_exit!\n");
}
module_init(test_netlink_init);
module_exit(test_netlink_exit);