Linux下内存空间分配物理地址与虚拟地址映射

Posted DS小龙哥

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux下内存空间分配物理地址与虚拟地址映射相关的知识,希望对你有一定的参考价值。


一、Linux内核动态内存分配与释放

1.1 kmalloc函数

Kmalloc分配的是连续的物理地址空间。如果需要连续的物理页,可以使用此函数,这是内核中内存分配的常用方式,也是大多数情况下应该使用的内存分配方式。

传递给函数的最常用的标志是GTP_ATOMIC和GTP_KERNEL。前面的标志表示进行不睡眠的高优先级分配。在中断处理程序和其他不能睡眠的代码段中需要。后面的标志可以睡眠,在没有持自旋锁的进程上下文中使用。此函数返回内核逻辑地址。

头文件:#include <linux/slab.h>

1.1.1 申请空间

static void *kmalloc(size_t size, gfp_t flags)

参数:

size_t size :申请的空间大小

gfp_t flags:申请的标志(模式)

返回值:申请的空间首地址。如果为NULL,表示分配失败!

一般填写的模式:

GFP_ATOMIC:用来从中断处理和进程上下文之外的其他代码中分配内存,分配内存优先级高,不会阻塞

GFP_KERNEL:内核内存的正常分配方式,可能会阻塞。


1.1.2释放内存空间

void kfree(const void *block)

参数:

void *block:将要释放空间的首地址

1.1.3示例

#include <linux/init.h>

#include <linux/module.h>


#include <linux/slab.h>

char *buff;

static int __init interrupt_init(void)

printk("init ok\\n");

申请空间*/

buff=kmalloc(1024, GFP_KERNEL);


初始化空间*/

memset(buff,0x10,1024);


打印出空间的数据*/

int i;

for(i=0;i<1024;i++)

printk("0x%X \\n",buff[i]);

return 0;


static void __exit interrupt_exit(void)

释放申请的空间*/

kfree(buff);

printk("exit ok\\n");


module_init(interrupt_init); /*驱动入口*/

module_exit(interrupt_exit); /*驱动出口*/

MODULE_LICENSE("GPL");


1.2 vmalloc 函数

分配的空间是线性的,在物理地址上不连续!最多分配1GB的空间。

定义文件:\\mm\\vmalloc.c

头文件:#include <linux/vmalloc.h>

1.2.1 申请空间

void *vmalloc(unsigned long size)

参数:

unsigned long size :分配空间的大小

返回值:申请的空间首地址

1.2.2 释放空间

void vfree(const void *addr)

参数:

const void *addr:释放的空间首地址

1.2.3 示例

#include <linux/init.h>

#include <linux/module.h>

#include <linux/vmalloc.h>

#include <linux/slab.h>

char *buff=NULL;

static int __init interrupt_init(void)

printk("init ok\\n");

申请空间*/

buff=vmalloc(1024);


if(buff==NULL)

内存空间分配失败!\\n\\n");


初始化空间*/

memset(buff,0x10,1024);


打印出空间的数据*/

int i;

for(i=0;i<1024;i++)

printk("0x%X \\n",buff[i]);

printk("buff=0x%x\\n",buff); //buff=0xf0537000

return 0;


static void __exit interrupt_exit(void)

释放申请的空间*/

vfree(buff);

printk("exit ok\\n");


module_init(interrupt_init); /*驱动入口*/

module_exit(interrupt_exit); /*驱动出口*/

MODULE_LICENSE("GPL");


1.3 区别总结

  1. kmalloc和vmalloc是分配的是内核的内存,malloc分配的是用户的内存
  2. kmalloc保证分配的内存在物理上是连续的,vmalloc保证的是在虚拟地址空间上的连续
  3. kmalloc能分配的大小有限,vmalloc能分配的大小相对较大
  4. 内存只有在要被DMA访问的时候才需要物理上连续
  5. vmalloc比kmalloc要慢


MMAP驱动实现

2.1 应用层mmap函数介绍

mmap函数用于将一个文件或者其它对象映射进内存,通过对这段内存的读取和修改,来实现对文件的读取和修改,而不需要再调用read,write等操作。

头文件:<sys/mman.h>

函数原型:

void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

int munmap(void* start,size_t length);

  • 映射函数

