Kernel module 访问 用户空间内存

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/

  • 更新

    1
    2023.12.20 初始

导语

有这么个需求: 内核模块需要访问 用户进程的某个变量, 还要特别避免内核态/用户态的切换.

首先无论如何这是个危险的动作, 两个同级别的进程直接共享内存就需要各种锁,各种同步, 而且这个需求下稍有不慎即内核恐慌.

正文

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;

// get task_pid
rcu_read_lock();
task = pid_task(find_vpid(pid), PIDTYPE_PID);
rcu_read_unlock();

if (!task) { // no task
return NULL;
}
mm = get_task_mm(task); // memory descriptor
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); // Map page to kernel space
return (void *)((unsigned long)(kaddr) + offset);
}

void free_page(struct page *page, unsigned long kaddr){
kunmap(kaddr);
put_page(page); // down page count
}
  • 获取到 kaddr 后强制类型转换, 可读可写. 强烈不推荐写入.
  • 示例中没有使用任何同步措施, 请参考使用共享内存的同步措施.
    • 或许共享内存的措施也不够…毕竟跨越了 内核态 / 用户态

大页内存问题

hugepage 默认内存分页 4k 太小了…一般业务中总是会使用 hp 将内存页调整到 2M - 1G…

简要介绍下:

linux 内核编译: CONFIG_HUGETLBFSCONFIG_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;
// 在 hp_path 下打开一个 hugetlbfs 文件
fd = open("hp_path", O_CREAT | O_RDWR, 0755);
if (fd < 0) {
perror("open");
return 1;
}

// 映射 Huge Page 内存
addr = mmap(0, HUGEPAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
// 大页内存 int 类型变量
int *p = (int *)addr;
*p = 0;

大页内存下如上的 内核空间挂载会浪费一些内存寻址空间, x64 无所谓, 32 位还是慎用.

并且大页内存申请卸载通常与业务联系非常紧密, 内核模块以如此方式挂载后,可能会造成未知问题.

尾巴

高风险操作, 用户进程关闭时,必须及时卸载.否则 boom.