linux /dev/mem /dev/kmem驱动分析

/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接口,进行物理页面映射到虚拟地址的操作。操作同上。

上一篇:mmap的随机化


下一篇:c – shm_open和ftruncate竞争条件可能吗?