段有利于程序员开发应用程序,因为程序员可以将一个程序分为几个程序段(如主程序段、数据段、函数库等等),分而治之;
页对物理内存比较友好,因为页的使用提高了内存的空间效率,引入快表后也提高了时间效率。
所以实际内存使用段页结合的方式来进行管理。
L23 段页结合的实际内存管理
虚拟内存的引出
段有利于程序员开发应用程序,因为程序员可以将一个程序分为几个程序段(如主程序段、数据段、函数库等等),分而治之;
页对物理内存比较友好,因为页的使用提高了内存的空间效率,引入快表后也提高了时间效率。
所以实际内存使用段页结合的方式来进行管理。
从一整个程序出发,为了实现段,就要将一整个程序分为几个程序段,然后存储在“内存”中,但如果直接将程序段存储在物理内存中,就无法实现页了,所以是将程序段存储在一个虚拟内存中;为了实现页,就要将划分为一页一页的物理内存一页一页地分配给虚拟内存中的程序段,就实现了页。
★段页同时存在时的重定位
当段页同时存在时,程序段中的逻辑地址就要经过两层地址翻译才能找到对应于它的物理内存地址:
- 第一层地址翻译是通过段表在虚拟内存中找到对应于逻辑地址的虚拟地址;
- 第二层地址翻译是通过页表在物理内存中找到对应于虚拟地址的物理地址。
★总结:逻辑地址 -> 虚拟地址 -> 物理地址
一个实际的段页式内存管理
- 内存管理首先要能使用内存,即分配内存,将程序存储到内存中。为了使用内存,就要建立段表和页表,分配内存是从进程被创建开始的,所以一切都要从 fork 说起。
★★★程序载入内存
一整个程序被划分为多个程序段,每个段分别载入虚拟内存,可以利用“L21-可变分区的管理”中的分区分配算法来实现,然后用段表来存储程序段在虚拟内存中的地址;
-> 虚拟内存中的程序段又被划分为多页(每一页的大小和物理内存中一页的大小相同),分别将这些页载入物理内存中,然后用页表来存储程序段中的每一页在物理内存中的地址。
以
mov[300]
中的逻辑地址 300 为例,首先通过段表可知,逻辑地址 300 处的数据存储在虚拟内存中 0x00045300 的位置,然后 0x00045300 右移 12 位(4K = 2^12)得 0x45,余 0x300;所以通过页表可知,第 0x45 个页表项的页框号为 7,所以物理地址为 0x0000007<<12 + 0x300 = 0x0007300 处。所以总共可以分为 5 步:①从虚拟内存中找出一段空闲空间;②建立段表;③从物理内存中找出几页空闲空间并将程序存储进去;④建立页表;⑤根据段表和页表,找到逻辑地址所对应的物理地址。
分配虚存、建段表
在 linux-0.11/kernel/fork.c 文件中,
copy_mem
函数用于分配虚拟内存并建立段表。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24int copy_mem(int nr,struct task_struct * p)
{
unsigned long old_data_base,new_data_base,data_limit;
unsigned long old_code_base,new_code_base,code_limit;
code_limit=get_limit(0x0f);
data_limit=get_limit(0x17);
old_code_base = get_base(current->ldt[1]);
old_data_base = get_base(current->ldt[2]);
if (old_data_base != old_code_base)
panic("We don't support separate I&D");
if (data_limit < code_limit)
panic("Bad data_limit");
new_data_base = new_code_base = nr * 0x4000000; //64M*nr,步骤①,从虚拟内存中找出一段空闲空间
p->start_code = new_code_base;
set_base(p->ldt[1],new_code_base); //步骤②,建立段表
set_base(p->ldt[2],new_data_base);
if (copy_page_tables(old_data_base,new_data_base,data_limit)) {
printk("free_page_tables: from copy_mem\n");
free_page_tables(new_data_base,data_limit);
return -ENOMEM;
}
return 0;
}注意:在 linux-0.11 中,每个进程的代码段和数据段是同一个段。每个进程占用 64M 虚拟地址空间,互不重叠,这意味着,就算这些进程使用同一套页表,也不会产生物理内存冲突的问题,所以可以共用一套页表。
linux-0.11 操作系统其实是简化的操作系统,在现代的操作系统中,多个进程并不共用一套页表。
分配内存、建页表
在 linux-0.11/mm/memory.c 文件中,
copy_page_tables
函数用于分配内存并建立页表。注意:由于 fork 是从父进程分叉出子进程,子进程是复制父进程的,所以子进程和父进程共用物理内存的同一套页,所以在这里子进程就不需要再在物理内存中分配页了,但还是要建页表(建页表需要物理内存),拷贝父进程的页表即可。
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
40int 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;
if ((from&0x3fffff) || (to&0x3fffff))
panic("copy_page_tables called with wrong alignment");
from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
to_dir = (unsigned long *) ((to>>20) & 0xffc);
size = ((unsigned) (size+0x3fffff)) >> 22;
for( ; size-->0 ; from_dir++,to_dir++) {
if (1 & *to_dir)
panic("copy_page_tables: already exist");
if (!(1 & *from_dir))
continue;
from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
if (!(to_page_table = (unsigned long *) get_free_page()))
return -1; /* Out of memory, see freeing */
*to_dir = ((unsigned long) to_page_table) | 7;
nr = (from==0)?0xA0:1024;
for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
this_page = *from_page_table;
if (!(1 & this_page))
continue;
this_page &= ~2;
*to_page_table = this_page;
if (this_page > LOW_MEM) {
*from_page_table = this_page;
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
}
}
invalidate();
return 0;
}本来是
from>>22
得到页目录号,然后再将其 *4(因为每项四个字节),就可以得到页目录的基地址,这里是并成一句了,from>>20
就是(from>>22)*4
,再&0xffc
的目的就是为了得到的是基地址(0xffc = 111111111100
)。size 是父进程的页目录数,这里就是为子进程分配建立页表所需的物理内存,然后将父进程页表的内容复制给子进程。
此时,子进程和父进程就在物理内存中共用了同一套页,子进程的页表中的内容就是复制的父进程的页表中的内容,不过在虚拟内存中,子进程和父进程并不共用同样的虚拟内存空间,所以它们的段表并不相同。
使用内存
父进程(进程1)的虚拟内存的基地址为 64M = 0x4000000,偏移 0x300,所以 p 在虚拟内存中的地址为 0x4000300,然后 0x4000300>>12 得 0x4000(这里没有考虑页目录号的情况),余 0x300,再查页表,就能得到最后得物理内存地址;子进程(进程2)同理,不过需要注意子进程的虚拟内存的基地址为 128M = 0x8000000。
由于父进程和子进程共用物理内存的一套页,并且在 fork 时子进程的页表是复制父进程的页表的,所以子进程在找它的 p 的物理内存地址时,也会找到 0x0007300,会和父进程的 p 的物理内存地址冲突,所以这时就要在共用的这一套页中找到一个空闲的页,并修改子进程的页表,使得 p 的虚拟地址在子进程的页表中找到的物理内存地址为空闲的页的地址(这里为 0x0008300),这样就实现了父进程的 p 和子进程的 p 的物理内存地址分离。