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和数据结构大多与用户态不太相同,这个时候需要查看内核中的相关代码寻找线索。

可以使用在线文档工具查看相关的函数定义。

Lei Yang
Lei Yang
PhD candidate

My research interests include visual speech recognition and semantics segmentation.