页面映射

linux体系结构

一个完整的操作系统包括4部分:硬件,操作系统内核,操作系统服务,和用户应用程序。内核分2种模式:单内核,微内核。单内核,结构紧凑,速度快,但层次性不强。linux0.11是单内核

linux内核源代码的目录结构

boot->系统引导
fs-〉文件系统
include
init
kenerl/blk_drv,chr_drv,sys
lib
mm

linux采用intel cpu的内存分页管理机制(1页4k物理内存),使用虚拟线形地址和实际物理地址映射的方法让同时执行的程序共同使用有限的内存。使用这种方法,每个执行中的程序可以使用比实际内存大得多的地址空间。

linux0.11虚拟地址空间是4G=64M*64, 每个任务(进程)有64M虚拟地址空间,最多64个任务.
每个虚拟地址空间,头部是代码区,紧接着数据区,尾部是堆栈区.

boot目录下有3个汇编文件,是内核源文件中最先被编译的程序。
主要功能是计算机上电时阴道内核启动,将内核代码加载到内存中,并作进入32位保护模式前的系统初始化工作。
bootsect.s,setup.s 需要as86来编译,使用的是as86汇编语言格式,head.s需要as编译,使用的是AT&T格式的汇编。
bootsect.s 磁盘引导块程序,编译后驻留在磁盘第一个扇区MBR。pc上电bios自检后,被bios加载到地址0x7c00处执行。
setup.s 读取机器的硬件配置参数,并把内核模块system移到合适的内存地址。
head.s 被编译连接在system模块的最前部分,做硬件探测工作,和内存管理的初始配置工作。

fs文件系统目录
包含了17个文件,诸如
buffer.c 对内存高速缓冲区的处理
file_table.c 定义了文件句柄
ioctrl.c 定义了io控制功能
exec.c exec函数簇
fcntl.c 文件i/o控制
open.c 修改,创建文件
char_dev.c 定义了字符设备读写函数
pipe.c 定义了管道读取
file_dev.c 文件读写函数
block_dev.c 块读写
truncate.c 删除文件时释放文件占用的设备空间
inode.c i节点操作
bitmap.c i节点和逻辑数据块
super.c 超级块

文件系统程序本身并不直接跟块设备的驱动打交道,而是通过高速缓冲区。

所有对文件系统中数据的访问,都需要首先读取到高速缓冲区.

include头文件主目录
kernel目录下有12个代码文件
asm.s 处理系统硬件异常引起的中断,在中断处理过程中,分别调用traps.c的处理函数
exit.c 处理进程终止的调用
fork.c 使用了2个c函数:find_empty_process, copy_process
mktime.c 时间函数mktime()
panic.c 显示内核出错信息并停机
printk.c 内核专用打印函数printk
sched.c 调度
signal.c 信号处理
sys.c 很多系统函数
system_calls.c INT 0x80 调用的系统处理
vsprintf.c 字符串格式化函数


http://linux.chinaitlab.com/Special/Linuxneihe/

http://code.google.com/p/msysgit/downloads/list?q=full+installer+official+git


