Linux Netlink 套接字通信导致 VM 崩溃

Linux Netlink Socket Communication Crashes VM

我写了一个内核模块和用户空间程序,内核模块发送netlink多播消息,用户空间程序读取这些消息并打印出来。内核模块和用户空间程序可在此处获得 (https://github.com/akshayknarayan/netlink-test) and replicated below. The code was adapted from this post: Multicast from kernel to user space via Netlink in C

如果用户空间程序的第 69 行(对 usleep 的调用)被注释掉,那么一切正常;加载内核模块后,它会重复多播消息,用户空间程序将它们打印出来。

但是,如果用户空间程序的第 69 行未被注释,则在加载内核模块后的一秒内,我的 VM 会挂起并变得无响应。

为什么会这样?如何防止内核挂起?

Linux ubuntu-xenial 4.4.0-75-generic #96-Ubuntu SMP Thu Apr 20 09:56:33 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

用户空间程序:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <unistd.h>

/* Multicast group, consistent in both kernel prog and user prog. */
#define MYMGRP 22

int nl_open(void) {
    int sock;
    struct sockaddr_nl addr;
    int group = MYMGRP;

    sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_USERSOCK);
    if (sock < 0) {
        printf("sock < 0.\n");
        return sock;
    }

    memset((void *) &addr, 0, sizeof(addr));
    addr.nl_family = AF_NETLINK;
    addr.nl_pid = getpid();

    if (bind(sock, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
        printf("bind < 0.\n");
        return -1;
    }

    if (setsockopt(sock, 270, NETLINK_ADD_MEMBERSHIP, &group, sizeof(group)) < 0) {
        printf("setsockopt < 0\n");
        return -1;
    }

    return sock;
}

void nl_recv(int sock) {
    struct sockaddr_nl nladdr;
    struct msghdr msg;
    struct iovec iov;
    char buffer[65536];
    int ret;

    iov.iov_base = (void *) buffer;
    iov.iov_len = sizeof(buffer);
    msg.msg_name = (void *) &(nladdr);
    msg.msg_namelen = sizeof(nladdr);
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

    ret = recvmsg(sock, &msg, 0);
    if (ret < 0)
        printf("ret < 0.\n");
    else
        printf("Received message payload: %s\n", (char*) NLMSG_DATA((struct nlmsghdr *) &buffer));
}

int main(int argc, char *argv[]) {
    int nls;

    nls = nl_open();
    if (nls < 0)
        return nls;

    while (1) {
        nl_recv(nls);
        usleep(5000);
    }

    return 0;
}

内核模块:

#include <linux/module.h>
#include <linux/netlink.h>
#include <linux/skbuff.h>
#include <linux/gfp.h>
#include <net/sock.h>

#define MYMGRP 22

struct sock *nl_sk = NULL;
static struct timer_list timer;

void nl_send_msg(unsigned long data) {
    struct sk_buff *skb_out;
    struct nlmsghdr *nlh;
    int res;
    char *msg = "hello from kernel!\n";
    int msg_size = strlen(msg);

    skb_out = nlmsg_new(
        NLMSG_ALIGN(msg_size), // @payload: size of the message payload
        GFP_KERNEL             // @flags: the type of memory to allocate.
    );
    if (!skb_out) {
        printk(KERN_ERR "Failed to allocate new skb\n");
        return;
    }

    nlh = nlmsg_put(
        skb_out,    // @skb: socket buffer to store message in
        0,          // @portid: netlink PORTID of requesting application
        0,          // @seq: sequence number of message
        NLMSG_DONE, // @type: message type
        msg_size,   // @payload: length of message payload
        0           // @flags: message flags
    );

    memcpy(nlmsg_data(nlh), msg, msg_size+1);
    res = nlmsg_multicast(
            nl_sk,     // @sk: netlink socket to spread messages to
            skb_out,   // @skb: netlink message as socket buffer
            0,         // @portid: own netlink portid to avoid sending to yourself
            MYMGRP,    // @group: multicast group id
            GFP_KERNEL // @flags: allocation flags
    );
    if (res < 0) {
        printk(KERN_INFO "Error while sending to user: %d\n", res);
    } else {
        mod_timer(&timer, jiffies + msecs_to_jiffies(1));
    }
}

static int __init nl_init(void) {
    struct netlink_kernel_cfg cfg = {};

    printk(KERN_INFO "init NL\n");
    nl_sk = netlink_kernel_create(&init_net, NETLINK_USERSOCK, &cfg);
    if (!nl_sk) {
        printk(KERN_ALERT "Error creating socket.\n");
        return -10;
    }

    init_timer(&timer);
    timer.function = nl_send_msg;
    timer.expires = jiffies + 1000;
    timer.data = 0;
    add_timer(&timer);
    nl_send_msg(0);

    return 0;
}

static void __exit nl_exit(void) {
    printk(KERN_INFO "exit NL\n");
    del_timer_sync(&timer);
    netlink_kernel_release(nl_sk);
}

module_init(nl_init);
module_exit(nl_exit);

MODULE_LICENSE("GPL");

对于后代:我认为问题出在 nlmsg_new 中的分配,这不应发生在中断处理程序(定时器处理程序,nl_send_msg)中,如 here 所述。

没有睡眠,我相信nlmsg_new在分配时不需要睡眠,所以不违反中断处理程序不睡眠的要求。但是,如果用户空间进程落后于内核,则有可能内核在分配期间休眠,导致挂起。