/dev/mem
说明
/dev/mem”是linux系统的一个虚拟字符设备,无论是标准linux系统还是嵌入式linux系统,都支持该设备。 物理内存的全镜像。可以用来访问物理内存
/dev/kmem: kernel看到的虚拟内存的全镜像。可以用来访问kernel的内容。kernel部分内存用户空间本不可访问。但是因为所有进程共享内核空间的页表。所以内核虚拟地址对应物理地址是确定的。kmem的作用即是根据kernel的虚拟地址,找到对应的物理地址,再将物理地址映射到用户空间。这样访问映射的虚拟地址,即访问内核对应的内容。
驱动注册
/dev/mem
和/dev/kmem
驱动注册代码位于内核drivers/char/mem.c
。
根据下面代码可以看到,mem、kmem、null、zero等驱动设备都是在mem.c中预定义的。
static const struct memdev {
const char *name;
umode_t mode;
const struct file_operations *fops;
fmode_t fmode;
} devlist[] = {
#ifdef CONFIG_DEVMEM
[1] = { "mem", 0, &mem_fops, FMODE_UNSIGNED_OFFSET },
#endif
#ifdef CONFIG_DEVKMEM
[2] = { "kmem", 0, &kmem_fops, FMODE_UNSIGNED_OFFSET },
#endif
[3] = { "null", 0666, &null_fops, 0 },
#ifdef CONFIG_DEVPORT
[4] = { "port", 0, &port_fops, 0 },
#endif
[5] = { "zero", 0666, &zero_fops, 0 },
[7] = { "full", 0666, &full_fops, 0 },
[8] = { "random", 0666, &random_fops, 0 },
[9] = { "urandom", 0666, &urandom_fops, 0 },
#ifdef CONFIG_PRINTK
[11] = { "kmsg", 0644, &kmsg_fops, 0 },
#endif
};
class创建,设备节点注册,在chr_dev_init中进行。
static int __init chr_dev_init(void)
{
int minor;
if (register_chrdev(MEM_MAJOR, "mem", &memory_fops))
printk("unable to get major %d for memory devs\n", MEM_MAJOR);
mem_class = class_create(THIS_MODULE, "mem");
if (IS_ERR(mem_class))
return PTR_ERR(mem_class);
mem_class->devnode = mem_devnode;
for (minor = 1; minor < ARRAY_SIZE(devlist); minor++) {
if (!devlist[minor].name)
continue;
/*
* Create /dev/port?
*/
if ((minor == DEVPORT_MINOR) && !arch_has_dev_port())
continue;
device_create(mem_class, NULL, MKDEV(MEM_MAJOR, minor),
NULL, devlist[minor].name);
}
return tty_init();
}
代码中首先创建了mem class。随后遍历devlist数组,逐一调用device_create
创建设备节点。根据这段代码我们也可以看到,这些设备驱动,并没有什么初始化操作,唯一相关的就是各自的file_operations
回调。
本文主要分析/dev/mem
和/dev/kmem
驱动。
用户空间调用
“/dev/mem”设备通常与“mmap”结合使用,将该设备的物理内存映射到用户态,在用户态直接访问内核态物理内存。
1)第一步,open一个“/dev/mem”文件描述符,访问权限可以为只读(O_RDONLY )、只写(O_WRONLY )、读写(O_RDWR )的阻塞或者非阻塞方式。
int fd = 0;
fd = open("/dev/mem", O_RDWR | O_NDELAY); /* 读写权限,非阻塞 */
2)第二步,通过mmap把需访问的目标物理地址与“/dev/mem”文件描述符建立映射。
char *mmap_addr = NULL;
mmap_addr=(char *)mmap(NULL, MMAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, MMAP_ADDR);
3)第三步,地址读写访问。
int a = 0;
*(int*)mmap_addr = 0x10; /* 写地址 */
a = *(int)mmap_addr; /* 读地址 */
/dev/kmem
通常用来读取内核静态变量值。可以通过cat /proc/kallsyms
命令获取内核symbol或者,直接查看内核编译生成的System.map
文件。获取要访问的变量的虚拟地址。随后进行上述(1)(2)(3)操作即可。
mmap方法
不管是/dev/mem还是/dev/kmem。最核心代码就是file_operations->mmap
。
/dev/mem
mmap是系统调用,产生软中断进入内核后调用sys_mmap,最终会调用到mem驱动的mmap实现函数。
来看下mem.c中的mmap实现:mmap_mem
static int mmap_mem(struct file *file, struct vm_area_struct *vma)
{
size_t size = vma->vm_end - vma->vm_start;
if (!valid_mmap_phys_addr_range(vma->vm_pgoff, size))
return -EINVAL;
if (!private_mapping_ok(vma))
return -ENOSYS;
if (!range_is_allowed(vma->vm_pgoff, size))
return -EPERM;
if (!phys_mem_access_prot_allowed(file, vma->vm_pgoff, size,
&vma->vm_page_prot))
return -EINVAL;
vma->vm_page_prot = phys_mem_access_prot(file, vma->vm_pgoff,
size,
vma->vm_page_prot);
vma->vm_ops = &mmap_mem_ops;
/* Remap-pfn-range will mark the range VM_IO */
if (remap_pfn_range(vma,
vma->vm_start,
vma->vm_pgoff,
size,
vma->vm_page_prot)) {
return -EAGAIN;
}
return 0;
}
sys_mmap会查找一片空闲的用户空间虚拟地址,创建对应的vma。mmap_mem要做的就将vma的虚拟地址,与实际物理页面创建映射。
vma->vm_pgoff代表要映射的物理地址页序号。valid_mmap_phys_addr_range
检查物理地址是否非法。range_is_allowed
检查映射范围是否被允许。当内核未配置CONFIG_STRICT_DEVMEM
选项,该函数直接返回1,即可以访问所有地址空间。当内核配置了CONFIG_STRICT_DEVMEM
选项,则需要对地址进行判断,是否允许访问。phys_mem_access_prot
确定我们映射页的权限,该函数是平台函数,以arm实现为例,如下
pgprot_t phys_mem_access_prot(struct file *file, unsigned long pfn,
unsigned long size, pgprot_t vma_prot)
{
if (!pfn_valid(pfn))
return pgprot_noncached(vma_prot);
else if (file->f_flags & O_SYNC)
return pgprot_writecombine(vma_prot);
return vma_prot;
}
以上的检查完毕,最后调用remap_pfn_range完成页表设置。vma->vm_start
是vma虚拟起始地址。vma->vm_pgoff
是要映射的物理页面页帧号。
完成mmap
操作后,最终,sys_mmap将映射完成的vma的起始地址返回给用户空间。这样,用户空间就可以通过mmap返回的地址,访问要访问的物理地址了。
/dev/kmem
static int mmap_kmem(struct file *file, struct vm_area_struct *vma)
{
unsigned long pfn;
/* Turn a kernel-virtual address into a physical page frame */
pfn = __pa((u64)vma->vm_pgoff << PAGE_SHIFT) >> PAGE_SHIFT;
/*
* RED-PEN: on some architectures there is more mapped memory than
* available in mem_map which pfn_valid checks for. Perhaps should add a
* new macro here.
*
* RED-PEN: vmalloc is not supported right now.
*/
if (!pfn_valid(pfn))
return -EIO;
vma->vm_pgoff = pfn;
return mmap_mem(file, vma);
}
kmem是内核内存访问。所以vma传进来的pg_off是虚拟地址页帧号。通过__pa
宏获取物理页帧号。然后调用mmap_mem接口,进行物理页面映射到虚拟地址的操作。操作同上。