void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);

  1. addr: 指定映射的起始地址,通常设为NULL,由系统指定。
  2. length:映射到内存的文件长度。
  3. prot:映射的保护方式,可以是:PROT_EXEC:映射区可被执行
    PROT_READ:映射区可被读取
    PROT_WRITE:映射区可被写入
    PROT_NONE:映射区不能存取
  4. Flags:映射区的特性,可以是:MAP_SHARED:写入映射区的数据会复制回文件,且允许其他映射该文件的进程共享。
    MAP_PRIVATE:对映射区的写入操作会产生一个映射区的复制(copy_on_write),对此区域所做的修改不会写回原文件。
  5. fd:由open返回的文件描述符,代表要映射的文件。
  6. offset:以文件开始处的偏移量,必须是分页大小的整数倍,通常为0,表示从文件头开始映射。
  • 解除映射

int munmap(void *start, size_t length);

功能:取消参数start所指向的映射内存,参数length表示欲取消的内存大小。

返回值:解除成功返回0,否则返回-1

2.2 Linux内核的mmap接口

2.2.1 内核描述虚拟内存的结构体

Linux内核中使用结构体vm_area_struct来描述虚拟内存的区域,其中几个主要成员如下:

unsigned long vm_start 虚拟内存区域起始地址

unsigned long vm_end 虚拟内存区域结束地址

unsigned long vm_flags 该区域的标志

该结构体定义在<linux/mm_types.h>头文件中。

该结构体的vm_flags成员赋值的标志为:VM_IO和VM_RESERVED。

其中:VM_IO表示对设备IO空间的映射,M_RESERVED表示该内存区不能被换出,在设备驱动中虚拟页和物理页的关系应该是长期的,应该保留起来,不能随便被别的虚拟页换出(取消)。

2.2.2 mmap操作接口

在字符设备的文件操作集合(struct file_operations)中有mmap函数的接口。原型如下:

int (*mmap) (struct file *, struct vm_area_struct *);

其中第二个参数struct vm_area_struct *相当于内核找到的,可以拿来用的虚拟内存区间。mmap内部可以完成页表的建立。

2.2.3 实现mmap映射

映射一个设备是指把用户空间的一段地址关联到设备内存上,当程序读写这段用户空间的地址时,它实际上是在访问设备。这里需要做的两个操作:

1.找到可以用来关联的虚拟地址区间。

2.实现关联操作。

mmap设备操作实例如下:

static int tiny4412_mmap(struct file *myfile, struct vm_area_struct *vma)

vma->vm_flags |= VM_IO;//表示对设备IO空间的映射

vma->vm_flags |= VM_RESERVED;//标志该内存区不能被换出,在设备驱动中虚拟页和物理页的关系应该是长期的,应该保留起来,不能随便被别的虚拟页换出

if(remap_pfn_range(vma,//虚拟内存区域,即设备地址将要映射到这里

虚拟空间的起始地址

virt_to_phys(buf)>>PAGE_SHIFT,//与物理内存对应的页帧号,物理地址右移12位

//说明:向后面移动12位相当于除4096 ,为了得到页编号

映射区域大小,一般是页大小的整数倍

保护属性,

return -EAGAIN;


printk("tiny4412_mmap\\n");

return 0;

其中的buf就是在内核中申请的一段空间。使用kmalloc函数实现。

代码如下:

buf = (char *)kmalloc(MM_SIZE, GFP_KERNEL);

//内核申请内存只能按页申请,申请该内存以便后面把它当作虚拟设备

2.2.4 remap_pfn_range函数

remap_pfn_range函数用于一次建立所有页表。函数原型如下:

int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn, unsigned long size, pgprot_t prot);

其中vma是内核为我们找到的虚拟地址空间,addr要关联的是虚拟地址,pfn是要关联的物理地址,size是关联的长度是多少。

  • ioremap与phys_to_virt、virt_to_phys的区别:ioremap是用来为IO内存建立映射的, 它为IO内存分配了虚拟地址,这样驱动程序才可以访问这块内存。
    phys_to_virt只是计算出某个已知物理地址所对应的虚拟地址。将内核物理地址转化为虚拟地址
    virt_to_phys :将内核虚拟地址转化为物理地址
  • Linux下内存空间分配、物理地址与虚拟地址映射​_虚拟地址

IO地址空间映射

3.1 ioremap函数

ioremap将一个IO地址空间映射到内核的虚拟地址空间上去,便于访问。

void * __ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags)

