仔细的分析了一下各个内存管理函数的实现,发现里面涉及到了几个技巧,如果知道了这几个技巧,那么阅读内存管理源码将会事半功倍(主要是这几个技巧在几个函数中都出现过),当然也会选择性的分析几个比较重要的函数实现;
函数实现技巧
1、向上取整:以一个页面为了例,如果地址是1,那么向上取整就是4096;如果地址是 4095,向上取整就是4096;如果地址是4098,向上取整就是4096 x 2.......
#define CODE_SPACE(addr) ((((addr)+4095)&~4095) < \这个是比较addr地址是否在进程的代码段内,至于为什么要这样实现,到现在我还没弄清楚(等看完进程调度那块再到回来看看);这里用到一个技巧就是向上取整,addr是一个随机地址,要找出该地址的下一个页面的物理起始地址,那么就要向上取整了。(addr + 4095) & (~4095) :addr如果是4096的倍数,那么(addr+4095) & (~4095)结果就是addr;如果addr不是4096的倍数,那么addr以4096取余后得到的值范围为:1~4095,再加上4095得到范围:4096~4096+4094;然后&(~4095)就可以得到一个完整的4096了;其实上面的 &(~4095)就是相当于整除4096:/4096;可以使用(x + 9)/ 10,(其中x为任意数)来验证下;
current->start_code + current->end_code)
在free_page_tables()函数中:size = (size + 0x3fffff) >> 22;也是向上取整的实例;
2、获取目录项/表项的物理起始地址:一般是从线性地址中获取到目录项/表项号,然后右移2个字节(因为一个项占用4个字节)就可以得到物理起始地址,也称目录项指针;
dir = (unsigned long *) ((from>>20) & 0xffc);上面函数在free_page_tables()中(其他函数中有),from是线性地址,要得到目录项号,则:from = from >> 22(线性地址中和目录项有关的只有高10,这里就是获取到高10位的地址);注意这里获取到的仅仅只是目录项的号,而不是目录项的物理地址。
又根据一个目录项占用4个字节,那么from = from << 2(左移2位表示 乘以 2^2);所以两个合起来就干脆只右移20位得了,那么就有上面的 from >> 20了。但是这样有个问题:开始数据为1111,右移2位结果:1111 >> 2 为 0011;接着左移回2位结果:0011 << 2 为 1100,而开始的结果为1111,所以若按照这个来移动的话就会出错的。但是如果后面两位为0,则结果是正确的,相当于少移的2位是空的。
为了解决上面的问题,干脆把低2位和谐掉,因为如果11,12位是0,那倒无所谓;但如果不是0,那么结果就出错了。0xffc == 1111 1111 1100 ,或上它就是把低2位干掉(用这种方法可以验证下 1111 移动问题)。其实本来低2位就是要舍弃的,因为目录项是4字节对齐的;
同理,在其他函数中要获取页表项的物理地址方法类似:((address>>10) & 0xffc) 在write_verify()函数中有实现;
3、修改数组映射值:这个应该不是函数设计技巧,但在内存管理中却频繁出现,而且很重要。
this_page -= LOW_MEM; // 地址减去1MB表示主内存中的相对地址其是上面的方法在最开始的地方用宏也实现过:#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)//指定内存地址映射为页号;
this_page >>= 12; // 右移12位表示以页为单位的物理起始页
mem_map[this_page]++;// 对相应的物理页修改映射值
函数列表简介
1、 static inline volatile void oom(void);内敛函数,显示内存已经用完,返回码11(资源不可用);2、#define invalidate() __asm__("movl %%eax,%%cr3"::"a" (0)) 刷新页高速缓存区,其实就是重新加载下页目录起始地址,0 --->> CR3;
3、#define copy_page(from,to) __asm__("cld ; rep ; movsl"::"S" (from),"D" (to),"c" (1024):"cx","di","si") 从from拷贝一页的内容到to,其中我发现一个问题:为什么在内核的嵌入汇编中都不指定段描述符?向这里的拷贝页面内容,一样本来是从 ds:si 到 es:di的,为什么不指定ds和es段寄存器呢?其实是因为Linux系统设置的是平坦内存模式,也就是说ds和es等寄存器都为0(我是这么理解的,如果有错误,希望大家指正),32位地址,通用寄存器也是32位的,所以用通用寄存器足够表示任何一个地址了。好像在内嵌汇编中是不可以修改段寄存器的(也修改不了);
4、unsigned long get_free_page(void):得到一页空闲的物理页,因为要扫描所有的物理页,所以用内嵌汇编可以提高效率。实现函数如下:
unsigned long get_free_page(void)5、功能:释放一个addr物理地址所在的物理页面;参数:addr,表示要所在物理页面将要被释放(确切的说是解引用),注意addr表示的是物理地址
{
//__res是寄存器级变量,值保存在ax寄存器中,就是说对__res的操作等于ax寄存器的操作,为效率考虑
register unsigned long __res asm("ax");
__asm__("std ; repne ; scasb\n\t"//设置方向,从字符串尾部开始比较,al和di比较
"jne 1f\n\t"
"movb $1,1(%%edi)\n\t"//查找到最后一个空闲页面,置1表示要被引用
"sall $12,%%ecx\n\t" // 页面数算数左移12位,表示页面的基地址(主内存中)
"addl %2,%%ecx\n\t"// 在上面的基础上加上最小内存1MB,则表示实际物理内存页基地址
"movl %%ecx,%%edx\n\t"
"movl $1024,%%ecx\n\t"//下面清零循环次数
"leal 4092(%%edx),%%edi\n\t"// 把空闲页面的最后第四个字节起始地址赋值给edi,也即是最后一页开始地址
"rep ; stosl\n\t"// 因为eax=0,所以这是从4092地址开始,反向清零
"movl %%edx,%%eax\n"
"1:"
:"=a" (__res)
:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
"D" (mem_map+PAGING_PAGES-1) //表示最后一个页面的地址
:"di","cx","dx");
return __res;
}
函数原型:void free_page(unsigned long addr);
简介:释放以addr所在的一页主内存,所谓释放就是找到addr地址所在的页面号,然后对应的映射函数值减1.(因为如果是共享页面,那么映射值就是大于1的,这个函数就是仅仅解除某个进程对该页面的引用。如果映射值为1,表示只有一个进程引用,那这个就是真正彻底的释放掉这一页内存了);
6、 功能:从线性地址from所在的页目录项开始顺序释放size个连续页表及所指向的物理内存(其实就是释放size×4MB内存大小); 参数:from,线性地址;size,释放内存大小;返回:成功 0 ;出错 死机
函数原型:int free_page_tables(unsigned long from,unsigned long size);
简介:通过from线性地址获取到页目录项号----》获取到页目录项的起始地址----》根据size向上取整后右移22位得到页表个数(多少个4MB)----》根据size的个数循环遍历每个页表项---》根据每个页表项提供的内容得到页表物理地址----》用from的线性地址和页表物理地址确定一个物理页面----》调用上面的函数释放掉这个物理页面;(如果能很熟悉线性地址转换物理地址方法(如果不理解可以先看下http://blog.csdn.net/yuzhihui_no1/article/details/43021405),看这个函数就非常容易)
7、功能:根据from线性地址查找到映射的物理地址,然后把to的线性地址也映射到该物理地址页上,共享的大小为size个页表(size×4MB)。一般用于fork()创建进程使用;参数:from,源线性地址;to,目的线性地址;size,共享页面大小;返回:成功 0;出错 -1(或死机)
函数原型:int copy_page_tables(unsigned long from,unsigned long to,long size);
简介:根据from和to这两个线性地址分别得到页目录项from_dir和to_dir-----》接着根据size向上取整后右移22位得到页表个数(也即是多少个4MB)----》根据页目录项内容判断:如果目的页表存在,则死机(因为不能更改已存在的页表);如果源页表无效,则让from_dir和to_dir分别指向下一个页目录项 ----》根据页目录项内容得到页表物理地址:from_page_table,以及目的页表申请得到的页物理地址:to_page_table -----》根据申请到的页表地址和页面属性,设置页目录项内容(页目录项内容开始一定为空的,要不在第三步已经死机了) ----》 根据页面项内容开始共享页面,全部共享页面都设置为只读,并且设置映射函数mem_map[] ----》刷新页变高速缓存
8、功能:把page物理内存映射到address线性地址处;参数:page,物理页地址;address,线性地址;返回:成功 返回页物理地址,出错 0
函数原型:unsigned long put_page(unsigned long page,unsigned long address);
简介:先判断指定的物理页是否在主内存区 ----》 判断是否是共享页面(映射值为1)----》根据线性地址获取到页目录项 ----》根据页目录项内容,判断页表是否存在;存在就 获取到页表物理地址,不存在就申请一个空闲页(当然还需要根据申请到的页的物理地址和页属性设置页目录项内容) ----》根据线性地址中间页表相关10位数,定位到页表项,然后把页表项中内容设置为参数物理页地址page以及该页的属性;
9、功能:对table_entry页表项指向的物理页面取消写保护;参数:table_entry 页表项
函数原型:void un_wp_page(unsigned long * table_entry);
简介:根据参数table_entry获取到前20位页框地址(其实也就是物理页地址)----》如果该物理页在主内存中并且只被引用一次,那么直接可以把只读变成可读可写,退出 -----》根据上一步可以知道,该物理页可能在内核空间或者被多次引用,那么就在主内存中申请一个空闲页 ----》如果物理页是在主内存中,那么让映射值减去1 ----》并且把申请到页地址和页属性添加到参数table_entry中 ----》最后把旧的页面上数据拷贝到新页面上;
其实这就是写时复制的本质(当然,do_wp_page()函数才是被中断调用的写实复制处理函数),因为用fork()创建子进程时,会先调用上面copy_page_tables()函数对父进程的内存共享给子进程,同时设置内存页面为只读的。当一个进程准备往页面上写入数据时,因为写入的是只读页面,所以会产生中断,就会调用这个函数,为想写入数据的进程复制得到另外一个内存空间,并且新内存空间被设置为可写的(开始共享的页面,现在还是只读的。当下次向该页面写入数据时,发生中断再调用这个do_wp_page()函数给该页面取消写保护,而该函数其本质就是提前线性地址所在的页表项传递给un_wp_page()函数执行;
10、写页面验证,验证address线性地址所映射到的物理地址页是否可写。不可写则调用上一个函数up_wp_page()来创建新页面;参数:address验证的页面线性地址;
函数原型:void write_verify(unsigned long address);
简介:首先根据address线性地址获取到页目录项,然后依次获取到页表项,判断页表项中后面几位属性字段(倒数第一位为p位,表示是否存在内存中;倒数第二位为可写位,表示页面是否可写;如果可写什么也不做,返回;如果不可写则调用un_wp_page函数去处理);
11、功能:取一块空闲页,映射到address线性地址上;参数:address,要映射到的指定线性地址
函数原型:void get_empty_page(unsigned long address);
简介:用get_free_page()函数获取一页空闲内存,然后用put_page(tmp,address)函数把获取到的物理内存和参数address进行映射;
12、功能:把当前进程中的地址address和指定进程p中的地址address进行共享;参数:address,进程中的的地址;p,进程(也就是任务)
函数原型:static int try_to_share(unsigned long address, struct task_struct * p);
简介:这个函数有点难理解,如果你不懂进程的话,建议可以跳开,稍微浏览下进程相关概念,再到回来看会相对好理解些;首先说明下address不是线性地址,所谓线性地址就是在2^32范围内的那个地址;而这里的address是进程中的地址,每个进程都有64MB大小地址(进程中的地址都是从0到64MB的),而每个进程中的起始线性地址为:nr×64MB(其中nr是表示该进程是第几号进程),所以进程中的地址要转换成线性地址,就必须要把进程的起始地址:nr×64MB 加上;nr*64MB + address 才是我们熟悉的线性地址;
首先根据进程内地址address,获取到对应的页目录项指针 ====》 然后根据两进程(current和p)的起始地址获取到相应的页目录项指针 ====》 接着获取到两进程内地址address变换成线性地址后应该得到页目录项指针(通过前面两个页目录项指针相加) ====》 按照老规矩依次获取到p进程中address对应的页表项,并做一些必要检查(是否存在,是否干净)====》 同样的方法获取到当前进程中address对应的页表项 ====》 把p中得到的页表项设置为只读,并且把该页表项复制给当前进程的address对应的页表项;====》 最后对他们共享的页表项对应的物理页表映射值进行加1,表示又有一个进程引用了该页面(其实就是当前进程引用了)。
13、功能:这个函数是查找进程组中是否有可以共享页面的进程;参数:address,进程中的地址,用来给try_to_share()函数做参数
函数原型:static int share_page(unsigned long address);
简介:判断当前进程是否有执行的文件,如果没有,或者执行文件只有一个进程在引用(即是当前进程自己在用),那就别折腾,自己退出;====》 扫描进程组,查找引用了和当前进程一样的执行文件,那么调用上面的函数 try_to_share()进行去共享页面;
14、处理缺页中断函数;参数:error_code,出错类型;address,产生异常的页面线性地址;
函数原型:void do_no_page(unsigned long error_code,unsigned long address);
简介:说实话这个函数很难,因为涉及到进程、块设备、文件系统,所以等我看完文件系统和块设备后,要回来修改下; 首先还是老规矩获取到address的页面地址,根据当前进程的起始地址,再得到addrss在进程中对应的逻辑地址;====》 如果当前进程没有引用执行文件,或者逻辑地址超出范围,则申请一个新的物理地址,并且把address映射上去; =====》 尝试给当前进程找一个共享页面 ====》如果没成功,则申请一个物理页,从address对应的数据块号(逻辑块 在0.11中他们是一一对应的)中把数据读取到内存中;====》对超出执行文件的内容进行处理,就是全部置0;====》最后把产生缺页中的页面线性地址映射到前面获取到数据的内存处;
15、对内存初始化;参数:start_men,主内存开始的地址;end_men,内存结束位置;
函数原型:void mem_init(long start_mem, long end_mem);
简介:其实很简单,就是对1MB~16MB物理页面设置占用标记,然后对主内存中的映射值设置为0;
16、计算内存空闲页面并且显示;
函数原型:void calc_mem(void);
简介:循环统计物理内存空闲页,并打印出空闲页和总页数;====》循环统计每个页表中有多少个有效物理页;
转载请注明作者和原文出处,原文地址:http://blog.csdn.net/yuzhihui_no1/article/details/43058405
如果有什么不正确之处,欢迎大家指正,一起努力,共同学习!!