940 likes | 1.23k Views
Solaris 操作系统实验 - 内存管理. Outline. 内存管理 概述 虚拟内存 匿名内存 物理内存. 虚存管理中涉及到的实体. 虚拟内存 程序员看到的地址空间,还没有被 MMU 转换 通常大于实际的物理内存 物理内存 由物理地址或实地址应用 系统中实际安装的内存 文件 磁盘上的数据. 虚拟内存与物理内存的关系. 虚拟地址通过页表映射到物理地址 进程地址空间 一个进程可以访问的所有虚拟地址集合 每个地址空间有一个页表. 虚拟内存与文件的关系. 虚地址空间包含到不同的映射 每个映射是一个地址范围,用一个段( seg )表示
E N D
Outline • 内存管理概述 • 虚拟内存 • 匿名内存 • 物理内存
虚存管理中涉及到的实体 • 虚拟内存 • 程序员看到的地址空间,还没有被MMU转换 • 通常大于实际的物理内存 • 物理内存 • 由物理地址或实地址应用 • 系统中实际安装的内存 • 文件 • 磁盘上的数据
虚拟内存与物理内存的关系 • 虚拟地址通过页表映射到物理地址 • 进程地址空间 • 一个进程可以访问的所有虚拟地址集合 • 每个地址空间有一个页表
虚拟内存与文件的关系 • 虚地址空间包含到不同的映射 • 每个映射是一个地址范围,用一个段(seg)表示 • 每个段把它的虚地址映射到文件或设备
物理内存与文件的关系 • 每个物理页面与文件或设备中的一块相对应 • 可能页面的内容来自文件 • 可能页面在换出时需要写入文件
Outline • 内存管理概述 • 虚拟内存 • 匿名内存 • 物理内存
虚拟内存 • 虚拟内存 • 观察进程和内核的地址空间 • 文件映射 • 页故障
简介 • 目的 • 了解进程和内核的地址空间布局 • 主要步骤 • 编写一个程序,声明不同类型的变量,观察这些变量位于什么段中 • 观察堆段的创建 • 观察内核的地址空间 • 知识点 • 进程地址空间的布局 • 内核地址空间的布局 • 数据段的映射 • 堆段的创建
地址空间 • 每个地址空间中的虚存被分成连续的段,用seg数据结构表示
源程序 - test.c #include <stdio.h> #include <stdlib.h> // 已初始化全局变量 int var1 = 1; char str[] = "Global!"; // 未初始化全局变量 int var2; char *buffer; int main() { // 局部变量 int var3; buffer = (char*)malloc(4096); // 动态分配的内存 printf("Address:\n"); printf("\tvar1:\t%p\n", &var1); printf("\tvar2:\t%p\n", &var2); printf("\tvar3:\t%p\n", &var3); printf("\tstr:\t%p\n", str); printf("\tbuffer:\t%p\n", buffer); getchar(); free((void*)buffer); return 0; }
查看不同变量所在的段 - 32位x86平台 • 运行test -bash-3.00$ ./test Address: var1: 8060ba4 // 已初始化全局变量 var2: 8060bb0 // 未初始化全局变量 var3: 8047cc4 // 局部变量 str: 8060ba8 // 已初始化全局变量 buffer: 8060f88 // 动态分配的内存 • 观察test的地址空间,并比较不同的变量落入什么段中 -bash-3.00$ pmap `pgrep test` 17785: ./test 08046000 8K rwx-- [ stack ] // 栈段 08050000 4K r-x-- /home/user1/source/test // 代码段 08060000 4K rwx-- /home/user1/source/test // 数据段 08061000 8K rwx-- [ heap ] // 堆段 FEEA0000 24K rwx-- [ anon ] FEEB0000 896K r-x-- /lib/libc.so.1 …… total 1284K 动态分配的内存一部分在数据段中,一部分在堆中,为什么?
观察ELF文件信息 • 观察程序头: 程序头定义了执行程序时映射进内存的部分,包括映射的虚地址、长度、对齐边界等 -bash-3.00$ elfdump -p test ………… 程序头[3]: p_vaddr: 0x8050000 p_flags: [ PF_X PF_R ] p_paddr: 0 p_type: [ PT_LOAD ] p_filesz: 0xa55 p_memsz: 0xa55 p_offset: 0 p_align: 0x10000 程序头[4]: p_vaddr: 0x8060a58 p_flags: [ PF_X PF_W PF_R ] p_paddr: 0 p_type: [ PT_LOAD ] p_filesz: 0x158 p_memsz: 0x524 p_offset: 0xa58 p_align: 0x10000 ………… • 观察章节头: -bash-3.00$ elfdump -c test ………… 章节头[19]: sh_name: .data sh_addr: 0x8060b58 sh_flags: [ SHF_WRITE SHF_ALLOC ] sh_size: 0x58 sh_type: [ SHT_PROGBITS ] sh_offset: 0xb58 sh_entsize: 0 sh_link: 0 sh_info: 0 sh_addralign: 0x4 ………… 章节头[21]: sh_name: .bss sh_addr: 0x8060bb0 sh_flags: [ SHF_WRITE SHF_ALLOC ] sh_size: 0x3cc sh_type: [ SHT_NOBITS ] sh_offset: 0xbb0 sh_entsize: 0 sh_link: 0 sh_info: 0 sh_addralign: 0x8 ………… 映射的虚地址 访问权限 对齐边界 内存长度:内存中实际占的长度,包括已初始化全局变量和未初始化全局变量 文件长度:ELF文件中只需要存放已初始化变量的初始值,所以这个长度等于已初始化变量的长度 章节虚地址 章节长度
查看不同变量所在的段 - 64位x86平台 • 编译并运行64位程序 -bash-3.00$ cc -o test64 -xarch=amd64 test.c -bash-3.00$ ./test64 Address: var1: 410fa8 var2: 410fc0 var3: fffffd7fffdffbf8 str: 410fb0 buffer: 4119f0 • 查看64位程序的地址空间 -bash-3.00$ pmap `pgrep test64` 27493: ./test64 0000000000400000 4K r-x-- /home/user1/source/test64 0000000000410000 4K rw--- /home/user1/source/test64 0000000000411000 20K rw--- [ heap ] FFFFFD7FFF1B0000 4K rwx-- [ anon ] …… FFFFFD7FFF3FC000 8K rwx-- /lib/amd64/ld.so.1 FFFFFD7FFFDFE000 8K rw--- [ stack ] total 2088K 64位进程中,堆位于下方,栈位于顶端。
观察test中堆段的创建 • test刚启动时没有堆段 • 整个.bss段位于数据段中,brk的起始地址也位于数据段中 • 堆段在调用malloc时创建 • 步骤 • 使用segvn_create.d跟踪test中的段创建 • 单步运行test,看segvn_create.d何时捕获到堆栈的创建。
segvn_create.d脚本 #!/usr/sbin/dtrace -s #pragma D option quiet /* segvn_create的函数原型为: int segvn_create(struct seg *seg, void *argsp) */ self struct segvn_crargs *ap; /* 跟踪segvn_create函数的调用,可以指定跟踪什么程序,以及跟踪在什么地址范围内的段创建 */ fbt::segvn_create:entry / execname == $$1 && (long)args[0]->s_base >= $2 && (long)args[0]->s_base < $3 / { self->ap = (struct segvn_crargs*)args[1]; printf("\nUser space stack:"); ustack(); printf("\nKernel space stack:"); stack(); printf("\nexecname:%s\tpid:%d\tbase:%x\n", execname, pid, (long)args[0]->s_base); printf("\tvp:\t%X\n\toffset:\t%X\n\ttype:\t%d\n\tflags:\t%X\n\n", (long)self->ap->vp, self->ap->offset, self->ap->type, self->ap->flags); } 调用segvn_create的程序的名称 跟踪哪个地址范围内的段创建
观察test刚启动时的地址空间 • 启动dtrace脚本 # ./segvn_create.d test 0x8061000 0x10000000 • 用mdb运行程序 -bash-3.00$ mdb test > main::bp > ::run mdb: stop at main mdb: target stopped at: main: pushl %ebp • 观察test的地址空间,此时还没有堆段存在 > ::mappings BASE LIMIT SIZE NAME 8046000 8048000 2000 [ stack ] 8050000 8051000 1000 /home/user1/source/test 8060000 8061000 1000 /home/user1/source/test feea0000 feea6000 6000 [ anon ] ……
捕获seg_vn段的创建 • 单步运行test > :e mdb: target stopped at: main+0x14: call -0x185 <PLT:malloc> > :e mdb: target stopped at: main+0x19: addl $0x4,%esp • 此时segvn_create.d脚本跟踪到堆段的创建,脚本的输出为: User space stack: libc.so.1`_brk_unlocked+0x15 libc.so.1`sbrk+0x2c libc.so.1`_morecore+0xf9 libc.so.1`_malloc_unlocked+0x164 libc.so.1`malloc+0x37 test`main+0x19 test`_start+0x7a Kernel space stack: genunix`as_map_locked+0x15c genunix`as_map+0x53 genunix`brk_internal+0x2aa genunix`brk+0x62 unix`_sys_sysenter_post_swapgs+0x14b execname:test pid:18185 base:8061000 vp: 0 offset: 0 type: 2 flags: 0 执行完malloc之后脚本就会有输出 程序调用malloc分配内存,malloc调用brk完成栈段的创建和扩展
观察堆段创建完后的地址空间 > ::mappings BASE LIMIT SIZE NAME 8046000 8048000 2000 [ stack ] 8050000 8051000 1000 /home/user1/source/test 8060000 8061000 1000 /home/user1/source/test 8061000 8063000 2000 [ heap ] feea0000 feea6000 6000 [ anon ] …… 执行完malloc函数后进程的地址空间中就多了一个堆段
查看内核的地址空间 • 在“mdb -k”中用mappings命令观察 64位平台 > ::mappings BASE LIMIT SIZE NAME fffffe0000000000 fffffe001f687000 1f687000 kpmseg fffffe8000000000 fffffe801f000000 1f000000 kpseg fffffe805f000000 fffffe8063000000 4000000 kmapseg fffffe8063000000 ffffffffc0000000 17f5d000000 kvseg ffffffffc0000000 fffffffffa7fe000 3a7fe000 kvseg_core fffffffffa800000 fffffffffb800000 1000000 kvalloc fffffffffb800000 fffffffffbd15000 515000 ktextseg ffffffffff800000 ffffffffffc00000 400000 kdebugseg 32位平台 > ::mappings BASE LIMIT SIZE NAME d1802000 d2800000 ffe000 kmapseg d2800000 f93fe000 26bfe000 kvseg fd400000 fe800000 1400000 kvalloc fe800000 fecd1000 4d1000 ktextseg ff800000 ffc00000 400000 kdebugseg
64-Bit x86 Kernel Address Space Layout kernel heap Pageable kernel memory Mapping physical memory
虚拟内存 • 虚拟内存 • 观察进程和内核的地址空间 • 文件映射 • 页故障
简介 • 目的 • 了解Solaris中的文件映射I/O • 主要步骤 • 编写一段代码,调用mmap函数映射一个文件 • 在执行mmap函数前后分别观察进程的地址空间,看有什么不同 • 观察文件映射段中页面的内容 • 观察多个进程映射同一个文件时物理页面的共享 • 知识点 • mmap系统调用 • 文件映射的实现方式 • 在文件映射中的页面共享
参考源代码 - mmaptest.c #include <stdio.h> …… void *addr = (void*)0x10000000, *map_addr; int main(int argc, char** argv) { int fildes, len; struct stat statinfo; char c, buffer[256]; fildes = open(argv[1], O_RDWR); fstat(fildes, &statinfo)); len = statinfo.st_size; gets(buffer); if ((map_addr = mmap(addr, len, PROT_READ, MAP_SHARED | MAP_FIXED, fildes, 0)) == MAP_FAILED) printf("Map failed!\n"); else { // 访问映射段中每一个虚页,使得内核为这些虚页分配物理页面,并从文件中读入内存 for (int i = 0; i < len; i += 4096) c = ((char*)map_addr)[i]; ………… munmap(map_addr, statinfo.st_size); } …… } 映射之前要打开文件 调用mmap完成映射 程序退出之前解除映射
查看mmaptest的地址空间 单步运行mmaptest,在mmap调用之前分别用pmap命令观察mmaptest的进程地址空间 • 查看执行mmap之前的地址空间 -bash-3.00$ pmap `pgrep mmaptest` 18587: ./mmaptest test.txt 08046000 8K rwx-- [ stack ] 08050000 4K r-x-- /home/user1/source/mmaptest 08060000 4K rwx-- /home/user1/source/mmaptest 08061000 4K rwx-- [ heap ] FEEA0000 24K rwx-- [ anon ] …… • 查看执行mmap之后的地址空间 -bash-3.00$ pmap `pgrep mmaptest` 18587: ./mmaptest test.txt 08046000 8K rwx-- [ stack ] 08050000 4K r-x-- /home/user1/source/mmaptest 08060000 4K rwx-- /home/user1/source/mmaptest 08061000 4K rwx-- [ heap ] 10000000 4K r--s- dev:102,7 ino:12273 FEEA0000 24K rwx-- [ anon ] …… mmap在地址空间中新增加了一个段
查看新增段对应的文件及其页面内容 • 查看新增加的段对应的文件 > ::pgrep mmaptest S PID PPID PGID SID UID FLAGS ADDR NAME R 18587 18231 18587 18231 100 0x4a004000 ffffffff82067690 mmaptest > ffffffff82067690::print struct proc p_as p_as = 0xffffffff82dfd2a0 > 0xffffffff82dfd2a0::walk seg | ::seg ! grep 10000000 ffffffff8378ea68 10000000 1000 ffffffff95245b18 segvn_ops > ffffffff8378ea68::print struct seg s_data s_data = 0xffffffff95245b18 > 0xffffffff95245b18::print struct segvn_data vp | ::vnode2path /home/user1/source/test.txt • 查看新增加的段中页面的内容 > 10000000::vtop -a 0xffffffff82dfd2a0 virtual 10000000 mapped to physical 1222b000 > 1222b000\s 0x1222b000: abcde 获取进程的proc结构地址 获取as结构的地址 遍历各个段,找出起始地址为10000000的段 对于seg_vn类型的段,s_data字段这个值指向一个segvn_data结构 映射的文件名为test.txt,与打开的文件相同 把虚地址10000000转换成物理地址 查看物理页面的内容,这个内容与文件内容一致
共享映射文件 • 启动两个mmaptest进程,并且让它们映射相同的文件 • 然后查看这两个进程中文件映射段对应的物理页面 > ::pgrep mmaptest S PID PPID PGID SID UID FLAGS ADDR NAME R 18598 18231 18598 18231 100 0x4a004000 ffffffff820682e0 mmaptest R 18597 18231 18597 18231 100 0x4a004000 ffffffff82067690 mmaptest 查看0x10000000在第一个进程中对应的物理内存 > ffffffff820682e0::print struct proc p_as p_as = 0xffffffff82dfd2a0 > 10000000::vtop -a 0xffffffff82dfd2a0 virtual 10000000 mapped to physical 1222b000 查看0x10000000在第二个进程中对应的物理内存 > ffffffff82067690::print struct proc p_as p_as = 0xffffffff834342a8 > 10000000::vtop -a 0xffffffff834342a8 virtual 10000000 mapped to physical 1222b000 两个进程使用的物理页面是相同的
虚拟内存 • 虚拟内存 • 观察进程和内核的地址空间 • 文件映射 • 页故障
简介 • 目的 • 了解Solaris中的页故障类型以及页故障处理方式 • 主要步骤 • 构造三个程序,来引发三种不同类型的页故障 • 用DTrace脚本跟踪页故障 • 观察不同类型的段上的页故障,以及未映射页故障 • 知识点 • 三种页故障类型 • 严重页故障(Major Page Fault) • 轻度页故障(Minor Page Fault) • 保护性页故障(Protection Page Fault) • 不同段上的页故障处理
DTrace脚本 - pagefault.d #!/usr/sbin/dtrace -s #pragma D option flowindent // 跟踪pagefault函数,每次发生页故障时,系统就会调用这个函数 fbt::pagefault:entry / execname == $$1 && (unsigned long)args[0] >= $2 && (unsigned long)args[0] < $3 / { stack(); printf("\nexecname:%s pid:%d addr:%x", execname, pid, (long)args[0]); self->start = 1; } fbt::pagefault:return / self->start == 1 / { // 显示pagefault函数的返回值,4(FC_PROTFC)表示越权操作,FC_NOMAP表示未映射 printf("\nReturn Addr:%x, Return Value:%d", arg0, arg1); exit(0); } fbt::: / self->start == 1 / { }
严重页故障 • 基本思路 • 程序在调用malloc操作的时候只是分配虚拟内存 • 当第一个访问分配的虚拟内存时,会引发页故障,这时系统才真正分配物理页面 • 分配的物理页面是新创建的物理页面,且会被清零 • 这次页故障就是一次严重页故障
严重页故障 - major.c • major.c源码 #include <stdlib.h> char* ptr; int main() { ptr = (char*)malloc(2 * 4096); // 这条指令将会引发一次页故障,导致系统为堆上的虚地址分配新的物理页 ptr[4095] = '\0'; free((void*)ptr); } • 运行pagefault.d,监视major的页故障,监视地址范围为0x8061000到0x8062000,包含了ptr[4095]这个位置 # ./pagefault.d major 0x8061000 0x8062000 dtrace: script './pagefault.d' matched 49260 probes
严重页故障 -地址映射的变化 • 在mdb中单步运行major -bash-3.00$ mdb major > main::bp > :r • ptr[4095]被访问之前虚地址的映射 > ::pgrep major S PID PPID PGID SID UID FLAGS ADDR NAME R 19063 19062 19062 18231 100 0x4a004000 ffffffffa50df8f8 major > ffffffffa50df8f8::print struct proc p_as p_as = 0xffffffff83434b68 > 8061997::vtop -a 0xffffffff83434b68 mdb: failed to get physical mapping: no mapping for address • ptr[4095]被访问之后虚地址的映射 > 8061997::vtop -a 0xffffffff83434b68 virtual 8061997 mapped to physical fc97997 系统为ptr[4095]所在的虚拟页面分配了物理页面
严重页故障 -处理过程 pagefault.d的输出 CPU FUNCTION 0 -> pagefault unix`trap+0xb6c unix`_cmntrap+0x140 execname:major pid:19063 addr:8061997 0 | pagefault:entry 0 -> segkp_map_red 0 <- segkp_map_red 0 -> as_fault 0 -> as_segat 0 <- as_segat 0 -> segvn_fault …… 0 <- segvn_fault 0 <- as_fault 0 <- pagefault Return Addr:218, Return Value:0
轻度页故障 • 基本思路 • 创建一个文件,系统会为这个文件分配页面 • 创建过程退出后,新页面会被放入文件缓存中 • 用DTrace脚本捕获新创建的页面的地址 • 使用前面的mmaptest程序再次访问这个文件,由于页面仍在文件缓存中,这时就会引发一次轻度页故障 • 验证:比较mmaptest中的页面的地址是否与前面捕获的页面一样
轻度页故障 -监视页面的创建 • page_create.d源码 #!/usr/sbin/dtrace -s #pragma D option quiet fbt::page_create_va:return, fbt::page_lookup_create:return / arg1 != 0 && ((struct page*)arg1)->p_vnode && ((struct page*)arg1)->p_vnode->v_path == $$1 / { printf("%-15s %-20s page:%x\n", execname, probefunc, arg1); } • 监视与某个文件相关的页面的创建 # ./page_create.d /home/user1/source/test.txt • 创建test.txt -bash-3.00$ echo "abcde" > test.txt • page_create.d的输出为 bash page_create_va page:fffffffffac18530
轻度页故障 - 监视页故障 • 用pagefault.d监视前面编译好的mmaptest • 运行mmaptest,访问新创建的test.txt • 观察pagefault.d的输出 • 查看页故障后新分配的物理页面的page结构地址,看它是否与刚才被缓存的页面一致 > 0x10000000::vtop -a 0xffffffff95238e00 virtual 10000000 mapped to physical 8b5b000 > 8b5b::page_num2pp 8b5b has page at fffffffffac18530
保护性页故障 • 基本思路 • 让程序对代码段执行写操作 • protect.c源码。protect.c中执行了往代码段写的操作。 int main() { ((char*)main)[0] = '\0'; return 0; }
保护性页故障 运行时发生段错误 -bash-3.00$ ./protect 段错误 (core dumped) pagefault.d的输出 CPU FUNCTION …… execname:protect pid:18914 addr:8050740 1 | pagefault:entry 1 -> segkp_map_red 1 <- segkp_map_red 1 -> as_fault …… 1 <- as_fault 1 <- pagefault Return Addr:218, Return Value:4 返回值4(FC_PROT)表示越权操作
不同段类型上的页故障 seg_fault.d源码,这个脚本用来统计不同段类型上的页故障次数。 #!/usr/sbin/dtrace -s #pragma D option quiet fbt::segvn_fault:entry, fbt::segmap_fault:entry, fbt::segdev_fault:entry, fbt::segkmem_fault:entry, fbt::segkpm_fault:entry, fbt::segkp_fault:entry { @[probefunc] = count(); } nomaptest.c源码。这个程序访问未映射区域,导致程序发生段错误。 int main() { char* p = (char*)0x10000000; p[0] = '\0'; return 0; } • 用户进程的段基本上都是seg_vn类型的段 • 其他具有页故障处理函数的段类型还有seg_map、seg_kpm、seg_kp和seg_dev等 • 如果一个程序访问了一个不属于任何段的虚地址,就会发生一次未映射错误