现在main函数开始执行了,可真正意义上的说linux操作系统开始运行了。main函数将设置程序在操作系统下运行所需的环境并创建进程0,操作系统才有了第一个进程。
1. 设置根设备和硬盘信息。之前在setup程序中加载了一些硬件信息并存储在物理内存0x90000-0x901FC处,main函数从这些参数中设置了全局变量根设备ROOT_DEV、硬盘信息drive_info,这两个信息在设置操作系统环境时会被访问。
2. 规划物理内存格局,设置缓冲区,虚拟盘和主内存:内存分为内核代码和数据所占空间(也就是物理内存从0x00000到0xFFFFF的1MB空间)、主内存区(程序进行的空间,以及一些申请内存的操作都是使用的该空间,包括内核管理内存的数据结构task_struct都是存放在此。16MB的物理内存情况下长度设置为10MB)、缓冲区(主机与外设数据交互的中转站,主要是为了外设的数据能够被多个进程共享,防止频繁访问外设,外设的访问速度比内存的访问速度慢多了。16MB的物理内存情况下长度设置为3MB)、虚拟盘(可选,物理内存不够时可以不设置,外设的数据可以先载入这里提高访问速度。16MB的物理内存情况下长度设置为2MB)。16MB的物理内存分布如下图:

3. 调用mem_init初始化mem_map全局内存管理结构:主内存的访问需要通过mem_map来管理。该结构是一个数组,每一项表示物理内存页面的使用计数。mem_init将主内存中的页面使用计数置为0,其它的置为100,之后系统申请内存只能够使用计数为0的页面。
4. 调用trap_init将异常处理类中断服务程序挂接:也就是设置一些异常类型的中断(如缺页、溢出等)的中断响应函数。
5. 调用blk_dev_init初始化块设备请求项结构:进程要想与块设备通信,必须先经过内存中的缓冲区。内核有个名称为request、长度为32、类型为struct request的请求项管理数组,每一项管理系统对一个硬件设备的读写请求操作。现在还没有操作硬件先把这些设置为空。
6. 调用chr_dev_init初始化字符设备:这个函数的实现为空。
7. 调用tty_init设置串口、显示器、键盘等:2个串口和显示器是通过全局变量struct tty_struct tty_table进行管理的,并在内核数据域进行了静态初始化。tty_init首先通过调用rs_init设置串口1和串口2的中断服务程序,然后通过调用con_init初始化显示器和设置键盘中断服务程序。
8. 调用time_init设置开机启动时间:通过CMOS设定启动时间startup_time。
9. 调用sched_init初始化进程0:进程0是操作系统建立的第一个进程,一直运行在系统中,当系统没有可执行的任务会切换到进程0执行。注意到现在的代码是从bootsect开始一路跳转到此处的,还处于内核态。
操作系统通过struct task_struct结构管理进程,通过task数组存储当前系统所有进程的程序控制块,数组长度为64,也就是说当前操作系统做多只能有64个进程,操作系统另外并为每个进程在内核空间分配一个内核栈,task_struct和内核栈是一个union结构共同占用一个内存页面,如下图所示:

进程0的task_struct结构的每一项在内核代码中就静态写好了,进程0的数据段基址为0,段限长为640KB ,代码段基址为0,段限长为640KB。任务0的数据段和代码段和系统内核的代码段和数据段是重合的,代码段和数据段都是从物理内存0开始处640KB的空间。进程0的内核态堆栈和进程控制块都是位于系统模块内(在物理内存640KB以内的空间);
sched_init接下来设置时钟中断的处理函数;然后设置系统调用的总入口函数,以后每次进程通过int 0x80产生系统调用时就会跳到这个系统调入总入口函数执行,并根据寄存器eax存储的系统调用的标号找到对应系统调用的处理函数。
10. 调用buffer_init初始化缓冲区管理结构:前面说到操作系统会在紧挨着内核代码和数据区为缓冲区分配3MB的内存,这个内存区域就是用来创建多个缓冲区,每个缓冲区可以存储1KB的数据,所以总共有3072个缓冲区。操作系统通过struct buffer_header*类型的free_list链表和hash_table[307]哈希表管理缓冲区,这两个都是内核数据区中的变量。当进程要从设备上读取数据时,操作系统先把数据从设备中按block的大小读取到缓冲区中,并设置哈希表和free_list,这样如果其他进程也要读取这个设备上相同的block可以直接从内存上读取而不必再次访问外设。buffer_head的结构如下所示:

