原理
什么是netlink
netlink 是一种基于网络的通信机制,允许内核内部、内核与用户态应用之间甚至用户态应用之间进行通信;netlink 的主要作用是内核与用户态之间通信;它的思想是,基于 BSD 的 socket 使用网络框架在内核和用户态之间进行通信;
NetLink是一种基于应用层跟内核态的通信机制,其特点是一种异步全双工的通信方式,支持内核态主动发起通信的机制。该机制提供了一组特殊的API接口,用户态则通过socket API调用。内核发送的数据再应用层接收后会保存在接收进程socket的缓存中,再由接收进程处理。

Netlink套接字是用以实现用户进程与内核进程通信的一种特殊的进程间通信(IPC) ,也是网络应用程序与内核通信的最常用的接口。
在 Linux 内核中,使用 netlink 进行应用与内核通信的应用有很多,如路由 daemon(NETLINK_ROUTE)、用户态 socket 协议(NETLINK_USERSOCK)、防火墙(NETLINK_FIREWALL)、netfilter 子系统(NETLINK_NETFILTER)、内核事件向用户态通知(NETLINK_KOBJECT_UEVENT)、通用 netlink(NETLINK_GENERIC)
Netlink 是一种在内核与用户应用间进行双向数据传输的非常好的方式,用户态应用使用标准的 socket API 就可以使用 netlink 提供的强大功能,内核态需要使用专门的内核 API 来使用 netlink。一般来说用户空间和内核空间的通信方式有三种:/proc、ioctl、Netlink。而前两种都是单向的,而 Netlink 可以实现双工通信。
Netlink 相对于系统调用,ioctl 以及 /proc文件系统而言,具有以下优点:
- netlink使用简单,只需要在
include/linux/netlink.h中增加一个新类型的 netlink 协议定义即可,(如 #define NETLINK_TEST 20 然后,内核和用户态应用就可以立即通过 socket API 使用该 netlink 协议类型进行数据交换)
- netlink 是一种异步通信机制,在内核与用户态应用之间传递的消息保存在 socket 缓存队列中,发送消息只是把消息保存在接收者的 socket 接收队列,而不需要等待接收者收到消息。使用 netlink 的内核部分可以采用模块的方式实现,使用 netlink 的应用部分和内核部分没有编译时依赖
- netlink 支持多播,内核模块或应用可以把消息多播给一个 netlink 组,属于该 neilink 组的任何内核模块或应用都能接收到该消息,内核事件向用户态的通知机制就使用了这一特性。内核可以使用 netlink 首先发起会话
Netlink 协议基于 BSD socket 和 AF_NETLINK 地址簇,使用 32 位的端口号寻址,每个 Netlink 协议通常与一个或一组内核服务/组件相关联,如NETLINK_ROUTE 用于获取和设置路由与链路信息、NETLINK_KOBJECT_UEVENT 用于内核向用户空间的 udev 进程发送通知等。
netlink架构图

为什么要有netlink
内核中有其他一些方法可以实现用户空间和内核通信,如 procfs、sysfs 和 ioctrl 等;netlink 相比于这些方法,有以下优势:
- 任何一方都不需要轮询;如果通过文件通信,用户态应用需要不断检查是否有新消息到达;
- netlink 使用简单,它是基于 socket 的,可以使用 socket api;
- 只需要在 netlink 协议族中新增加一个协议;
- 使用 netlink 的内核部分可以采用模块的方式实现,之后使用 socket api 进行通信;
- 内核可以直接向用户层发送信息,而无需用户层事先请求;
- netlink 支持单播、组播;内核模块可以把消息发送到一个多播组;
数据结构
在内核include/uapi/linux/netlink.h中
sockaddr_nl
消息地址
netlink 是基于网络的,使用 socket 通信;类似于其它网络协议,每个 netlink socket 都需要分配一个地址;struct sockaddr_nl表示 netlink 地址;
1
2
3
4
5
6
|
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 */
};
|
nl_family:固定为AF_NETLINK,表示 netlink 协议族;
nl_pad:总是为0
nl_pid:socket 的唯一标识符;对内核自身来说,该字段是 0,而用户空间的应用程序通常使用其线程组 ID;netlink 并没有要求该字段是进程 ID,它可以是任何值,只需要保证其唯一性;使用线程组 ID 不过是方便而已;nl_pid 是一个单播地址;
nl_groups:多播组掩码,每个 bit 表示一个多播组;为 0 表示不加入任何多播组,每个 netlink 协议族最多支持 32 个多播组;
/include/uapi/linux/netlink.h
netlink 消息由两部分组成:消息头和消息体;消息头固定为16字节,消息体长度可变;

nlmsghdr
Netlink 消息的固定格式元数据头

消息头定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/**
* struct nlmsghdr - fixed format metadata header of Netlink messages
* @nlmsg_len: Length of message including header
* @nlmsg_type: Message content type
* @nlmsg_flags: Additional flags
* @nlmsg_seq: Sequence number
* @nlmsg_pid: Sending process port ID
*/
struct nlmsghdr {
__u32 nlmsg_len;
__u16 nlmsg_type;
__u16 nlmsg_flags;
__u32 nlmsg_seq;
__u32 nlmsg_pid;
};
|
-
nlmsg_len:包括标头的消息长度。整个消息的长度,包括消息头;
-
nlmsg_type:消息内容类型。应用内部定义消息的类型,对netlink内核实现是透明的,因此大部分情况下设置为 0。netlink定义以下四种通用消息类型
1
2
3
4
5
6
|
#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:附加标志。消息标志。内核读取和修改这些标志。对于一般的使用,设置为 0 即可。高级应用(如 netfilter 和路由 daemon 需要它进行一些复杂的操作),可用的标志包括:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
/* Flags values */
#define NLM_F_REQUEST 0x01 /* It is request message. 标识请求消息,一般从用户空间向内核空间发起 */
#define NLM_F_MULTI 0x02 /* Multipart message, terminated by NLMSG_DONE 用于指示该消息是一个多部分消息的一部分,后续的消息可以通过宏NLMSG_NEXT来获得 */
#define NLM_F_ACK 0x04 /* Reply with ack, with zero or error code 表示该消息是前一个请求消息的响应,顺序号与进程ID可以把请求与响应关联起来 */
#define NLM_F_ECHO 0x08 /* Receive resulting notifications 表示该消息是相关的一个包的回传 */
#define NLM_F_DUMP_INTR 0x10 /* Dump was inconsistent due to sequence change */
#define NLM_F_DUMP_FILTERED 0x20 /* Dump was filtered as requested */
/* Modifiers to GET request */
#define NLM_F_ROOT 0x100 /* specify tree root 被许多 netlink 协议的各种数据获取操作使用,该标志指示被请求的数据表应当整体返回用户应用,而不是一个条目一个条目地返回。有该标志的请求通常导致响应消息设置 NLM_F_MULTI标志。注意,当设置了该标志时,请求是协议特定的,因此,需要在字段 nlmsg_type 中指定协议类型 */
#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 用于和 CREATE 和 APPEND 配合使用,如果条目已经存在,将失败 */
#define NLM_F_CREATE 0x400 /* Create, if it does not exist指示应当在指定的表中创建一个条目 */
#define NLM_F_APPEND 0x800 /* Add to end of list 指示在表末尾添加新的条目 */
/* Modifiers to DELETE request */
#define NLM_F_NONREC 0x100 /* Do not delete recursively */
#define NLM_F_BULK 0x200 /* Delete multiple objects */
/* Flags for ACK message */
#define NLM_F_CAPPED 0x100 /* request was capped */
#define NLM_F_ACK_TLVS 0x200 /* extended ACK TVLs were included */
...
|
-
nlmsg_seq,消息序列号,表示一系列消息之间在时间上的前后关系;也可以通过request消息和ack消息使用相同的序列号,保证消息不丢失;
-
nlmsg_pid,消息发送者的进程 port id;
nlattr
消息体
netlink 协议并没有严格要求消息体的格式,可以发送任意消息;但一般标准做法,消息体是用nlattr,即属性,采用 tlv 的形式,即标识域(Tag)/类型(Type)+长度域(Length)+值域(Value);消息体组织形式如下:

