《Linux内核设计的艺术》——2.激活进程0

1. 前言

Linux 0.11是一个支持多进程的现代操作系统。这就意味着,各个用户进程在运行过程中,彼此不能相互干扰,这样才能保证进程在主机中正常地运算。然而,进程自

身并没有一个天然的“边界”来对其进行保护,要靠系统“人为”地给它设计一套“边界”来对其进行保护。这套“边界”就是系统为进程提供的进程管理信息数据
结构。进程管理信息数据结构包括:task_struct、task[64]、GDT等。task_struct是每个进程所独有的结构。它标识了进程的
各项属性值,包括剩余时间片、进程执行状态、局部数据描述符表(LDT)和任务状态描述符表(TSS)等。task[64]和GDT是为管理多进程提供的
数据结构。task[64]结构中存储着系统中所有进程的task_struct指针。如果操作系统需要对多个进程加以比较并选择,就可以通过遍历
task[64]结构来实现。GDT中存储着一套针对所有进程的索引结构。通过索引项,操作系统可以间接地与每个进程中的LDT和TSS建立关系。

2. 规划内存

具体规划如下:除内核代码和数据所占的内存空间之外,其余物理内存主要分为三部分,分别是主内存区、缓冲区和虚拟盘。主内存区是进程代码运行的空间,也包
括内核管理进程的数据结构;缓冲区主要作为主机与外设进行数据交互的中转站;“虚拟盘区”是一个可选的区域,如果选择使用虚拟盘,就可以将外设上的数据先
复制进虚拟盘区,然后加以使用。由于从内存中操作数据的速度远高于外设,因此这样可以提高系统执行效率。

2.1 虚拟盘

虚拟盘是可选项,当开启虚拟盘,内存规划如下

#define NR_BLK_DEV 7

struct blk_dev_struct {
   void (*request_fn) (viod);
   struct request *current_request;
};

#define DEVICE_REQUEST do_rd_request

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 */
};

#define MAJOR_NR 1

long rd_init(long mem_start, int length)
{
   int i;
   char *cp;
   blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;
   rd_start = (char *) mem_start;
   rd_length = length;
   cp = rd_start;
   for (i = 0; i < length; i++)
      *cp++ = '\0';
   return length;
}

blk_dev 是将某类设备和请求项挂接。
挂接后,意味 虚拟盘 相关请求项使用 do_rd_request 处理。
并将虚拟盘初始化为0

2.2 对主内存设置

void mem_init(long start_mem, long end_mem)
{
   int i;
   HIGH_MEMORY = end_mem;
   for (i = 0; i < PAGING_PAGS; i++)
      mem_map[i] = USED;
   i = MAP_NR(start_mem); // start_mem为6MB,虚拟盘之后
   end_mem -= start_mem;
   end_mem >>= 12; // 16MB的页数
   while(end_mem-- > 0)
      mem_map[i++] = 0;
}

虚拟盘占用了部分主内存,对应的mem_map[]被设置为USED,其他主内存清空。

对于0-1MB的内存为内核使用的内存,不使用mem_map[],因为内核内存的分页映射是直接映射,即线性地址和物理地址完全一样。
而对于 1MB-16MB的内存为应用程序使用的内存,使用mem_map[]进行映射,要得到物理地址,必须先把逻辑地址变成线性地址,
再使用线性地址通过mem_map[]进行映射,得到物理地址,线性地址和物理地址没有逻辑关系,所以应用程序无法自己猜测物理地址,
所以应用程序之间内存隔离。

3. 异常中断初始化


将异常挂接到IDT

void trap_init()
{
   set_trap_gate(0, &divide_error); // 除零异常
   set_trap_gate(1, &debug); // 单步调试
   ...
}

4. 初始化块设备请求项

进程要想与块设备进行沟通,必须经过主机内存中的缓冲区。请求项管理结构request[32]就是操作系统管理缓冲区中的缓冲块与块设备上逻辑块之间读写关系的数据结构。

操作系统根据所有进程读写任务的轻重缓急,决定缓冲块与块设备之间的读写操作,并把需要操作的缓冲块记录在请求项上,得到读写块设备操作指令后,只根据请求项中的记录来决定当前需要处理哪个设备的哪个逻辑块。

struct request {
   int dev;
   int cmd;
   int error;
   unsigned long sector;
   unsigned long nr_sector;
   char *buffer;
   struct task_struct *waiting;
   struct buffer_head *bh;
   struct request *next;
};

