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);
}
Live casino games offer a practical gaming experience similar to a land-based casino!!! Want to know more about the site? visit here —>> OKBet Online Casino
Wow, incredible weblog layout! How long have you been blogging for? you made running a blog glance easy. The entire look of your web site is great, let alone the content! ufabet1688
how to get meloset without a prescription how to buy norethindrone buy danazol 100 mg generic
ferrous medication buy ferrous sulfate 100 mg for sale sotalol 40mg price
omeprazole 10mg price omeprazole 10mg ca lopressor 50mg us
modafinil 100mg oral modafinil 200mg without prescription deltasone 40mg without prescription
azithromycin order omnacortil 10mg tablet neurontin sale
livewin365 เล่นเกมสล็อตออนไลน์กับเว็บที่เหมาะสมที่สุด pg slot เป็นค่ายเกมสล็อตออนไลน์ที่มีความปลอดภัยสูง สามารถแทงได้ไม่จำกัด สล็อตเว็บใหญ่ จ่ายไม่ยั้ง ด้วยความที่เล่นได้มากมาย
order allopurinol 100mg without prescription allopurinol for sale online cheap crestor 10mg