当开发linux内核特性的时候,将必要的内核子系统的详细信息暴露给用户空间的程序是一个比较好的习惯,因为这增强了内核的扩展性。通常来说,软件开发者必须面对这样一个任务:寻找一种好的方法使得用户空间和内核空间进行交互。
NetLinux作为内核接口的一种,担任了这样的功能。维基百科对于它的解释,:
Netlink socketfamily is a Linux
kernel interface usedfor inter-process
communication (IPC)between the kernel and user-space processes,as
well as between user processes (e.g. Unix domain sockets) or amixture
of both types. However, unlike INET sockets, it cannot traversehost boundaries,
because it addresses processes by their (inherently local) PIDs.
在linux内核2.0系列的时候被并入,2000年之前,当时用来替代Ioctl。不过直到现在这个ioctl也还存在,只不过它比较陈旧,rtnetlink和它完成同样的功能。(rtnetlink或许就是netlink最原本的目的,可能是后来随着netlink的壮大,才慢慢的出现了别的netlink,比如generic netlink)。
设计和实用性
主要是linux系统的跨平台的使用,和linux的设计技巧。有些内核子系统,比如网络和设备需要从用户空间来进行配置和管理。用户态程序需要获得资源,服务和信息,同时配置监控内核子系统(后面将要分析lldpad和DCB子系统的关系)。
内核接口是操作系统非常重要的一部分,提供灵活的内核接口对于内核的体验简直太棒了(不然要想要很多功能,内核就会变得非常臃肿),一方面内核可以保持比较小,另一方面又使得用户空间程序可以进行想要的操作。
netlink有很多好处,比如强大的可扩展性和支持基于事件的通知机制。要理解netlink,主要还是要理解c语言编程,要理解内核的一些基本知识,和BSD sockets。
linux内核接口
linux本身有很多内核和用户接口,但是哪个更好,哪个更加适合在什么场合下使用,还是很值得我们深思的。内核接口应该提供的几个特性有可扩展性、架构可移植、事件通知机制和大数据传输特性。
系统调用,是一个在多种操作系统上(包括Windows)非常通用和标准的接口。然而,linux内核开发者非常不情愿在linux上面添加系统调用,它也不能给每个子模块添加特别的调用,针对的是一些通用的接口,没有针对特殊模块的特殊接口。使用固定的layout格式的接口:ioctl;每一个配置都有一个唯一的ioctl号来识别。因为在内核和用户程序之间需要传递消息,为了让这个消息被识别,这个消息一般会用一个固定格式的数据结构(ioctl调用时,会有一个指向这个结构的指针)来表示,不过呢,如果新的配置需要去改变信息的格式,就会需要使用一个新的ioctl调用了。
虚拟文件系统
主要是对于字符和块设备驱动接口来说,内核给用户程序提供了一种像访问文件一样的方式来和这个驱动进行通信。有/proc和sysfs等,这些接口基本上都是为内核设备的驱动留的。那么proc文件系统呢?原本是设计用来存储进程信息的,缺点是有很多信息不能满足,没有事件通知,不支持大文件传输(限制为一页)。对于应用程序来说并不实际,因为这些信息很难convert成为想要的消息啦。sysfs呢?只是接口(文本)对于应用程序来说,也还是不太实用。
BSD socket
最先开始的socket选项是用AF_INET选项,然后调用相关的ioctl来进行通信。socket选项还有AF_RAW,比如用在内核防火墙子系统里面。而Netlink的好处更是表现在可扩展性(格式不固定,可以任意增加属性)。然后,还有事件通知机制等好处,可有效的传输大数据。
上面提到的所有内核接口的设计特性如下图所示:
Netlink
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_RDMA 20 #define NETLINK_CRYPTO 21 /* Crypto layer */ #define MAX_LINKS 32 struct sockaddr_nl { __kernel_sa_family_t nl_family; /* AF_NETLINK */ unsigned short nl_pad; /* zero */ __u32 nl_pid; /* port ID */ __u32 nl_groups; /* multicast groups mask */ }; |
netlink总线
内核最多支持32根netlink总线,比如nfnetlink和rtnetlink分别是两个不同的总线。rtnetlink总线一般负责网络设备的管理,路由和排队机制等。nfnetlink总线被linux下面所有的防火墙子系统模块使用。
传输方式和单播和多播两种方式。
消息的格式
16B,安装4字节对齐。
学过计算机网络的人都知道,每一种协议的包都有她自己的格式,比如说以太网包,IP包,UDP或者TCP包,我们一直致力于研究FCoE包,FCoE包也有FCoE包的格式(不过FCoE作为存储网的特性,可不是用socket来进行通信的,而是通过文件系统或者块设备啦啦啦)。
Netlink也是通过socket来进行通信的,那么他的包类型是怎么样的呢?
不像TCP协议的头部标志和目的地址是自动产生的,NETLINK因为是SOCK_RAW的模式,他的头部必须由调用者来构造。
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 port ID */ }; |
Bit offset |
0–15 |
16–31 |
0 |
Message length |
|
32 |
Type |
Flags |
64 |
Sequence number |
|
96 |
PID |
|
128+ |
Data |
/* Flags values */ #define NLM_F_REQUEST 1 /* It is request message. */ #define NLM_F_MULTI 2 /* Multipart message, terminated by NLMSG_DONE */ #define NLM_F_ACK 4 /* Reply with ack, with zero or error code */ #define NLM_F_ECHO 8 /* Echo this request */ #define NLM_F_DUMP_INTR 16 /* Dump was inconsistent due to sequence change */ |
? Message type (16 bits): the type of this message. There are twosorts, data and controlmessages. Data messages depend on the set of actionsthat the given kernel-spacesubsystem allows. Control messages are common to allNetlink subsystems, there arecurrently
four types of control messages, althoughthere are 16 slots reserved (seeNLM MIN TYPE constant in linux/netlink.h). Theexisting control types are:– NLMSG NOOP: no operation, this can be used to implement a Netlinkping utilityto know if a given Netlink bus is
available.– NLMSGERROR: this message contains an error.– NLMSG DONE: this isthe trailing message that is part of a multi-part message. Amulti-part messageis composed of a set of messages all with the NLM F MULTI
??ag set.– NLMSG OVERRUN: this control message type is currently unused.
用户空间是怎么和内核子系统通信的呢?大概可以在这里看的出来。
比如DCB子系统和lldpad程序。
DCB子系统注册了
rtnl_register(PF_UNSPEC, RTM_GETDCB,dcb_doit, NULL, NULL);
rtnl_register(PF_UNSPEC,RTM_SETDCB, dcb_doit, NULL, NULL);
lldpad中就是通过RTM_GETDCB和RTM_SETDCB这两种消息类型来寻址的:
static struct nlmsghdr *start_msg(__u16 msg_type, __u8 arg) { struct nlmsghdr *nlh; struct dcbmsg *d; struct ifinfomsg *ifi; /* nlh needs to be free‘d by send_msg() */ nlh = (struct nlmsghdr *)malloc(MAX_MSG_SIZE); if (NULL == nlh) return NULL; memset((void *)nlh, 0, MAX_MSG_SIZE); nlh->nlmsg_type = msg_type; nlh->nlmsg_flags = NLM_F_REQUEST; nlh->nlmsg_seq = next_rtseq(); nlh->nlmsg_pid = getpid(); switch (msg_type) { case RTM_GETDCB: case RTM_SETDCB: nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct dcbmsg)); d = NLMSG_DATA(nlh); d->cmd = arg; d->dcb_family = AF_UNSPEC; d->dcb_pad = 0; break; case RTM_GETLINK: nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)); ifi = NLMSG_DATA(nlh); ifi->ifi_family = AF_UNSPEC; ifi->ifi_index = arg; ifi->ifi_change = 0xffffffff; break; default: free(nlh); nlh = NULL; break; } return nlh; } |
这个函数被调用的情况:
Lldp_dcbx_nl.c:static struct nlmsghdr *start_msg(__u16 msg_type, __u8 arg) Lldp_dcbx_nl.c:/* free‘s nlh which was allocated by start_msg */ Lldp_dcbx_nl.c: nlh = start_msg(RTM_GETDCB, DCB_CMD_GSTATE); Lldp_dcbx_nl.c: nlh = start_msg(RTM_SETDCB, DCB_CMD_SSTATE); Lldp_dcbx_nl.c: nlh = start_msg(RTM_SETDCB, DCB_CMD_PFC_SCFG); Lldp_dcbx_nl.c: nlh = start_msg(RTM_SETDCB, DCB_CMD_PFC_SSTATE); Lldp_dcbx_nl.c: nlh = start_msg(RTM_SETDCB, cmd); Lldp_dcbx_nl.c: nlh = start_msg(RTM_SETDCB, DCB_CMD_SDCBX); Lldp_dcbx_nl.c: nlh = start_msg(RTM_GETDCB, DCB_CMD_GCAP); Lldp_dcbx_nl.c: nlh = start_msg(RTM_GETDCB, DCB_CMD_GNUMTCS); Lldp_dcbx_nl.c: nlh = start_msg(RTM_SETDCB, DCB_CMD_SAPP); Lldp_dcbx_nl.c: nlh = start_msg(RTM_SETDCB, DCB_CMD_SET_ALL); Nltest.c (test):static struct nlmsghdr *start_msg(__u16 msg_type, __u8 arg) Nltest.c (test): nlh = start_msg(RTM_SETDCB, DCB_CMD_SSTATE); Nltest.c (test): nlh = start_msg(RTM_GETDCB, DCB_CMD_GSTATE); Nltest.c (test): nlh = start_msg(RTM_GETDCB, DCB_CMD_PFC_GCFG); Nltest.c (test): nlh = start_msg(RTM_GETDCB, DCB_CMD_PFC_GSTATE); Nltest.c (test): nlh = start_msg(RTM_GETDCB, cmd); Nltest.c (test): nlh = start_msg(RTM_GETDCB, DCB_CMD_GPERM_HWADDR); Nltest.c (test): nlh = start_msg(RTM_GETDCB, DCB_CMD_GCAP); Nltest.c (test): nlh = start_msg(RTM_SETDCB, DCB_CMD_SNUMTCS); Nltest.c (test): nlh = start_msg(RTM_GETDCB, DCB_CMD_GNUMTCS); Nltest.c (test): nlh = start_msg(CMD, DCB_CMD_SET_ALL); Nltest.c (test): nlh = start_msg(RTM_GETDCB, DCB_CMD_BCN_GCFG); Nltest.c (test): nlh = start_msg(RTM_SETDCB, DCB_CMD_BCN_SCFG); Nltest.c (test): nlh = start_msg(RTM_GETDCB, DCB_CMD_GAPP); Nltest.c (test): nlh = start_msg(RTM_SETDCB, DCB_CMD_SAPP); Nltest.c (test): nlh = start_msg(RTM_SETDCB, DCB_CMD_IEEE_SET); Nltest.c (test): nlh = start_msg(RTM_GETDCB, DCB_CMD_IEEE_GET); Nltest.c (test): printf("start_msg failed\n"); Nltest.c (test): nlh = start_msg(RTM_GETLINK, ifindex); |
序列号:这是一个消息的序列号。这个和flag的#define NLM_F_ACK在一起使用的话才非常有用(用户程序想要验证请求正确的发出去了)。
端口ID:这是Netlink分配的一个ID,不同的值代表不同的socket通道,默认的值是进程PID。在某些情况下,这个值被设置为0:比如消息来自内核空间,或者想要Netlink来设置这个值。
一些现有的linux内核子系统也可以增加一些额外的固定大小的头部在Netlink头部的后面,使得Netlink bus的多路复用。这就是GeNetlink.
Netlink message的负载时由很多的属性组成的,使用TLV结构呢,哈哈哈。每一个netlink的属性头部使用struct nlattr定义,包括T,L,V字段。TLV(type-length-value)在8914的FIP帧里面就封装了这个。这个结构使得可以创建新的属性,而不必打破内核接口的后向兼容性的优势。
struct nlattr { __u16 nla_len; __u16 nla_type; }; |
是否使用TLV,取决于程序员,不过使用了就明显更灵活啦。只要在内核模块中添加属性,然后更新用户程序的特性就可以了。
为了更方便的使用,TLV还有内嵌结构呢。。。
参考文献
http://en.wikipedia.org/wiki/Netlink
http://qos.ittc.ku.edu/netlink/netlink.pdf
http://1984.lsi.us.es/~pablo/docs/spae.pdf
Pablo Neira Ayuso?,?1 , Rafael M.Gasca1 and Laurent Lefevre. Communicating between the kernel and user-space in Linuxusing Netlink sockets.2010.
Linux3.2的内核源码
p.s:由于时间和经历关系,此文描述得还不够通透,有时间再补充。