Linux 内存管理

1、内存空间

1.1 地址映射

在驱动开发过程中,一般来说外设的IO内存资源的物理地址是已知的,由硬件的设计决定。但是CPU不会为这些已知的外设IO内存资源预先指定虚拟地址的值,所以驱动程序不可以直接就通过外设的物理地址访问到IO内存,而必须要将其映射到虚拟地址空间(通过页表),然后才能根据内核映射过后的虚拟地址来通过内存指令访问这些IO内存,并对其进行操作。

在Linux内核的io.h头文件中声明了ioremap()函数,用来将IO内存资源映射到核心虚拟地址空间(3Gb~4GB)中,当然不用了可以将其取消映射iounmap()。

在支持MMU的32位处理器平台上,Linux系统中的物理存储空间和虚拟存储空间的地址范围分别都是从0x000000000xFFFFFFFF,共4GB,但物理存储空间与虚拟存储空间布局完全不同。Linux运行在虚拟存储空间,并负责把系统中实际存在的远小于4GB的物理内存根据不同需求映射到整个4GB的虚拟存储空间中。

在虚拟地址空间中,linux又将这4G分为用户空间(0-0xbfffffff)和内核空间(0xc0000000-0xffffffff),为了让用户空间没机会直接接触物理地址,linux的物理地址映射都是在内核空间完成的。

1.1.1 什么时候需要进行地址映射

在跑Linux系统中,存在 MMU(Memory Management Unit),即内存管理单元,它是一种负责处理中央处理器(CPU)的内存访问请求的计算机硬件。它的功能包括虚拟地址到物理地址的转换(即虚拟内存管理)、内存保护、中央处理器高速缓存的控制。

故在内核运行起来阶段的一些操作,都是需要将物理地址映射为虚拟地址的。比如高通平台,在kernel中的驱动,都需要映射;在sbl 和lk 阶段,进行设备初始化等操作,因为此时kernel 还未运行起来,故需要直接对寄存器进行操作。

(1)内核驱动进行地址映射:
在这里插入图片描述
(2)uboot直接操作寄存器:
在这里插入图片描述

#define USB_OTG_HS_ULPI_VIEWPORT 0x078D9170

在这里插入图片描述
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210311092537783.png

1.1.2 对同一物理地址多次映射

1、是否可以对一个物理地址进行多次ioremap —> 应该可以
2、ioremap返回的地址是否可以直接对物理寄存器进行操作 —> 不一定,要看寄存器的特点
3、多个虚拟地址是否都可以正常操作一个物理地址。 —> 理论上可以

#define ULCON1     ((volatile unsigned *)0x50004000)
 
unsigned long* vaddr1;
unsigned long* vaddr2; 
u32 tmp;
    vaddr1 = ioremap(ULCON1,4);
    printk("vaddr1:%x:%x\n",vaddr1,*vaddr1);
    *vaddr1=0xff;
    udelay(100);
    printk("*vaddr1:%x\n",*vaddr1);
    printk("ioread32:%x\n",ioread32(vaddr1));
    vaddr2 = ioremap(ULCON1,4);
    printk("vaddr2:%x:%x\n",vaddr2,*vaddr2);
    tmp = ioread32(vaddr2);
    printk("tmp:%x\n",tmp); 

(1)功能:对同一个寄存器两次ioremap,得到两个虚拟地址vaddr1和vaddr2,他们都对应一个真实的物理地址ULCON1,在第一个vaddr1处写入一个数6,然后再从vaddr2处读出来。
(2)运行结果:

vaddr1:c4812000:0
*vaddr1:0
ioread32:0
vaddr2:c4814000:0
tmp:0

(3)结果说明:vaddr1根本没有改变寄存器ULCON1的值(*vaddr1:0)。
(4)分析:在嵌入式CPU上,有些寄存器可读不可写,有些可写不可读,有些读出的值与写入的值是不同的,有的寄存器是自动置位的。。

1.2 用户空间与内核空间

我们知道现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。
  操心系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核,保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC00000000xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x000000000xBFFFFFFF),供各个进程使用,称为用户空间。
  每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。
  有了用户空间和内核空间,整个linux内部结构可以分为三部分,从最底层到最上层依次是:硬件–>内核空间–>用户空间。
  
  需要注意的细节问题:
(1) 内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中
(2) Linux使用两级保护机制:0级供内核使用,3级供用户程序使用。

内核态与用户态:
(1)当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。

(2)当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。

参考资料:

http://blog.csdn.net/f22jay/article/details/7925531

http://blog.csdn.net/zhangskd/article/details/6956638

http://blog.chinaunix.net/uid-26838492-id-3162146.html

1.3 进程上下文与中断上下文

我在看《linux内核设计与实现》这本书的第三章进程管理时候,看到进程上下文。书中说当一个程序执行了系统调用或者触发某个异常(软中断),此时就会陷入内核空间,内核此时代表进程执行,并处于进程上下文中。看后还是没有弄清楚,什么是进程上下文,如何上google上面狂搜一把,总结如下:

程序在执行过程中通常有用户态和内核态两种状态,CPU对处于内核态根据上下文环境进一步细分,因此有了下面三种状态:

(1)内核态,运行于进程上下文,内核代表进程运行于内核空间。
(2)内核态,运行于中断上下文,内核代表硬件运行于内核空间。
(3)用户态,运行于用户空间。

上下文context: 上下文简单说来就是一个环境。

用户空间的应用程序,通过系统调用,进入内核空间。这个时候用户空间的进程要传递很多变量、参数的值给内核,内核态运行的时候也要保存用户进程的一些寄存器值、变量等。所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。

相对于进程而言,就是进程执行时的环境。具体来说就是各个变量和数据,包括所有的寄存器变量、进程打开的文件、内存信息等。一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。

(1)用户级上下文: 正文、数据、用户堆栈以及共享存储区;
(2)寄存器上下文: 通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP);
(3)系统级上下文: 进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈。

当发生进程调度时,进行进程切换就是上下文切换(context switch).操作系统必须对上面提到的全部信息进行切换,新调度的进程才能运行。而系统调用进行的模式切换(mode switch)。模式切换与进程切换比较起来,容易很多,而且节省时间,因为模式切换最主要的任务只是切换进程寄存器上下文的切换。

硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的 一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。所谓的“ 中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。中断时,内核不代表任何进程运行,它一般只访问系统空间,而不会访问进程空间,内核在中断上下文中执行时一般不会阻塞。

2、数据存储格式

2.1 int 0x12345678存储为什么是0x78 0x56 0x34 0x12

0x12345678 这个是16进制的表示方法
转换成2进制:0001 0010 0011 0100 0101 0110 0111 1000
内存的存储的最小单元是一个字节,对于多字节存储的方式一般采用低位优先。
一字节=8位,0001 0010 0011 0100 0101 0110 0111 1000就是4字节,32位。
低字节优先,所以先存储0x78,即0111 1000
依此,就是0x78 0x56 0x34 0x12

2.2 大小端模式

大端存储模式:数据的低位保存在高地址中。
小端存储模式:数据的低位存储在低地址中。
例如:0x1122 0x11为高字节 0x22为低字节
大端模式:存储为0x2211
小端模式:存储为0x1122

很多是ARM、DSP都为小端模式

2.3 int指针强转为char

例:

int a=12;
int *p=&a; //假设 &a为0x1234
char *pc=(char *)(&a);

由于int占4字节,故int类型指针p的管辖范围为从p起始的4个字节范围,同理char类型指针pc的管辖范围为从pc起始的1字节范围。

执行pc = (char*)(&a)后,pc也指向0x1234地址,只是pc的为char 型的,所以它只能管辖1个字节的范围,故*pc实际上是把p管辖的4个字节下的第一个字节取出来。

用来判断编译器的大小端模式:

int a=12=0x0C  二进制:00000000  00000000 00000000 000001100

若为小端模式,低位存储在低字节,存储为00000000 00000000 00000000 000001100
*pc取1个字节即000001100,值为12
若为大端模式,低位存储在高字节,存储为000001100 00000000 00000000 00000000
*pc取1个字节即000000000,值为0

3、内存分配

在计算机的系统中有四个内存区域:
1):在栈里面储存一些我们定义的局部变量以及形参(形式参数)

2)字符常量区:主要是储存一些字符常量,比如:char *p_str=”cgat”; 其中”cgat”就储存在字符常量区里面;

3)全局区:在全局区里储存一些全局变量和静态变量

