Linux Netlink机制:现代网络通信的核心
在Linux的广袤世界里,进程间通信(IPC)是一个核心话题,它就像是系统的神经系统,让不同的进程能够交流协作,共同维持系统的正常运转。而在众多的 IPC 机制中,Netlink 犹如一把秘密武器,发挥着独特而关键的作用。传统的进程间通信方式,如管道、消息队列、共享内存等,各自有着特定的应用场景和局限性。管道虽然简单易用,但它是半双工的,数据只能单向流动,并且通常只适用于具有亲缘关系的进程之间;消息队列可以实现消息的异步传递,但它的效率相对较低,不太适合大量数据的传输;共享内存虽然速度快,但需要复杂的同步机制来保证数据的一致性。
Netlink 的出现,为 Linux 系统中的进程间通信带来了新的活力。它主要用于内核空间与用户空间的进程之间的通信,为两者搭建了一座高效沟通的桥梁。与传统的系统调用相比,Netlink 提供了更为灵活且高效的通讯方式,特别适用于在多线程环境中进行大量数据传输。在网络配置、路由管理、设备驱动等众多场景中,Netlink 都有着广泛的应用。
比如,我们日常使用的网络配置工具,就是通过 Netlink 与内核进行交互,实现对网络接口的配置和管理;路由守护进程(routed)也依赖 Netlink 来获取和更新路由信息,确保网络数据包能够正确地转发。Netlink 之所以能够在这些复杂的场景中发挥重要作用,得益于它的一些独特优势。接下来,就让我们深入探索 Netlink 的奥秘,揭开它神秘的面纱。
一、什么是Netlink?
Netlink 套接字是实现用户进程与内核进程通信的一种特殊的进程间通信(IPC)方式,也是网络应用程序与内核通信的最常用接口 。它是 Linux 所特有的一种特殊 socket,类似于 BSD 中的 AF_ROUTE,但功能远比其强大。在最新的 Linux 内核(2.6.14 及以后版本)中,有众多应用借助 netlink 实现应用与内核的通信。
从本质上讲,Netlink 是一种在内核与用户应用间进行双向数据传输的机制。用户态应用可以使用标准的 socket API 来利用 netlink 提供的强大功能,而内核态则需要使用专门的内核 API 来操作 netlink。这就好比是两个不同世界的居民,使用着不同的语言(API),却通过 Netlink 这座桥梁能够顺畅地交流。
Netlink 的通信基于 BSD socket 和 AF_NETLINK 地址簇,采用 32 位的端口号寻址(以前也称作 PID) 。每个 Netlink 协议(也可称作总线,在 man 手册中被称为 netlink family),通常都与一个或一组内核服务 / 组件紧密相关联。例如,NETLINK_ROUTE 专门用于获取和设置路由与链路信息,当我们需要配置网络路由时,相关的用户空间程序就会通过 NETLINK_ROUTE 协议与内核进行交互;NETLINK_KOBJECT_UEVENT 则用于内核向用户空间的 udev 进程发送通知,在设备热插拔等事件发生时,内核就会通过这个协议将事件信息传递给用户空间,以便 udev 进程做出相应的处理。
一般来说用户空间和内核空间的通信方式有三种:/proc、ioctl、Netlink。而前两种都是单向的,而Netlink可以实现双工通信。
Netlink 相对于系统调用,ioctl 以及 /proc 文件系统而言具有以下优点:
-
为了使用 netlink,用户仅需要在 include/linux/netlink.h 中增加一个新类型的 netlink 协议定义即可, 如 #define NETLINK_MYTEST 17 然后,内核和用户态应用就可以立即通过 socket API 使用该 netlink 协议类型进行数据交换。但系统调用需要增加新的系统调用,ioctl 则需要增加设备或文件, 那需要不少代码,proc 文件系统则需要在 /proc 下添加新的文件或目录,那将使本来就混乱的 /proc 更加混乱。
-
netlink是一种异步通信机制,在内核与用户态应用之间传递的消息保存在socket缓存队列中,发送消息只是把消息保存在接收者的socket的接 收队列,而不需要等待接收者收到消息,但系统调用与 ioctl 则是同步通信机制,如果传递的数据太长,将影响调度粒度。
-
使用 netlink 的内核部分可以采用模块的方式实现,使用 netlink 的应用部分和内核部分没有编译时依赖,但系统调用就有依赖,而且新的系统调用的实现必须静态地连接到内核中,它无法在模块中实现,使用新系统调用的应用在编译时需要依赖内核。
-
netlink 支持多播,内核模块或应用可以把消息多播给一个netlink组,属于该neilink 组的任何内核模块或应用都能接收到该消息,内核事件向用户态的通知机制就使用了这一特性,任何对内核事件感兴趣的应用都能收到该子系统发送的内核事件,在 后面的文章中将介绍这一机制的使用。
-
内核可以使用 netlink 首先发起会话,但系统调用和 ioctl 只能由用户应用发起调用。
-
netlink 使用标准的 socket API,因此很容易使用,但系统调用和 ioctl则需要专门的培训才能使用。
Netlink协议基于BSD socket和AF_NETLINK地址簇,使用32位的端口号寻址,每个Netlink协议通常与一个或一组内核服务/组件相关联,如NETLINK_ROUTE用于获取和设置路由与链路信息、NETLINK_KOBJECT_UEVENT用于内核向用户空间的udev进程发送通知等。
二、用户态数据结构
用户态应用使用标准的socket APIs, socket(), bind(), sendmsg(), recvmsg() 和 close() 就能很容易地使用 netlink socket,查询手册页可以了解这些函数的使用细节,本文只是讲解使用 netlink 的用户应该如何使用这些函数。注意,使用 netlink 的应用必须包含头文件 linux/netlink.h。当然 socket 需要的头文件也必不可少,sys/socket.h。Netlink通信跟常用UDP Socket通信类似,struct sockaddr_nl是netlink通信地址,跟普通socket struct sockaddr_in类似。
(1)struct sockaddr_nl结构:
struct sockaddr_nl {__kernel_sa_family_t nl_family; /* AF_NETLINK (跟AF_INET对应)*/unsigned short nl_pad; /* zero */__u32 nl_pid; /* port ID (通信端口号)*/__u32 nl_groups; /* multicast groups mask */
};
(2)struct nlmsghd 结构:
/* struct nlmsghd 是netlink消息头*/
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 */
};
nlmsg_type:消息状态,内核在include/uapi/linux/netlink.h中定义了以下4种通用的消息类型,它们分别是:
#define NLMSG_NOOP 0x1 /* Nothing. */
#define NLMSG_ERROR 0x2 /* Error */
#define NLMSG_DONE 0x3 /* End of a dump */
#define NLMSG_OVERRUN 0x4 /* Data lost */
#define NLMSG_MIN_TYPE 0x10 /* < 0x10: reserved control messages */
nlmsg_flags:消息标记,它们用以表示消息的类型,如下:
/* 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 *//* Modifiers to GET request */
#define NLM_F_ROOT 0x100 /* specify tree root */
#define NLM_F_MATCH 0x200 /* return all matching */
#define NLM_F_ATOMIC 0x400 /* atomic GET */
#define NLM_F_DUMP (NLM_F_ROOT|NLM_F_MATCH)/* Modifiers to NEW request */
#define NLM_F_REPLACE 0x100 /* Override existing */
#define NLM_F_EXCL 0x200 /* Do not touch, if it exists */
#define NLM_F_CREATE 0x400 /* Create, if it does not exist */
#define NLM_F_APPEND 0x800 /* Add to end of list */
(3)struct msghdr 结构体
struct iovec { /* Scatter/gather array items */void *iov_base; /* Starting address */size_t iov_len; /* Number of bytes to transfer */};/* iov_base: iov_base指向数据包缓冲区,即参数buff,iov_len是buff的长度。msghdr中允许一次传递多个buff,以数组的形式组织在 msg_iov中,msg_iovlen就记录数组的长度 (即有多少个buff) */struct msghdr {void *msg_name; /* optional address */socklen_t msg_namelen; /* size of address */struct iovec *msg_iov; /* scatter/gather array */size_t msg_iovlen; /* # elements in msg_iov */void *msg_control; /* ancillary data, see below */size_t msg_controllen; /* ancillary data buffer len */int msg_flags; /* flags on received message */};
为了创建一个 netlink socket,用户需要使用如下参数调用 socket():
socket(AF_NETLINK, SOCK_RAW, netlink_type)
第一个参数必须是 AF_NETLINK 或 PF_NETLINK,在 Linux 中,它们俩实际为一个东西,它表示要使用netlink,第二个参数必须是SOCK_RAW或SOCK_DGRAM, 第三个参数指定netlink协议类型,如前面讲的用户自定义协议类型NETLINK_MYTEST, NETLINK_GENERIC是一个通用的协议类型,它是专门为用户使用的,因此,用户可以直接使用它,而不必再添加新的协议类型。内核预定义的协议类 型有:
#define NETLINK_ROUTE 0 /* Routing/device hook */
#define NETLINK_W1 1 /* 1-wire subsystem */
#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
对于每一个netlink协议类型,可以有多达 32多播组,每一个多播组用一个位表示,netlink 的多播特性使得发送消息给同一个组仅需要一次系统调用,因而对于需要多拨消息的应用而言,大大地降低了系统调用的次数。
函数 bind() 用于把一个打开的 netlink socket 与 netlink 源 socket 地址绑定在一起。netlink socket 的地址结构如下:
struct sockaddr_nl
{sa_family_t nl_family;unsigned short nl_pad;__u32 nl_pid;__u32 nl_groups;
};
字段 nl_family 必须设置为 AF_NETLINK 或着 PF_NETLINK,字段 nl_pad 当前没有使用,因此要总是设置为 0,字段 nl_pid 为接收或发送消息的进程的 ID,如果希望内核处理消息或多播消息,就把该字段设置为 0,否则设置为处理消息的进程 ID。字段 nl_groups 用于指定多播组,bind 函数用于把调用进程加入到该字段指定的多播组,如果设置为 0,表示调用者不加入任何多播组。
传递给 bind 函数的地址的 nl_pid 字段应当设置为本进程的进程 ID,这相当于 netlink socket 的本地地址。但是,对于一个进程的多个线程使用 netlink socket 的情况,字段 nl_pid 则可以设置为其它的值,如:
pthread_self() << 16 | getpid();
因此字段 nl_pid 实际上未必是进程 ID,它只是用于区分不同的接收者或发送者的一个标识,用户可以根据自己需要设置该字段。函数 bind 的调用方式如下:
bind(fd, (struct sockaddr*)&nladdr, sizeof(struct sockaddr_nl));
fd为前面的 socket 调用返回的文件描述符,参数 nladdr 为 struct sockaddr_nl 类型的地址。为了发送一个 netlink 消息给内核或其他用户态应用,需要填充目标 netlink socket 地址,此时,字段 nl_pid 和 nl_groups 分别表示接收消息者的进程 ID 与多播组。如果字段 nl_pid 设置为 0,表示消息接收者为内核或多播组,如果 nl_groups为 0,表示该消息为单播消息,否则表示多播消息。使用函数 sendmsg 发送 netlink 消息时还需要引用结构 struct msghdr、struct nlmsghdr 和 struct iovec,结构 struct msghdr 需如下设置:
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);
其中 nladdr 为消息接收者的 netlink 地址,struct nlmsghdr 为 netlink socket 自己的消息头,这用于多路复用和多路分解 netlink 定义的所有协议类型以及其它一些控制,netlink 的内核实现将利用这个消息头来多路复用和多路分解已经其它的一些控制,因此它也被称为netlink 控制块。因此,应用在发送 netlink 消息时必须提供该消息头。
struct nlmsghdr
{__u32 nlmsg_len; /* Length of message */__u16 nlmsg_type; /* Message type*/__u16 nlmsg_flags; /* Additional flags */__u32 nlmsg_seq; /* Sequence number */__u32 nlmsg_pid; /* Sending process PID */
};
字段 nlmsg_len 指定消息的总长度,包括紧跟该结构的数据部分长度以及该结构的大小,字段 nlmsg_type 用于应用内部定义消息的类型,它对 netlink 内核实现是透明的,因此大部分情况下设置为 0,字段 nlmsg_flags 用于设置消息标志,可用的标志包括:
/* 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 */
/* Modifiers to GET request */
#define NLM_F_ROOT 0x100 /* specify tree root */
#define NLM_F_MATCH 0x200 /* return all matching */
#define NLM_F_ATOMIC 0x400 /* atomic GET */
#define NLM_F_DUMP (NLM_F_ROOT|NLM_F_MATCH)
/* Modifiers to NEW request */
#define NLM_F_REPLACE 0x100 /* Override existing */
#define NLM_F_EXCL 0x200 /* Do not touch, if it exists */
#define NLM_F_CREATE 0x400 /* Create, if it does not exist */
#define NLM_F_APPEND 0x800 /* Add to end of list */
-
标志NLM_F_REQUEST用于表示消息是一个请求,所有应用首先发起的消息都应设置该标志。
-
标志NLM_F_MULTI 用于指示该消息是一个多部分消息的一部分,后续的消息可以通过宏NLMSG_NEXT来获得。
-
宏NLM_F_ACK表示该消息是前一个请求消息的响应,顺序号与进程ID可以把请求与响应关联起来。
-
标志NLM_F_ECHO表示该消息是相关的一个包的回传。
-
标志NLM_F_ROOT 被许多 netlink 协议的各种数据获取操作使用,该标志指示被请求的数据表应当整体返回用户应用,而不是一个条目一个条目地返回。有该标志的请求通常导致响应消息设置 NLM_F_MULTI标志。注意,当设置了该标志时,请求是协议特定的,因此,需要在字段 nlmsg_type 中指定协议类型。
-
标志 NLM_F_MATCH 表示该协议特定的请求只需要一个数据子集,数据子集由指定的协议特定的过滤器来匹配。
-
标志 NLM_F_ATOMIC 指示请求返回的数据应当原子地收集,这预防数据在获取期间被修改。
-
标志 NLM_F_DUMP 未实现。
-
标志 NLM_F_REPLACE 用于取代在数据表中的现有条目。
-
标志 NLM_F_EXCL_ 用于和 CREATE 和 APPEND 配合使用,如果条目已经存在,将失败。
-
标志 NLM_F_CREATE 指示应当在指定的表中创建一个条目。
-
标志 NLM_F_APPEND 指示在表末尾添加新的条目。
内核需要读取和修改这些标志,对于一般的使用,用户把它设置为 0 就可以,只是一些高级应用(如 netfilter 和路由 daemon 需要它进行一些复杂的操作),字段 nlmsg_seq 和 nlmsg_pid 用于应用追踪消息,前者表示顺序号,后者为消息来源进程 ID。下面是一个示例:
#define MAX_MSGSIZE 1024
char buffer[] = "An example message";
struct nlmsghdr nlhdr;
nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_MSGSIZE));
strcpy(NLMSG_DATA(nlhdr),buffer);
nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));
nlhdr->nlmsg_pid = getpid(); /* self pid */
nlhdr->nlmsg_flags = 0;
结构 struct iovec 用于把多个消息通过一次系统调用来发送,下面是该结构使用示例:
struct iovec iov;
iov.iov_base = (void *)nlhdr;
iov.iov_len = nlh->nlmsg_len;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
在完成以上步骤后,消息就可以通过下面语句直接发送:
sendmsg(fd, &msg, 0);
应用接收消息时需要首先分配一个足够大的缓存来保存消息头以及消息的数据部分,然后填充消息头,添完后就可以直接调用函数 recvmsg() 来接收。
#define MAX_NL_MSG_LEN 1024
struct sockaddr_nl nladdr;
struct msghdr msg;
struct iovec iov;
struct nlmsghdr * nlhdr;
nlhdr = (struct nlmsghdr *)malloc(MAX_NL_MSG_LEN);
iov.iov_base = (void *)nlhdr;
iov.iov_len = MAX_NL_MSG_LEN;
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
recvmsg(fd, &msg, 0);
注意:fd为socket调用打开的netlink socket描述符,在消息接收后,nlhdr指向接收到的消息的消息头,nladdr保存了接收到的消息的目标地址,宏NLMSG_DATA(nlhdr)返回指向消息的数据部分的指针。
在linux/netlink.h中定义了一些方便对消息进行处理的宏,这些宏包括:
#define NLMSG_ALIGNTO 4
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
宏NLMSG_ALIGN(len)用于得到不小于len且字节对齐的最小数值。
#define NLMSG_LENGTH(len) ((len)+NLMSG_ALIGN(sizeof(struct nlmsghdr)))
宏NLMSG_LENGTH(len)用于计算数据部分长度为len时实际的消息长度。它一般用于分配消息缓存。
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
宏NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字节对齐的最小数值,它也用于分配消息缓存。
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
宏NLMSG_DATA(nlh)用于取得消息的数据部分的首地址,设置和读取消息数据部分时需要使用该宏。
#define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \(struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))
宏NLMSG_NEXT(nlh,len)用于得到下一个消息的首地址,同时len也减少为剩余消息的总长度,该宏一般在一个消息被分成几个部分发送或接收时使用。
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \(nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \(nlh)->nlmsg_len <= (len))
宏NLMSG_OK(nlh,len)用于判断消息是否有len这么长。
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))
宏NLMSG_PAYLOAD(nlh,len)用于返回payload的长度,函数close用于关闭打开的netlink socket。
三、netlink内核数据结构
(1)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 /* Unused number, formerly ip_queue */
#define NETLINK_SOCK_DIAG 4 /* 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 NETLINK_INET_DIAG NETLINK_SOCK_DIAG#define MAX_LINKS 32
(2)netlink常用宏:
#define NLMSG_ALIGNTO 4U
/* 宏NLMSG_ALIGN(len)用于得到不小于len且字节对齐的最小数值 */
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )/* Netlink 头部长度 */
#define NLMSG_HDRLEN ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))/* 计算消息数据len的真实消息长度(消息体 + 消息头)*/
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)/* 宏NLMSG_SPACE(len)返回不小于NLMSG_LENGTH(len)且字节对齐的最小数值 */
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))/* 宏NLMSG_DATA(nlh)用于取得消息的数据部分的首地址,设置和读取消息数据部分时需要使用该宏 */
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))/* 宏NLMSG_NEXT(nlh,len)用于得到下一个消息的首地址, 同时len 变为剩余消息的长度 */
#define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \(struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))/* 判断消息是否 >len */
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \(nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \(nlh)->nlmsg_len <= (len))/* NLMSG_PAYLOAD(nlh,len) 用于返回payload的长度*/
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))
(3)netlink 内核常用函数
netlink_kernel_create内核函数用于创建内核socket与用户态通信
static inline struct sock *
netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
/* net: net指向所在的网络命名空间, 一般默认传入的是&init_net(不需要定义); 定义在net_namespace.c(extern struct net init_net);unit:netlink协议类型cfg:cfg存放的是netlink内核配置参数(如下)
*//* optional Netlink kernel configuration parameters */
struct netlink_kernel_cfg {unsigned int groups; unsigned int flags; void (*input)(struct sk_buff *skb); /* input 回调函数 */struct mutex *cb_mutex; void (*bind)(int group); bool (*compare)(struct net *net, struct sock *sk);
};
(4)单播netlink_unicast() 和 多播netlink_broadcast()
/* 发送单播消息 */
extern int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock);
/*ssk: netlink socket skb: skb buff 指针portid:通信的端口号nonblock:表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函数在没有接收缓存可利用定时睡眠
*//* 发送多播消息 */
extern int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, __u32 portid,__u32 group, gfp_t allocation);
/* ssk: 同上(对应netlink_kernel_create 返回值)、skb: 内核skb buffportid:端口idgroup: 是所有目标多播组对应掩码的"OR"操作的合值。allocation: 指定内核内存分配方式,通常GFP_ATOMIC用于中断上下文,而GFP_KERNEL用于其他场合。这个参数的存在是因为该API可能需要分配一个或多个缓冲区来对多播消息进行clone
*/
四、netlink内核API
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就可以:
#define NETLINK_MYTEST 17
只要增加这个定义之后,用户就可以在内核的任何地方引用该协议,在内核中,为了创建一个netlink socket用户需要调用如下函数:
struct sock *netlink_kernel_create(
int unit, void (*input)(struct sock *sk, int len));
参数unit表示netlink协议类型,如NETLINK_MYTEST,参数input则为内核模块定义的netlink消息处理函数,当有消 息到达这个netlink socket时,该input函数指针就会被引用。函数指针input的参数sk实际上就是函数netlink_kernel_create返回的 struct sock指针,sock实际是socket的一个内核表示数据结构,用户态应用创建的socket在内核中也会有一个struct sock结构来表示。下面是一个input函数的示例:
void input (struct sock *sk, int len)
{struct sk_buff *skb;struct nlmsghdr *nlh = NULL;u8 *data = NULL;while ((skb = skb_dequeue(&sk->receive_queue)) != NULL) {/* process netlink message pointed by skb->data */nlh = (struct nlmsghdr *)skb->data;data = NLMSG_DATA(nlh);/* process netlink message with header pointed by * nlh and data pointed by data*/}
}
函数input()会在发送进程执行sendmsg()时被调用,这样处理消息比较及时,但是,如果消息特别长时,这样处理将增加系统调用 sendmsg()的执行时间,对于这种情况,可以定义一个内核线程专门负责消息接收,而函数input的工作只是唤醒该内核线程,这样sendmsg将 很快返回。
函数skb = skb_dequeue(&sk->receive_queue)用于取得socket sk的接收队列上的消息,返回为一个struct sk_buff的结构,skb->data指向实际的netlink消息。
函数skb_recv_datagram(nl_sk)也用于在netlink socket nl_sk上接收消息,与skb_dequeue的不同指出是,如果socket的接收队列上没有消息,它将导致调用进程睡眠在等待队列 nl_sk->sk_sleep,因此它必须在进程上下文使用,刚才讲的内核线程就可以采用这种方式来接收消息。
下面的函数input就是这种使用的示例:
void input (struct sock *sk, int len)
{wake_up_interruptible(sk->sk_sleep);
}
当内核中发送netlink消息时,也需要设置目标地址与源地址,而且内核中消息是通过struct sk_buff来管理的, linux/netlink.h中定义了一个宏:
#define NETLINK_CB(skb) (*(struc
t netlink_skb_parms*)&((skb)->cb))
来方便消息的地址设置。下面是一个消息地址设置的例子:
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_unicast 来发送单播消息:
int netlink_unicast(
struct sock *sk, struct sk_buff *skb, u32 pid, int nonblock);
参数sk为函数netlink_kernel_create()返回的socket,参数skb存放消息,它的data字段指向要发送的 netlink消息结构,而skb的控制块保存了消息的地址信息,前面的宏NETLINK_CB(skb)就用于方便设置该控制块, 参数pid为接收消息进程的pid,参数nonblock表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函 数在没有接收缓存可利用时睡眠。
内核模块或子系统也可以使用函数netlink_broadcast来发送广播消息:
void netlink_broadcast(
struct sock *sk, struct sk_bu
ff *skb, u32 pid, u32 group, int allocation
);
前面的三个参数与netlink_unicast相同,参数group为接收消息的多播组,该参数的每一个代表一个多播组,因此如果发送给多个多播 组,就把该参数设置为多个多播组组ID的位或。参数allocation为内核内存分配类型,一般地为GFP_ATOMIC或 GFP_KERNEL,GFP_ATOMIC用于原子的上下文(即不可以睡眠),而GFP_KERNEL用于非原子上下文。
在内核中使用函数sock_release来释放函数netlink_kernel_create()创建的netlink socket:
void sock_release(struct socket * sock);
注意函数netlink_kernel_create()返回的类型为struct sock,因此函数sock_release应该这种调用:
sock_release(sk->sk_socket);
sk为函数netlink_kernel_create()的返回值。在源代码包中 给出了一个使用 netlink 的示例,它包括一个内核模块 netlink-exam-kern.c 和两个应用程序 netlink-exam-user-recv.c, netlink-exam-user-send.c。内核模块必须先插入到内核,然后在一个终端上运行用户态接收程序,在另一个终端上运行用户态发送程 序,发送程序读取参数指定的文本文件并把它作为 netlink 消息的内容发送给内核模块,内核模块接受该消息保存到内核缓存中,它也通过proc接口出口到 procfs,因此用户也能够通过 /proc/netlink_exam_buffer 看到全部的内容,同时内核也把该消息发送给用户态接收程序,用户态接收程序将把接收到的内容输出到屏幕上。
五、Netlink性能优势
(1)异步通信,效率飞升
Netlink 是一种异步通信机制 ,这是它的一大亮点。在异步通信中,当发送方发送消息时,消息会被暂存在 socket 接收缓存中,发送方无需等待接收者立即处理消息,就可以继续执行其他任务。就好比我们寄快递,把包裹交给快递员后,我们不用一直等着包裹被收件人签收,就可以去做别的事情了。
与之相对的同步通信,就像打电话,在对方接听并回应之前,我们只能干等着,啥也做不了。在系统调用和 ioctl 这类同步通信机制中,如果传递的数据量较大或者接收方处理速度较慢,发送方就会被阻塞,这无疑会影响整个系统的调度粒度和效率。而 Netlink 的异步通信机制避免了这种等待,大大提升了系统的效率,使得系统能够更高效地处理多个任务。
(2)全双工通信,双向奔赴
全双工通信是指在通信的任意时刻,线路上可以同时存在 A 到 B 和 B 到 A 的双向信号传输。Netlink 就支持全双工通信,这意味着在内核与用户空间之间,数据能够同时在两个方向上传输 。例如,当用户空间的网络配置工具向内核发送配置请求时,内核可以同时将网络状态信息反馈给用户空间,双方的交流就像两个人面对面交谈一样顺畅,无需等待一方说完另一方才能开口。
这种双向数据传输的能力,使得内核和用户空间能够及时地交换信息,极大地增强了系统的交互性和响应速度。在网络监控场景中,用户空间的监控程序可以实时向内核询问网络流量等信息,内核也能随时将新的网络事件通知给监控程序,确保监控的实时性和准确性。
(3)多播功能,一对多的狂欢
Netlink 支持多播功能,这为它在一些特定场景下的应用提供了强大的支持。通过多播,内核模块或应用可以把消息发送给一个 Netlink 组,属于该组的任何内核模块或应用都能接收到该消息 。每个 Netlink 协议类型最多可以有 32 个多播组,每个多播组用一个位表示,发送消息给同一个组仅需要一次系统调用,大大降低了系统调用的次数。
在网络管理中,当内核检测到网络拓扑发生变化时,它可以通过多播将这一消息同时发送给多个关注网络状态的用户空间进程,如网络监控程序、路由守护进程等,这些进程就能及时做出相应的调整,而不需要内核分别向每个进程单独发送消息,大大提高了信息传递的效率。
(4)简单易用,上手轻松
对于开发者来说,Netlink 的易用性也是它的一大优势。用户态应用可以使用标准的 socket API,如 socket ()、bind ()、sendmsg ()、recvmsg () 和 close () 等函数来使用 Netlink socket 。这对于熟悉 socket 编程的开发者来说,几乎没有学习成本,能够快速上手进行 Netlink 相关的开发。不需要像使用系统调用和 ioctl 那样,需要专门的培训才能使用,降低了开发的门槛,使得开发者能够更专注于业务逻辑的实现。
(5)无编译依赖,灵活部署
使用 Netlink 的内核部分可以采用模块的方式实现,并且使用 Netlink 的应用部分和内核部分没有编译时依赖 。这意味着在开发过程中,我们可以独立地对内核模块和用户空间应用进行开发、调试和更新,而不需要因为一方的改动而重新编译另一方。当我们需要更新内核模块的功能时,只需要重新编译内核模块并加载,而用户空间应用无需重新编译就可以继续使用;反之,当用户空间应用进行功能升级时,也不会影响到内核模块。这种灵活性大大提高了开发和部署的效率,使得系统的维护和升级更加便捷。
六、Netlink常用场景
6.1网络配置与管理
在网络配置与管理领域,Netlink 堪称一把 “瑞士军刀”,发挥着举足轻重的作用。
在网络接口管理方面,Netlink 为用户空间程序与内核之间搭建了一座沟通的桥梁,使得对网络接口的各种操作变得轻而易举。通过 Netlink,我们可以创建、删除虚拟接口,就像在搭建一个虚拟网络世界时,能够自由地添加或移除各种 “虚拟桥梁” 。比如,在容器网络中,经常会用到 veth 对(虚拟以太网设备对)来实现容器与外部网络的通信,而创建 veth 对就是通过 Netlink 与内核交互完成的。我们还能灵活地配置接口属性,像设置 MTU(最大传输单元),就如同为网络传输的 “通道” 设定合适的宽度,确保数据能够高效传输;修改 MAC 地址,就像是给网络设备换了一个独特的 “身份标识”;以及启用或禁用接口,控制网络设备的 “开关” 。在企业网络中,当需要调整网络架构时,管理员可以利用 Netlink 相关工具,快速地对网络接口进行配置,保障网络的稳定运行。
路由表更新也是 Netlink 的重要应用场景之一。它能够帮助我们添加、删除路由条目,如同在网络的 “地图” 上标记或抹去特定的路径 。当我们要设置静态路由,让数据包按照指定的路线传输时,就可以借助 Netlink 向内核发送相应的消息。例如,在一个拥有多个子网的企业网络中,为了实现不同子网之间的通信,管理员可以通过 Netlink 添加静态路由条目。查询当前路由表状态也不在话下,通过 Netlink,我们能随时获取路由表的信息,了解网络的 “交通路线” 状况 。这对于网络故障排查非常重要,当网络出现连接问题时,管理员可以通过查询路由表,判断数据包的传输路径是否正确,从而快速定位问题所在。
在实际应用中,有许多强大的工具都依赖 Netlink 来实现网络配置与管理功能。其中,iproute2 工具集就是一个典型代表 。它包含了众多实用的命令,如 ip addr 用于管理网络接口地址,ip route 用于操作路由表等。这些命令底层都是通过 Netlink 与内核进行交互,替代了传统的 ifconfig 和 route 命令。当我们执行 “ip route add 192.168.2.0/24 via 10.0.0.1” 这条命令时,实际上是通过 NETLINK_ROUTE 协议向内核发送 RTM_NEWROUTE 消息,告知内核添加一条到 192.168.2.0/24 网络的路由,下一跳为 10.0.0.1 。还有一些网络自动化配置工具,在大规模网络部署中,通过调用 Netlink 接口,能够快速、批量地对网络设备进行配置,大大提高了网络部署的效率。
6.2系统监控与安全
Netlink 在系统监控与安全领域同样有着不可忽视的作用。
在系统资源监控方面,Netlink 为用户空间的监控工具打开了一扇通往内核信息宝库的大门。通过 Netlink,监控工具可以实时获取系统资源的使用情况,如 CPU 使用率、内存占用、磁盘 I/O 等 。这就好比我们在驾驶汽车时,仪表盘上的各种指针和数据能够实时反馈汽车的运行状态,让我们随时了解车辆的情况。以 top 命令为例,它能够动态显示系统中各个进程的资源占用情况,而这背后就离不开 Netlink 的支持。top 命令通过 Netlink 与内核通信,获取进程的相关信息,然后进行整理和展示,让用户对系统的运行状态一目了然。在服务器运维中,管理员可以利用基于 Netlink 的监控工具,实时监控服务器的资源使用情况,当发现资源使用率过高时,及时采取措施进行优化,保障服务器的稳定运行。
在安全策略管理方面,Netlink 扮演着重要的角色。它为用户空间的安全工具与内核之间的通信提供了通道,使得安全策略的管理和配置变得更加高效 。防火墙是保障网络安全的重要防线,用户空间的防火墙配置工具(如 iptables、nftables)可以通过 Netlink 与内核中的 netfilter 模块进行通信,实现对网络数据包的过滤和安全策略的设置 。我们可以通过这些工具,根据实际需求制定规则,允许或禁止特定的网络连接,就像在城堡的大门设置守卫,对进出的人员进行严格的检查和筛选。入侵检测系统(IDS)和入侵防御系统(IPS)也可以利用 Netlink 获取网络数据包和系统状态信息,及时发现并阻止潜在的安全威胁 。当 IDS 检测到异常的网络流量时,它可以通过 Netlink 向内核发送相关信息,触发相应的防御机制,保障系统的安全。
6.3内核与用户空间交互
Netlink 作为内核与用户空间通信的桥梁,其重要性不言而喻,在众多场景中都发挥着关键作用。
设备驱动与用户空间程序的交互是一个常见的场景。设备驱动在内核空间负责与硬件设备进行通信,而用户空间程序则需要与设备驱动交互,以实现对硬件设备的控制和数据传输 。以网络设备驱动为例,当用户空间的网络应用程序需要发送或接收网络数据包时,它会通过 Netlink 与网络设备驱动进行通信。用户空间程序将数据包发送给设备驱动,设备驱动再将数据包发送到物理网络上;反之,当设备驱动接收到来自网络的数据包时,也会通过 Netlink 将数据包传递给用户空间程序 。这就像在一个工厂中,生产线上的工人(设备驱动)与管理人员(用户空间程序)需要密切配合,通过特定的沟通渠道(Netlink)来协调工作,确保生产的顺利进行。
系统服务与内核之间的通信也经常依赖 Netlink。许多系统服务,如网络服务、存储服务等,需要与内核进行交互,获取系统资源或执行特定的操作 。在网络服务中,DHCP(动态主机配置协议)服务器需要与内核通信,获取网络接口信息,为客户端分配 IP 地址 。这一过程中,DHCP 服务器就会通过 Netlink 向内核发送请求,内核响应请求并返回相关信息,实现了系统服务与内核之间的信息交互,保障了网络服务的正常运行。
七、Netlink 工作原理
Netlink 的架构就像是一个精心构建的通信网络,各个部分协同工作,实现了内核与用户空间之间高效的通信。我们来看下面这张:
从图中可以看出,Netlink 主要由以下几个部分组成:
-
用户空间应用:这是我们日常使用的各种应用程序,它们通过标准的 socket API 与 Netlink 套接字进行交互。比如我们前面提到的网络配置工具、系统监控程序等,它们通过 Netlink 向内核发送请求,获取系统信息或者执行特定的操作。
-
Netlink 套接字:作为用户空间与内核空间通信的桥梁,Netlink 套接字负责在两者之间传递数据。它基于 BSD socket 和 AF_NETLINK 地址簇,采用 32 位的端口号寻址 。每个 Netlink 套接字都有一个对应的协议类型,用于标识通信的内容和目的。
-
内核空间:内核是 Linux 系统的核心,它包含了各种设备驱动、网络协议栈等重要组件。内核通过 Netlink 与用户空间进行通信,接收用户空间的请求并返回相应的结果,同时也可以主动向用户空间发送通知和事件信息。
-
Netlink 协议族:Netlink 支持多种协议类型,每种协议类型都与特定的内核服务或组件相关联。例如,NETLINK_ROUTE 用于网络路由相关的操作,NETLINK_KOBJECT_UEVENT 用于内核向用户空间发送设备事件通知等 。不同的协议类型使得 Netlink 能够满足各种不同的通信需求。
7.1Netlink 协议类型全解析
Netlink 协议族丰富多样,目前支持 32 种协议类型 ,它们在不同的场景中发挥着关键作用。以下是一些常见的 Netlink 协议类型及其用途:
NETLINK_ROUTE:这是最为常用的协议类型之一,主要用于网络路由和设备相关的操作 。通过它,我们可以获取和设置路由信息,就像为网络数据包规划行进的路线;管理网络接口,如创建、删除接口,配置接口属性等,相当于对网络的 “出入口” 进行管控;还能监控网络状态,实时掌握网络的运行情况 。在企业网络中,网络管理员经常会使用基于 NETLINK_ROUTE 的工具来配置和管理网络路由,确保网络的高效运行。
NETLINK_SOCK_DIAG:主要用于监控系统中的套接字信息 。它可以获取套接字的状态,比如是处于监听状态、连接状态还是关闭状态;查看套接字选项,了解套接字的各种配置参数 。在网络故障排查中,NETLINK_SOCK_DIAG 非常有用,管理员可以通过它来检查网络连接是否正常,是否存在套接字资源泄漏等问题。例如,当我们发现某个网络应用无法正常连接时,可以使用基于 NETLINK_SOCK_DIAG 的工具来查看相关套接字的状态,找出问题所在。
NETLINK_NFLOG:是 netfilter/iptables ULOG 的通信接口,在防火墙和网络安全领域有着重要应用 。它允许用户空间的防火墙工具(如 iptables)与内核中的 netfilter 模块进行通信,实现对网络数据包的过滤和日志记录 。当有网络数据包进入系统时,netfilter 模块会根据预设的规则对数据包进行检查,然后通过 NETLINK_NFLOG 将相关信息(如数据包是否被允许通过、被丢弃的原因等)传递给用户空间的防火墙工具,以便进行进一步的处理和分析。在企业网络安全防护中,管理员可以利用 NETLINK_NFLOG 来监控网络流量,及时发现和阻止潜在的安全威胁。
NETLINK_KOBJECT_UEVENT:用于内核向用户空间发送设备事件通知,是内核热插拔机制的基础 。当有设备插入或拔出系统时,内核会通过 NETLINK_KOBJECT_UEVENT 协议向用户空间发送相应的事件消息 。用户空间的 udev 进程接收到这些消息后,会根据设备的属性和规则,自动完成设备的识别、驱动加载等操作。在我们日常使用的电脑中,当插入 U 盘时,系统能够自动识别并挂载 U 盘,这背后就离不开 NETLINK_KOBJECT_UEVENT 的支持。
NETLINK_GENERIC:作为一种通用的 Netlink 协议类型,它就像是一个灵活的 “瑞士军刀”,为用户提供了自定义协议的能力 。当现有的协议类型无法满足特定需求时,用户可以基于 NETLINK_GENERIC 定义自己的子协议类型 。在一些特定的行业应用中,可能需要与内核进行特定的数据交互,此时就可以利用 NETLINK_GENERIC 来实现自定义的通信协议,满足业务的特殊需求。
7.2数据结构与函数
在 Netlink 的世界里,有一些关键的数据结构和函数,它们是实现高效通信的基础。
⑴数据结构
struct sockaddr_nl:这是 Netlink 通信地址的数据结构,类似于普通 socket 编程中的 struct sockaddr_in 。它包含以下几个重要成员:
-
nl_family:固定为 AF_NETLINK,用于标识地址族,就像给通信地址贴上一个 “Netlink 专属” 的标签 。
-
nl_pad:目前未使用,通常填充为 0,就像一个暂时闲置的小隔间。
-
nl_pid:表示端口 ID,通常设置为当前进程的进程号,用于唯一标识一个基于 netlink 的 socket 通道 。当用户空间的进程与内核进行通信时,内核可以通过这个 nl_pid 来识别是哪个进程在发送请求。
-
nl_groups:用于指定多播组,是多播组掩码 。每个 Netlink 协议最多支持 32 个多播组,每个多播组用一个 bit 表示。如果进程希望加入某个多播组,就需要设置相应的 bit 位 。在网络监控场景中,多个监控程序可以加入同一个多播组,接收内核发送的网络状态信息。
struct nlmsghdr:这是 Netlink 消息头的数据结构,每个 Netlink 消息都包含这样一个消息头 。它的成员如下:
-
nlmsg_len:表示整个消息的长度,包括消息头和消息体就像一个包裹的总重量包含了包装盒和里面的物品 。
-
nlmsg_type:用于标识消息的类型,比如是数据消息还是控制消息 。内核定义了一些标准的消息类型,如 NLMSG_NOOP(空消息,什么也不做)、NLMSG_ERROR(表示消息中包含错误)、NLMSG_DONE(用于标记消息队列的结束)等 。
-
nlmsg_flags:是消息的附加标志位,用于对消息进行额外的控制 。例如,NLM_F_REQUEST 表示这是一个请求消息,NLM_F_MULTI 表示消息由多个部分组成,最后一个部分会标注 NLMSG_DONE 。
-
nlmsg_seq:是序列号,用于追踪消息的顺序,类似于快递单号,方便接收方对消息进行排序和处理 。
-
nlmsg_pid:表示发送进程的端口 ID,用于接收方识别消息的来源 。
⑵函数
-
socket():在用户空间创建 Netlink 套接字时使用,它的参数包括地址族(AF_NETLINK)、套接字类型(SOCK_RAW 或 SOCK_DGRAM)和协议类型 。就像是在通信网络中搭建一个新的 “通信站点”,指定它的类型和所属的 “通信频道”。例如,int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); 这行代码创建了一个基于 NETLINK_ROUTE 协议的 RAW 类型的 Netlink 套接字。
-
bind():用于将 Netlink 套接字与本地地址绑定,也就是给这个 “通信站点” 确定一个具体的位置 。它的参数包括套接字描述符、本地地址和地址长度 。通过绑定,内核和其他进程就知道该去哪里与这个套接字进行通信。例如,bind(sock_fd, (struct sockaddr*)&nl_addr, sizeof(nl_addr)); 这行代码将之前创建的套接字 sock_fd 与本地地址 nl_addr 进行绑定。
-
sendmsg():用于发送 Netlink 消息,它需要一个 struct msghdr 结构体来指定消息的各种参数,包括目标地址、消息内容等 。就像是把装满信息的 “包裹” 发送出去,告诉快递员(内核或其他进程)要送到哪里。在发送消息前,需要填充好 struct msghdr 结构体的各个成员,然后调用 sendmsg 函数将消息发送出去。
-
recvmsg():用于接收 Netlink 消息,接收方通过这个函数从套接字接收内核或其他进程发送过来的消息 。它就像是在 “通信站点” 等待接收 “包裹”,当有消息到达时,将其接收并进行处理。例如,recvmsg(sock_fd, &msg, 0); 这行代码从套接字 sock_fd 接收消息,并将消息存储在 msg 结构体中。
7.3通信流程大起底
Netlink 的通信流程就像是一场有序的接力赛,每个环节都紧密相连,确保数据能够准确、高效地传输。下面我们来详细了解一下 Netlink 的通信流程:
①消息发送:
-
用户空间应用首先使用 socket () 函数创建一个 Netlink 套接字,并指定协议类型 。比如要进行网络路由相关的操作,就创建一个基于 NETLINK_ROUTE 协议的套接字。
-
然后,应用使用 bind () 函数将套接字与本地地址绑定,确定通信的端点 。
-
接下来,应用构造一个 Netlink 消息,填充 struct nlmsghdr 消息头和消息体 。消息头中设置消息类型、标志位、序列号等信息,消息体中包含具体的数据内容。
-
最后,应用使用 sendmsg () 函数将消息发送出去 。在发送时,需要指定目标地址(如果是发送给内核,nl_pid 和 nl_groups 通常设置为 0;如果是发送给其他进程,则设置为目标进程的 pid 和相应的多播组掩码)。
②消息接收:
-
内核或其他接收方进程在接收到 Netlink 消息后,首先会根据消息头中的信息进行初步处理 。
-
内核会检查消息的合法性,包括消息类型是否正确、消息长度是否符合要求等 。如果消息不合法,会返回错误信息。
-
对于合法的消息,内核会根据消息类型和协议类型进行进一步的处理 。例如,如果是 NETLINK_ROUTE 协议的消息,内核会将其转发到网络路由模块进行处理。
③消息处理:
-
内核或接收方进程根据消息的内容执行相应的操作 。如果是网络配置请求,内核会更新网络配置信息;如果是设备事件通知,内核会通知相关的设备驱动进行处理。
-
处理完成后,内核或接收方进程可能会返回一个响应消息给发送方 。响应消息同样包含消息头和消息体,消息头中会设置相应的标志位和消息类型(如 NLMSG_ACK 表示确认消息,NLMSG_ERROR 表示错误消息)。
-
发送方接收到响应消息后,根据消息内容进行相应的处理 。如果是确认消息,发送方知道自己的请求已经被成功处理;如果是错误消息,发送方会根据错误信息进行调试和修正。
八、Netlink 应用开发实战
8.1准备工作
在开始 Netlink 应用开发之前,我们需要搭建好开发环境,准备好必要的工具。
首先,确保你的开发机器上安装了 Linux 系统,推荐使用较新的版本,如 Ubuntu 20.04 或 CentOS 8,因为它们对 Netlink 的支持更加完善 。如果你还没有安装 Linux 系统,可以通过虚拟机软件(如 VirtualBox 或 VMware)来安装,这就好比在你的电脑里搭建了一个虚拟的 Linux 世界,让你可以在不影响原有系统的情况下进行开发。
接下来,安装开发工具。GCC(GNU Compiler Collection)是 Linux 下常用的编译器,我们需要它来编译我们的代码 。在 Ubuntu 系统中,可以通过以下命令安装:
sudo apt-get update
sudo apt-get install build-essential
在 CentOS 系统中,安装命令如下:
sudo yum groupinstall "Development Tools"
除了 GCC,还需要安装一些开发库,如 libnl。libnl 是一个用于简化 Netlink 编程的库,它提供了一些封装好的函数,让我们可以更方便地使用 Netlink 。在 Ubuntu 系统中,可以通过以下命令安装:
sudo apt-get install libnl-3-dev libnl-genl-3-dev
在 CentOS 系统中,安装命令如下:
sudo yum install libnl3-devel libnl3-genl-devel
8.2用户态编程示例
下面我们来看一个用户态的 Netlink 编程示例,通过这个示例,你将学会如何创建 Netlink 套接字、绑定地址、发送和接收消息。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <linux/netlink.h>#define NETLINK_USER 31 // 自定义Netlink协议号
#define MSG_LEN 1024int main() {// 创建Netlink套接字int sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_USER);if (sock_fd < 0) {perror("socket");return -1;}// 本地地址配置struct sockaddr_nl src_addr;memset(&src_addr, 0, sizeof(src_addr));src_addr.nl_family = AF_NETLINK;src_addr.nl_pid = getpid(); // 绑定到当前进程src_addr.nl_groups = 0; // 不订阅多播// 绑定套接字if (bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr)) < 0) {perror("bind");close(sock_fd);return -1;}// 目标地址配置(内核)struct sockaddr_nl dest_addr;memset(&dest_addr, 0, sizeof(dest_addr));dest_addr.nl_family = AF_NETLINK;dest_addr.nl_pid = 0; // 发送到内核dest_addr.nl_groups = 0; // 不订阅多播// 构造发送消息struct nlmsghdr *nlh = (struct nlmsghdr*)malloc(NLMSG_SPACE(MSG_LEN));memset(nlh, 0, NLMSG_SPACE(MSG_LEN));nlh->nlmsg_len = NLMSG_SPACE(MSG_LEN); // 消息长度nlh->nlmsg_pid = getpid(); // 发送者PIDnlh->nlmsg_flags = 0; // 无特殊标志位strcpy((char*)NLMSG_DATA(nlh), "Hello from user space!"); // 消息内容// 发送消息到内核if (sendto(sock_fd, nlh, nlh->nlmsg_len, 0, (struct sockaddr*)&dest_addr, sizeof(dest_addr)) < 0) {perror("sendto");free(nlh);close(sock_fd);return -1;}printf("Message sent to kernel: %s\n", (char*)NLMSG_DATA(nlh));// 接收内核响应memset(nlh, 0, NLMSG_SPACE(MSG_LEN));if (recv(sock_fd, nlh, NLMSG_SPACE(MSG_LEN), 0) < 0) {perror("recv");free(nlh);close(sock_fd);return -1;}printf("Message received from kernel: %s\n", (char*)NLMSG_DATA(nlh));// 清理资源free(nlh);close(sock_fd);return 0;
}
-
创建 Netlink 套接字:socket(AF_NETLINK, SOCK_RAW, NETLINK_USER) 函数用于创建一个 Netlink 套接字,AF_NETLINK 表示地址族,SOCK_RAW 表示套接字类型为原始套接字,NETLINK_USER 是我们自定义的 Netlink 协议号 。这一步就像是在网络世界中搭建了一个专门用于 Netlink 通信的 “站点”。
-
绑定地址:通过 bind 函数将套接字与本地地址绑定,src_addr.nl_pid = getpid() 将端口 ID 设置为当前进程的进程号,这样内核就知道该套接字属于哪个进程 。就好比给这个 “站点” 贴上了一个独一无二的 “标签”,方便识别。
-
构造发送消息:创建一个 nlmsghdr 结构体来表示消息头,设置消息长度、发送者 PID、标志位等信息,并将消息内容复制到消息体中 。这就像是把要发送的信息装进一个 “包裹”,并填写好收件人和寄件人的信息。
-
发送消息:使用 sendto 函数将消息发送到内核,指定目标地址为内核地址(dest_addr.nl_pid = 0) 。就像是把 “包裹” 寄给内核这个 “收件人”。
-
接收消息:通过 recv 函数接收内核返回的响应消息,并打印出来 。就像是在 “站点” 等待接收内核寄回的 “包裹”,并查看里面的内容。
8.3内核态编程示例
接下来是内核态的 Netlink 编程示例,展示如何在内核模块中创建 Netlink socket、注册回调函数、发送和接收消息。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netlink.h>
#include <linux/skbuff.h>
#include <net/sock.h>#define NETLINK_USER 31
struct sock *nl_sk = NULL;static void netlink_recv_msg(struct sk_buff *skb) {struct nlmsghdr *nlh;int pid;struct sk_buff *skb_out;char *msg = "Hello from kernel!";int msg_size = strlen(msg);int res;// 获取Netlink消息头部nlh = (struct nlmsghdr*)skb->data;printk(KERN_INFO "Kernel received message: %s\n", (char*)NLMSG_DATA(nlh));pid = nlh->nlmsg_pid; // 获取用户进程PID// 构造响应消息skb_out = nlmsg_new(msg_size, 0);if (!skb_out) {printk(KERN_ERR "Failed to allocate new skb\n");return;}nlh = nlmsg_put(skb_out, 0, 0, NLMSG_DONE, msg_size, 0);strncpy(NLMSG_DATA(nlh), msg, msg_size);res = nlmsg_unicast(nl_sk, skb_out, pid); // 发送响应if (res < 0) {printk(KERN_INFO "Error sending message to user\n");}
}static int __init netlink_init(void) {struct netlink_kernel_cfg cfg = {.input = netlink_recv_msg // 注册消息接收回调};nl_sk = netlink_kernel_create(&init_net, NETLINK_USER, &cfg);if (!nl_sk) {printk(KERN_ALERT "Error creating Netlink socket\n");return -10;}printk(KERN_INFO "Netlink module loaded\n");return 0;
}static void __exit netlink_exit(void) {netlink_kernel_release(nl_sk);printk(KERN_INFO "Netlink module unloaded\n");
}module_init(netlink_init);
module_exit(netlink_exit);
MODULE_LICENSE("GPL");
-
创建 Netlink socket 并注册回调函数:netlink_kernel_create 函数用于在内核中创建一个 Netlink socket,并注册一个回调函数 netlink_recv_msg 。当有 Netlink 消息到达时,内核会调用这个回调函数进行处理 。这就像是在内核中设置了一个 “消息接收站”,并指定了处理消息的 “工作人员”。
-
接收消息:在 netlink_recv_msg 函数中,首先获取消息头部,打印接收到的消息内容,并获取发送方的 PID 。这一步就像是 “工作人员” 接收消息,并查看寄件人的信息。
-
构造响应消息:使用 nlmsg_new 函数创建一个新的套接字缓冲区来存放响应消息,然后使用 nlmsg_put 函数填充消息头和消息体 。这就像是 “工作人员” 准备好要回复的 “包裹”,并填写好相关信息。
-
发送响应消息:通过 nlmsg_unicast 函数将响应消息发送回给用户空间的进程,指定目标 PID 为发送方的 PID 。这就像是 “工作人员” 把回复的 “包裹” 寄给寄件人。
8.4测试与调试
在完成代码编写后,我们需要对程序进行测试和调试,以确保其正常运行。
测试方法:
①编译内核模块:将上述内核态代码保存为 netlink_kernel.c 文件,然后使用以下命令编译:
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
编译完成后,会生成 netlink_kernel.ko 文件。
②加载内核模块:使用以下命令加载内核模块:
sudo insmod netlink_kernel.ko
③编译用户态程序:将上述用户态代码保存为 netlink_user.c 文件,然后使用以下命令编译:
gcc -o netlink_user netlink_user.c
④运行用户态程序:执行编译好的用户态程序:
./netlink_user
如果一切正常,你应该能看到用户态程序发送消息到内核,并接收到内核返回的响应消息。
调试技巧:
①打印调试信息:在内核模块中,可以使用 printk 函数打印调试信息 。通过查看内核日志(使用 dmesg 命令),可以了解内核模块的运行情况。比如,在 netlink_recv_msg 函数中,我们使用 printk 打印了接收到的消息内容,这样在调试时就可以清楚地知道内核接收到了什么消息。
②使用 GDB 调试用户态程序:对于用户态程序,可以使用 GDB(GNU Debugger)进行调试 。在编译用户态程序时,加上 -g 选项,生成包含调试信息的可执行文件:
gcc -g -o netlink_user netlink_user.c
然后使用 GDB 启动调试:
gdb netlink_user
在 GDB 中,可以设置断点、单步执行等,帮助我们找出程序中的问题。比如,我们可以在 sendto 函数调用处设置断点,查看发送消息时的参数是否正确。
③检查 Netlink 套接字状态:使用 netstat -anp | grep netlink 命令可以查看 Netlink 套接字的状态,包括是否绑定成功、是否正在监听等 。这对于排查网络连接问题非常有帮助。如果发现套接字没有正确绑定,就需要检查绑定代码和地址配置是否正确。
九、Netlink 使用中的注意事项
9.1错误处理
在 Netlink 通信中,错误处理至关重要。它就像是通信过程中的 “安全卫士”,能够确保通信的稳定性和可靠性。如果忽视错误处理,一旦出现问题,程序可能会出现异常行为,甚至导致系统崩溃。常见的错误类型包括:
套接字创建失败:在使用 socket () 函数创建 Netlink 套接字时,可能会因为系统资源不足、地址族或协议类型错误等原因导致创建失败 。比如,当系统中同时创建的套接字数量过多,超过了系统的限制时,就会出现套接字创建失败的情况。此时,我们需要检查返回值,如果返回值小于 0,说明创建失败,应使用perror函数打印错误信息,以便定位问题所在。
绑定地址失败:bind () 函数用于将套接字与本地地址绑定,如果绑定失败,可能是因为地址已被占用、权限不足等原因 。在一个多进程的系统中,如果多个进程尝试绑定同一个地址,就会出现地址冲突,导致绑定失败。当绑定失败时,同样需要打印错误信息,根据错误提示进行处理,比如更换绑定地址或者提升权限。
消息发送失败:sendmsg () 函数在发送 Netlink 消息时,可能会因为网络故障、目标地址错误、消息长度超过限制等原因导致发送失败 。当网络出现中断时,消息就无法正常发送。如果消息发送失败,我们需要根据具体的错误情况进行处理,比如重新发送消息、检查目标地址等。
消息接收失败:recvmsg () 函数接收消息时,也可能会遇到各种问题,如套接字未正确连接、缓冲区溢出等 。当接收缓冲区的大小小于接收到的消息长度时,就会发生缓冲区溢出,导致消息接收失败。在这种情况下,我们可以通过调整缓冲区大小、检查套接字连接状态等方式来解决问题。
9.2性能优化
为了提高 Netlink 通信的性能,我们可以从以下几个方面入手:
合理设置缓冲区大小:缓冲区的大小直接影响着通信的效率。如果缓冲区过小,可能会导致消息丢失或需要频繁地进行数据传输;如果缓冲区过大,又会浪费系统资源 。在实际应用中,我们需要根据消息的大小和通信的频率来合理设置缓冲区大小。对于一些实时性要求较高、消息量较小的通信场景,可以适当减小缓冲区大小,以提高消息的处理速度;而对于一些大数据量的传输场景,则需要增大缓冲区大小,减少数据传输的次数。
优化消息处理逻辑:在处理 Netlink 消息时,应尽量减少不必要的计算和操作,提高消息处理的速度 。可以采用多线程或异步处理的方式,将消息处理任务分配到不同的线程中,避免主线程被阻塞。在处理网络配置消息时,如果需要进行复杂的网络计算,可以将这些计算任务放到单独的线程中执行,主线程继续接收和处理其他消息,从而提高系统的整体性能。
减少系统调用次数:系统调用是一种比较耗时的操作,应尽量减少不必要的系统调用 。在 Netlink 通信中,可以通过批量处理消息的方式,减少 sendmsg () 和 recvmsg () 等系统调用的次数。当需要发送多个小消息时,可以将这些消息合并成一个大消息进行发送,从而减少系统调用的开销。
使用高效的数据结构和算法:选择合适的数据结构和算法可以显著提高程序的性能 。在存储和处理 Netlink 消息时,可以使用链表、哈希表等数据结构来提高数据的查找和访问效率。对于一些需要频繁查找消息的场景,使用哈希表可以大大提高查找速度,减少处理时间。
9.3安全问题
在 Netlink 通信中,安全问题不容忽视。如果通信过程中出现安全漏洞,可能会导致系统信息泄露、被恶意攻击等严重后果。以下是一些需要注意的安全问题:
防止非法访问:Netlink 通信涉及到内核与用户空间的交互,需要确保只有合法的进程能够访问 Netlink 套接字 。可以通过权限控制、身份验证等方式来防止非法访问。在创建 Netlink 套接字时,可以设置适当的权限,只有具有相应权限的用户或进程才能访问;也可以采用身份验证机制,如使用数字证书、密钥等方式,验证通信双方的身份,确保通信的安全性。
防止数据篡改:在 Netlink 消息传输过程中,需要防止消息被篡改 。可以使用消息摘要、加密等技术来保证数据的完整性和保密性。消息摘要算法(如 MD5、SHA-1 等)可以生成消息的唯一摘要,接收方可以通过验证摘要来判断消息是否被篡改;加密技术(如 AES、RSA 等)可以对消息进行加密,只有拥有正确密钥的接收方才能解密消息,从而保证消息的保密性。
避免缓冲区溢出:缓冲区溢出是一种常见的安全漏洞,可能会被攻击者利用来执行恶意代码 。在处理 Netlink 消息时,要确保接收和发送缓冲区的大小足够,并且对输入的数据进行严格的边界检查,防止缓冲区溢出的发生。在接收消息时,要检查消息的长度是否超过了缓冲区的大小,如果超过,应采取相应的措施,如拒绝接收或调整缓冲区大小。