【代码模板】如何通过netlink从内核态主动发送消息到用户态?(netlink_kernel_create、nlmsg_new、nlmsg_put、netlink_unicast)

摘要

本文通过一个简单的例子介绍了Netlink机制的使用,展示了内核态与用户态之间的异步通信。主要流程包括:内核态驱动注册Netlink用户自定义消息组,用户态程序通过写入字符设备将PID发送给内核态驱动,内核态使用Netlink单播或广播机制通知对应的PID,用户态收到消息后打印出来自内核的信息。

背景

netlink是内核中常用的机制,可以使内核态主动通知用户态并且是异步的操作。本文实现了一个简单的例子介绍netlink的使用机制。主要流程是:内核态驱动注册netlink用户自定义消息组,然后用户态程序通过write字符设备将pid发送给内核态驱动,内核态然后使用netlink单播/广播机制通知到对应的pid,然后用户态收到消息后打印出来自内核的消息

代码与主要流程

内核态程序 test.c

主要流程:
初始化的时候通过netlink_kernel_create创建netlink的用户自定义的netlink消息组。
然后创建字符设备至此写入和ioctl。写入是指定pid,ioctl是触发发送到用户态的netlink中去。

通过nlmsg_new创建一个sk_buffer
然后nlmsg_put初始化skb
然后将需要传送的消息拷贝到mlh的data域段并且设置mlh的msg len(如果不设置,用户态收到的该部分信息不对)
然后调用netlink_unicast单播到某个pid,指定创建的netlink的自定义消息组句柄、需要传送的skb、用户pid、以及flag。也可以组播,组播需要把netlink_unicast换成netlink_broadcast。

#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>

#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

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

    if (user_pid == 0) {
        printk(KERN_ERR "User PID not set\n");
        return;
    }

    skb = nlmsg_new(size, GFP_KERNEL);
    if (!skb) {
        printk(KERN_ERR "Failed to allocate new skb\n");
        return;
    }

    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;

    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) {
    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';
    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,
    .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");

    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);

    cl = class_create(THIS_MODULE, CLASS_NAME);
    device_create(cl, NULL, dev_num, NULL, DEVICE_NAME);

    return 0;
}

