这本书是上一本书的续集。上一本书我们说time_init方法通过与CMOS端口的读写交互获取年月日时分秒等数据,并计算出startup_time变量为秒数1970年1月1日0时至开机时。我们继续往下看,大名鼎鼎的进程调度初始化,shed_init。voidmain(void){...mem_init(main_memory_start,memory_end);trap_init();blk_dev_init();chr_dev_init();tty_init();time_init();sched_init();buffer_init(buffer_memory_end);hd_init();floppy_init();sti();move_to_user_mode();if(!fork()){init();}for(;;)pause();}这个方法很神奇,因为它是多进程的基石!终于到了激动的时刻你是不是很激动?不过先别激动,这只是进程调度的初始化,也就是准备进程调度需要的数据结构。真正的进程调度还需要调度算法、时钟中断等机制。合作。当然,对于理解操作系统来说,进程和数据结构是最重要的,而这一节作为整个进程的起点和数据结构建立的地方尤为重要。让我们进入这个方法并稍微回顾一下。voidsched_init(void){set_tss_desc(gdt+4,&(init_task.task.tss));set_ldt_desc(gdt+5,&(init_task.task.ldt));...}两行代码初始化下一个TSS和低密度脂蛋白。不要急着问这两个结构是什么。还记得前面提到的全局描述符表gdt吗?它在内存中的这个位置,它是这样设置的。我忘了读第8章|无聊的时候又得重新设置idt和gdt,可见之前看似无用的细节是多么的重要,大家要有耐心。说起这两行代码,其实后面又加了两项,就是TSS和LDT。好了,说说这两个结构是干什么用的,不过本文先简单了解一下,后面再详细说。TSS称为任务状态段,保存和恢复进程的上下文。所谓context其实就是各个寄存器的信息,这样当进程切换的时候,可以保存context并恢复继续执行。你应该能够从它的数据结构中看到一些东西。structtss_struct{longback_link;longesp0;longss0;longesp1;longss1;longesp2;longss2;longcr3;longeip;longeflags;longeax,ecx,edx,ebx;longesp;longesp;longesi;longedi;longes;longcs;longss;longds;longfs;longgs;longldt;longtrace_bitmap;structi387_structi387;};而LDT称为局部描述符表,对应GDT全局描述符表。内核态的代码使用GDT中的数据段和代码段,而用户进程中的代码使用每个用户进程自己的LDT中的数据段和代码段。不管了,我这里放一张超大纲图,大家先摸摸自己的感受。我们往下看。structdesc_struct{unsignedlonga,b;}structtask_struct*task[64]={&(init_task.task),};voidsched_init(void){...inti;structdesc_struct*p;p=gdt+6;for(i=1;i<64;i++){task[i]=NULL;p->a=p->b=0;p++;p->a=p->b=0;p++;}...}这段代码有是一个做两件事的循环。一种是给一个长度为64、结构为task_struct的数组task附加一个初始值。这个task_struct结构就是代表各个进程的信息。这是一个非常重要的结构,所以请记住它。structtask_struct{/*thesearehardcoded-don'ttouch*/longstate;/*-1unrunnable,0runnable,>0stopped*/longcounter;longpriority;longsignal;structsigactionsigaction[32];longblocked;/*bitmapofmaskedsignals*//*variousfields*/intexit_longcode;start_signed,end_code,end_data,brk,start_stack;longpid,father,pgrp,session,leader;unsignedshortuid,euid,suid;unsignedshortgid,egid,sgid;longalarm;longutime,stime,cutime,cstime,start_time;unsignedshortued_math;/*文件系统信息*/inttty;/*-1ifnotty,soitmustbesigned*/unsignedshortumask;structm_inode*pwd;structm_inode*root;structm_inode*executable;unsignedlongclose_on_exec;structfile*filp[NR_OPEN];/*ldtforthistask0-zero1-cs2-ds&ss*/structlddesc_struct[3*tssforthistask*/structtss_structtss;};这个循环做的另一件事是用0填充gdt的剩余位置,也就是为TSS和LDT保留的剩余描述符附加空值。往前看,以后每创建一个新进程,后面都会增加一组TSS和LDT,代表这个进程的任务状态段和局部描述符表信息。还记得刚才的超大纲图吗,整个内存的未来规划就是这样的,但是不用去详细了解。那为什么我们一开始就有一套TSS和LDT呢?尚未创建任何进程。错了,虽然我们还没有建立进程调度机制,但是我们运行的代码就是以后作为进程的指令流。即当future进程调度机制建立后,正在执行的代码将成为0进程的代码。所以我们需要提前写入这些future将作为0进程使用的信息。如果你觉得一头雾水,别着急,等到后面整个进程调度机制建立起来,让你看到进程0和进程1的创建,以及它们后续因为进程调度机制的切换,你就明白意思了这一切。好了,回来了,初始化了一组TSS和LDT之后,再往下看两行。#defineltr(n)__asm__("ltr%%ax"::"a"(_TSS(n)))#definelldt(n)__asm__("lldt%%ax"::"a"(_LDT(n)))voidsched_init(void){...ltr(0);lldt(0);...}这里又涉及到前面的知识了。还记得lidt和lgdt指令吗?一种是给idtr寄存器赋值,告诉CPU中断描述符表idt在内存中的位置;另一种是给gdtr寄存器赋值,告诉CPU全局描述符表gdt在内存中的位置。这两行和刚才那行类似,ltr是给tr寄存器赋值,告诉CPU任务状态段TSS在内存中的位置;lldt就是给ldt寄存器赋值,告诉CPU局部描述符LDT在内存中的位置。这样CPU接着就可以通过tr寄存器找到当前进程的任务状态段信息,即上下文信息,通过ldt寄存器找到当前进程使用的局部描述符表信息。让我们继续观察。voidsched_init(void){...outb_p(0x36,0x43);/*二进制,mode3,LSB/MSB,ch0*/outb_p(LATCH&0xff,0x40);/*LSB*/outb(LATCH>>8,0x40);/*MSB*/set_intr_gate(0x20,&timer_interrupt);outb(inb_p(0x21)&~0x01,0x21);set_system_gate(0x80,&system_call);...}四行端口读写代码,两行中断代码.端口读写我们已经很熟悉了,这是CPU与外设进行交互的一种方式。我们在讲硬盘读写和CMOS读写的时候已经接触过了。这种交互的外围设备是一个可编程定时器芯片。这四行代码启动了定时器,然后定时器会以一定的频率不断向CPU发送中断信号。这段代码设置的两个中断,第一个是时钟中断,中断号是0x20,中断处理程序是timer_interrupt。那么定时器每次向CPU发送中断,都会执行这个函数。这个定时器的触发,以及时钟中断函数的设置,是操作系统主进程调度的关键!没有像它们这样的外部信号不断触发中断,操作系统就没有办法成为进程管理的主人,通过强制手段收回进程的CPU执行权限。第二组中断调用系统调用system_call,中断号为0x80,这个中断是非常非常非常非常非常非常非常非常非常非常重要的中断,所有用户态程序都想调用内核提供的方法,就需要以此系统调用进行。比如Java程序员写一个read,底层会执行汇编指令int0x80,会触发系统调用的中断,最后调用Linux中的sys_read方法。这个过程以后再说,现在你只需要知道,在这个地方,秘密设置了这个极其重要的中断。所以看本章的内容,偷偷设置两个影响进程和用户程序调用系统方法的重量级中断处理程序,其实并不容易~至此,已经设置了很多中断,我们来现在看看设置了哪些中断。中断号中断处理函数0~0x10trap_init中设置的一堆中断0x20timer_interrupt0x21keyboard_interrupt0x80system_call其中,0-0x10这17个中断是在trap_init中初始化的,都是一些基本的中断,比如被零除异常等。这个在第14章中断初始化trap_init中有提到。之后在console初始化con_init中,我们设置0x21键盘中断,这样当键盘被按下时就会有响应。这个在第16个控制台初始化tty_init中有提到。现在,我们设置0x20时钟中断并启动定时器。最后偷偷设置了一个很重要的0x80系统调用中断。是不是发现了什么,是不是越来越发现操作系统有点中断驱动,各个模块不断初始化各种中断处理函数,打开指定的外设开关,让操作系统慢慢“活”起来?,逐渐通过打扰忙于各种事情,无法自拔。恭喜,我们正在逐渐接近操作系统的本质。回顾我们今天所做的,只有三件事。首先,我们向全局描述符表中写入两个结构体TSS和LDT,作为未来进程0的任务状态段和局部描述符表信息。其次,我们初始化一个数组,其结构为task_struct,该数组将存储所有进程的信息以后,我们把init_task.init的具体值附加到数组的第一个位置,也作为以后进程0的信息。第三,设置时钟中断0x20和系统调用0x80,一个作为进程调度的起点,一个作为用户程序调用操作系统函数的桥梁,非常重要。后面我们会逐渐看到,这些重要的东西是如何紧密、巧妙地结合在一起,发挥出奇妙的作用。想知道接下来发生了什么,就听下一章吧。本文转载自微信公众号《低并发编程》,可通过以下二维码关注。转载本文请联系低并发编程公众号。本站已授权低并发编程
