linux 0.11内存管理

linux 0.11内存管理

线性地址空间的格局

每个线程的虚拟地址空间并不重叠,每个进程不跨越自己的空间。如何防止进程跨越64M的空间进行非法访问?

非法跨越进程边界有两种情况:

  • 一个进程非法跨越到内核

  • 一个进程非法跨越到另一个进程

示意图

非法跨越问题

如何防止进程跨越到内核的非法访问

通过cpu硬件禁止从3特权级跳转到0特权级。通过设置LDT,GDT设定了特权级。进程能否自己修改LDT,GDT?不能,因为这两个都在内核数据段,是0特权级,进程无法访问。那进程能不能自己建一个LDT,GDT狸猫换太子?也不能,因为运行的时候CPU只将GDTR和LDTR寄存器指向的数据结构认定为LDT,GDT,进程自己没法伪造。而LDTR和GDTR是0特权级的进程没法把自己伪造的挂上去。

在寻址过程中我们会对这一内容做一个总结。详见下面的寻址章节。

进程怎么合理的跨越特权级

涉及进程通信和中断门,TSS进程切换

分页管理

页面地址映射

重要寄存器:

CR3:指向页目录表的地址。CR3里面存放的是物理地址

CR0寄存器: CR0

内核分页机制

建立过程:

1
2
3
4
5
6
7
8
9
.org 0x1000
pg0:
.org 0x2000
pg1:
.org 0x3000
pg2:
.org 0x4000
pg3:
.org 0x5000
1
2
3
4
5
6
7
8
9
   movl $pg0+7,_pg_dir		/* set present bit/user r/w */
movl $pg1+7,_pg_dir+4 /* --------- " " --------- */
movl $pg2+7,_pg_dir+8 /* --------- " " --------- */
movl $pg3+7,_pg_dir+12 /* --------- " " --------- */
/*在这段汇编代码中,_pg_dir 是一个地址,_pg_dir+4 是将 _pg_dir 的地址值增加 4 个字节,
以访问 _pg_dir 中的下一个 4 字节的位置。这种情况通常出现在处理数组或数据结构的情况下,
需要按字节访问连续的内存位置。
在汇编语言中,符号$通常用于表示立即数(immediate value),而不是地址偏移。$pg2+7
中的7是一个立即数,而不是偏移量。这个7代表了一个具体的数值,而不是表示字节或二进制值。 */

上面的7代表111,这三位分别代表用户u,读写rw,存在p。如果是000代表内核,只读,不存在页。加7就是把后12位用于存权限问题。movl $pg0+7,_pg_dir 这行汇编指令的含义是将地址 pg0 加上 7 存储到 _pg_dir 地址处。因此上面这一段代码的意义就是让页目录表指向每个页表项,同时第一个地方指向页目录自己,每次递增4个字节即32位地址。也就是如下图所示,这里的_pg_dir就是head的起始地址,这里是在一边执行head.s的代码一边覆盖。

1
2
3
4
5
6
	movl $pg3+4092,%edi/*一个页表的最后一项在页表中的位置是1023*4=4092(一共1024项,第1024项的开始地址) */
movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) 一个页面4k=4096,16M是整个页面管理的空间,所以这里是最后一个页面的地址*/
std/*方向位置位,edi递减 */
1: stosl /* fill pages backwards - more efficient :-) */
subl $0x1000,%eax /*每写好一项物理地址递减0x1000 ,16^3=2^12=4k */
jge 1b/*如果小于0说明全填好了 */

edi指向了第一个页面,eax指向了最后一个页面

其中进程0用的是内核的页表。

地址32位4字节,一共页表4k,可以保存1k个线性地址间接指向下一级页表页表映射关系

页面管理

内核通过mem_map管理1M以上的内存空间。

mem_map

目录项或页表项P位为1的时候说明已经和某页面建立了映射,如果为0说明没有建立映射机制。此时如果使用该页表项的页面会发送缺页中断。

进程管理自己的页

进程的页目录表是通过内核代码调用得到的,处于内核数据段。因此进程自己是没有权限修改自己的页目录表的,是由内核全权代理。

父子进程共享页面

copy pagetable:

进入copy_page_tables函数后,先为新的页表申请一个空闲页面,并把进程0中第一个页表里的前160个页表项复制到这个页面中(1个页表项控制一个页面4KB内存空间,160个页表项可以控制640KB内存空间)。进程0和进程1的页表暂时度指向了相同的页面,意味着进程1也可以操作进程0的页面。之后对进程1的页目录表进行设置。最后,用重置CR3的方法刷新页面变换高速缓存。进程1的页表和页目录表设置完毕。进程1此时是一个空架子,还没有对应的程序,它的页表又是从进程0的页表复制过来的,它们管理的页面完全一致,也就是它暂时和进程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
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
69
70
71
72
73
74
75
//from 父进程的段基地址,to子进程的段基地址
/*分页实现的是从线性地址到物理地址的转换,因此函数的输入一定是线性地址,
为了方便遍历,把单位换算为M,此处硬件MMU要求页目录项的地址4M对齐*/
int copy_page_tables(unsigned long from,unsigned long to,long size)
{
unsigned long * from_page_table;
unsigned long * to_page_table;
unsigned long this_page;
unsigned long * from_dir, * to_dir;
unsigned long nr;
//0x3fffff 是4M,是一个页表的管辖范围,22位from和to必须是4MB的整数倍,一个页表对应的4MB连续的线性地址空间必须是从0开始的4MB的整数倍数
//4M一个页表的覆盖范围,如果4M没有对齐则panic,cpu的要求,cpu分页要对齐,页目录表项要4M对齐
if ((from&0x3fffff) || (to&0x3fffff)) //必须满足后面22位都是0才能不panic
panic("copy_page_tables called with wrong alignment");
//父进程页目录表项的位置,一个线性地址空间对应一个页目录表
//from右移20位:以MB为单位,例如0010 0000 0000 0000 0000 000》0010就是2M
//确保from_dir是4M的倍数
from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 *///地址数由字节数变成M,c:1100,ffc肯定是4的倍数,因此是4M的倍数,
//有ff是因为32位地址,右移20,还剩12位,ffc,十二位
//确保to_dir是4M的倍数
to_dir = (unsigned long *) ((to>>20) & 0xffc);
//22 4MB,这里是不足4M强行等于一个4M
size = ((unsigned) (size+0x3fffff)) >> 22;
//基地址的低第一位:是P位,指示页表是否存在
/*
以进程0创建进程1为例:
此时from:0
to :0x4000000(64M)
SIZE:段限长:0x9f(0xa0)160*4k=640k
进行单位换算以后就变成了:from 0M TO 64 M size=1M ,如下所示的from和to两个指针进行拷贝操作
---------
^(0M from_dir)
----...----
^(64M to_dir)
两层循环:
for 父进程的遍历页目录项对应的每一个页表
if子进程页目录项存在报错
if 父进程页目录项不存在则跳过,继续便历下一项
为子进程的页表分配页面
for 遍历该页目录项指向的页表对应的页面
if(父进程该页表不存在,则跳过,继续遍历下一个页表)
(用nr来记录要遍历的页面的数量,如果from是0M也就是进程0,则要遍历的页面长度是段限长640K.这是因为进程0比较特殊,与内核公用了一个页表,进程0的东西不能都被拷贝给子进程,否则就有问题了.比如进程0的页目录由16 M后两页的内容,后两页有子进程1的task_struct 和页表项,这俩子进程应该无权访问,否则子进程就可以修改自己的页面映射关系了显然不大对。)
注意:1M以内的页面不参与用户分页管理
1.设置页面的权限,因为子进程共享了父进程的页面,因此应该把子进程的权限设置位只读,否则两个进程如果存在同时写的情况就会出错。子进程如果需要修改页面,则会引出后面要讲的copy on write
2.将页面填到子进程的页表(页框frame)里面,完成映射关系
3.修改mem_map里面的引用计数,表面该页面被新增的进程占用
*/
for( ; size-->0 ; from_dir++,to_dir++) {
if (1 & *to_dir)//1:p位。
panic("copy_page_tables: already exist");
if (!(1 & *from_dir)) //from的页表不存在,则没必要进行下去了
continue;
//0xfffff000 低12位清零,from_dir是页目录项的地址,高20位是页表的地址
from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
if (!(to_page_table = (unsigned long *) get_free_page()))//获得空页面。上次调用是在找一个空页面放 task_struct
return -1; /* Out of memory, see freeing */
*to_dir = ((unsigned long) to_page_table) | 7;//这次是给子进程的基地址段分配页面
nr = (from==0)?0xA0:1024; //0xa0:0x9f 可以查看init task里面的ldt的断限长计算,把父进程的160个页表项640KB空间的内容复制给子进程
for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
this_page = *from_page_table;
if (!(1 & this_page))
continue;
this_page &= ~2;//设置页表属性~2:101 用户,只读,存在
*to_page_table = this_page;
if (this_page > LOW_MEM) {//1MB以内的内核区域不参与用户分页管理
*from_page_table = this_page;
this_page -= LOW_MEM;
this_page >>= 12; //
mem_map[this_page]++; //增加引用计数,说明这个页被这个进程占用了
}
}
}
invalidate();//重置CR3为0,刷新页变换高速缓存
return 0;
}
copypagetable

页面双指针管理

分配给进程的页面除了进程通过建立页表项的映射,内核本身也在管理

双指针

寻址

寻址过程

寻址机制

逻辑地址->线性地址

一个==逻辑地址==由一个 16 位的段选择子和一个 32 位的偏移组成

例如jmpi 0,8,0是偏移,8是段选择子:==1000==

段选择子格式:

段选择子格式

1000:

解释图

转换关系:

转换关系

线性地址->物理地址

32位地址:CPU 在看到我们给出的内存地址后,首先把线性地址被拆分成

高 10 位:中间 10 位:后 12 位。

线性地址

\(2^{10}\)=1K,一个页目录表有1k个页表,一个页表包含1k个页表项,一个页表指向一个页面(4k)。

因此一个页表对应了4M的物理空间。这里一共建立了4个页表:16M空间,理论上可以更大。由于硬件中cpu是4k对齐的

