Kernel module 访问 用户空间内存
资料来源:
https://bbs.kanxue.com/thread-276666.htm
https://lwn.net/Articles/807108/
http://liujunming.top/2017/07/03/get-user-pages%E5%87%BD%E6%95%B0%E8%AF%A6%E8%A7%A3/
更新
导语
有这么个需求: 内核模块需要访问 用户进程的某个变量, 还要特别避免内核态/用户态的切换.
首先无论如何这是个危险的动作, 两个同级别的进程直接共享内存就需要各种锁,各种同步, 而且这个需求下稍有不慎即内核恐慌.
正文
Kdump
高风险操作, 先上保险 kdump
测试环境分分钟干碎.. 得抓 log , systemd 启动 kdump 提示: No memory reserved for crash kernel
- 参考: https://tech.kurojica.com/archives/50460/
在 /etc/default/grub
文件中 crashkernel=
auto
情况下, 内存<4G 不会自动分配… 将 auto 改为具体数值,强制分配;
1 2 3 4 5 6 7
| GRUB_CMDLINE_LINUX="vconsole.keymap=jp106 vga=771 crashkernel=auto rhgb quiet ipv6.disable=1" ↓ GRUB_CMDLINE_LINUX="vconsole.keymap=jp106 vga=771 crashkernel=128 rhgb quiet ipv6.disable=1"
grub2-mkconfig -o /boot/grub2/grub.cfg
reboot
|
真.正文
传入到 内核模块是 pid 和 用户空间的地址 addr 加上长度;
整个过程的核心是获取到 addr 的内存页的 物理内存页, 再将其挂载到对应的内核空间访问.
函数调用
get_task_mm
获取到 task 的内存映射文件get_user_pages_remote
获取虚拟地址 对应的 物理内存页.kmap()
挂载物理页到内核 / kunmap
卸载页
还是结合例子来吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| void *to_kernel_space_ptr(pid_t pid, unsigned long addr) { struct task_struct *task; struct mm_struct *mm; void *kaddr; struct page *page; int ret;
unsigned long aligned_addr = addr & PAGE_MASK; unsigned long offset = addr & ~PAGE_MASK;
rcu_read_lock(); task = pid_task(find_vpid(pid), PIDTYPE_PID); rcu_read_unlock();
if (!task) { return NULL; } mm = get_task_mm(task); if (!mm) { return NULL; } down_read(&task->mm->mmap_lock); ret = get_user_pages_remote(task->mm, aligned_addr, 1, FOLL_FORCE, &(page), NULL, NULL); up_read(&task->mm->mmap_lock);
if (ret != 1) { printk(KERN_ERR "No user page\n"); return NULL; } kaddr = kmap(page); return (void *)((unsigned long)(kaddr) + offset); }
void free_page(struct page *page, unsigned long kaddr){ kunmap(kaddr); put_page(page); }
|
- 获取到 kaddr 后强制类型转换, 可读可写. 强烈不推荐写入.
- 示例中没有使用任何同步措施, 请参考使用共享内存的同步措施.
- 或许共享内存的措施也不够…毕竟跨越了 内核态 / 用户态
大页内存问题
hugepage 默认内存分页 4k 太小了…一般业务中总是会使用 hp 将内存页调整到 2M - 1G…
简要介绍下:
linux 内核编译: CONFIG_HUGETLBFS
和 CONFIG_HUGETLB_PAGE
启用后需要将其挂载到某个路径下, 然后 使用方式类似挂载文件系统… mmap 和 munmap.
应用程序使用创建文件 就直接对应到 大页了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| # 一般情况下/proc/meminfo 的战事信息主要如下: HugePages_Total: uuu HugePages_Free: vvv HugePages_Rsvd: www HugePages_Surp: xxx Hugepagesize: yyy kB Hugetlb: zzz kB
其中: HugePages_Total 大页内存的总量 HugePages_Free 没有被分配的大页内存数量. HugePages_Rsvd 系统当前总共保留的大页数目,具体点是指程序已经向系统申请,但是还没有具体执行写入 表示为尚未实际分配给程序的HugePages数目。 HugePages_Surp 超过设定的大页内存的数量. Hugepagesize 大页内存的单独每个页面的大小. Hugetlb 大页内存的总计大小,单位为KB 可以理解为 Hugepagesize*HugePages_Total
|
- https://www.modb.pro/db/609888
一个示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| int fd; void *addr;
fd = open("hp_path", O_CREAT | O_RDWR, 0755); if (fd < 0) { perror("open"); return 1; }
addr = mmap(0, HUGEPAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (addr == MAP_FAILED) { perror("mmap"); close(fd); return 1; }
int *p = (int *)addr; *p = 0;
|
大页内存下如上的 内核空间挂载会浪费一些内存寻址空间, x64 无所谓, 32 位还是慎用.
并且大页内存申请卸载通常与业务联系非常紧密, 内核模块以如此方式挂载后,可能会造成未知问题.
尾巴
高风险操作, 用户进程关闭时,必须及时卸载.否则 boom.