static void __exit my_exit(void) {
    if (nl_sk) {
        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("Your Name");
MODULE_DESCRIPTION("A simple Netlink example with ioctl and write");

makefile:

obj-m := test.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
                $(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
                rm -rf *.o *.ko *.mod.c *.symvers *.order

用户态程序 listen.c

可以看到,先将设备pid写到驱动。
然后用socket(PF_NETLINK, SOCK_RAW, NETLINK_USER); 创建netlink的socket注意是PF_NETLINK不是AF_NETLINK。
然后创建struct sockaddr_nl src_addr初始化地址为AF_NETLINK,以及指定pid和组播地址(如果组播0可能有问题用1)
然后用bind将src_addr信息绑定。
然后分配nlmsg hdr头部
然后创建msg(注意msg还有自己的header和nlmsghdr不一样),并且初始化dst_addr,以及初始化msg的名字(dst addr地址)和长度。并且将创建的nlh放入iov的base和len中。
然后通过ioctl下发消息过去触发kernel发送消息,本来kernel也可以自己主动发送,本实验需要额外触发。
然后输入recvmsg进行收录数据。收到后从mlmsg中获取出来。

#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>

#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
    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
    sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_USER);
    if (sock_fd < 0) {
        perror("Socket creation failed");
        return -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 = 1; /* 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
    nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
    if (!nlh) {
        perror("Failed to allocate memory for nlmsghdr");
        close(sock_fd);
        exit(EXIT_FAILURE);
    }

    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
    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);

    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){
        fprintf(stderr, "Invalid message length:%d. NLMSG_HDRLEN %d\n", nlh->nlmsg_len, NLMSG_HDRLEN);
        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;
}

编译运行

make
insmod test.ko
gcc listen.c
./a.out

实操:
在这里插入图片描述

在这里插入图片描述
可以看到收到了来自于内核态的信息
在这里插入图片描述

<think>好的,我需要回答用户关于generic netlink的doit回调函数是同步还是异步执行的问题。首先,我得回忆一下Netlink和Generic Netlink的基本知识。NetlinkLinux内核提供的用户空间内核空间通信的机制,而Generic Netlink是对传统Netlink的扩展,更方便使用。 用户的问题核心在于发送消息到generic netlink时,doit函数是同步还是异步执行。记得在Linux内核中,当用户空间发送消息内核,处理通常是同步的。也就是说,当用户程序通过socket发送一个消息内核会在接收到这个消息后立即处理,处理完成后再返回结果。对于Generic Netlink的doit回调函数,这个函数应该是在发送消息的上下文里被调用的,也就是同步执行。 接下来需要验证这个理解是否正确。可以查阅内核源码中的相关部分,比如net/netlink/genetlink.c中的处理流程。当内核收到一个Netlink消息时,会调用相应的处理函数,比如doit,这个过程应该是同步的,即发送消息的进程会等待doit执行完毕后才继续执行。如果是异步的话,消息会被放入队列,由内核线程处理,但这种情况通常用于需要延后处理的情况,而Generic Netlink的标准操作应该是同步的。 另外,可以参考内核文档或者权威资料,确认doit回调的执行模式。例如,内核文档可能提到,每个Netlink消息的处理都是在接收的软中断上下文中处理的,或者是通过工作队列异步处理。但根据经验,Netlink消息处理通常是同步的,以保证操作的顺序性和及时性。 还需要考虑用户空间发送消息的方式,比如使用sendmsg发送消息后,是否会阻塞直到内核处理完毕。如果用户空间调用发送后立即可以收到回复,这说明处理是同步的。而如果是异步的,用户可能需要等待某个事件或回调才能获取结果,但Generic Netlink通常使用同步通信模型。 总结起来,用户空间的请求会触发内核同步执行doit回调,处理完成后返回结果。因此,doit是同步执行的。需要确保在回答时结构清晰,逐步解释,并引用相关机制来支持结论。</think>在Linux内核中,用户空间向Generic Netlink发送消息时,`doit`回调函数是**同步执行**的。以下是分步说明: --- ### 1. **Generic Netlink通信机制基础** - **Generic Netlink**是传统Netlink的扩展,采用客户端-服务端模型。内核模块作为服务端注册Family,用户空间作为客户端通过套接字通信。 - 每个Family会定义一组操作(operations),对应`doit`回调函数。 --- ### 2. **消息处理流程** 1. **用户空间发送消息**:通过`sendmsg()`系统调用向内核发送Netlink消息。 2. **内核接收消息**:内核在**进程上下文**(通常是发送进程的上下文)中接收并解析消息。 3. **调用`doit`回调**:内核根据消息中的操作类型(如`cmd`字段),直接调用对应的`doit`函数。 4. **同步执行**:`doit`函数会**阻塞当前用户进程**,直到执行完成并返回响应。 --- ### 3. **关键证据** - **内核源码逻辑**: 在`genl_family_rcv_msg`函数(位于`net/netlink/genetlink.c`)中,`doit`回调直接通过函数指针调用,无异步队列或工作队列机制。 ```c if (ops->doit) return ops->doit(skb, info); ``` - **用户空间行为**: 用户调用`sendmsg()`后,通常需要同步调用`recvmsg()`等待内核响应。若`doit`异步执行,用户需通过事件驱动机制获取结果,但Generic Netlink未采用此设计。 --- ### 4. **同步执行的影响** - **优点**:保证操作顺序性,简化用户空间编程(直接通过请求-响应模型交互)。 - **缺点**:若`doit`函数执行耗时较长,会阻塞用户进程,甚至导致超时。 --- ### 5. **异步场景的例外** 若需异步处理,需自行实现机制: - 在`doit`中触发异步任务(如内核工作队列)。 - 通过其他通道(如另一个Netlink会话)通知用户空间结果。 --- ### 总结 Generic Netlink的`doit`回调是同步执行的,用户空间发送消息后会阻塞等待内核处理完成。这种设计符合大多数内核操作的实时性要求,但需注意避免在`doit`中执行耗时操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值