操作系统源码学习笔记(二) 开始执行main函数,进行物理内存规划

/ 默认分类 / 0 条评论 / 296浏览

一.操作系统的怠速状态

上一篇笔记中,我们说到了即将开始执行main函数,并且在执行main函数的时候,实际上中断还是没有开启的,此时操作系统无法接受任何中断,这一点在源码中,linus也有注释。

/*
 *  linux/init/main.c
 *
 *  (C) 1991  Linus Torvalds
 */

void main(void)		/* This really IS void, no error here. */
{			/* The startup routine assumes (well, ...) this */
/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */

系统达到怠速状态前所做的一切准备工作的核心目的就是让用户程序能够以“进程”的方式正常运行,也就是让用户可以将自己的程序正确地运行在操作系统上。

这里说的怠速的意思就是操作系统已经完成了所有的准备工作,随时可以响应用户的操作的一种状态。 这里是借用了汽车的“怠速”一词,是为了更加形象。 汽车进入怠速状态,就意味着汽车已经完全启动,只要驾驶员踩油门,就可以正常行驶了。

那么,怠速状态到底是应该具备哪些能力呢?下面大致列举一下。

而这一切,都需要操作系统的main函数来实现。下面就让我们一起来大致分析一下,linus编写的优美的代码吧。

二.main函数

操作系统程序从main函数开始执行,分别实现了对计算机环境进行初始化,并激活第一个进程——进程0。 Linux 0.11是一个支持多进程的现代操作系统。 这就意味着,各个用户进程在运行过程中,彼此不能相互干扰,这样才能保证进程在主机中正常地运算。 然而,进程自身并没有一个天然的“边界”来对其进行保护,要靠系统“人为”地给它设计一套“边界”来对其进行保护。 这套“边界”就是系统为进程提供的进程管理信息数据结构。这个我们后面再详细说。除了进程,操作系统程序还需要对内存、CPU、串行口、显示器、键盘、硬盘、软盘等硬件进行设置,并将这些硬件所对应的中断服务程序与IDT相挂接,为进程0及其直接、间接创建的所有后续进程与外设沟通构建环境。 下面我们就依次深入了解一下。

2.1 设置根存储设备

源码

/*
 * This is set up by the setup-routine at boot-time
 */
// 下面三行分别将指定的线性地址强行转换为给定数据类型的指针,并获取指针所指
// 的内容。由于内核代码段被映射到从物理地址零开始的地方,因此这些线性地址
// 正好也是对应的物理地址。这些指定地址处内存值的含义请参见setup程序读取并保存的参数。
#define EXT_MEM_K (*(unsigned short *)0x90002)
#define DRIVE_INFO (*(struct drive_info *)0x90080)
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)