4):堆主要是通过动态分配的储存空间,也就是我们接下需要讲的动态分配内存空间。

3.1 什么时候我们需要动态分配内存空间

举一个例子吧。

int *p; 

我们定义了一个指向int类型的指针p;p是用来储存一个地址的值的,我们之所以要为 p 这个变量分配空间是让它有一个明确的指向。

虽然我们在计算机的内存里定义了一个指针变量,但是我们并没有让这个变量指示一个确切int类型变量的地址。这样我们就要通过动态分配内存的方式来认为的规定它的方向!

int *p;
p=&a;

这种方法不是指针的动态分配内存,这个叫做指针变量的初始化!初始化同样也可以让指针变量有方向可指。

int *p;
p=malloc(n*sizeof(类型名称))

我们通过malloc()函数为一个指针变量p分配了地址,这样我们从键盘上键入的值就这样存储在p里面了,接下来我们就可以对这个p进行具体的操作了,比如scanf(“%s”,p)等等。

例子:

void foo()
{
  int a,*pa,*pb;
  pa=&a;
  pb=malloc(sizeof(int));
  free(pb);
}

(1)指针pa指向的是栈上的空间,函数退出时会自动释放;
(2)指针pb指向的是malloc分配的堆上的内存,只有用free手动释放,
(3)退出函数foo时,pa,pb指向的内存都会被释放,pb是调用free释放的,pa指向的是a,a是自动释放的。

3.2 内存泄漏

当我们对p结束操作的时候还要释放p的内存空间。为什么要释放内存空间呢?

在上面我已经讲过动态分配的变量时储存在堆里面,但是这个堆的空间并不是无限大的,也许当我们编一个小的程序的时候可能我们并不能够发现什么,但是对于那些大的程序,如果我们比及时释放堆的空间的时候会放生内存泄露。

所谓内存泄露是因为堆的空间北我们动态分配用完了,这样当我们再去使用动态分配堆的空间的时候就没有足够的空间让我们使用了,这样就需要占有原来的空间,也就是会把其他的空间来储存我们键入的值,这样会导致原来储存的数据被破坏掉,导致了内存的泄露了。

3.3 空指针与野指针

同时当我们使用malloc()函数的时候还应该注意:当我们释放完空间的时候还要将原先的指针变量赋予一个NULL,也就是赋予一个空指针,留着下次的时候使用它!

如果我们不赋予NULL行不行呢??
答案是:不行的!如果我们不赋予一个空指针这样会导致原先的指针变量成为了一个野指针!
何谓野指针?野指针就是一个没有明确指向的指针,系统不知道它会指向什么地方,野指针是很危险的,因此当我们每次使用完malloc()函数的时候都必须将指针赋予一个空指针!

相对于malloc()函数,calloc()函数就不需要我们赋予NULL了,这是因为在每次调用完calloc()函数的时候系统会自动将原先的指针赋予一个空指针,即归于“0”。

除了malloc()与calloc(),还有一个动态分配空间的函数realloc()函数,这个函数比前两个函数分配更多的空间,原型:void *realloc(void *p,size_t size);

4、IO空间和内存空间

4.1 IO空间和内存空间

在X86处器中存在I/O空间的概念,I/O空间是相对内存空间而言的,他们是彼此独立的地址空间,在32位的x86系统中,I/O空间大小为64K,内存空间大小为4G。

IO空间X86支持,但是ARM并不支持IO空间。

4.2 IO端口与IO内存

当一个寄存器或内存位于IO空间时,称为IO端口

当一个寄存器或内存位于内存空间时,称为IO内存

4.3 IO端口的操作

①申请IO端口

struct resource*request_region(unsignedlong first, unsignedlong n, const char *name)

功能:这个函数告诉内核,你要使用从 first 开始的n个端口,name参数是设备的名字。如果申请成功,返回非 NULL,申请失败,返回 NULL。

注意:系统的IO端口可能已经被占用,所以分配的时候需要查看哪些端口被占用。

②访问IO端口

unsigned inb(unsigned port) //读字节端口( 8 位宽 )

void outb(unsignedchar byte, unsigned port)  //写字节端口( 8 位宽 )

unsigned inw (unsigned port)
void outw(unsignedshort word, unsigned port)
//存取 16-位 端口。
unsigned inl(unsigned port)
void outl(unsigned longword, unsigned port)
//存取 32-位 端口。

