http://blog.csdn.net/astonqa/article/details/7598548
http://oss.org.cn/kernel-book/ch02/2.4.1.htm
看linux0.11的源码有一段时间了,发现前期的轮廓建立起来后,重点马上到了具体操作上。即函数,毕竟OS本身是由一系列函数组成的,“源码面前了无秘密”,所以要深刻理解操作系统的神奇,深入理解每一个函数的每一行代码很是关键。
接下来一段时间,会随着学习的步骤,参看赵炯博士的内核注释和网上其他达人的点评注解,以每个函数为题目进行一个个人的注解。姑且厚颜算作原创吧,不为其他,只为记录下学习印记,加深印象而已。今天是第一个函数,linux/mm/memory.c/free_page_tables()
/*
* This function frees a continuos block of page tables, as needed
* by 'exit()'. As does copy_page_tables(), this handles only 4Mb blocks.
*/
int free_page_tables(unsigned long from,unsigned long size)
{
unsigned long *pg_table;
unsigned long * dir, nr;
// 这里的from是线性地址,需4MB对齐。因为free_page_tables函数用来释放一个页表的内存
// 而不是一个页面的内存。
if (from & 0x3fffff)
panic("free_page_tables called with wrong alignment");
if (!from)
panic("Trying to free up swapper memory space");
// size在计算前是字节为单位的需要释放的内存长度。而在右移22位并向上取整计算后则得到了
// 需要释放的内存页面的个数,即以4MB为单位的需要释放的内存页面个数。这里+0x3fffff是为了
// 向上取整,譬如4.01MB经过计算后也会得到2(4MB则刚好得到1)
size = (size + 0x3fffff) >> 22;
// dir意义为from地址对应的这个页表在页目录表中的表项首地址。(页目录表位于物理内存0x0000处,
// 占有1页(4k)内存,共有1K个页表项,每个页表项4字节。每个页表项的4字节作为一个指针又都指向
// 一个内存地址,这个地址就是一个页表的基地址。因此共有1K个页表,每个页表对应4M内存,因此
// linux0.11共支持4G的线性地址范围。这4G线性地址范围被64个进程瓜分,因此每个进程的独立
// 地址范围为4G/64 = 64M,这也就是每个进程最大线性地址的限制来历了。)
// 这里from为线性地址,因此可根据线性地址空间与页目录表中表项的对应关系来计算这个from
// 对应在哪个页表项中。from>>22即为对应的页表项号,但是每个页表项占4字节,因此要得到页表项
// 在页目录表内的首地址(注意区分页表项号与页表项号首地址),则要(from>>22)<<2 = from>>20
// & 0xffc是为了确保这个地址一定是页表项的首地址,而不会跑到4字节的中间某个字节去。实际上
// 在函数刚开始已经检验过from了,能执行到这里即说明from已经是4MB对齐了。但是linus还是执着
// 的进行了这个检测,可见linus编程风格之严谨。我能想到的这句代码的作用就是:在函数首部检验
// 代码和这句代码之间,若维护时不小心改变了from的值使其失去了4MB对齐的特性,这里又没检测
// 则会造成很大的崩溃。
dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 ,0.页目录表位于物理内存0x0000处 1.线性 地址应该右移 22 位才能得出高10位的页目录中 目录项索引. 2. 因为一个目录项占用4个字节,所以在取目录项内容(也即页表基地址)时要把目录项索引*4. 也就是
(address>>22)*4.*/
for ( ; size-->0 ; dir++) {
// dir为ulong型指针,占4个字节(即from对应的页目录表项首地址),指向一个ulong型数据,即
// 这个页目录表基地址。*dir即为这个页目录表项内容,也就是这个页目录表的基地址。此处检验
// 其最低位是不是1,查页目录表项结构即可知,最低位为P(Present,1表示这个表项可做映射,0
// 表示这个表项不可做映射),因此这个if是检验from要求释放的页表是不是有效页表。
if (!(1 & *dir))
continue;
// pg_table即为第一个要释放的页表的基地址指针,这里与0xfffff位与也是为了对齐验证
pg_table = (unsigned long *) (0xfffff000 & *dir);
// 找到了要释放的页表基地址,下面开始循环释放这个页表中1024个页面。调用free_page函数
// 实现释放一个物理页面。这里要注意free_page函数接收的addr为物理地址,而不是线性地址。
// 上面一直都在用线性地址,到了这里突然就成了物理地址,这里要注意的就是这个转折点了。
// 经过分析得知,从线性地址到物理地址的转折出现在从dir到*dir。也就是用线性地址得到dir,
// 然后*dir取出的却是物理地址,即页目录表中存的页表地址都是物理地址。整个地址转换系统中,
// 用户进程内的虚拟地址(逻辑地址)先经过段式转换到线性地址,然后页目录级使用线性地址,到了
// 页表级和页内都使用物理地址了。
// 这里也可以看出,一个进程初步建立时,至少要耗费2个page的主内存。第一个用来存task_struct
// 结构和内核栈;第二个即用来存该进程第一个页表。
// 这里我突然有个疑问:在这之后execve等耗费的内存可以由这个已经建立的页表来管理,那么这前两个
// 页面的内存由谁来管理呢?换句话说,这两个页面的物理内存由谁来释放呢?这里第一个页表所在page
// 的释放在下面就能找到答案,原来释free_page_tables函数中在释放完了一个页表中1024个页面后会
// 顺带释放这个页表本身占有的这个内存页面。而task_struct和内核栈共同占有的内存页的释放,
// 猜测只能由其父进程来代为释放了···具体如何,待继续寻找。
for (nr=0 ; nr<1024 ; nr++) {
if (1 & *pg_table)
// 调用free_page释放一个物理页面,释放的意思是在内核mem_map[]中对应该页
// 的项内数值减一。若减一后为0则代表该页内存成了空闲内存,若不为0则表示
// 该页内存还被别的进程引用,只是本进程对它的引用被释放断开了。
free_page(0xfffff000 & *pg_table);
// 页表中表项清零。因为这个表项指向的内存页面已经被释放了,与本进程无关了,因此
// 这里页表也再不用记录它了,直接清零即可。
*pg_table = 0;
pg_table++;
}
// 释放完第一个页表指向的1024个页面后,最后释放掉这个页表本身所在的那个页面。
free_page(0xfffff000 & *dir);
// 释放了页表所在页面,将该页表在页目录表中对应的页表项内容也清零
*dir = 0;
}
// 刷新页变换高速缓存,进入invalidate函数可知,这里是通过重新给CR3寄存器赋值来刷新的。
// 这里的页表换高速缓存是存在于CPU内部的,应该跟MMU有些关联。这一块暂时没搞明白···
// 现在能确定的是,free_page_tables是给exit函数调用的,因此是关闭进程时使用的。而关闭进程
// 必然导致CPU会访问的页面的变化,因此在这里进行了刷新
invalidate();
return 0;
}
===================================================================================================================
Linux0.11针对的内存是16M。采用了两级分页机制进行内存的管理。
根据head.s中第114行的.org 0x1000可知,物理地址0x1000之前的所有数据都将被页目录表覆盖(这个覆盖,是指更改了内存中的内核镜像文件,而不是磁盘上的内核镜像文件)。
1、首先,Linux从0x00000地址开始对五页内存进行清零。(1页页目录表+4页页表)
1 | setup_paging: |
2 | movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */ |
3 | xorl %eax,%eax |
4 | xorl %edi,%edi /* pg_dir is at 0x000 */ |
5 | cld;rep;stosl |
2、接着,填写页目录表(页目录表的位置为0x00000-0x00fff,大小为4K,每一项占4字节)。因为只有4个页表,所以只填写了前四项。
1 movl $pg0+7,pg_dir /* set present bit/user r/w */ |
2 movl $pg1+7,pg_dir+4 /* --------- " " --------- */ |
3 movl $pg2+7,pg_dir+8 /* --------- " " --------- */ |
4 movl $pg3+7,pg_dir+12 /* --------- " " --------- */ |
这里,4个页目录项的内容分别是$pg0(1,2,3)+7,分别是4个页表的物理地址+111B。前面的$pg0(1,2,3)是页表的物理地址,而111B则代表这4个页表权限为可读写。
3、填写页表项的内容
1 movl $pg3+4092,%edi |
2 movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */ |
3 std |
4 1: stosl /* fill pages backwards - more efficient :-) */ |
5 subl $0x1000,%eax |
6 jge 1b |
这里的填写是逆序填写的,也就是首先将16M物理内存最后一页的启始地址+权限(16M-4K+111B)填写到第4张页表的最后一项。地址为$pg3+4092,其中$pg3为(第4张页表的起始地址),4092是因为1024*4(1024项,每项占用4字节)-4(最后一项页占用4字节)。所以第4张页表的最后一项是(16M-4K)0xfff000+111B=0xfff007。
最终,std以4递减edi寄存器(一个页表项占4字节,edi指向正在操作的页表项),subl $0x1000,%eax将减去0x1000(一页内存的大小,eax指向正在操作的内存边界),l:stosl是填写页表项,知道eax的内容为0,这样就填写完了4个页表。
这样,内存中的页目录表和页表分布就是:
物理内存地址 | 所含信息及备注 | 单元内容 |
0X00FF FFFC …… | …… …… | |
0X0000 4FFC 0X0000 4FF8 … … 0X0000 4004 0X0000 4000 | 页表4 4K (0X00004000-0X00004FFC) 4字节为一项 | 0X00FF F000+7 0X00FF E000+7 … … 0X00C0 2000+7 0X00C0 1000+7 0X00C0 0000+7 |
0X0000 3FFC 0X0000 3FF8 … … 0X0000 3004 0X0000 3000 | 页表3 4K (0X00003000-0X00003FFC) 4字节为一项 | 0X00BF F000+7 0X00BF E000+7 … … 0X0080 2000+7 0X0080 1000+7 0X0080 0000+7 |
0X0000 2FFC 0X0000 2FF8 … … 0X0000 2004 0X0000 2000 | 页表2 4K (0X00002000-0X00002FFC) 4字节为一项 | 0X007F F000+7 0X007F E000+7 … … 0X0040 2000+7 0X0040 1000+7 0X0040 0000+7 |
0X0000 1FFC 0X0000 1FF8 … … 0X0000 1004 0X0000 1000 | 页表1 4K (0X00001000-0X00001FFC) 4字节为一项 | 0X003F F000+7 0X003F E000+7 … … 0X0000 2000+7 0X0000 1000+7 0X0000 0000+7 |
0X0000 0FFC 0X0000 0FF8 … … 0X0000 0004 0X0000 0000 | PDT(页目录表) 4K (0X000000-0X00000FFF) 只有前四项有内容 | … 0x0000 4000+7 0x0000 3000+7 0x0000 2000+7 0x0000 1000+7 |
最后,把页目录表的地址(0x000000)写到控制寄存器CR3,然后置控制寄存器CR0的PG位,这样就开启了内存的分页管理功能。
Linux0.11在分页机制下的寻址(两级表寻址)
第一级表称为页目录。
存放在1页4k页面中。具有1k个4字节长度的表项。这些表项指向第二级表。线性地址的最高10位(位31-22)用作一级表(页目录)中的索引值来选择某个页目录项,用以选择某个二级表。
第二级表称为页表。
长度也是1个页面,每个表含有1k个4字节的表项。每个4字节表项含有相关页面的20位物理地址。二级页表使用线性地址中间的10位(位21-12)作为表项索引值,在表内索引含有页面20位物理地址的表项。该20位页面物理地址和线性地址中的低12位(页内偏移)组合在一起就得到了分页转换中的输出值,也就是最终的物理地址。
也就是说:
线性地址高10位---------索引页目录表----------->找到相应页表
线性地址中间10位---------索引页表----------->得到页表中相应的项,其中的高20位就是物理地址的高20位
线性地址低12位-------------------->物理地址的低12位
下面举两个例子说明Linux0.11的分页寻址。
1、寻址0x38
首先写成32位地址为0x0000 0038。取最高10位,0000 0000 00B,索引页目录项。这里找到页目录表的第0项,内容为$pg0+7=0x0000 1007。取线性地址的中间10位,00 0000 0000B,索引页表1(pg0)中的第0项,内容为
0x0000 0000+7,取他的高20位0x0000 0加上线性地址的低12位0x038就得到最终的物理地址0x0000 0038
2、寻址0x00f5 9f50
首先取最高10位,0000 0000 11,索引页目录表。这里为3就找到页目录表的第3项,指向页表4,内容为$pg3+7=0x0000 4000+7。取线性地址的中间10位,11 0101 1001B,索引页表4(pg3)中的0x359项,内容为0x00f5 9000+7。取其高20位0x00f5 9000加上线性地址的低12位0xf50就得到最终的物理地址0x00f5 9f50