3.操作系统引导——操作系统启动

时间:2020-12-23 16:44:44

1.setup.s

setup中,操作系统接手硬件,初始化
开机做两件事:bootsect读入系统,setup初始化

!
! setup.s (C) 1991 Linus Torvalds
!
! setup.s is responsible for getting the system data from the BIOS,
! and putting them into the appropriate places in system memory.
! both setup.s and system has been loaded by the bootblock.
!
! This code asks the bios for memory/disk/other parameters, and
! puts them in a "safe" place: 0x90000-0x901FF, ie where the
! boot-block used to be. It is then up to the protected mode
! system to read them from there before the area is overwritten
! for buffer-blocks.
!
! setup.s 负责从BIOS 中获取系统数据,并将这些数据放到系统内存的适当地方。
! 此时setup.ssystem 已经由bootsect 引导块加载到内存中。
!
! 这段代码询问bios 有关内存/磁盘/其它参数,并将这些参数放到一个
! “安全的”地方:0x90000-0x901FF,也即原来bootsect 代码块曾经在
! 的地方,然后在被缓冲块覆盖掉之前由保护模式的system 读取。
!

entry start
start:

! ok, the read went well so we get current cursor position and save it for
! posterity.
! ok,整个读磁盘过程都正常,现在将光标位置保存以备今后使用。

mov ax,#INITSEG ! this is done in bootsect already, but...
mov ds,ax   !ds 置成#INITSEG(0x9000)。
!这已经在bootsect 程序中设置过,但是现在是setup 程序,Linus 觉得需要再重新设置一下。
mov ah,#0x03 ! read cursor pos

! BIOS 中断0x10 的读光标功能号 ah = 0x03,输入:bh = 页号,返回:ch = 扫描开始线,cl = 扫描结束线,
! dh = 行号(0x00 是顶端),dl = 列号(0x00 是左边)。
xor bh,bh
int 0x10 ! save it in known place, con_init fetches
mov [0],dx ! it from 0x90000.dx0x90000
! 上两句是说将光标位置信息存放在0x90000 处,控制台初始化时会来取。

! Get memory size (extended mem, kB) ! 下面3 句取扩展内存的大小值(KB)。
! 是调用中断0x15,功能号ah = 0x88
! 返回:ax = 从0x100000(1M)处开始的扩展内存大小(KB)。
! 若出错则CF 置位,ax = 出错码。

mov ah,#0x88
int 0x15    ! BIOS中断int 0x15,用于获得物理内存的大小,放到axmov [2],ax ! 将扩展内存数值存在0x90002 处(1 个字)。
! ax赋值给[2],[2]是间接寻址,[2]前有默认的段寄存器,存的是0x9000
! [2]是将0x9000左移4位加2,即0x90002,存的数值用于扩展内存的大小
! 操作系统要管理内存,要知道内存有多大

……

! now we want to move to protected mode ... ! 从这里开始我们要保护模式方面的工作了。

cli ! no interrupts allowed ! ! 此时不允许中断。

! first we move the system to it's rightful place
! 首先我们将system 模块移到正确的位置。
! bootsect 引导程序是将system 模块读入到从0x10000(64k,SYSSEG=0x1000)开始的位置。由于当时假设
! system 模块最大长度不会超过0x80000(512k),也即其末端不会超过内存地址0x90000,
! 所以bootsect 会将自己移动到0x90000 开始的地方,并把setup 加载到它的后面。
! 下面这段程序的用途是再把整个system 模块移动到0x00000 位置,即把从0x10000 到0x8ffff
! 的内存数据块(512k),整块地向内存低端移动了0x10000(64k)的位置。

mov ax,#0x0000
cld      ! 'direction'=0, movs moves forward

