一. 动态内存
RAM 的某些部分永久地分配给内核, 并用来存放内核代码以及静态内核数据结构. RAM 的其余部分称为动态内存 (dynamic memory). 动态内存不仅是进程所需的宝贵资源, 也是内核本身所需的宝贵资源. 实际上,整个系统的性能取决于如何有效地管理动态内存. 因此, 现在所有多任务操作系统都在尽力优化对动态内存的使用, 也就是说, 尽可能做到当需要时分配, 不需要时释放.
当给内核分配动态内存时, 是相对容易的, 有如下两点原因:
-
内核是操作系统中优先级最高的成分. 如果某个内核函数请求动态内存, 那么, 必定有正当的理由发出这个请求, 因此, 没有道理试图推迟这个请求.
-
内核信任自己. 所有的内核函数都被假定是没有错误的, 因此内核函数不必针对程序错误施加任何保护措施.
而当给用户态进程分配内存时, 情况完全不同:
-
进程对动态内存的请求被认为是不紧急的. 例如, 当进程对应在磁盘上所存储的可执行文件被装入内存时, 进程并不一定会立即对所有的代码和数据进行访问. 类似地, 当进程调用
malloc()
以请求获得额外的动态内存时, 也并不意味着进程很快就会访问所获得的额外的动态内存. 因此, 一般来说, 内核总是尽量推迟给用户态进程分配动态内存. -
由于用户进程是不可信任的, 因此, 内核必须能随时准备捕获用户态进程引起的所有寻址错误.
为了使得动态内存得到最大限度的使用, 内核使用一种新的资源成功实现了对进程动态内存的推迟分配 . 当用户态进程请求动态内存时, 并没有获得请求的动态内存, 而仅仅得到了对一个新的线性地址区间的使用权, 这样的线性地址区间有很多, 由允许进程使用的全部线性地址区间所组成的集合就叫做进程地址空间.
二. 内存描述符 (memory descriptor)
与进程地址空间有关的全部信息都包含在一个叫做内存描述符的数据结构中 (实际上就是描述进程虚拟内存 的数据结构), 这个结构的类型为 mm_struct
, 进程描述符的 mm
字段就指向这个结构.
c
struct mm_struct *mm;
如下为Linux 2.6.11版的内核中mm_struct的实现.
c
struct mm_struct {
struct vm_area_struct * mmap; /* list of VMAs */
struct rb_root mm_rb;
struct vm_area_struct * mmap_cache; /* last find_vma result */
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
void (*unmap_area) (struct vm_area_struct *area);
unsigned long mmap_base; /* base of mmap area */
unsigned long free_area_cache; /* first hole */
pgd_t * pgd;
atomic_t mm_users; /* How many users with user space? */
atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */
int map_count; /* number of VMAs */
struct rw_semaphore mmap_sem;
spinlock_t page_table_lock; /* Protects page tables, mm->rss, mm->anon_rss */
struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung
* together off init_mm.mmlist, and are protected
* by mmlist_lock
*/
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long rss, anon_rss, total_vm, locked_vm, shared_vm;
unsigned long exec_vm, stack_vm, reserved_vm, def_flags, nr_ptes;
unsigned long saved_auxv[42]; /* for /proc/PID/auxv */
unsigned dumpable:1;
cpumask_t cpu_vm_mask;
/* Architecture-specific MM context */
mm_context_t context;
/* Token based thrashing protection. */
unsigned long swap_token_time;
char recent_pagein;
/* coredumping support */
int core_waiters;
struct completion *core_startup_done, core_done;
/* aio bits */
rwlock_t ioctx_list_lock;
struct kioctx *ioctx_list;
struct kioctx default_kioctx;
unsigned long hiwater_rss; /* High-water RSS usage */
unsigned long hiwater_vm; /* High-water virtual memory usage */
};
其中用来标识相应进程特定线性区的字段如下:
start_code, end_code
- 正文代码的起始地址和终止地址.
start_data, end_data
- 已初始化数据的起始地址和终止地址.
start brk, brk
- 堆的起始地址和当前终止地址.
start_stack
- 用户态堆栈的起始地址.
arg_start, arg_end
- 命令行参数的起始地址和终止地址.
env_start, env_end
- 环境变量的起始地址和终止地址.
三. 进程地址空间布局
如下为进程地址空间的布局, 由一个一个的线性地址区间组成, 线性地址 (linear address), 也称虚拟地址 (virtual address) 是一个 32 位无符号整数 (unsigned long), 可以用来表示数值高达 4GB 的地址, 也就是 4,294,967,296 个内存单元. 线性地址通常用十六进制数字表示, 值的范围从 0x00000000 到 0xffffffff.
0x00000000 ~ 0xbfffffff 这一线性地址区间被称为用户空间, 大小为 3GB; 而0xc0000000 ~ 0xffffffff 这一线性地址区间被称为内核空间, 大小为 1GB.
可以通过以下代码对进程地址空间的布局图进行验证.
c
#include <stdio.h>
#include <stdlib.h>
int uninitialized_global_var;
int initialized_global_var = 100;
int main(int argc, char *argv[], char *envp[])
{
printf("Code address:%p\n", main); // 正文代码
printf("Initialized Data address:%p\n", &initialized_global_var); // 已初始化数据
printf("Uninitialized Data address:%p\n", &uninitialized_global_var); // 未初始化数据
int *p = (int*)malloc(sizeof(int));
printf("Heap address:%p\n", p); //堆区
printf("Stack address:%p\n", &p); // 栈区
for (int i = 0; i < argc; i++) {
printf("Command-line Arguments address:%p\n", argv[i]); // 命令行参数
}
for (int i = 0; envp[i]; i++) {
printf("Environment Variables address:%p\n", envp[i]); // 环境变量
}
return 0;
}
运行结果如下, 与进程地址空间的布局相吻合.
线性地址(虚拟地址)的集合称为虚拟内存, 物理地址的集合称为物理内存, 进程对于内存访问的终点是物理内存而不是虚拟内存, 所以必然存在一种将虚拟内存转化为物理内存的结构, 这种结构被称为页表.
四. 页表
页 (Page) && 页帧 (Page Frame)
内核使用 struct page
作为基本单位来管理物理内存, 在内核看来, 所有的 RAM 都被划分成了固定长度的页帧 (页帧也叫页框, 通常大小为4KB) . 每一个页帧包含了一个页, 也就是说一个页帧的长度和一个页的长度相同. 页和页帧的区别在于, 页是抽象的数据结构, 可以存放在任意地方, 而页帧是真实的存储区域, 属于主存的一部分.
如下为Linux 2.6.11版的内核中struct page的实现.
c
struct page {
page_flags_t flags; /* Atomic flags, some possibly
* updated asynchronously */
atomic_t _count; /* Usage count, see below. */
atomic_t _mapcount; /* Count of ptes mapped in mms,
* to show when page is mapped
* & limit reverse map searches.
*/
unsigned long private; /* Mapping-private opaque data:
* usually used for buffer_heads
* if PagePrivate set; used for
* swp_entry_t if PageSwapCache
* When page is free, this indicates
* order in the buddy system.
*/
struct address_space *mapping; /* If low bit clear, points to
* inode address_space, or NULL.
* If page mapped as anonymous
* memory, low bit is set, and
* it points to anon_vma object:
* see PAGE_MAPPING_ANON below.
*/
pgoff_t index; /* Our offset within mapping. */
struct list_head lru; /* Pageout list, eg. active_list
* protected by zone->lru_lock !
*/
/*
* On machines where all RAM is mapped into kernel address space,
* we can simply calculate the virtual address. On machines with
* highmem some memory is mapped into kernel virtual memory
* dynamically, so we need a place to store that address.
* Note that this field could be 16 bits on x86 ... ;)
*
* Architectures with slow multiplication can define
* WANT_PAGE_VIRTUAL in asm/page.h
*/
#if defined(WANT_PAGE_VIRTUAL)
void *virtual; /* Kernel virtual address (NULL if
not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */
};
CPU 管理物理地址, 因而虚拟地址需要转化为物理地址才能给 CPU 使用. 用于将进程(虚拟)地址空间映射成物理地址空间的数据结构称为页表.
- 进程地址空间, 页表的存在有什么意义?
-
让所有进程以统一的视角看待内存, 进程地址空间的存在让我们在编写程序的时候只需关注虚拟地址, 而无需关注数据在物理内存当中实际的存储位置.
-
页表的存在让进程在间接访问内存的时候, 增加一个转换的过程, 在这个转换的过程中, 内核对进程的寻址请求进行检查, 如果该进程的寻址请求异常, 则该请求被操作系统拦截, 从而实现对物理内存的保护.
-
进程地址空间与页表的存在 , 让内核对于进程管理模块与内存管理模块进行了解耦.