Linux 0.11-透过 fork 来看进程的内存规划-28
Linux 0.11-透过 fork 来看进程的内存规划-28
透过 fork 来看进程的内存规划
书接上回,上回书咱们说到,fork 函数为新的进程(进程 1)申请了槽位,并把全部 task_struct 结构的值都从进程零复制了过来。
之后,覆盖了新进程自己的基本信息,包括元信息和 tss 里的寄存器信息。
int copy_process(int nr, ...) {
...
p->state = TASK_UNINTERRUPTIBLE;
p->pid = last_pid;
p->counter = p->priority;
..
p->tss.edx = edx;
p->tss.ebx = ebx;
p->tss.esp = esp;
...
}
这可以说将 fork 函数的一半都讲完了,那我们今天展开讲讲另一半,也就是 copy_mem 函数。
int copy_process(int nr, ...) {
...
copy_mem(nr,p);
...
}
这将会决定进程之间的内存规划问题,十分精彩,我们开始吧。
------
整个函数不长,我们还是试着先直译一下。
int copy_mem(int nr,struct task_struct * p) {
// 局部描述符表 LDT 赋值
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);
new_code_base = nr * 0x4000000;
new_data_base = nr * 0x4000000;
set_base(p->ldt[1],new_code_base);
set_base(p->ldt[2],new_data_base);
// 拷贝页表
old_code_base = get_base(current->ldt[1]);
old_data_base = get_base(current->ldt[2]);
copy_page_tables(old_data_base,new_data_base,data_limit);
return 0;
}
看,其实就是新进程 LDT 表项的赋值,以及页表的拷贝。
LDT 的赋值
那我们先看 LDT 表项的赋值,要说明白这个赋值的意义,得先回忆一下我们在 第九回 | Intel 内存管理两板斧:分段与分页 刚设置完页表时说过的问题。
程序员给出的逻辑地址最终转化为物理地址要经过这几步骤。
而我们已经开启了分页,那么分页机制的具体转化是这样的。
因为有了页表的存在,所以多了线性地址空间的概念,即经过分段机制转化后,分页机制转化前的地址。
不考虑段限长的话,32 位的 CPU 线性地址空间应为 4G。现在只有四个页目录表,也就是将前 16M 的线性地址空间,与 16M 的物理地址空间一一对应起来了。
把这个图和全局描述符表 GDT 联系起来,这个线性地址空间,就是经过分段机制(段可能是 GDT 也可能是 LDT)后的地址,是这样对应的。
我们给进程 0 准备的 LDT 的代码段和数据段,段基址都是 0,段限长是 640K。给进程 1,也就是我们现在正在 fork 的这个进程,其代码段和数据段还没有设置。
所以第一步,局部描述符表 LDT 的赋值,就是给上图中那两个还未设置的代码段和数据段赋值。
其中段限长,就是取自进程 0 设置好的段限长,也就是 640K。
int copy_mem(int nr,struct task_struct * p) {
...
code_limit = get_limit(0x0f);
data_limit = get_limit(0x17);
...
}
而段基址有点意思,是取决于当前是几号进程,也就是 nr 的值。
int copy_mem(int nr,struct task_struct * p) {
...
new_code_base = nr * 0x4000000;
new_data_base = nr * 0x4000000;
...
}
这里的 0x4000000 等于 64M。
也就是说,今后每个进程通过段基址的手段,分别在线性地址空间中占用 64M 的空间(暂不考虑段限长),且紧挨着。
接着就把 LDT 设置进了 LDT 表里。
int copy_mem(int nr,struct task_struct * p) {
...
set_base(p->ldt[1],new_code_base);
set_base(p->ldt[2],new_data_base);
...
}
最终效果如图。
经过以上的步骤,就通过分段的方式,将进程映射到了相互隔离的线性地址空间里,这就是段式管理。
当然,Linux 0.11 不但是分段管理,也开启了分页管理,最终形成段页式的管理方式。这就涉及到下面要说的,页表的复制。
Linux 0.11中并不是一个进程独占一个4G大小的虚拟地址空间,而是由64个进程共享一个4G大小虚拟地址空间,每个进程占用其中64M大小的虚拟地址空间
页表的复制
OK,上面刚刚讲完段表的赋值,接下来就是页表的复制了,这也是 copy_mem 函数里的最后一行代码。
int copy_mem(int nr,struct task_struct * p) {
...
// old=0, new=64M, limit=640K
copy_page_tables(old_data_base,new_data_base,data_limit)
}
原来进程 0 有一个页目录表和四个页表,将线性地址空间的 0-16M 原封不动映射到了物理地址空间的 0-16M。
那么新诞生的这个进程 2,也需要一套映射关系的页表,那我们看看这些页表是怎么建立的。
/*
* Well, here is one of the most complicated functions in mm. It
* copies a range of linerar addresses by copying only the pages.
* Let's hope this is bug-free, 'cause this one I don't want to debug :-)
*/
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;
from_dir = (unsigned long *) ((from>>20) & 0xffc);
to_dir = (unsigned long *) ((to>>20) & 0xffc);
size = ((unsigned) (size+0x3fffff)) >> 22;
for( ; size-->0 ; from_dir++,to_dir++) {
if (!(1 & *from_dir))
continue;
from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
to_page_table = (unsigned long *) get_free_page()
*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;
}
先不讲这个函数,我们先看看注释。
注释是 Linus 自己写的,他说:
“这部分是内存管理中最复杂的代码,希望这段代码没有错误(bug-free),因为我实在不想调试它!”
可见这是一套让 Linus 都觉得烧脑的逻辑。
虽说代码实现很复杂,但要完成的事情确实非常简单!我想我们要是产品经理,一定会和 Linus 说这么简单的功能有啥难实现的?哈哈。
回归正题,这个函数要完成什么事情呢?
你想,现在进程 0 的线性地址空间是 0 - 64M,进程 1 的线性地址空间是 64M - 128M。我们现在要造一个进程 1 的页表,使得进程 1 和进程 0 最终被映射到的物理空间都是 0 - 64M,这样进程 1 才能顺利运行起来,不然就乱套了。
总之,最终的效果就是:
假设现在正在运行进程 0,代码中给出一个虚拟地址 0x03,由于进程 0 的 LDT 中代码段基址是 0,所以线性地址也是 0x03,最终由进程 0 页表映射到物理地址 0x03 处。
假设现在正在运行进程 1,代码中给出一个虚拟地址 0x03,由于进程 1 的 LDT 中代码段基址是 64M,所以线性地址是 64M + 3,最终由进程 1 页表映射到物理地址也同样是 0x03 处。
即,进程 0 和进程 1 目前共同映射物理内存的前 640K 的空间。
至于如何将不同地址通过不同页表映射到相同物理地址空间,很简单,举个刚刚的例子。
刚刚的进程 1 的线性地址 64M + 0x03 用二进制表示是:
0000010000_0000000000_000000000011
刚刚的进程 0 的线性地址 0x03 用二进制表示是:
0000000000_0000000000_000000000011
根据分页机制的转化规则,前 10 位表示页目录项,中间 10 位表示页表项,后 12 位表页内偏移。
进程 1 要找的是页目录项 16 中的第 0 号页表
进程 0 要找的是页目录项 0 中的第 0 号页表
那只要让这俩最终找到的两个页表里的数据一模一样即可。
由于理解起来非常简单,但代码中的计算就非常绕,所以我们就不细致分析代码了,只要理解其最终的作用就好。
------
OK,本章的内容就讲完了,再稍稍展开一个未来要说的东西。还记得页表的结构吧?
其中 RW 位表示读写状态,0 表示只读(或可执行),1表示可读写(或可执行)。当然,在内核态也就是 0 特权级时,这个标志位是没用的。
那我们看下面的代码。
int copy_page_tables(unsigned long from,unsigned long to,long size) {
...
for( ; size-->0 ; from_dir++,to_dir++) {
...
for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
...
this_page &= ~2;
...
if (this_page > LOW_MEM) {
*from_page_table = this_page;
...
}
}
}
...
}
~2 表示取反,2 用二进制表示是 10,取反就是 01,其目的是把 this_page 也就是当前的页表的 RW 位置零,也就是是把该页变成只读。
而 *from_page_table = this_page 表示又把源页表也变成只读。
也就是说,经过 fork 创建出的新进程,其页表项都是只读的,而且导致源进程的页表项也变成了只读。
这个就是写时复制的基础,新老进程一开始共享同一个物理内存空间,如果只有读,那就相安无事,但如果任何一方有写操作,由于页面是只读的,将触发缺页中断,然后就会分配一块新的物理内存给产生写操作的那个进程,此时这一块内存就不再共享了。
这是后话了,这里先埋个伏笔。
------
好了,至此 fork 中的 copy_process 函数就全部被我们读完了,总共做了三件事,把整个进程的数据结构个性化地从进程 0 复制给了进程 1。
第一,原封不动复制了一下 task_struct。
第二,LDT 的复制和改造,使得进程 0 和进程 1 分别映射到了不同的线性地址空间。
第三,页表的复制,使得进程 0 和进程 1 又从不同的线性地址空间,被映射到了相同的物理地址空间。
最后,将新老进程的页表都变成只读状态,为后面写时复制的缺页中断做准备。
欲知后事如何,且听下回分解。
转载
相关文章
- Linux查看内存:最简洁方式来监控资源(linux查看内存值)
- Linux实现快速文件字符串搜索(linux在文件中查找字符串)
- 内存Linux查看主机CPU内存信息指南(linux查看主机cpu)
- 深入了解Linux内存占用情况(linux查询内存占用)
- 探索Linux下查看内存型号的方法(linux查看内存型号)
- Linux下如何限制用户内存使用(linux限制用户内存)
- Linux系统中如何管理日志文件(linux日志文件)
- Linux文件内存管理技术研究(linux把文件内存)
- 错误分析Linux中内存段错误的原因(linux内存段)
- Linux精细化内存操作指南(linux内存操作)
- Linux中的内存拷贝函数:memcpy(linux内存拷贝函数)
- Linux内存管理:堆栈和堆空间的使用(堆栈或堆空间linux)
- 实现双系统:Windows 10和Linux共存(win10装linux)
- Linux共享中断实现原理(linux共享中断)
- 深入浅出:Linux内存占用优化技巧(linux内存占用)
- 编程Linux下的单片机编程技术突破(linux下的单片机)
- Linux支持LCD驱动: 让你畅享可视化环境(lcd驱动linux)
- 内存Linux下查看、使用共享内存(linux查看共享)
- 学习Linux 视频编程,抓住未来发展机遇(linux视频编程)
- 查看Linux内存使用情况(查看内存linux)
- 快速重命名文件:Linux批量重命名的简便方法(批量重命名linux)
- 【工具推荐】Linux 内存监控必备!25字助你掌握内存情况(linux内存监控工具)
- 如何在Linux上启动HBase?(linux启动hbase)
- 内存管理理解Linux中Swap内存管理有多重要(linux中swap)
- Linux内存管理之交换区技术研究(linux内存交换区)
- Linux系统内存管理析:实现高效与可靠性(linux 内存管理分析)
- 如何在Linux系统上编程高效运行(怎么用linux编程)
- Linux下查看线程与内存的实用方法(linux查看线程内存)
- Linux下查看进程内存使用情况(linux查看进程内存占用)
- Linux查看服务器IP地址的简易方法(linux查看服务器ip)