void *ioremap(unsigned long phys_addr, unsigned long size)

参数:

phys_addr:要映射的起始的IO地址;

size:要映射的空间的大小;

flags:要映射的IO空间的和权限有关的标志;

phys_addr:是要映射的物理地址

size:是要映射的长度,单位是字节

头文件:#include <linux/io.h>

功能:将一个IO地址空间映射到内核的虚拟地址空间上去,便于访问;

实现:对要映射的IO地址空间进行判断,低PCI/ISA地址不需要重新映射,也不允许用户将IO地址空间映射到正在使用的RAM中,最后申请一个 vm_area_struct结构,调用remap_area_pages填写页表,若填写过程不成功则释放申请的vm_area_struct空间;

ioremap 依靠 __ioremap实现,它只是在__ioremap中以第三个参数为0调用来实现.

ioremap是内核提供的用来映射外设寄存器到主存的函数,我们要映射的地址已经从pci_dev中读了出来(上一步),这样就水到渠成的成功映射了而不会和其他地址有冲突。映射完了有什么效果呢,我举个例子,比如某个网卡有100 个寄存器,他们都是连在一块的,位置是固定的,假如每个寄存器占4个字节,那么一共400个字节的空间被映射到内存成功后,ioaddr就是这段地址的开头(注意ioaddr是虚拟地址,而mmio_start是物理地址,它是Bios得到的,肯定是物理地址,而保护模式下CPU不认物理地址,只认虚拟地址),ioaddr+0就是第一个寄存器的地址,ioaddr+4就是第二个寄存器地址(每个寄存器占4个字节),以此类推,我们就能够在内存中访问到所有的寄存器进而操控他们了。

3.2 iounmap函数

void iounmap(void *addr)

取消ioremap映射的空间。

3.3 补充说明

1、ioremap 按照页大小进行映射,而且是整页 。

2ioremap 允许对一个物理地址进行多次映射,而且分配的虚拟空间地址各不相同(多个虚拟地址对应于同一个物理地址)。而且,ioumap相互不影响。

3.4 示例

volatile unsigned int *GPD0CON;

volatile unsigned int *GPD0DAT;

GPD0CON=ioremap(0x114000A0,4); /*CON*/

GPD0DAT=ioremap(0x114000A4,4);

四、linux内核readl()和writel()函数

writel()往内存映射的I/O 上写入 32 位数据 (4字节)。

readl()从内存映射的I/O上读取32位的数据(4字节)

writel函数:

void writel(unsigned char data , unsigned short addr)

参数说明

data:写入的数据

addr:I/O地址


readl函数:

unsigned char readl (unsigned int addr )

参数说明

addr : I/O 地址。

返回值 : 从I/O空间读取的数值。

示例:

static long beep_ioctl(struct file *file, unsigned int cmd, unsigned long argv)

u32 data;

switch(cmd)

case 1234:

data=readl(GPD0DAT);

data|=1<<0;

writel(data,GPD0DAT);

printk("开蜂鸣器\\n");

//*GPD0DAT &=~(1 << 0); //关蜂鸣器

break;

case 5678:

data=readl(GPD0DAT);

data&=~(1<<0);

writel(data,GPD0DAT);

printk("关蜂鸣器\\n");

//*GPD0DAT |=(1 << 0); //开蜂鸣器

break;

return 0;

五、MMU(内存管理单元)

MMU是中央处理器(CPU)中用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权,多用户多进程操作系统。它一个与软件密切相关的硬件部件,也是理解Linux等操作系统内核机制的最大障碍之一。不搞清楚MMU原理会使编程思想停留在单片机与无OS的时代。

5.1 MMU历史概述

Linux下内存空间分配物理地址与虚拟地址映射

Linux下内存空间分配物理地址与虚拟地址映射

Linux 内核 内存管理内存映射原理 ① ( 物理地址空间 | 外围设备寄存器 | 外围设备寄存器的物理地址 映射到 虚拟地址空间 )

linux内存池能分配连续物理内存吗

malloc内存分配原理 [linux]--mallocbrkmmap

malloc内存分配原理 [linux]--mallocbrkmmap