Netlink Socket内核通信
最近接手了一个Linux下内核编程的项目,在阅读项目原有代码的基础上,学到了很多新知识,总结一下记录在这里。
在这个项目中,编写内核模块进行操作,与用户态进程之间使用Netlink Socket进行通信。
用户空间和内核空间相互通信的方式有三种,/proc目录、ioctl和neltink。使用netlink可以很很简单的建立起内核态和用户态的全双工通信,并且可以使用简单的socket语法进行操作,比较简单。需要注意的是,内核中已经定义了多种netlink消息类型/协议,如果不使用现成的消息类型可以添加一个新的协议定义。
需要注意的是,使用netlink通信的用户态和内核态的语法有些许的不同。而且在netlink的数据传输并不是同步的,而是将报文信息加到了接收者的接受队列中,因此netlink socket支持iov机制,也就是在一次系统调用的过程中,将多个报文信息打包发送。
环境
操作系统:Ubuntu 18.10(使用Linux 4.18.0-25内核)
基本数据结构
// Netlink使用sockaddr_nl地址
struct sockaddr_nl {
__kernel_sa_family_t nl_family;
unsigned short nl_pad;
__u32 nl_pid;
__u32 nl_groups;
};
// struct nlmsghd 是netlink消息头
struct nlmsghdr {
__u32 nlmsg_len;
__u16 nlmsg_type;
__u16 nlmsg_flags;
__u32 nlmsg_seq;
__u32 nlmsg_pid;
};
/*
iov_base: iov_base指向数据包缓冲区,即参数buff,
iov_len是buff的长度。
msghdr中允许一次传递多个buff,以数组的形式组织在 msg_iov中,msg_iovlen就记录数组的长度
*/
struct iovec {
void *iov_base;
size_t iov_len;
};
// msghdr是发送的报文信息的头部
struct msghdr {
void *msg_name;
socklen_t msg_namelen;
struct iovec *msg_iov;
size_t msg_iovlen;
void *msg_control;
size_t msg_controllen;
int msg_flags;
};
基本操作
用户态:相关变量
// 创建所需的变量
struct sockaddr_nl src_sockaddr, dest_sockaddr;
struct nlmsghdr * nlh = NULL;
struct msghdr msg;
struct iovec iov;
// 变量初始化
nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(sizeof(unsigned long)));
memset(& src_sockaddr, 0, sizeof(struct sockaddr_nl));
memset(& dest_sockaddr, 0, sizeof(struct sockaddr_nl));
memset(nlh, 0, NLMSG_SPACE(sizeof(unsigned long)));
memset(& msg, 0, sizeof(struct msghdr));
用户态:创建socket
如果不使用内核中定义好的协议类型,可以自己增加一个新的协议定义作为socket的初始化参数。
#define NETLINK_SAFE 30
server_sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_SAFE);
用户态:绑定地址
通常使用进程号作为用户空间的socket地址。实际上只是一个socket的标示号码,可以任意设置,对于同一个进程的不同线程可以使用不同的标示号。
由于netlink支持多播,还需要指定多播组,指定为0时表示不开启多播功能。
src_sockaddr.nl_family = AF_NETLINK;
src_sockaddr.nl_pid = getpid();
src_sockaddr.nl_groups = 0;
// 绑定socket和地址
bind(server_sock, (struct sockaddr *)& src_sockaddr, sizeof(struct sockaddr_nl));
用户态:构建并发送消息
使用netlink通信时需要在消息头部指定通信目标,因此需要首先建立接受方的用户地址,也就是核心态的内核模块。然后依次设置netlink消息头和每一条信息的消息头。
需要注意使用宏NLMSG_DATA获得netlink报文的实际数据地址。使用宏NLMSG_SPACE获得报文的实际数据大小。
// 设置核心态用户地址,核心态的pid必须设置为0
dest_sockaddr.nl_family = AF_NETLINK;
dest_sockaddr.nl_pid = 0;
dest_sockaddr.nl_groups = 0;
// 设置netlink socket的信息头部
nlh -> nlmsg_len = NLMSG_SPACE(sizeof(unsigned long));
nlh -> nlmsg_pid = getpid();
nlh -> nlmsg_flags = 0;
// 设置iov 可以把多个信息通过一次系统调用发送
iov.iov_base = (void *)nlh;
iov.iov_len = NLMSG_SPACE(sizeof(unsigned long));
// 设置接收地址
msg.msg_name = (void *)& dest_sockaddr;
msg.msg_namelen = sizeof(struct sockaddr_nl);
msg.msg_iov = & iov;
msg.msg_iovlen = 1;
// 填充消息内容
* (unsigned long *)NLMSG_DATA(nlh) = (unsigned long)0xffffffff << 32;
// 发送和接收消息
sendmsg(server_sock, & msg, 0);
recvmsg(server_sock, & msg, 0);
内核态:相关变量
static struct sock* socket;
static int pid = 0;
static int ino_len = sizeof(unsigned long);
static atomic_t sequence = ATOMIC_INIT(0);
内核态:初始化
在内核模块的编写中需要指定初始化函数,在初始化函数中创建内核态的netlink socket。由于使用异步通信,需要在内核态中定义接收回调函数,对于接收到的消息进行处理,内核态接收到的消息类型为sk_buff类型,可以转换为nlmsghdr结构并进一步提取信息。
static void nl_receive_callback(struct sk_buff* skb){
// 转换格式
struct nlmsghdr* nlh = (struct nlmsghdr*)skb->data;
// 获得用户凭证
int pid = NETLINK_CREDS(skb)->pid;
}
static int __init netlink_init(void) {
// 设置接收到消息的回调函数
struct netlink_kernel_cfg cfg = {
.input = nl_receive_callback,
};
int i;
// 创建内核态netlink套接字
socket = netlink_kernel_create(&init_net, NETLINK_SAFE, &cfg);
return 0;
}
内核态:资源回收
在内核模块的编写中需要指定出口函数,进行资源的回收。
static void __exit netlink_exit(void) {
if (socket) {
netlink_kernel_release(socket);
}
}
内核态:构建和发送数据
// 内核态存储网络结构的数据为sk_buff
// 首先创建 sk_buff空间,
skb = nlmsg_new(msg_len, GFP_ATOMIC);
if (!skb) {
return 0;
}
// 设置netlink消息头部
nlh = nlmsg_put(skb, 0, 0, NLMSG_DONE, msg_len, 0);
seq = atomic_inc_return(&sequence);
nlh->nlmsg_seq = seq;
memcpy(NLMSG_DATA(nlh), msg_buf, meg_len)
// 单播类型发送数据
// 用户态使用pid作为标识符
nlmsg_unicast(socket, skb, pid);
总结
在Linux内核编程的过程中,需要注意使用的API和数据结构大多与用户态不太相同,这个时候需要查看内核中的相关代码寻找线索。
可以使用在线文档工具查看相关的函数定义。