struct nlattr定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/*
* <------- 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_len: 属性的长度
- nla_type: 属性的类型
常用宏
在 linux/netlink.h 中定义了一些方便对消息进行处理的宏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) ) /* 宏NLMSG_ALIGN(len)用于得到不小于len且字节对齐的最小数值 */
#define NLMSG_HDRLEN ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr))) /* Netlink 头部长度 */
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN) /* 计算消息数据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)) /* 判断消息是否 >len */
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len))) /* 用于返回payload的长度*/
|
枚举
常量
-
NL_ATTR_TYPE_INVALID
未使用
-
NL_ATTR_TYPE_FLAG
标志属性(存在/不存在)
-
NL_ATTR_TYPE_U8
8 位无符号属性
-
NL_ATTR_TYPE_U16
16 位无符号属性
-
NL_ATTR_TYPE_U32
32 位无符号属性
-
NL_ATTR_TYPE_U64
64 位无符号属性
-
NL_ATTR_TYPE_S8
8 位有符号属性
-
NL_ATTR_TYPE_S16
16 位有符号属性
-
NL_ATTR_TYPE_S32
32 位有符号属性
-
NL_ATTR_TYPE_S64
64 位有符号属性
-
NL_ATTR_TYPE_BINARY
二进制数据,可以指定最小/最大长度
-
NL_ATTR_TYPE_STRING
字符串,可以指定最小/最大长度
-
NL_ATTR_TYPE_NUL_STRING
以 NUL 结尾的字符串,可以指定最小/最大长度
-
NL_ATTR_TYPE_NESTED
嵌套,即此属性的内容由子属性组成。可以指定嵌套策略和内部 maxtype。
-
NL_ATTR_TYPE_NESTED_ARRAY
嵌套数组,即此属性的内容包含子属性,其类型无关紧要(仅用于分隔数组条目),并且每个此类数组条目又具有属性,可以指定这些内部属性的策略和相应的 maxtype。
-
NL_ATTR_TYPE_BITFIELD32
struct nla_bitfield32 属性
-
NL_ATTR_TYPE_SINT
32 位或 64 位有符号属性,与 4B 对齐
-
NL_ATTR_TYPE_UINT
32 位或 64 位无符号属性,与 4B 对齐
-
enum netlink_policy_type_attr
策略类型属性
常量
-
NL_POLICY_TYPE_ATTR_UNSPEC
未使用
-
NL_POLICY_TYPE_ATTR_TYPE
属性类型,enum netlink_attribute_type (U32)
-
NL_POLICY_TYPE_ATTR_MIN_VALUE_S
有符号整数的最小值 (S64)
-
NL_POLICY_TYPE_ATTR_MAX_VALUE_S
有符号整数的最大值 (S64)
-
NL_POLICY_TYPE_ATTR_MIN_VALUE_U
无符号整数的最小值 (U64)
-
NL_POLICY_TYPE_ATTR_MAX_VALUE_U
无符号整数 (U64) 的最大值
-
NL_POLICY_TYPE_ATTR_MIN_LENGTH
二进制属性的最小长度,如果未指定则无最小值 (U32)
-
NL_POLICY_TYPE_ATTR_MAX_LENGTH
二进制属性的最大长度,如果未指定则无最大值 (U32)
-
NL_POLICY_TYPE_ATTR_POLICY_IDX
嵌套类型和嵌套数组类型的子策略 (U32)
-
NL_POLICY_TYPE_ATTR_POLICY_MAXTYPE
嵌套类型和嵌套数组类型的最大子策略属性,理论上可以小于索引指向策略的大小,如果在嵌套内部限制 (U32)
-
NL_POLICY_TYPE_ATTR_BITFIELD32_MASK
bitfield32 类型的有效掩码 (U32)
-
NL_POLICY_TYPE_ATTR_PAD
用于 64 位对齐的填充属性
-
NL_POLICY_TYPE_ATTR_MASK
无符号整数的有效位掩码 (U64)
-
__NL_POLICY_TYPE_ATTR_MAX
属性数量
-
NL_POLICY_TYPE_ATTR_MAX
最高属性号
用户态 API
用户态使用标准的 socket API 如socket,bind,sendmsg,recvmsg和close等接口就能很容易地使用 netlink socket。注意,使用 netlink 的应用必须包含头文件 linux/netlink.h。当然 socket 需要的头文件sys/socket.h也必不可少
socket()
创建netlink socket
1
|
int socket(int domain, int type, int protocol);
|
- domain:
AF_NETLINK或者PF_NETLINK
- type:
SOCK_RAW或SOCK_DGRAM (面向数据报的服务)
- protocol: 指定 netlink 协议类型,
NETLINK_ROUTE、NETLINK_FIREWALL、NETLINK_APRD、NETLINK_ROUTE6_FW
- 返回值
- 成功:新套接字对应的文件描述符
- 失败:
-1 errno,
目前 netlink 协议族支持 32 种协议类型,最大值 32。内核使用了0~22,理论上 32 以内未被占用的协议号,都可以用于自定义 netlink 协议,但这种方法并不规范,对于未来更新内核版本兼容性不友好。具体定义在include/uapi/linux/netlink.h:
更加合适的方法,是在 generic netlink 协议族中,添加子协议,如 nl80211 就是 generic netlink 的一个子协议;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
#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_SMC 22 /* SMC monitoring */
#define NETLINK_INET_DIAG NETLINK_SOCK_DIAG
#define MAX_LINKS 32
|
bind()
给 socket 绑定一个地址结构
1
|
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
|
sockfd:套接字对应的文件描述符
addr:(struct sockaddr*)&nl_addr,netlink socket 地址结构为sockaddr_nl
addrlen:sizeof(nl_addr),地址结构的大小
- 返回值
sendto()
将数据由socket传给对方主机,直接通过参数传递目标地址和数据缓冲区,结构简单
1
|
int sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
|
sockfd:套接字
buf:存储数据的缓冲区
len:数据长度
flags:调用操作方式
src_addr:传入参数,传入目标地址结构
addrlen:传入传出参数,地址结构长度
- 返回值:
>0:成功写出的字节数
0:对端关闭连接
-1:errno
sendmsg()
将数据由socket传给对方主机,与sendto()最大区别是使用 struct msghdr 封装所有信息
1
|
int sendmsg(int sockfd, const struct msghdr *msg, int flags);
|
使用函数 sendmsg() 发送 netlink 消息时需要引用结构 struct msghdr
recvmsg 和 sendmsg 函数详解-CSDN博客
recvfrom()
接收socket传来的数据,并把数据存到buf指向的内存空间
1
|
int recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr,socklen_t *addrlen);
|
sockfd:socket 套接字
buf:接收数据的缓冲区
len:缓冲区大小
flags:调用操作方式
src_addr:传出参数,传出对端地址结构
addrlen:传入传出参数,地址结构长度
- 返回值:
>0:成功接收到的字节数
0:对端关闭连接
-1:errno
recvmsg()
接收socket传来的数据,与recvfrom()最大区别是使用 struct msghdr 封装所有信息
1
|
int (*recvmsg)(struct sock *sk, struct msghdr *msg, size_t len, int flags, int *addr_len);
|
当消息正确接收后,msg_buffer 里保存包含 netlink 消息头的整个 netlink消息,nlhdr 指向接收到的消息的消息头,nladdr 包含接收到的消息体的目的地址信息。宏 NLMSG_DATA(nlhdr) 返回指向消息的数据部分的指针(宏部分会介绍到)
close()
把 netlink socket 套接字标记为已关闭,释放socket资源
1
|
int close(int sockfd); //返回值:成功为0,出错为-1.
|
内核态 API
/include/linux/netlink.h
netlink_kernel_create()
内核创建 netlink socket:
1
2
3
4
5
|
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);
}
|
net,网络命名空间;
uint,表示 netlink 子协议族,如:#define NETLINK_ROUTE 0
cfg,netlink kernel 创建 socket 的可选参数;其中,input是该内核 netlink 模块收到消息后的处理函数
- 返回值
1
2
3
4
5
6
7
8
9
|
/* optional Netlink kernel configuration parameters */
struct netlink_kernel_cfg {
unsigned int groups;
unsigned int flags;
void (*input)(struct sk_buff *skb);
int (*bind)(struct net *net, int group);
void (*unbind)(struct net *net, int group);
void (*release) (struct sock *sk, unsigned long *groups);
};
|
- groups:组编号
sockaddr_nl 结构的成员 nl_groups 最多只能设置32个组播组。可使用套接字选项 NETLINK_ADD_MEMBERSHIP /NETLINK_DROP_MEMBERSHIP来加入或退出组播组
- flags:
NL_CFG_F_NONROOT_RECV (非root用户可绑定到组播组) 或NL_CFG_F_NONROOT_SEND(非root用户可发送组播)
input:用于指定回调函数。用于处理用户空间发送到内核方向的数据。内核接收到数据包后会传给 input 函数。接收一个sk_buff 结构,数据包含一个 nlmsghdr 协议头
nlmsg_put() 处理消息头
添加新的 netlink 信息到 skb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/**
* nlmsg_put - Add a new netlink message to an skb
* @skb: socket buffer to store message in
* @portid: netlink PORTID of requesting application
* @seq: sequence number of message
* @type: message type
* @payload: length of message payload
* @flags: message flags
*
* Returns: NULL if the tailroom of the skb is insufficient to store
* the message header and payload.
*/
static inline struct nlmsghdr *nlmsg_put(struct sk_buff *skb, u32 portid, u32 seq,
int type, int payload, int flags)
{
if (unlikely(skb_tailroom(skb) < nlmsg_total_size(payload)))
return NULL;
return __nlmsg_put(skb, portid, seq, type, payload, flags);
}
|
netlink_unicast()
发送一个单播消息
1
|
int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock);
|
-
ssk:netlink_kernel_create()返回的socket;
-
skb:其中的data指向需要发送的netlink消息体,而skb的控制块保存了消息的地址信息
-
portid:要发往的用户进程的PID
-
nonblock:表示该函数是否为非阻塞,如果为1,该函数将在没有接收缓存可利用时立即返回,而如果为0,该函数在没有接收缓存可利用时睡眠
netlink_broadcast()
发送一个多播消息
1
2
|
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, __u32 portid,
__u32 group, gfp_t allocation);
|
- group:所有目标多播组对应掩码进行或运算的结果
- allocation:内核申请内存的类型,通常情况下在中断上下文中使用GFP_ATOMIC(即不可以睡眠),否则使用GFP_KERNEL。
netlink_kernel_release()
释放 Netlink 内核套接字
1
|
void netlink_kernel_release(struct sock *sk);
|
经典 Netlink 与通用 Netlink
Netlink 的初始实现依赖于对子系统的 ID 的静态分配,并且几乎没有提供基础设施支持。让我们将这些协议统称为 经典 Netlink。它们的列表定义在 include/uapi/linux/netlink.h 文件的顶部,其中包括但不限于 - 通用网络 (NETLINK_ROUTE)、iSCSI (NETLINK_ISCSI) 和审计 (NETLINK_AUDIT) 等。
通用 Netlink(于 2005 年引入)允许动态注册子系统(和子系统 ID 分配)、自检并简化了内核端的接口实现。
以下部分描述了如何使用通用 Netlink,因为使用通用 Netlink 的子系统数量比旧协议多了一个数量级。也没有计划向内核添加更多经典 Netlink 协议。本文档后面提供了有关如何与 Linux 内核的核心网络部分(或使用经典 Netlink 的其他 20 个子系统)通信的基本信息,这些信息与通用Netlink有所不同。
通用 Netlink
除了 Netlink 固定元数据头之外,每个 Netlink 协议还定义了自己的固定元数据头。(类似于网络头的堆叠方式:以太网 -> IP -> TCP,我们有 Netlink -> Generic N. -> Family)
Netlink 消息始终以 struct nlmsghdr 开头,后跟协议特定的头。在通用 Netlink 的情况下,协议头是 struct genlmsghdr
在通用 Netlink 的情况下,字段的实际含义如下
1
2
3
4
5
6
7
8
9
10
11
12
13
|
struct nlmsghdr {
__u32 nlmsg_len; /* Length of message including headers */
__u16 nlmsg_type; /* Generic Netlink Family (subsystem) ID */
__u16 nlmsg_flags; /* Flags - request or dump */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Port ID, set to 0 */
};
struct genlmsghdr {
__u8 cmd; /* Command, as defined by the Family */
__u8 version; /* Irrelevant, set to 1 */
__u16 reserved; /* Reserved, set to 0 */
};
/* TLV attributes follow... */
|
在经典 Netlink 中,nlmsghdr.nlmsg_type 用于标识消息正在引用的子系统中的哪个操作(例如,获取有关网络设备的信息)。通用 Netlink 需要在一个协议中复用多个子系统,因此它使用此字段来标识子系统,而 genlmsghdr.cmd 则标识操作。(有关如何查找感兴趣子系统的系列 ID 的信息,请参阅 解析系列 ID。)请注意,此字段的前 16 个值(0 - 15)保留用于经典 Netlink 和通用 Netlink 中的控制消息。有关更多详细信息,请参阅 Netlink 消息类型。
Netlink 套接字上有 3 种常见的消息交换类型
- 执行单个操作 (
do);
- dump 信息 (
dump);
- 获取异步通知 (
multicast)。
经典 Netlink 非常灵活,并且可能允许发生其他类型的交换,但在实践中,使用的是这三种。
异步通知由内核发送,并由订阅它们的用户套接字接收。do 和 dump 请求由用户发起。nlmsghdr.nlmsg_flags 应设置如下
- 对于
do:NLM_F_REQUEST | NLM_F_ACK
- 对于
dump:NLM_F_REQUEST | NLM_F_ACK | NLM_F_DUMP
nlmsghdr.nlmsg_seq 应设置为单调递增的值。该值会在响应中回显,并且在实践中无关紧要,但对于发送的每个消息将其设置为递增值被认为是良好的做法。该字段的目的是将响应与请求进行匹配。异步通知的 nlmsghdr.nlmsg_seq 将为 0。
nlmsghdr.nlmsg_pid 是 Netlink 中地址的等效项。与内核通信时,此字段可以设置为 0。有关该字段的(不常见)用法,请参阅 nlmsg_pid。
genlmsghdr.version 的预期用途是允许对子系统提供的 API 进行版本控制。迄今为止,没有子系统对该字段进行过重大使用,因此将其设置为 1 似乎是一个安全的选择。
Netlink 消息类型
如前所述,nlmsghdr.nlmsg_type 携带协议特定值,但前 16 个标识符是保留的(第一个子系统特定消息类型应等于 NLMSG_MIN_TYPE,即 0x10)。
仅定义了 4 个 Netlink 控制消息
NLMSG_NOOP - 忽略消息,在实践中未使用;
NLMSG_ERROR - 携带操作的返回码;
NLMSG_DONE - 标记 dump 的结束;
NLMSG_OVERRUN - 套接字缓冲区溢出,迄今为止未使用。
NLMSG_ERROR 和 NLMSG_DONE 具有实际意义。它们携带操作的返回码。请注意,除非在请求上设置了 NLM_F_ACK 标志,否则如果没有错误,Netlink 不会响应 NLMSG_ERROR。为了避免不得不特殊处理此怪癖,建议始终设置 NLM_F_ACK。
NLMSG_ERROR 的格式由 struct nlmsgerr 描述
1
2
3
4
5
6
7
8
9
10
11
|
----------------------------------------------
| struct nlmsghdr - response header |
----------------------------------------------
| int error |
----------------------------------------------
| struct nlmsghdr - original request header |
----------------------------------------------
| ** optionally (1) payload of the request |
----------------------------------------------
| ** optionally (2) extended ACK |
----------------------------------------------
|
这里有两个 struct nlmsghdr 的实例,第一个是响应,第二个是请求。NLMSG_ERROR 携带有关导致错误的请求的信息。当尝试将请求与响应匹配或重新解析请求以将其 dump 到日志中时,这可能很有用。
请求的有效负载不会在报告成功的消息中回显(error == 0)或如果设置了 NETLINK_CAP_ACK setsockopt()。后者很常见,或许建议这样做,因为必须从内核读取每个请求的副本相当浪费。请求有效负载的缺失由 nlmsghdr.nlmsg_flags 中的 NLM_F_CAPPED 指示。
NLMSG_ERROR 的第二个可选元素是扩展 ACK 属性。有关更多详细信息,请参阅 扩展 ACK。扩展 ACK 的存在由 nlmsghdr.nlmsg_flags 中的 NLM_F_ACK_TLVS 指示。
NLMSG_DONE 更简单,请求永远不会回显,但可能存在扩展 ACK 属性
1
2
3
4
5
6
7
|
----------------------------------------------
| struct nlmsghdr - response header |
----------------------------------------------
| int error |
----------------------------------------------
| ** optionally extended ACK |
----------------------------------------------
|
请注意,某些实现可能会发出自定义的 NLMSG_DONE 消息来回复 do 操作请求。在这种情况下,有效负载是特定于实现的,也可能不存在。
解析系列 ID
系列:Family,族
本节解释如何查找子系统的系列 ID。它也用作通用 Netlink 通信的示例。
通用 Netlink 本身是通过通用 Netlink API 暴露的一个子系统。为了避免循环依赖,通用 Netlink 具有静态分配的 Family ID(GENL_ID_CTRL,等于 NLMSG_MIN_TYPE)。通用 Netlink 系列实现了一个命令,用于查找有关其他系列的信息(CTRL_CMD_GETFAMILY)。
要获取有关名为例如 "test1" 的通用 Netlink 系列的信息,我们需要在先前打开的通用 Netlink 套接字上发送消息。该消息应以通用 Netlink 系列 (1) 为目标,是对 CTRL_CMD_GETFAMILY (3) 的 do (2) 调用。此调用的 dump 版本将使内核响应它所知道的所有系列的信息。最后但并非最不重要的是,必须以适当的类型 (4) 将所讨论的系列的名称指定为属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
struct nlmsghdr:
__u32 nlmsg_len: 32
__u16 nlmsg_type: GENL_ID_CTRL // (1)
__u16 nlmsg_flags: NLM_F_REQUEST | NLM_F_ACK // (2)
__u32 nlmsg_seq: 1
__u32 nlmsg_pid: 0
struct genlmsghdr:
__u8 cmd: CTRL_CMD_GETFAMILY // (3)
__u8 version: 2 /* or 1, doesn't matter */
__u16 reserved: 0
struct nlattr: // (4)
__u16 nla_len: 10
__u16 nla_type: CTRL_ATTR_FAMILY_NAME
char data: test1\0
(padding:)
char data: \0\0
|
Netlink 中的长度字段(nlmsghdr.nlmsg_len 和 nlattr.nla_len)始终包含标头。netlink 中的属性标头必须从消息的开头对齐到 4 个字节,因此在 CTRL_ATTR_FAMILY_NAME 之后有额外的 \0\0。属性长度不包括填充。
如果找到该系列,内核将回复两条消息,一条是包含该系列所有信息的响应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
/* Message #1 - reply */
struct nlmsghdr:
__u32 nlmsg_len: 136
__u16 nlmsg_type: GENL_ID_CTRL
__u16 nlmsg_flags: 0
__u32 nlmsg_seq: 1 /* echoed from our request */
__u32 nlmsg_pid: 5831 /* The PID of our user space process */
struct genlmsghdr:
__u8 cmd: CTRL_CMD_GETFAMILY
__u8 version: 2
__u16 reserved: 0
struct nlattr:
__u16 nla_len: 10
__u16 nla_type: CTRL_ATTR_FAMILY_NAME
char data: test1\0
(padding:)
data: \0\0
struct nlattr:
__u16 nla_len: 6
__u16 nla_type: CTRL_ATTR_FAMILY_ID
__u16: 123 /* The Family ID we are after */
(padding:)
char data: \0\0
struct nlattr:
__u16 nla_len: 9
__u16 nla_type: CTRL_ATTR_FAMILY_VERSION
__u16: 1
/* ... etc, more attributes will follow. */
|
以及错误代码(成功),因为请求上已设置 NLM_F_ACK
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
/* Message #2 - the ACK */
struct nlmsghdr:
__u32 nlmsg_len: 36
__u16 nlmsg_type: NLMSG_ERROR
__u16 nlmsg_flags: NLM_F_CAPPED /* There won't be a payload */
__u32 nlmsg_seq: 1 /* echoed from our request */
__u32 nlmsg_pid: 5831 /* The PID of our user space process */
int error: 0
struct nlmsghdr: /* Copy of the request header as we sent it */
__u32 nlmsg_len: 32
__u16 nlmsg_type: GENL_ID_CTRL
__u16 nlmsg_flags: NLM_F_REQUEST | NLM_F_ACK
__u32 nlmsg_seq: 1
__u32 nlmsg_pid: 0
|
属性(struct nlattr)的顺序不能保证,因此用户必须遍历属性并解析它们。
请注意,通用 Netlink 套接字不与单个系列关联或绑定。一个套接字可以用于与许多不同的系列交换消息,使用 nlmsghdr.nlmsg_type 字段在逐条消息的基础上选择接收方系列。
扩展 ACK
扩展 ACK 控制在 NLMSG_ERROR 和 NLMSG_DONE 消息中报告额外的错误/警告 TLV。为了保持向后兼容性,必须通过将 NETLINK_EXT_ACK setsockopt() 设置为 1 来显式启用此功能。
扩展 ACK 属性的类型在 enum nlmsgerr_attrs 中定义。最常用的属性是 NLMSGERR_ATTR_MSG、NLMSGERR_ATTR_OFFS 和 NLMSGERR_ATTR_MISS_*。
NLMSGERR_ATTR_MSG 包含一条用英文描述遇到的问题的消息。这些消息比通过标准 UNIX 错误代码表达的要详细得多。
NLMSGERR_ATTR_OFFS 指向导致问题的属性。
NLMSGERR_ATTR_MISS_TYPE 和 NLMSGERR_ATTR_MISS_NEST 通知缺少属性。
扩展 ACK 可以报告错误以及成功情况。后者应被视为警告。
扩展 ACK 极大地提高了 Netlink 的可用性,应始终启用、适当解析并报告给用户。
高级主题
转储一致性
内核用于存储某些对象的数据结构很难提供转储中所有对象的原子快照(而不会影响更新它们的快速路径)。
如果转储被中断且可能不一致(例如,缺少对象),内核可以在转储中的任何消息(包括 NLMSG_DONE 消息)上设置 NLM_F_DUMP_INTR 标志。如果用户空间看到设置了该标志,则应重试转储。
自省
通过访问 解析 Family ID 中报告的 Family 对象来启用基本自省功能。用户可以查询有关通用 Netlink 系列的信息,包括内核支持哪些操作以及内核理解哪些属性。系列信息包括内核可以解析的属性的最高 ID,一个单独的命令(CTRL_CMD_GETPOLICY)提供有关受支持属性的详细信息,包括内核接受的值的范围。
在用户空间需要确保内核支持某个功能然后再发出请求的情况下,查询系列信息非常有用。
nlmsg_pid
nlmsghdr.nlmsg_pid 是 Netlink 等效于地址的概念。它被称为 Port ID,有时也被称为 Process ID,因为由于历史原因,如果应用程序不选择(绑定到)显式 Port ID,内核将自动为其分配一个等于其 Process ID 的 ID(如 getpid() 系统调用报告的那样)。
与 TCP/IP 网络协议的 bind() 语义类似,零值表示“自动分配”,因此应用程序通常将 nlmsghdr.nlmsg_pid 字段初始化为 0。
该字段在今天仍然在内核需要发送单播通知的极少数情况下使用。用户空间应用程序可以使用 bind() 将其套接字与特定的 PID 关联,然后将其 PID 传递给内核。这样,内核就可以到达特定的用户空间进程。
当内核需要触发用户空间处理或向用户空间请求策略决策时,在类似 UMH(用户模式助手)的场景中会使用这种通信。
多播通知
Netlink 的优势之一是能够向用户空间发送事件通知。这是一种单向通信形式(内核 -> 用户),不涉及任何控制消息,如 NLMSG_ERROR 或 NLMSG_DONE。
例如,通用 Netlink 系列本身定义了一组关于已注册系列的多播通知。当添加新系列时,订阅通知的套接字将收到以下消息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
struct nlmsghdr:
__u32 nlmsg_len: 136
__u16 nlmsg_type: GENL_ID_CTRL
__u16 nlmsg_flags: 0
__u32 nlmsg_seq: 0
__u32 nlmsg_pid: 0
struct genlmsghdr:
__u8 cmd: CTRL_CMD_NEWFAMILY
__u8 version: 2
__u16 reserved: 0
struct nlattr:
__u16 nla_len: 10
__u16 nla_type: CTRL_ATTR_FAMILY_NAME
char data: test1\0
(padding:)
data: \0\0
struct nlattr:
__u16 nla_len: 6
__u16 nla_type: CTRL_ATTR_FAMILY_ID
__u16: 123 /* The Family ID we are after */
(padding:)
char data: \0\0
struct nlattr:
__u16 nla_len: 9
__u16 nla_type: CTRL_ATTR_FAMILY_VERSION
__u16: 1
/* ... etc, more attributes will follow. */
|
通知包含与对 CTRL_CMD_GETFAMILY 请求的响应相同的信息。
通知的 Netlink 标头大多为 0 且不相关。nlmsghdr.nlmsg_seq 可以是零,也可以是该系列维护的单调递增的通知序列号。
要接收通知,用户套接字必须订阅相关的通知组。与 Family ID 非常相似,给定多播组的 Group ID 是动态的,可以在 Family 信息中找到。CTRL_ATTR_MCAST_GROUPS 属性包含具有组的名称(CTRL_ATTR_MCAST_GRP_NAME)和 ID(CTRL_ATTR_MCAST_GRP_ID)的嵌套。
一旦知道 Group ID,setsockopt() 调用会将套接字添加到组中
1
2
3
4
5
6
|
unsigned int group_id;
/* .. find the group ID... */
setsockopt(fd, SOL_NETLINK, NETLINK_ADD_MEMBERSHIP,
&group_id, sizeof(group_id));
|
套接字现在将接收通知。
建议使用单独的套接字来接收通知和向内核发送请求。通知的异步性质意味着它们可能会与响应混淆,从而使消息处理变得更加困难。
缓冲区大小调整
Netlink 套接字是数据报套接字,而不是流套接字,这意味着每个消息都必须通过单个 recv()/recvmsg() 系统调用完整接收。如果用户提供的缓冲区太短,消息将被截断,并且 struct msghdr 中将设置 MSG_TRUNC 标志(struct msghdr 是 recvmsg() 系统调用的第二个参数,不是 Netlink 标头)。
截断后,消息的其余部分将被丢弃。
Netlink 希望用户缓冲区至少为 8kB 或 CPU 架构的页面大小,以较大者为准。但是,特定的 Netlink 系列可能需要更大的缓冲区。建议使用 32kB 缓冲区来最有效地处理转储(更大的缓冲区可以容纳更多转储的对象,因此需要更少的 recvmsg() 调用)。
经典 Netlink
经典 Netlink 和通用 Netlink 之间的主要区别在于子系统标识符的动态分配和自省的可用性。理论上,协议没有显着差异,但是,实际上,经典 Netlink 尝试了一些在通用 Netlink 中被放弃的概念(实际上,它们通常只在单个子系统的某个小角落中找到用途)。本节旨在解释其中一些概念,其明确目标是让通用 Netlink 用户在阅读 uAPI 标头时有信心忽略它们。
这里的大多数概念和示例都指的是 NETLINK_ROUTE 系列,它涵盖了 Linux 网络堆栈的大部分配置。该系列的真实文档值得单独一章(或一本书)。
系列
系列:Family,族
Netlink 将子系统称为族。这是使用套接字和协议族概念的遗留问题,协议族是 NETLINK_ROUTE 中消息多路分解的一部分。
可悲的是,每一层封装都喜欢将其携带的内容称为“家族”,这使得这个术语非常混乱
- AF_NETLINK 是一个真正的套接字协议族
- AF_NETLINK 的文档将其自身头部(
struct nlmsghdr)之后的消息内容称为“族头部”。
- 通用 Netlink 是 AF_NETLINK 的一个族(struct genlmsghdr 紧随
struct nlmsghdr 之后),但它也称其用户为“族”。
请注意,通用 Netlink 族 ID 位于不同的“ID 空间”中,并与经典 Netlink 协议号重叠(例如,NETLINK_CRYPTO 的经典 Netlink 协议 ID 为 21,而通用 Netlink 也会将其分配给其某个族)。
严格检查
NETLINK_GET_STRICT_CHK 套接字选项在 NETLINK_ROUTE 中启用严格的输入检查。之所以需要它,是因为历史上内核不会验证它不处理的结构的字段。这使得以后开始使用这些字段变得不可能,而不会冒应用程序不正确初始化或根本不初始化这些字段的风险。
NETLINK_GET_STRICT_CHK 声明应用程序正在正确初始化所有字段。它还选择验证消息是否不包含尾随数据,并请求内核拒绝类型高于内核已知最大属性类型的属性。
NETLINK_GET_STRICT_CHK 不在 NETLINK_ROUTE 之外使用。
未知属性
历史上,Netlink 忽略所有未知属性。当时的考虑是,这可以使应用程序不必探测内核支持什么。应用程序可以发出更改状态的请求,并检查请求的哪些部分“生效”了。
对于新的通用 Netlink 族以及选择加入严格检查的族,情况不再如此。请参阅枚举 netlink_validation 以了解执行的验证类型。
固定元数据和结构
经典 Netlink 在消息中大量使用固定格式的结构。消息通常在 struct nlmsghdr 之后包含具有大量字段的结构。也常见将具有多个成员的结构放入属性中,而不将每个成员分解为其自身的属性。
这导致了验证和可扩展性方面的问题,因此强烈建议不要将二进制结构用于新属性。
请求类型
NETLINK_ROUTE 将请求分为 4 种类型:NEW、DEL、GET 和 SET。每个对象都可以处理所有或部分这些请求(对象包括 netdevs、路由、地址、qdiscs 等)。请求类型由消息类型的最低 2 位定义,因此新对象的命令始终以 4 的步幅分配。
每个对象还具有所有请求类型共享的自己的固定元数据(例如,用于 netdev 请求的 struct ifinfomsg,用于地址请求的 struct ifaddrmsg,用于 qdisc 请求的 struct tcmsg)。
尽管其他协议和通用 Netlink 命令在其消息名称中经常使用相同的动词(GET、SET),但请求类型的概念并未得到更广泛的采用。
通知回显
请求 NLM_F_ECHO 将请求产生的通知排队到请求套接字上。这有助于发现请求的影响。
请注意,此功能并非普遍实现。
其他特定于请求类型的标志
经典 Netlink 在 struct nlmsghdr 中的 nlmsg_flags 的高字节中为其 GET、NEW 和 DEL 请求定义了各种标志。由于请求类型尚未被普遍化,因此特定于请求类型的标志很少使用(并且被认为对新族已弃用)。
对于 GET - NLM_F_ROOT 和 NLM_F_MATCH 合并为 NLM_F_DUMP,不再单独使用。NLM_F_ATOMIC 从未使用。
对于 DEL - NLM_F_NONREC 仅由 nftables 使用,而 NLM_F_BULK 仅由 FDB 的某些操作使用。
NEW 的标志在经典 Netlink 中最常用。不幸的是,含义并不十分明确。以下描述基于作者意图的最佳猜测,并且实际上所有族都以某种方式偏离了它。NLM_F_REPLACE 要求替换现有对象,如果没有匹配的对象,则操作应失败。NLM_F_EXCL 具有相反的语义,仅当对象已存在时才成功。NLM_F_CREATE 要求在对象不存在时创建对象,它可以与 NLM_F_REPLACE 和 NLM_F_EXCL 组合使用。
主 Netlink uAPI 标头中的注释指出
1
2
3
4
5
6
|
4.4BSD ADD NLM_F_CREATE|NLM_F_EXCL
4.4BSD CHANGE NLM_F_REPLACE
True CHANGE NLM_F_CREATE|NLM_F_REPLACE
Append NLM_F_CREATE
Check NLM_F_EXCL
|
这似乎表明这些标志早于请求类型。最初使用不带 NLM_F_CREATE 的 NLM_F_REPLACE 来代替 SET 命令。使用不带 NLM_F_CREATE 的 NLM_F_EXCL 来检查对象是否存在而不创建它,大概早于 GET 命令。
NLM_F_APPEND 表示,如果一个键可以关联多个对象(例如,路由的多个下一跳对象),则应将新对象添加到列表中,而不是替换整个列表。
应用
用户->内核
大概步骤
- 直接在 netlink 中直接加入新的协议,定义协议号为 30;
- 内核中新增一个模块,处理该协议的消息;
- 应用程序通过该协议,和内核通信;简单起见,直接传输字符串;
- 应用程序先向内核发送一条消息,内核收到消息后进行回复;