因此低12位会是零,所以可以用高20位来表示4KB对齐的页表和页,因此可以用后12位设置权限。

线性地址

代码示例

以如下代码为例展示从线性地址转换到物理地址的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void write_verify(unsigned long address)
{
unsigned long page;
// 判断指定地址所对应页目录项的页表是否存在(P),若不存在(P=0)则返回。4M对齐
if (!( (page = *((unsigned long *) ((address>>20) & 0xffc)) )&1))
return;
// 取页表的地址,加上指定地址的页面在页表中的页表项偏移值,得对应物理页面的页表项指针。
page &= 0xfffff000;//4k对齐
page += ((address>>10) & 0xffc);//(ffc,置0了除了11~2位的所有位)高10位置零,低十位置零只留了中间十位,后两位置零了起到乘4的效果,因为每一个偏移量是以4字节(32位)为单位的,避免后面取到了index再乘4的操作
// 如果该页面不可写(标志 R/W 没有置位),则执行共享检验和复制页面操作(写时复制)。
if ((3 & *(unsigned long *) page) == 1) /* non-writeable, present */
un_wp_page((unsigned long *) page);
return;
}

page = *((unsigned long *) ((address>>20) & 0xffc))

地址转换

IA-32 特权级保护

linux 0.11的特权级保护是基于==段==的。

访问控制是针对==内存==的访问控制,是以字节为单位的。

首先,程序发生跳转指令的时候会有非法访问的嫌疑。短跳转不会涉及段的变化,不会非法访问。而长跳转例如 jmpi 0,8

怎么确保不会从低特权级跳到高特权级?

访问数据段

高特权级的代码段可以访问第特权级的数据段,低特权不允许访问高特权的数据

访问数据段的时候的特权检查

举个例子

堆栈

SS寄存器加载

访问代码段

程序控制从一个代码段转换到另一个代码段时,必须把目标代码段的段选择子装 入 CS 寄存器。在装载过程中,处理器会检查目标代码段的段描述符,对段界限、类型 及特权级进行检验。进程控制转移是用 JMP、CALL、RET、SYEENTER、SYSEXIT、INT n 和 IRET 指令或者异常和中断机制来实现的。

目标代码段描述符的一致性(C)标志(段描述符类型域),这个标志确定一个段是一个一致性代 码段(标志为 1)还是非一致性代码段(标志为 0)。

访问非一致性代码段

当访问非一致性代码段时,调用例程的 CPL 必须与目标代码段的 DPL 相等,否则 处理器会产生一个一般保护异常(#GP)。

访问非一致性代码段

访问一致性代码段

当访问一致性代码段时,调用例程的 CPL 可以在数值上等于或小于(较低特权级)目标代码段的 DPL。仅当 CPL 大于 DPL 的时候,处理器产生一个一般保护异常(#GP)。 (当目标代码段是一致性代码段时,不用检验目标代码段的 RPL。)

门调用

门调用

GDT的变迁

==详见:第一章和第二章笔记==

GDT的变迁

段描述符结构

段描述符:写在gdt表里面的那个内容也是段描述符。段描述符是 GDT 或 LDT 中的一个数据结构,它为处理器提供诸如段基地址、段大 小、访问权限及状态等信息。

段描述符结构

G表示粒度:

  • 如果粒度标志位为 0,则段大小可以从 1 字节到 1M 字节,段长 增量单位为字节。

  • 如果粒度标志位为 1,则段大小可以从 4K 字节到 4G 字节,段长 增量单位为 4K 字节。

D/B(默认操 作数大小/默认 栈指针大小和/ 或上限)标志

根据段描述符所指的是可执行代码段、向下扩展的数据段还是堆栈 段,这个标志有不同的功能。(对 32 位的代码和数据段,这个标志 总是被置为 1,而 16 位的代码和数据段,这个标志总是被置为 0。)

P(段存在) 标志

指明段当前是否在内存中(1 表示在内存中,0 表示不在)。当指向 段描述符的段选择子被装进段寄存器时,如果这个标志为 0,处理器 会产生段不存在异常(#NP)。内存管理软件可以通过这个标志,来 控制在某个特定时间有哪些段是真正的被载入物理内存的。这是除 分页之外的另一个虚拟内存控制机制。 图 3-9 演示了段存在标志置 0 时段描述符的格式。当这个标志置 0 时,操作系统或者管理软件就可以随意去使用标明为“可用”的地 方(段描述符里)来存贮自己的数据,比如有关已消失段的所在位 置的信息。

DPL(描述符 特权级)域

指明段的特权级。特权级从 0 到 3,0 为最高特权级。DPL 用来控制 对段的访问。DPL:访问该段的权限设置

S(描述符类 型)标志

确定段描述符是系统描述符(S 标记为 0)或者代码、数据段描述符 (S 标记为 1)

类型域 指明段或者门的类型,确定段的访问权限和增长方向。如何解释这 个域,取决于该描述符是应用程序描述符(代码或数据)还是系统 描述符。代码段、数据段和系统段对类型域有不同的编码

类型域