Linux内核源代码情景分析-外部设备存储空间的地址映射

随着计算机技术的发展,人们发现单纯的I/O映射方式是不能满足要求的。此种方式只适合于早期的计算机技术,那时候一个外设通常都只有几个寄存器,通过这几个寄存器就可以完成对外设的所有操作了。而现在的情况却不大一样。例如,在PC机上可以插上一块图像卡,带有2MB的存储器,甚至还可能带有一块ROM,里面装有可执行代码。所以要将外设卡上的存储器映射到内存空间,实际上是虚拟空间的手段。在Linux内核中,这样的映射是通过函数ioremap()来建立的。

对于内存页面的管理,通常 我们都是先在虚拟空间分配一个虚拟空间,然后为此区间分配相应的物理内存页面并建立起映射。而且这样的映射也并不是一次就建立完毕,可以在访问这些虚拟页面引起页面异常时逐步地建立。

但是,ioremap()则不同,首先我们先有一个物理存储区间,其地址就是外设卡上的存储器出现在总线的地址(不是存储单元在外设卡上局部的物理地址)。在Linux系统中,CPU不能按物理地址来访问存储空间,而必须使用虚拟地址,所以必须”反向“地从物理地址出发找到一片虚拟空间并建立起映射。其次,这样的需求只发生于对外部设备的操作,而这是内核的事,所以相应的虚拟空间是在系统空间(3GB以上)。

我们先看ioremap,代码如下:

extern inline void * ioremap (unsigned long offset, unsigned long size)
{
	return __ioremap(offset, size, 0);
}

__ioremap,代码如下:

void * __ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags)
{
	void * addr;
	struct vm_struct * area;
	unsigned long offset, last_addr;

	/* Don‘t allow wraparound or zero size */
	last_addr = phys_addr + size - 1;
	if (!size || last_addr < phys_addr)
		return NULL;

	/*
	 * Don‘t remap the low PCI/ISA area, it‘s always mapped..
	 */
	if (phys_addr >= 0xA0000 && last_addr < 0x100000)
		return phys_to_virt(phys_addr);

	/*
	 * Don‘t allow anybody to remap normal RAM that we‘re using..
	 */
	if (phys_addr < virt_to_phys(high_memory)) {
		char *t_addr, *t_end;
		struct page *page;

		t_addr = __va(phys_addr);
		t_end = t_addr + (size - 1);

		for(page = virt_to_page(t_addr); page <= virt_to_page(t_end); page++)
			if(!PageReserved(page))
				return NULL;
	}
        //我们假设符合条件,执行到这里
	/*
	 * Mappings have to be page-aligned
	 */
	offset = phys_addr & ~PAGE_MASK;
	phys_addr &= PAGE_MASK;//物理地址
	size = PAGE_ALIGN(last_addr) - phys_addr; //从起始物理地址到结束共多少个字节

	/*
	 * Ok, go for it..
	 */
	area = get_vm_area(size, VM_IOREMAP);//在vmlist_lock找出合适的虚拟地址,并将vm_struct插入vmlist_lock
	if (!area)
		return NULL;
	addr = area->addr;//虚拟地址
	if (remap_area_pages(VMALLOC_VMADDR(addr), phys_addr, size, flags)) {//建立虚拟地址和物理地址之间的映射
		vfree(addr);
		return NULL;
	}
	return (void *) (offset + (char *)addr);//返回的是虚拟地址加上物理页面的后12位偏移
}

get_vm_area,在vmlist_lock找出合适的虚拟地址,并将vm_struct插入vmlist_lock,代码如下:

struct vm_struct * get_vm_area(unsigned long size, unsigned long flags)
{
	unsigned long addr;
	struct vm_struct **p, *tmp, *area;

	area = (struct vm_struct *) kmalloc(sizeof(*area), GFP_KERNEL);
	if (!area)
		return NULL;
	size += PAGE_SIZE;
	addr = VMALLOC_START;
	write_lock(&vmlist_lock);
	for (p = &vmlist; (tmp = *p) ; p = &tmp->next) {//在vmlist_lock找出合适的虚拟地址
		if ((size + addr) < addr) {
			write_unlock(&vmlist_lock);
			kfree(area);
			return NULL;
		}
		if (size + addr < (unsigned long) tmp->addr)
			break;
		addr = tmp->size + (unsigned long) tmp->addr;
		if (addr > VMALLOC_END-size) {
			write_unlock(&vmlist_lock);
			kfree(area);
			return NULL;
		}
	}
	area->flags = flags;
	area->addr = (void *)addr;//虚拟地址
	area->size = size;//大小
	area->next = *p;//将vm_struct插入vmlist_lock
	*p = area;
	write_unlock(&vmlist_lock);
	return area;
}

