源码下载
Linux 源码下载路径位于 https://mirrors.edge.kernel.org/pub/linux/kernel/,这篇博客所需要的 0.10 版本源码通过点击链接 https://mirrors.edge.kernel.org/pub/linux/kernel/Historic/old-versions/linux-0.11.tar.gz 可下载。
对比 Linux 0.10 版本 变化
0.11 版的内存管理最大和 0.10 版本一样,都是 16MB,这两个版本最大的变化在于 0.11 版本增加了页面共享和RAMDISK功能。RAMDISK 是将一部分内存作为磁盘使用,它导致内存模型变化如下:

图中的虚拟盘就是 RAMDISK 了。这部分不具体介绍,涉及文件系统内容。下面介绍页面共享内容。
页面共享
页面共享功能主要关注 mm/memory.c 中的几个函数:try_to_share、share_page 和 do_no_page。其中前两个函数是 0.11 版本新增的函数,do_no_page 函数 0.10 版本已经存在(0.01 版本已存在),但是实现很简单:当缺页时,分配新页。但如果这个进程是被 fork 出来的,也许可以与父进程进行页面共享,如果父进程的该页面存在并且不脏。当然父进程缺页时也可以和子进程共享。下面具体看下这三个函数,try_to_share 是为 share_page 服务的,share_page 是为了 do_no_page 服务的,接下来介绍这三个函数。
try_to_share
这个函数有两个参数,第一个 address 是逻辑地址,是想要共享页面中的地址,第二个参数 p 是当前任务(task)想要与之共享页面的任务。调用该函数的函数保证了 p != current。
/*
* try_to_share() checks the page at address "address" in the task "p",
* to see if it exists, and if it is clean. If so, share it with the current
* task.
*
* NOTE! This assumes we have checked that p != current, and that they
* share the same executable.
*/
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); ----------------- (1)
from_page += ((p->start_code>>20) & 0xffc);
to_page += ((current->start_code>>20) & 0xffc);
/* is there a page-directory at from? */
from = *(unsigned long *) from_page; ----------------- (2)
if (!(from & 1))
return 0;
from &= 0xfffff000; ----------------- (3)
from_page = from + ((address>>10) & 0xffc);
phys_addr = *(unsigned long *) from_page;
/* is the page clean and present? */
if ((phys_addr & 0x41) != 0x01)
return 0;
phys_addr &= 0xfffff000;
if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM) ---------------- (4)
return 0;
to = *(unsigned long *) to_page; ---------------- (5)
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");
/* share them: write-protect */
*(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;
}
-
要理解这三行代码,得要理解什么是逻辑地址。逻辑地址是段内偏移值(虚拟地址是段选择符和段内偏移值),所以第 17 行代码表示
address映射的线性地址在页目录表中对应项的地址与起始代码段在页目录表对应项地址的偏移,第 18 行中的start_code是代码段的线性地址,在 0.1x 版本中,这个值是 64MB 的整数倍。因此在这三行代码之后,from_page和to_page表示address在页目录表中的地址。这个计算方法值得记好,可能以后的代码中还有一样的做法,那就可以很快理解代码了。 -
查看该页面存不存在,由于处理器不会修改页目录项中的 D 标志(当处理器对一个页面执行写操作时,就会设置对应页表表项的 D 标志,表示 Dirty),所以不检查 D 标志位。如果页面不存在,那当然就不会有接下来的共享操作啦。
-
接下来的几行取得页表项中的内容,也就是页帧基地址,但是这个地址低 12 位是属性值:

由图可知,第 6 位表示 D 标志,第 0 位是 P 标志,所以第 28 行判断这一页存不存在且是不是干净的,存在且干净则继续往下走,否则返回。 -
在这里,
phys_addr已经是页帧基地址了。这个地址当然要在主内存页的范围了,这里LOW_MEM可能并不是主内存区的最低端,在init/main.c中有个变量main_memory_start才是,所以这里比较的范围还是宽了些。 -
看过我Linux 0.01 内存管理这篇文章并理解了的同学对这几行肯定很快可以理解了,这里的
to_page是页目录表表项的地址,判断页表存不存在,不存在的话就分配一页,并将分配得到的物理地址或上 7 设置属性;页表本身就存在的话那就接着查看页面存不存在,页面存在的话还共享个啥,都已经存在了,居然还跑来共享,panic~否则就清除页面的写属性(与上 ~2),改成页面只读,因为页表有效项属性改了,那就需要调用invalidate刷新页表嘛,最后呢,更新下mem_map表咯,将对应物理页的引用数加 1。
share_page
share_page 这个函数的参数也是一个逻辑地址,它的作用是找到一个任务,这个任务的可执行文件和当前任务必须得是一样的,这样它们才能共享页面(逻辑地址都一样,逻辑地址是相对于代码段的偏移),找到这个任务了,那就尝试共享页面,共享不到,那就接着找下一个任务。
/*
* share_page() tries to find a process that could share a page with
* the current one. Address is the address of the wanted page relative
* to the current data space.
*
* We first check if it is at all feasible by checking executable->i_count.
* It should be >1 if there are other tasks sharing this inode.
*/
static int share_page(unsigned long address)
{
struct task_struct ** p;
if (!current->executable) -------------- (1)
return 0;
if (current->executable->i_count < 2) -------------- (2)
return 0;
for (p = &LAST_TASK ; p > &FIRST_TASK ; --p) { -------------- (3)
if (!*p)
continue;
if (current == *p)
continue;
if ((*p)->executable != current->executable)
continue;
if (try_to_share(address,*p))
return 1;
}
return 0;
}
-
关于这一行代码,判断当前进程有没有可执行文件。为什么会有这种情况呢?一是为了代码严谨性,在第 15 行代码要访问这个指针,当然要对其项判空啦,第二是确实存在没有可执行文件的情况,譬如任务 0,直接由内核创建,在内核中运行,没有文件背景。具体看下面
task_struct中关于struct m_inode *executable的解释:

这个图来自于 《Linux内核完全解释 内核版本0.11》修正版 V3.0。 -
关于
i_count,看上图。如果小于 2,即为 1,不能小于 1,表示当前系统中执行该文件的进程只有当前进程,因此肯定没有啥可共享的。 -
找合格的任务来尝试共享。
do_no_page
这个函数变得有点复杂,它的参数和之前的内核版本一样,一个处理器传过来的错误码,还有一个出错的地址,这个地址不同于前面两个函数,它是线性地址。
void do_no_page(unsigned long error_code,unsigned long address)
{
int nr[4];
unsigned long tmp;
unsigned long page;
int block,i;
address &= 0xfffff000; --------------- (1)
tmp = address - current->start_code;
if (!current->executable || tmp >= current->end_data) { --------------- (2)
get_empty_page(address);
return;
}
if (share_page(tmp)) --------------- (3)
return;
if (!(page = get_free_page())) --------------- (4)
oom();
/* remember that 1 block is used for header */
block = 1 + tmp/BLOCK_SIZE; --------------- (5)
for (i=0 ; i<4 ; block++,i++)
nr[i] = bmap(current->executable,block);
bread_page(page,current->executable->i_dev,nr);
i = tmp + 4096 - current->end_data; --------------- (6)
tmp = page + 4096; --------------- (7)
while (i-- > 0) {
tmp--;
*(char *)tmp = 0;
}
if (put_page(page,address)) --------------- (8)
return;
free_page(page);
oom();
}
- 取得所在页面基地址,并转化为逻辑地址。这一步要理解记住,对看内核内存管理代码应该挺有帮助的。
- 没有文件背景的任务可以看下
share_page函数中的那张图了解下,对于这种缺页当然是直接分配页啦;如果该逻辑地址超过了当前数据段,那就是数据段扩展(理应应该有段限长超出保护的),这种也没有共享,那就直接分配空页面(因为是超过数据段,那就是要写数据,全新地空页面)。 - 到了这里,可以肯定的是,这个任务是有文件背景地,
tmp这个地址也在代码段和数据段范围内。那就尝试share_page,成功则返回 1。 - 到了这里,那就是没有共享内存,但是这里是有文件背景地,所以要将执行文件对应的页面映射到
tmp处,这些动作在之前版本的内核里都没有做,单纯的分配了空页,可能调用函数有做了吧(应该吧。。。)。 - 先看下下面这张图,它是可执行文件在磁盘中的简单图示:

程序头在磁盘中占 1 个 block,tmp / BLOCK_SIZE得出tmp所占的 block 数(当然要加上程序头占的 1 块)。要了解每个 block 占 1K,由于我们要取一页,所以有 4 个 block,在函数前面定义了一个数组变量int nr[4];,这 4 个元素的数组就是用来存储 4 个逻辑块号的。根据 inode 求逻辑块号用函数bmap,这个函数具体在这里不说,等到文件系统再看。根据逻辑块号读页面的函数是bread_page,同样在这里不看这个函数的细节。 - 到了这一步,
page中包含了缺的那一页内容。task_struct中end_data成员是指代码段和数据段的长度,单位是字节,tmp是 4K 对齐的的(start_code是 64MB 的整数倍)。由于取一整页内容可能已经超过end_data了,这里的做法是求出可能超过end_data的长度,然后将里面的内容置为 0。第 23 行就是求可能超出的长度,其示意图如下:

如图,超出部分就是 t m p + 4096 − e n d _ d a t a tmp + 4096 - end\_data tmp+4096−end_data。 - 超出部分清零。
- 将页面
page映射到线性地址address上。put_page函数见这里。
页面映射讨论到此结束,如果大家发现哪里有错误,请不吝赐教,当然有相关问题,可以评论区见~
本文详细解析了 Linux 0.11 版本中引入的页面共享功能和 RAMDISK 的工作原理。页面共享通过 try_to_share 和 share_page 函数实现,允许父子进程间共享相同的页面,提高内存利用率。RAMDISK 则将部分内存作为磁盘使用,改变了内存模型。

886

被折叠的 条评论
为什么被折叠?