内核空间
- 新建一个独立目录(如
~/netlink_test/kernel/)存放内核相关文件
1
2
3
|
mkdir -p ~/netlink_test/kernel
cd ~/netlink_test/kernel/
touch nl_test_kernel.c Makefile
|
- 编写代码
nl_test_kernel.c:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
|
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <net/sock.h>
#include <linux/netlink.h>
// 自定义的 Netlink 协议号
#define NETLINK_TEST 30
#define MSG_LEN 125
MODULE_LICENSE("GPL");
// 内核中的 Netlink 套接字指针
struct sock *nlsk = NULL;
extern struct net init_net;
// 用于向用户空间发送消息
int send_usrmsg(char *pbuf, uint16_t len, uint32_t pid)
{
struct sk_buff *nl_skb;
struct nlmsghdr *nlh;
int ret;
/* Allocate a new netlink message */
nl_skb = nlmsg_new(len + 1, GFP_ATOMIC);
if(!nl_skb)
{
printk("\nError:netlink alloc failure.\n\n");
return -1;
}
/* Add a new netlink message to an skb
pid是0,说明是从内核发送的
*/
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;
}
/* copy payload */
memcpy(nlmsg_data(nlh), pbuf, len);
ret = netlink_unicast(nlsk, nl_skb, pid, MSG_DONTWAIT);
return ret;
}
// Netlink 套接字的回调函数
static void netlink_rcv_msg(struct sk_buff *skb)
{
struct nlmsghdr *nlh = NULL;
char *umsg = NULL;
char *kmsg = "Hello user's program.";
if(skb->len >= nlmsg_total_size(0))
{
nlh = nlmsg_hdr(skb);
umsg = NLMSG_DATA(nlh);
if(umsg)
{
// 打印接收到的用户空间消息
printk("kernel recv from user space: %s\n", umsg);
// 回复一条固定字符串
send_usrmsg(kmsg, strlen(kmsg), nlh->nlmsg_pid);
}
}
}
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");
}
// 模块加载时调用:创建 Netlink 套接字,注册回调
module_init(test_netlink_init);
// 模块卸载时调用:释放 Netlink 套接字资源
module_exit(test_netlink_exit);
|
Makefile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
# 定义模块名,最终会生成 nl_test_kernel.ko 文件
MODULE_NAME :=nl_test_kernel
# 指定要编译的模块对象文件
obj-m:=$(MODULE_NAME).o
# 指定内核源码的构建目录,通常用于内核模块编译
KERNELDIR ?=/lib/modules/$(shell uname -r)/build
# 当前目录
PWD :=$(shell pwd)
# 编译模块。调用内核的 Makefile,M=$(PWD) 表示在当前目录下查找模块源码
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD)
# 清理编译生成的文件
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
|
obj-m:表示生成的.o文件,编译完成后会生成一个对应名称的ko文件。(不会将编译生成的ko文件导入到内核),如果有多个依赖,使用*-objs方式,写法如下:
1
2
3
|
obj-m := self_kkk.o
self_kkk-objs := aaa.o bbb.o
|
obj-y: 表示会将生成的ko文件直接拷贝到系统内核中
-C :指定的是系统内核模块代码的Makefile
M:表示返回到当前路径并执行当前Makefile
- 编译模块:在当前目录生成
nl_test_kernel.ko
- 加载模块到内核
1
|
sudo insmod nl_test_kernel.ko
|
- 验证模块是否加载:
1
|
dmesg | tail # 查看内核日志,确认初始化消息
|

