总体功能:
在Intel 80X86 CPU中,程序在寻址过程中使用的是由段和偏移值构成的地址。该地址并不能直接用来寻址物理内存地址,因此被称为虚拟地址。为了能寻址物理内存,就需要一种地址变换机制将虚拟地址映射或变换到物理内存中,这种地址变换机制就是内存管理的主要功能之一(内存管理的另外一个主要功能是内存的寻址保护机制。由于篇幅所限,本章不对其进行讨论)。虚拟地址通过段管理机制首先变换成一种中间地址形式一CPU32位的线性地址,然后使用分页管理机制将此线性地址映射到物理地址。
为了弄清 Linux内核对内存的管理操作方式,我们需要了解内存分页管理的工作原理,了解其寻址的机制。分页管理的目的是将物理内存页面映射到某一线性地址处。在分析本章的内存管理程序时,需明确区分清楚给定的地址是指线性地址还是实际物理内存的地址。
0.11 linux 内核与后来的linux 有所区别。
0.11 虚拟地址 不等于线性地址。后面的虚拟地址等于线性地址





个人理解:https://blog.csdn.net/weixin_42110038/article/details/116907981
linux内存采用分页的内存管理,而32位pc机还采用了段式内存管理,它的地址关系为:逻辑地址--->虚拟地址(也叫线性地址)--->物理地址。
linux所有段基地址都为0,所以可以看做linux并没有采用段式的内存管理,linux的地址关系:虚拟地址--->物理地址
因为,linux会为每个进程分配独立的进程地址空间(大小为3GB),这里的地址指的是虚拟地址并不是实际意义上的物理地址,通过map映射到对应的物理地址。
malloc()
malloc申请内存返回的是虚拟地址而不是物理地址,而且,返回的这个虚拟地址是没有map映射到物理内存中的,只有当程序(进程)用到访问了这个地址的时候,
内核才响应“缺页异常”,才回去映射一块物理地址。
关于malloc(0) 能申请成功是肯定的,它返回的是虚拟地址(对应的物理地址还不确定的),在你没用到这块地址空间的时候,它不会再物理上开辟内存。
在你用到malloc(0)返回的地址时,因为参数为0,缺页异常不会为你的进程空间申请额外内存。所以这个指针应该是不能用的。
关于虚拟地址
可以把一个程序运行3次,为了保证程序一直运行可以在程序中加10秒的延迟。可以看到这3个进程的使用的地址是一样的(虚拟地址),映射的物理地址肯定不一样的
===================================================================
来自:https://www.cnblogs.com/kukudi/p/11416993.html
一、逻辑地址转线性地址
机器语言指令中出现的内存地址,都是逻辑地址,需要转换成线性地址,再经过MMU(CPU中的内存管理单元)转换成物理地址才能够被访问到
我们写个最简单的hello world程序,用gcc编译,再反汇编后会看到以下指令:
mov 0x80495b0, %eax
这里的内存地址0x80495b0 就是一个逻辑地址,必须加上隐含的DS 数据段的基地址,才能构成线性地址。也就是说 0x80495b0 是当前任务的DS数据段内的偏移。
在x86保护模式下,段的信息(段基线性地址、长度、权限等)即段描述符占8个字节,段信息无法直接存放在段寄存器中(段寄存器只有2字节)。Intel的设计是段描述符集中存放在GDT或LDT中,而段寄存器存放的是段描述符在GDT或LDT内的索引值(index)。
Linux中逻辑地址等于线性地址。为什么这么说呢?因为Linux所有的段(用户代码段、用户数据段、内核代码段、内核数据段)的线性地址都是从 0x00000000 开始,长度4G,这样 线性地址=逻辑地址+ 0x00000000,也就是说逻辑地址等于线性地址了。
这样的情况下Linux只用到了GDT,不论是用户任务还是内核任务,都没有用到LDT。GDT的第12和13项段描述符是 __KERNEL_CS 和__KERNEL_DS,第14和15项段描述符是 __USER_CS 和__USER_DS。内核任务使用__KERNEL_CS 和__KERNEL_DS,所有的用户任务共用__USER_CS 和__USER_DS,也就是说不需要给每个任务再单独分配段描述符。内核段描述符和用户段描述符虽然起始线性地址和长度都一样,但DPL(描述符特权级)是不一样的。__KERNEL_CS 和__KERNEL_DS 的DPL值为0(最高特权),__USER_CS 和__USER_DS的DPL值为3。
用gdb调试程序的时候,用info reg 显示当前寄存器的值:
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
可以看到ds值为0x7b, 转换成二进制为 00000000 01111011,TI字段值为0,表示使用GDT,GDT索引值为 01111,即十进制15,对应的就是GDT内的__USER_DS用户数据段描述符。
从上面可以看到,Linux在x86的分段机制上运行,却通过一个巧妙的方式绕开了分段(即逻辑地址=线性地址)。Linux主要以分页的方式实现内存管理

