Chinaunix首页 | 论坛 | 博客
  • 博客访问: 773713
  • 博文数量: 196
  • 博客积分: 115
  • 博客等级: 民兵
  • 技术积分: 354
  • 用 户 组: 普通用户
  • 注册时间: 2010-05-13 23:19
文章分类

全部博文(196)

文章存档

2021年(1)

2019年(5)

2018年(11)

2017年(15)

2016年(13)

2015年(46)

2014年(81)

2013年(22)

2012年(2)

分类: LINUX

2014-05-27 19:08:43


    进程地址空间由进程可寻址的虚拟内存组成,linux采取的虚拟内存技术使得所有进程以虚拟方式共享内存。对于某个进程,它好像可以访问所以物理内存,而且它的地址空间可以远远大于物理内存。进程的虚拟内存区域可以包括各种内存对象:
文本段
:代码段是用来存放可执行文件的操作指令也就是说是它是可执行程序在内存中的镜像,包含一些字符串、常量和只读数据。代码段需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作——它是不可写的。
数据段
:数据段用来存放可执行文件中已初始化全局变量,换句话说就是存放程序静态分配的变量和全局变量。
BSS
BSS段包含了程序中未初始化的全局变量,在内存中 bss段全部置零。实际上为减少二进制程序的大小,linux只是将该段映射到零页上
堆(
heap:堆是用于存放进程运行中被动态分配的内存区域,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
:栈是用户存放程序临时创建的局部变量也就是说我们函数括弧“{}中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
      除了上述内存对象,还有共享库的映射、内存映射文件、共享内存等对象。当可执行文件被装入时,进程并不为所有对象立即分配实际的物理内存,而是尽量的推迟分配。注意:int *p = malloc(4); printf("%d\n",*p);  你会发现结果为0,linux会对没有关联到物理页的虚拟内存的读操作直接返回零页。
       程序内存段和进程地址空间中的内存区域是种模糊对应,也就是说,堆、bss、数据段(初始化过的)都在进程空间种由数据段内存区域表示。内存区域中数据段、BSS和堆通常是被连续存储的——内存位置上是连续的,而代码段和栈往往会被独立存放。

    内核使用内存描述符struct mm_struct(进程描述符struct task_struct的mm域)表示进程的地址空间。fork()创建进程的时候,子进程复制父进程的地址空间。线程组中的线程共享同一地址空间。内核线程没有地址空间,也即没有相关的内存描述符,因此内核线程在用户空间没有相关的映射。一般内核线程也不会访问用户空间,通常只在系统调用内核代表用户进程执行,内核才访问用户空间。内核线程访问内核内存地址时,也是需要页表的,通常直接使用上一个进程的页表(内存描述符的pgd字段指向页全局目录),这样可以省事不少。内核态的进程修改了内核空间的页表项时,理应更新系统所有进程的相应页表项,但是操作会耗费不少时间 ,linux采用了一种延迟方式,上一节的非连续内存分配有所阐述。
    特别说明一下:虚拟页有两种状态,valid和invalid,有效页关联一个实际的数据页,它可能位于RAM,也可能位于硬盘上的交换分区和文件(进程访问时会产生page fault),一个无效页表示没有被分配和使用。

    vm_area_struct结构描述了指定地址空间内连续区间上的一个独立内存范围。内核将每个内存区域作为一个单独的内存对象管理,每个内存区域具有一致的属性,相应的操作也一致。/proc//maps或者pmap pid 的输出会显示pid对应进程的虚拟内存区域。

上面给出了一个进程的地址空间,每一行都是用start-end  perm   offset  major:minor   inode image形式表示的,包含了很多的映射文件。经过验证,程序执行时产生了5次page faults. 内核总是尽量推迟给用户态进程分配动态内存,所以堆栈是不会在开始实际分配物理内存的,当我们使用malloc()是也是如此。
    内核提供了各种函数对虚拟内存区域的操作,如合并、插入、查找、创建和删除。为了优化查找,内核维护了VMA的链表和红黑树结构。
    创建进程fork()、程序载入execve()、映射文件mmap()、动态内存分配malloc()/brk()等进程相关操作都需要分配内存给进程。不过这时进程申请和获得的还不是实际内存,而是虚拟内存,准确的说是“内存区域”。进程对内存区域的分配最终都会归结到do_mmap()函数上来(brk调用被单独以系统调用实现,不用do_mmap())
   当进程需要内存时,从内核获得的仅仅是虚拟的内存区域,而不是实际的物理地址,进程并没有获得物理内存,获得的仅仅是对一个新的线性地址区间的使用权。实际的物理内存只有当进程真的去访问新获取的虚拟地址时,才会由“请页机制”产生“缺页”异常,从而进入分配实际叶框的程序。该异常是虚拟内存机制赖以存在的基本保证---它会告诉内核去为进程分配物理页,并建立对应的页表,这之后虚拟地址才实实在在的映射到了物理地址上。如果物理页被换出到磁盘,当访问虚拟地址时,也会产生缺页异常,不过这时不用再建立页表了

1.创建VMA
    do_map为当前进程创建并初始化一个新的VMA,当分配之后,可以将该VMA与相邻的具有相同的访问权限的VMA进行合并:
     unsigned long do_mmap(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,unsigned long flag, unsigned long offset)
    该函数映射由file指定的文件,具体映射的是从文件中偏移offset处开始,长度为len范围内的数据,如果file为NULL且offset为0,那么代表这次映射没有和文件相关,称为匿名映射,否则称为文件映射。用户空间通过mmap调用获取do_mmap的功能。

2.文件映射
    一个新建立的VMA就是不包含任何页的线性区,当进程引用其中的一个地址时,缺页异常发生,缺处理程序检查struct vm_operations_struct 中的fault函数是否被定义,如果其为NULL,说明VMA没有映射文件,为匿名映射,内核为其映射到该地址;如果不为NULL,则进行文件映射, paging in,返回page结构的指针。

3.建立页表
    int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot);\
    该函数为设置了PG_reserved的物理页和RAM之外的物理映射地址建立页表,它能够重新映射高端PCI缓冲区。



用户空间的内存管理
1.动态内存分配
    void *malloc(size_t size); 分配固定字节大小的内存
    vodi *calloc(size_t nr, size_t size); 为nr个大小为size字节的元素分配内存,内存每一位都清0
    void realloc(void *ptr, size_t size); 改变已分配内存的大小,返回一个新空间的指针

2.匿名内存映射
    int brk(void *end); 把堆的末端的地址设置为end指定的值
    对于较大的内存分配,glibc并不使用堆,而是创建一个匿名内存映射(已用0初始化的大的内存块),由于不基于堆,因此不会造成碎片。每个内存映射都是页大小的整数倍,大小可调整。
   
void *mmap(void *start,  length, int prot, int flags, int fd, off_t offset);
    例子:
       void *p;
        p = mmap(NULL, 512 * 1024, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1 ,0);

    

阅读(526) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~