struct request request[NR_REQUEST];

void blk_init()
{
   int i;
   for (i = 0; i < NR_REQUEST; i++) {
      request[i].dev = -1;
      request[i].next = NULL;
   }
}

request[i].dev = -1说明该请求项没有具体的设备,
request[i].next = NULL, 说明没有形成请求项队列。

5. 建立人机交互相关的外设的中断服务程序挂接

void tty_init(void)
{
   rs_init(); // 设置串口
   con_init(); // 设置显示器
}
void rs_init()
{
   set_intr_gate(0x24, rs1_interrupt);  // 设置串口1中断
   set_intr_gate(0x23, rs2_interrupt);  // 设置串口2中断
   init(tty_table[1].read_q.data);  // 初始化串口1
   init(tty_table[2].read_q.data);  // 初始化串口2
   outb(inb_p(0x21) & 0xE7, 0x21);  // 允许IRQ3 IRQ4
}

设置显示器

根据机器系统数据提供的显卡是“单色”还是“彩色”来设置配套信息。由于在Linux 

0.11那个时代,大部分显卡器是单色的,所以我们假设显卡的属性是单色EGA。那么显存的位置就要被设置为0xb0000~0xb8000,索引寄存器
端口被设置为0x3b4,数据寄存器端口被设置为0x3b5,再将显卡的属性——EGA这三个字符,显示在屏幕上。另外,再初始化一些用于滚屏的变量,其
中包括滚屏的起始显存地址、滚屏结束显存地址、最顶端行号以及最低端行号。效果如图2-14所示。

设置键盘

6. 初始化时间

具体执行步骤是:CMOS是主板上的一个小存储芯片,系统通过调用time_init()函数,先对它上面记录的时间数据进行采集,提取不同等级的时间要
素,比如秒(time.tm_sec)、分(time.tm_min)、年(time.tm_year)等,然后对这些要素进行整合,并最终得出开机启动
时间(startup_time)。

7. 初始化进程0

1)系统先初始化进程0。进程0管理结构task_struct的母本(init_task={INIT_TASK,})已经在代码设计阶段事先设计好
了,但这并不代表进程0已经可用了,还要将进程0的task_struct中的LDT、TSS与GDT相挂接,并对GDT、task[64]以及与进程调
度相关的寄存器进行初始化设置。

2)Linux 0.11作为一个现代操作系统,其最重要的标志就是能够支持多进程轮流执行,这要求进程具备参与多进程轮询的能力。系统这里对时钟中断进行设置,以便在进程0运行后,为进程0以及后续由它直接、间接创建出来的进程能够参与轮转奠定基础。

3)进程0要具备处理系统调用的能力。每个进程在运算时都可能需要与内核进行交互,而交互的端口就是系统调用程序。系统通过函数
set_system_gate将system_call与IDT相挂接,这样进程0就具备了处理系统调用的能力了。这个system_call就是系统
调用的总入口。

7.1 初始化进程0结构


使用kernel内置的数据,设置了进程0的 TSS 和 LDT


申请 task_struct 是申请一个页面,多余部分做该进程的内核栈。

初始化进程0相关的管理结构的最后一步是非常重要的一步,是将TR寄存器指向TSS0、LDTR寄存器指向LDT0,这样,CPU就能通过TR、LDTR寄存器找到进程0的TSS0、LDT0,也能找到一切和进程0相关的管理信息。

7.2 设置时钟中断

Linux0.11设置没1/100秒产生一个时钟中断,中断服务函数为 timer_interrupt()

7.3 设置系统调用入口

将系统调用处理函数system_call与int 

0x80中断描述符表挂接。system_call是整个操作系统中系统调用软中断的总入口。所有用户程序使用系统调用,产生int
0x80软中断后,操作系统都是通过这个总入口找到具体的系统调用函数。该过程如图2-23所示。

系统调用函数是操作系统对用户程序的基本支持。在操作系统中,依托硬件提供的特权级对内核进行保护,不允许用户进程直接访问内核代码。但进程有大量的像读
盘、创建子进程之类的具体事务处理需要内核代码的支持。为了解决这个矛盾,操作系统的设计者提供了系统调用的解决方案,提供一套系统服务接口。用户进程只
要想和内核打交道,就调用这套接口程序,之后,就会立即引发int
0x80软中断,后面的事情就不需要用户程序管了,而是通过另一条执行路线——由CPU对这个中断信号响应,翻转特权级(从用户进程的3特权级翻转到内核
的0特权级),通过IDT找到系统调用端口,调用具体的系统调用函数来处理事务,之后,再iret翻转回到进程的3特权级,进程继续执行原来的逻辑,这样
矛盾就解决了。

