内存管理
实验:获取系统的物理内存信息
Linux 内核中使用 page 结构体来描述物理页面。用于描述的意思是,你可以把 page 当成每个物理页的“说明书”。其中包含了物理页的各种信息,以便快速查找和管理页面。
现在我们来学习一下怎么查看“这张说明书”:这节实验主要使用 page 的 flags 成员。
- #include <linux/version.h>
- #include <linux/module.h>
- #include <linux/init.h>
- #include <linux/mm.h>
- #define PRT(a ,b) pr_info("%-15s=%10d %10ld %8ld\n", \
- a, b, (PAGE_SIZE*b)/1024, (PAGE_SIZE*b)/1024/1024)
- static int __init my_init(void)
- {
- struct page* p;
- unsigned long i, pfn, valid = 0;
- int free = 0, locked = 0, reserved = 0, swapcache = 0,
- referenced = 0, slab = 0, private = 0, uptodate = 0,
- dirty = 0, active = 0, writeback = 0, mappedtodisk = 0;
- unsigned long num_physpages;
- num_physpages = get_num_physpages();
- for (i = 0; i < num_physpages; i++)
- {
- /* Most of ARM system have ARCH_PFN_OFFSET */
- pfn = i + ARCH_PFN_OFFSET;
- /* may be holes due to remapping */
- if (!pfn_valid(pfn))
- continue;
- valid++;
- p = pfn_to_page(pfn);
- if (!p)
- continue;
- /* page_count(page) == 0 is a free page */
- if (!page_count(p))
- {
- free++;
- continue;
- }
- if (PageLocked(p))
- locked++;
- if (PageReserved(p))
- reserved++;
- if (PageSwapCache(p))
- swapcache++;
- if (PageReferenced(p))
- referenced++;
- if (PageSlab(p))
- slab++;
- if (PagePrivate(p))
- private++;
- if (PageUptodate(p))
- uptodate++;
- if (PageDirty(p))
- dirty++;
- if (PageActive(p))
- active++;
- if (PageWriteback(p))
- writeback++;
- if (PageMappedToDisk(p))
- mappedtodisk++;
- }
- pr_info("\nExamining %ld pages (num_phys_pages) = %ld MB\n",
- num_physpages, num_physpages * PAGE_SIZE / 1024 / 1024);
- pr_info("Page with valid PFN's=%ld, =%ld MB\n",
- valid, valid * PAGE_SIZE / 1024 /1024);
- pr_info("\n Pages KB MB\n\n");
- PRT("free", free);
- PRT("locked", locked);
- PRT("reserved", reserved);
- PRT("swapcache", swapcache);
- PRT("referenced", referenced);
- PRT("slab", slab);
- PRT("private", private);
- PRT("uptodate", uptodate);
- PRT("dirty", dirty);
- PRT("active", active);
- PRT("writeback", writeback);
- PRT("mappedtodisk", mappedtodisk);
- return 0;
- }
- static void __exit my_exit(void)
- {
- pr_info("Module exit\n");
- }
- module_init(my_init);
- module_exit(my_exit);
- MODULE_AUTHOR("rlk");
- MODULE_LICENSE("GPL v2");
如代码清单 1 所示,我们首先使用 get_num_physpages 获取系统的物理页数。以次作为页帧号(pfn,Page Frame Number)的编码。
不是所有 ARM 系统都从物理地址 0 开始使用内存。针对大多数情况,这边加上 ARCH_PFN_OFFSET 偏移。
pfn_valid 函数检查指定的页帧号是否有映射到实际的物理内存页。有实际映射才进行下一步处理。
遗留一个问题:这边直接使用 get_num_physpages() 作为页帧号范围。不知道对于多内存条或者多处理器系统,编码是否就是按顺序排下来的。
最后可以通过 pfn_to_page 函数获取指定页帧号对应的 page 结构体。获取到 page 结构体,我们就可以检索其中的信息了。
这边我们使用到 page 结构的 flags 成员,以获取当前物理页所处的各种状态:
PageLocked:表示页面被锁定,不能被换出。
PageReserved:表示页面被保留,不参与常规的交换操作。
PageSwapCache:表示页面被换出到交换设备上了。
PageReferenced:表示页面最近被访问过。
PageSlab:表示页面是 Slab 分配器的一部分,用于管理小的内核对象。
PagePrivate:表示页面有关联的私有数据。
PageUptodate:表示页面的内容是最新的,与磁盘的数据一致。
PageDirty:表示页面的内容已被修改,但尚未写回到磁盘。
PageActive:表示页面是活动的,经常被访问。
PageWriteback:表示页面正在被写回磁盘。
PageMappedToDisk:表示页面有与之对应的磁盘位置。
PageLocked 这些函数,在 include/linux/page-flags.h 头文件中。
- #define TESTPAGEFLAG(uname, lname, policy) \
- static __always_inline int Page##uname(struct page *page) \
- { return test_bit(PG_##lname, &policy(page, 0)->flags); }
TESTPAGEFLAG 宏展开会获得以上 Page 打头的查询函数,可以看到就是检查 page->flags 中的各个位。
这些函数比较难找,是通过宏定义的。
实验:分配内存
这节实验我们了解 linux 内核中常用的内存分配接口,我们先直接看到代码清单 2。
如代码清单 2 所示,里面使用了 3 个内存分配接口:__get_free_pages、kmalloc 和 vmalloc。
- #include <linux/module.h>
- #include <linux/slab.h>
- #include <linux/init.h>
- #include <linux/vmalloc.h>
- static int mem = 1024;
- #define MB (1024*1024)
- static int __init my_init(void)
- {
- char* kbuf;
- unsigned long order;
- unsigned long size;
- char* vm_buff;
- /* try __get_free_pages__ */
- for (size = PAGE_SIZE, order = 0; order < MAX_ORDER; order++, size *= 2)
- {
- pr_info(" order=%2lu, pages=%5lu, size=%8lu ", order, size / PAGE_SIZE, size);
- kbuf = (char*)__get_free_pages(GFP_ATOMIC, order);
- if (!kbuf)
- {
- pr_err("... __get_free_pages failed\n");
- break;
- }
- pr_info("... __get_free_pages OK\n");
- free_pages((unsigned long)kbuf, order);
- }
- /* try kmalloc */
- for (size = PAGE_SIZE, order = 0; order < MAX_ORDER; order++, size *= 2)
- {
- pr_info(" order=%2lu, pages=%5lu, size=%8lu ", order, size / PAGE_SIZE, size);
- kbuf = kmalloc((size_t) size, GFP_ATOMIC);
- if (!kbuf)
- {
- pr_err("... kmalloc failed\n");
- break;
- }
- pr_info("... kmalloc OK\n");
- kfree(kbuf);
- }
- /* try vmalloc */
- for (size = 20 * MB; size <= mem * MB; size += 20 * MB)
- {
- pr_info(" order=%2lu, pages=%5lu, size=%8lu ", order, size / PAGE_SIZE, size);
- vm_buff = vmalloc(size);
- if (!vm_buff)
- {
- pr_err("... vmalloc failed\n");
- break;
- }
- pr_info("... vmalloc OK\n");
- vfree(vm_buff);
- }
- return 0;
- }
- static void __exit my_exit(void)
- {
- pr_info("Module exit\n");
- }
- module_init(my_init);
- module_exit(my_exit);
- MODULE_AUTHOR("rlk");
- MODULE_LICENSE("GPL v2");
__get_free_pages 用于从内核的页分配器请求一个或多个连续的物理内存页。与其对应的释放函数是 free_pages。__get_free_pages 的原型为:
- unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
其中,gfp_mask 是内存分配标志的位掩码。这边的 GFP_ATOMIC 表示用于原子上下文,即在分配内存的过程中不会执行页面回收或睡眠动作。order 表示要分配的页数是 2 的 order 次方。
这边的原子上下文可以理解成是“非阻塞”的。如果分配失败则直接返回,不会被挂起并放入等待队列,直到资源可用为止。
从日志中可以看到,本地环境 __get_free_pages 可以最多分配 1024 个连续的物理页。
- [ 796.021287] order= 0, pages= 1, size= 4096
- [ 796.021291] ... __get_free_pages OK
- [ 796.021291] order= 1, pages= 2, size= 8192
- [ 796.021293] ... __get_free_pages OK
- [ 796.021293] order= 2, pages= 4, size= 16384
- [ 796.021296] ... __get_free_pages OK
- [ 796.021296] order= 3, pages= 8, size= 32768
- [ 796.021299] ... __get_free_pages OK
- [ 796.021299] order= 4, pages= 16, size= 65536
- [ 796.021304] ... __get_free_pages OK
- [ 796.021305] order= 5, pages= 32, size= 131072
- [ 796.021313] ... __get_free_pages OK
- [ 796.021313] order= 6, pages= 64, size= 262144
- [ 796.021330] ... __get_free_pages OK
- [ 796.021330] order= 7, pages= 128, size= 524288
- [ 796.021364] ... __get_free_pages OK
- [ 796.021365] order= 8, pages= 256, size= 1048576
- [ 796.021482] ... __get_free_pages OK
- [ 796.021484] order= 9, pages= 512, size= 2097152
- [ 796.021620] ... __get_free_pages OK
- [ 796.021624] order=10, pages= 1024, size= 4194304
- [ 796.021920] ... __get_free_pages OK
kmalloc 用于分配连续的物理内存。与其对应的释放函数为 kfree。kmalloc 的函数原型为:
- void* kmalloc(size_t size, gfp_t flags);
从日志中可以看到,本地环境 kmalloc 最多可以分配到 2MB 的连续物理内存。
- [ 796.021926] order= 0, pages= 1, size= 4096
- [ 796.021927] ... kmalloc OK
- [ 796.021927] order= 1, pages= 2, size= 8192
- [ 796.021929] ... kmalloc OK
- [ 796.021929] order= 2, pages= 4, size= 16384
- [ 796.021931] ... kmalloc OK
- [ 796.021933] order= 3, pages= 8, size= 32768
- [ 796.021935] ... kmalloc OK
- [ 796.021936] order= 4, pages= 16, size= 65536
- [ 796.021940] ... kmalloc OK
- [ 796.021941] order= 5, pages= 32, size= 131072
- [ 796.021949] ... kmalloc OK
- [ 796.021949] order= 6, pages= 64, size= 262144
- [ 796.021965] ... kmalloc OK
- [ 796.021966] order= 7, pages= 128, size= 524288
- [ 796.021998] ... kmalloc OK
- [ 796.021999] order= 8, pages= 256, size= 1048576
- [ 796.022060] ... kmalloc OK
- [ 796.022061] order= 9, pages= 512, size= 2097152
- [ 796.022181] ... kmalloc OK
vmalloc 和 kmalloc 的区别在于,vmalloc 分配的是连续的虚拟地址空间,在物理地址上不一定连续。与之对应的释放函数为 vfree。vmalloc 的函数原型为:
- void* vmalloc(unsigned long size);
从日志中可以看到,本地环境 vmalloc 最多可以分配到约 1G 的内存。
- [ 801.104194] order=11, pages=230400, size=943718400
- [ 801.254238] ... vmalloc OK
- [ 801.265481] order=11, pages=235520, size=964689920
- [ 801.418673] ... vmalloc OK
- [ 801.432217] order=11, pages=240640, size=985661440
- [ 801.587991] ... vmalloc OK
- [ 801.599674] order=11, pages=245760, size=1006632960
- [ 801.763869] ... vmalloc OK
- [ 801.775801] order=11, pages=250880, size=1027604480
- [ 801.934132] ... vmalloc OK
- [ 801.946595] order=11, pages=256000, size=1048576000
- [ 802.106948] ... vmalloc OK
- [ 802.119573] order=11, pages=261120, size=1069547520
- [ 802.283889] ... vmalloc OK
分配过程和原理不做深究,这边先感性认识下这些函数:
__get_free_pages:分配连续物理页面。
kmalloc:分配小块的连续物理内存。
vmalloc:分配大块的虚拟地址空间中连续、但物理内存不一定连续的内存。
实验:slab
slab 分配器是 linux 内核中的一个内存管理机制,主要用于内核层的内存分配。它是为了满足内核中频繁分配和释放小块内存的需要而设计的,特别是为了提高这种小块内存分配的效率并减少碎片化。
我们直接看到代码清单 3,学习如何使用 slab。
- #include <linux/module.h>
- #include <linux/mm.h>
- #include <linux/slab.h>
- #include <linux/init.h>
- static char* kbuf;
- static int size = 20;
- static struct kmem_cache* my_cache;
- module_param(size, int, 0644);
- static int __init my_init(void)
- {
- /* create a memory cache */
- if (size > KMALLOC_MAX_SIZE)
- {
- pr_err("size=%d is too large; you can't have more than %lu!\n", size, KMALLOC_MAX_SIZE);
- return -1;
- }
- my_cache = kmem_cache_create("mycache", size, 0, SLAB_HWCACHE_ALIGN, NULL);
- if (!my_cache)
- {
- pr_err("kmem_cache_create failed.\n");
- return -ENOMEM;
- }
- pr_info("create mycache correctly\n");
- /* allocate a memory cache object */
- kbuf = kmem_cache_alloc(my_cache, GFP_ATOMIC);
- if (!kbuf)
- {
- pr_err("failed to create a cache object\n");
- kmem_cache_destroy(my_cache);
- return -1;
- }
- pr_info("successfully created a object, kbuf_addr_0x%x\n", (unsigned long)kbuf);
- return 0;
- }
- static void __exit my_exit(void)
- {
- /* destroy a memory cache object */
- kmem_cache_free(my_cache, kbuf);
- pr_info("destroyed a cache object\n");
- /* destroy the memory cache */
- kmem_cache_destroy(my_cache);
- pr_info("destroyed mycache\n");
- }
- module_init(my_init);
- module_exit(my_exit);
- MODULE_LICENSE("GPL v2");
- MODULE_AUTHOR("rlk");
kmem_cache_create 函数用于创建一个 slab 缓存,与之对应的销毁函数是 kmem_cache_destroy。其原型如下:
- struct kmem_cache*
- kmem_cache_create(const char* name, size_t size, size_t align,
- unsigned long flags, void (*ctor)(void*));
其中,name 是要创建的 slab 缓存的名称;size 定义 slab 缓存中分配的每一个对象应该占多少字节;align 指定对齐;flags 用于定义 slab 标志,此处使用 SLAB_HWCACHE_ALIGN 指定按硬件缓存行对齐;ctor 指定一个构造函数,此处设置为 NULL。
如果创建成功,函数返回一个 kmem_cache 的结构指针,用于标识此 slab 缓存。
kmem_cache_alloc 用于从一个指定的 slab 缓存中分配一个对象,与之对应的释放函数是 kmem_cache_free。其原型如下:
- void* kmem_cache_alloc(struct kmem_cache* cache, gfp_t flags);
其中,cache 就是 kmem_cache_create 创建的 slab 缓存指针;flags 指定分配标志。此处使用了 GFP_ATOMIC,代表分配用于原子上下文。
如果分配成功,则会返回一个新分配的对象指针。
这边虽说返回的是对象指针,但是使用起来和内存指针是一样的。
slab 上下文中特意使用“对象”术语。我感觉是要强调它是一个整体的概念,它是一个预先定义的有着特定大小和属性的内存块。
我的简单对比理解:kmem_cache_create 可以理解为创建一个特定的“内存池”,然后使用 kmem_cache_alloc 往其中分配内存。
加载上述内核模块后,我们可以在 /proc/slabinfo 中查看创建的 slab 的缓存信息。
- tim@tim:~$ sudo cat /proc/slabinfo | grep -iE my
- mycache 6 6 5056 6 8 : tunables 0 0 0 : slabdata 1 1 0
当创建的 slab 缓存和现有存在的 slab 缓存,大小接近且具有相同的属性时,slab 会进行合并。这时候在 /proc/slabinfo 中会搜索不到我们创建的 slab 名字。
当发生 slab 合并时,我们可以在 /sys/kernel/slab/ 目录下查看。在这个目录下,每一个 slab 都会有一个子目录,包含其属性和统计信息。
- tim@tim:/sys/kernel/slab$ ls -l | grep -iE my
- lrwxrwxrwx 1 root root 0 10月 5 16:02 mycache -> :0005056
实验:VMA
VMA(Virtual Memory Area)是 linux 内存管理中的核心概念,它为程序提供了一个连续、受保护的虚拟地址空间,并通过页表机制将这些地址映射到物理内存或硬盘上。
这节实验,我们编写一个内核模块,遍历一个用户进程的所有 VMA,并打印其信息。
- #include <linux/module.h>
- #include <linux/init.h>
- #include <linux/mm.h>
- #include <linux/sched.h>
- static int pid;
- module_param(pid, int, 0644);
- static void printit(struct task_struct* tsk)
- {
- struct mm_struct* mm;
- struct vm_area_struct* vma;
- int j = 0;
- unsigned long start, end, length;
- mm = tsk->mm;
- pr_info("mm_struct addr = 0x%lx\n", (unsigned long)mm);
- vma = mm->mmap;
- /* protect from simultaneous modification */
- down_read(&mm->mmap_lock);
- pr_info("vmas: vma start end length\n");
- while (vma)
- {
- j++;
- start = vma->vm_start;
- end = vma->vm_end;
- length = end - start;
- pr_info("%6d: %16lx %16lx %16lx %8ld\n",
- j, (unsigned long)vma, start, end, length);
- vma = vma->vm_next;
- }
- up_read(&mm->mmap_lock);
- }
- static int __init my_init(void)
- {
- struct task_struct* tsk;
- /* if don't pass the pid over insmod, the use the current process */
- if (pid == 0)
- {
- tsk = current;
- pid = current->pid;
- pr_info("using current process\n");
- }
- else
- {
- tsk = pid_task(find_vpid(pid), PIDTYPE_PID);
- }
- if (!tsk)
- return -1;
- pr_info("Examining vma's for pid=%d, command=%s\n", pid, tsk->comm);
- printit(tsk);
- return 0;
- }
- static void __exit my_exit(void)
- {
- pr_info("Module exit\n");
- }
- module_init(my_init);
- module_exit(my_exit);
- MODULE_LICENSE("GPL v2");
书上使用的是 mm_struct.mmap_sem,新内核版本中这个成员改为 mmap_lock。
如代码清单 4 所示,它针对当前进程或者指定进程 ID 的进程,来打印它所有的 VMA。
核心函数在 printit。task_struct 的 mm 成员描述了整个虚拟内存空间。mm 的类型是 mm_struct,其中 mmap 成员是一个链表头,记录了每一个 VMA。我们可以依次遍历。
VMA 使用 vm_area_struct 结构体描述,如代码所示,我们可以得到 VMA 的起始虚拟地址、结束虚拟地址和长度。
因为 mmap 链表在运行时可能被其他内核线程修改,我们使用读写锁确保安全地读取它。
我们可以使用 cat /proc/[进程ID]/smaps 来验证所写代码的正确性。
实验:mmap
mmap 可以让用户空间应用程序,将一个文件或设备映射到其虚拟地址空间。一旦映射,应用程序可以直接通过内存访问映射的内容,就不需要调用 read 和 write 这些系统调用,从而提高效率。
我们看到实验代码,如代码清单 5.1 所示,我们在之前学习的简单字符设备驱动上增加 mmap 接口。
读写接口中的 simple_read_from_buffer 和 simple_write_to_buffer 第一次见。它们是 linux 内核提供的辅助函数,就是增加了逻辑的 copy_to_user 和 copy_from_user 封装。它们的函数原型参数类似,这边介绍一下 simple_read_from_buffer 的定义:
- ssize_t simple_read_from_buffer(void __user* to, size_t count, loff_t* ppos,
- const void* from, size_t available);
simple_read_from_buffer 从一个内核空间的缓冲区复制数据到用户空间的缓冲区。to 用户空间的目标缓冲区;count 要读取的字节数;ppos 读取的位置偏移量的指针;from 内核空间的源缓冲区;available 源缓冲区中的可用字节数。
- #include <linux/module.h>
- #include <linux/fs.h>
- #include <linux/uaccess.h>
- #include <linux/init.h>
- #include <linux/miscdevice.h>
- #include <linux/device.h>
- #include <linux/slab.h>
- #include <linux/kfifo.h>
- #define DEMO_NAME "mydemo_mmap_dev"
- static struct device* mydemodrv_device;
- /* virtual FIFO device's buffer */
- static char* device_buffer;
- #define MAX_DEVICE_BUFFER_SIZE (10 * PAGE_SIZE)
- #define MYDEV_CMD_GET_BUFSIZE 1 /* defines our IOCTL cmd */
- static int demodrv_open(struct inode* inode, struct file* file)
- {
- int major = MAJOR(inode->i_rdev);
- int minor = MINOR(inode->i_rdev);
- printk("%s: major=%d, minor=%d\n", __func__, major, minor);
- return 0;
- }
- static int demodrv_release(struct inode* inode, struct file* file)
- {
- return 0;
- }
- static ssize_t demodrv_read(struct file* file, char __user* buf, size_t count, loff_t* ppos)
- {
- int nbytes = simple_read_from_buffer(buf, count, ppos, device_buffer, MAX_DEVICE_BUFFER_SIZE);
- printk("%s: read nbytes=%d done at pos=%d\n", __func__, nbytes, (int)*ppos);
- return nbytes;
- }
- static ssize_t demodrv_write(struct file* file, const char __user* buf, size_t count, loff_t* ppos)
- {
- int nbytes = simple_write_to_buffer(device_buffer, MAX_DEVICE_BUFFER_SIZE, ppos, buf, count);
- printk("%s: write nbytes=%d done at pos=%d\n", __func__, nbytes, (int)*ppos);
- return nbytes;
- }
- static int demodrv_mmap(struct file* filp, struct vm_area_struct* vma)
- {
- unsigned long pfn;
- unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
- unsigned long len = vma->vm_end - vma->vm_start;
- if (offset >= MAX_DEVICE_BUFFER_SIZE)
- return -EINVAL;
- if (len > (MAX_DEVICE_BUFFER_SIZE - offset))
- return -EINVAL;
- printk("%s: mapping %ld bytes of device buffer at offset %ld\n", __func__, len, offset);
- /* pfn = page_to_pfn (virt_to_page (ramdisk + offset)); */
- pfn = virt_to_phys(device_buffer + offset) >> PAGE_SHIFT;
- vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
- if (remap_pfn_range(vma, vma->vm_start, pfn, len, vma->vm_page_prot))
- return -EAGAIN;
- return 0;
- }
- static long demodrv_unlocked_ioctl(struct file* filp, unsigned int cmd, unsigned long arg)
- {
- unsigned long tbs = MAX_DEVICE_BUFFER_SIZE;
- void __user* ioargp = (void __user*)arg;
- switch (cmd)
- {
- default:
- return -EINVAL;
- case MYDEV_CMD_GET_BUFSIZE:
- if (copy_to_user(ioargp, &tbs, sizeof(tbs)))
- return -EFAULT;
- return 0;
- }
- }
- static const struct file_operations demodrv_fops = {
- .owner = THIS_MODULE,
- .open = demodrv_open,
- .release = demodrv_release,
- .read = demodrv_read,
- .write = demodrv_write,
- .mmap = demodrv_mmap,
- .unlocked_ioctl = demodrv_unlocked_ioctl,
- };
- static struct miscdevice mydemodrv_misc_device = {
- .minor = MISC_DYNAMIC_MINOR,
- .name = DEMO_NAME,
- .fops = &demodrv_fops,
- };
- static int __init simple_char_init(void)
- {
- int ret;
- device_buffer = kmalloc(MAX_DEVICE_BUFFER_SIZE, GFP_KERNEL);
- if (!device_buffer)
- return -ENOMEM;
- ret = misc_register(&mydemodrv_misc_device);
- if (ret)
- {
- printk("failed register misc device\n");
- kfree(device_buffer);
- return ret;
- }
- mydemodrv_device = mydemodrv_misc_device.this_device;
- printk("succeeded register char device: %s\n", DEMO_NAME);
- return 0;
- }
- static void __exit simple_char_exit(void)
- {
- printk("removing device\n");
- kfree(device_buffer);
- misc_deregister(&mydemodrv_misc_device);
- }
- module_init(simple_char_init);
- module_exit(simple_char_exit);
- MODULE_AUTHOR("rlk");
- MODULE_LICENSE("GPL v2");
现在我们开始看本节实验的重点—— demodrv_mmap 函数。我们可以通过参数中的 vm_area_struct 来获取请求映射的虚拟内存空间情况。
vm_area_struct 的确定是由内核的内存管理子系统完成填充的。
原理留作后续研究。
如代码所示,我们通过 vm_pgoff 成员获取映射的偏移信息。注意 vm_pgoff 的单位是页面大小,需要将其转换成字节。通过 vm_start 和 vm_end 成员,可以获取映射范围。
接着我们通过 virt_to_phys 函数,来获取内核模块中即将被映射的缓冲的物理地址。并右移 PAGE_SHIFT 位来获取物理页帧号。
kmalloc 返回的也是虚拟地址。
vm_page_prot 描述了页面的保护属性。此处我们使用 pgprot_noncached 函数将其修改为非缓存。防止读写使用到 CPU 缓存,引发同步问题。
最后,我们使用 remap_pfn_range 函数,建立用户空间虚拟地址区域与物理内存之间的映射。以上过程即,我们映射了 kmalloc 出来的内核空间区域到用户层区域。
内核模块完成后,我们再写一个用户层的测试程序,验证我们写的 mmap 步骤是否正确。
- #include <stdio.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <sys/mman.h>
- #include <string.h>
- #include <errno.h>
- #include <sys/ioctl.h>
- #include <malloc.h>
- #define DEMO_DEV_NAME "/dev/mydemo_mmap_dev"
- #define MYDEV_CMD_GET_BUFSIZE 1 /* defines our IOCTL cmd */
- int main()
- {
- int fd;
- int i;
- size_t len;
- char message[] = "Testing the virtual FIFO device";
- char* read_buffer, *mmap_buffer;
- len = sizeof(message);
- fd = open(DEMO_DEV_NAME, O_RDWR);
- if (fd < 0)
- {
- printf("open device %s fail\n", DEMO_DEV_NAME);
- return -1;
- }
- if (ioctl(fd, MYDEV_CMD_GET_BUFSIZE, &len) < 0)
- {
- printf("ioctl fail\n");
- goto open_fail;
- }
- printf("driver max buffer size=%ld\n", len);
- read_buffer = malloc(len);
- if (!read_buffer)
- goto open_fail;
- mmap_buffer = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
- if (mmap_buffer == (char*)MAP_FAILED)
- {
- printf("mmap driver buffer fail\n");
- goto map_fail;
- }
- printf("mmap driver buffer succeeded: %p\n", mmap_buffer);
- /* modify the mmaped buffer */
- for (i = 0; i < len; i++)
- *(mmap_buffer + i) = (char)random();
- /* read the buffer back and compare with the mmap buffer */
- if (read(fd, read_buffer, len) != len)
- {
- printf("read fail\n");
- goto read_fail;
- }
- if (memcmp(read_buffer, mmap_buffer, len))
- {
- printf("buffer compare fail\n");
- goto read_fail;
- }
- printf("data modify and compare succussful\n");
- munmap(mmap_buffer, len);
- free(read_buffer);
- close(fd);
- return 0;
- read_fail:
- munmap(mmap_buffer, len);
- map_fail:
- free(read_buffer);
- open_fail:
- close(fd);
- return -1;
- }
用户层这边的 mmap 系统调用的原型为:
- void* mmap(void* start, size_t len, int prot, int flags, int fd, off_t offset);
其中,start 为映射的建议开始地址,通常设置为 NULL,让内核选择地址;len 是要映射的字节数;prot 指定保护内存区域的方式;flags 描述映射类型和其他属性,比如此处设置的 MAP_SHARED 表示对映射内容的更改会写回到文件;fd 为打开的文件描述符,即将被映射的文件;offset 指定映射的偏移。
如代码清单 5.2 所示,它首先映射设备的内存区域到用户空间(第 43 行)。然后通过这个映射的地址往里面写数据(第 53 至 54 行)。最后我们通过 read 带复制机制的接口读取设备内存(第 57 行)。以此我们可以验证我们写入的内容是否生效。
实验:映射用户内存
上一节我们介绍了,如何将一个文件或设备(例子中是内核中分配的内存)映射到用户可访问的虚拟空间中,从而减少用户层操作内核中数据的开销。同样,反过来,我们也有机制在内核中映射用户空间页面的机制,减少内核层操作用户层中数据的开销。
我们直接看到代码清单 6.1 来学习,fops 接口没有增加,还是调用基本的 read 和 write 接口。核心函数封装在 demodrv_read_write 中,参数 buf 就是应用层传下来的应用层地址。
- #include <linux/module.h>
- #include <linux/fs.h>
- #include <linux/uaccess.h>
- #include <linux/init.h>
- #include <linux/miscdevice.h>
- #include <linux/device.h>
- #include <linux/slab.h>
- #include <linux/kfifo.h>
- #include <linux/highmem.h>
- #define DEMO_NAME "my_demo_dev"
- static struct device* mydemodrv_device;
- #define MYDEMO_READ 0
- #define MYDEMO_WRITE 1
- /* virtual FIFO device's buffer */
- static char* device_buffer;
- #define MAX_DEVICE_BUFFER_SIZE (1 * PAGE_SIZE)
- #define MYDEV_CMD_GET_BUFSIZE 1 /* defines our IOCTL cmd */
- static size_t demodrv_read_write(void* buf, size_t len, int rw)
- {
- int ret, npages, i;
- struct page** pages;
- struct mm_struct* mm = current->mm;
- char* kmap_addr, *dev_buf;
- size_t size = 0;
- size_t count = 0;
- dev_buf = device_buffer;
- /* how many pages? */
- npages = DIV_ROUND_UP(len, PAGE_SIZE);
- printk("%s: len=%ld, npage=%d\n", __func__, len, npages);
- pages = kmalloc(npages * sizeof(pages), GFP_KERNEL);
- if (!pages)
- {
- printk("alloc pages fail\n");
- return -ENOMEM;
- }
- down_read(&mm->mmap_sem);
- ret = get_user_pages_fast((unsigned long)buf, npages, 1, pages);
- if (ret < npages)
- {
- printk("pin page fail\n");
- goto fail_pin_pages;
- }
- up_read(&mm->mmap_sem);
- printk("pin %d pages from user done\n", npages);
- for (i = 0; i < npages; i++)
- {
- kmap_addr = kmap(pages[i]);
- size = min_t(size_t, PAGE_SIZE, len);
- switch (rw)
- {
- case MYDEMO_READ:
- memcpy(kmap_addr, dev_buf + PAGE_SIZE * i, size);
- break;
- case MYDEMO_WRITE:
- memcpy(dev_buf + PAGE_SIZE * i, kmap_addr, size);
- break;
- default:
- break;
- }
- put_page(pages[i]);
- kunmap(pages[i]);
- len -= size;
- count += size;
- }
- kfree(pages);
- printk("%s: %s user buffer %ld bytes done\n", __func__, rw ? "write" : "read", count);
- return count;
- fail_pin_pages:
- upread(&mm->mmap_sem);
- for (i = 0; i < ret; i++)
- put_page(pages[i]);
- kfree(pages);
- return -EFAULT;
- }
- static int demodrv_open(struct inode* inode, struct file* file)
- {
- int major = MAJOR(inode->i_rdev);
- int minor = MINOR(inode->i_rdev);
- printk("%s: major=%d, minor=%d\n", __func__, major, minor);
- return 0;
- }
- static int demodrv_release(struct inode* inode, struct file* file)
- {
- return 0;
- }
- static ssize_t
- demodrv_read(struct file* file, char __user* buf, size_t count, loff_t* ppos)
- {
- size_t nbytes =
- demodrv_read_write(buf, count, MYDEMO_READ);
- printk("%s: read nbytes=%ld done at pos=%d\n",
- __func__, nbytes, (int)*ppos);
- return nbytes;
- }
- static ssize_t
- demodrv_write(struct file* file, const char __user* buf, size_t count, loff_t* ppos)
- {
- size_t nbytes =
- demodrv_read_write((void*)buf, count, MYDEMO_WRITE);
- printk("%s: write nbytes=%ld done at pos=%d\n",
- __func__, nbytes, (int)*ppos);
- return nbytes;
- }
- static int
- demodrv_mmap(struct file* filp, struct vm_area_struct* vma)
- {
- unsigned long pfn;
- unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
- unsigned long len = vma->vm_end - vma->vm_start;
- if (offset >= MAX_DEVICE_BUFFER_SIZE)
- return -EINVAL;
- if (len > (MAX_DEVICE_BUFFER_SIZE - offset))
- return -EINVAL;
- printk("%s: mapping %ld bytes of device buffer at offset %ld\n",
- __func__, len, offset);
- /* pfn = page_to_pfn (virt_to_page (ramdisk + offset)); */
- pfn = virt_to_phys(device_buffer + offset) >> PAGE_SHIFT;
- if (remap_pfn_range(vma, vma->vm_start, pfn, len, vma->vm_page_prot))
- return -EAGAIN;
- return 0;
- }
- static long
- demodrv_unlocked_ioctl(struct file* filp, unsigned int cmd, unsigned long arg)
- {
- unsigned long tbs = MAX_DEVICE_BUFFER_SIZE;
- void __user* ioargp = (void __user*)arg;
- switch (cmd) {
- default:
- return -EINVAL;
- case MYDEV_CMD_GET_BUFSIZE:
- if (copy_to_user(ioargp, &tbs, sizeof(tbs)))
- return -EFAULT;
- return 0;
- }
- }
- static const struct file_operations demodrv_fops = {
- .owner = THIS_MODULE,
- .open = demodrv_open,
- .release = demodrv_release,
- .read = demodrv_read,
- .write = demodrv_write,
- .mmap = demodrv_mmap,
- .unlocked_ioctl = demodrv_unlocked_ioctl,
- };
- static struct miscdevice mydemodrv_misc_device = {
- .minor = MISC_DYNAMIC_MINOR,
- .name = DEMO_NAME,
- .fops = &demodrv_fops,
- };
- static int __init simple_char_init(void)
- {
- int ret;
- device_buffer = kmalloc(MAX_DEVICE_BUFFER_SIZE, GFP_KERNEL);
- if (!device_buffer)
- return -ENOMEM;
- ret = misc_register(&mydemodrv_misc_device);
- if (ret) {
- printk("failed register misc device\n");
- kfree(device_buffer);
- return ret;
- }
- mydemodrv_device = mydemodrv_misc_device.this_device;
- printk("succeeded register char device: %s\n", DEMO_NAME);
- return 0;
- }
- static void __exit simple_char_exit(void)
- {
- printk("removing device\n");
- kfree(device_buffer);
- misc_deregister(&mydemodrv_misc_device);
- }
- module_init(simple_char_init);
- module_exit(simple_char_exit);
- MODULE_AUTHOR("rlk");
- MODULE_LICENSE("GPL v2");
get_user_pages_fast 用于锁定用户空间的页面,以便它们在操作期间不会被交换出内存。它的函数原型为
- int get_user_pages_fast(unsigned long start,
- int nr_pages, int write, struct page** pages);
其中,start 是开始的用户地址;nr_pages 是要锁定的页数;write 写访问标志,如果设置,锁定的页面将为写访问,否则为读访问;pages 返回锁定页的数组。
如果 get_user_pages_fast 调用成功,它返回实际锁定的页面数;错误的话,返回一个负的错误码。
get_user_pages_fast 锁定的页面会增加其引用计数。所以,注意,在对页面的处理完成后,需要使用 put_page 函数减少页面的引用计数,确保后续页面交换和释放的正确进行。其函数定义为
- void put_page(struct page* page);
get/put。put 为“放回”的意思。
当得到用户空间地址所对应的页面后,为了使内核地址空间可以访问其内容,需要使用 kmap 函数。其函数定义为
- void* kmap(struct page* page);
kmap 函数的作用是将高端内存区域(这边就是用户空间区域)的页面映射到内核的地址空间中,从而允许内核直接访问它。注意,使用完之后,调用 kunmap 函数释放。
在 kmap 得到映射的地址后,我们就可以在内核空间中,自由的调用像 memcpy 这种内存操作函数,对内存进行操作。本处代码中,就是将用户层中的 buf,按一页面一页面进行复制操作。
写完内核模块之后,我们写一个用户层的测试程序进行测试。如代码清单 6.2 所示,它先在用户空间申请两个读写缓冲区,然后调用 write 接口,把写缓冲区内容写入内核模块;接着调用 read 接口,把内核模块中的内容写回用户层的读缓冲区。测试结果是读、写缓存区中的内容是要一样的。
- #include <stdio.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <sys/mman.h>
- #include <string.h>
- #include <errno.h>
- #include <fcntl.h>
- #include <sys/ioctl.h>
- #include <malloc.h>
- #define DEMO_DEV_NAME "/dev/my_demo_dev"
- #define MYDEV_CMD_GET_BUFSIZE 1 /* defines our IOCTL cmd */
- int main()
- {
- int fd;
- int i;
- size_t len;
- char *read_buffer, *write_buffer;
- fd = open(DEMO_DEV_NAME, O_RDWR);
- if (fd < 0) {
- printf("open device %s failded\n", DEMO_DEV_NAME);
- return -1;
- }
- if (ioctl(fd, MYDEV_CMD_GET_BUFSIZE, &len) < 0) {
- printf("ioctl fail\n");
- goto open_fail;
- }
- printf("driver max buffer size=%ld\n", len);
- read_buffer = malloc(len);
- if (!read_buffer)
- goto open_fail;
- write_buffer = malloc(len);
- if (!write_buffer)
- goto buffer_fail;
- /* modify the write buffer */
- for (i = 0; i < len; i++)
- *(write_buffer + i) = 0x55;
- if (write(fd, write_buffer, len) != len) {
- printf("write fail\n");
- goto rw_fail;
- }
- /* read the buffer back and compare with the mmap buffer*/
- if (read(fd, read_buffer, len) != len) {
- printf("read fail\n");
- goto rw_fail;
- }
- if (memcmp(write_buffer, read_buffer, len)) {
- printf("buffer compare fail\n");
- goto rw_fail;
- }
- printf("data modify and compare succussful\n");
- free(write_buffer);
- free(read_buffer);
- close(fd);
- return 0;
- rw_fail:
- if (write_buffer)
- free(write_buffer);
- buffer_fail:
- if (read_buffer)
- free(read_buffer);
- open_fail:
- close(fd);
- return 0;
- }
但是,测试下来,不仅读、写两个缓存区的内容不一致,而且 free 时产生 crash。
为了调查问题产生的原因,如代码清单 6.2 所示,我们添加内存内容的打印,以此核对读写哪里发生了问题。
从打印的结果看,写入的内容和用户空间写入的内容不一致。有一个偏移开始,才是正确的内容,写入了很多“随机”数据。
进一步了解各个函数的作用,可以得知,get_user_pages_fast 是获取地址所在的页面,然后 kmap 映射的是整个页面的内容。举一个极端的例子,buf 即使只有两个字节,但是它们正好是跨页,get_user_pages_fast 也是返回的两页。
目前内核模块中的流程,读写都是按整页整页进行的,所以会把整页中的“未定义”数据也进行写入了。自然测试程序中的比较步骤不会通过。
同时内核模块中往用户空间写数据的时候,按页写的话就动了它不该动的地方。malloc 的数据内容前后,也包括 malloc 本身维护记录的内容。把它们也改写了的话,在 free 的时候就会发生错误。
- void custom_print_hex_dump_bytes(const char *prefix_str, const void *buf, size_t len)
- {
- int i;
- const unsigned char *data = buf;
- char linebuf[3 * 64 + 2]; // 为了适应最多64个字节的情况
- if (!prefix_str) prefix_str = "";
- for (i = 0; i < len; i++) {
- if (i % 64 == 0 && i) { // 这里改为每64字节换一行
- printk("%s%s\n", prefix_str, linebuf);
- memset(linebuf, 0, sizeof(linebuf)); // 清空buffer
- }
- snprintf(linebuf + 3 * (i % 64), sizeof(linebuf) - 3 * (i % 64), "%02x ", data[i]);
- }
- if (i % 64 || i == len) {
- linebuf[3 * (i % 64)] = 0; // 结束字符串
- printk("%s%s\n", prefix_str, linebuf);
- }
- }
- static size_t demodrv_read_write(void* buf, size_t len, int rw)
- {
- int ret, npages, i;
- struct page** pages;
- struct mm_struct* mm = current->mm;
- char* kmap_addr, *dev_buf;
- size_t size = 0;
- size_t count = 0;
- unsigned long user_offset = offset_in_page(buf);
- dev_buf = device_buffer;
- /* how many pages? */
- npages = DIV_ROUND_UP(len + user_offset, PAGE_SIZE);
- printk("%s: len=%ld, npage=%d\n", __func__, len, npages);
- pages = kmalloc(npages * sizeof(pages), GFP_KERNEL);
- if (!pages)
- {
- printk("alloc pages fail\n");
- return -ENOMEM;
- }
- down_read(&mm->mmap_lock);
- printk("buf=%px,user_offset=%lu,npages=%d\n", buf, user_offset, npages);
- ret = get_user_pages_fast((unsigned long)buf, npages, 1, pages);
- if (ret < npages)
- {
- printk("pin page fail\n");
- goto fail_pin_pages;
- }
- up_read(&mm->mmap_lock);
- printk("pin %d pages from user done\n", npages);
- for (i = 0; i < npages; i++)
- {
- kmap_addr = kmap(pages[i]);
- custom_print_hex_dump_bytes("page: ", kmap_addr, PAGE_SIZE);
- size = min_t(size_t, PAGE_SIZE - user_offset, len);
- printk("PAGE_SIZE=%lu,user_offset=%lu,size=%lu\n", PAGE_SIZE, user_offset, size);
- switch (rw)
- {
- case MYDEMO_READ:
- memcpy(kmap_addr + user_offset, dev_buf + count, size);
- custom_print_hex_dump_bytes("read page: ", kmap_addr, size);
- break;
- case MYDEMO_WRITE:
- memcpy(dev_buf + count, kmap_addr + user_offset, size);
- custom_print_hex_dump_bytes("write device: ", kmap_addr, size);
- break;
- default:
- break;
- }
- put_page(pages[i]);
- kunmap(pages[i]);
- len -= size;
- count += size;
- user_offset = 0;
- }
- kfree(pages);
- printk("%s: %s user buffer %ld bytes done\n", __func__, rw ? "write" : "read", count);
- return count;
- fail_pin_pages:
- up_read(&mm->mmap_lock);
- for (i = 0; i < ret; i++)
- put_page(pages[i]);
- kfree(pages);
- return -EFAULT;
- }
代码清单 6.2 中的修复做法是,使用 offset_in_page 函数获取地址在页中的偏移量,然后在第一页操作的时候加上对偏移的考虑,确保读写的位置正确。
书中的修复做法如代码清单 6.3 所示,它改变应用层,使用匿名映射(MAP_ANON)来申请一块内存。因为 mmap 返回的地址是天然页对齐的。匿名映射不关联任何文件,只是简单的分配内存。
- read_buffer = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
- if (read_buffer == MAP_FAILED)
- goto open_fail;
- write_buffer = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
- if (write_buffer == MAP_FAILED)
- goto buffer_fail;
mmap 返回的地址是天然页对齐的。
改变应用层的思路是,只要传入的地址是页对齐的,那么数据肯定在页面开头,原先内核模块的逻辑就能跑通。所以如代码清单 6.4,用 posix_memalign 手动按页对齐也是可以的。
- size_t alignment = sysconf(_SC_PAGESIZE);
- if (posix_memalign(&read_buffer, alignment, len) != 0)
- {
- goto buffer_fail;
- }
- if (posix_memalign(&write_buffer, alignment, len) != 0)
- {
- goto buffer_fail;
- }