Linux内核源码分析--内存管理(二、函数实现技巧)

时间:2021-03-23 12:27:51

        仔细的分析了一下各个内存管理函数的实现,发现里面涉及到了几个技巧,如果知道了这几个技巧,那么阅读内存管理源码将会事半功倍(主要是这几个技巧在几个函数中都出现过),当然也会选择性的分析几个比较重要的函数实现;


函数实现技巧

        1、向上取整:以一个页面为了例,如果地址是1,那么向上取整就是4096;如果地址是 4095,向上取整就是4096;如果地址是4098,向上取整就是4096 x  2.......

#define CODE_SPACE(addr) ((((addr)+4095)&~4095) < \
current->start_code + current->end_code)
        这个是比较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为任意数)来验证下;

        在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表示主内存中的相对地址
this_page >>= 12; // 右移12位表示以页为单位的物理起始页
mem_map[this_page]++;// 对相应的物理页修改映射值
        其是上面的方法在最开始的地方用宏也实现过:#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)//指定内存地址映射为页号;


函数列表简介

        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)
{
//__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;
}
        5、功能:释放一个addr物理地址所在的物理页面;参数:addr,表示要所在物理页面将要被释放(确切的说是解引用),注意addr表示的是物理地址

        函数原型: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

        如果有什么不正确之处,欢迎大家指正,一起努力,共同学习!!