标签:顺序 $0 long 编译器 基础 能力 恢复 传统 模仿
在执行main函数之前,其实计算机从上电到main执行了一系列操作,不过由于个人原因,迫不及待先理解了0号进程,不过在说0号进程之前,先说说main函数启动到0号进程之间的事,也就是设备环境初始化的过程,这部分工作完成后系统进程怠速状态。
首先进程的定义是计算机中的程序关于某数据集合上的一次运行活动。而且进程之间不能相互干扰,这就需要有一套管理进程的数据结构了,在这里先介绍三种:task_struct(标识了进程的属性,包括剩余时间片,进程执行状态,LDT局部数据描述符,TSS任务状态描述表),task[64](这里存储这task_struct结构体指针),GDT(存储着一套针对所有进程的索引结构,通过索引项将进程和LDT和TSS建立关系)。开展活动之间需要确保活动能够正常展开,需要实现三个目标:
接下来具体说明14个步骤(包含激活0号进程):
1.设置根设备和硬盘
... #define DRIVE_INFO (*(struct drive_info *)0x90080) // 硬盘参数表基址。 #define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)// 根文件系统所在设备号。 ... struct drive_info{char dummy[32];}drive_info;// 用于存放硬盘参数表信息。 void main(void) { ROOT_DEV = ORIG_ROOT_DEV;//根设备 drive_info = DRIVE_INFO;//硬盘 ... }
2.规划物理内存格局,设置缓冲区,虚拟盘,主内存
缓冲区:主机和外设进行数据交互的中转站
虚拟盘:可选区域,如果选择使用虚拟盘,可以将外设上的数据先复制到虚拟盘上,然后加以使用
主内存:进程代码运行空间,也包括内核管理进程的数据结构。
设置了缓冲区的末端位置和主内存区的起始位置。
3.设置虚拟盘空间并初始化
虚拟盘是可选的,在Makefile中的选择,在main.c中由RAMDISK宏来控制,为1就是选中了,表示设置了虚拟盘,虚拟空间的大小是可以设置的,大小也是由RAMDISK控制,本系统中设置为2MB,起始位置为缓冲区的结尾,这个时候主内存区后移2MB,后移的多少是由虚拟空间处理函数rd_init的返回值决定的,此过程中最重要的一步是将do_rd_request()和blk_dev[7]的第二项挂接(挂接第二项,一共7个设备项,这样第一项就为空,后面可以有6类设备),意味着内核可以调用do_rd_request()函数来处理虚拟盘相关请求项的操作。挂接后,虚拟盘所在的内存区域全部清零。
4.内存管理结构mem_map初始化
主内存起始位置的重新确认,标志着主内存和缓冲区的位置和大小已经全部确认了。 开始调用mem_init(),这里重点指出1MB以内和以外的内存分页机制不同,0-1MB,可以通过线性地址获取物理地址,由内核接管。大于1MB空间不可以通过线性地址获取物理地址,没有可递推的逻辑关系。
这里的内存管理指的是对大于1MB地址位置的内存分页管理,先将使用所有内存的使用计数为100,再将内存页计数都清零,计数为0的内存页视为空闲页。
5.异常处理类中断服务程序挂接
使用trap_init()函数将中断和异常的服务程序和IDT挂接,重建中断服务体系。也就是将idt[]数组中元素设置成中断服务程序的地址,剩余的数组元素int0x11-int0x2f为保留项。这里的中断是被动响应的,硬件产生信号并被CPU接收到,就会打断正在执行的程序,保存现场,根据IDT找到中断服务程序并执行,执行完恢复现场,继续执行原来的程序,如果又收到中断,就又执行,低优先级的中断可以被高优先级的中断打断。早年的中断时对所有硬件进行轮询是否有中断,大大浪费了CPU资源,轮询完再去执行原来的程序,那就浪费了太多时间。由此可见主动轮询到被动响应是计算机历史上的一大进步。
6.初始化块设备请求项结构
Linux0.11将外设分成块设备(存储)和字符设备(字节流IO通信)(后来有了网络设备)。进程想要和块设备沟通,必须经过缓冲区。
请求项管理结构request[32]就是OS管理缓冲区中的缓冲块和块设备上逻辑块之间的读写关系数据结构,是链表。
struct request { int dev; /* -1 if no request */// 使用的设备号。 int cmd; /* READ or WRITE */// 命令(READ 或WRITE)。 int errors; //操作时产生的错误次数。 unsigned long sector; // 起始扇区。(1 块=2 扇区) unsigned long nr_sectors; // 读/写扇区数。 char *buffer; // 数据缓冲区。 struct task_struct *waiting; // 任务等待操作执行完成的地方。 struct buffer_head *bh; // 缓冲区头指针(include/linux/fs.h,68)。 struct request *next; // 指向下一请求项。 };
这样就可以根据进程的读写任务的轻重缓急来决定缓冲块和块设备之间的读写操作,并把处理需求记录在请求项中。
7.与监理人机交互界面相关的外设的中断服务程序的挂接
初始化字符设备的是chr_dev_init(),但是linus在里面并没有写内容。tty_init()首先调用rs_init()来设置串行口(这里将串行口中断服务程序和IDT挂接,然后初始化两个串行口,初始化包括设置线路控制寄存器的DLAB位,波特率因子,DTR,RTS,最后是允许主控芯片的IRQ3和IRQ4发送中断请求),再调用con_init()来设置显示器(设置显存位置,另外初始化一些滚动屏幕的变量,索引寄存器端口被设置成0x3b4,数据寄存器端口被设置成0x3b5),对键盘进行设置(将键盘中断服务程序和IDT挂接,取消8259A中对键盘的中断屏蔽,允许IRQ1发送中断信号,这里是通过先禁止键盘工作,再允许键盘工作,这样键盘就可以使用了)
8.开机启动时间设置
开机时间是计算机中大部分时间的基础,这部分是重要的,大部分需要用到的时间都是根据开机时间推算出来的。具体操作步骤是:CMOS是主板上的歌小存储芯片,通过调用time_init(),对芯片中记录的时间进行采集,根据提取的时间要素,进行整合就可以计算出开机启动时间了(开机时间是从1970年1月1日0时开始计算的),开机时间存储在内核数据区中。
9.初始化进程0
这部分主要包含三个内容:系统初始化进程0的资源,使进程具备多进程轮询能力,具备处理系统调用的能力(系统调用有统一入口)。只有具备以上能力,0号进程才可以在主机是那个正常运行,并且这些能力具备遗传的功能。这三点都是在sched_init()中实现的。
初始化0号进程:初始化0号进程所占有的TSS和LDT(这两个是GDT中的4,5两项),0号进程的task_struct的母体(init_task={INIT_TASK},)是系统事先写好的,使用INIT_TASK的指针初始化task[64]的0项,接下将0号以外的63项清空,同时将TSS1和LDT1往上的所有表项清零,最后就是将TR寄存器指向TSS0,LDTR寄存器指向LDT0,这样CPU就可以找到0号进程了。
设置时钟中断:对支持轮询的8253定时器进行设置#define LATCH(1193180/HZ)。第二步是设置时钟中断,将timer_interrupt()中断挂接,这样IDT就可以找到具体的服务程序了。第三步是打开屏蔽码,这样时钟中断就可以产生了,现在开始每1/100秒就产生一次时钟中断,但这个时候CPU不会响应,因为此时处于CPU处于关闭中断状态,但是0号进程已经具备进程轮转的能力了。
设置系统调用总入口:将system_call()和int 0x80中断描述符挂接。所以说系统调用由int0x80产生时调用,此时已经交给内核了,后面的流程就不需要用户程序了,但是系统调用还有一个途径,就是通过CPU中断响应,翻转权限3为0,通过IDT找到系统调用的端口,再通过系统调用来处理事务,然后再翻转权限0到3,这样进程就可以继续执行原来的工作了。
10.初始化缓冲区管理结构
缓冲区是内存和外设进行数据交互的媒介。这里要明白硬盘和内存的最大区别就是,硬盘可以使用很低的成本来断电保存,而且CPU不可以对硬盘进行直接寻址,而内存除了保存数据之外,更重要的是要和CPU、总线配合进行计算,硬盘是不进行计算的。
操作系统是通过hash_table[NR_HASH],buffer_head双向环装链表来组成复杂的哈希表管理缓冲区,这里会对hash_table所有项设置为NULL。缓冲区的起始位置是由start_buffer控制的,start_buffer跟内核代码末端地址有关。
11.初始化硬盘
这一步的目的是,为进程和硬盘建德IO通信建立了环境基础。这里具体的操作是,将硬盘请求项的服务do_hd_request()和blk_dev控制结构挂接,硬盘和请求项的交互工作交给do_hd_request(),然后将硬盘中断服务程序和IDT挂接,最后复位主8259A int2的屏蔽位,这么做的意义就是允许中断请求信号,还有一不是复位硬盘的中断请求屏蔽位,这一步的意义是允许硬盘控制器发送中断请求信号。
12.初始化软盘
这里的流程和硬盘处理基本一样,就是具体的函数不同了,因为硬件的驱动不同,具体的中断也不同。
13.开启中断
这个时候所有中断的服务程序已经和IDT正常挂接了,这意味着中断服务体系已经构建完毕了,系统可以再32位保护模式下处理中断了,中应该要的意义就是可以使用系统调用了。这里就是把EFLAGS(标志寄存器)中的中断标志位从0变成1。
14.进程0由0特权级翻转到3特权级,成为真正的进程
0号进程的建立是设计者提前写好的,在linux操作系统中规定了,除了0进程,其他所有进程都是一个已有进程在3特权下创建的。
进程0的特级翻转调用的是move_to_user_mode()函数,来模仿返回动作实现的。
//// 切换到用户模式运行。 // 该函数利用iret 指令实现从内核模式切换到用户模式(初始任务0)。 #define move_to_user_mode() __asm__ ( "movl %%esp,%%eax\n\t" \ // 保存堆栈指针esp 到eax 寄存器中。 "pushl $0x17\n\t" \ // 首先将堆栈段选择符(SS)入栈。 "pushl %%eax\n\t" \ // 然后将保存的堆栈指针值(esp)入栈。 "pushfl\n\t" \ // 将标志寄存器(eflags)内容入栈。 "pushl $0x0f\n\t" \ // 将内核代码段选择符(cs)入栈。 "pushl $1f\n\t" \ // 将下面标号1 的偏移地址(eip)入栈。 "iret\n" \ // 执行中断返回指令,则会跳转到下面标号1 处。 "1:\tmovl $0x17,%%eax\n\t" \ // 此时开始执行任务0, "movw %%ax,%%ds\n\t" \ // 初始化段寄存器指向本局部表的数据段。 "movw %%ax,%%es\n\t" "movw %%ax,%%fs\n\t" "movw %%ax,%%gs":::"ax")
这里有一个问题就是函数调用和中断的不同点在哪里,看样子都是压栈出栈。函数调用是预先设计好的,知道代码在那个地方调用,编译器预先设计好压栈保护现场和出栈恢复现场的代码;而中断的发生是不可预测的,无法预先编译出保护和恢复的代码,所以只能由硬件来完成,核心就是int指令会引发CPU硬件完成SS,ESP,EFLAGS,CS,EIP的值按顺序进栈,同理恢复现场时按顺序恢复给这五个寄存器。
在CPU响应中断的时候,根据DPL的设置,可以实现指定的特权级之间的翻转。0到3和3到0的两个操作是两个不同的函数实现的,但都是通过系统调用实现的。汇编指令就是iret。特权翻转就相当于进行了一次中断。传统意义上将3特权级下的进程才是真正的进程。
标签:顺序 $0 long 编译器 基础 能力 恢复 传统 模仿
原文地址:https://www.cnblogs.com/still-smile/p/12989149.html