Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2045
  • 博文数量: 2
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 30
  • 用 户 组: 普通用户
  • 注册时间: 2015-06-15 08:45
文章分类
文章存档

2015年(2)

我的朋友

分类: 嵌入式

2015-06-27 11:49:22

最近在学习linux内存管理的知识,之前只是有个模糊的了解,现在重新查阅了些资料,系个人理解,总结如下。

1.几个资料中经常提到,又容易混淆的地址概念。
逻辑地址:由段基址(segment)和偏移量(offset)组成。偏移量指明了从段基址到实际地址间的距离。逻辑地址只有x86支持,一般32系统中逻辑地址共48位,段选择符16位,偏移量32位。linux中简化了对段的支持,即段基址为0。
线性地址:我们所说的地址空间,就是指的线性地址空间,32位系统中是4G的范围,x86中0~3G(0xC000 0000)为用户空间,3G-4G为内核空间。linux系统中程序使用的或者%p打印出来的地址其实就是线性地址。
虚拟地址:linux中,上述的线性地址就是一般意义上的虚拟地址,但是这样说也不太准确,一般线性地址是与物理地址一一映射的,而内核空间中的高端内存是没有跟物理内存一一映射的关系的,如vmalloc分配的地址,也称为虚拟地址,但不是线性地址。通常我们说的虚拟地址其实就是指的线性地址。
物理地址:处理器访问内存或外设所使用的地址。

2.几种地址的转换关系。


3.程序中几个重要的区域。
一般程序会分为代码段、数据段、bss段、堆、栈五个区域。
代码段:存放程序的静态代码,只读。
数据段:存放静态变量、已初始化的全局变量。
bss段:存放未初始化的全局变量,且全部置0。
堆(heap):程序运行时动态内存分配的区域。
栈(stack):存放局部变量、函数的部分参数等。
在内存中的关系如下图:
                                         
其中数据段、bss、堆在内存是连续的,栈从用户空间的顶部往下生长。

下面这个例子是程序各段运行是打印的地址:

(原形取自《User-Level Memory Management》)

#include<stdio.h>

#include<malloc.h>

#include<unistd.h>

int bss_var;

int data_var0=1;

int main(int argc,char **argv)

{

  printf("below are addresses of types of process's mem/n");

  printf("Text location:/n");

  printf("/tAddress of main(Code Segment):%p/n",main);

  printf("____________________________/n");

  int stack_var0=2;

  printf("Stack Location:/n");

  printf("/tInitial end of stack:%p/n",&stack_var0);

  int stack_var1=3;

  printf("/tnew end of stack:%p/n",&stack_var1);

  printf("____________________________/n");

  printf("Data Location:/n");

  printf("/tAddress of data_var(Data Segment):%p/n",&data_var0);

  static int data_var1=4;

  printf("/tNew end of data_var(Data Segment):%p/n",&data_var1);

  printf("____________________________/n");

  printf("BSS Location:/n");

  printf("/tAddress of bss_var:%p/n",&bss_var);

  printf("____________________________/n");

  char *b = sbrk((ptrdiff_t)0);

  printf("Heap Location:/n");

  printf("/tInitial end of heap:%p/n",b);

  brk(b+4);

  b=sbrk((ptrdiff_t)0);

  printf("/tNew end of heap:%p/n",b);

return 0;

 }

它的结果如下

below are addresses of types of process's mem

Text location:

   Address of main(Code Segment):0x8048388

____________________________

Stack Location:

   Initial end of stack:0xbffffab4

   new end of stack:0xbffffab0

____________________________

Data Location:

   Address of data_var(Data Segment):0x8049758

   New end of data_var(Data Segment):0x804975c

____________________________

BSS Location:

   Address of bss_var:0x8049864

____________________________

Heap Location:

   Initial end of heap:0x8049868

   New end of heap:0x804986c



4.编程的角度分析内存管理的应用。
1)首先明确几个概念
linux虚拟内存区域:linux将虚拟存储器组织成一些区域的集合,也叫段,一个区域就是已经存在的(已经分配的)虚拟存储器的连续片,如代码段、数据段、bss段等,不属于某个区域的虚拟页是不存在的,即程序中引用的虚拟地址必然包含在某一个段中。内核为每个进程分配一个任务结构tast_struct,其中的mm域指向mm_struct,描述了虚拟存储器当前状态,mm_struct中的pgd域指向全局页表、mmap域指向vm_area_struct结构的链表,内核中每个区域结构由vm_area_struct描述,每个进程的所有vm_area_struct分别用链表和红黑树两种数据结构组织便于查找。
内部碎片:为了一小段内存的需要不得不分配一大块连续区域,未用的区域为内部碎片。用slab方式解决。
外部碎片:虽然系统有足够多的空闲内存总量,但都不连续无法满足大块连续内存的需要。伙伴系统解决,但只能尽量减小外部碎片的产生,不能彻底消除。