8. 初始化缓存区管理结构

缓冲区是内存与外设(如硬盘,以后以硬盘为例)进行数据交互的媒介。内存与硬盘最大的区别在于,硬盘的作用仅仅是对数据信息以很低的成本做大量数据的断电
保存,并不参与运算(因为CPU无法到硬盘上进行寻址),而内存除了需要对数据进行保存以外,更重要的是要与CPU、总线配合进行数据运算。缓冲区则介于
两者之间,它既对数据信息进行保存,也能够参与一些像查找、组织之类的间接、辅助性运算。有了缓冲区这个媒介以后,对外设而言,它仅需要考虑与缓冲区进行
数据交互是否符合要求,而不需要考虑内存如何使用这些交互的数据;对内存而言,它也仅需要考虑与缓冲区交互的条件是否成熟,而不需要关心此时外设对缓冲区
的交互情况。两者的组织、管理和协调将由操作系统统一操作。

操作系统通过hash_table[NR_HASH]、buffer_head双向环链表组成的复杂的哈希表管理缓冲区。


struct buffer_head *start_buffer = (struct buffer_head *)&end; // end是内核空间的结束,缓存区的开始
struct buffer_head *hash_table[NR_HASH];
static struct buffer_head *free_list;

void buffer_init(long buffer_end)
{
   struct buffer_head *h = start_buffer;
   void *b;
   int i;
   if (buffer_end == 1 < 20)
     b = (void*)(640*1024);
   else
     b = (void *) buffer_end;
   while ( (b -= BLOCK_SIZE) >= ( (void *) (h+1) ) { // 直到剩余空间不足BLOCK_SIZE时退出
       h->b_dev=0;
       h->b_dirt = 0;
       h->b_count = 0;
       h->b_lock = 0;
       h->b_uptodate = 0;
       h->wait = NULL;
       h->b_next = NULL;
       h->b_prev = NULL;
       h->b_data = (char *)b;
       h->b_prev_free = h - 1; // 这两项使 buffer_head 分别与前后buffer_head挂接,形成双向链表
       h->b_next_free = h + 1;
       h++; 
       NR_BUFFERS++;
       if (b == 0x100000) // 避开ROM BIOS & VGA
         b = 0xA0000;
   }
   h--;   // 使h指向最后一个 buffer, 以实现双向双向循环链表
   free_list = start_buffer;
   free_list->b_prev_free = h;
   h->b_next_free = free_list;
   for (i = 0; i < NR_HASH; i++)
      hash_table[i] = NULL;
}

将buffer_head的成员设备号b_dev、引用次数b_count、“更新”标志b_uptodate、“脏”标志b_dirt、“锁定”标志
b_lock设置为0。如图2-24所示,将b_data指针指向对应的缓冲块。利用buffer_head的b_prev_free、
b_next_free,将所有的buffer_head形成双向链表。使free_list指向第一个buffer_head,并利用
free_list将buffer_head形成双向链表链接成双向环链表,如图2-25所示。

9. 初始化硬盘

硬盘的初始化为进程与硬盘这种块设备进行I/O通信建立了环境基础。

在hd_init()函数中,将硬盘请求项服务程序do_hd_request()与blk_dev控制结构相挂接,硬盘与请求项的交互工作将由
do_hd_request()函数来处理,然后将硬盘中断服务程序hd_interrupt()与IDT相挂接,最后,复位主8259A
int2的屏蔽位,允许从片发出中断请求信号,复位硬盘的中断请求屏蔽位(在从片上),允许硬盘控制器发送中断请求信号。

10. 开启中断

11. 进程0由特权0翻转到特权3,成为真正的进程

Linux操作系统规定,除进程0之外,所有进程都要由一个已有进程在3特权级下创建。在Linux 

0.11中,进程0的代码和数据都是由操作系统的设计者写在内核代码、数据区,并且,此前处在0特权级,严格说还不是真正意义上的进程。为了遵守规则,在
进程0正式创建进程1之前,要将进程0由0特权级转变为3特权级。方法是调用move_to_user_mode()函数,模仿中断返回动作,实现进程0
的特权级从0转变为3。

posted on 2022-08-08 22:49  开心种树  阅读(120)  评论(0编辑  收藏  举报