linux内核内存

**********************************************************
linux内核内存相关内容:
1.内存空间和IO空间:
X86架构:有两类总线
一类总线的位宽为16位,硬件地址空间范围64K,如果将外设接到这个总线上,那么CPU访问这个外设通过in,out指令来完成访问;这个地址空间称之为IO空间;

另一类总线的位宽是32位,硬件地址空间范围是4G,如果将外设接到这个总线上,比如内存,GPIO寄存器,nandflash控制器等,那么CPU访问这写外设通过地址或者指针的形式来访问;这个地址空间称之为内存空间;

ARM结构:只有内存空间;无IO空间一说!

2.物理地址和虚拟地址
明确:CPU最终访问的地址都是物理地址;
明确:不管是在内核空间还是用户空间,程序访问的地址都是虚拟地址;
虚拟地址的优点:
1.如果物理内存比如只有1G,采用虚拟地址这个策略,能够让用户看到比实际的物理内存大很多的内存空间;
2.由于CPU最终访问的地址是物理地址,所以在进行虚拟地址和物理地址转换的时候,可以对地址进行有效性和权限的检查,保证系统的安全;
3.4G虚拟地址空间的划分:
用户空间:0x00000000 ~ 0xBFFFFFFF
内核空间:0xC0000000~ 0x FFFFFFFF
4.用户进程都有自己独立的3G空间,互补干涉;但是内核空间的1G,所有进程都共享;
5.用户空间和内核空间要进行数据交互,必须通过系统调用

3.MMU
MMU是CPU自带的硬件逻辑单元,一旦有这个硬件逻辑单元,那么CPU就可以采用虚拟地址策略,MMU的主要职责:
1.将虚拟地址转换成对应的物理地址,根据页表
2.检查地址的有效性和权限,决定是访问操作这个地址还是给CPU发送一个异常信号!

4.TLB
TLB是MMU自带的硬件逻辑单元,类似"Cache",它用于页表的缓存,每当MMU进行地址转换的时候,首先到TLB中找转换关系,如果找到,进行转换,如果没有找到,就从主存中取出页表信息进行地址转换,更新TLB,为下一次转换做准备!

5.uclinux
运行在不带MMU,不支持MMU的CPU上;
处理的地址都是物理地址;
ARM7,比如三星s3c4510,s3c44b0等;
FPGA处理器+ARM核
DSP处理器

6.物理页
linux内核管理物理内存,单位不是按字节,按页来进行管理,一页默认是4K;
内核描述每一个物理页都是使用struct page结构体,如果这个物理地址和一个虚拟地址做好了映射,这个结构体中的virtual字段保存映射的虚拟地址;
内核在初始化时,给每一个物理页创建一个struct page结构体来描述每一个物理页的信息,所占用的内存为:
sizeof(struct page) * 1G/4K

7.用户空间,内核空间和物理内存的映射关系:
用户空间3G和物理内存的映射是一个动态的过程,也就是说用户需要访问某个物理内存,就动态创建物理内存和用户虚拟地址的映射(建立页表),如果不再使用,接触映射关系(销毁页表),这个访问方式效率不高!
用户空间能够访问的物理内存最大也只有3G;

内核空间1G和物理内存的映射是在内核初始化时就已经建立一一映射的关系,一旦建立好,后续访问无需动态建立页表,加快地址的访问速度。
物理和虚拟地址的一一映射:
物理地址        虚拟地址
0x0        0xC0000000
0x1        0xC0000001
0X2        0xC0000002
...        ...
问题:内核虚拟地址为1G,如果物理内存大于1G,如果让内核访问其他的物理内存呢?
答:linux内核将1G的虚拟地址划分为若干个区域来实现访问所有的物理内存:
X86:将1G的内核虚拟地址分别划分为4个区域:
直接内存映射区:
映射关系:在内核初始化(不是动态映射),如果物理内存大于1G,内核将内核的1G虚拟内存的前896M虚拟内存和物理内存进行一一映射;1G的虚拟内存还剩余128M;
起始地址:0xC0000000
大小:如果物理内存大于1G,那么直接内存映射区的大小就为896M,如果物理内存的大小就是直接内存映射区的大小
别名:低端内存

动态内存映射区
映射关系:如果需要访问某一个物理地址(寄存器地址,内存地址),只需动态的建立物理地址和动态内存映射区中虚拟地址的映射关系(页表)即可,每当程序访问这个内核的动态内存映射区的虚拟地址其实最终也是在访问对应的物理地址(MMU);
如果不在使用对应物理地址,一定要解除地址映射!
起始地址:随着物理内存的大小变化而变化,如果物理内存小于896M,比如512M,起始地址=0xC0000000 + 物理内存的大小,如果物理内存大于896M,起始地址0xC000000+896M
默认的大小为120M

永久内存映射区:
映射关系:它也是实现物理地址和内核虚拟地址的一个动态映射,只是如果在某些时刻,如果访问这个物理地址的频率很大,如果频繁的建立页表和销毁页表,那么访问效率不高,于是可以将这个物理地址映射到永久内存映射区,一经映射,无需再销毁对应的映射关系,加快地址的访问速度。当然可以人为的销毁这个映射关系。使用kmap函数进行映射,这个映射有可能会休眠,所以不能在中断上下文中使用。
大小:4M

固定内存映射区:
和永久内存映射区的目的是一样的,区别仅仅是固定内存映射区的虚拟地址在映射的时候,可以用在中断上下文中。
大小:4M

注意:直接内存映射区又叫低端内存;
动态内存映射区+永久内存映射区+固定内存映射区=高端内存;