2)用户态:用户对内存的管理主要涉及文件的重映射,接口是mmap(),用来创建新的虚拟内存区域,并将对象映射到这些区域中,而这里的对象如磁盘上的文件等。原型如下:


#include<unistd.h>
#include<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);

start参数仅仅是一个暗示,通常定义为NULL,munmap()中的start为mmap返回的起始地址;
prot参数包含新映射区域的访问权限位:
    PROT_EXEC
    PROT_READ
    PROT_WRITE
    PROT_NONE:这个区域的页面不能被访问
flags参数由描述被映射对象类型的位组成:
    MAP_ANON:被映射的是匿名对象,且相应的虚拟页面是请求二进制0的
    MAP_PRIVATE:被映射的对象是私有的,写时拷贝的
    MAP_SHARED:共享对象


    注:如果要通过映射并修改一个文件的内容,需要在映射时设置为共享对象,否则修改的内容不会被保存到原文件,而且mmap必须以PAGE_SIZE为单位进行映射,不能映射串口及其他面向流的设备。
例:
    bufp = mmap(-1, size, PROT_READ, MAP_PRIVATE|MAP_ANON, 0, 0);
使用mmap映射到用户空间的好处有,如在系统中负责和显存间读写大量数据,比用lseek、write极大得提高了吞吐量。再如控制PCI设备的程序,大多数PCI外围设备将控制寄存器映射到内存地址中,高性能的应用程序更愿意直接访问寄存器,而不是不停得调用ioctl去获得需要的信息。

fork()函数的工作原理
当fork函数被当前进程调用时,内核为新进程创建各种数据结构,并分配PID,为了给新进程创建虚拟存储器,它创建了当前进程的mm_struct、区域结构和页表的原样拷贝,它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都设置为写时拷贝。以后任何一个进程进行写操作时,写时拷贝机制就会创建新页面。

execve()函数的工作原理
如执行execve("a.out", NULL, NULL),加载并执行a.out需要经过以下步骤:
  • 删除已存在的用户区域:删除当前进程虚拟地址空间的用户部分中的已存在的区域结构。
  • 映射私有区域:为新程序的文本、数据、bss、栈创建新的内存区域结构,并分别映射a.out的相关部分,所有这些区域都是私有的、写时拷贝的。
  • 映射共享区域:如果程序与共享库链接,如libc.so,则映射到用户虚拟地址空间中的共享区域内。
  • 设置程序计数器PC:execve做的最后一件事是设置pc指针,是之指向新文本区域的入口。
用户态动态内存分配
接口是malloc(),malloc可以以字节为单位申请,但是内核中仍是以页为单位进行分配,malloc中调用了brk()系统调用。

3)内核态
进程中的每个虚拟内存区域由一个mm_area_struct描述,以链表和红黑树两种形式组织。
内核分配空闲页框接口是get_free_page(s)(),整页分配基于buddy实现,内核struct page存在mem_map[]中。
kmalloc是slab提供的接口,用于小内存的分配(<128KB)。
vmalloc()函数的说明:
  • 分配的每个块都对应一个vm_struct结构,与进程虚拟内存区域(VMA)结构vm_area_struct不同,vm_area_struct在内核中管理并与进程的每个内存区域对应。
  • 分配时物理页可以不连续,返回的虚拟空间地址连续,因此需要更新页表,而kmalloc()与get_free_page()不需要,且缺页时才分配物理页。
  • 分配的地址不能在处理器之外使用,因为需要处理器中的mmu来翻译,如dma中用的地址就不能用vmalloc分配,一般用于装载模块时分配,且不能用在原子上下文中。
ioremap()函数的说明:
  • 用于映射PCI缓冲区到内核空间(专用于I/O内存区域分配虚拟地址),但并不实际分配内存,也会建立新页表,注意不能把其返回的地址直接当指针直接访问,如直接赋值,要用read8()等接口访问。
4)内存操作常见错误
  • 段错误:引用的地址不包含在任一个vm_area_struct的start和end区间内,如scanf("%d", val) //&val、用mmap映射的虚拟内存区已经被munmap释放,再去引用其中地址。
  • malloc申请的内存未初始化就使用。
  • 声明的指针未指向一块内存就使用。
  • 指针运算加减是以指向的数据类型长度为单位,而不是以字节为单位。
  • 数据过界:数组栈溢出、混淆指针长度与指向的数据类型长度、循环时边界条件控制不准。
  • 引用指针而不是指向的对象。如*val++;//(*val)++, ++的优先级高。
  • 引用返回的栈指针,函数返回后栈已无效。
  • 引用野指针或引用已释放数据块中的数据。
  • 内存泄漏。
5)内核kmalloc、__get_free_page、zone_sizes_init调用关系
阅读(209) | 评论(0) | 转发(0) |
0

上一篇:没有了

下一篇:处理器访问外设的方式

给主人留下些什么吧!~~