Chinaunix首页 | 论坛 | 博客
  • 博客访问: 58959
  • 博文数量: 42
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 78
  • 用 户 组: 普通用户
  • 注册时间: 2013-04-03 11:19
文章分类
文章存档

2014年(4)

2013年(38)

我的朋友

分类: LINUX

2013-04-23 09:25:09

 
Linux下的内存管理
本文系转载自
2006年04月28日 09:28 来源:ChinaUnix博客 作者:Leaf_KA 编辑:周荣茂

    Linux0.11 下的内存管理

    作者:袁镱

    [email=robertyi@163.com]robertyi@163.com[/email]

    QQ:30131195

    愿借此结交广大linux 爱好者

    1 如何在保护模式下实现对物理内存的管理

    保护模式在硬件上为实现虚拟存储创造了条件,但是内存的管理还是要由软件来做。操作系统作为

    资源的管理者,当然要对内存的管理就要由他来做了。

    在386 保护模式下,对任何一个物理地址的访问都要通过页目录表和页表的映射机制来间接访问,

    而程序提供的任何地址信息都会被当成线性地址进行映射,这就使得地址提供者不知道他所提供的线性

    地址最后被映射到了哪个具体的物理地址单元。这样的措施使得用户程序不能随意地操作物理内存,提

    高了系统的安全性,但是也给操作系统管理物理内存造成了障碍。而操作系统必须要了解物理内存的使

    用情况才谈得上管理。

    要能够在保护模式下感知物理内存,也就是说要能够避开保护模式下线性地址的影响,直接对物理

    内存进行操作。如何避开呢?正如前面所说:在保护模式下对任何一个物理地址的访问都要通过对线性

    地址的映射来实现。

    不可能绕过这个映射机制,那只有让他对内核失效。如果让内核使用的线性地址和物理地址重合,比如:

    当内核使用0x0000 1000 这个线性地址时访问到的就是物理内存中的0x00001000 单元。问题不就解决

    了吗!linux0.11 中采用的正是这种方法。

    在进入保护模式之前,要初始化页目录表和页表,以供在切换到保护模式之后使用,要实现内核线

    性地址和物理地址的重合,必须要在这个时候在页目录表和页表上做文章。

    在看代码之前首先说明几点:

    由于linus 当时编写程序时使用的机器只有16M 的内存,所以程序中也只处理了16M 物理内存的

    情况,而且只考虑了4G 线性空间的情况。一个页表可以寻址4M 的物理空间,所以只需要4 个页表,

    一个页目录表可以寻址4G 的线性空间,所以只需要1 个页目录表。

    程序将页目录表放在物理地址_pg_dir=0x0000 处,4 个页表分别放在pg0=0x1000, pg1=0x2000,

    pg2=0x3000, pg3=0x4000 处

    下面是最核心的几行代码:在linux/boot/head.s 中

    首先对5 页内存清零

    198 setup_paging:

    199 movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */

    #设置填充次数ecx=1024*5

    200 xorl %eax,%eax #设置填充到内存单元中的数eax=0

    201 xorl %edi,%edi /* pg_dir is at 0x000 */

    #设置填充的起始地址0,也是页目录表的起始位置

    202 cld;rep;stosl

    下面填写页目录表的页目录项

    对于4 个页目录项,将属性设置为用户可读写,存在于物理内存,所以页目录项的低12 位是0000 0000

    0111B

    以第一个页目录项为例,$ pg0+7=0x0000 1007

    表示第一个页表的物理地址是0x0000 1007&0xffff f000=0x0000 1000;

    权限是0x0000 1007&0x0000 0fff=0x0000 0007

    203 movl $pg0+7,_pg_dir /* set present bit/user r/w */

    204 movl $pg1+7,_pg_dir+4 /* --------- " " --------- */

    205 movl $pg2+7,_pg_dir+8 /* --------- " " --------- */

    206 movl $pg3+7,_pg_dir+12 /* --------- " " --------- */

    接着便是对页表的设置:

    4 个页表×1024 个页表项×每个页表项寻址4K 物理空间:4*1024*4*1024=16M

    每个页表项的内容是:当前项所映射的物理内存地址 + 该页的权限

    其中该页的属性仍然是用户可读写,存在于物理内存,即0x0000 0007

    具体的操作是从16M 物理空间的最后一个页面开始逆序填写页表项:

    最后一个页面的起始物理地址是0x0xfff000,加上权限位便是0x fff007,以后每减0x1000(一个页面

    的大小)便是下一个要填写的页表项的内容。

    207 movl $pg3+4092,%edi # edi 指向第四个页表的最后一项4096-4。

    208 movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */

    #把第四个页表的最后一项的内容放进eax

    209 std # 置方向位,edi 值以4 字节的速度递减。

    210 1: stosl /* fill pages backwards - more efficient :-) */

    211 subl $0x1000,%eax # 每填写好一项,物理地址值减0x1000。

    212 jge 1b # 如果eax 小于0 则说明全填写好了。

    # 使页目录表基址寄存器cr3 指向页目录表。

    213 xorl %eax,%eax /* pg_dir is at 0x0000 */

    令eax=0x0000 0000(页目录表基址)

    214 movl %eax,%cr3 /* cr3 - page directory start */

    # 设置cr0 的PG 标志(位31),启动保护模式

    215 movl %cr0,%eax

    216 orl $0x80000000,%eax # 添上PG 标志位。

    217 movl %eax,%cr0 /* set paging (PG) bit */

    在分析完这段代码之后,应该对初始化后的页目录表和页表有了一个大概的了解了,当这段代

    码运行完后内存中的映射关系应该如图所示:

    接下来将内核代码段描述符gdt 设置为

    0x00c09a0000000fff /* 16Mb */ # 代码段最大长度16M。

    这样线性地址就和物理地址重合了。

    下面用两个例子验证一下:

    (1) 要寻找pg_dir 的第15 项的内容

    这个地址应该是在页目录表的(15-1)*4=0x38 位置,把它写成32 为地址使0x0000 0038,当内

    核使用这个地址时,仍然要通过映射:首先取高10 位,0000 0000 00B,根据203 行的代码,

    页目录表第0 项的内容是$pg0+7,得到页表地址是pg0=0x0000 1000,CPU 将用这个地址加上偏

    移量找到对应的页表项,偏移量=线性地址中间10 位*4=0,根据203~221 行执行的结果,

    在pg0 中偏移量为0 的页表项为0x0000 0007, CPU 得到页表地址是0x0000 0000 加上线性地址

    的最后12 位,将找到0x0000 0038 单元的内容。

    (2)寻找任意物理单元0x00f5 9f50

    与第一个例子一样,用这个地址作为线性地址寻址,先用高10 位寻找页表,页目录表第0000

    0000 11B 项指向pg3,根据线性地址中间10 位11 0101 1001B 寻找页表项,pg3 的第11 0101

    1001B 应该是0x00f5 9007,

    取得页表基址0x00f5 9000,加上页内偏移量0x f50,最后得到的就是物理地址0x00f5 9f50 的

    内容。

    从上面两个例子可以看出:内核中使用的线性地址实际上已经是物理地址,这样从现象上

    看386 的地址映射机制对内核失效了:-)

    明白了这一点之后,对后面内存管理方面的的分析就容易得多了

    2 内存初始化

    当操作系统启动前期实现对于物理内存感知之后,接下来要做的就是对物理内存的管理,要合理的

    使用。对于Linux 这样一个操作系统而言,内存有以下一些使用:面向进程,要分配给进程用于执行所必

    要的内存空间;面向文件系统,要为文件缓冲机制提供缓冲区,同时也要为虚拟盘机制提供必要的空

    间。这三种对于内存的使用相对独立,要实现这一些,就决定了物理内存在使用时需要进行划分,而最

    简单的方式就是分块,将内存划分为不同的块,各个块之间各司其职,互不干扰。linux0.11 中就是这样

    作的。

    Linux0.11 将内存分为内核程序、高速缓冲、虚拟盘、主内存四个部分(黑色部分是页目录表、几个

    页表,全局描述符表,局部描述符表。一般将他们看作内核的一部分)。为什么要为内核程序单独划出一

    个块来呢?主要是为了实现上简单。操作系统作为整个计算机资源的管理者,内核程序起着主要的作

    用,它的代码在操作系统运行时会经常被调用,需要常驻内存。所以将这部分代码与一般进程所使用的

    空间区分开,为他们专门化出一块内存区域。专门划出一块区域还有一个好处,对于内核程序来说, 对

    于自己的的管理就简单了,内核不用对自己代码进行管理。比如:当内核要执行一个系统调用时,发现

    相应的代码没有在内存,就必须调用相关的内核代码去将这个系统调用的代码加载到内存,在这个过程

    中,有可能出现再次被调用的相关内核代码不在内存中的情况,最后就可能会导致系统崩溃。操作系统

    为了避免这种情况,在内核的设计上就变得复杂了。如果将内核代码专门划一个块出来,将内核代码全

    部载入这个块保护起来,就不会出现上面讲的情况了。

    在linux0.11 中内存管理主要是对主内存块的管理。

    要实现对于这一块的管理,内核就必须对这一块中的每一个物理页面的状态很清楚。一个物理页面

    应该有以下基本情况:是否被分配,对于它的存取权限(可读、可写),是否被访问过, 是否被写过,被

    多少个不同对象使用。对于linux0.11 来说,后面几个情况可以通过物理页面的页表项的D、A、XW 三项

    得到,所以对于是否被分配,被多少个对象使用就必须要由内核建立相关数据结构来记录。在linux0.11

    定义了一个字符数组mem_map [ PAGING_PAGES ] 用于对主内存区的页面分配和共享信息进行记录。

    以下代码均在/mm/memory.c 中

    43 #define LOW_MEM 0x100000 // 主内存块可能的最低端(1MB)。

    44 #define PAGING_MEMORY (15*1024*1024) // 主内存区最多可以占用15M。

    45 #define PAGING_PAGES (PAGING_MEMORY>>12) // 主内存块最多可以占用的物理页面数

    46 #define MAP_NR(addr) (((addr)-LOW_MEM)>>12) // 将指定物理内存地址映射为映射数组标号。

    47 #define USED 100 // 页面被占用标志

    57 static unsigned char mem_map [ PAGING_PAGES ] = {0,}; // 主内存块映射数组

    mem_map 中每一项的内容表示物理内存被多少个的对象使用,所以对应项为0 就表示对应物理内存

    页面空闲。

    可以看出当内核在定义映射数组时是以主内存块最大可能大小mem_map 15M 来定义的,最低起始地

    址为LOW_MEM,mem_map 的第一项对应于物理内存的地址为LOW_MEM,所以就有了第46 行的映射

    关系MAP_NR。而当实际运行时主内存块却不一定是这么大,这就需要根据实际主内存块的大小对mem

    _map 的内容进行调整。对于不是属于实际主内存块的物理内存的对应项清除掉,linux0.11 采用的做法是

    在初始化时将属于实际属于主内存块的物理内存的对应项的值清零,将不属于的置为一个相对较大的值

    USED。这样在作管理时这些不属于主内存块的页面就不会通过主内存块的管理程序被分配出去使用了。

    下面就是主内存块初始化的代码:

    /init/main.c

    当系统初启时,启动程序通过BIOS 调用将1M 以后的扩展内存大小(KB)读入到内存0x90002 号单元

    58 #define EXT_MEM_K (*(unsigned short *)0x90002)

    下面是系统初始化函数main() 中的内容

    112 memory_end = (1 16*1024*1024) // linux0.11 最大支持16M 物理内存

    115 memory_end = 16*1024*1024;

    116 if (memory_end > 12*1024*1024) // 根据内存大小设置缓冲区末端的位置

    117 buffer_memory_end = 4*1024*1024;

    118 else if (memory_end > 6*1024*1024)

    119 buffer_memory_end = 2*1024*1024;

    120 else

    121 buffer_memory_end = 1*1024*1024;

    122 main_memory_start = buffer_memory_end; // 主内存起始位置= 缓冲区末端;

    123 #ifdef RAMDISK // 如果定义了虚拟盘,重新设置主内存块起始位置

    //rs_init() 返回虚拟盘的大小

    124 main_memory_start += rd_init(main_memory_start, RAMDISK*1024);

    125 #endif

    126 mem_init(main_memory_start,memory_end); // 初始化主内存块

    下面就是mem_init 的代码。

    399 void mem_init(long start_mem, long end_mem)

    400 {

    401 int i;

    402

    403 HIGH_MEMORY = end_mem; // 设置物理内存最高端。

    404 for (i=0 ; i>= 12; // 计算需要初始化的映射项数目

    409 while (end_mem-->0) 将实际主内存块对// 应的映射项置为0(空闲)

    410 mem_map[i++]=0;

    411 }

    通过以上的操作之后,操作系统便可以了解主内存块中物理内存页面的使用情况了。

    3 内存的分配与回收

    分配

    当内核本身或者进程需要一页新的物理页面时,内核就要给他分配一个空闲的物理页面。内核需要

    查询相关信息,以尽量最优的方案分配一个空闲页面,尤其是在有虚存管理机制的操作系统中对于空闲

    页面的选取方案非常重要,如果选取不当将导致系统抖动。linux0.11 没有实现虚存管理,也就不用考虑

    这些,只需要考虑如何找出一个空闲页面。

    知道了内核对主内存块中空闲物理内存页面的映射结构mem_map,查找空闲页面的工作就简单了。

    只需要在mem_map 找出一个空闲项,并将该项映射为对应的物理页面地址。算法如下:

    算法:get_free_page

    输入:无

    输出:空闲页面物理地址

    {

    从最后一项开始查找mem_map 空闲项;

    if( 没有空闲项)

    renturn 0;

    将空闲项内容置1,表示已经被占用;

    将空闲项对应的下标转换为对应的物理页面的物理地址=

    ( 数组下标 页面实际物理起始地址。

    72 "movl %%ecx,%%edx\n\t" // 保存页面实际物理起始地址。

    73 "movl $1024,%%ecx\n\t" // 置计数值1024

    74 "leal 4092(%%edx),%%edi\n\t" // 使edi 指向该物理页末端

    75 "rep ; stosl\n\t" // 沿反方向将该页清零。

    76 "movl %%edx,%%eax\n" // 将页面实际物理起始地址放入eax(返回值)。

    77 "1:"

    78 : "=a" (__res)

    79 : "" (0), "i" (LOW_MEM), "c" (PAGING_PAGES),

    80 "D" (mem_map+PAGING_PAGES-1)

    81 : "di", "cx", "dx");

    82 return __res; // 返回空闲页面实际物理起始地址(如果无空闲也则返回0)。

    83 }

    84

    这个函数返回的只是物理页面的物理地址,下一节将具体讲如何将物理地址映射为线性地址。

    回收:

    当内核使用完一个物理页面或者进程退出时内核归还申请了的物理页面。这时就需要更改相应的信

    息,以便下一次使用。在归还页面时可能会出现下面几种情况:

    1)页面物理地址低于主内存块可能的最低端,这种情况不需要处理直接退出,因为这部分内存空

    间被用于内核程序和缓冲,没有作为分配页面的内存空间。还有一种情况会出现这种情况,当内存操作

    失败时,会调用回收页面过程回收已经分配了的物理页,如果因为内存分配失败造成的,就不需要真正

    的回收操作,调用回收过程时会以0 为输入参数。

    2)页面物理地址高于实际物理内存最高地址。这种情况是不允许的,内核将使调用对象进入死循

    环,这是一种简单而有效的方法,因为这种情况要判断出错原因是很困难的。

    3)调用对象试图释放一块空闲物理内存。出现这种情况可能是因为多个对象共享该物理页,在释

    放时出现了重复释放。比如:进程A、B共享物理页170,由于系统的原因A将该页释放了两次,当B

    释放该页时就会出现这种情况。这种情况也是不允许的,一般意味着内核出错,内核将使调用对象进入

    死循环以避免错误扩散。

    4)要释放的页面正确。因为可能是共享内存,所以要将该页对应的映射项的值减1,表示减少了

    一个引用对象。如果引用数减到0了,并不对物理页面的内容清0,等到被分配时再做,因为可能这个页

    面不会在被使用,同时在分配时用汇编代码来做效率会很高。

    这样下面的代码就很好理解了:

    85 /*

    86 * Free a page of memory at physical address 'addr'. Used by

    87 * 'free_page_tables()'

    88 */

    /*

    * 释放物理地址'addr' 开始的一页内存。用于函数'free_page_tables()'。

    */

    89 void free_page(unsigned long addr)

    90 {

    91 if (addr = HIGH_MEMORY)

    // 如果物理地址addr>= 实际内存大小,则显示出错信息,调用对象死机。

    93 panic( "trying to free nonexistent page");

    94 addr - = LOW_MEM; // 将物理地址换算为对应的内存映射数组下标。

    95 addr >>= 12;

    96 if (mem_map[addr]--) return; // 如果对应内存映射数组项不等于0,则减1,返回

    97 mem_map[addr]=0; // 否则置对应映射项为0,并显示出错信息,调用对象死机。

    98 panic( "trying to free free page");

    99 }

    100

    4 页面映射

    如果进程请求一页空闲内存,或者页失效错误时,会出现页面请求。在这个时候请求是以线性地址

    的形式提出来的。因为对于一个进程来说,它感知不到其他进程的存在,对它自己,觉得独占了所有资

    源。操作系统在控制物理内存的同时又要控制进程的虚拟空间,这就需要在内存线性地址与物理地址之

    间作转换工作。比如:进程在线性地址0x0104 F380 处 产生了缺页中断,内核将进行一系列的处理,最

    后分配一个物理页面,但是并不能这样返回进程执行,因为进程仍然需要从线性地址0x0104 F380 处读取

    数据,就像没有发生过缺页中断一样。操作系统就必须要做这个工作,将物理页面映射到线性地址上。

    要将物理页面映射到线性地址上,就应该修改页目录表和页表的相关内容,这样进程才能通过线性

    地址找到相应的物理页面。回顾一下386 页面映射机制,cpu 通过线性地址的高10 位寻找到相应的页

    表,再通过中间10 位寻找到物理页面,最后通过低12 位在物理页面中寻找到相应的内存单元。所以要

    让进程找到物理页面,就必须根据线性地址设置页目录项和页表项。linux0.11 使用put_page 来作这个处

    理,其算法如下:

    算法:put_page

    输入:物理页面地址page

    线性地址address

    输出:如果成功,返回page;如果失败,返回0

    {

    if ( 物理页面地址低于LOW_MEM 或者不小于HIGH_MEMORY)

    显示出错信息,返回0;

    if ( 物理页面地址对应的内存映射数组映射项的值!= 1)

    显示出错信息,返回0;

    根据线性地址高10 位找到对应的页目录表项;

    if ( 页目录表项对应的页表在内存中)

    根据页目录表项的到页表的物理地址;

    else{

    分配新的物理页面作为新的页表;

    初始化页目录表项,使它指向新的页表;

    根据页目录表项的到页表的物理地址;

    }

    根据线性地址中间10 位找到对应的页表项;

    if( 对应的页表项已经被使用)

    显示出错信息,返回0;

    设置对应的页表项,使它指向物理页面;

    return 物理页面地址;

    }

    put_page 操纵的是由get_free_page()分配得到的物理页面,所以物理页面地址应该是在主内存块

    中,如果不在,就应该终止映射,返回失败。然后调用put_page 函数的对象根据自身的特性作相关处

    理。同样是因为put_page 操纵的是新分配的物理页面,所以物理页面地址对应的内存映射数组映射项的

    值应该是1。如果不是1,也应该终止映射,返回失败。如果前面的检查通过了,就应改进行映射了。首

    先在页目录表中找到对应页目录项,如果页目录项有效,即对应页表在内存中,就直接寻找页表项。否

    则就必须先分配一个物理页作为页表。从理论上讲,在设置对应的页表项之前应该检查一下该页表项是

    否已经被使用。从而确保映射的一致性,因为如果页表项已经被使用,对其的第二次赋值会使原来的映

    射关系失效。但是由于linux 在总体设计上的特点,而且新分配的页表被全部清零,所以不会出现这个问

    题。随着对代码分析的深入,将体会到这一点。

    下面就是的put_page 代码:

    /mm/memory.c

    190

    191 /*

    192 * This function puts a page in memory at the wanted address.

    193 * It returns the physical address of the page gotten, 0 if

    194 * out of memory (either when trying to access page-table or

    195 * page.)

    196 */

    /*

    * 下面函数将一内存页面放置在指定地址处。它返回页面的物理地址,如果

    * 内存不够(在访问页表或页面时),则返回0。

    */

    197 unsigned long put_page(unsigned long page,unsigned long address)

    198 {

    199 unsigned long tmp, *page_table;

    200

    201 /* NOTE !!! This uses the fact that _pg_dir=0 */

    /* 注意!!! 这里使用了页目录基址_pg_dir=0 的条件 */

    202

    203 if (page = HIGH_MEMORY) // 判断是否在主内存块中

    204 printk( "Trying to put page %p at %p\n",page,address);

    205 if (mem_map[(page-LOW_MEM)>>12] != 1) // 判断对应映射项的值是否为1

    206 printk( "mem_map disagrees with %p at %p\n",page,address);

    207 page_table = (unsigned long *) ((address>>20) & 0xffc); // 根据线性地址找到对应的页目录表项;

    208 if ((*page_table)&1) // 判断页表是否存在

    209 page_table = (unsigned long *) (0xfffff000 & *page_table); // 取对应页表物理地址

    210 else {

    211 if (!(tmp=get_free_page())) // 申请新物理页作为页表

    212 return 0;

    213 *page_table = tmp|7; // 设置页目录项

    214 page_table = (unsigned long *) tmp;

    215 }

    216 page_table[(address>>12) & 0x3ff] = page | 7; // 页面设置为用户权限、可写、有效

    217 /* no need for invalidate */

    /* 不需要刷新页变换高速缓冲 */

    218 return page; // 返回物理页面地址。

    219 }

    220

    在这个代码中,如果第一个判断为真时,只是打印出错信息,并没有返回。这将导致第二个判断时

    数组溢出,由于语言并不对数组溢出进行出错处理。这里将可能出现错误。而且mem_map c 第二个判断

    也没有在打印错误信息之后返回,这将导致错误蔓延。不过幸运的是,linux0.11 中不会以这种参数调用

    put_page,所以这里只是作一个算法上的说明。

    看了put_page 之后,那么get_empty_page 的代码就很好理解了。get_empty_page 以线性地址为参数,

    申请新的物理页面并完成映射过程。

    /mm/memory.c

    274 void get_empty_page(unsigned long address)

    275 {

    276 unsigned long tmp;

    277

    278 if (!(tmp=get_free_page()) || !put_page(tmp,address)) {

    279 free_page(tmp); /* 0 is ok - ignored */

    280 oom();

    281 }

    282 }

    283

    其中oom() 是用于内存使用完后的处理,显示完信息之后使调用进程退出。

    /mm/memory.c

    33 static inline volatile void oom(void)

    34 {

    35 printk( "out of memory\n\r");

    36 do_exit(SIGSEGV); // 进程退出,出错码:SIGSEGV(资源暂时不可用)

    37 }

    38

    5 释放页表:

    内核使用了内存,自然就会有释放的时候。当进程创建时,需要获得大量的内存,也会释放大量的

    内存空间;当进程退出时,肯定有大量的内存需要释放。而伴随这种大量的内存释放工作,这些空间对

    应的页表也会变成无用的。如果不进行回收,将是巨大的浪费。

    内核如果要做这种释放(见算法free_page_tables),至少需要释放一个页表所映射的4M 的线性空

    间,所以释放空间起始地址应该是以4M 为边界的。要释放的空间不可以是低16M 的空间。

    算法:free_page_tables

    输入:要释放空间起始线性地址from

    要释放空间大小size

    输出:如果成功,返回0;如果失败,使调用对象进入死循环

    {

    if( 要释放的空间不是以4M 为边界)

    显示出错信息,调用对象死循环;

    if( 要释放的空间是用于内核控制物理内存的低16M 空间)

    显示出错信息,调用对象死循环;

    计算要释放的空间所占的页表数;

    for( 每个要释放的页表){

    for( 每个页表项)

    if( 页表项映射有物理页面)

    释放物理页面free_page();

    将该页表项设为空闲;

    }

    释放页表使用的物理页;

    将该页表对应的页目录项设为空闲;

    }

    刷新页变换高速缓冲;

    return 0;

    }

    因为这个线性空间是用于内核对物理内存的控制,不可以被释放。接下来要做的就很明显了。整个操作

    将导致页目录表的变化。由于cpu 为了提高内存访问速度,会将页目录表和部分页表加载到cpu 页变换高

    速缓存中,我们修改了页目录表就必须使cpu 页变换高速缓存中的内容同我们修改后的相同,所以必须刷

    新页变换高速缓冲。通过重新对页目录表寄存器cr3 赋值就可以使cpu 刷新页变换高速缓冲。具体代码见

    下:

    /mm/memory.c

    39 #define invalidate() \

    40 __asm__( "movl %%eax,%%cr3":: "a" (0)) // 寄存器eax 中存放0,即页目录表起始地址

    41

    101 /*

    102 * This function frees a continuos block of page tables, as needed

    103 * by 'exit()'. As does copy_page_tables(), this handles only 4Mb blocks.

    104 */

    /*

    * 下面函数释放页表连续的内存块,'exit()' 需要该函数。与copy_page_tables()

    * 类似,该函数仅处理4Mb 的内存块。

    */

    105 int free_page_tables(unsigned long from,unsigned long size)

    106 {

    107 unsigned long *pg_table;

    108 unsigned long * dir, nr;

    109

    110 if (from & 0x3fffff) 要释放// 空间线性地址应该以4M 为边界。

    111 panic( "free_page_tables called with wrong alignment");

    112 if (!from) // 这里只对低4M 空间的释放进行限制,BUG

    113 panic( "Trying to free up swapper memory space");

    114 size = (size + 0x3fffff) >> 22; // 计算要释放的页表数

    115 dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */ // 第一个要释放页表对应的页目录项

    116 for ( ; size-->0 ; dir++) {

    117 if (!(1 & *dir)) // 该目录项是否有效

    118 continue;

    119 pg_table = (unsigned long *) (0xfffff000 & *dir); // 计算页表起始地址。

    120 for (nr=0 ; nr

    #include

    #include

    void main(){

    int childpid.data=100;

    childpid=fork();

    if(childpid==0){

    printf("I'm child!/n");

    printf("My father have a data ,it's %d!/n",data);

    exit(0);

    }

    printf("I'm father!I have a child %d/n",childpid);

    exit(0);

    }

    创建一个进程后,父进程和子进程使用同样的代码。但是他们中childpid 的值不同,如果是子进

    程,childpid 的值是0;如果是父进程,childpid 的值是子进程的进程号。在这以后,子进程可能会使用父

    进程中的一些数据。如果子进程不调用另一个可执行文件作为其执行代码,子进程将一直使用父进程的

    代码。

    6.1 共享空间

    有了386 对页面共享的支持,共享空间的方法就很容易想到了。将被共享的空间的页目录表和页表复

    制一份,并且将所有页表项的访问属性设为只读,并修改页面映射表中的页面引用信息即可。具体算法

    如下:

    算法:copy_page_tables

    输入:共享源页面起始地址from

    共享目的空间页面起始地址to

    被共享空间的大小size

    输出:如果成功,返回0

    {

    if(from 或者to 不是以4M 为边界)

    显示出错信息,使调用对象进入死循环;

    for( 共享源空间的每一个页目录项){

    if( 对应共享目的空间的页表已经存在)

    显示出错信息,死循环;

    if( 共享源空间的页目录项不存在)

    continue;

    为对应共享目的空间分配空闲页作为页表;

    设置该空闲页属性(可写、用户、有效)

    if( 共享源空间本次复制的是前4M 的内核空间)

    本次共享空间只是前640K;

    for( 每个要共享空间的页表项){

    复制页表项;

    if( 对应页不存在)

    continue;

    if( 被共享页在主内存块映射表映射范围内 ){

    将两个页表项都置为只读;

    对应页面映射项内容加1;

    }

    else

    只将复制的页表项置为只读;

    }

    }

    刷新页变换高速缓冲;

    }

    对于带有页表复制,和带有页表的释放一样,必须保证被共享的空间和被共享到的空间起始地址是

    以4M 为边界的;每次共享4M 的空间,像以前一样,对于内核空间必须作特殊处理。640K 到1M 的空

    间本来是高速缓冲块的空间,但是被显存和BIOS 占用了,所以这部分空间是不共享的;因为linus 当初

    使用的计算机有16M 的内存,高速缓冲空间结束位置是4M(见启动后内存分配),所以可能是由于这个

    原因,1M 到3,071K 这个空间也是不共享的,对高速缓冲共享是没有意义的,这样内核的前4M 空间就

    只共享640K。如果被共享页不在主内存块映射表范围内,共享的就是这640K 的空间,是内核使用的,

    在共享时,源页表项不被置为只读。

    /mm/memory.c

    132

    133 /*

    134 * Well, here is one of the most complicated functions in mm. It

    135 * copies a range of linerar addresses by copying only the pages.

    136 * Let's hope this is bug-free, 'cause this one I don't want to debug :-)

    137 *

    138 * Note! We don't copy just any chunks of memory - addresses have to

    139 * be divisible by 4Mb (one page-directory entry), as this makes the

    140 * function easier. It's used only by fork anyway.

    141 *

    142 * NOTE 2!! When from==0 we are copying kernel space for the first

    143 * fork(). Then we DONT want to copy a full page-directory entry, as

    144 * that would lead to some serious memory waste - we just copy the

    145 * first 160 pages - 640kB. Even that is more than we need, but it

    146 * doesn't take any more memory - we don't copy-on-write in the low

    147 * 1 Mb-range, so the pages can be shared with the kernel. Thus the

    148 * special case for nr=xxxx.

    149 */

    /*

    好了,下面是内存管理中最为复杂的程序之* mm 一。它通过只复制内存页面

    * 来拷贝一定范围内线性地址中的内容。希望代码中没有错误,因为我不想

    * 再调试这块代码了.。

    *

    * 注意!我们并不是仅复制任何内存块 - 内存块的地址需要是4Mb 的倍数(正好

    * 一个页目录项对应的内存大小),因为这样处理可使函数很简单。不管怎样,

    * 它仅被fork() 使用(fork.c 第56 行)。

    *

    * 注意2!!当from==0 时,是在为第一次fork() 调用复制内核空间。此时我们

    * 不想复制整个页目录项对应的内存,因为这样做会导致内存严重的浪费 - 我们

    * 只复制头160 个页面 - 对应640kB。即使是复制这些页面也已经超出我们的需求,

    * 但这不会占用更多的内存 - 在低1Mb 内存范围内我们不执行写时复制操作,所以

    * 这些页面可以与内核共享。因此这是nr=xxxx 的特殊情况(nr 在程序中指页面数)。

    */

    150 int copy_page_tables(unsigned long from,unsigned long to,long size)

    151 {

    152 unsigned long * from_page_table;

    153 unsigned long * to_page_table;

    154 unsigned long this_page;

    155 unsigned long * from_dir, * to_dir;

    156 unsigned long nr;

    157

    158 if ((from&0x3fffff) || (to&0x3fffff)) // 判断是否以4M 为边界

    159 panic( "copy_page_tables called with wrong alignment");

    160 from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */ 计// 算起始页目录项

    161 to_dir = (unsigned long *) ((to>>20) & 0xffc);

    162 size = ((unsigned) (size+0x3fffff)) >> 22; // 计算要共享的页表数

    163 for( ; size-->0 ; from_dir++,to_dir++) {

    164 if (1 & *to_dir) // 被共享到的页表已经存在

    165 panic( "copy_page_tables: already exist");

    166 if (!(1 & *from_dir)) // 被共享的页表不存在

    167 continue;

    168 from_page_table = (unsigned long *) (0xfffff000 & *from_dir); // 取源页表地址

    169 if (!(to_page_table = (unsigned long *) get_free_page()))

    170 return -1; /* Out of memory, see freeing */

    171 *to_dir = ((unsigned long) to_page_table) | 7; // 设置该页属性(可写、用户、有效)

    172 nr = (from==0)?0xA0:1024; // 如果是前4M 空间,只共享640K(160 页)

    173 for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {

    174 this_page = *from_page_table;

    175 if (!(1 & this_page)) // 如果当前源页表项没有使用,则不用复制

    176 continue;

    177 this_page &= ~2; // 将目的页表项设为只读

    178 *to_page_table = this_page;

    179 if (this_page > LOW_MEM) { // 如果被共享页在主内存块映射表映射范围内

    180 *from_page_table = this_page; // 源页表项设为只读

    181 this_page -= LOW_MEM;

    182 this_page >>= 12;

    183 mem_map[this_page]++; // 共享数加一

    184 }

    185 }

    186 }

    187 invalidate(); // 刷新页变换高速缓冲。

    188 return 0;

    189 }

阅读(361) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~