摘要
本文通过一个简单的例子介绍了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
实操:
可以看到收到了来自于内核态的信息