! 移动
do_move:
mov es,ax   ! destination segment ! es:di 目的地址(初始为0x0000:0x0)
add ax,#0x1000
cmp ax,#0x9000  ! 已经把从0x8000 段开始的64k 代码移动完
jz end_move
mov ds,ax   ! source segment
sub di,di   ! di 减为0ds:si 源地址(初始为0x1000:0x0)
sub si,si    ! si 减为0 
mov cx,#0x8000  ! 移动0x8000 字(64k 字节)。
rep
movsw
jmp do_move
! 将操作系统移动到0x0000(es:di)处,地址0x0000-0x80000用于放操作系统
! 之前的0x7c00要腾出空间给操作系统

内存地址:0x90000,长度:2,名称:光标位置
内存地址:0x90002,长度:2,名称:扩展内存数
内存地址:0x9000C,长度2,名称:显卡参数
内存地址:0x901FC,长度:2,名称:根设备号

下边要进入保护模式了

! then we load the segment descriptors
! 此后,我们加载段描述符。
! 从这里开始会遇到32 位保护模式的操作,因此需要Intel 32 位保护模式编程方面的知识了,
! 有关这方面的信息请查阅列表后的简单介绍或附录中的详细说明。这里仅作概要说明。
!
! lidt 指令用于加载中断描述符表(idt)寄存器,它的操作数是6 个字节,0-1 字节是描述符表的
! 长度值(字节);2-5 字节是描述符表的32 位线性基地址(首地址),其形式参见下面
! 219-220 行和223-224 行的说明。中断描述符表中的每一个表项(8 字节)指出发生中断时
! 需要调用的代码的信息,与中断向量有些相似,但要包含更多的信息。
!
! lgdt 指令用于加载全局描述符表(gdt)寄存器,其操作数格式与lidt 指令的相同。全局描述符
! 表中的每个描述符项(8 字节)描述了保护模式下数据和代码段(块)的信息。其中包括段的
! 最大长度限制(16 位)、段的线性基址(32 位)、段的特权级、段是否在内存、读写许可以及
! 其它一些保护模式运行的标志。参见后面205-216 行。
!

end_move:
mov ax,#SETUPSEG ! right, forgot this at first. didn't work :-)
mov ds,ax ! ds 指向本程序(setup)段。
lidt idt_48 ! load idt with 0,0IDT 是保护模式下的中断函数表
! 加载中断描述符表(idt)寄存器,idt_486 字节操作数的位置
!2 字节表示idt 表的限长,后4 字节表示idt 表所处的基地址。

lgdt gdt_48 ! load gdt with whatever appropriateGDT是保护模式下的寻址表
! 初始化表,加载全局描述符表(gdt)寄存器,gdt_486 字节操作数的位置

! that was painless, now we enable A20
! 以上的操作很简单,现在我们开启A20 地址线。参见程序列表后有关A20 信号线的说明。


! 8042是键盘控制器,其输出端口P2用来控制A20地址线
call empty_8042  ! 等待输入缓冲器空。只有当输入缓冲器为空时才可以对其进行写命令。
mov al,#0xD1    ! command write ! 0xD1 命令码-表示要写数据到8042P2 端口
out #0x64,al    ! P2 端口的位1 用于A20 线的选通。


call empty_8042     ! 等待输入缓冲器空,看命令是否被接受。
mov al,#0xDF    ! A20 on ! 选通A20 地址线的参数。
out #0x60,al    ! 数据要写到0x60 口。
call empty_8042      ! 输入缓冲器为空,则表示A20 线已经选通。