其中,vm_struct 结构如下:

struct vm_struct {
	unsigned long flags;
	void * addr;
	unsigned long size;
	struct vm_struct * next;
};
struct vm_struct * vmlist;

回到__ioremap,开始执行remap_area_pages,建立虚拟地址和物理地址之间的映射,代码如下:

static int remap_area_pages(unsigned long address, unsigned long phys_addr,
				 unsigned long size, unsigned long flags)//address为虚拟地址,phys_addr为物理地址
{
	pgd_t * dir;
	unsigned long end = address + size;

	phys_addr -= address;//减去address,后来又加上了address
	dir = pgd_offset(&init_mm, address);//指向页目录项
	flush_cache_all();
	if (address >= end)
		BUG();
	do {
		pmd_t *pmd;
		pmd = pmd_alloc_kernel(dir, address);//中转一下,还是指向页目录项
		if (!pmd)
			return -ENOMEM;
		if (remap_area_pmd(pmd, address, end - address,
					 phys_addr + address, flags))//end-address为大小,phys_addr + address是物理地址
			return -ENOMEM;
		address = (address + PGDIR_SIZE) & PGDIR_MASK;
		dir++;//指向下一个页目录项
	} while (address && (address < end));
	flush_tlb_all();
	return 0;
}

remap_area_pmd,代码如下:

static inline int remap_area_pmd(pmd_t * pmd, unsigned long address, unsigned long size,
	unsigned long phys_addr, unsigned long flags)
{
	unsigned long end;

	address &= ~PGDIR_MASK;
	end = address + size;
	if (end > PGDIR_SIZE)
		end = PGDIR_SIZE;
	phys_addr -= address;
	if (address >= end)
		BUG();
	do {
		pte_t * pte = pte_alloc_kernel(pmd, address);//指向了页表项
		if (!pte)
			return -ENOMEM;
		remap_area_pte(pte, address, end - address, address + phys_addr, flags);//end-address为大小,phys_addr + address是物理地址
		address = (address + PMD_SIZE) & PMD_MASK;
		pmd++;//指向下一个页目录项
	} while (address && (address < end));
	return 0;
}

remap_area_pte,代码如下:

static inline void remap_area_pte(pte_t * pte, unsigned long address, unsigned long size,
	unsigned long phys_addr, unsigned long flags)
{
	unsigned long end;

	address &= ~PMD_MASK;
	end = address + size;
	if (end > PMD_SIZE)
		end = PMD_SIZE;
	if (address >= end)
		BUG();
	do {
		if (!pte_none(*pte)) {
			printk("remap_area_pte: page already exists\n");
			BUG();
		}
		set_pte(pte, mk_pte_phys(phys_addr, __pgprot(_PAGE_PRESENT | _PAGE_RW |
					_PAGE_DIRTY | _PAGE_ACCESSED | flags)));//页表项指向对应的页面,且页面的属性被设置为_PAGE_PRESENT,_PAGE_RW,_PAGE_DIRTY,_PAGE_ACCESSED
		address += PAGE_SIZE;
		phys_addr += PAGE_SIZE;
		pte++;//指向下一个页表项
	} while (address && (address < end));
}

回到__ioremap,最后返回的是虚拟地址加上物理页面的后12位偏移为最终的虚拟地址,前面已经建立了映射(页目录项和页表项),这样,根据这个虚拟地址就可以访问到物理地址的第一行代码。

由于内核mm_struct结构init_mm是单独的,从任何一个进程的task结构中都到达不了init_mm。所以,kswapd根本就看不到init_mm中的虚拟区间,这些区间的页面就自然不会被换出而常驻内存。

时间: 2024-12-21 02:38:38

Linux内核源代码情景分析-外部设备存储空间的地址映射的相关文章

Linux内核源代码情景分析-共享内存

