点击上方“Linux随笔录”,选择“置顶/星标公众号”
前言
本篇环境
netlink的介绍
netlink的数据结构
netlink常用宏
netlink常用的API
创建netlink消息
释放netlink消息
创建skb
释放skb
netlink消息加入到skb
获取struct nlmsghdr数据结构
获取对应的payload
发送单播信息
发送多播信息
用户空间socket操作API
socket
bind
sendto
recvfrom
netlink的消息格式
netlink的使用
内核空间netlink的使用
用户空间netlink的使用
总结
用户空间访问内核空间通常由很多方式如open
,write
,read
,ioctl
,mmap
,socket
,poll
等。今天讲的netlink
也是一种用户空间访问内核空间的方式,它是在内核与用户态之间进行双向通信,在有线无线网络中用的比较多,所以本篇探讨下netlink
的学习和使用
硬件平台:飞凌OK3588开发板
内核源码:5.10.66-rt53
编译环境:Ubuntu 20.04 LTS
编译工具链:aarch64-linux-gnu-
Netlink
是 Linux 内核提供的一种机制,用于在内核空间和用户空间之间进行通信。Netlink
套接字允许内核模块和用户空间应用程序进行高效的双向通信,Netlink
套接字可以用于各种网络操作,支持多个协议族,如NETLINK_ROUTE
、NETLINK_NETFILTER
等。常用于网络配置、路由、设备管理等场景。
netlink和其他方法实现用户与内核通信的方法有何优势
struct sockaddr_nl: 表示netlink
通信地址;
file: include/uapi/linux/netlink.h
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 */
};
参数说明:
AF_NETLINK
,表示netlink
协议族netlink
并没有要求该字段是进程ID,它可以是任何值,只需要保证其唯一性;使用线程组ID不过是方便而已;nl_pid是一个单播地址netlink
协议族最多支持32个多播组其中netlink
协议簇包含多个协议,内核中规定最大值是32。理论上是32以内没有被占用的协议号用户都可以用于自定义netlink
。下面是内核中规定netlink
协议簇的内容
file: include/uapi/linux/netlink.h
#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
struct nlmsghdr:描述netlink message的消息头部的结构体
file: include/uapi/linux/netlink.h
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 */
};
参数说明:
#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 */
/* Flags values */
#define NLM_F_REQUEST 0x01 /* It is request message. */
#define NLM_F_MULTI 0x02 /* Multipart message, terminated by NLMSG_DONE */
#define NLM_F_ACK 0x04 /* Reply with ack, with zero or error code */
#define NLM_F_ECHO 0x08 /* Echo this request */
#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 */
#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 */
/* Flags for ACK message */
#define NLM_F_CAPPED 0x100 /* request was capped */
#define NLM_F_ACK_TLVS 0x200 /* extended ACK TVLs were included */
netlink_kernel_cfg: 描述netlink配置选项参数的结构体
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);
};
常用的参数说明
groups: 这个成员指定了该 Netlink 套接字可以接收的多播组的掩码。多播组用于向多个进程发送消息。
input: 这是一个函数指针,用于指定一个回调函数,当有消息到达该Netlink
套接字时,将调用此函数。
bind: 这是一个可选的函数指针,允许用户指定自定义的绑定行为。
unbind: 这是一个可选的函数指针,允许用户指定自定义的解绑行为。
nlattr:描述消息体
struct nlattr {
__u16 nla_len;
__u16 nla_type;
};
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
用于得到不小于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_LENGTH(len)且字节对齐的最小数值
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
用于取得消息的数据部分的首地址,设置和读取消息数据部分时需要使用该宏
#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))
判断消息是否>len
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))
用于返回payload的长度
下面介绍下本实验会用到的一些API函数,这些API函数定义在include/linux/netlink.h
netlink_kernel_create:创建netlink
内核的消息
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);
}
参数:
netlink
子协议族,如NETLINK_GENERI
C。填的是你用户空间传进来的netlink
协议族netlink
内核配置参数struct netlink_kernel_cfg数据结构。 其中,input是该内核netlink
模块收到消息后的处理回调函数返回值:netlink
消息句柄
netlink_kernel_release:释放netlink
内核的消息
void netlink_kernel_release(struct sock *sk)
参数:
nlmsg_new:创建一个新的skb
struct sk_buff *nlmsg_new(size_t payload, gfp_t flags)
参数:
返回值:分配得到socket
缓冲区的指针。
nlmsg_free:释放nlmsg_new()创建的skb
static inline void nlmsg_free(struct sk_buff *skb)
{
kfree_skb(skb);
}
参数:
socket
缓冲区的指针nlmsg_put:将一个新的netlink
消息加入到skb中。如果skb无法存放消息则返回NULL
static inline struct nlmsghdr *nlmsg_put(struct sk_buff *skb, u32 portid, u32 seq,
int type, int payload, int flags)
参数:
0
表示内核发送消息给用户空间,或者使用目标用户空间进程的 PID。RTM_NEWLINK
或RTM_DELLINK
等,具体类型取决于应用场景。NLM_F_REQUEST
(表示这是一个请求消息)和NLM_F_ACK
(表示需要确认消息)。nlmsg_hdr:从sk_buff->data获取struct nlmsghdr数据结构
static inline struct nlmsghdr *nlmsg_hdr(const struct sk_buff *skb)
{
return (struct nlmsghdr *)skb->data;
}
参数:
nlmsg_data:根据nlmsghdr
指针获取对应的payload
static inline void *nlmsg_data(const struct nlmsghdr *nlh)
{
return (unsigned char *) nlh + NLMSG_HDRLEN;
}
参数:
netlink_unicast:用来发送单播信息
int netlink_unicast(struct sock *ssk, struct sk_buff *skb,
u32 portid, int nonblock)
参数:
0
表示内核发送消息给用户空间,或者使用目标用户空间进程的 PID。netlink_broadcast:用来发送多播信息
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, u32 portid,
u32 group, gfp_t allocation)
参数:
0
表示内核发送消息给用户空间,或者使用目标用户空间进程的 PID。GFP_ATOMIC
用于中断上下文,而GFP_KERNEL
用于其他场合。这个参数的存在是因为该API可能需要分配一个或多个缓冲区来对多播消息进行cloneint socket(int domain, int type, int protocol);
该函数用来创建一个套接字,并返回一个描述符,该描述符可以用来访问该套接字。protocol参数设置为0表示使用默认协议。
int bind( int socket, const struct sockaddr *address, size_t address_len);
把通过socket()创建的套接字命名,从而让它可以被其他进程使用
int sendto(int sockfd, void *buffer, size_t len, int flags, struct sockaddr *to, socklen_t tolen);
把缓冲区buffer中的信息送给制定的IP端口程序,buffer存放将要发送的数据,len是buffer长度,to是要发送数据到的程序IP端口,tolen是to参数长度。
int recvfrom(int sockfd, void *buffer, size_t len,int flags, struct sockaddr *src_from, socklen_t *src_len);
把发送给程序的信息存储在缓冲区buffer中,并记录数据来源的程序IP端口。buffer存放接收的数据,len是buffer长度,src_from是数据来源程序IP端口,src_len是src_from长度。
netlink消息由两部分组成:消息头和消息体;消息头固定位16字节,消息体长度可变,最后要补齐NLMSG_ALLGNTO;
对netlink
的测试分为两部分,一部分是内核提供接收用户空间消息,并响应发送功能;另一部分是用户空间发送netlink
到内核,并等待回复。
file: netlink_test.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <net/sock.h>
#include <linux/netlink.h>
#define NETLINK_TEST 30
#define MSG_LEN 128
#define USER_PORT 100
struct sock *nl_sk = NULL;
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_KERNEL);
if(!nl_skb)
{
printk("netlink alloc failure\n");
return -1;
}
/* 设置netlink消息头部 */
nlh = nlmsg_put(nl_skb, 0, 0, NETLINK_TEST, len, 0);
if(nlh == NULL)
{
printk("nlmsg_put failaure \n");
nlmsg_free(nl_skb);
return -1;
}
/* 拷贝pbuf的缓冲区到netlink消息数据 */
memcpy(nlmsg_data(nlh), pbuf, len);
/* 发送单播数据 */
ret = netlink_unicast(nl_sk, 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 users!!!";
//如果skb数据长度大于等于netlink头部大小
if(skb->len >= nlmsg_total_size(0))
{
//获得skb的数据部分
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 */
};
static int __init netlink_init(void)
{
/* create netlink socket */
nl_sk = (struct sock *)netlink_kernel_create(&init_net, NETLINK_TEST, &cfg);
if(nl_sk == NULL)
{
printk("netlink_kernel_create error !\n");
return -1;
}
printk(KERN_INFO "Netlink socket created.\n");
return 0;
}
static void __exit netlink_exit(void)
{
if (nl_sk){
netlink_kernel_release(nl_sk); /* release ..*/
nl_sk = NULL;
}
printk(KERN_INFO "Netlink socket released.\n");
}
module_init(netlink_init);
module_exit(netlink_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Pan Shuai");
MODULE_DESCRIPTION("netlink example");
file:netlink.c
#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>
#define NETLINK_TEST 30
#define MSG_LEN 128
#define MAX_PLOAD 128
typedef struct _user_msg_info
{
struct nlmsghdr hdr;
char msg[MSG_LEN];
} user_msg_info;
int main(int argc, char **argv)
{
int sock_fd;
int ret;
user_msg_info u_info;
socklen_t len;
struct nlmsghdr *nlh = NULL;
struct sockaddr_nl src_addr, dest_addr;
char *umsg = "hello netlink!!";
/* 创建NETLINK socket */
sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
if(sock_fd == -1)
{
perror("create socket error\n");
return -1;
}
// 填充源地址
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK; //AF_NETLINK
src_addr.nl_pid = 100; //端口号(port ID)
src_addr.nl_groups = 0;
// 绑定套接字
if(bind(sock_fd, (struct sockaddr *)&src_addr, sizeof(src_addr)) != 0)
{
perror("bind() error\n");
close(sock_fd);
return -1;
}
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0; // to kernel
dest_addr.nl_groups = 0;
nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PLOAD));
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 = src_addr.nl_pid; //self port
// 填充消息数据
memcpy(NLMSG_DATA(nlh), umsg, strlen(umsg));
// 发送消息
ret = sendto(sock_fd, nlh, nlh->nlmsg_len, 0, (struct sockaddr *)&dest_addr, sizeof(struct sockaddr_nl));
if(!ret)
{
perror("sendto error\n");
close(sock_fd);
exit(-1);
}
printf("userspace send kernel:%s\n", umsg);
// 接收消息
memset(&u_info, 0, sizeof(u_info));
len = sizeof(struct sockaddr_nl);
ret = recvfrom(sock_fd, &u_info, sizeof(user_msg_info), 0, (struct sockaddr *)&dest_addr, &len);
if(!ret)
{
perror("recv form kernel error\n");
close(sock_fd);
exit(-1);
}
printf("userspace recv from kernel:%s\n", u_info.msg);
// 关闭套接字
close(sock_fd);
free((void *)nlh);
return 0;
}
注意:代码中的NETLINK_TEST
是我自定义的NETLINK
消息类型
放在板子上进行编译测试:
aarch64-linux-gnu-gcc netlink.c -o netlink
执行
从测试结果来看,编译测试看到打印先从用户空间发字符串"hello netlink"到内核空间,然后在收到内核空间发给用户空间字符串"hello users"。由此可见,netlink是可以在用户和内核空间进行双向通信的机制
本篇讲了下netlink的使用方法,学废的话一键三连支持下小编,欢迎关注公众号[Linux随笔录],继续输出Linux随笔知识
参考文章:
[1] https://blog.csdn.net/congchp/article/details/
[2] https://www.cnblogs.com/arnoldlu/p/9532254.html
end
往期推荐