【Linux内核】---- 02 从main到怠速
系统达到怠速状态前所做的一切准备工作的核心目的是让用户进程能够以“进程” 的方式正常运行,
能够实现 用户程序能够在主机上进行运算、能够与外设进行交互、以及能够让用户以它为媒介进行人机交互。
系统大体分为三个阶段:
- 第一阶段:创建进程0,并让进程0具备在32位保护模式下在主机运算的能力
- 第二阶段:以进程0为母本创建进程1,使进程1不仅仅具备程0 所拥有的能力,而且还能以文件的形式与外设进行数据交互
- 第三阶段:以进程1为母本创建进程2,使进程2在全面具备进程1所拥有的能力 和 环境的基础上,进一步具备支持“人机交互”的能力,最终实现怠速。
2.1 开中断之前的准备工作
2.1.1 复制根设备号 和 硬盘参数表
在后续进行缓冲区初始化时,会对“根设备号” 和 “硬盘参数表” 重新规划,
具体执行代码如下:
void main(void)
{
ROOT_DEV = ORIG_ROOT_DEV;
// 系统用 ROOT_DEV 备份 “根设备号” ,用以从软盘上回载根文件系统
struct drive_info {char dummy[32];} drive_info;
// 用 drive_info 备份硬盘参数表,大小为32字节
// 每个硬盘参数表为16字节,所以drive_info能够将两个硬盘参数表全部备份。
}
2.2.2 规划物理内存
进程0 在主机中的运算主要是通过CPU 和内存相互配合工作而得以实现的,因此,对主机物理内存的使用及管理进进行规划,这样在才能为进程0 具备运算能力打下基础。
具体操作如下:
除1MB 以内的内核区之外,其余物理内存要完成的工作是不同,
“主内存区” 主要用来承载进程的相关信息,包括进程管结构、进程对应的程序等 。
“缓冲区” 主要作为主机与外设进行数据并互的中转站;
“虚拟盘区” 是一个可选的区域,如果选择使用虚拟盘,就可以将外设上的数据先复制到虚拟盘区,然后再使用。
由于内存中操作数据的速度远高于外设,因此这样可以提高系统执行效率。
先根据内存条大小对“缓冲区” 和 “主内存区” 的位置 和 大小进行初步设定。
// 代码: //int/main.c
void main(void){
memory_end = (1<<20) + (EXT_MEM_K<<10); // 主内存区的末端
memory_end &= 0xfffff000;
if(memory_end > 16 * 1024 * 1024)
memory_end = 16*1024*1024;
if(memory_end > 16 * 1024 * 1024)
buffer_memory_end = 4*1024*1024; // 缓冲区的末端位置
else if(memory_end > 6 * 1024 * 1024)
buffer_memory_end= 2*1024*1024;
else
buffer_memory_end= 1*1024*1024;
main_memory_start = buffer_memory_end; //主内存区的起始位置
}
2.2.3 虚拟盘设置与初始化
虚拟盘大小设置为2MB,表现为在主内存起始处为虚拟盘开辟一段2MB的内存空间
// init/main.c
void main(void)
{
#ifdef RAMDISK
main_memory_start += rd_init(main_memory_start, RAMDISK *1024);
#endif
}
// kernel/ramdisk.c
long rd_init(long mem_start, int length)
{
blk_dev[MAJOR_NR].request_fn = DEVICE_RQUEST;
for(i = 0; i<length; i++)
*cp++ = '\0';
return (length);
}
2.2.3 内存管理结构mem_map 初始化
对主内存区起始位置的重新确定,标志着主内存区和缓冲区的位置和大小已全部确定了。
于是系统开始调用mem_init 函数,先对主内存区的管理结构进行置。
// init/main.c
void main(void){
mem_init(main_memory_start, memory_end);
}
// mm/memory.c
void mem_init(long start_mem, long end_mem){
for(i = 0; i<PAGING_PAGES; i++)
mem_map[i] = USED;
while(end_mem-- > 0)
mem_map[i++] = 0;
}
系统对1MB 以上的内存都是分页管理的,于是系统通过一个叫做mem_map的数组记录每一个页面的使用次数。
先将所有的内存页面使用次数均设置为100, 然后再依据主内存的起始位置 和 终止位置将处于主内存中所有的页面的使用次数全部清0,
系统以后只把使用次数为0的页面视为空闲页面。
2.2.4 异常处理类中断服务程序挂接
由于CPU在运算过程中免不了进行“异常处理” ,这些异常处理都需要具体的服务程序来执行。
此时,调用trap_init 函数 将“ 异常处理 ” 一类的中断服务程序 与中断描述符表进行挂接,开始逐步地重建中断服务休系,以此来支持进程0在主机中的运算。
“异常处理” 就是由CPU 探测到的一些不可预知的错误而导致的中断,具体包括: 除0 错误、溢出操作、边界检查错误、缺页错误等。
进入trap_init 函数后,系统主要通过调用 set_trap_gate 这样的宏函数来实现挂接,具体操作如下:
set_trap_gate(0 , ÷_error);
参数中的0 表示将该函数地址挂接在中断描述符表的第0项位置年,÷_error 代表了“除0错误”处理函数的地址。
另外,挂接工作除了要将服务程序的地址值载入描述符表外,还要对每个与之建立关系的描述符进行设置。
每个中断描述符为8个字节,挂接的时伟会将中断处理程序的地址值按照一定的规则进行拆分,然后分散存储在这8个字节中,
同时还要将一些针对本中断的属性信息记录在这8个字节的其余空间中,比如,中断的优先权值等 。
其他异常处理服务程序的挂接方式与“除0 错误” 服务程序的挂接方式大体一致,32位保护模式下的中断服务体系就是通过不断地建立这种挂接关系形成的。
这种32位中断服务体系是为适应一种被动响应中断信号的机制而建立的,它的执行路线如下:
一方面,硬件产生信号,并传达给8259A,8259A 对信号进行初步处理,然后视CPU执行情况传递中断信号。
另一方面,如果CPU没有收到信号,就不断地处理正在执行的程序;如果接收到了信号,就打断正在执行的程序并通过中断描述符表找到具体的中断服务程序,让其执行,执行完后,从刚才打断的程序点继续执行。可见CPU 采取的是一套“被动响应”的机制来处理中断,这样CPU就可以把全部精力都放在为用户程序服务上,对于随时可产生而又不可能时时都产生的中断信号,不用刻意去考虑,这就提高了操作系统的综合效率。
最原始的设计其实不是这样的,那时候CPU每隔一段时间就要对所有硬件进行轮询,以检测它刚才工作的进程中有没有产生信号,这样就分散了CPU处理用户程序的精力,从而降低了系统的综合效率。不仅如此,每一个硬件在一次轮询的过程中产生多少信号也不确定,所以这个硬件还需要存储自身产 生的信号,这就提 高了硬 件设计要求。
可见,以“被动响应” 模式 取代 “主动轮询” 模式 来处理中断问题是现代操作系统之所以被称为“现代” 的一个重要标志。
2.2.5 初始化块设备请求项结构
2.2.6 与建立人机交互界面相关的外设的中断服务程序挂接
- 对串行口进行设置
- 对显示器进行设置
- 对键盘进行设置
2.2.7 开机启动时间设置
进程0 在主机中正常运算需要具体对时间的掌控能力,
2.2.8 系统开始激活进程0
- 系统先要将进程0 激活,使其具备运算及创建其他进程的能力
进程0管理结构task_struct 的母本已经在代码设计阶段事先设计好了,但这并不代表进程0已经可用了
本文学自《Linux内核设计的艺术》