Posted on 2016-04-16 06:19:41 os
在前一篇博客中,我们介绍了Linux内核中内存映射的实现,以及用户态调用mmap系统调用的原理。接下来,我们会通过Linux内核模块的方式,映射两种物理内存:物理地址空间和内核模块申请的内存,演示内核模块中如何与用户态进程之间共享内存。
在开始之前,介绍一下/dev/mem
这个字符设备,打开/dev/mem
之后,通过mmap系统调用,就可以映射物理地址空间。需要注意的是,这里的物理地址空间,是站在CPU角度看到的地址空间,而不仅仅是RAM空间,还有外设的IO空间,例如BIOS ROM、Video ROM、PCI BUS等。
下面的内核模块代码工作的行为类似于/dev/mem
,打开对应的设备文件后,同样可以映射CPU地址空间。完整的代码在文章最后。
/*
* 第三个参数直接使用vma->vm_pgoff,
* 是因为这部分代码是直接从drivers/char/mem.c中拷贝而来,所以它模拟的设备工作方式也类似/dev/mem,
* 而/dev/mem的pgoff刚好等于pgn。
*
* 1.在进程的虚拟空间查找一块VMA.
* 2.将这块VMA进行映射.
* 3.如果设备驱动程序中定义了mmap函数,则调用它.
* 4.将这个VMA插入到进程的VMA链表中.
* 内存映射工作大部分由内核完成,驱动程序中的mmap函数只需要为该地址范围建立合适的页表,
* 并将vma->vm_ops替换为一系列的新操作就可以了。
*
* vma->start代表要建立页表的用户虚拟地址的起始地址,remap_pfn_range函数为处于vma->start和vma->start+size之间的虚拟地址建立页表。
* pfn是与物理内存起始地址对应的页帧号,虚拟内存将要被映射到该物理内存上。
* 页帧号只是将物理地址右移PAGE_SHIFT位。在大多数情况下,VMA结构中的vm_pgoff赋值给pfn即可。
* remap_pfn_range函数建立页表,对应的物理地址是pfn<<PAGE_SHIFT到pfn<<(PAGE_SHIFT)+size。
* size代表虚拟内存区域大小。
*/
static int simple_remap_mmap(struct file *filp, struct vm_area_struct *vma)
{
size_t size;
size = vma->vm_end - vma->vm_start;
printk (KERN_NOTICE "########### CALL remap_mmap");
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(filp, vma->vm_pgoff, size,
&vma->vm_page_prot))
return -EINVAL;
vma->vm_page_prot = phys_mem_access_prot(filp, vma->vm_pgoff,
size,
vma->vm_page_prot);
/* 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;
}
vma->vm_ops = &simple_remap_vm_ops;
simple_vma_open(vma);
return 0;
}
/*
* The fault version.
*/
/*
* 每次只映射一个PAGE
* 1.找到缺页的虚拟地址所在的VMA。
* 2.如果必要,分配中间页目录表和页表。
* 3.如果页表项对应的物理页面不存在,则调用fault函数,它返回物理页面的页描述符。
* 4.将物理页面的地址填充到页表中。
* 在上面第3步中,分配好的页目录和页表直接对应的是物理PAGE,
* 如果在不修改页表映射的情况下是这样,如果修改了就是别的PAGE物理地址。
* 下面的代码拿到的物理地址没有经过修改,所以是直接映射到物理内存的。
* 和上面的remap的行为一直,都是直接映射虚拟地址对应的物理地址。
* 和/dev/mem一致。
*/
int simple_vma_fault(struct vm_area_struct *vma,
struct vm_fault *vmf)
{
printk (KERN_NOTICE "########### CALL fault_mmap");
struct page *pageptr;
// 得到起始物理地址保存在offset中
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
// 得到vmf->virtual_address对应的物理地址,保存在physaddr中
unsigned long physaddr = (unsigned long)(vmf->virtual_address - vma->vm_start) + offset;
// 得到物理地址对应的页帧号,保存在pageframe中。
unsigned long pageframe = physaddr >> PAGE_SHIFT;
// Eventually remove these printks
printk (KERN_NOTICE "---- fault, off %lx phys %lx\n", offset, physaddr);
printk (KERN_NOTICE "VA is %p\n", __va (physaddr));
printk (KERN_NOTICE "Page at %p\n", virt_to_page (__va (physaddr)));
if (!pfn_valid(pageframe))
return VM_FAULT_SIGBUS;
// 由页帧号返回对应的page结构指针保存在pageptr中
pageptr = pfn_to_page(pageframe);
printk (KERN_NOTICE "page->index = %ld mapping %p\n", pageptr->index, pageptr->mapping);
printk (KERN_NOTICE "Page frame %ld\n", pageframe);
// 调用get_page增加pageptr指向页面的引用计数
get_page(pageptr);
vmf->page = pageptr;
return 0;
}
注意这里实现内存映射的方式有两种,simple_remap_mmap
中是通过remap_pfn_page
函数映射整个内存区域,而simple_vma_fault
中只对单一的内存页面做映射。
remap_pfn_page的函数原型含义如下:
remap_pfn_range(struct vma_area_struct *vma,
unsigned long addr,
unsigned long pfn,
unsigned long size,
pgprot_t prot);
vm_area_struct
,它定义了一个进程中的内存映射区域vm_area_struct
中定义的起始虚拟地址address >> PAGE_SHIFT
计算,PAGE_SHIFT是体系结构相关的宏定义,在32位下是12。上面的参数中比较关键的是pfn参数,它指的是物理页帧号。对应于mmap
系统调用的最后一个参数。因为内核模块(包括/dev/mem)实现中,没有做页地址对齐,所以在用户态进程中调用mmap时,要根据当前机器的PAGE大小做好地址对齐:
pa_offset = offset & ~(sysconf(_SC_PAGE_SIZE) - 1);
刚才介绍的是通过simple_remap_mmap
映射整个虚拟内存区域,一次性映射了整个vm_area_struct
指定的空间。如果我们每次只想映射一页呢?可以通过vma->vm_ops
指定fault
回调函数,
fault
回调函数中,比较关键的是下面几行:
struct page *pageptr;
// 得到起始物理地址保存在offset中
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
// 得到vmf->virtual_address对应的物理地址,保存在physaddr中
unsigned long physaddr = (unsigned long)(vmf->virtual_address - vma->vm_start) + offset;
// 得到物理地址对应的页帧号,保存在pageframe中。
unsigned long pageframe = physaddr >> PAGE_SHIFT;
// 由页帧号返回对应的page结构指针保存在pageptr中
pageptr = pfn_to_page(pageframe);
// 调用get_page增加pageptr指向页面的引用计数
get_page(pageptr);
// 将指向page的指针赋值给vmf->page
vmf->page = pageptr;
对应的用户态代码:
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <error.h>
#include <errno.h>
// BIOS ROM
#define SIZE 0x10000
#define OFFSET 0xf0000
// Video ROM
//#define SIZE 32767
//#define OFFSET 0x000c0000
// ACPI Tables
//#define SIZE 65535
//#define OFFSET 0x2fff0000
int main(int argc, char *argv[])
{
int fdin, fdout;
void *src, *dst;
struct stat statbuf;
unsigned char sz[SIZE]={0};
/*
if ((fdin = open("/dev/simple_fault", O_RDWR|O_SYNC)) < 0)
perror("can't open /dev/simple_fault for reading");
*/
if ((fdin = open("/dev/simple_remap", O_RDWR|O_SYNC)) < 0)
perror("can't open /dev/simple_remap for reading");
if ((src = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED,
fdin, OFFSET)) == MAP_FAILED)
perror("mmap error for simplen");
memcpy(sz, src, SIZE);
int i;
for (i=0; i<SIZE; i++) {
printf("%c", sz[i]);
}
munmap(src, SIZE);
close(fdin);
exit(0);
}
上面我们定义了此次映射内存的范围和大小:
#define SIZE 0x10000
#define OFFSET 0xf0000
从0xf0000
开始的大小为0x10000
(64K)内存区域,此处物理内存保存的是BIOS ROM。
上面一节的代码,映射的是物理内存,也就是从CPU角度看到的物理内存地址空间。 下面的代码中,映射了设备驱动中的内存。在用户态使用mmpa映射的方式和上面一样,区别在于设备驱动中对于这些虚拟内存地址不同的处理方式。
设备驱动中申请的内存是线性地址,需要将虚拟地址转换为物理地址,再得到物理地址的页帧号。
// 申明设备的内存地址指针
static unsigned char *myaddr_fault = NULL;
static unsigned char *myaddr_remap = NULL;
...
// 在内核高端内存空间分配一个页框,并返回指向页的线性地址
myaddr_fault = __get_free_pages(GFP_KERNEL, 1);
myaddr_remap = __get_free_pages(GFP_KERNEL, 1);
// 拷贝一些内容
strcpy(myaddr_fault, "00000000, fault version");
strcpy(myaddr_remap, "11111111, remap version");
...
/*
The remap version
*/
static int simple_remap_mmap(struct file *filp, struct vm_area_struct *vma)
{
size_t size;
size = vma->vm_end - vma->vm_start;
printk (KERN_NOTICE "########### CALL remap_mmap");
// 将线性地址转换为物理地址,并得到物理地址页框号
vma->vm_pgoff = __pa(myaddr_remap)>>PAGE_SHIFT;
/* 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;
}
vma->vm_ops = &simple_remap_vm_ops;
simple_vma_open(vma);
return 0;
}
...
/*
* The fault version.
*/
int simple_vma_fault(struct vm_area_struct *vma,
struct vm_fault *vmf)
{
struct page *pageptr;
printk (KERN_NOTICE "########### CALL fault_mmap");
unsigned long offset = vmf->virtual_address - vma->vm_start;
pageptr = virt_to_page(myaddr_fault);
printk (KERN_NOTICE "page->index = %ld mapping %p\n", pageptr->index, pageptr->mapping);
get_page(pageptr);
vmf->page = pageptr;
return 0;
}
上面的代码中,首先为设备申请了一页内存。申请内存使用使用了__get_free_pages函数。
__get_free_pages
在内核高端内存空间分配连续的页,并第一页的线性地址。在内核高端内存中,由于内核只能使用线性地址的最后一个GB,所以在高端内存中分配了一个页框之后,在内线线性地址空间最后128MB中的一部分映射高端内存页框,这种映射是暂时的。
因为内核只有1GB的线性地址空间,所以调用__pa
宏将内核线性地址转换为物理地址,宏的定义是:
// 将线性地址减去PAGE_OFFSET
// 32位下PAGE_OFFSET是3GB
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
接着执行有两种不同的映射方式:simple_remap_mmap和simple_vma_fault。
simple_remap_mmap的流程如下:
__pa(myaddr_remap)>>PAGE_SHIFT
simple_vma_fault的流程如下:
调用virt_to_page
从一个内核线性地址,得到它的也面描述符结构地址。原理是先将线性地址变为物理地址并得到物理地址的页帧号。得到页帧号之后,在全局的mem_map数据中就很容得到该页的页描述符地址。
get_page(pageptr)增加页的引用计数
将页描述符地址赋值给vmf->page
用户空间的代码如下:
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <error.h>
#include <errno.h>
#define SIZE 4096
#define OFFSET 0x00000000
//#define VERSION "remap"
#define VERSION "fault"
int main(int argc, char *argv[])
{
int fdin, fdout;
void *src, *dst;
struct stat statbuf;
unsigned char sz[SIZE]={0};
if (VERSION == "fault") {
if ((fdin = open("/dev/simple_fault", O_RDWR|O_SYNC)) < 0)
perror("can't open /dev/simple_fault for reading");
} else if (VERSION == "remap") {
if ((fdin = open("/dev/simple_remap", O_RDWR|O_SYNC)) < 0)
perror("can't open /dev/simple_remap for reading");
} else {
printf("bad version\n");
exit(1);
}
if ((src = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED,
fdin, OFFSET)) == MAP_FAILED)
perror("mmap error for simplen");
memcpy(sz, src, SIZE);
printf("%s\n", sz);
munmap(src, SIZE);
close(fdin);
exit(0);
}
Generic Alternative To Cialis
buy cialis 10mg sale - order cialis 5mg for sale cheap tadalafil sale
ivermectin 0.08 oral solution - ivermectin for people can you buy stromectol over the counter
order zithromax 500mg online cheap - buy zithromax 250mg online buy methylprednisolone 8 mg
propecia hairline regrowth where can i buy propecia will propecia affect anabolic steroids how to know if propecia is working
Мажор 4 сезон
20mg baclofen 430 mg baclofen can baclofen cause weight loss how long does it take for baclofen to take effect