一.库函数shmget()--共享内存区的创建与寻找 asmlinkage long sys_shmget (key_t key, size_t size, int shmflg) { struct shmid_kernel *shp; int err, id = 0; down(&shm_ids.sem); if (key == IPC_PRIVATE) { err = newseg(key, shmflg, size);//分配一个共享内存区供本进程专用,最后返回的是一体化的标示号 } el

Linux内核源代码情景分析-系统初始化

我们跳过boot,setup,直接来到head代码,内核映像的起点是stext,也是_stext,引导和解压缩以后的整个映像放在内存从0x100000即1MB开始的区间.CPU执行内核映像的入口startup_32就在内核映像开头的地方,因此其物理地址也是0x100000. 然而,在正常运行时整个内核映像都应该在系统空间中,系统空间的虚拟地址与物理地址间有个固定的位移,这就是0xC0000000,即3GB.所以,在连接内核映像时已经在所有的符号地址加了一个偏移量0xC0000000,这样star

Linux内核源代码情景分析系列

http://blog.sina.com.cn/s/blog_6b94d5680101vfqv.html Linux内核源代码情景分析---第五章 文件系统 5.1 概述 构成一个操作系统最重要的就是 进程管理 与 文件系统: 有些操作系统有进程管理而没有文件系统,有些操作系统有文件系统而没有进程管理(MSDOS):两者都没有那就不是操作系统了: 狭义的文件:指磁盘文件,进入指可以是有序地存储在任何介质中(包括内存)的一组信息. 广义的文件:(unix把外部设备也当成文件)凡是可以产生或消耗信息

Linux内核源代码情景分析-内存管理之slab-回收

在上一篇文章Linux内核源代码情景分析-内存管理之slab-分配与释放,最后形成了如下图的结构: 图 1 我们看到空闲slab块占用的若干页面,不会自己释放:我们是通过kmem_cache_reap和kmem_cache_shrink来回收的.他们的区别是: 1.我们先看kmem_cache_shrink,代码如下: int kmem_cache_shrink(kmem_cache_t *cachep) { if (!cachep || in_interrupt() || !is_chaine

Linux内核源代码情景分析-系统调用mknod

普通文件可以用open或者create创建,FIFO文件可以用pipe创建,mknod主要用于设备文件的创建. 在内核中,mknod是由sys_mknod实现的,代码如下: asmlinkage long sys_mknod(const char * filename, int mode, dev_t dev) //比如filename为/tmp/server_socket,dev是设备号 { int error = 0; char * tmp; struct dentry * dentry;

Linux内核源代码情景分析-文件系统的安装

执行sudo mount -t ext2 /dev/sdb1 /mnt/sdb,将文件系统挂在到/mnt/sdb上.系统调用mount,映射到内核层执行的是sys_mount.假设/dev/sdb1和/mnt/sdb都位于ext2文件系统中. asmlinkage long sys_mount(char * dev_name, char * dir_name, char * type, unsigned long flags, void * data)//dev_name指向了"/dev/sdb

Linux内核源代码情景分析-访问权限与文件安全性

在Linux内核源代码情景分析-从路径名到目标节点,一文中path_walk代码中,err = permission(inode, MAY_EXEC)当前进程是否可以访问这个节点,代码如下: int permission(struct inode * inode,int mask) { if (inode->i_op && inode->i_op->permission) { int retval; lock_kernel(); retval = inode->i_

Linux内核源代码情景分析-文件系统安装后的访问

在Linux内核源代码情景分析-文件系统的安装,一文中,已经调用sudo mount -t ext2 /dev/sdb1 /mnt/sdb,在/mnt/sdb节点上挂载了文件系统,那么我们接下来访问/mnt/sdb/hello.c节点.我们来看一下path_walk的执行有什么不同? int path_walk(const char * name, struct nameidata *nd) { struct dentry *dentry; struct inode *inode; int er

Linux内核源代码情景分析-内存管理

用户空间的页面有下面几种: 1.普通的用户空间页面,包括进程的代码段.数据段.堆栈段.以及动态分配的"存储堆". 2.通过系统调用mmap()映射到用户空间的已打开文件的内容. 3.进程间的共享内存区. 这些页面的的周转有两方面的意思. 1.页面的分配,使用,回收.如进程压栈时新申请的页面,这类页面不进行盘区交换,不使用时释放得以回收. 这部分通过一个场景来解释: Linux内核源代码情景分析-内存管理之用户堆栈的扩展. 2.盘区交换.如要执行硬盘上的对应代码段.把硬盘上的代码段换入内