③释放IO端口

void release_region(unsignedlong start, unsignedlong n)

4.4 IO内存的操作

①申请IO内存

struct resource*request_mem_region(unsigned long start, unsignedlonglen, char *name)

功能:这个函数申请一个从start 开始,长度为len 字节的内存区。如果成功,返回非NULL;否则返回NULL,所有已经在使用的I/O内存在/proc/iomem 中列出。

②映射IO内存

原因:在访问IO内存之前必须进行物理地址到虚拟地址的映射。

void*ioremap(unsigned long phys_addr, unsignedlong size)

③访问IO内存

读函数:

unsigned ioread8(void *addr)
unsigned ioread16(void *addr)
unsigned ioread32(void *addr)

写函数:

void iowrite8(u8 value, void *addr)
void iowrite16(u16 value, void *addr)
void iowrite32(u32 value, void *addr)

④释放IO内存

void iounmap(void * addr)
void release_mem_region(unsigned long start, unsignedlonglen)

5、DMA 与 MMU

5.1 DMA(Direct Memory Access,直接内存存取)

一般来说,计算机对内存数据进行处理的时候,需要从内存把数据读进寄存器,然后进行进一步的操作(比如运算处理)。

但是有些数据并不需要运算处理这一类型的操作,只是单纯的移动数据,而把数据读进寄存器,然后再把数据从寄存器写进内存会消耗cpu资源,当需要读写大量数据的时候更是如此,DMA技术就很好地解决了这一问题。

DMA,顾名思义,不占用 cpu 资源,从一个硬件存储区域把一部分连续的数据复制到另一个硬件存储区域。其中硬件包括系统总线上的硬件(内存),和外部总线上的硬件(磁盘,iis外设等)。

5.1.1 占用资源

DMA的原子传输会占用系统总线资源。

DMA虽然不会占用 cpu 资源,但是如果DMA的源跟目的都为内存的时候,由于内存位于系统总线上,DMA会占用总线资源,此时由于系统总线忙,cpu 会由于得不到总线资源而无法进行跟外部的交流操作。

如果DMA的源或目的为内存的时候,我们需要为其分配一块在物理上连续的内存。如果用 kmalloc 等函数分配的内存,虽然在虚拟地址看起来是连续的,但是物理上可能不连续,因此需要用到分配物理上连续的内存的函数(dma_alloc_writecombine)。

为什么要分配连续的内存空间呢,因为平时cpu使用的时候,寻址会经过mmu(内存管理单元)进行虚拟地址到物理地址的转换,而DMA是不经过mmu 直接对内存进行读写的,直接对物理地址进行操作。

5.1.2 使用DMA

当我们要使用到DMA的时候,需要把传输的源(物理地址)、目的(物理地址)、大小告诉DMA,然后启动DMA,就能进行数据移动了。

那么什么时候DMA会结束呢,当DMA传输结束的时候会发出一个中断,我们可以在该中断处理程序内部做进一步的操作(如打印消息等)。

5.1.3 函数使用

1、函数申明

/**
 * dma_alloc_coherent - allocate consistent memory for DMA
 * @dev: valid struct device pointer, or NULL for ISA and EISA-like devices
 * @size: required memory size
 * @handle: bus-specific DMA address
 *
 * Allocate some uncached, unbuffered memory for a device for
 * performing DMA.  This function allocates pages, and will
 * return the CPU-viewed address, and sets @handle to be the
 * device-viewed address.
 */
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag);

该函数实际获得两个地址,
1、函数的返回值是一个 void *,代表缓冲区的内核虚拟地址
2、相关的总线地址(物理地址),保存在dma_handle 中

2、调用

A = dma_alloc_coherent(B,C,D,GFP_KERNEL);
含义:
A: 内存的虚拟起始地址,在内核要用此地址来操作所分配的内存
B: struct device指针,可以平台初始化里指定,主要是dma_mask之类,可参考framebuffer
C: 实际分配大小,传入dma_map_size 即可
D: 返回的内存物理地址,dma 就可以用。

所以,A 和 D 是一一对应的,只不过,A 是虚拟地址,而 D 是物理地址。对
任意一个操作都将改变缓冲区内容。当然要注意操作环境。

注 size 最好以页为单位分配。

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值