1
|
lsmod | grep nl_test_kernel # 检查模块列表
|

内核现在就可以处理NETLINK_TEST的消息了
用户空间
- 新建一个独立目录(如
~/netlink_test/user/)存放用户空间相关文件
1
2
3
|
mkdir -p ~/netlink_test/user/
cd ~/netlink_test/user/
touch nl_test_user.c
|
- 编写代码:
nl_test_user.c:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <string.h>
#include <linux/netlink.h>
#include <stdint.h>
#include <unistd.h>
#include <errno.h>
// 指定 Netlink 协议号、消息长度
#define NETLINK_TEST 30
#define MSG_LEN 125
#define MAX_PLOAD 125
// user_msg_info 用于接收内核返回的数据,包含 Netlink 消息头和消息体
typedef struct _user_msg_info
{
struct nlmsghdr hdr;
char msg[MSG_LEN];
} user_msg_info;
int main(int argc, char **argv)
{
int skfd;
int ret;
user_msg_info u_info;
socklen_t len;
struct nlmsghdr *nlh = NULL;
struct sockaddr_nl saddr, daddr;
char *umsg = "Hello Netlink protocol.";
/* 创建NETLINK socket 协议号为 30(自定义) */
skfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
if(skfd == -1)
{
perror("\nError:Create socket error.\n");
return -1;
}
// 绑定本地 Netlink 地址,nl_pid 用当前进程 PID,表示用户空间的“端口”
memset(&saddr, 0, sizeof(saddr));
saddr.nl_family = AF_NETLINK; // AF_NETLINK
saddr.nl_pid = getpid(); // 端口号(port ID)
saddr.nl_groups = 0;
if(bind(skfd, (struct sockaddr *)&saddr, sizeof(saddr)) != 0)
{
perror("\nError:bind() error.\n");
close(skfd);
return -1;
}
// 设置目标地址(内核),目标地址的 nl_pid 为 0,表示消息发往内核
memset(&daddr, 0, sizeof(daddr));
daddr.nl_family = AF_NETLINK;
daddr.nl_pid = 0; // to kernel
daddr.nl_groups = 0;
// 构造并发送消息
nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PLOAD));
// 构造 Netlink 消息头
memset(nlh, 0, sizeof(struct nlmsghdr));
nlh->nlmsg_len = NLMSG_SPACE(MAX_PLOAD);
nlh->nlmsg_flags = 0;
nlh->nlmsg_type = 0;
nlh->nlmsg_seq = 0;
nlh->nlmsg_pid = saddr.nl_pid; //self port
// 将用户消息拷贝到消息体
memcpy(NLMSG_DATA(nlh), umsg, strlen(umsg));
// 通过 sendto 发送消息到内核
ret = sendto(skfd, nlh, nlh->nlmsg_len, 0, (struct sockaddr *)&daddr, sizeof(struct sockaddr_nl));
if(!ret)
{
perror("\nError:sendto error.\n");
close(skfd);
exit(-1);
}
printf("\nApplication-->Send to kernel:%s\n\n", umsg);
// 阻塞等待内核返回消息,收到后存储到 u_info 结构体
memset(&u_info, 0, sizeof(u_info));
len = sizeof(struct sockaddr_nl);
ret = recvfrom(skfd, &u_info, sizeof(user_msg_info), 0, (struct sockaddr *)&daddr, &len);
if(!ret)
{
perror("\nError:recv form kernel error.\n");
close(skfd);
exit(-1);
}
// 打印从内核收到的消息,关闭 socket,释放内存
printf("\nApplication-->From kernel:%s\n\n", u_info.msg);
close(skfd);
free((void *)nlh);
return 0;
}
|
- 编译:
1
|
gcc -o nl_test_user nl_test_user.c
|
测试结果
- 运行用户空间程序:
- 用户空间程序输出:

- 用户程序会打印从内核收到的回复消息:
"Hello user's program."

- 查看内核日志接收到的用户消息

清理操作
- 卸载内核模块:
1
|
sudo rmmod nl_test_kernel
|
- 清理编译文件:
1
2
|
cd ~/netlink_test/kernel
make clean
|
字符设备触发
- 内核态驱动注册 netlink 用户自定义消息组
- 用户态程序通过 write 字符设备将 pid 发送给内核态驱动
- 用户态程序通过 ioctl 触发内核向该 pid 发送 Netlink 消息
- 内核态使用 netlink 单播/广播机制通知到用户态对应的 pid
- 用户态收到消息后打印出来自内核的消息
内核空间
- 新建一个独立目录(如
~/netlink_test/kernel/)存放内核相关文件
1
2
3
|
mkdir -p ~/netlink_test/kernel/nl_test2_kernel
cd ~/netlink_test/kernel/nl_test2_kernel
touch nl_test2_kernel.c Makefile
|
- 编写代码
nl_test2_kernel.c:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/netlink.h>
#include <linux/skbuff.h>
#include <net/netlink.h>
#include <net/net_namespace.h>
// 自定义的 Netlink 协议号、字符设备名、类名和 ioctl 命令号
#define NETLINK_USER 31
#define DEVICE_NAME "netlink_device"
#define CLASS_NAME "nlclass"
#define IOCTL_CMD 0x12345678 // Must match the command in the user program
// 全局变量:Netlink socket 指针、字符设备号、字符设备结构体、设备类指针和用户进程 PID
static struct sock *nl_sk = NULL;
static dev_t dev_num;
static struct cdev c_dev;
static struct class *cl;
static int user_pid = 0; // Store the user space process PID
// 向用户空间进程发送消息
static void send_to_user(void) {
struct sk_buff *skb;
char msg[] = "Hello from kernel!";
int size = NLMSG_SPACE(strlen(msg) + 1); // including null terminator
// 检查 user_pid 是否已设置
if (user_pid == 0) {
printk(KERN_ERR "User PID not set\n");
return;
}
// 创建 Netlink 消息缓冲区(skb)
skb = nlmsg_new(size, GFP_KERNEL);
if (!skb) {
printk(KERN_ERR "Failed to allocate new skb\n");
return;
}
// 填充 Netlink 消息头和数据
struct nlmsghdr *nlh = nlmsg_put(skb, 0, 0, NLMSG_DONE, strlen(msg) + 1, 0);
if (!nlh) {
nlmsg_free(skb);
printk(KERN_ERR "Failed to put nlmsghdr\n");
return;
}
memcpy(NLMSG_DATA(nlh), msg, strlen(msg) + 1);
// nlh->nlmsg_len = strlen(msg) + 1;
// 发送消息到用户空间指定 PID
int res = netlink_unicast(nl_sk, skb, user_pid, MSG_DONTWAIT);
if (res < 0) {
printk(KERN_ERR "Error while sending to user: %d\n", res);
}
}
static long device_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
// 用户空间通过 ioctl 命令 IOCTL_CMD 触发内核向用户空间发送 Netlink 消息。
switch (cmd) {
case IOCTL_CMD:
send_to_user();
break;
default:
return -ENOTTY;
}
return 0;
}
static ssize_t device_write(struct file *filp, const char __user *buf, size_t len, loff_t *off) {
char kbuf[10];
if (len > sizeof(kbuf)) {
return -EINVAL;
}
if (copy_from_user(kbuf, buf, len)) {
return -EFAULT;
}
kbuf[len] = '\0';
// 内核解析 PID 并保存到 user_pid
user_pid = simple_strtol(kbuf, NULL, 10);
printk(KERN_INFO "User PID set to %d\n", user_pid);
return len;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
// 触发消息发送
.unlocked_ioctl = device_ioctl,
// 设置 PID
.write = device_write,
};
static int __init my_init(void) {
struct netlink_kernel_cfg cfg = {
.input = NULL,
.groups = 0,
.flags = 0,
};
printk(KERN_INFO "Module loaded\n");
// 创建 Netlink socket
nl_sk = netlink_kernel_create(&init_net, NETLINK_USER, &cfg);
if (!nl_sk) {
printk(KERN_ALERT "Error creating socket.\n");
return -ENOMEM;
}
// 注册字符设备号,初始化并添加字符设备
alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
cdev_init(&c_dev, &fops);
cdev_add(&c_dev, dev_num, 1);
// 创建设备类和设备节点 /dev/netlink_device
cl = class_create(CLASS_NAME);
device_create(cl, NULL, dev_num, NULL, DEVICE_NAME);
return 0;
}
static void __exit my_exit(void) {
if (nl_sk) {
// 释放 Netlink socket
netlink_kernel_release(nl_sk);
nl_sk = NULL;
}
// 销毁设备节点、类,注销字符设备号,删除字符设备
device_destroy(cl, dev_num);
class_unregister(cl);
class_destroy(cl);
unregister_chrdev_region(dev_num, 1);
cdev_del(&c_dev);
printk(KERN_INFO "Module unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wyatt");
MODULE_DESCRIPTION("A simple Netlink example with ioctl and write");
|
Makefile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
# 定义模块名,最终会生成 nl_test2_kernel.ko 文件
MODULE_NAME :=nl_test2_kernel
# 指定要编译的模块对象文件
obj-m:=$(MODULE_NAME).o
# 指定内核源码的构建目录,通常用于内核模块编译
KERNELDIR ?=/lib/modules/$(shell uname -r)/build
# 当前目录
PWD :=$(shell pwd)
# 编译模块。调用内核的 Makefile,M=$(PWD) 表示在当前目录下查找模块源码
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
# 清理编译生成的文件
clean:
rm -rf *.o *.ko *.mod.c *.symvers *.order
|
- 编译模块:在当前目录生成
nl_test2_kernel.ko
- 加载模块到内核
1
|
sudo insmod nl_test2_kernel.ko
|
- 验证模块是否加载:
1
|
dmesg | tail # 查看内核日志,确认初始化消息
|

1
|
lsmod | grep nl_test2_kernel # 检查模块列表
|

内核现在就可以处理NETLINK_USER的消息了
用户空间
- 新建一个独立目录(如
~/netlink_test/user/)存放用户空间相关文件
1
2
3
|
mkdir -p ~/netlink_test/user/nl_test2_user
cd ~/netlink_test/user/nl_test2_user
touch nl_test2_user.c
|
- 编写代码:
nl_test2_user.c:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <string.h>
// 定义 Netlink 协议号、最大消息长度、ioctl 命令号(需与内核模块一致)
#define NETLINK_USER 31
#define MAX_PAYLOAD 1024 /* maximum payload size*/
#define IOCTL_CMD 0x12345678 // Must match the command in the kernel module
int main(int argc, char* argv[]) {
struct sockaddr_nl src_addr, dest_addr;
struct nlmsghdr *nlh = NULL;
struct iovec iov;
int sock_fd;
struct msghdr msg;
int fd;
char pid_str[10];
// Open the device file
// 打开内核模块创建的字符设备
fd = open("/dev/netlink_device", O_RDWR);
if (fd < 0) {
perror("Cannot open device file");
exit(EXIT_FAILURE);
}
// Write the current PID to the device file
// 获取当前进程 PID,转为字符串写入设备
snprintf(pid_str, sizeof(pid_str), "%d", getpid());
if (write(fd, pid_str, strlen(pid_str)) != strlen(pid_str)) {
perror("Write to device file failed");
close(fd);
exit(EXIT_FAILURE);
}
// Close the device file after writing the PID
close(fd);
// Create Netlink socket
// 创建 Netlink socket,协议号与内核模块一致
sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_USER);
if (sock_fd < 0) {
perror("Socket creation failed");
return -1;
}
// 绑定本地地址,nl_pid 设置为本进程 PID,nl_groups 设置为 1(单播)
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid(); /* self pid */
src_addr.nl_groups = 0; /* unicast */
bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
printf("Waiting for message from kernel...\n");
// Allocate memory for nlmsghdr and set up iovec and msghdr
// 分配 Netlink 消息头缓冲区
nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
if (!nlh) {
perror("Failed to allocate memory for nlmsghdr");
close(sock_fd);
exit(EXIT_FAILURE);
}
// 设置 iovec 和 msghdr,用于后续 recvmsg 接收消息
iov.iov_base = (void *)nlh;
iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void *)&dest_addr;
msg.msg_namelen = sizeof(dest_addr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
// Trigger the ioctl call to send a message from the kernel
// 通过 ioctl 触发内核发送消息
fd = open("/dev/netlink_device", O_RDWR);
if (fd < 0) {
perror("Cannot open device file for ioctl");
free(nlh);
close(sock_fd);
exit(EXIT_FAILURE);
}
if (ioctl(fd, IOCTL_CMD, 0) < 0) {
perror("IOCTL failed");
close(fd);
free(nlh);
close(sock_fd);
exit(EXIT_FAILURE);
}
close(fd);
// 使用 recvmsg 阻塞等待内核消息
ssize_t len = recvmsg(sock_fd, &msg, 0);
if (len < 0) {
perror("recvmsg failed");
free(nlh);
close(sock_fd);
exit(EXIT_FAILURE);
}
// Ensure the received message is valid
// 检查消息长度,确保消息有效
// if (nlh->nlmsg_len < NLMSG_HDRLEN || len != nlh->nlmsg_len) {
if (nlh->nlmsg_len < NLMSG_HDRLEN || len < nlh->nlmsg_len) {
//if (nlh->nlmsg_len < NLMSG_HDRLEN){
// fprintf(stderr, "Invalid message length:%d. NLMSG_HDRLEN %d\n", nlh->nlmsg_len, NLMSG_HDRLEN);
fprintf(stderr, "Invalid message length:%d. NLMSG_HDRLEN %d, recvmsg returned %zd\n", nlh->nlmsg_len, NLMSG_HDRLEN, len);
// 释放分配的内存,关闭 socket
free(nlh);
close(sock_fd);
exit(EXIT_FAILURE);
}
// 打印收到的消息内容
printf("Received message payload: %s\n", (char*)NLMSG_DATA(nlh));
free(nlh);
close(sock_fd);
return 0;
}
|
- 编译:
1
|
gcc -o nl_test2_user nl_test2_user.c
|
测试结果
- 运行用户空间程序:
- 用户空间程序输出:

- 用户程序会打印从内核收到的回复消息:
"Hello from kernel!"
- 查看内核日志接收到的用户消息

清理操作
- 卸载内核模块:
1
|
sudo rmmod nl_test2_kernel
|
- 清理编译文件:
1
2
|
cd ~/netlink_test/kernel/nl_test2_kernel
make clean
|
内核->用户
- 注册机制:用户空间程序启动后首先发送"REGISTER"消息告知内核它的PID,这样内核就知道向哪个PID发送消息。
- 定时发送:内核模块使用定时器每2秒主动发送一条消息到用户空间。
内核空间
- 新建一个独立目录(如
~/netlink_test3/kernel/)存放内核相关文件
1
2
3
|
mkdir -p ~/netlink_test/kernel/test3/
cd ~/netlink_test/kernel/test3/
touch nl_test3_kernel.c Makefile
|
- 编写代码
nl_test3_kernel.c:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
#include <linux/module.h>
#include <linux/netlink.h>
#include <linux/skbuff.h>
#include <net/sock.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
#define NETLINK_TEST 31
static struct sock *nl_sk = NULL;
static int user_pid = 0;
static struct timer_list my_timer;
// 发送消息到用户空间
static void send_to_user(const char *msg) {
struct sk_buff *skb;
struct nlmsghdr *nlh;
int msg_size = strlen(msg) + 1;
if (user_pid == 0) {
printk(KERN_INFO "No user process registered yet\n");
return;
}
skb = nlmsg_new(msg_size, GFP_ATOMIC);
if (!skb) {
printk(KERN_ERR "Failed to allocate skb\n");
return;
}
nlh = nlmsg_put(skb, 0, 0, NLMSG_DONE, msg_size, 0);
memcpy(nlmsg_data(nlh), msg, msg_size);
netlink_unicast(nl_sk, skb, user_pid, MSG_DONTWAIT);
}
// 定时器回调函数
static void timer_callback(struct timer_list *timer) {
static int count = 0;
char msg[100];
snprintf(msg, sizeof(msg), "Kernel message %d", ++count);
send_to_user(msg);
// 重新设置定时器
mod_timer(&my_timer, jiffies + msecs_to_jiffies(2000)); // 每2秒发送一次
}
// 接收用户空间消息
static void nl_recv_msg(struct sk_buff *skb) {
struct nlmsghdr *nlh = nlmsg_hdr(skb);
char *msg = NLMSG_DATA(nlh);
if (strncmp(msg, "REGISTER", 8) == 0) {
user_pid = nlh->nlmsg_pid;
printk(KERN_INFO "Registered user PID: %d\n", user_pid);
send_to_user("Registration successful");
} else {
printk(KERN_INFO "Kernel received: %s\n", msg);
}
}
static int __init nl_init(void) {
struct netlink_kernel_cfg cfg = {
.input = nl_recv_msg,
};
nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST, &cfg);
if (!nl_sk) {
printk(KERN_ERR "Failed to create netlink socket\n");
return -ENOMEM;
}
// 初始化定时器
timer_setup(&my_timer, timer_callback, 0);
mod_timer(&my_timer, jiffies + msecs_to_jiffies(2000));
printk(KERN_INFO "Netlink kernel module loaded\n");
return 0;
}
static void __exit nl_exit(void) {
del_timer(&my_timer);
if (nl_sk) {
netlink_kernel_release(nl_sk);
nl_sk = NULL;
}
printk(KERN_INFO "Netlink kernel module unloaded\n");
}
module_init(nl_init);
module_exit(nl_exit);
MODULE_LICENSE("GPL");
|
Makefile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
# 定义模块名,最终会生成 nl_test3_kernel.ko 文件
MODULE_NAME :=nl_test3_kernel
# 指定要编译的模块对象文件
obj-m:=$(MODULE_NAME).o
# 指定内核源码的构建目录,通常用于内核模块编译
KERNELDIR ?=/lib/modules/$(shell uname -r)/build
# 当前目录
PWD :=$(shell pwd)
# 编译模块。调用内核的 Makefile,M=$(PWD) 表示在当前目录下查找模块源码
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD)
# 清理编译生成的文件
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
|
- 编译模块:在当前目录生成
nl_test3_kernel.ko
- 加载模块到内核
1
|
sudo insmod nl_test3_kernel.ko
|
- 验证模块是否加载:
1
2
|
dmesg | tail # 查看内核日志,确认初始化消息
lsmod | grep nl_test3_kernel # 检查模块列表
|
用户空间
- 新建一个独立目录(如
~/netlink_test/user/)存放用户空间相关文件
1
2
3
|
mkdir -p ~/netlink_test/user/test3
cd ~/netlink_test/user/test3
touch nl_test3_user.c
|
- 编写代码:
nl_test3_user.c:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
|
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#define NETLINK_TEST 31
#define MAX_PAYLOAD 1024
int main() {
struct sockaddr_nl src_addr, dest_addr;
struct nlmsghdr *nlh = NULL;
struct msghdr msg;
struct iovec iov;
int sock_fd;
// 创建Netlink socket
sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
if (sock_fd < 0) {
perror("socket");
return -1;
}
// 绑定本地地址
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid(); // 使用进程ID作为端口号
src_addr.nl_groups = 0; // 单播
if (bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr)) < 0) {
perror("bind");
close(sock_fd);
return -1;
}
// 向内核注册我们的PID
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0; // 发送到内核
dest_addr.nl_groups = 0; // 单播
// 准备注册消息
nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
nlh->nlmsg_pid = getpid();
nlh->nlmsg_flags = 0;
strcpy(NLMSG_DATA(nlh), "REGISTER");
// 设置消息结构
iov.iov_base = (void *)nlh;
iov.iov_len = nlh->nlmsg_len;
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void *)&dest_addr;
msg.msg_namelen = sizeof(dest_addr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
// 发送注册消息
printf("Registering with kernel...\n");
if (sendmsg(sock_fd, &msg, 0) < 0) {
perror("sendmsg");
free(nlh);
close(sock_fd);
return -1;
}
// 准备接收消息
printf("Waiting for messages from kernel...\n");
while (1) {
char buffer[1024];
struct nlmsghdr *nlh = (struct nlmsghdr *)buffer;
memset(&msg, 0, sizeof(msg));
iov.iov_base = (void *)nlh;
iov.iov_len = sizeof(buffer);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
// 接收内核消息
int len = recvmsg(sock_fd, &msg, 0);
if (len < 0) {
perror("recvmsg");
break;
}
// 处理接收到的消息
printf("Received from kernel: %s\n", (char *)NLMSG_DATA(nlh));
}
free(nlh);
close(sock_fd);
return 0;
}
|
- 编译:
1
|
gcc -o nl_test3_user nl_test3_user.c
|
测试结果
- 运行用户空间程序:
- 观察输出:用户程序会每2秒收到一条来自内核的消息。
1
2
3
|
root@localhost [~/netlink_test/user/test3] ➜ sudo ./nl_test3_user
User space ready (PID=3695)
Waiting for kernel message...
|
内核:
1
2
3
4
5
6
7
8
9
10
11
|
root@localhost [/] ➜ dmesg | tail # 查看内核日志,确认初始化消息 [23:27:02]
[13520.575856] No user process registered yet
[13522.623843] No user process registered yet
[13524.671834] No user process registered yet
[13526.720822] No user process registered yet
[13528.767820] No user process registered yet
[13530.815806] No user process registered yet
[13532.863796] No user process registered yet
[13534.911789] No user process registered yet
[13536.959782] No user process registered yet
[13538.464832] Registered user PID: 4599
|
清理操作
- 卸载内核模块:
1
|
sudo rmmod nl_test3_kernel
|
- 清理编译文件:
1
2
|
cd ~/netlink_test/kernel
make clean
|
未整理
netlink协议族组织形式
netlink 协议族、子协议族、子协议、命令,组织结构如下:

自定义netlink协议
如何在NETLINK_GENERIC中新增 netlink 协议?
模块初始化时,通过genl_register_family注册通用 netlink 协议族,将命令以及处理函数进行注册
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/* initialisation/exit functions */
int __init nl80211_init(void)
{
int err;
err = genl_register_family(&nl80211_fam);
if (err)
return err;
err = netlink_register_notifier(&nl80211_netlink_notifier);
if (err)
goto err_out;
return 0;
err_out:
genl_unregister_family(&nl80211_fam);
return err;
}
|
genl_register_family解释:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/**
* genl_register_family - register a generic netlink family
* @family: generic netlink family
*
* Registers the specified family after validating it first. Only one
* family may be registered with the same family name or identifier.
*
* The family's ops, multicast groups and module pointer must already
* be assigned.
*
* Return 0 on success or a negative error code.
*/
int genl_register_family(struct genl_family *family)
|
genl_ops nl80211_ops结构体:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
static const struct genl_ops nl80211_ops[] = {
{
.cmd = NL80211_CMD_GET_WIPHY,
.doit = nl80211_get_wiphy,
.dumpit = nl80211_dump_wiphy,
.done = nl80211_dump_wiphy_done,
.policy = nl80211_policy,
/* can be retrieved by unprivileged users */
.internal_flags = NL80211_FLAG_NEED_WIPHY |
NL80211_FLAG_NEED_RTNL,
},
{
.cmd = NL80211_CMD_SET_WIPHY,
.doit = nl80211_set_wiphy,
.policy = nl80211_policy,
.flags = GENL_UNS_ADMIN_PERM,
.internal_flags = NL80211_FLAG_NEED_RTNL,
},
......
}
|
加入netlink子协议族
如何将自定义 netlink 协议加入到 netlink 协议族中,与 NETLINK_GENERIC 同一级别?
- 只需定义一个 netlink 协议号即可,由于 netlink 对消息体格式不做强制要求,可以传输简单的字符串;实际使用中,不建议这样做,但作为学习,可以简单的这样操作;
- 使用中增加自定义 netlink 协议,建议加入到 NETLINK_GENERIC 协议族中,类似 nl80211 这样;
参考