标签:mm unsigned long mem 地址 源码 linux page 页面
总体功能:
在Intel 80X86 CPU中,程序在寻址过程中使用的是由段和偏移值构成的地址。该地址并不能直接用来寻址物理内存地址,因此被称为虚拟地址。为了能寻址物理内存,就需要一种地址变换机制将虚拟地址映射或变换到物理内存中,这种地址变换机制就是内存管理的主要功能之一(内存管理的另外一个主要功能是内存的寻址保护机制。由于篇幅所限,本章不对其进行讨论)。虚拟地址通过段管理机制首先变换成一种中间地址形式一CPU32位的线性地址,然后使用分页管理机制将此线性地址映射到物理地址。为了弄清 Linux内核对内存的管理操作方式,我们需要了解内存分页管理的工作原理,了解其寻址的机制。分页管理的目的是将物理内存页面映射到某一线性地址处。在分析本章的内存管理程序时,需
明确区分清楚给定的地址是指线性地址还是实际物理内存的地址。
来自:
https://in1t.top/2020/04/27/linux%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB-mm/
mm 是 Linux 0.11 内存管理的模块,一共两个文件 memory.c 与 page.s。开篇先来“再续前缘”,继续探讨写时复制技术的后半部分。
写时复制之页错误
上一篇文章提到了,当父/子进程其中之一对只读的内存页面进行写操作时,会产生页错误的异常,该异常处理程序负责将共享的内存页面复制到新内存页中,并重新构建该页表项,使其指向新内存页并可写。实际上,页错误异常不仅由写保护引发,还有可能是缺页引起的。页错误异常就定义在 page.s 中,该文件也就只有 page_fault 的代码:
PLAINTEXT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
.globl page_fault /* 引发页错误的线性地址保存在控制寄存器 CR2 中 */ page_fault: xchgl %eax,(%esp) /* 将出错码取到 eax 中 */ pushl %ecx pushl %edx push %ds push %es push %fs /* 保存现场 */ movl $0x10,%edx mov %dx,%ds mov %dx,%es mov %dx,%fs /* 修改段寄存器,指向内核数据段 */ movl %cr2,%edx /* 将引起页错误的线性地址放到 edx 中 */ pushl %edx pushl %eax /* 压参(页错误线性地址与错误码) */ testl $1,%eax /* 页存在 P 位如果不为 0,表明不是由缺页引起的异常 */ jne 1f /* 而是由写保护引发的异常,跳去调用 do_wp_page */ call do_no_page /* 如果是缺页引发的异常,则调用 do_no_page */ jmp 2f 1: call do_wp_page 2: addl $8,%esp /* 栈平衡 */ pop %fs pop %es pop %ds popl %edx popl %ecx popl %eax /* 还原现场 */ iret
|
先来看由写保护引起的异常处理函数 do_wp_page(之后涉及的函数都在 memory.c 中)
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
// Line 248 void do_wp_page(unsigned long error_code,unsigned long address) { #if 0 if (CODE_SPACE(address)) // 这段代码本意是如果线性地址位于进程代码空间中, do_exit(SIGSEGV); // 则终止程序,但 #if 0 表示该段代码不起作用 #endif un_wp_page((unsigned long *) // 实际通过 un_wp_page 实现,参数是线性地址 address (((address>>10) & 0xffc) + (0xfffff000 & // 对应的页面在页表中的页表项指针 *((unsigned long *) ((address>>20) &0xffc)))));
}
// Line 222 void un_wp_page(unsigned long * table_entry) { unsigned long old_page,new_page;
old_page = 0xfffff000 & *table_entry; // 获取页面物理地址 // 判断该页面是否在主内存区(LOW_MEM 值为 1MB,1MB 以上为主内存区),并且没有被共享 if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) { *table_entry |= 2; // 加上写属性 invalidate(); // 刷新 TLB return; } // 否则在主内存区申请空闲页给执行写操作的进程单独使用 if (!(new_page=get_free_page())) // 如果没有空闲页则调用 oom 报错退出,oom 定义在 33 行 oom(); // out of memory if (old_page >= LOW_MEM) // 如果页面物理在主内存区,且被共享了 mem_map[MAP_NR(old_page)]--; // 将映射的数量减 1 *table_entry = new_page | 7; // 设置新页面可读写、存在 invalidate(); // 刷新 TLB copy_page(old_page,new_page); // 拷贝旧页面内容到新页面中 }
// Line 54 #define copy_page(from,to) \ __asm__("cld ; rep ; movsl"::"S" (from),"D" (to),"c" (1024))
|
于是,写时复制的全貌就展现完毕了。由缺页引发的页错误处理涉及到块设备的知识,之后再做记录。
mem_map数组
之前涉及内存管理的代码都或多或少地有 mem_map 数组的影子,这个字符数组就是 Linux 用于判断 1MB 以上物理内存使用情况的,每个字节描述一个物理页面的占用状态,该字节的数值表示该页面被占用的次数,0 代表该页面空闲,100 代表该页面已被完全占用,不能再被分配/共享。Linux 0.11 的物理内存区域划分如下:
mm 模块中的几类函数
释放内存
接着来看 memory.c 中还剩下的一些函数,可根据功能分为几类,首先是释放内存:
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
// Line 106 // 该函数释放从 from 开始,长度为 size 字节的线性地址空间 int free_page_tables(unsigned long from,unsigned long size) { unsigned long *pg_table; unsigned long * dir, nr;
if (from & 0x3fffff) // 检查 from 是否是 4MB 对齐 panic("free_page_tables called with wrong alignment"); if (!from) // 如果 from 为 0,则不允许释放内核空间 panic("Trying to free up swapper memory space"); size = (size + 0x3fffff) >> 22; // 对 size 进行 4MB 舍入,右移 22 位求出涉及到的页目录项数 dir = (unsigned long *) ((from>>20) & 0xffc); // 计算起始的页目录项地址 for ( ; size-->0 ; dir++) { // 遍历涉及到的页目录项 if (!(1 & *dir)) // 如果该项不存在(P 位为 0),则跳过 continue; pg_table = (unsigned long *) (0xfffff000 & *dir); // 页表基址 for (nr=0 ; nr<1024 ; nr++) { // 遍历所有页表项 if (1 & *pg_table) // 如果该页存在 free_page(0xfffff000 & *pg_table); // 释放该页所占用的空间 *pg_table = 0; // 页表项置空 pg_table++; } free_page(0xfffff000 & *dir); // 释放该页表所占用的空间 *dir = 0; } invalidate(); // 刷新 TLB return 0; }
// Line 90 // 释放一页空间实际上就是将 mem_map 数组对应项的映射数减 1 void free_page(unsigned long addr) { if (addr < LOW_MEM) return; // 不释放内核占用的空间 if (addr >= HIGH_MEMORY) // 对于超出内存大小的地址,直接死机 panic("trying to free nonexistent page"); addr -= LOW_MEM; addr >>= 12; // 求出在 mem_map 数组中的索引 if (mem_map[addr]--) return; // 如果映射数不为 0,则减 1 后返回 mem_map[addr]=0; // 否则置 0 并死机 panic("trying to free free page"); }
|
获取空闲页面
第二类有关获取空闲页面
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
|
// Line 275 // 实际通过 get_free_page 与 put_page 实现 void get_empty_page(unsigned long address) { unsigned long tmp;
if (!(tmp=get_free_page()) || !put_page(tmp,address)) { free_page(tmp); oom(); } }
// Line 63 // PAGING_PAGES 为页面总数,输入为 ax = 0,cx = PAGING_PAGES, // edi = mem_map+PAGING_PAGES-1(即 mem_map 最后一项的地址) unsigned long get_free_page(void) { register unsigned long __res asm("ax"); // std: 置方向位,从高到低 // repne: repeat if not equal,即下一条指令如果不能使 ZF 标志位为 0,则重复该指令,最多 ecx 次 // scasb: scas 用于比较字符串,加个 b 表示一次比较一字符,比较一次,di 自动递减(std 设置了方向) // 每次操作比较 es:[di] 和 al 是否相等,这里从 mem_map 最后一项开始寻找映射数为 0 的项 // __asm__("std ; repne ; scasb\n\t" "jne 1f\n\t" // 如果遍历完 mem_map 还是没有找到空闲页面,则返回 "movb $1,1(%%edi)\n\t" // 到这里说明找到了空闲页,将 mem_map 对应项置 1 "sall $12,%%ecx\n\t" // 索引 * 4K 得到页面的相对起始地址 "addl %2,%%ecx\n\t" // 加上 LOW_MEM 得到页面实际物理起始地址 "movl %%ecx,%%edx\n\t" // 起始地址赋给 edi "movl $1024,%%ecx\n\t" // 循环 1024 次 "leal 4092(%%edx),%%edi\n\t" // 当前页面末端 4 字节地址赋给 edi "rep ; stosl\n\t" // stosl 将 eax 中的内容赋值给 es:[edi] "movl %%edx,%%eax\n\t" // 返回值为页面物理起始地址 "1:" "cld\n\t" // 复位方向位 :"=a" (__res) :"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES), "D" (mem_map+PAGING_PAGES-1) ); return __res; }
// Line 198 // 该函数将内存页面物理地址 page 映射到指定线性地址 address 处 unsigned long put_page(unsigned long page,unsigned long address) { unsigned long tmp, *page_table; // page 应该在 LOW_MEM 与 HIGH_MEMORY 之间 if (page < LOW_MEM || page >= HIGH_MEMORY) printk("Trying to put page %p at %p\n",page,address); if (mem_map[(page-LOW_MEM)>>12] != 1) // 检查 page 是否是已经申请的页面 printk("mem_map disagrees with %p at %p\n",page,address); page_table = (unsigned long *) ((address>>20) & 0xffc); // 线性地址对应页目录项指针 if ((*page_table)&1) // 如果页目录存在,直接获取页表地址 page_table = (unsigned long *) (0xfffff000 & *page_table); else { // 否则申请一空内存页,存放页表 if (!(tmp=get_free_page())) return 0; *page_table = tmp|7; // 设置权限等 page_table = (unsigned long *) tmp; // 获取页表地址 } page_table[(address>>12) & 0x3ff] = page | 7; // 修改线性地址对应的页表项,指向给定的物理地址 return page; // 无需刷新 TLB,直接返回 }
|
共享内存
第三类有关共享内存,share_page 函数仅被缺页处理函数 do_no_page 调用。这里引入一个新概念——页面逻辑地址,意为该页面地址是以进程的代码/数据起始地址算起的页面地址。以下是 do_no_page 部分代码:
C
1 2 3 4 5 6 7 8
|
// Line 373 // address 是产生页错误的线性地址 // current->start_code 是当前进程的线性地址空间基址(64MB 对齐) // address - current->start_code 求出地址对应的页面逻辑地址 address &= 0xfffff000; // 取得线性地址所在的线性页面的地址 tmp = address - current->start_code; if (share_page(tmp)) return;
|
share_page 的具体实现:
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
|
// Line 345 // 寻找运行相同执行文件的进程,并尝试与之共享页面 // 参数 address 是页面逻辑地址 static int share_page(unsigned long address) { struct task_struct ** p;
if (!current->executable) // 如果当前进程没有执行文件,则返回 return 0; // 如果当前进程正在执行的文件引用数小于 2,说明只有当前进程在运行该文件,直接返回 if (current->executable->i_count < 2) return 0; for (p = &LAST_TASK ; p > &FIRST_TASK ; --p) { // 遍历任务数组 if (!*p) // 跳过空任务项 continue; if (current == *p) // 跳过当前进程 continue; if ((*p)->executable != current->executable) // 跳过与当前进程执行的文件不同的进程 continue; if (try_to_share(address,*p)) // 调用 try_to_share 尝试共享页面 return 1; } return 0; }
// Line 293 // 尝试与目标任务 p 共享内存 // address 为产生页错误的线性地址所在的线性页面地址对应的页面逻辑地址(有点绕) static int try_to_share(unsigned long address, struct task_struct * p) { unsigned long from; unsigned long to; unsigned long from_page; unsigned long to_page; unsigned long phys_addr;
from_page = to_page = ((address>>20) & 0xffc); from_page += ((p->start_code>>20) & 0xffc); // 计算进程 p 的 address 对应的页目录项地址 to_page += ((current->start_code>>20) & 0xffc); // 计算当前进程的 address 对应的页目录项地址 from = *(unsigned long *) from_page; // 获得进程 p 页目录项 if (!(from & 1)) // 如果进程 p 该页表不存在,返回 0 return 0; from &= 0xfffff000; // 否则取得页表基址 from_page = from + ((address>>10) & 0xffc); // 计算页表项地址 phys_addr = *(unsigned long *) from_page; // 获得页表项内容 if ((phys_addr & 0x41) != 0x01) // 0x41 对应 dirty 与 P 位,判断页面是否干净且存在 return 0; phys_addr &= 0xfffff000; // 满足条件取得页面物理地址 if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM) // 判断物理地址是否越界 return 0; to = *(unsigned long *) to_page; // 获得当前进程页目录项 if (!(to & 1)) // 如果当前进程该页表不存在 if (to = get_free_page()) // 则申请一页内存当页表 *(unsigned long *) to_page = to | 7; else oom(); to &= 0xfffff000; // 取得页表基址 to_page = to + ((address>>10) & 0xffc); // 计算页表项地址 if (1 & *(unsigned long *) to_page) // 如果当前进程的该页已有(我们本意是想共享内存),则死机 panic("try_to_share: to_page already exists"); *(unsigned long *) from_page &= ~2; // 取消页的写权限 *(unsigned long *) to_page = *(unsigned long *) from_page; // 建立映射 invalidate(); // 刷新 TLB phys_addr -= LOW_MEM; phys_addr >>= 12; mem_map[phys_addr]++; // 物理地址在 mem_map 数组中对应项的映射数 + 1 return 1; }
|
初始化函数
第四类是 main.c 中调用的 mem_init 初始化函数:
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
// Line 400 void mem_init(long start_mem, long end_mem) { int i;
HIGH_MEMORY = end_mem; // 设置内存最高地址(16MB) for (i=0 ; i<PAGING_PAGES ; i++) // 将 mem_map 所有项都赋值为 100,表示已占用 mem_map[i] = USED; i = MAP_NR(start_mem); // 获取主内存开始地址对应的索引 end_mem -= start_mem; // 计算主内存区域大小 end_mem >>= 12; // 计算在 mem_map 数组中一共有多少项 while (end_mem-->0) // 将 mem_map 从后往前清零 mem_map[i++]=0; }
|
其他
最后是一些杂项:
C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
// Line 262 // 写页面的验证,address 是页面的线性地址 void write_verify(unsigned long address) { unsigned long page;
if (!( (page = *((unsigned long *) ((address>>20) & 0xffc)) )&1)) // 页表不存在则返回 return; page &= 0xfffff000; // 取得页表基址 page += ((address>>10) & 0xffc); // 计算页表项地址 if ((3 & *(unsigned long *) page) == 1) // 如果对应的页只读、存在 un_wp_page((unsigned long *) page); // 则执行复制页面、构建新映射的操作 return; }
// Line 414 // 显示当前内存信息 void calc_mem(void) { int i,j,k,free=0; long * pg_tbl;
for(i=0 ; i<PAGING_PAGES ; i++) if (!mem_map[i]) free++; // 计算主内存区中有多少空闲页面并打印 printk("%d pages free (of %d)\n\r",free,PAGING_PAGES); for(i=2 ; i<1024 ; i++) { // 遍历所有页目录项 if (1&pg_dir[i]) { // 如果对应页表存在 pg_tbl=(long *) (0xfffff000 & pg_dir[i]); // 获取页表基址 for(j=k=0 ; j<1024 ; j++) //遍历页表项 if (pg_tbl[j]&1) // 如果对应物理页存在,计数变量 k 加 1 k++; printk("Pg-dir[%d] uses %d pages\n",i,k); // 打印页目录中有多少正在使用的页 } } }
|
参考:https://in1t.top/2020/04/27/linux%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB-mm/
൘ Intel 80X86 CPU ѝˈ〻ᒿ൘ራ൰䗷〻ѝ֯⭘Ⲵᱟ⭡⇥઼ٿ〫٬ᶴᡀⲴൠ൰DŽ䈕ൠ൰ᒦн㜭ⴤ᧕⭘
ᶕራ൰⢙⨶ᆈൠ൰ˈഐ↔㻛〠Ѫ㲊ᤏൠ൰DŽѪҶ㜭ራ൰⢙⨶ᆈˈቡ䴰㾱аൠ൰ਈᦒᵪࡦሶ㲊ᤏൠ
൰᱐ሴᡆਈᦒࡠ⢙⨶ᆈѝˈ䘉ൠ൰ਈᦒᵪࡦቡᱟᆈ㇑⨶Ⲵѫ㾱࣏㜭ѻа˄ᆈ㇑⨶Ⲵਖཆањѫ
㾱࣏㜭ᱟᆈⲴራ൰؍ᣔᵪࡦDŽ⭡Ҿㇷᑵᡰ䲀ˈᵜㄐнሩަ䘋㹼䇘䇪˅ DŽ㲊ᤏൠ൰䙊䗷⇥㇑⨶ᵪࡦ俆ݸਈ
ᦒᡀаѝ䰤ൠ൰ᖒᔿ —CPU 32 սⲴ㓯ᙗൠ൰ˈ❦ਾ֯⭘࠶亥㇑⨶ᵪࡦሶ↔㓯ᙗൠ൰᱐ሴࡠ⢙⨶ൠ൰DŽ
ѪҶᔴ Linux ṨሩᆈⲴ㇑⨶ᯩᔿˈᡁԜ䴰㾱Ҷ䀓ᆈ࠶亥㇑⨶Ⲵᐕ⨶ˈҶ䀓ަራ൰
ⲴᵪࡦDŽ࠶亥㇑⨶ⲴⴞⲴᱟሶ⢙⨶ᆈ亥䶒᱐ሴࡠḀа㓯ᙗൠ൰༴DŽ൘࠶᷀ᵜㄐⲴᆈ㇑⨶〻ᒿᰦˈ䴰
᰾⺞४࠶ᾊ㔉ᇊⲴൠ൰ᱟᤷ㓯ᙗൠ൰䘈ᱟᇎ䱵⢙⨶ᆈⲴൠ൰DŽ
标签:mm,
unsigned,
long,
mem,
地址,
源码,
linux,
page,
页面
From: https://www.cnblogs.com/rebrobot/p/18319139