系统申请缓冲区时是从free_list的头部开始遍历,找到第一个可用的buffer_head,也就是从缓冲区尾部往前查找,将其对应的缓冲区作为新的缓冲区。这种从高地址往低地址方向使用内存的思想在内核申请内存页面get_free_page时也被使用,内存更加紧凑但是为什么非要从后往前?
最后初始化hash_table,全部置为NULL。
11. 调用hd_init初始化硬盘:linux-0.11的只能有7个块设备,并通过blk_dev数组进行管理:
。
12. 调用floppy_init初始化软盘:和硬盘的初始化类似。
13. 开启中断:现在操作系统要使用的中断服务程序以及设置好了,可以通过sti开启中断了。
14. 将进程0从内核级别变为用户级别:当前执行环境还是内核态,需要转变成用户态使进程0成为真正意义上的第一个用户进程。这里的模式变换是通过iret实现的。
所有类型的中断处理时会先通过进程task_struct结构中的的tss找到进程的内核栈栈顶(现在的内核栈栈底就是在bootsect中设置的0x9000),依次把寄存器SS(用户栈栈底)、ESP(用户栈栈顶)、EFLAGS(标志位)、CS(代码段)、EIP(下条指令的地址)入栈,并将系统从用户模式0x11翻转为0x00系统模式,执行完毕后通过iret返回。iret会从内核栈中弹出五个值分别赋值给5个寄存器,并将系统从系统模式翻转回用户模式。
Linux0.11中每个进程都有个tss保存进程的寄存器状态,其中的esp0和ss0字段分别表示内核栈的栈顶和栈段(这两个值是只读的,在fork时被初始化。程序每次切换到内核态的时候,读取这两个值到寄存器中ss和esp中,将堆栈切换为内核栈。程序返回至用户空间时,当前的esp和ss不会被重新保存,因此进程每次切换至内核栈都是最初的地址,内核栈都是空的)。
进程0现在的工作几乎做完了,接下来它要创建进程1,并将自己处于无限执行状态。
1. 设置根设备和硬盘信息。之前在setup程序中加载了一些硬件信息并存储在物理内存0x90000-0x901FC处,main函数从这些参数中设置了全局变量根设备ROOT_DEV、硬盘信息drive_info,这两个信息在设置操作系统环境时会被访问。
2. 规划物理内存格局,设置缓冲区,虚拟盘和主内存:内存分为内核代码和数据所占空间(也就是物理内存从0x00000到0xFFFFF的1MB空间)、主内存区(程序进行的空间,以及一些申请内存的操作都是使用的该空间,包括内核管理内存的数据结构task_struct都是存放在此。16MB的物理内存情况下长度设置为10MB)、缓冲区(主机与外设数据交互的中转站,主要是为了外设的数据能够被多个进程共享,防止频繁访问外设,外设的访问速度比内存的访问速度慢多了。16MB的物理内存情况下长度设置为3MB)、虚拟盘(可选,物理内存不够时可以不设置,外设的数据可以先载入这里提高访问速度。16MB的物理内存情况下长度设置为2MB)。16MB的物理内存分布如下图:
3. 调用mem_init初始化mem_map全局内存管理结构:主内存的访问需要通过mem_map来管理。该结构是一个数组,每一项表示物理内存页面的使用计数。mem_init将主内存中的页面使用计数置为0,其它的置为100,之后系统申请内存只能够使用计数为0的页面。
4. 调用trap_init将异常处理类中断服务程序挂接:也就是设置一些异常类型的中断(如缺页、溢出等)的中断响应函数。
5. 调用blk_dev_init初始化块设备请求项结构:进程要想与块设备通信,必须先经过内存中的缓冲区。内核有个名称为request、长度为32、类型为struct request的请求项管理数组,每一项管理系统对一个硬件设备的读写请求操作。现在还没有操作硬件先把这些设置为空。
struct request {
int dev; /* -1 if no request */ //对应的设备号
int cmd; /* READ or WRITE */ //是读操作还是写操作
int errors;
unsigned long sector; //操作块设备的那个扇区
unsigned long nr_sectors; //总的扇区数?
char * buffer; //具体的数据
struct task_struct * waiting; //在请求该设备的进程
struct buffer_head * bh; //对应内存中哪个缓冲区
struct request * next; //用于构成请求队列
};
6. 调用chr_dev_init初始化字符设备:这个函数的实现为空。
7. 调用tty_init设置串口、显示器、键盘等:2个串口和显示器是通过全局变量struct tty_struct tty_table进行管理的,并在内核数据域进行了静态初始化。tty_init首先通过调用rs_init设置串口1和串口2的中断服务程序,然后通过调用con_init初始化显示器和设置键盘中断服务程序。
8. 调用time_init设置开机启动时间:通过CMOS设定启动时间startup_time。
9. 调用sched_init初始化进程0:进程0是操作系统建立的第一个进程,一直运行在系统中,当系统没有可执行的任务会切换到进程0执行。注意到现在的代码是从bootsect开始一路跳转到此处的,还处于内核态。
操作系统通过struct task_struct结构管理进程,通过task数组存储当前系统所有进程的程序控制块,数组长度为64,也就是说当前操作系统做多只能有64个进程,操作系统另外并为每个进程在内核空间分配一个内核栈,task_struct和内核栈是一个union结构共同占用一个内存页面,如下图所示:
进程0的task_struct结构的每一项在内核代码中就静态写好了,进程0的数据段基址为0,段限长为640KB ,代码段基址为0,段限长为640KB。任务0的数据段和代码段和系统内核的代码段和数据段是重合的,代码段和数据段都是从物理内存0开始处640KB的空间。进程0的内核态堆栈和进程控制块都是位于系统模块内(在物理内存640KB以内的空间);
sched_init接下来设置时钟中断的处理函数;然后设置系统调用的总入口函数,以后每次进程通过int 0x80产生系统调用时就会跳到这个系统调入总入口函数执行,并根据寄存器eax存储的系统调用的标号找到对应系统调用的处理函数。
10. 调用buffer_init初始化缓冲区管理结构:前面说到操作系统会在紧挨着内核代码和数据区为缓冲区分配3MB的内存,这个内存区域就是用来创建多个缓冲区,每个缓冲区可以存储1KB的数据,所以总共有3072个缓冲区。操作系统通过struct buffer_header*类型的free_list链表和hash_table[307]哈希表管理缓冲区,这两个都是内核数据区中的变量。当进程要从设备上读取数据时,操作系统先把数据从设备中按block的大小读取到缓冲区中,并设置哈希表和free_list,这样如果其他进程也要读取这个设备上相同的block可以直接从内存上读取而不必再次访问外设。buffer_head的结构如下所示:
struct buffer_head {
char * b_data; //每个缓冲区只能存储1个blokc也就是1KB的数据,b_data直接指向数据也就是内存中数据的位置
unsigned long b_blocknr; //该缓冲区对应块设备中的哪一个block
unsigned short b_dev; //该缓冲区对应哪一个块设备
unsigned char b_uptodate; //该标志位用来表示该缓冲区的数据是否是块设备中对应block的最新数据,1表示是最新的,进程可以使用该缓冲区的数据
unsigned char b_dirt; //该标志位表示该缓冲区是否被进程改写了,是的话需要及时同步到硬盘上
unsigned char b_count; //当前有多少个进程共享该缓冲区
unsigned char b_lock; //缓冲区锁
struct task_struct * b_wait; //如果当前缓冲区被锁住了,申请访问的进程就会被放入b_wait并挂起,解锁后唤醒该进程
struct buffer_head * b_prev; //下面四个构成双向链表
struct buffer_head * b_next;
struct buffer_head * b_prev_free;
struct buffer_head * b_next_free;
};
buffer_init函数设置缓冲区内存中的3072个缓冲区和free_list的对应关系,free_list的第0项对应缓冲区的最后一项、free_list的第1项对应缓冲区的倒数第二项……如下所示:系统申请缓冲区时是从free_list的头部开始遍历,找到第一个可用的buffer_head,也就是从缓冲区尾部往前查找,将其对应的缓冲区作为新的缓冲区。这种从高地址往低地址方向使用内存的思想在内核申请内存页面get_free_page时也被使用,内存更加紧凑但是为什么非要从后往前?
最后初始化hash_table,全部置为NULL。
11. 调用hd_init初始化硬盘:linux-0.11的只能有7个块设备,并通过blk_dev数组进行管理:
struct blk_dev_struct blk_dev[NR_BLK_DEV] = {
{ NULL, NULL }, /* no_dev */
{ NULL, NULL }, /* dev mem */
{ NULL, NULL }, /* dev fd */
{ NULL, NULL }, /* dev hd */
{ NULL, NULL }, /* dev ttyx */
{ NULL, NULL }, /* dev tty */
{ NULL, NULL } /* dev lp */
};
blk_dev_struct结构如下所示:struct blk_dev_struct {
void (*request_fn)(void); //块设备的请求服务程序函数指针
struct request * current_request; //当前块设备的请求项
};
硬盘是数组中的第3项,hd_init设置其对应的服务程序函数是do_hd_request;然后设置硬盘中断处理函数hd_interrupt。
12. 调用floppy_init初始化软盘:和硬盘的初始化类似。
13. 开启中断:现在操作系统要使用的中断服务程序以及设置好了,可以通过sti开启中断了。
14. 将进程0从内核级别变为用户级别:当前执行环境还是内核态,需要转变成用户态使进程0成为真正意义上的第一个用户进程。这里的模式变换是通过iret实现的。
所有类型的中断处理时会先通过进程task_struct结构中的的tss找到进程的内核栈栈顶(现在的内核栈栈底就是在bootsect中设置的0x9000),依次把寄存器SS(用户栈栈底)、ESP(用户栈栈顶)、EFLAGS(标志位)、CS(代码段)、EIP(下条指令的地址)入栈,并将系统从用户模式0x11翻转为0x00系统模式,执行完毕后通过iret返回。iret会从内核栈中弹出五个值分别赋值给5个寄存器,并将系统从系统模式翻转回用户模式。
Linux0.11中每个进程都有个tss保存进程的寄存器状态,其中的esp0和ss0字段分别表示内核栈的栈顶和栈段(这两个值是只读的,在fork时被初始化。程序每次切换到内核态的时候,读取这两个值到寄存器中ss和esp中,将堆栈切换为内核栈。程序返回至用户空间时,当前的esp和ss不会被重新保存,因此进程每次切换至内核栈都是最初的地址,内核栈都是空的)。
进程0现在的工作几乎做完了,接下来它要创建进程1,并将自己处于无限执行状态。