! well, that went ok, I hope. Now we have to reprogram the interrupts :-(
! we put them right after the intel-reserved hardware interrupts, at
! int 0x20-0x2F. There they won't mess up anything. Sadly IBM really
! messed this up with the original PC, and they haven't been able to
! rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f,
! which is used for the internal hardware interrupts as well. We just
! have to reprogram the 8259's, and it isn't fun.
!! 希望以上一切正常。现在我们必须重新对中断进行编程??
!! 我们将它们放在正好处于intel 保留的硬件中断后面,在int 0x20-0x2F!! 在那里它们不会引起冲突。不幸的是IBM 在原PC 机中搞糟了,以后也没有纠正过来。
!! PC 机的bios 将中断放在了0x08-0x0f,这些中断也被用于内部硬件中断。
!! 所以我们就必须重新对8259 中断控制器进行编程,这一点都没劲。
……


! well, that certainly wasn't fun :-(. Hopefully it works, and we don't
! need no steenking BIOS anyway (except for the initial loading :-).
! The BIOS-routine wants lots of unnecessary data, and it's less
! "interesting" anyway. This is how REAL programmers do it.
!
! Well, now's the time to actually move into protected mode. To make
! things as simple as possible, we do no register set-up or anything,
! we let the gnu-compiled 32-bit programs do that. We just jump to
! absolute address 0x00000, in 32-bit protected mode.
!! 哼,上面这段当然没劲,希望这样能工作,而且我们也不再需要乏味的BIOS 了(除了
!! 初始的加载。BIOS 子程序要求很多不必要的数据,而且它一点都没趣。那是“真正”的
!! 程序员所做的事。

! 这里设置进入32 位保护模式运行。首先加载机器状态字(lmsw - Load Machine Status Word),也称
! 控制寄存器CR0,其比特位01 将导致CPU 工作在保护模式。
mov ax,#0x0001  ! protected mode (PE) bit ! 保护模式比特位(PE)。
lmsw ax     ! This is it! ! 就这样加载机器状态字!
jmpi 0,8    ! jmp offset 0 of segment 8 (cs) ! 跳转至cs8,偏移0 处。
! 跳转至0处,即跳转到system
! jmpi 0,80 赋给IP,将8赋给CScs=8用来插gdt,按照之前的寻址方式,段寄存器=CS<<4+IP=0x80,
! 这种寻址方式,CSIP都是16位寄存器,CS<<4+IP,最多是20位地址,最大值的1M! 也就是说 计算机的寻址空间是1M
! 现在的操作系统是4G的,16->32位,1M->4G,32位也叫保护模式
! 16位与32位的本质区别是CPU的解释程序不一样

! 我们已经将system 模块移动到0x00000 开始的地方,所以这里的偏移地址是0。这里的段
! 值的8 已经是保护模式下的段选择符了,用于选择描述符表和描述符表项以及所要求的特权级。
! 段选择符长度为16 位(2 字节);位0-1 表示请求的特权级0-3linux 操作系统只
! 用到两级:0 级(系统级)和3 级(用户级);位2 用于选择全局描述符表(0)还是局部描
! 述符表(1);位3-15 是描述符表项的索引,指出选择第几项描述符。所以段选择符
! 8(0b0000,0000,0000,1000)表示请求特权级0、使用全局描述符表中的第1 项,该项指出
! 代码的基地址是0,因此这里的跳转指令就会去执行system 中的代码。

! This routine checks that the keyboard command queue is empty
! No timeout is used - if this hangs there is something wrong with
! the machine, and we probably couldn't proceed anyway.
! 下面这个子程序检查键盘命令队列是否为空。这里不使用超时方法 - 如果这里死机,
! 则说明PC 机有问题,我们就没有办法再处理下去了。
! 只有当输入缓冲器为空时(状态寄存器位2 = 0)才可以对其进行写命令。
empty_8042:
.word 0x00eb,0x00eb     ! 这是两个跳转指令的机器码(跳转到下一句),相当于延时空操作。
in al,#0x64     ! 8042 status port !AT 键盘控制器状态寄存器。
test al,#2  ! is input buffer full? ! 测试位2,输入缓冲器满?
jnz empty_8042 ! yes - loop
ret


gdt: ! 全局描述符表开始处。描述符表由多个8 字节长的描述符项组成。
! 这里给出了3 个描述符项。第1 项无用(206 行),但须存在。第2 项是系统代码段
! 描述符(208-211 行),第3 项是系统数据段描述符(213-216 行)。每个描述符的具体
! 含义参见列表后说明。
.word 0,0,0,0 ! dummy !0个表项,从0开始寻址。第1 个描述符,不用。
! 这里在gdt 表中的偏移量为0x08,当加载代码段寄存器(段选择符)时,使用的是这个偏移值。以8个字节为单位寻址
! 第一个表项,一个word16位,共4个,表一个项是16*4位
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9A00 ! code read/exec
.word 0x00C0 ! granularity=4096, 386

! 这里在gdt 表中的偏移量是0x10,当加载数据段寄存器(如ds 等)时,使用的是这个偏移值。
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9200 ! data read/write
.word 0x00C0 ! granularity=4096, 386

3.操作系统引导——操作系统启动

cr0的最后一位:
置0是16位模式,即 实模式
置1是32位模式,即保护模式

保护模式和实模式下的地址翻译与中断处理

  • 实模式下,地址是 CS<<4+IP
  • 保护模式下,地址是 根据CS查GDT,查到的值+IP,中断处理的int n,中断函数是查IDT表的n对应的数据

GDT:Global Descriptor Table 全局描述符表,由硬件实现,快。GDT的内容存放,由硬件决定
CS 是selector 选择子,存放的是索引,GDT的index(描述符索引)是 段的描述符 在描述符表的位置,该位置的值 与 IP一起,组成了32位的地址

3.操作系统引导——操作系统启动

2.进入system

system模块的第一部分是 head.s

2.1 为什么system模块的第一部分是head.s

这是由Makefile决定的,Makefile:

disk: Image # 表示disk 这个目标要由Image 产生。
dd bs=8192 if=Image of=/dev/PS0 # dd 为UNIX 标准命令:复制一个文件,根据选项进行转换和格式化
# bs=表示一次读/写的字节数。
# if=表示输入的文件,of=表示输出到的文件。
# 这里/dev/PS0 是指第一个软盘驱动器(设备文件)。

Image: boot/bootsect boot/setup tools/system tools/build # 说明目标(Image 文件)是由
# 冒号后面的4 个元素产生,分别是boot/bootsect 和setup 文件、tools/system 和build 文件。
# boot/bootsect依赖的是bootsect.s
# boot/setup依赖的是setup.s
……

# 表示tools/system 文件要由分号右边所列的元素生成。
# 其中 boot/head.o 依赖于head.s
# init/main.o依赖于 main.c
# DRIVERS置驱动
tools/system: boot/head.o init/main.o \
$(ARCHIVES) $(DRIVERS) $(MATH) $(LIBS) 
$(LD) $(LDFLAGS) boot/head.o init/main.o \
# $(LD) 是链接,将 boot/head.o init/main.o …… 连接到一起,就有了system 
……
-o tools/system > System.map # 生成system 的命令。
# 最后的 > System.map 表示gld 需要将连接映象重定向存放在System.map 文件中。

2.2 head.s

setup是进入保护模式,head是进入之后的初始化

3.操作系统引导——操作系统启动

movl $0x10,%eax # 将0x10的值赋给eax,这里是32位汇编,与之前的16位汇编不同

/*
* linux/boot/head.s
*
* (C) 1991 Linus Torvalds
*/

/*
* head.s contains the 32-bit startup code.
*
* NOTE!!! Startup happens at absolute address 0x00000000, which is also where
* the page directory will exist. The startup code will be overwritten by
* the page directory.
*/
/*
* head.s 含有32 位启动代码。
* 注意!!! 32 位启动代码是从绝对地址0x00000000 开始的,这里也同样是页目录将存在的地方,
* 因此这里的启动代码将被页目录覆盖掉。
*/
.text
.globl _idt,_gdt,_pg_dir,_tmp_floppy_area
_pg_dir: # 页目录将会存放在这里。
startup_32: # 18-22 行设置各个数据段寄存器。
movl $0x10,%eax # 对于GNU 汇编来说,每个直接数要以'$'开始,否则是表示地址。
# 每个寄存器名都要以'%'开头,eax 表示是32 位的ax 寄存器。
# 再次注意!!! 这里已经处于32 位运行模式,因此这里的$0x10 并不是把地址0x10 装入各个
# 段寄存器,它现在其实是全局段描述符表中的偏移值,或者更正确地说是一个描述符表项
# 的选择符。这里$0x10 的含义是请求特权级0(位0-1=0)、选择全局描述符表(位2=0)、
# 选择表中第2 项(位3-15=2)。它正好指在当前的Linux 操作系统中,gas 和gld 已经分别更名为as 和ld。
# 向表中的数据段描述符项。
# 下面代码的含义是:置ds,es,fs,gs 中的选择符为setup.s 中构造的数据段(全局段描述符表
# 的第2 项)=0x10,并将堆栈放置在数据段中的_stack_start 数组内,然后使用新的中断描述
# 符表和全局段描述表.新的全局段描述表中初始内容与setup.s 中的完全一样。
# 指向gdt的0x10项(数据段)
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss _stack_start,%esp # 表示_stack_start??ss:esp,设置系统堆栈。
# stack_start 定义在kernel/sched.c,69 行。
call setup_idt # 调用设置中断描述符表子程序。
call setup_gdt # 调用设置全局描述符表子程序。


movl $0x10,%eax # reload all the segment registers
mov %ax,%ds # after changing gdt. CS was already
mov %ax,%es # reloaded in 'setup_gdt'
mov %ax,%fs # 因为修改了gdt,所以需要重新装载所有的段寄存器。
mov %ax,%gs # CS 代码段寄存器已经在setup_gdt 中重新加载过了。

lss _stack_start,%esp


# 32-36 行用于测试A20 地址线是否已经开启。采用的方法是向内存地址0x000000 处写入任意
# 一个数值,然后看内存地址0x100000(1M)处是否也是这个数值。如果一直相同的话,就一直
# 比较下去,也即死循环、死机。表示地址A20 线没有选通,结果内核就不能使用1M 以上内存。
xorl %eax,%eax
1: incl %eax # check that A20 really IS enabled
movl %eax,0x000000 # loop forever if it isn't
cmpl %eax,0x100000
je 1b # '1b'表示向后(backward)跳转到标号1 去(33 行)。若是'5f'则表示向前(forward)跳转到标号5 去。
# 0地址处和1M地址处相同(A20没开启),就死循环
/*
* NOTE! 486 should set bit 16, to check for write-protect in supervisor
* mode. Then it would be unnecessary with the "verify_area()"-calls.
* 486 users probably want to set the NE (#5) bit also, so as to use
* int 16 for math errors.
*/
/*
* 注意! 在下面这段程序中,486 应该将位16 置位,以检查在超级用户模式下的写保护,
* 此后"verify_area()"调用中就不需要了。486 的用户通常也会想将NE(#5)置位,以便
* 对数学协处理器的出错使用int 16*/
# 下面这段程序(43-65)用于检查数学协处理器芯片是否存在。方法是修改控制寄存器CR0,在
# 假设存在协处理器的情况下执行一个协处理器指令,如果出错的话则说明协处理器芯片不存在,
# 需要设置CR0 中的协处理器仿真位EM(位2),并复位协处理器存在标志MP(位1)。
movl %cr0,%eax # check math chip
andl $0x80000011,%eax # Save PG,PE,ET
/* "orl $0x10020,%eax" here for 486 might be good */
orl $2,%eax # set MP
movl %eax,%cr0
call check_x87
jmp after_page_tables # 页表

……

/*
* setup_idt
*
* sets up a idt with 256 entries pointing to
* ignore_int, interrupt gates. It then loads
* idt. Everything that wants to install itself
* in the idt-table may do so themselves. Interrupts
* are enabled elsewhere, when we can be relatively
* sure everything is ok. This routine will be over-
* written by the page tables.
*/
/*
* 下面这段是设置中断描述符表子程序 setup_idt
*
* 将中断描述符表idt 设置成具有256 个项,并都指向ignore_int 中断门。然后加载中断
* 描述符表寄存器(用lidt 指令)。真正实用的中断门以后再安装。当我们在其它地方认为一切
* 都正常时再开启中断。该子程序将会被页表覆盖掉。
*/
# 中断描述符表中的项虽然也是8 字节组成,但其格式与全局表中的不同,被称为门描述符
# (Gate Descriptor)。它的0-1,6-7 字节是偏移量,2-3 字节是选择符,4-5 字节是一些标志。
setup_idt:
lea ignore_int,%edx # 将ignore_int 的有效地址(偏移值)值??edx 寄存器
movl $0x00080000,%eax # 将选择符0x0008 置入eax 的高16 位中。
movw %dx,%ax /* selector = 0x0008 = cs */
# 偏移值的低16 位置入eax 的低16 位中。此时eax 含有
#门描述符低4 字节的值。
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
# 此时edx 含有门描述符高4 字节的值。
lea _idt,%edi # _idt 是中断描述符表的地址。
mov $256,%ecx
rp_sidt:
movl %eax,(%edi) # 将哑中断门描述符存入表中。
movl %edx,4(%edi)
addl $8,%edi # edi 指向表中下一项。
dec %ecx
jne rp_sidt
lidt idt_descr # 加载中断描述符表寄存器值。
ret

……

# 下面这几个入栈操作(pushl)用于为调用/init/main.c 程序和返回作准备。
# 前面3 个入栈指令不知道作什么用的,也许是Linus 用于在调试时能看清机器码用的?。
# 139 行的入栈操作是模拟调用main.c 程序时首先将返回地址入栈的操作,所以如果
# main.c 程序真的退出时,就会返回到这里的标号L6 处继续执行下去,也即死循环。
# 140 行将main.c 的地址压入堆栈,这样,在设置分页处理(setup_paging)结束后
# 执行'ret'返回指令时就会将main.c 程序的地址弹出堆栈,并去执行main.c 程序去了。
after_page_tables:
pushl $0 # These are the parameters to main :-)
pushl $0 # 这些是调用main 程序的参数(指init/main.c)。
pushl $0
pushl $L6 # return address for main, if it decides to.
pushl $_main # '_main'是编译程序对main 的内部表示方法。
jmp setup_paging # 跳转到 设置页表
L6:
jmp L6 # main should never return here, but
# just in case, we know what happens.
……

setup_paging执行ret后,会执行main函数
进入main后的栈位 0,0,0,L6
main函数的三个参数是0,0,0
main函数返回时进入L6,死循环

head.s 到main.c,从汇编跳到c语言。C语言执行时,最终也转换为汇编执行,所以 C语言的 函数间跳转,也是通过汇编实现的,与汇编跳转到C语言 没有本质区别。

C语言执行函数func时(假设func有3个参数 p1,p2,p3)
执行时,将 返回地址,参数压栈。然后 跳转到该函数处,执行该函数,
当函数执行完,遇到 结束的 } 汇编将其转为ret,从栈中弹出该 返回地址

after_page_tables中,pushl 压栈,栈如下:0 0 0 L6 _main
jmp setup_paging 跳转到 setup_paging,执行setup_paging 函数,执行完遇到ret 跳出该函数,将栈中的 _main 返回,0 0 0 是main函数的参数,返回到 L6,main函数是一个死循环

3. main

3.1 init/main.c

void main (void)        /* This really IS void, no error here. */
{               /* The startup routine assumes (well, ...) this */
  /* 这里确实是void,并没错。在startup 程序(head.s)中就是这样假设的。 */
……

  // 以下是内核进行所有方面的初始化工作。阅读时最好跟着调用的程序深入进去看,实在看
  // 不下去了,就先放一放,看下一个初始化调用 -- 这是经验之谈?。
  mem_init (main_memory_start, memory_end);
  trap_init ();         // 陷阱门(硬件中断向量)初始化。(kernel/traps.c,181 行)
  blk_dev_init ();      // 块设备初始化。 (kernel/blk_dev/ll_rw_blk.c,157 行)
  chr_dev_init ();      // 字符设备初始化。 (kernel/chr_dev/tty_io.c,347 行)
  tty_init ();          // tty 初始化。 (kernel/chr_dev/tty_io.c,105 行)
  time_init ();         // 设置开机启动时间??startup_time(见76 行)。
  sched_init ();        // 调度程序初始化(加载了任务0 的tr, ldtr) (kernel/sched.c,385)
  buffer_init (buffer_memory_end);  // 缓冲管理初始化,建内存链表等。(fs/buffer.c,348)
  hd_init ();           // 硬盘初始化。 (kernel/blk_dev/hd.c,343 行)
  floppy_init ();       // 软驱初始化。 (kernel/blk_dev/floppy.c,457 行)
  sti ();           // 所有初始化工作都做完了,开启中断。
  // 下面过程通过在堆栈中设置的参数,利用中断返回指令切换到任务0。
  move_to_user_mode ();     // 移到用户模式。 (include/asm/system.h,第1 行)
  if (!fork ())
    {               /* we count on this going ok 保证main一直不会退出*/
      init ();
    }
  /*
   * NOTE!! For any other task 'pause()' would mean we have to get a
   * signal to awaken, but task0 is the sole exception (see 'schedule()')
   * as task 0 gets activated at every idle moment (when no other tasks
   * can run). For task0 'pause()' just means we go check if some other
   * task can run, and if not we return here.
   */
  /* 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到一个信号才会返
   * 回就绪运行态,但任务0(task0)是唯一的意外情况(参见'schedule()'),因为任务0 在
   * 任何空闲时间里都会被激活(当没有其它任务在运行时),因此对于任务0'pause()'仅意味着
   * 我们返回来查看是否有其它任务可以运行,如果没有的话我们就回到这里,一直循环执行'pause()'。
   */
  for (;;)
    pause ();
}

main的工作就是 xx_init:内存、中断、设备、时钟、CPU等的初始化

3.2 mm/memory.c

用数据结构+算法 管理硬件
初始化了mem_map数组

//// 物理内存初始化。
// 参数:start_mem - 可用作分页处理的物理内存起始位置(已去除RAMDISK 所占内存空间等)。
// end_mem - 实际物理内存最大地址,决定了mem_map多长,有多大内存。
// 在该版的linux 内核中,最多能使用16Mb 的内存,大于16Mb 的内存将不于考虑,弃置不用。
// 0 - 1Mb 内存空间用于内核系统(其实是0-640Kb)。
void mem_init (long start_mem, long end_mem)
{
  int i;

  HIGH_MEMORY = end_mem;    // 设置内存最高端。
  for (i = 0; i < PAGING_PAGES; i++)    // 首先置所有页面为已占用(USED=100)状态,
    mem_map[i] = USED;      // 初始化mem_map数组,即将页面映射数组全置成USED。
  i = MAP_NR (start_mem);   // 然后计算可使用起始内存的页面号。
  end_mem -= start_mem;     // 再计算可分页处理的内存块大小。
  end_mem >>= 12;       // 右移12位,即减掉4k,是一页。从而计算出可用于分页处理的页面数。
  while (end_mem-- > 0)     // 最后将这些可用页面对应的页面映射数组清零。
        mem_map[i++] = 0;   // 将 mem_map 数组元素值 置0,1 吊事该内存使用了,0表示没用
}

4.操作系统的启动过程

  1. boot:将操作系统从磁盘读入
  2. setup:获得一些参数,启动保护模式
  3. head:初始化GDT表,页表,跳到main
  4. main:执行好多init,进行初始化,比如 mem_init:内存初始化

操作系统开机:读入曹组哦系统,完成初始化