进程地址空间由进程可寻址的虚拟内存组成,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);
阅读(503) | 评论(0) | 转发(0) |