1.--CPU的段寄存器:
在CPU中,跟段有关的CPU寄存器一共有6个:cs,ss,ds,es,fs,gs,它们保存的是段选择符(或者叫段描述符)。而同时这六个寄存器每个都有一个对应的非编程寄存器,它们对应的非编程寄存器中保存的是段描述符。系统可以把同一个寄存器用于不同的目的,方法是先将其寄存器中的值保存到内存中,之后恢复。而在系统中最主要的是cs,ds,ss这三个寄存器。
2.--段描述符
段描述符就是保存在全局描述符表或者局部描述符表中,当某个段寄存器试图通过自己的段选择符获取对于的段描述符时,会将获取到的段描述符放到自己的非编程寄存器中,这样就不用每次访问段都要跑到内存中的段描述符表中获取。
-
BASE(32位):段首地址的线性地址。
-
G:为0代表此段长度以字节为单位,为1代表此段长度以4K为单位。
-
LIMIT(20位):此最后一个地址的偏移量,也相当于长度,G=0,段大小在1~1MB,G=1,段大小为4KB~4GB。
-
S:为0表示是系统段,否则为代码段或数据段。
-
Type:描述段的类型和存取权限。
-
DPL:描述符特权级,表示访问这个段CPU要求的最小优先级(保存在cs寄存器的CPL特权级),当DPL为0时,只有CPL为0才能访问,DPL为3时,CPL为0为3都可以访问这个段。
-
P:表示此段是否被交换到磁盘,总是置为1,因为linux不会把一个段都交换到磁盘中。
-
D或B:如果段的LIMIT是32位长,则置1,如果是16位长,置0。(详见intel手册)
-
AVL:忽略。
2.1--数据段描述符:
表示这个段描述符代表一个数据段,这种描述符可以放在GDT或者LDT。该描述符的S标志位为1,也就是非系统段。需要注意内核数据段属于数据段描述符,并不属于系统段描述符。
2.2--代码段描述符:
表示这个段描述符代表一个数据段,这种描述符可以放在GDT或者LDT。该描述符的S标志位为1,也就是非系统段。需要注意内核代码段属于代码段描述符,并不属于系统段描述符
3.--全局描述符表与局部描述符表
全局描述符表和局部描述符表保存的都是段描述符,记住要把段描述符和段选择符区别开来,保存在寄存器中的是段选择符,这个段选择符会到描述符表中获取对于的段描述符,然后将段描述符保存到对应寄存器的非编程寄存器中。
系统中每个CPU有属于自己的一个全局描述符表(GDT),其所在内存的基地址和其大小一起保存在CPU的gdtr寄存器中。其大小为64K,一共可保存8192个段描述符,不过第一个一般都会置空,也就是能保存8191个段描述符。第一个置空的原因是防止加电后段寄存器未经初始化就进入保护模式而使用GDT。
而对于局部描述符表,CPU设定是每个进程可以创建属于自己的局部描述符表(LDT),当前被使用的LDT的基地址和大小一起保存在ldtr寄存器中。不过大多数用户态的liunx程序都不使用局部描述符表,所以linux内核只定义了一个缺省的LDT供大多数进程共享。描述这个局部描述符表的局部描述符表描述符保存在GDT中
===================================================================
GDT的全称叫做Global Descriptor Table,中文名叫全局描述符。 其实在早期x86系统中是没有GDT的,GDT的引入是为了向下兼容和引入保护模式才出现的产物。 我们知道当计算机加点时,CPU最开始是运行在实模式上的。 要想从实模式运行到保护模式则需要引入GDT
===================================================================
来自:
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 的代码:
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 中)
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
|
void do_wp_page(unsigned long error_code,unsigned long address) { #if 0 if (CODE_SPACE(address)) do_exit(SIGSEGV); #endif un_wp_page((unsigned long *) (((address>>10) & 0xffc) + (0xfffff000 & *((unsigned long *) ((address>>20) &0xffc)))));
}
void un_wp_page(unsigned long * table_entry) { unsigned long old_page,new_page;
old_page = 0xfffff000 & *table_entry; if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) { *table_entry |= 2; invalidate(); return; } if (!(new_page=get_free_page())) oom(); if (old_page >= LOW_MEM) mem_map[MAP_NR(old_page)]--; *table_entry = new_page | 7; invalidate(); copy_page(old_page,new_page); }
#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 中还剩下的一些函数,可根据功能分为几类,首先是释放内存:
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
|
int free_page_tables(unsigned long from,unsigned long size) { unsigned long *pg_table; unsigned long * dir, nr;
if (from & 0x3fffff) panic("free_page_tables called with wrong alignment"); if (!from) panic("Trying to free up swapper memory space"); size = (size + 0x3fffff) >> 22; dir = (unsigned long *) ((from>>20) & 0xffc); for ( ; size-->0 ; dir++) { if (!(1 & *dir)) 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(); return 0; }
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; if (mem_map[addr]--) return; mem_map[addr]=0; panic("trying to free free page"); }
|
获取空闲页面
第二类有关获取空闲页面
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
|
void get_empty_page(unsigned long address) { unsigned long tmp;
if (!(tmp=get_free_page()) || !put_page(tmp,address)) { free_page(tmp); oom(); } }
unsigned long get_free_page(void) { register unsigned long __res asm("ax");
__asm__("std ; repne ; scasb\n\t" "jne 1f\n\t" "movb $1,1(%%edi)\n\t" "sall $12,%%ecx\n\t" "addl %2,%%ecx\n\t" "movl %%ecx,%%edx\n\t" "movl $1024,%%ecx\n\t" "leal 4092(%%edx),%%edi\n\t" "rep ; stosl\n\t" "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; }
unsigned long put_page(unsigned long page,unsigned long address) { unsigned long tmp, *page_table; 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) 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; }
|
共享内存
第三类有关共享内存,share_page 函数仅被缺页处理函数 do_no_page 调用。这里引入一个新概念——页面逻辑地址,意为该页面地址是以进程的代码/数据起始地址算起的页面地址。以下是 do_no_page 部分代码:
1 2 3 4 5 6 7 8
|
address &= 0xfffff000; tmp = address - current->start_code; if (share_page(tmp)) return;
|
share_page 的具体实现:
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
|
static int share_page(unsigned long address) { struct task_struct ** p;
if (!current->executable) return 0; 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)) return 1; } return 0; }
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); to_page += ((current->start_code>>20) & 0xffc); from = *(unsigned long *) from_page; if (!(from & 1)) return 0; from &= 0xfffff000; from_page = from + ((address>>10) & 0xffc); phys_addr = *(unsigned long *) from_page; if ((phys_addr & 0x41) != 0x01) 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(); phys_addr -= LOW_MEM; phys_addr >>= 12; mem_map[phys_addr]++; return 1; }
|
初始化函数
第四类是 main.c 中调用的 mem_init 初始化函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
void mem_init(long start_mem, long end_mem) { int i;
HIGH_MEMORY = end_mem; for (i=0 ; i<PAGING_PAGES ; i++) mem_map[i] = USED; i = MAP_NR(start_mem); end_mem -= start_mem; end_mem >>= 12; while (end_mem-->0) mem_map[i++]=0; }
|
其他
最后是一些杂项:
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
|
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; }
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++; 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Ž
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!