如何通过send_sig_info以信号实现内核到用户态的数据传输?
内核模块通过信号向用户态传输数据的实现方案
核心逻辑说明
要通过信号传递数据,核心是利用send_sig_info()传递siginfo_t结构体,其中si_ptr字段只能存放用户态虚拟地址(不能直接传内核地址,用户态无法访问)。步骤大致是:
- 内核用
vmalloc()分配可映射的内存存储事件数据; - 将这块内核内存映射到用户态进程的虚拟地址空间;
- 把映射后的用户态地址填入
si_ptr,通过send_sig_info()发送信号; - 用户态收到信号后从
si_ptr指向的地址读取数据,再通过内核接口通知内核释放内存。
示例代码
内核模块代码
#include <linux/module.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/sched.h> #include <linux/signal.h> #include <linux/vmalloc.h> #include <linux/mm.h> #include <linux/workqueue.h> #define DEVICE_NAME "sig_ipc_dev" #define IOCTL_REGISTER_PID _IOR('s', 1, pid_t) #define IOCTL_RELEASE_MEM _IOW('s', 2, unsigned long) #define DATA_SIZE 256 static pid_t target_pid = 0; static struct class *sig_class; static struct device *sig_device; static dev_t dev_num; static struct cdev sig_cdev; // 存储内核内存与用户态地址的映射关系,实际项目可改用哈希表管理多组映射 static char *kernel_mem; static unsigned long user_vaddr; static long sig_ioctl(struct file *file, unsigned int cmd, unsigned long arg) { switch (cmd) { case IOCTL_REGISTER_PID: if (copy_from_user(&target_pid, (pid_t __user *)arg, sizeof(pid_t))) { return -EFAULT; } pr_info("Registered user PID: %d\n", target_pid); break; case IOCTL_RELEASE_MEM: if (kernel_mem && user_vaddr == arg) { // 解除用户态地址映射 struct task_struct *task; rcu_read_lock(); task = pid_task(find_vpid(target_pid), PIDTYPE_PID); if (task) { struct mm_struct *mm = get_task_mm(task); if (mm) { down_write(&mm->mmap_sem); zap_page_range(mm, arg, DATA_SIZE); up_write(&mm->mmap_sem); mmput(mm); } } rcu_read_unlock(); // 释放内核内存 vfree(kernel_mem); kernel_mem = NULL; user_vaddr = 0; pr_info("Released kernel memory\n"); } break; default: return -ENOTTY; } return 0; } static const struct file_operations sig_fops = { .unlocked_ioctl = sig_ioctl, }; static void send_event_work(struct work_struct *work) { struct siginfo info; struct task_struct *task; struct mm_struct *mm; struct page *page; if (!target_pid) { pr_warn("No registered user PID\n"); return; } // 分配内核内存(vmalloc分配的内存可映射到用户态) kernel_mem = vmalloc(DATA_SIZE); if (!kernel_mem) { pr_err("vmalloc failed\n"); return; } // 填充事件数据 snprintf(kernel_mem, DATA_SIZE, "Kernel event triggered at jiffies: %lu", jiffies); pr_info("Event data: %s\n", kernel_mem); // 获取目标进程的task_struct rcu_read_lock(); task = pid_task(find_vpid(target_pid), PIDTYPE_PID); if (!task) { rcu_read_unlock(); vfree(kernel_mem); kernel_mem = NULL; pr_err("Target process not found\n"); return; } // 获取进程内存描述符 mm = get_task_mm(task); if (!mm) { rcu_read_unlock(); vfree(kernel_mem); kernel_mem = NULL; pr_err("Failed to get mm_struct\n"); return; } // 转换vmalloc内存到page结构 page = vmalloc_to_page(kernel_mem); if (!page) { mmput(mm); rcu_read_unlock(); vfree(kernel_mem); kernel_mem = NULL; pr_err("vmalloc_to_page failed\n"); return; } // 分配用户态空闲虚拟地址 user_vaddr = get_unmapped_area(NULL, 0, DATA_SIZE, 0, 0); if (user_vaddr == (unsigned long)-ENOMEM) { mmput(mm); rcu_read_unlock(); vfree(kernel_mem); kernel_mem = NULL; pr_err("get_unmapped_area failed\n"); return; } // 将内核page插入到用户态地址空间 down_write(&mm->mmap_sem); if (vm_insert_page(mm, user_vaddr, page) != 0) { up_write(&mm->mmap_sem); mmput(mm); rcu_read_unlock(); vfree(kernel_mem); kernel_mem = NULL; pr_err("vm_insert_page failed\n"); return; } up_write(&mm->mmap_sem); // 初始化siginfo结构体 memset(&info, 0, sizeof(info)); info.si_signo = SIGUSR1; info.si_code = SI_QUEUE; // 必须用SI_QUEUE才能传递完整siginfo info.si_ptr = (void __user *)user_vaddr; // 发送信号 if (send_sig_info(SIGUSR1, &info, task) < 0) { pr_err("send_sig_info failed\n"); // 发送失败,清理已分配资源 down_write(&mm->mmap_sem); zap_page_range(mm, user_vaddr, DATA_SIZE); up_write(&mm->mmap_sem); mmput(mm); rcu_read_unlock(); vfree(kernel_mem); kernel_mem = NULL; return; } mmput(mm); rcu_read_unlock(); pr_info("Signal sent, user address: %p\n", (void *)user_vaddr); } static DECLARE_DELAYED_WORK(event_work, send_event_work); static int __init sig_ipc_init(void) { int ret; // 分配字符设备号 ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME); if (ret < 0) { pr_err("alloc_chrdev_region failed\n"); return ret; } // 创建设备类和设备节点 sig_class = class_create(THIS_MODULE, DEVICE_NAME); if (IS_ERR(sig_class)) { unregister_chrdev_region(dev_num, 1); return PTR_ERR(sig_class); } sig_device = device_create(sig_class, NULL, dev_num, NULL, DEVICE_NAME); if (IS_ERR(sig_device)) { class_destroy(sig_class); unregister_chrdev_region(dev_num, 1); return PTR_ERR(sig_device); } // 注册字符设备 cdev_init(&sig_cdev, &sig_fops); ret = cdev_add(&sig_cdev, dev_num, 1); if (ret < 0) { device_destroy(sig_class, dev_num); class_destroy(sig_class); unregister_chrdev_region(dev_num, 1); return ret; } pr_info("Signal IPC module loaded\n"); // 2秒后触发模拟事件 schedule_delayed_work(&event_work, msecs_to_jiffies(2000)); return 0; } static void __exit sig_ipc_exit(void) { cancel_delayed_work_sync(&event_work); if (kernel_mem) { vfree(kernel_mem); } cdev_del(&sig_cdev); device_destroy(sig_class, dev_num); class_destroy(sig_class); unregister_chrdev_region(dev_num, 1); pr_info("Signal IPC module unloaded\n"); } module_init(sig_ipc_init); module_exit(sig_ipc_exit); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("Kernel to user IPC via signal");
用户态应用代码
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <sys/mman.h> #define DEVICE_PATH "/dev/sig_ipc_dev" #define IOCTL_REGISTER_PID _IOR('s', 1, pid_t) #define IOCTL_RELEASE_MEM _IOW('s', 2, unsigned long) #define DATA_SIZE 256 static int dev_fd; static void signal_handler(int signo, siginfo_t *info, void *context) { if (signo != SIGUSR1 || info->si_code != SI_QUEUE) { return; } char *event_data = (char *)info->si_ptr; printf("Received kernel event: %s\n", event_data); // 解除用户态地址映射 munmap(event_data, DATA_SIZE); // 通知内核释放对应内存 if (ioctl(dev_fd, IOCTL_RELEASE_MEM, (unsigned long)event_data) < 0) { perror("ioctl release mem failed"); } } int main() { struct sigaction sa; pid_t pid = getpid(); // 打开设备节点 dev_fd = open(DEVICE_PATH, O_RDWR); if (dev_fd < 0) { perror("open device failed"); exit(EXIT_FAILURE); } // 向内核注册当前进程PID if (ioctl(dev_fd, IOCTL_REGISTER_PID, &pid) < 0) { perror("ioctl register pid failed"); close(dev_fd); exit(EXIT_FAILURE); } // 设置信号处理函数(必须启用SA_SIGINFO才能获取siginfo结构体) memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = signal_handler; sa.sa_flags = SA_SIGINFO; if (sigaction(SIGUSR1, &sa, NULL) < 0) { perror("sigaction failed"); close(dev_fd); exit(EXIT_FAILURE); } printf("Waiting for kernel events... (PID: %d)\n", pid); while (1) { pause(); } close(dev_fd); return EXIT_SUCCESS; }
内存释放的关键说明
- 用户态不能直接释放内核内存:内核内存的生命周期由内核管理,用户态没有权限直接操作内核地址空间,强行操作会导致崩溃或系统异常。
- 正确的释放流程:
- 用户态收到数据后,先调用
munmap()解除自身地址空间中对内核内存的映射,避免后续非法访问; - 通过自定义的ioctl命令(示例中的
IOCTL_RELEASE_MEM)将用户态地址传递给内核; - 内核根据用户态地址找到对应的内核内存块,先解除用户态的页映射(
zap_page_range()),再调用vfree()释放内核内存。
- 用户态收到数据后,先调用
- 异常情况处理:如果用户态进程意外崩溃,内核需要监听进程退出事件(比如注册
mm_notifier),自动清理未释放的内核内存,避免内存泄漏。
注意事项
- 必须使用
SI_QUEUE作为si_code:只有这个代码能保证用户态收到完整的siginfo_t结构体,否则si_ptr字段可能无法正确传递。 - 内核内存必须用
vmalloc()分配:kmalloc()分配的内存物理地址不连续,无法直接映射到用户态;vmalloc()分配的是连续虚拟地址,可通过vmalloc_to_page()获取对应的页结构。 - 用户态必须设置
SA_SIGINFO标志:否则信号处理函数无法获取siginfo_t结构体,也就无法读取si_ptr中的数据。
内容的提问来源于stack exchange,提问作者Adel Belkhiri
相关产品推荐
相关产品推荐

