段与页结合的初步思路
-
虚拟内存的引入:
- 为了结合段和页的优势,操作系统引入了虚拟内存的概念。虚拟内存是一段地址空间,它映射到物理内存上,但对用户程序是透明的。
-
段到虚拟内存的映射:
- 用户程序中的段首先映射到虚拟内存的相应区域。这一步骤通过段表(Segment Table)实现,段表记录了每个段的起始地址和长度。
-
虚拟内存到物理内存的映射:
- 虚拟内存中的区域再映射到物理内存的页上。这一步骤通过页表(Page Table)实现,页表记录了虚拟页到物理页框的映射关系。
-
地址翻译:
- 用户程序发出的地址首先被翻译成虚拟地址,然后再翻译成物理地址。这一过程通常由硬件中的内存管理单元(MMU)自动完成。
-
内存访问:
- 一旦地址被翻译成物理地址,CPU就可以访问物理内存中的数据,执行指令或进行数据操作。
用户从应用程序视角看待虚拟内存中的段和页

从用户程序的视角来看,虚拟内存中的段和页提供了一种抽象的内存模型,使得程序能够以一种简化和统一的方式来访问内存。以下是用户程序如何查看和与虚拟内存中的段和页交互的方式:
-
逻辑地址空间:
- 用户程序操作的是逻辑地址空间,这是一个由操作系统管理的虚拟地址空间。在这个空间中,程序可以使用段和偏移量(如
cs:ip
)来访问内存,而不需要关心物理内存的实际布局。
- 用户程序操作的是逻辑地址空间,这是一个由操作系统管理的虚拟地址空间。在这个空间中,程序可以使用段和偏移量(如
-
地址翻译:
-
用户程序发出的地址(如
0x00345008
)首先被翻译成虚拟地址,然后再由操作系统和硬件(如内存管理单元MMU)翻译成物理地址(如0x7008
)。 -
这个过程对用户程序是透明的,用户程序不需要知道具体的物理地址。
-
-
内存保护和隔离:
- 段和页的结合允许操作系统实现内存保护和隔离。每个进程都有自己的虚拟地址空间,操作系统确保一个进程不能访问另一个进程的内存空间。
-
虚拟内存的优势:
-
虚拟内存提供了内存的抽象,使得程序可以假设它拥有一个连续的、大的内存空间,而实际上物理内存可能是分散的、有限的。
-
这种抽象简化了程序设计,提高了内存使用的灵活性,并允许操作系统更有效地管理内存资源。
-
重定位

逻辑地址到物理地址的转换过程
-
逻辑地址 :由段号和偏移量组成,例如
段号+偏移(cs:ip)
。 -
段表:包含段号、基址、长度和保护信息。
-
段号:标识段的编号。
-
基址:段在内存中的起始地址。
-
长度:段的大小。
-
保护:段的访问权限(如只读R、读写R/W)。
-
-
页号和偏移:逻辑地址中的偏移量被进一步分解为页号和页内偏移。
-
页表:包含页框号和保护信息。
-
页框号:页在内存中的物理位置。
-
保护:页的访问权限(如只读R、读写R/W)。
-
-
物理地址:由物理页号和偏移量组成。
地址翻译过程
-
逻辑地址:由段号和偏移量组成。
-
段表查找:根据段号查找段表,获取基址和长度。
-
偏移分解:将偏移量分解为页号和页内偏移。
-
页表查找:根据页号查找页表,获取页框号。
-
物理地址计算:将页框号和页内偏移组合成物理地址。
段页同时存在的场景下,重定位过程是怎样的
在断页同时存在的场景下,用户发出的逻辑地址是CS加上段号和偏移量。
操作系统首先通过段表找到段在虚拟内存中的位置,并生成一个虚拟地址。
然而,这个虚拟地址并不是直接对应的物理内存地址,而是需要经过一次映射,根据虚拟地址计算出页号,再结合页内偏移得到物理地址。
最后,操作系统将这个物理地址发送到地址总线上,从而实现从段和页两个层次上的地址翻译,确保代码能够正确执行和获取数据。
一个实际的段页结合的例子
内存分配

内存管理的核心
- 内存管理核心就是内存分配:强调了内存分配是内存管理的关键部分。
具体示例
-
指令 :
_sum: .int 0
定义了一个名为_sum
的变量,初始值为0。 -
指令 :
_main: mov [300], 0
将0移动到偏移量为300的地址。 -
地址计算 :
base + 300(offset)
表示将基地址和偏移量相加得到物理地址。
内存管理过程
-
分配段、建段表:为进程分配内存段,并建立段表来管理这些段。
-
分配页、建页表:为进程分配内存页,并建立页表来管理这些页。
-
进程带动内存使用的图谱:展示了进程如何使用内存的图谱。
-
从进程fork中的内存分配开始:说明了进程创建(fork)时的内存分配过程。
载入内存

程序加载过程
-
分配段:为程序的不同部分(如代码、数据、栈)分配内存段。
-
建立段表:记录每个段的基址、长度和保护属性。
-
分配页:将每个段进一步分为页,并为每页分配物理内存。
-
建立页表:记录每个页的页框号和保护属性。
-
地址转换:将程序中的逻辑地址转换为物理地址,以便访问实际的内存。
示例
-
逻辑地址
0x300
:表示用户数据段中的一个地址。 -
指令
_main: mov [300], 0
:将0移动到偏移量为300的地址。 -
段表和页表:展示了如何通过段号和页号查找对应的基址和页框号,从而完成地址转换。
分配虚拟内存,建立段表
从幻灯片中提取的代码如下:
cpp
int copy_process(int nr, long ebp, ...)
{
...
copy_mem(nr, p); ...
}
int copy_mem(int nr, task_struct *p)
{
unsigned long new_data_base;
new_data_base = nr * 0x40000000; // 64M * nr
set_base(p->ldt[1], new_data_base);
set_base(p->ldt[2], new_data_base);
}
-
计算新的数据基址:
- 在
copy_mem
函数中,首先计算新的数据基址**new_data_base
** 。这个基址是通过将进程编号nr
乘以0x40000000
(即64MB)来得到的。这意味着每个进程将获得64MB的虚拟内存空间。
- 在
-
设置段表:
-
使用**
set_base
** 函数将计算出的新数据基址设置到进程的局部描述符表(LDT)中的相应段。在这个例子中,p->ldt[1]
和p->ldt[2]
分别代表了两个不同的段(例如,代码段和数据段)。 -
set_base
函数的作用是更新段描述符中的基址字段,使其指向新的虚拟内存区域。
-
-
进程控制块(PCB):
-
**
task_struct *p
是一个指向进程控制块(PCB)的指针,**它包含了进程的所有信息,包括段表。 -
在进程创建时,操作系统会为新进程分配一个新的PCB,并复制父进程的相关信息,同时为新进程分配独立的虚拟内存空间。
-
-
内存分配:
- 在Linux中,每个进程都有独立的虚拟内存空间。通过
fork()
系统调用创建的新进程会继承父进程的虚拟内存布局,但是操作系统会确保新进程的虚拟内存空间是独立的,以防止进程间的相互干扰。
- 在Linux中,每个进程都有独立的虚拟内存空间。通过
-
进程切换:
- 在进程切换时,操作系统需要更新CPU的段寄存器,以指向新进程的段表。这样,当新进程开始执行时,它将在自己的虚拟内存空间中运行。
分配内存,建立页表

这张幻灯片展示了在Linux操作系统中,如何为新创建的进程分配内存并建立页表。以下是提取的代码和对分配内存、建立页表过程的总结:
提取的代码
cpp
int copy_mem(int nr, task_struct *p)
{
unsigned long old_data_base;
old_data_base = get_base(current->ldt[2]);
copy_page_tables(old_data_base, new_data_base, data_limit);
}
int copy_page_tables(unsigned long from, unsigned long to, long size)
{
from_dir = (unsigned long *)(((from >> 20) & 0xffc));
to_dir = (unsigned long *)(((to >> 20) & 0xffc));
size = (unsigned long)((size + 0x3fffff) >> 22);
for (; size-- > 0; from_dir++, to_dir++) {
from_page_table = (0xfffff000 & *from_dir);
to_page_table = get_free_page();
// 这里应该还有代码来复制页表项和设置新的页表
}
}
分配内存、建立页表的总结
-
获取旧数据基址:
- 在
copy_mem
函数中,首先获取当前进程的数据段基址(old_data_base
),这通常是通过读取当前进程的段表(LDT)来实现的。
- 在
-
调用复制页表函数:
- 然后调用
copy_page_tables
函数,传入旧数据基址、新数据基址(new_data_base
)和数据段的大小(data_limit
),以复制页表。
- 然后调用
-
计算目录基址:
- 在
copy_page_tables
函数中,计算源目录(from_dir
)和目标目录(to_dir
)的基址。这是通过将虚拟地址右移20位(即页目录的索引)并取低12位(页目录项的偏移)来实现的。
- 在
-
计算页表项数量:
- 计算需要复制的页表项数量(
size
),这是通过将数据段大小加上0x3FFFFF(即1MB-1,因为页表项是按页大小为单位的),然后右移22位(即页大小为4KB)来实现的。
- 计算需要复制的页表项数量(
-
复制页表项:
-
在循环中,对于每个页表项**,从源目录读取页表项(
from_page_table
),然后从页框(page frame)中获取一个空闲页(to_page_table = get_free_page()
)来存储目标页表项。** -
这里应该还有代码来复制页表项的内容,并设置新的页表项,包括页框号、保护位等。
-
-
建立页表:
- 通过上述步骤,为新进程建立了页表,将虚拟地址映射到物理内存。
-
更新进程控制块(PCB):
- 最后,需要更新新进程的PCB,包括新的页表基址等信息,以便新进程可以正确地访问其虚拟内存。
from_dir,to_dir

cpp
from_dir = (unsigned long *)(((from >> 20) & 0xffc));
to_dir = (unsigned long *)(((to >> 20) & 0xffc));
size = (unsigned long)((size + 0x3fffff) >> 22);
for (; size-- > 0; from_dir++, to_dir++) {
from_page_table = (0xfffff000 & *from_dir);
// 这里应该还有代码来复制页表项和设置新的页表
}
-
计算页目录地址:
from_dir
和to_dir
是指向页目录的指针。它们是通过将虚拟地址右移20位(即页目录的索引)并取低12位(页目录项的偏移)来计算得到的。这是因为在x86架构中,页目录项是按4KB对齐的,所以需要取低12位来得到页目录项的偏移。
-
计算页表项数量:
size
是需要复制的页表项数量。这是通过将数据段大小加上0x3FFFFF(即1MB-1,因为页表项是按页大小为单位的),然后右移22位(即页大小为4KB)来计算得到的。
-
复制页表项:
-
在循环中,对于每个页表项,从源目录读取页表项(
from_page_table
),然后从页框(page frame)中获取一个空闲页(get_free_page()
)来存储目标页表项。 -
这里应该还有代码来复制页表项的内容,并设置新的页表项,包括页框号、保护位等。
-
-
页目录指针:
- 页目录指针(CR3)用于指向当前进程的页目录。在进程切换时,需要更新CR3寄存器以指向新进程的页目录。
from_page_table,to_page_table

从幻灯片中提取的代码和相关信息如下:
提取的代码:
cpp
for (; size-- > 0; from_dir++, to_dir++) {
to_page_table = get_free_page();
*to_dir = ((unsigned long)to_page_table) | 7;
}
unsigned long get_free_page(void) {
register unsigned long _res asm("ax");
__asm__("std; repne; scasb\n\t"
"movl %%edx, %%eax\n\t"
"D"(mem_map+PAGING_END-1));
return _res;
}
总结:
-
页表复制过程:
-
在循环中,对于每个页表项,首先通过调用
get_free_page()
函数为新进程分配一个空闲的页框(物理内存页)。 -
然后,将新分配的页框地址设置到目标页目录项中(
to_dir
),并设置页表项的权限(这里通过或操作| 7
来设置)。
-
-
页表项权限设置:
- 在x86架构中,页表项通常包含页框号和一些权限位。这里的
| 7
操作可能是设置页表项的权限位,例如可读写(RW)和存在位(P)。
- 在x86架构中,页表项通常包含页框号和一些权限位。这里的
-
页表项结构:
- 幻灯片中展示了页表项的结构,包括页目录号(10 bits)、页号(10 bits)和偏移(12 bits)。这是x86架构中分页机制的基本概念。
-
页目录和页表:
from_dir
和to_dir
分别指向源进程和目标进程的页目录项。通过复制页目录项,可以实现页表的复制和更新。
-
获取空闲页框:
get_free_page()
函数通过汇编语言实现,用于扫描内存映射(mem_map
)来找到一个空闲的页框。这个过程涉及到检查内存映射中的每个条目,直到找到一个空闲的页框。
-
内存映射:
mem_map
是一个内存映射数组,用于跟踪哪些页框是空闲的,哪些已经被使用。PAGING_END
可能是定义了内存映射数组的结束位置。
复制和更新页表
这张幻灯片展示了在操作系统中,如何复制和更新页表项以实现内存管理。以下是提取的代码和对过程的总结:
提取的代码:
cpp
for (; nr-- > 0; from_page_table++, to_page_table++) {
this_page = *from_page_table;
this_page &= ~2; // 只读
*to_page_table = this_page;
*from_page_table = this_page;
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
总结:
-
页表项复制:
- 代码中的循环遍历所有的页表项,从源页表(
from_page_table
)复制到目标页表(to_page_table
)。
- 代码中的循环遍历所有的页表项,从源页表(
-
权限设置:
this_page &= ~2;
这行代码通过位操作清除页表项中的某个权限位(通常是只读位),确保复制的页表项具有适当的权限设置。
-
页表项更新:
-
*to_page_table = this_page;
将修改后的页表项写入目标页表。 -
*from_page_table = this_page;
也将修改后的页表项写回源页表,这可能是为了确保源进程的页表项也反映了权限的更改。
-
-
页框号计算:
this_page -= LOW_MEM;
这行代码从页框号中减去一个基址(LOW_MEM
),可能是为了将页框号转换为相对于某个特定内存区域的偏移量。
-
页框号转换:
this_page >>= 12;
这行代码将页框号右移12位,这通常是为了将页框号转换为页框数组中的索引。
-
内存映射更新:
mem_map[this_page]++;
这行代码更新内存映射数组,增加对应页框的使用计数。这是为了跟踪每个页框的使用情况,特别是在使用写时复制(copy-on-write)技术时。
使用内存

写时复制(COW)技术
写时复制是一种优化技术,用于减少复制内存的开销,特别是在创建新进程时。在写时复制中,父子进程最初共享相同的物理内存页,只有当进程实际修改内存页时,才会创建该内存页的副本。
幻灯片内容分析
-
内存共享与写时复制:
-
只要段表和页表设置正确,父子进程就可以通过MMU(内存管理单元)自动访问相同的物理内存页。
-
当父子进程中的任何一个尝试修改共享的内存页时,操作系统会触发写时复制机制,为修改内存页的进程创建一个新的物理内存页副本
-