S5PV210处理器1G内核虚拟地址的划分:
异常向量表:    
vector  : 0xffff0000 - 0xffff1000   (   4 kB)
固定内存映射区:    
fixmap  : 0xfff00000 - 0xfffe0000   ( 896 kB)
DMA内存映射区:
    DMA     : 0xff000000 - 0xffe00000   (  14 MB)
动态内存映射区:   
 vmalloc : 0xf4800000 - 0xfc000000   ( 120 MB)
直接内存映射区:    
lowmem  : 0xc0000000 - 0xf4000000   ( 832 MB)

注意:ARM架构本身的异常向量表的入口地址有两个:
0地址还有0xFFFF0000

**********************************************************
linux内核分配内存的方法:
1.kmalloc/kfree
void *kmalloc(size_t size, gfp_t flags);
函数功能:从低端内存(直接内存映射区)分配,物理上连续,虚拟上也连续,一次性最小分配32字节,新版本内核(2.6.33以后)最大只能分配4M,之前内核最大只能是128K。

参数:
size:大小,最小是32字节,最大是4M/128K
flags:分配内存的行为
GFP_KERNEL:如果在分配内存时,指定了这个宏,就等于告诉内核请努力的帮我分配好内存,如果内存不足,会导致休眠,所以这个宏不能用于中断上下文中。
GFP_ATOMIC:如果在分配内存时,指定了这个宏,就等于告诉内核,如果内存不足,不做休眠,而是理解返回一个-ENOMEM,可以用在中断上下文中;
返回值:就是分配的内核的虚拟内存的起始地址

void kfree(void *addr); //释放内存

2.__get_free_pages()/free_pages;
unsigned long __get_free_pages(gfp_t  gfp_mask,             unsigned int order);
函数的功能:这个函数由kmalloc实现,它也是从低端内存分配,物理,虚拟上都连续,最小为1页,最大是4M
参数:
gfp_mask:分配内存的行为GFP_KERNEL,GFP_ATOMIC
order:如果order=0,分配1页,order=1,分配2页,order=2,分配4页...
返回值:就是分配的内核的虚拟内存的起始地址

//释放内存
void free_pages(unsigned long addr,                         unsigned int order);

3.vmalloc/vfree:
void *vmalloc(unsigned long size);
函数功能:分配内存从动态内存映射区分配,虚拟上连续的,但物理地址不一定连续!理论上最大能分配120M
size:大小
默认的分配行为是阻塞方式(会休眠)
返回值:分配的内核虚拟内存的起始地址

释放内存
vfree(void *addr); 

4.内存分配的其他方法:
4.1.在驱动中定义一个全局数组,例如
   static char buf[5*1024*1024]; //BSS段,不影响文件的大小
   static char buf[5*1024*1024] = {0xaa, 0x55, 0xaa,                           0x55};//数据段,会影响ko文件的大小,最终影响ko文件的加载速度!

4.2.通过设置内核的启动参数调整将动态内存映射区的大小,例如:
setenv bootargs root=/dev/nfs ... vmalloc=256M //将动态内存映射区的大小由120M调整到256M

4.3.通过设置内核的启动参数,添加mem=8M来将物理内存高地址的8M预留出来,然后驱动通过ioremap函数将这个物理内存映射到直接内存映射区中再使用。

**********************************************************
问题:如何在设备驱动中,访问设备的物理地址呢?
例如设备驱动访问GPIO对应的寄存器来实现对灯的操作!

答:
千万明确一点:不管是在内核空间还是在用户空间,软件一律不能去直接访问物理地址,只能访问用户虚拟地址或者内核虚拟地址,如果驱动要访问物理地址,思路就是将物理地址映射到内核的虚拟地址上即可,一旦映射成功,以后访问这个内核虚拟地址就是在访问物理地址!
这里使用大名鼎鼎:ioremap函数;

函数原型:
void *ioremap(unsiged long phy_addr, int size);
函数功能:
给定一个物理地址,将这个物理地址映射到内核的虚拟地址上,映射到内核的动态内存映射区的虚拟地址上。这个过程是动态映射的过程(动态建立页表)
参数说明:
phy_addr:要访问的物理地址
size:映射的内存区域的大小
返回值:就是映射的内核虚拟地址,一旦有这个映射好的内核虚拟地址,以后用户访问这个内核虚拟地址就是在访问对应的物理地址;
例如:物理起始地址为0x10000000,地址空间为12字节;
void *addr = ioremap(0x10000000, 12);
注意:映射完毕以后,物理上连续,虚拟上也连续!
映射的关系:
物理地址        虚拟地址
0x10000000    addr
0x10000004    addr+4
0x10000008    addr+8
...        ...
注意:一个物理地址可以有多个虚拟地址;

一旦不在使用物理地址,一定要解除地址映射:
iounmap(void *addr); //addr就是映射的内核虚拟地址

案例:利用ioremap实现操作LED对应的寄存器来操作LED

案例:分析4.0代码的作用和功能!

1.mmap:
作用:就是将设备的物理地址映射到用户的虚拟内存空间上;
read,write,ioctl三个系统调用函数,如果涉及数据的访问,必然要经过两次的数据拷贝:用户空间,内核空间,硬件。
mmap的使用将2次的数据操作变成1次,大大的提高了设备数据的访问效率!
mmap系统调用过程:
1.应用程序mmap
2.C库的mmap
3.sys_mmap:
内核到当前进程的3G的MMAP内存映射区中找一块空闲的内存区域;一旦找到,内核使用struct vm_area_struct来描述这块空闲的内存区域的信息;
4.调用底层驱动的mmap
5.remap_pfn_range进行将物理地址映射到内核帮你找的虚拟内存进行地址映射!

ioremap 映射到内核空间虚拟地址

mmap 映射道用户空间虚拟地址
 

猜你喜欢

转载自blog.csdn.net/coolperl/article/details/81363399
今日推荐