1.1.1 BIOS启动
开始加电,CPU硬件逻辑设计为强行将CS的值位置位0xFFFF,IP的值位0x0000,这样CS:IP就指向了0xFFFF0.这个位置位于BIOS的地址访问。BIOS的rom(0xFE000-0xFFFFF)
即BIOS程序的入口地址为0xFFFF0.BIOS的第一条指令就在这里。
随着其中的程序的执行,接着屏幕会显示显卡信息,内存信息,即自检的过程。在这期间一个重要的内容就是要建立BIOS在内存中的中断相连表和中断服务程序。
BIOS在内存的最开始的位置用1KB的内存空间(共256项,每项4字节,分别位CS与IP的值)构建中断自己的中断向量表,并在其后面构建了大约8KB与中断向量表对应的中断服务程序。
加载内核就要用到其中的中断服务。
内核是分位三此加载的。
首先BIOS启动中断0x19,加载bootsect内容,1个扇区。,然后,setup和system模块在bootsect的之后下,加载进来,分别4个扇区和240个扇区。
int 0x19中断将软驱的0磁头0磁道1扇区的内容拷贝至0x07c00处。、
1.2.2 加载setup
bootsect规划内存。通常用高级语言编写应用程序的时候,代码和数据运行在什么地方放在内存的哪里,是否覆盖我们呢都不必关心,因为操作系统和编译器会规划号,但这里只能靠
操作系统的实际这自己规划内存。 在实模式下,最大寻址范围为1MB(cs:ip 构成20位)
bootsect启动程序把自身从0x07c00处复制到0x9000处 共256B.
第2步,借助BIOS的0x13中断,即磁盘服务程序,(这与前面的19中断的启动有电不同,19是BIOS程序里面的动作,而13是在bootsect中的代码)
首先传递参数。指定扇区数目和内存地址。载入4个扇区到bootsect的后面,即setup载入进来。
1.2。3加载system,
仍然使用13中断,和加载setup一样,把system的240个扇区加载进来,从内存的0x10000出靠后的120KB.
然后,bootsect完成任务,setup开始运行,首先它做的事情就是把机器系统数据加载到内存,包括光标位置和显示页面数据等 占据内存的0x90000-x901FD,共510个字节。这个位置是原理bootsect的位置,
1.3
接着要进入保护模式,首先要关闭中断,即把EFLAGS寄存器的中断位屏蔽。知道main函数能够适应保护模式下重新建立的中断服务体系。
关中段后,进行实模式下中断向量表和保护模式下中断描述副标 IDT的交接工作。
setup把位于0x10000处的system加载到内存的起始地址0x00000出了。这个位置原理存放这BIOS的中断向量表。
setup要对中断描述副寄存器IDTR和全局描述副寄存器GDTR初始化。
GDT中存放着每个任务的局部描述副LDT的地址和任务状态段TSS的地址,LDT用于完成进程的段的寻址,TSS用于完成中断过程的现场保护和现场恢复。
此时,内核尚未运行起来,还没有进程,现在的GDT的第一项位空,第二想为内核代码段描述符,第三项为内核数据段描述副。其余位空。IDT虽然已经设置,带为一张空表,在后续的
运行中会逐渐完善,因为此刻已经关闭中断,无须中断参与。
1.3.1
system模块复制到0x00000这个动作废除了BIOS的中断向量表,也就是废除了16位的中断机制。操作系统是不能没有中断的,对外设的使用,进程的调度都离不开中断。
linux是32位的现代操作系统,16位的中断机制对32位的操作系统是不合适的,这也是废除16位中断机制的根本原因。为此,需要建立新的中断机制。
1.3.1 设置中断描述附表和全局描述副表。
setup程序自身提供的数据信息对中断描述副表寄存器IDTR和全局描述副表寄存器GDTR进行初始化设置。
IDT,类似于实模式下的中断向量表 IDTR保存IDT的起始地址
32位和16位的中断机制有所不同,最明显的是16位的中断机制用的是中断向量表,中断向量表的起始位置是在0x00000出,这个位置是固定的,32位的中断机制用的是中断描述符IDT,位置不是固定的,可以有操作系统的设计者根据设计要求灵活安排,由IDTR寄存器来锁定其位置。
此时,因为内核尚未运行起来,还没有进程,所以现在创建的GDT表的第一项位空,第二想位内核代码段描述副(描述这个段的所有属性的),第三项位内核数据段描述符 ,其余皆为空 (都是8字节的数据)
创建的过程:
在设计内核代码时候,已经将两个表写好,并且把需要的数据也写好
将专用寄存器IDTR和GDTR指向两张表。
1.3.4
为了建立保护模式下的中断机制,setup将对可编程中断副按控制器8259A进行重新编程。
即有16个输入1个输出到cpu的那个寄存器。int 0x00-0x1f被intel留做内部(不可屏蔽)中断和异常中断,若不对其重新编程,int 0x00-int0x1f中断将被覆盖。即在保护模式下,IRQ0x00-IRQ0x0f的中断号位int0x20-int0x2f。如鼠标中断即0x2c中断号,中断副按请求号是IRQ12.
setup程序通过代码将CPU的工作方式设置位保护模式,将CR0的第0位置1,即设定处理器的工作方式位保护模式。
CR0寄存器,0号控制寄存器,存放系统控制标志,第0位是PE标志,(protected Model Enable,保护模式使能)标志,置1cpu工作在保护模式,置0时为实模式。
cpu工作方式变为保护模式,一个重要特征就是根据GDT表来决定以后执行哪里的程序(哪个段的程序)
我们知道,在GDT中,每一项都是8字节的信息,,他描述一个段的起始地址,段长,以及段的管理属性。
如,00C0 9A00 0000 07FF 这个表项,表示的段的信息位:
起始地址第 1 2 7 8 9 10 11 12 半字节表示的,这里位0x00000000,共32位
段的大小第 4 13 14 15 16 半字节表示,这里为0x007FF, 共20位
段的管理信息3 5 6 半字节表示,这里,0xC9A 位12位
其中,管理信息的解读是这样的,c 即1100 第一个位置1表示每个段长单元位4k,表示分页,这里
段长位0x007FF,即这个段长最长为0x007FF*4K=8M
9 即1001 中间的两个0表示这是特权级的段,即00,若为11表示是用户级
A 即1010 第一个1 表示这是个代码段,若是0表示是个数据段。
暂时知道的管理信息就是这么多,其他的位的解读有带发掘。
在保护模式下的寻址要有两个东西,其中一个就是这里的GDT中的信息,另一个就是段选择子。
这个段选择子也就是一个寄存器,不过阻塞实模式下他当作段寄存器使用,在保护模式下他用来选择
在GDT中的那个表项,这是一个16位的寄存器(所有的段寄存器如CS,ES,DS等都可以当作选择子,SS除外)
这个段寄存器怎么解读呢,
16位,
前13位位索引,即选择读GDT或LDT中的第几个表项
后面一位为选择GDT还是LDT,为0选择GDT,为1选择LDT
最后两位 位特权即00表示最高即,11表示最低级。这个数字是要和选择的GDT或LDT中的信息的管理属性中的特权级别比较的,如果它优先级低于管理属性中的优先级,则会被禁止访问,(保护??)
举个例子;
若21H:12345678H 位逻辑地址,在保护模式下如何转为线性地址。
首先,21H为段选择子中的值,12345678H为偏移地址
21即=0000000000100001 其中,前13位为4,中间一位为0表示访问GDT,后面两位位01,特权级别位1
假设GDT中的第4个选项的段表示的起始地址为11111111H(这个具体要看),那么转换就是
11111111H+12345678H=23456789H,此即线性地址。
在setup.s中末尾,有一句
jmpi 0,8 这里的0表示偏移,8为段选择子。
故整理得到,它是要跳转到GDT的第1项(开始位0项)中的段,而GDT中的第1项位内核代码段,起始地址位
0x00000000,偏移这里也位0,故跳转到内存的首地址中存放的head.s程序中开始执行。
至此,setup就完成了。
1.3.5 head.s开始执行
在system模块里面,既有内核程序,又有head程序,两者是紧挨,head在前面,占有25KB+184B的空间。且开始于0x00000这个位置,即开始。
head要做的事情,在它自身的位置处创建内核分页机制,即在0x00000出创建页目录,页表,GDT,IDT,并覆盖自身的内存空间,然后main开始执行。(GDT在setup中只是刚刚初始化了前面三项,后面的要用head来初始化,IDT还没有设置,也需要head来完成。)
现在,DS,ES,FS,GS,SS等其他寄存器也要从实模式转位保护模式(CS在jumpi 0,8中已经转为了保护模式了,把8付给了CS)。
head把10付给了上面的几个段寄存器。如果拆分10,会发现是把GDT中的第3项结合了他们。即内核数据段。(在GDT中,第二和第三的起始地址都是从00000开始的。。。?后面会有解释么?)
然后设置中断描述副表IDT,其中的每项都是8字节共64位的信息。
包含了对应的中断服务程序的段内偏移地址,岁在啊段选择符,段特权级别等信息。
不过刚刚开始的时候,这些项全部指向同一个地方,具体的对于服务程序是在main函数中完成的。
然后重新设置了GDT,一个重要的变化是每项的信息中段长修改位了FFF(第一项和第而项,后面的暂时没有设置)。且把GDT起始位置移动了,因为如果不宜动,后面会被覆盖掉。
而且还准备了4k的内核栈空间,即设置了ESP指针。
这时候,要把main函数的地址压入内核栈,目的使head程序执行完后通过ret指令可以直接执行main函数。
之后,head跳转到setup_pagin去执行,开始创建分页机制。
把页目录和4个页表放在物理内存的起始位置。从内存起始开始的5页空间内容全部清0(每页4KB),为初始化页目录和页表做准备。这个动作也就把head自身覆盖了。从0x00000-0x04FFFF.20KB
然后,设置页目录的前4项,使之指向分配的4个页面。
即页目录的第一项(4个字节)填入第一个页表的起始地址,0x1000 pg0,后面的12位还没有讲到
第二项 二 0x2000 pg1
,。。。
然后,将第4张页表的最有一个页表项(pg3+4092处)填入寻址范围的最后一个页面,(在保护模式下,
linux 0.11支持的最大寻址范围位0xFFFFFF,16MB),即这里填入0xFFF000,
第4个页表的最后一个页表项指向0xFFF000-0xFFFFFF这个页面。也即内存的最后一个页面。
然后,一次从高位地址设置,把4个页表都填满,自此,整个内存页面都有了映射。且4个页表自身也被页目录管理着。
这4个页表是内核专属的页表,将来每个用户进程都有他们专属的也便,两者在寻址范围方面的区别。后面详述。
最后需要把页目录的地址写入CR3寄存器,使之指向页目录。
CR3寄存器,3号32位的控制寄存器,高20位存放页目录的基地置(这里就是0x00000),当CR0中PG标志置位(即处于保护模式),CPU使用CR3指向的页目录和页表进行虚拟地址到物理地址的转换。
看看内存此时的布局:
0x00000-0x01000 页目录
0x10000-0x20000 页表0
0x20000-0x30000 页表1
0x30000-0x40000 页表2
0x40000-0x50000 页表3
0x50000-0x54000 软盘缓冲区
0x54000-0x54B8 空余
0x54B8-0x5CB8 中断描述副 2KB
0x5CB8-0x64B8 全局描述副 2KB
后面就是main函数了。。。。
最后一步,ret,跳入main函数执行
将main函数的地址赋值给EIP寄存器。即开始执行。
2.1开中断前的工作
2.1.2物理内存规划
除了1MB以内的内核区之外,其余物理内存要完成的工作是不同的,“主内存“主要用来承载进程的相关信息,包括进程管理,进程对于的程序等等。
”缓冲区“主要作为主机于外设进行数据的中转站”,“虚拟盘区“是可选的,
大体上可以这样认为:缓冲区在主内存的后面
2.1.4 对主内存初始化。
在linux中对内存是分页管理的。
系统通过一个叫做mem_map的数组记录每一个页面的使用情况。先将所有的内存页面使用次数全部设置位100,然后把主内存区域的页面使用次数清空为0.系统以后只把使用次数位0的页面视为空闲页面。
当然前1M的内核区域中的页面就不是空闲页,以后进程就操作不了的。
2.1.5 除0中断服务挂加
前面已经准备号了IDT表,但还没有初始化挂加中断服务程序的地址与其他一些信息(每个项8字节)。
具体就是:将每个中断服务程序的地址写道相应的中断描述副表IDT中制定的位置。其中的规则是可以查阅的。
那这套中断服务机制是怎样运行的?粗略过程。
一方面,硬件产生信号,并传送给8259A这个中断控制器,8259A对信号进行初步处理,让后视CPU执行情况传递中断信号。。
另一方面,CPU接受到信号,就打断正在执行的程序并通过中断描述副表找到具体的中断服务程序。
这里还有 压栈->IDTR->IDT->中断服务程序地址
2.1.6 初始化快设备请求项结构
进程想要与快设备进行沟通,就要构建此工作环境。
请求项管理结构requeset[32]位于内核数据段,设置该结构数组即可。每项包括使用的设备号,是读操作还是写操作,对应的缓冲快指针,下一请求项指针。
2.1.7 串口中断与显示器,键盘中断服务程序的挂接
两个串口,它们的中断处理程序是放在内核数据段的(所有的中断处理程序都放在那里)
即把它们的中断服务程序的地址放入IDT表中相应的项。
2.1.9 系统机会进程0
进程0的管理结构task_struct的母本已经在代码设计阶段事先设计好了。还要将其中的数据结构和全局描述副表等项挂接。
将其中的任务状态描述符表TSS和局部数据描述符表LDT挂接到全局描述符表GDT中。
这里先知道的:task[64]数组在内核数据段区,task_struct结构中有该进程的LDT表的位置和TSS表的位置。
这里的挂接是怎么回事呢?
在GDT中,我们知道是放入的整个内存各个段的描述符,现在又出现了LDT。起始,每个进程都有一个LDT,
LDT中的内容和GDT中的内容属性都是一样的,都是描述副段的信息,每个进程的LDT都是描述每个进程自身的段的信息的表项,如每个进程自身都有代码段,数据段等等。
那么GDT和LDT有什么关系呢?起始,可以把LDT想象成位一个段,LDT就是一张表,每张LDT都自成一段,
那么GDT就要描述该段(即LDT表)的信息吧,如某个LDT的起始地址,大小,管理属性等等。
也就是说,GDT中的某些项的信息是描述LDT的。
那么怎样利用GDT和LDT呢?
在CPU中只有一个GDTR,LDTR,他们存放GDT的地址和当前进程的LDT在GDT中的位置。
在转换地址中我们前面谈到过,如果段选择子的T1位为0,那么就是选择GDT,若位1,那么就是选择LDT。具体是怎样的呢?
收先看0,这时候只用到GDT,把段选择子的高13位索引取出,在GDT中索引出该段的基本信息就可以得到段的起始地址,即可。
在看1, 这时候用到LDT,但是首先还是的要找到GDT,因为LDT的地址是放在GDT中的。先从GDRT中找到GDT,然后,区LDTR中找,这个LDTR中的信息有点像段选择子中的信息,它也利用了前面的13位当作索引,这个索引在GDT中定位,得到当前进程的LDT的地址,即当前的LDT的描述副,取出其中的信息,找到LDT表。然后,在段选择子中的高13位派上用场,后面就和在GDT中找段一样了,在LDT中利用13位索引定位,找到相应的段的起始地址,再加上偏移,就可以了。
关键点在于每个LDT本身就是一段内存段。
解释完了LDT和GDT。进程0的前序工作就是把它的LDT的描述符在GDT中设置好,然后把LDTR中的值也设置好。
2.1.12 系统调用服务程序挂接
所有的系统调用都是通过以中断的形式来完成的,和前面的键盘,时钟中断挂接一样,其中断号位0x80.
系统调用也称为软中断,系统调用通过这个中断进一步找到系统调用的服务函数。system_call 是整个系统调用的入口函数。它与中断描述符表的0x80项结合起来。
2.1.14-15初始化硬盘软盘
也即把相应的中断服务程序与中断描述附表挂接。
2.1.16开中断
现在,系统所有的中断服务程序都已经和中断描述符表正常挂接了,这意味着中断服务体系已经建立完毕,系统可以在32位保护模式下处理中断信号了。
2.2
2.2.1 为进程0创建进程1做准备
在linux0.11中,除去进程0外,所有的进程都是由一个已有进程在用户太下完成创建的。
由于此前程序一直运行在内核态,故需要从特权级0跳到特权3.linux是通过模拟中断返回来实现特权即别的变化和创建进程0的。
通过将进程0的代码段选择子(进程0的代码已经编写好了)以及程序计数器EIP直接压入内核栈。
move_to_user_mode // 创建进程0,开始进入0号进程,切换到特权级别3运行
if (!fork()) {init ();} //创建进程1
move_to_user_mode
{
pushl $0x17
pushl %eax
pushl \n
pushl $0x0f
pushl $1f
iret
.......
}
在执行完iret后,这几个值会被返回到各个寄存器中,他们是ss ,esp,eflags,cs,eip
先看ss :0x17,因为ss是个段寄存器,它代表着保护模式下的选择子,再看0x17,翻译过来就是
00010111,看出来,这个段的特权即别位11,T1=1,查询LDT,索引位0010,即从LDT中的第3项中得到有关用户栈段的;描述符。
再看cs:0x0f 同理,cs表示的是代码段选择子,拆开可以看到是查询LDT,索引位2,从LDT的第2项中得到0进程的代码段描述符,也是用户级别。
这样在加上eip的值,进程0就开始从cs:eip处开始运行了。
进程0执行fork函数,开始创建进程1,这是一个系统调用,故用到了软中断号0x80。系统调用服务程序的入口函数system_call 开始执行。 然后从系统调用函数表sys_call_table中找到fork函数的服务函数地址。sys_call_talbe中关于fork调用的偏移是2,把2要复制给eax,准备完毕。
然后就开始int 0x80 ,这样进程0就进入了内核态。cs中的特权置由3到0.
因为是系统调用,故需要把一些列的寄存器压栈,,这里是压入进程0的栈中,中断完成后,还要恢复的。
2.2.2为进程1申请一个空闲位并获取进程号。 这还是sys_fork()函数做的工作。
即在task[64]数组中,
内核使用全局变量last_pid来存放系统自开机以来累计的进程数目,task数组中存放的是每个进程的
task_struct结构指针。
这样就得到了task的第二个位置来放入进程1的task_struct结构的地址。
2.2.4初步设置进程1的管理结构。既然是在fork()函数中
进程1的管理结构是怎样来的呢?是从进程0的管理结构中复制过来的,那么进程0的管理结构在哪里呢?
注意,进程0的管理结构在前面没有提到,这里,说明,即进程0的task_struct 有一个专门的名字,
叫做init_task,这个结构体是放在内核数据区的。但是,其他的进程的task_struct不是放在内核数据区而是放在主内存中的,例如下面要说的进程1,
首先,调用函数get_free_page在主内存的最末端申请一个空页面,这个页面是要放入进程1的task_struct结构体的。然后,把进程0的task_struct即init_task这个结构体中的管理信息大部分复制到这个页面。同时,把这个页面的地址放入task【1】数组中。(放入进程的task_struct的地址)。
2.2.5,现在假设在创建进程1的时候发生了时钟中断,
由于是在进程0中发生的时钟中断,现在,中断发生了,对于的中断服务程序将开始,于是系统先把在进程0
中的各个寄存器都压入进程0的task_struct中,以便保存被打断的“现场”。然后因为是时钟中断,它是要切换程序而发生的,这个时钟中断的中断服务程序开始判断是否要进行进程切换。
每个进程都有一个时间片,这个时间片是放在task_struct结构中的,时钟中断服务程序会检查当前的进程的时间片,减去1,如果为0了就要切换进程,否则,不会切换。
这里,在代码设计的时候把进程0的时间片实际位了15,故发送时钟中断不会切换。恢复栈中的各个寄存器后继续运行进程0.继续完成创建进程1的工作。
2.2.7,调整进程0的task_struct中的内容。
因为进程0中的管理信息不宜定全部适应进程1,故需要进行调整。比如进程号,进程的父亲进程号。
2.2.8 设置进程1的页目录和页表。 仍在fork()中。
一般,每个进程将来都可能家族属于字节的程序代码,故需要空间页面。
每个进程执行时候,都要根据其线性地址来进行寻址,然后转为物理地址(因为是运用了虚拟储存),将这个线性地址转位“页目录项”,“页表项” “页内偏移”,
线性地址是根据逻辑地址得到的,通过前面的GDT或LDT。前面叙述过。
(插:task_struct结构体中有本进程的LDT和TSS结构)
进程0在前面有4个页表,现在,在内村中申请一个空的页面,用来当作进程1的页表。这个页表的内容是怎样的呢?它是从进程0的页表0中复制了前面160项(每张页表共1024项)到自己的页表中的,
那么意味着这时候的进程1和进程0有着同样的640KB的页面空间是共用的。(160*4KB=640KB)。(复制160项只是在进程0创建进程1的时候特有的,其他进程的fork时,会把自己的页表项全部1024项都复制。)
还要,这是把进程0的代码段和数据段的基地址放入了进程0的task_struct结构中相应的地方。
2.2.9继续调整进程1的task_struct结构
比如打开了那些文件,进程的当前工作目录i节点等等,由于进程0中这些数据都还是ikong的,进程0只具备在主机中运算的能力,还不具备于外设和以文件形式进行交互的能力,故这些在进程1中也暂时不舍置。
之后,把进程1的任务状态描述副和局部描述符挂接在全局描述副中。(前面说过,每个LDT是潜在GDT中的,可以把LDT当作一个段来看待,TSS也和LDT可以用一样的性质(TSS是一个段,叫做任务状态段,故要挂接到GDT上)
下面叙述TSS,(TSS这个段,可以当作一个段吧,它放在task_struct结构体中)p97页),用来记录当前进程执行时候对应的寄存器的数据的,这写数据在进程切换时候即发挥作用,比如进程A切换到B时候,那么系统就将此时CPU中各个寄存器的数值,保存在进程A的任务状态描述副表中。以便将来
调度A时候可以继续无误无重复地执行A。之后,再用进程B中的TSS里面的数据来设置各个寄存器的置。
局部数据表述副表LDT,记录当前进程对于程序的代码段和数据段信息。比如代码的基地址。
系统将来通过GDT表中挂接的TSS描述符和LDT描述符来与当前进程建立来年系。这里将进程1的TSS和LDT挂接在全局描述符表GDT中,标志这系统从此具备操作进程1的能力。
此致,fork任务基本已经完成。进程1可以参与运行程序和调度了。接着要从进程0切换到进程1,由进程1来完成所后续工作。
2.2.10 进程0到进程1的切换
在进程0中开始系统调用pause,最终也会产生软中断,并映射到一个叫做sys_pause的系统函数上面。sys_pause中会用调度函数schedule()
shedule对task数组中的进程开始进行审视,判断各个进程的调度条件。现在看到只有进程0和进程1,且原先把进程0设置了可中断等待,进程1就绪状态,于是调用
switch_to(next),准备切换到进程1区执行。
在switch_to(next)zhong ,将cpu中的各个寄存器的值(于进程0有关的)放入进程0的TSS中。并用进程1的TSS来加载各个寄存器,同理,LDTR的值也被换掉了。
接下来,进程1开始工作,它将进一步构建环境,使得系统能够以文件的形式与外设交互。
2.3加载根文件系统
由于任何对文件的操作,从根本伤讲,都要依靠根文件系统的支持,所以进程1通过加载根文件系统达到这个目的。
前面构建了与硬盘和软盘沟通的环境,这位加载根文件系统提供了前提和保障。
进程1开始执行。执行函数init(),在init()中调用了系统调用setup函数,映射到调用函数sys_setup函数,即初始化设置
与硬盘操作相关的一些数据结构,如hd_info,hd ,drive_info等,都放在内核的数据区。其中hd_info是硬盘信息控制结构,
然后,在把硬盘的引导块读入缓冲区。(缓冲区在前面设置了。。。)
下面补充一下2.1.13节的缓冲区部分。
在内核数据区,有一个哈系表,有307项的位置。
在紧靠内核的后面,是缓冲快管理结构,由于它管理着内存中的3000多个缓冲快,因此它占据的空间和内核差不多大小。
系统就是通过对哈系表和空闲表(缓冲区管理结构)对缓冲区进行管理的。p63页。
硬盘的第0号逻辑快,即最开始的两个扇区1KB,那里存储着引导块的内容。
首先申请一个缓冲快。然后,让设备号与快号(这里就是0号硬盘的0号逻辑块)与该缓冲快对应,然后,将该缓冲快的地址放入哈系表中的空闲位置。
中间有多步骤是说明进程1如何将引导块读入缓冲快的,很复杂。这里假设已经读入指定的缓冲快了。
引导块的最后两个字节也是55AA.之后,从引导块中的分区表信息来设置硬盘分区管理结构。
5.1.6 p185 页。
在创建进程的时候,为何是父进程的页表原风不懂地复制给新进程呢?那不是让新进程和父进程的代码和数据都相同吗?先开始是这样的,因为新进程刚刚开始执行时并没有自己的程序,这就需要加载自己的程序。
这个过程是调用execve函数,但是execve函数代码呢?很显然,在父进程中。(这里以shell进程为父进程为例)故新进程一旦开始执行,必须要有能力执行shell程序里面的代码,故此。
当加载完自己的程序后,其页面就会发生相应的变化了。
补充:
call 0x12345 ,相当于执行了pushl %eip和movl $0x12345 ,%eip,这里0x12345即为要跳转的指令。但首先要把%eip的值压入栈,即当前要执行的指令。然后,把0x12345的值放入%eip。
ret ,这里执行的指令相当于 popl %eip ,即把栈定的原先的eip的值现在放入eip中去。即恢复执行。
leave,在实际的程序当中经常执行,相当于 movl %ebp,%esp,和popl %ebp 两天指令,ebp置栈的基址寄存器,esp是栈的顶端寄存器。先将栈基地址放入栈顶寄存器,然后,把栈定的值放入ebp中。
实现了由一个任务返回到调用它的任务的内存栈空间的操作。
在main函数的第一句(反汇编后)是push %ebp,可以看到,当前的ebp保存的是前一个进程执行时候所在内存栈空间的基地置,因此为了保证这个当前进程结束后,CPU能够正确恢复到之前的进程中去执行,
必须对其进行保存,这里的压栈就是这个目的。
插:
x86内存管理机制: 分为 分段机制和分页机制
分段机制是x86的特殊的段机制。也正是因为x86存在着段机制,故还有一种额外的地址---逻辑地址。(每个进程都有0--4GB的线性地址空间)
分段机制将内存划分成以起始地址和长度描述的块。
段可以和程序最基本的元素联系起来,因为程序在编译后就划分位了代码段,数据段和栈等,而段机制中刚刚好相应的代码段,数据段 栈段等等。
程序中使用逻辑地址,CPU使用的是线性地址, 故要从逻辑--线性转换 。(分段机制就是干这个的)
分段机制中有 逻辑地址,段选择子,段描述符 段描述附表 4个元素。
当程序使用逻辑地址访问内存时候,(比如,int *p=&a,这里p就是存储的a的逻辑地址,事实上存储的仅是逻辑地址的偏移,而相应的段选择符位于了段寄存器中。),cpu通过逻辑地址中的段选择子
来索引段描述附表,得到相应的段描述符,(描述符中有段的基址。)在加上偏移,就得到了相应的线性地址(虚拟地址)。(它存在于这个程序的0-4GB的线性地址空间中)。
因为逻辑地址覆盖了编译后的程序的各个段,如代码段,数据段,栈段等,故段选择子也相应有多个,如CS专门放入代码段的段选择子,DS放入数据段的段选择子,SS放入栈的段选择子,在上面的例子中,因为p引用的是数据a,
故可想见,这个逻辑偏移的段选择子应该是DS。 此外ES,FS,GS可以自由选用。
分页机制 (线性地址---物理地址)
分页机制的元素 页表 CR3寄存器 TLB 三个部件。
页表即线性页面到物理页面的映射
采用二级分页机制:其中页目录项和页表项都是4KB=32位,但从页目录项找页表位置和从页表找页都只用了高20位,在32位中的最低位位P位,表示物理页面是否在物理内存中,否则要发生缺页中断。。
页目录-》页目录项-》页表-》页表项-》页
故一个进程在运行时候,须将自己页目录的基址放入CR3寄存器中。(4GB的线性空间,每个进程都需要一个页目录:1K*1K*4K=4GB).
TLB中存管的是最近常用的从线性页面到物理页面的映射的表,可以加快转换速度。
进程上下文 p36
此处只关心和虚拟技术关系最紧密的概念--上下文
上下文是从CPU角度引出的。简单地说,上下文就是程序运行时候所需要的最小的寄存器集合。这些寄存器的后卖弄一般代表这程序运行的一类资源,例如LDTR就代表某个进程使用的段信息,CR3寄存器就代表进程的私有的线性地址空间(分页机制启动)
x86架构下的上下文寄存器包括,通用寄存器EAX EBX ECX EDX ESI EDI ESP EBP等,段相关CS DS SS ES FS GS 标志寄存器EFLAGS 程序指针寄存器EIP GDT基址 GDTR ,LDTR TR CR系列
上下文切换
指程序从一种状态切换到另一种状态(如用户态和内核态的切换)或从一个程序切换到另一个程序,如进程切换,导致上下文相关寄存器值的变化行为。
进程切换时候通常是全上下文的切换,例如把CR3切换成进程页目录的地址。EIP指向新进程的第一条指令。
中断上下文切换:通常只有部分的上下文切换,须对栈指针和EIP等值写过,而CR3不许要修改
用户太内核太切换:也只需要部分的上下文切换,如从用户栈切换到内核栈。(哪些寄存器会变化呢?)
开始加电,CPU硬件逻辑设计为强行将CS的值位置位0xFFFF,IP的值位0x0000,这样CS:IP就指向了0xFFFF0.这个位置位于BIOS的地址访问。BIOS的rom(0xFE000-0xFFFFF)
即BIOS程序的入口地址为0xFFFF0.BIOS的第一条指令就在这里。
随着其中的程序的执行,接着屏幕会显示显卡信息,内存信息,即自检的过程。在这期间一个重要的内容就是要建立BIOS在内存中的中断相连表和中断服务程序。
BIOS在内存的最开始的位置用1KB的内存空间(共256项,每项4字节,分别位CS与IP的值)构建中断自己的中断向量表,并在其后面构建了大约8KB与中断向量表对应的中断服务程序。
加载内核就要用到其中的中断服务。
内核是分位三此加载的。
首先BIOS启动中断0x19,加载bootsect内容,1个扇区。,然后,setup和system模块在bootsect的之后下,加载进来,分别4个扇区和240个扇区。
int 0x19中断将软驱的0磁头0磁道1扇区的内容拷贝至0x07c00处。、
1.2.2 加载setup
bootsect规划内存。通常用高级语言编写应用程序的时候,代码和数据运行在什么地方放在内存的哪里,是否覆盖我们呢都不必关心,因为操作系统和编译器会规划号,但这里只能靠
操作系统的实际这自己规划内存。 在实模式下,最大寻址范围为1MB(cs:ip 构成20位)
bootsect启动程序把自身从0x07c00处复制到0x9000处 共256B.
第2步,借助BIOS的0x13中断,即磁盘服务程序,(这与前面的19中断的启动有电不同,19是BIOS程序里面的动作,而13是在bootsect中的代码)
首先传递参数。指定扇区数目和内存地址。载入4个扇区到bootsect的后面,即setup载入进来。
1.2。3加载system,
仍然使用13中断,和加载setup一样,把system的240个扇区加载进来,从内存的0x10000出靠后的120KB.
然后,bootsect完成任务,setup开始运行,首先它做的事情就是把机器系统数据加载到内存,包括光标位置和显示页面数据等 占据内存的0x90000-x901FD,共510个字节。这个位置是原理bootsect的位置,
1.3
接着要进入保护模式,首先要关闭中断,即把EFLAGS寄存器的中断位屏蔽。知道main函数能够适应保护模式下重新建立的中断服务体系。
关中段后,进行实模式下中断向量表和保护模式下中断描述副标 IDT的交接工作。
setup把位于0x10000处的system加载到内存的起始地址0x00000出了。这个位置原理存放这BIOS的中断向量表。
setup要对中断描述副寄存器IDTR和全局描述副寄存器GDTR初始化。
GDT中存放着每个任务的局部描述副LDT的地址和任务状态段TSS的地址,LDT用于完成进程的段的寻址,TSS用于完成中断过程的现场保护和现场恢复。
此时,内核尚未运行起来,还没有进程,现在的GDT的第一项位空,第二想为内核代码段描述符,第三项为内核数据段描述副。其余位空。IDT虽然已经设置,带为一张空表,在后续的
运行中会逐渐完善,因为此刻已经关闭中断,无须中断参与。
1.3.1
system模块复制到0x00000这个动作废除了BIOS的中断向量表,也就是废除了16位的中断机制。操作系统是不能没有中断的,对外设的使用,进程的调度都离不开中断。
linux是32位的现代操作系统,16位的中断机制对32位的操作系统是不合适的,这也是废除16位中断机制的根本原因。为此,需要建立新的中断机制。
1.3.1 设置中断描述附表和全局描述副表。
setup程序自身提供的数据信息对中断描述副表寄存器IDTR和全局描述副表寄存器GDTR进行初始化设置。
IDT,类似于实模式下的中断向量表 IDTR保存IDT的起始地址
32位和16位的中断机制有所不同,最明显的是16位的中断机制用的是中断向量表,中断向量表的起始位置是在0x00000出,这个位置是固定的,32位的中断机制用的是中断描述符IDT,位置不是固定的,可以有操作系统的设计者根据设计要求灵活安排,由IDTR寄存器来锁定其位置。
此时,因为内核尚未运行起来,还没有进程,所以现在创建的GDT表的第一项位空,第二想位内核代码段描述副(描述这个段的所有属性的),第三项位内核数据段描述符 ,其余皆为空 (都是8字节的数据)
创建的过程:
在设计内核代码时候,已经将两个表写好,并且把需要的数据也写好
将专用寄存器IDTR和GDTR指向两张表。
1.3.4
为了建立保护模式下的中断机制,setup将对可编程中断副按控制器8259A进行重新编程。
即有16个输入1个输出到cpu的那个寄存器。int 0x00-0x1f被intel留做内部(不可屏蔽)中断和异常中断,若不对其重新编程,int 0x00-int0x1f中断将被覆盖。即在保护模式下,IRQ0x00-IRQ0x0f的中断号位int0x20-int0x2f。如鼠标中断即0x2c中断号,中断副按请求号是IRQ12.
setup程序通过代码将CPU的工作方式设置位保护模式,将CR0的第0位置1,即设定处理器的工作方式位保护模式。
CR0寄存器,0号控制寄存器,存放系统控制标志,第0位是PE标志,(protected Model Enable,保护模式使能)标志,置1cpu工作在保护模式,置0时为实模式。
cpu工作方式变为保护模式,一个重要特征就是根据GDT表来决定以后执行哪里的程序(哪个段的程序)
我们知道,在GDT中,每一项都是8字节的信息,,他描述一个段的起始地址,段长,以及段的管理属性。
如,00C0 9A00 0000 07FF 这个表项,表示的段的信息位:
起始地址第 1 2 7 8 9 10 11 12 半字节表示的,这里位0x00000000,共32位
段的大小第 4 13 14 15 16 半字节表示,这里为0x007FF, 共20位
段的管理信息3 5 6 半字节表示,这里,0xC9A 位12位
其中,管理信息的解读是这样的,c 即1100 第一个位置1表示每个段长单元位4k,表示分页,这里
段长位0x007FF,即这个段长最长为0x007FF*4K=8M
9 即1001 中间的两个0表示这是特权级的段,即00,若为11表示是用户级
A 即1010 第一个1 表示这是个代码段,若是0表示是个数据段。
暂时知道的管理信息就是这么多,其他的位的解读有带发掘。
在保护模式下的寻址要有两个东西,其中一个就是这里的GDT中的信息,另一个就是段选择子。
这个段选择子也就是一个寄存器,不过阻塞实模式下他当作段寄存器使用,在保护模式下他用来选择
在GDT中的那个表项,这是一个16位的寄存器(所有的段寄存器如CS,ES,DS等都可以当作选择子,SS除外)
这个段寄存器怎么解读呢,
16位,
前13位位索引,即选择读GDT或LDT中的第几个表项
后面一位为选择GDT还是LDT,为0选择GDT,为1选择LDT
最后两位 位特权即00表示最高即,11表示最低级。这个数字是要和选择的GDT或LDT中的信息的管理属性中的特权级别比较的,如果它优先级低于管理属性中的优先级,则会被禁止访问,(保护??)
举个例子;
若21H:12345678H 位逻辑地址,在保护模式下如何转为线性地址。
首先,21H为段选择子中的值,12345678H为偏移地址
21即=0000000000100001 其中,前13位为4,中间一位为0表示访问GDT,后面两位位01,特权级别位1
假设GDT中的第4个选项的段表示的起始地址为11111111H(这个具体要看),那么转换就是
11111111H+12345678H=23456789H,此即线性地址。
在setup.s中末尾,有一句
jmpi 0,8 这里的0表示偏移,8为段选择子。
故整理得到,它是要跳转到GDT的第1项(开始位0项)中的段,而GDT中的第1项位内核代码段,起始地址位
0x00000000,偏移这里也位0,故跳转到内存的首地址中存放的head.s程序中开始执行。
至此,setup就完成了。
1.3.5 head.s开始执行
在system模块里面,既有内核程序,又有head程序,两者是紧挨,head在前面,占有25KB+184B的空间。且开始于0x00000这个位置,即开始。
head要做的事情,在它自身的位置处创建内核分页机制,即在0x00000出创建页目录,页表,GDT,IDT,并覆盖自身的内存空间,然后main开始执行。(GDT在setup中只是刚刚初始化了前面三项,后面的要用head来初始化,IDT还没有设置,也需要head来完成。)
现在,DS,ES,FS,GS,SS等其他寄存器也要从实模式转位保护模式(CS在jumpi 0,8中已经转为了保护模式了,把8付给了CS)。
head把10付给了上面的几个段寄存器。如果拆分10,会发现是把GDT中的第3项结合了他们。即内核数据段。(在GDT中,第二和第三的起始地址都是从00000开始的。。。?后面会有解释么?)
然后设置中断描述副表IDT,其中的每项都是8字节共64位的信息。
包含了对应的中断服务程序的段内偏移地址,岁在啊段选择符,段特权级别等信息。
不过刚刚开始的时候,这些项全部指向同一个地方,具体的对于服务程序是在main函数中完成的。
然后重新设置了GDT,一个重要的变化是每项的信息中段长修改位了FFF(第一项和第而项,后面的暂时没有设置)。且把GDT起始位置移动了,因为如果不宜动,后面会被覆盖掉。
而且还准备了4k的内核栈空间,即设置了ESP指针。
这时候,要把main函数的地址压入内核栈,目的使head程序执行完后通过ret指令可以直接执行main函数。
之后,head跳转到setup_pagin去执行,开始创建分页机制。
把页目录和4个页表放在物理内存的起始位置。从内存起始开始的5页空间内容全部清0(每页4KB),为初始化页目录和页表做准备。这个动作也就把head自身覆盖了。从0x00000-0x04FFFF.20KB
然后,设置页目录的前4项,使之指向分配的4个页面。
即页目录的第一项(4个字节)填入第一个页表的起始地址,0x1000 pg0,后面的12位还没有讲到
第二项 二 0x2000 pg1
,。。。
然后,将第4张页表的最有一个页表项(pg3+4092处)填入寻址范围的最后一个页面,(在保护模式下,
linux 0.11支持的最大寻址范围位0xFFFFFF,16MB),即这里填入0xFFF000,
第4个页表的最后一个页表项指向0xFFF000-0xFFFFFF这个页面。也即内存的最后一个页面。
然后,一次从高位地址设置,把4个页表都填满,自此,整个内存页面都有了映射。且4个页表自身也被页目录管理着。
这4个页表是内核专属的页表,将来每个用户进程都有他们专属的也便,两者在寻址范围方面的区别。后面详述。
最后需要把页目录的地址写入CR3寄存器,使之指向页目录。
CR3寄存器,3号32位的控制寄存器,高20位存放页目录的基地置(这里就是0x00000),当CR0中PG标志置位(即处于保护模式),CPU使用CR3指向的页目录和页表进行虚拟地址到物理地址的转换。
看看内存此时的布局:
0x00000-0x01000 页目录
0x10000-0x20000 页表0
0x20000-0x30000 页表1
0x30000-0x40000 页表2
0x40000-0x50000 页表3
0x50000-0x54000 软盘缓冲区
0x54000-0x54B8 空余
0x54B8-0x5CB8 中断描述副 2KB
0x5CB8-0x64B8 全局描述副 2KB
后面就是main函数了。。。。
最后一步,ret,跳入main函数执行
将main函数的地址赋值给EIP寄存器。即开始执行。
2.1开中断前的工作
2.1.2物理内存规划
除了1MB以内的内核区之外,其余物理内存要完成的工作是不同的,“主内存“主要用来承载进程的相关信息,包括进程管理,进程对于的程序等等。
”缓冲区“主要作为主机于外设进行数据的中转站”,“虚拟盘区“是可选的,
大体上可以这样认为:缓冲区在主内存的后面
2.1.4 对主内存初始化。
在linux中对内存是分页管理的。
系统通过一个叫做mem_map的数组记录每一个页面的使用情况。先将所有的内存页面使用次数全部设置位100,然后把主内存区域的页面使用次数清空为0.系统以后只把使用次数位0的页面视为空闲页面。
当然前1M的内核区域中的页面就不是空闲页,以后进程就操作不了的。
2.1.5 除0中断服务挂加
前面已经准备号了IDT表,但还没有初始化挂加中断服务程序的地址与其他一些信息(每个项8字节)。
具体就是:将每个中断服务程序的地址写道相应的中断描述副表IDT中制定的位置。其中的规则是可以查阅的。
那这套中断服务机制是怎样运行的?粗略过程。
一方面,硬件产生信号,并传送给8259A这个中断控制器,8259A对信号进行初步处理,让后视CPU执行情况传递中断信号。。
另一方面,CPU接受到信号,就打断正在执行的程序并通过中断描述副表找到具体的中断服务程序。
这里还有 压栈->IDTR->IDT->中断服务程序地址
2.1.6 初始化快设备请求项结构
进程想要与快设备进行沟通,就要构建此工作环境。
请求项管理结构requeset[32]位于内核数据段,设置该结构数组即可。每项包括使用的设备号,是读操作还是写操作,对应的缓冲快指针,下一请求项指针。
2.1.7 串口中断与显示器,键盘中断服务程序的挂接
两个串口,它们的中断处理程序是放在内核数据段的(所有的中断处理程序都放在那里)
即把它们的中断服务程序的地址放入IDT表中相应的项。
2.1.9 系统机会进程0
进程0的管理结构task_struct的母本已经在代码设计阶段事先设计好了。还要将其中的数据结构和全局描述副表等项挂接。
将其中的任务状态描述符表TSS和局部数据描述符表LDT挂接到全局描述符表GDT中。
这里先知道的:task[64]数组在内核数据段区,task_struct结构中有该进程的LDT表的位置和TSS表的位置。
这里的挂接是怎么回事呢?
在GDT中,我们知道是放入的整个内存各个段的描述符,现在又出现了LDT。起始,每个进程都有一个LDT,
LDT中的内容和GDT中的内容属性都是一样的,都是描述副段的信息,每个进程的LDT都是描述每个进程自身的段的信息的表项,如每个进程自身都有代码段,数据段等等。
那么GDT和LDT有什么关系呢?起始,可以把LDT想象成位一个段,LDT就是一张表,每张LDT都自成一段,
那么GDT就要描述该段(即LDT表)的信息吧,如某个LDT的起始地址,大小,管理属性等等。
也就是说,GDT中的某些项的信息是描述LDT的。
那么怎样利用GDT和LDT呢?
在CPU中只有一个GDTR,LDTR,他们存放GDT的地址和当前进程的LDT在GDT中的位置。
在转换地址中我们前面谈到过,如果段选择子的T1位为0,那么就是选择GDT,若位1,那么就是选择LDT。具体是怎样的呢?
收先看0,这时候只用到GDT,把段选择子的高13位索引取出,在GDT中索引出该段的基本信息就可以得到段的起始地址,即可。
在看1, 这时候用到LDT,但是首先还是的要找到GDT,因为LDT的地址是放在GDT中的。先从GDRT中找到GDT,然后,区LDTR中找,这个LDTR中的信息有点像段选择子中的信息,它也利用了前面的13位当作索引,这个索引在GDT中定位,得到当前进程的LDT的地址,即当前的LDT的描述副,取出其中的信息,找到LDT表。然后,在段选择子中的高13位派上用场,后面就和在GDT中找段一样了,在LDT中利用13位索引定位,找到相应的段的起始地址,再加上偏移,就可以了。
关键点在于每个LDT本身就是一段内存段。
解释完了LDT和GDT。进程0的前序工作就是把它的LDT的描述符在GDT中设置好,然后把LDTR中的值也设置好。
2.1.12 系统调用服务程序挂接
所有的系统调用都是通过以中断的形式来完成的,和前面的键盘,时钟中断挂接一样,其中断号位0x80.
系统调用也称为软中断,系统调用通过这个中断进一步找到系统调用的服务函数。system_call 是整个系统调用的入口函数。它与中断描述符表的0x80项结合起来。
2.1.14-15初始化硬盘软盘
也即把相应的中断服务程序与中断描述附表挂接。
2.1.16开中断
现在,系统所有的中断服务程序都已经和中断描述符表正常挂接了,这意味着中断服务体系已经建立完毕,系统可以在32位保护模式下处理中断信号了。
2.2
2.2.1 为进程0创建进程1做准备
在linux0.11中,除去进程0外,所有的进程都是由一个已有进程在用户太下完成创建的。
由于此前程序一直运行在内核态,故需要从特权级0跳到特权3.linux是通过模拟中断返回来实现特权即别的变化和创建进程0的。
通过将进程0的代码段选择子(进程0的代码已经编写好了)以及程序计数器EIP直接压入内核栈。
move_to_user_mode // 创建进程0,开始进入0号进程,切换到特权级别3运行
if (!fork()) {init ();} //创建进程1
move_to_user_mode
{
pushl $0x17
pushl %eax
pushl \n
pushl $0x0f
pushl $1f
iret
.......
}
在执行完iret后,这几个值会被返回到各个寄存器中,他们是ss ,esp,eflags,cs,eip
先看ss :0x17,因为ss是个段寄存器,它代表着保护模式下的选择子,再看0x17,翻译过来就是
00010111,看出来,这个段的特权即别位11,T1=1,查询LDT,索引位0010,即从LDT中的第3项中得到有关用户栈段的;描述符。
再看cs:0x0f 同理,cs表示的是代码段选择子,拆开可以看到是查询LDT,索引位2,从LDT的第2项中得到0进程的代码段描述符,也是用户级别。
这样在加上eip的值,进程0就开始从cs:eip处开始运行了。
进程0执行fork函数,开始创建进程1,这是一个系统调用,故用到了软中断号0x80。系统调用服务程序的入口函数system_call 开始执行。 然后从系统调用函数表sys_call_table中找到fork函数的服务函数地址。sys_call_talbe中关于fork调用的偏移是2,把2要复制给eax,准备完毕。
然后就开始int 0x80 ,这样进程0就进入了内核态。cs中的特权置由3到0.
因为是系统调用,故需要把一些列的寄存器压栈,,这里是压入进程0的栈中,中断完成后,还要恢复的。
2.2.2为进程1申请一个空闲位并获取进程号。 这还是sys_fork()函数做的工作。
即在task[64]数组中,
内核使用全局变量last_pid来存放系统自开机以来累计的进程数目,task数组中存放的是每个进程的
task_struct结构指针。
这样就得到了task的第二个位置来放入进程1的task_struct结构的地址。
2.2.4初步设置进程1的管理结构。既然是在fork()函数中
进程1的管理结构是怎样来的呢?是从进程0的管理结构中复制过来的,那么进程0的管理结构在哪里呢?
注意,进程0的管理结构在前面没有提到,这里,说明,即进程0的task_struct 有一个专门的名字,
叫做init_task,这个结构体是放在内核数据区的。但是,其他的进程的task_struct不是放在内核数据区而是放在主内存中的,例如下面要说的进程1,
首先,调用函数get_free_page在主内存的最末端申请一个空页面,这个页面是要放入进程1的task_struct结构体的。然后,把进程0的task_struct即init_task这个结构体中的管理信息大部分复制到这个页面。同时,把这个页面的地址放入task【1】数组中。(放入进程的task_struct的地址)。
2.2.5,现在假设在创建进程1的时候发生了时钟中断,
由于是在进程0中发生的时钟中断,现在,中断发生了,对于的中断服务程序将开始,于是系统先把在进程0
中的各个寄存器都压入进程0的task_struct中,以便保存被打断的“现场”。然后因为是时钟中断,它是要切换程序而发生的,这个时钟中断的中断服务程序开始判断是否要进行进程切换。
每个进程都有一个时间片,这个时间片是放在task_struct结构中的,时钟中断服务程序会检查当前的进程的时间片,减去1,如果为0了就要切换进程,否则,不会切换。
这里,在代码设计的时候把进程0的时间片实际位了15,故发送时钟中断不会切换。恢复栈中的各个寄存器后继续运行进程0.继续完成创建进程1的工作。
2.2.7,调整进程0的task_struct中的内容。
因为进程0中的管理信息不宜定全部适应进程1,故需要进行调整。比如进程号,进程的父亲进程号。
2.2.8 设置进程1的页目录和页表。 仍在fork()中。
一般,每个进程将来都可能家族属于字节的程序代码,故需要空间页面。
每个进程执行时候,都要根据其线性地址来进行寻址,然后转为物理地址(因为是运用了虚拟储存),将这个线性地址转位“页目录项”,“页表项” “页内偏移”,
线性地址是根据逻辑地址得到的,通过前面的GDT或LDT。前面叙述过。
(插:task_struct结构体中有本进程的LDT和TSS结构)
进程0在前面有4个页表,现在,在内村中申请一个空的页面,用来当作进程1的页表。这个页表的内容是怎样的呢?它是从进程0的页表0中复制了前面160项(每张页表共1024项)到自己的页表中的,
那么意味着这时候的进程1和进程0有着同样的640KB的页面空间是共用的。(160*4KB=640KB)。(复制160项只是在进程0创建进程1的时候特有的,其他进程的fork时,会把自己的页表项全部1024项都复制。)
还要,这是把进程0的代码段和数据段的基地址放入了进程0的task_struct结构中相应的地方。
2.2.9继续调整进程1的task_struct结构
比如打开了那些文件,进程的当前工作目录i节点等等,由于进程0中这些数据都还是ikong的,进程0只具备在主机中运算的能力,还不具备于外设和以文件形式进行交互的能力,故这些在进程1中也暂时不舍置。
之后,把进程1的任务状态描述副和局部描述符挂接在全局描述副中。(前面说过,每个LDT是潜在GDT中的,可以把LDT当作一个段来看待,TSS也和LDT可以用一样的性质(TSS是一个段,叫做任务状态段,故要挂接到GDT上)
下面叙述TSS,(TSS这个段,可以当作一个段吧,它放在task_struct结构体中)p97页),用来记录当前进程执行时候对应的寄存器的数据的,这写数据在进程切换时候即发挥作用,比如进程A切换到B时候,那么系统就将此时CPU中各个寄存器的数值,保存在进程A的任务状态描述副表中。以便将来
调度A时候可以继续无误无重复地执行A。之后,再用进程B中的TSS里面的数据来设置各个寄存器的置。
局部数据表述副表LDT,记录当前进程对于程序的代码段和数据段信息。比如代码的基地址。
系统将来通过GDT表中挂接的TSS描述符和LDT描述符来与当前进程建立来年系。这里将进程1的TSS和LDT挂接在全局描述符表GDT中,标志这系统从此具备操作进程1的能力。
此致,fork任务基本已经完成。进程1可以参与运行程序和调度了。接着要从进程0切换到进程1,由进程1来完成所后续工作。
2.2.10 进程0到进程1的切换
在进程0中开始系统调用pause,最终也会产生软中断,并映射到一个叫做sys_pause的系统函数上面。sys_pause中会用调度函数schedule()
shedule对task数组中的进程开始进行审视,判断各个进程的调度条件。现在看到只有进程0和进程1,且原先把进程0设置了可中断等待,进程1就绪状态,于是调用
switch_to(next),准备切换到进程1区执行。
在switch_to(next)zhong ,将cpu中的各个寄存器的值(于进程0有关的)放入进程0的TSS中。并用进程1的TSS来加载各个寄存器,同理,LDTR的值也被换掉了。
接下来,进程1开始工作,它将进一步构建环境,使得系统能够以文件的形式与外设交互。
2.3加载根文件系统
由于任何对文件的操作,从根本伤讲,都要依靠根文件系统的支持,所以进程1通过加载根文件系统达到这个目的。
前面构建了与硬盘和软盘沟通的环境,这位加载根文件系统提供了前提和保障。
进程1开始执行。执行函数init(),在init()中调用了系统调用setup函数,映射到调用函数sys_setup函数,即初始化设置
与硬盘操作相关的一些数据结构,如hd_info,hd ,drive_info等,都放在内核的数据区。其中hd_info是硬盘信息控制结构,
然后,在把硬盘的引导块读入缓冲区。(缓冲区在前面设置了。。。)
下面补充一下2.1.13节的缓冲区部分。
在内核数据区,有一个哈系表,有307项的位置。
在紧靠内核的后面,是缓冲快管理结构,由于它管理着内存中的3000多个缓冲快,因此它占据的空间和内核差不多大小。
系统就是通过对哈系表和空闲表(缓冲区管理结构)对缓冲区进行管理的。p63页。
硬盘的第0号逻辑快,即最开始的两个扇区1KB,那里存储着引导块的内容。
首先申请一个缓冲快。然后,让设备号与快号(这里就是0号硬盘的0号逻辑块)与该缓冲快对应,然后,将该缓冲快的地址放入哈系表中的空闲位置。
中间有多步骤是说明进程1如何将引导块读入缓冲快的,很复杂。这里假设已经读入指定的缓冲快了。
引导块的最后两个字节也是55AA.之后,从引导块中的分区表信息来设置硬盘分区管理结构。
5.1.6 p185 页。
在创建进程的时候,为何是父进程的页表原风不懂地复制给新进程呢?那不是让新进程和父进程的代码和数据都相同吗?先开始是这样的,因为新进程刚刚开始执行时并没有自己的程序,这就需要加载自己的程序。
这个过程是调用execve函数,但是execve函数代码呢?很显然,在父进程中。(这里以shell进程为父进程为例)故新进程一旦开始执行,必须要有能力执行shell程序里面的代码,故此。
当加载完自己的程序后,其页面就会发生相应的变化了。
补充:
call 0x12345 ,相当于执行了pushl %eip和movl $0x12345 ,%eip,这里0x12345即为要跳转的指令。但首先要把%eip的值压入栈,即当前要执行的指令。然后,把0x12345的值放入%eip。
ret ,这里执行的指令相当于 popl %eip ,即把栈定的原先的eip的值现在放入eip中去。即恢复执行。
leave,在实际的程序当中经常执行,相当于 movl %ebp,%esp,和popl %ebp 两天指令,ebp置栈的基址寄存器,esp是栈的顶端寄存器。先将栈基地址放入栈顶寄存器,然后,把栈定的值放入ebp中。
实现了由一个任务返回到调用它的任务的内存栈空间的操作。
在main函数的第一句(反汇编后)是push %ebp,可以看到,当前的ebp保存的是前一个进程执行时候所在内存栈空间的基地置,因此为了保证这个当前进程结束后,CPU能够正确恢复到之前的进程中去执行,
必须对其进行保存,这里的压栈就是这个目的。
插:
x86内存管理机制: 分为 分段机制和分页机制
分段机制是x86的特殊的段机制。也正是因为x86存在着段机制,故还有一种额外的地址---逻辑地址。(每个进程都有0--4GB的线性地址空间)
分段机制将内存划分成以起始地址和长度描述的块。
段可以和程序最基本的元素联系起来,因为程序在编译后就划分位了代码段,数据段和栈等,而段机制中刚刚好相应的代码段,数据段 栈段等等。
程序中使用逻辑地址,CPU使用的是线性地址, 故要从逻辑--线性转换 。(分段机制就是干这个的)
分段机制中有 逻辑地址,段选择子,段描述符 段描述附表 4个元素。
当程序使用逻辑地址访问内存时候,(比如,int *p=&a,这里p就是存储的a的逻辑地址,事实上存储的仅是逻辑地址的偏移,而相应的段选择符位于了段寄存器中。),cpu通过逻辑地址中的段选择子
来索引段描述附表,得到相应的段描述符,(描述符中有段的基址。)在加上偏移,就得到了相应的线性地址(虚拟地址)。(它存在于这个程序的0-4GB的线性地址空间中)。
因为逻辑地址覆盖了编译后的程序的各个段,如代码段,数据段,栈段等,故段选择子也相应有多个,如CS专门放入代码段的段选择子,DS放入数据段的段选择子,SS放入栈的段选择子,在上面的例子中,因为p引用的是数据a,
故可想见,这个逻辑偏移的段选择子应该是DS。 此外ES,FS,GS可以自由选用。
分页机制 (线性地址---物理地址)
分页机制的元素 页表 CR3寄存器 TLB 三个部件。
页表即线性页面到物理页面的映射
采用二级分页机制:其中页目录项和页表项都是4KB=32位,但从页目录项找页表位置和从页表找页都只用了高20位,在32位中的最低位位P位,表示物理页面是否在物理内存中,否则要发生缺页中断。。
页目录-》页目录项-》页表-》页表项-》页
故一个进程在运行时候,须将自己页目录的基址放入CR3寄存器中。(4GB的线性空间,每个进程都需要一个页目录:1K*1K*4K=4GB).
TLB中存管的是最近常用的从线性页面到物理页面的映射的表,可以加快转换速度。
进程上下文 p36
此处只关心和虚拟技术关系最紧密的概念--上下文
上下文是从CPU角度引出的。简单地说,上下文就是程序运行时候所需要的最小的寄存器集合。这些寄存器的后卖弄一般代表这程序运行的一类资源,例如LDTR就代表某个进程使用的段信息,CR3寄存器就代表进程的私有的线性地址空间(分页机制启动)
x86架构下的上下文寄存器包括,通用寄存器EAX EBX ECX EDX ESI EDI ESP EBP等,段相关CS DS SS ES FS GS 标志寄存器EFLAGS 程序指针寄存器EIP GDT基址 GDTR ,LDTR TR CR系列
上下文切换
指程序从一种状态切换到另一种状态(如用户态和内核态的切换)或从一个程序切换到另一个程序,如进程切换,导致上下文相关寄存器值的变化行为。
进程切换时候通常是全上下文的切换,例如把CR3切换成进程页目录的地址。EIP指向新进程的第一条指令。
中断上下文切换:通常只有部分的上下文切换,须对栈指针和EIP等值写过,而CR3不许要修改
用户太内核太切换:也只需要部分的上下文切换,如从用户栈切换到内核栈。(哪些寄存器会变化呢?)