在setup的帮助下,我们顺利地从16位实地址模式过渡到32位段式寻址的保护模式。又在arch/i386/boot/compressed/head.S的帮助下实现了内核的自解压,并且从arch/i386/kernel/head.S中的startup_32开始。现在在线性地址0x100000(1M)处开始就是我们的解压后的内核了。而startup_32()的地址恰好是0x100000.由于还没有开启页面映射,所以必须引用变量的线性地址(即变量的虚拟地址-PAGE_OFFSET),带来了很多不便。所以下一步的任务,就是建立页表,开启页面映射了。我们不妨从arch/i386/kernel/head.S入手。
    
    由于在Linux中,每个进程拥有一个页表,那么,第一个页表也应该有一个对应的进程。通常情况下,Linux下通过fork()系统调用,复制原有进程,来产生新进程。然而第一个进程该如何产生呢?既然不能复制,那就只能像女娲造人一样,以全局变量的方式捏造一个出来。它就是init_thread_union.传说中的0号进程,名叫swapper.只要swapper进程运行起来,调用start_kernel(),剩下的事就好办了。不过,现在离运行swapper进程还差得很远。关键的一步,我们还没有为该进程设置页表。
    
    为了保持可移植性,Linux采用了三级页表。不过x86处理器只使用两级页表。所以,我们需要一个页目录和很多个页表(最多达1024个页表),页目录和页表的大小均为4k.swapper的页目录的创建与该进程的创建思维类似,也是捏造一个页表,叫swapper_pg_dir.
    
    417 ENTRY(swapper_pg_dir)
    
    418         .fill 1024,4,0
    
    它的意思是从swapper_pg_dir开始,填充1024项,每项为4字节,值为0,正好是4K一个页面。
    
    页目录有了,接下去看页表。一个问题产生了。该映射几个页表呢?尽管一个页目录最多能映射1024个页表,每个页表映射4M虚拟地址,所以总共可以映射4G虚拟地址空间。但是,通常应用程序用不了这么多。最简单的想法是,够用就行。先映射用到的代码和数据。还有一个问题:如何映射呢?运行cat /proc/$pid/maps可以看到,用户态进程的地址映射是断断续续的,相当复杂。这是由于不同进程的用户空间相互独立。但是,由于所有进程共享内核态代码和数据,所以映射关系可以大大简化。既然内核态虚拟地址从3G开始,而内核代码和数据事实上是从物理地址0x100000开始,那么本着KISS原则,一切从简,加上3G就作为对应的虚拟地址好了。由此可见,对内核态代码和数据来说:虚拟地址=物理地址+PAGE_OFFSET(3G)
    
    内核中有变量pg0,表示对应的页表。建立页表的过程如下:
    
    091 page_pde_offset = (__PAGE_OFFSET 》 20);
    
    092
    
    093         movl $(pg0 - __PAGE_OFFSET), %edi
    
    094         movl $(swapper_pg_dir - __PAGE_OFFSET), %edx
    
    095         movl $0x007, %eax                       /* 0x007 = PRESENT+RW+USER */
    
    096 10:
    
    097         leal 0x007(%edi),%ecx                   /* Create PDE entry */
    
    098         movl %ecx,(%edx)                        /* Store identity PDE entry */
    
    099         movl %ecx,page_pde_offset(%edx)         /* Store kernel PDE entry */
    
    100         addl $4,%edx
    
    101         movl $1024, %ecx
    
    102 11:
    
    103         stosl
    
    104         addl $0x1000,%eax
    
    105         loop 11b
    
    106         /* End condition: we must map up to and including INIT_MAP_BEYOND_END */
    
    107         /* bytes beyond the end of our own page tables; the +0x007 is the attribute bits */
    
    108         leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
    
    109         cmpl %ebp,%eax
    
    110         jb 10b
    
    111         movl %edi,(init_pg_tables_end - __PAGE_OFFSET)
    
    用伪代码表示就是:
    
    typedef unsigned int PTE;
    
    PTE *pg=pg0;
    
    PTE pte=0x007;
    
    for(i=0;;i++){//把线性地址i*4MB~(i+1)*4MB-1(用户空间地址)和3G+i*4MB~3G+(i+1)*4MB-1(内核空间地址)映射到物理地址i*4MB~(i+1)*4MB-1
    
    swapper_pg_dir[i]=pg+0x007;
    
    swapper_pg_dir[i+page_pde_offset]=pg+0x007;
    
    for(j=0;j<1024;j++){
    
    pte+=0x1000;
    
    pg[i*1024+j]=pte;
    
    }
    
    if(pte>=((char*)pg+i*1024+j)*4+0x007+INIT_MAP_BEYOND_END)
    
    {
    
    init_pg_tables_end=pg+i*0x1000+j;
    
    break;
    
    }
    
    }
    
    大致意思是从0开始,把连续的线性地址映射到物理地址。这里的0x007是什么意思呢?由于每个页表项有32位,但其实只需保存物理地址的高20位就够了,所以剩下的低12位可以用来表示页的属性。0x007正好表示PRESENT+RW+USER(在内存中,可读写,用户页面,这样在用户态和内核态都可读写,从而实现平滑过渡)。
    
    那么结束条件是什么呢?从代码中可知,当映射到当前所操作的页表项往下INIT_MAP_BEYOND_END(128K)处映射结束。nm vmlinux|grep pg0得c0595000.据此可以计算总共映射了多少页(小学计算题:P)
    
    所以映射了2个页表,映射地址从0x0~0x2000-1,大小为8M.
    
    最后,关键时刻到来了:
    
    183 /*
    
    184  * Enable paging
    
    185  */
    
    186         movl $swapper_pg_dir-__PAGE_OFFSET,%eax
    
    187         movl %eax,%cr3          /* set the page table pointer */
    
    188         movl %cr0,%eax
    
    189         orl $0x80000000,%eax
    
    190         movl %eax,%cr0          /* and set paging (PG) bit */
    
    开启页面映射后,可以直接引用内核中的所有变量了。不过离start_kernel还有点距离。要启动swapper进程,得首先设置内核堆栈。
    
    193         /* Set up the stack pointer */
    
    194         lss stack_start,%esp
    
    然后设置中断向量表,看到久违的“call”了
    
    215         call setup_idt
    
    检查CPU类型
    
    载入gdt(原来的gdt是临时的)和ldt
    
    302         lgdt cpu_gdt_descr
    
    303         lidt idt_descr
    
    最后,调用start_kernel
    
    327         call start_kernel
    
    到这一步,我们的目的地终于走到了。在摆脱了晦涩的汇编之后,接下去的代码,虽然与用户态程序相比,还有中断,同步等等的干扰,但相比较而言就好懂很多了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值