用户空间存取内核空间,具体的实现方法要从两个方面考虑,先是用户进程,需要调用mmap来将自己的一段虚拟空间映射到内核态分配的物理内存;然后内核空间需要重新设置用户进程的这段虚拟内存的页表,使它的物理地址指向对应的物理内存。针对linux内核的几种不同的内存分配方
式(kmalloc、vmalloc和ioremap),需要进行不同的处理。关于这个话题,前面已有文章论述了,<<Linxu设备驱动程序>>也专门用一章的内容来讲述,它们所用的方法是完全一样的。这里只是重复说一遍,以温故而知新。
一、Linux内存管理概述
这里说一下我的理解,主要从数据结构说。
1、物理内存都是按顺序分成一页一页的,每页用一个page结构来描述。系统所有的物理页面的page结
构描述就组成了一个数组mem_map。
2、进程的虚拟地址空间用task_struct的域mm来描述,它是一个mm_struct结构,这个结构包含了指向进
程页目录的指针(pgd_t * pgd)和指向进程虚拟内存区域的指针(struct vm_area_struct * mmap)
3、进程虚拟内存区域具有相同属性的段用结构vm_area_struct描述(简称为VMA)。进程所有的VMA组
成一个链表,表头就是进程PCB中的mm域的mmap指针。当进程的VMA较多的时候,它们同时利用AVL平衡
树组织。
4、每个VMA就是一个对象,定义了一组操作,可以通过这组操作来对不同类型的VMA进行不同的处理。
例如对vmalloc分配的内存的映射就是通过其中的nopage操作实现的。
二、mmap处理过程
当用户调用mmap的时候,内核进行如下的处理:
1、先在进程的虚拟空间查找一块VMA;
2、将这块VMA去映射
3、如果设备驱动程序或者文件系统的file_operations定义了mmap操作,则调用它
4、将这个VMA插入到进程的VMA链中
file_operations的中定义的mmap方法原型如下:
int (*mmap) (struct file *, struct vm_area_struct *);
其中file是虚拟空间映射到的文件结构,vm_area_struct就是步骤1中找到的VMA。
三、缺页故障处理过程
当访问一个无效的虚拟地址(可能是保护故障,也可能缺页故障等)的时候,就会产生一个页故障,系
统的处理过程如下:
1、找到这个虚拟地址所在的VMA;
2、如果必要,分配中间页目录表和页表
3、如果页表项对应的物理页面不存在,则调用这个VMA的nopage方法,它返回物理页面的page描述结构
(当然这只是其中的一种情况)
4、针对上面的情况,将物理页面的地址填充到页表中
当页故障处理完后,系统将重新启动引起故障的指令,然后就可以正常访问了
下面是VMA的方法:
struct vm_operations_struct {
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
struct page * (*nopage)(struct vm_area_struct * area, unsigned long address, int
write_access);
};
其中缺页函数nopage的address是引起缺页故障的虚拟地址,area是它所在的VMA,write_access是存取
属性。
三、具体实现
3.1、对kmalloc分配的内存的映射
对kmalloc分配的内存,因为是一段连续的物理内存,所以它可以简单的在mmap例程中设置好页表的物
理地址,方法是使用函数remap_page_range。它的原型如下:
int remap_page_range(unsigned long from, unsigned long phys_addr, unsigned long size,
pgprot_t prot)
其中from是映射开始的虚拟地址。这个函数为虚拟地址空间from和from+size之间的范围构造页表;
phys_addr是虚拟地址应该映射到的物理地址;size是被映射区域的大小;prot是保护标志。
remap_page_range的处理过程是对from到form+size之间的每一个页面,查找它所在的页目录和页表(
必要时建立页表),清除页表项旧的内容,重新填写它的物理地址与保护域。
remap_page_range可以对多个连续的物理页面进行处理。<<Linux设备驱动程序>>指出,
remap_page_range只能给予对保留的页和物理内存之上的物理地址的访问,当对非保留的页使用
remap_page_range时,缺省的nopage处理控制映射被访问的虚地址处的零页。所以在分配内存后,就要
对所分配的内存置保留位,它是通过函数mem_map_reserve实现的,它就是对相应物理页面置
PG_reserved标志位。(关于这一点,参见前面的主题为“关于remap_page_range的疑问”的讨论)
因为remap_page_range有上面的限制,所以可以用另外一种方式,就是采用和vmalloc分配的内存同样
的方法,对缺页故障进行处理。
3.2、对vmalloc分配的内存的映射
3.2.1、vmalloc分配内存的过程
(1)、进行预处理和合法性检查,例如将分配长度进行页面对齐,检查分配长度是否过大;
(2)、以GFP_KERNEL为优先级调用kmalloc分配(GFP_KERNEL用在进程上下文中,所以这里就限制了在
中断处理程序中调用vmalloc)描述vmalloc分配的内存的vm_struct结构。
(3)、将size加一个页面的长度,使中间形成4K的隔离带,然后在VMALLOC_START和VMALLOC_END之间
编历vmlist链表,寻找一段自由内存区间,将其地址填入vm_struct结构中
(4)、返回这个地址
vmalloc分配的物理内存并不连续
3.2.2、页目录与页表的定义
typedef struct { unsigned long pte_low; } pte_t;
typedef struct { unsigned long pmd; } pmd_t;
typedef struct { unsigned long pgd; } pgd_t;
#define pte_val(x) ((x).pte_low)
3.2.3、常见例程:
(1)、virt_to_phys():内核虚拟地址转化为物理地址
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
extern inline unsigned long virt_to_phys(volatile void * address)
{
return __pa(address);
}
上面转换过程是将虚拟地址减去3G(PAGE_OFFSET=0XC000000),因为内核空间从3G到3G+实际内存一一
映射到物理地址的0到实际内存
(2)、phys_to_virt():内核物理地址转化为虚拟地址
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
extern inline void * phys_to_virt(unsigned long address)
{
return __va(address);
}
virt_to_phys()和phys_to_virt()都定义在include/asm-i386/io.h中
(3)、#define virt_to_page(kaddr) (mem_map + (__pa(kaddr) >> PAGE_SHIFT))(内核2.4)
#define VALID_PAGE(page) ((page - mem_map) < max_mapnr)(内核2.4)
第一个宏根据虚拟地址,将其转换为相应的物理页面的page描述结构,第二个宏判断页面是不是在有效
的物理页面内。(这两个宏处理的虚拟地址必须是内核虚拟地址,例如kmalloc返回的地址,对于
vmalloc返回的地址并不能这样,因为vmalloc分配的并不是连续的物理内存,中间可能有空洞)
3.2.4、vmalloc分配的内存的mmap的实现:
对vmalloc分配的内存需要通过设置相应VMA的nopage方法来实现,当产生缺页故障的时候,会调用VMA
的nopage方法,我们的目的就是在nopage方法中返回一个page结构的指针,为此,需要通过如下步骤:
(1) pgd_offset_k或者 pgd_offset:查找虚拟地址所在的页目录表,前者对应内核空间的虚拟地址
,后者对应用户空间的虚拟地址
#define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))
#define pgd_offset_k(address) pgd_offset(&init_mm, address)
对于后者,init_mm是进程0(idle process)的虚拟内存mm_struct结构,所有进程的内核页表都一样
。在vmalloc分配内存的时候,要刷新内核页目录表,2.4中为了节省开销,只更改了进程0的内核页目
录,而对其它进程则通过访问时产生页面异常来进行更新各自的内核页目录
(2)pmd_offset:找到虚拟地址所在的中间页目录项。在查找之前应该使用pgd_none判断是否存在相
应的页目录项,这些函数如下:
extern inline int pgd_none(pgd_t pgd) { return 0; }
extern inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address)
{
return (pmd_t *) dir;
}
(3)pte_offset:找到虚拟地址对应的页表项。同样应该使用pmd_none判断是否存在相应的中间页目
录:
#define pmd_val(x) ((x).pmd)
#define pmd_none(x) (!pmd_val(x))
#define __pte_offset(address) /
((address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
#define pmd_page(pmd) /
((unsigned long) __va(pmd_val(pmd) & PAGE_MASK))
#define pte_offset(dir, address) ((pte_t *) pmd_page(*(dir)) + /
__pte_offset(address))
(4)pte_present和pte_page:前者判断页表对应的物理地址是否有效,后者取出页表中物理地址对应
的page描述结构
#define pte_present(x) ((x).pte_low & (_PAGE_PRESENT | _PAGE_PROTNONE))
#define pte_page(x) (mem_map+((unsigned long)(((x).pte_low >> PAGE_SHIFT))))
#define page_address(page) ((page)->virtual)
下面的一个DEMO与上面的关系不大,它是做这样一件事情,就是在启动的时候保留一段内存,然后使用
ioremap将它映射到内核虚拟空间,同时又用remap_page_range映射到用户虚拟空间,这样两边都能访
问,通过内核虚拟地址将这段内存初始化串"abcd",然后使用用户虚拟地址读出来。
/************mmap_ioremap.c**************/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/wrapper.h> /* for mem_map_(un)reserve */
#include <asm/io.h> /* for virt_to_phys */
#include <linux/slab.h> /* for kmalloc and kfree */
MODULE_PARM(mem_start,"i");
MODULE_PARM(mem_size,"i");
static int mem_start=101,mem_size=10;
static char * reserve_virt_addr;
static int major;
int mmapdrv_open(struct inode *inode, struct file *file);
int mmapdrv_release(struct inode *inode, struct file *file);
int mmapdrv_mmap(struct file *file, struct vm_area_struct *vma);
static struct file_operations mmapdrv_fops =
{
owner: THIS_MODULE,
mmap: mmapdrv_mmap,
open: mmapdrv_open,
release: mmapdrv_release,
};
int init_module(void)
{
if ( ( major = register_chrdev(0, "mmapdrv", &mmapdrv_fops) ) < 0 )
{
printk("mmapdrv: unable to register character device/n");
return (-EIO);
}
printk("mmap device major = %d/n",major );
printk( "high memory physical address 0x%ldM/n",
virt_to_phys(high_memory)/1024/1024 );
reserve_virt_addr = ioremap( mem_start*1024*1024,mem_size*1024*1024);
printk( "reserve_virt_addr = 0x%lx/n", (unsigned long)reserve_virt_addr );
if ( reserve_virt_addr )
{
int i;
for ( i=0;i<mem_size*1024*1024;i+=4)
{
reserve_virt_addr[i] = 'a';
reserve_virt_addr[i+1] = 'b';
reserve_virt_addr[i+2] = 'c';
reserve_virt_addr[i+3] = 'd';
}
}
else
{
unregister_chrdev( major, "mmapdrv" );
return -ENODEV;
}
return 0;
}
/* remove the module */
void cleanup_module(void)
{
if ( reserve_virt_addr )
iounmap( reserve_virt_addr );
unregister_chrdev( major, "mmapdrv" );
return;
}
int mmapdrv_open(struct inode *inode, struct file *file)
{
MOD_INC_USE_COUNT;
return(0);
}
int mmapdrv_release(struct inode *inode, struct file *file)
{
MOD_DEC_USE_COUNT;
return(0);
}
int mmapdrv_mmap(struct file *file, struct vm_area_struct *vma)
{
unsigned long offset = vma->vm_pgoff<<PAGE_SHIFT;
unsigned long size = vma->vm_end - vma->vm_start;
if ( size > mem_size*1024*1024 )
{
printk("size too big/n");
return(-ENXIO);
}
offset = offset + mem_start*1024*1024;
/* we do not want to have this area swapped out, lock it */
vma->vm_flags |= VM_LOCKED;
if ( remap_page_range(vma->vm_start,offset,size,PAGE_SHARED))
{
printk("remap page range failed/n");
return -ENXIO;
}
return(0);
}
使用LDD2源码里面自带的工具mapper测试结果如下:
[root@localhost modprg]# insmod mmap_ioremap.mod
mmap device major = 254
high memory physical address 0x100M
reserve_virt_addr = 0xc7038000
[root@localhost modprg]# mknod mmapdrv c 254 0
[root@localhost modprg]# ./mapper mmapdrv 0 1024 | od -Ax -t x1
mapped "mmapdrv" from 0 to 1024
000000 61 62 63 64 61 62 63 64 61 62 63 64 61 62 63 64
*
000400
[root@localhost modprg]#