void main(void)		/* This really IS void, no error here. */
{			/* The startup routine assumes (well, ...) this */
 	ROOT_DEV = ORIG_ROOT_DEV; 
 	drive_info = DRIVE_INFO;  

这两行内核代码做的就是初始化根设备和硬盘。因为在操作系统启动过程中,首先需要确定根设备和硬盘的信息,以便后续的文件系统挂载和访问。 也是确保操作系统能够正确启动并正确访问存储设备的关键步骤之一。

2.2 规划物理内存布局

计算机进行运算,需要cpu和内存进行完美的配合协调。 对整体物理内存的合理划分和分配,从根本上决定了所有进程可以使用内存的大小和方式, 必然会影响到进程在主机中的运算速度。 因此这一步十分重要。

下面具体介绍下操作系统对物理内存具体规划分配。除内核代码和数据所占的内存空间之外,其余物理内存主要分为以下三部分。

主内存区是进程代码运行的空间,也包括内核管理进程的数据结构;

缓冲区主要作为主机与外设进行数据交互的中转站;

“虚拟盘区”是一个可选的区域,如果选择使用虚拟盘,就可以将外设上的数据先复制进虚拟盘区,然后加以使用。 由于从内存中操作数据的速度远高于外设,因此这样可以提高系统执行效率。

实际上,操作系统所做的主要也就是对物理内存中的这三种不同性质的区域, 在大小、位置以及管理方式方面进行设置和规划。

2.2.1 规划主内存和缓冲区

源码

void main(void)		/* This really IS void, no error here. */
{			/* The startup routine assumes (well, ...) this */
/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */
 	ROOT_DEV = ORIG_ROOT_DEV;
 	drive_info = DRIVE_INFO;        // 复制0x90080处的硬盘参数
	memory_end = (1<<20) + (EXT_MEM_K<<10);     // 内存大小=1Mb + 扩展内存(k)*1024 byte
	memory_end &= 0xfffff000;                   // 忽略不到4kb(1页)的内存数
	if (memory_end > 16*1024*1024)              // 内存超过16Mb,则按16Mb计
		memory_end = 16*1024*1024;
	if (memory_end > 12*1024*1024)              // 如果内存>12Mb,则设置缓冲区末端=4Mb 
		buffer_memory_end = 4*1024*1024;
	else if (memory_end > 6*1024*1024)          // 否则若内存>6Mb,则设置缓冲区末端=2Mb
		buffer_memory_end = 2*1024*1024;
	else
		buffer_memory_end = 1*1024*1024;        // 否则设置缓冲区末端=1Mb
	main_memory_start = buffer_memory_end;

上面的程序执行完之后,也就完成了对主内存和缓冲区的内存大小规划。

有几个常用的左移、右移的数据关系需要记住: <<20 或 >>20 相当于乘或除以 1MB <<12 或 >>12 相当于乘或除以 4 KB(联想到页) <<10 或 >>10 相当于乘或除以 1 KB。

此时内存布局如下: image.png

2.2.2 规划虚拟盘

源码

#ifdef RAMDISK
	main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif

上面的代码就是先判断Makefile文件中是否定义了内存虚拟盘符号RAMDISK(也就是当前是否开启了使用虚拟盘),则初始化虚拟盘。

makefile 是一个用于自动化编译和构建程序的文件。它包含了一系列规则和命令,用于描述源代码文件之间的依赖关系以及如何生成最终的可执行文件或者库文件。这里通过makefile来编译再配合c中的条件编译块,提高了程序的效率。

#ifdef

#endif

通过使用 #ifdef#endif,可以根据代码中是否定义了某个标识符来选择性地编译特定的代码块。这在处理不同平台的特定代码、调试代码、或者实现不同功能的代码时非常有用。另外这样的预处理指令在编译时就确定了代码是否应该被包含在编译过程中,因此不需要在运行时进行条件判断。这种方式可以提高程序的效率,因为它避免了在运行时进行不必要的条件判断。

说到这里,不由的让我想起了操作系统重的jump lable机制, Jump Label 也可以用于程序流程控制。在编译时就能确定代码的执行路径,而不需要在运行时进行条件判断。只是Jump Label 是在运行时影响程序流程的,比如当我们修改了操作系统的某个开关之后,操作系统不需要每次执行的时候都判断当前开关是否打开或者关闭,然后来执行不同的代码,jump lable会在程序运行时实际改变代码的执行路径。开关的改变就会实时改变运行的代码,而不需要每次进行任何的开关实时判断。

关于规划虚拟盘的函数rd_init我们就不输入了解了,这里我们展示下最终规划后的整个物理内存的分布情况: image.png

三.整体的程序逻辑 #ifdef RAMDISK main_memory_start += rd_init(main_memory_start, RAMDISK1024); #endif // 以下是内核进行所有方面的初始化工作。阅读时最好跟着调用的程序深入进去看,若实在 // 看不下去了,就先放一放,继续看下一个初始化调用。——这是经验之谈。o(∩_∩)o 。;-) mem_init(main_memory_start,memory_end); // 主内存区初始化。mm/memory.c trap_init(); // 陷阱门(硬件中断向量)初始化,kernel/traps.c blk_dev_init(); // 块设备初始化,kernel/blk_drv/ll_rw_blk.c chr_dev_init(); // 字符设备初始化, kernel/chr_drv/tty_io.c tty_init(); // tty初始化, kernel/chr_drv/tty_io.c time_init(); // 设置开机启动时间 startup_time sched_init(); // 调度程序初始化(加载任务0的tr,ldtr)(kernel/sched.c) // 缓冲管理初始化,建内存链表等。(fs/buffer.c) buffer_init(buffer_memory_end); hd_init(); // 硬盘初始化,kernel/blk_drv/hd.c floppy_init(); // 软驱初始化,kernel/blk_drv/floppy.c sti(); // 所有初始化工作都做完了,开启中断 // 下面过程通过在堆栈中设置的参数,利用中断返回指令启动任务0执行。 move_to_user_mode(); // 移到用户模式下执行 if (!fork()) { / we count on this going ok */ init(); // 在新建的子进程(任务1)中执行。 } 后面的笔记中我们将详细介绍以上这些函数做了什么事情。