你好,我是LMOS。
前面三节课,我们为调用Cosmos的**第一个C函数hal_start做了大量工作。**这节课我们要让操作系统Cosmos里的第一个C函数真正跑起来啦,也就是说,我们会真正进入到我们的内核中。
今天我们会继续在这个hal_start函数里,首先执行板级初始化,其实就是hal层(硬件抽象层,下同)初始化,其中执行了平台初始化,hal层的内存初始化,中断初始化,最后进入到内核层的初始化。
这节课的配套代码,你可以从这里下载。
任何软件工程,第一个函数总是简单的,因为它是总调用者,像是一个管理者,坐在那里发号施令,自己却是啥活也不干。
由于这是第一个C函数,也是初始化函数,我们还是要为它单独建立一个文件,以显示对它的尊重,依然在Cosmos/hal/x86/下建立一个hal_start.c文件。写上这样一个函数。
void hal_start(){//第一步:初始化hal层//第二步:初始化内核层for(;;);return;}
根据前面的设计,Cosmos是有hal层和内核层之分,所以在上述代码中,要分两步走。第一步是初始化hal层;第二步,初始化内核层。只是这两步的函数我们还没有写。
然而最后的死循环却有点奇怪,其实它的目的很简单,就是避免这个函数返回,因为这个返回了就无处可去,避免走回头路。
为了分离硬件的特性,我们设计了hal层,把硬件相关的操作集中在这个层,并向上提供接口,目的是让内核上层不用关注硬件相关的细节,也能方便以后移植和扩展。(关于hal层的设计,可以回顾第3节课)
也许今天我们是在x86平台上写Cosmos,明天就要在ARM平台上开发Cosmos,那时我们就可以写个ARM平台的hal层,来替换Cosmos中的x86平台的hal层。
下面我们在Cosmos/hal/x86/下建立一个halinit.c文件,写出hal层的初始化函数。
void init_hal(){//初始化平台//初始化内存//初始化中断return;}
这个函数也是一个调用者,没怎么干活。不过根据代码的注释能看出,它调用的函数多一点,但主要是完成初始化平台、初始化内存、初始化中断的功能函数。
我们先来写好平台初始化函数,因为它需要最先被调用。
这个函数主要负责完成两个任务,一是把二级引导器建立的机器信息结构复制到hal层中的一个全局变量中,方便内核中的其它代码使用里面的信息,之后二级引导器建立的数据所占用的内存都会被释放。二是要初始化图形显示驱动,内核在运行过程要在屏幕上输出信息。
下面我们在Cosmos/hal/x86/下建立一个halplatform.c文件,写上如下代码。
void machbstart_t_init(machbstart_t *initp){//清零memset(initp, 0, sizeof(machbstart_t));return;}void init_machbstart(){machbstart_t *kmbsp = &kmachbsp;machbstart_t *smbsp = MBSPADR;//物理地址1MB处machbstart_t_init(kmbsp);//复制,要把地址转换成虚拟地址memcopy((void *)phyadr_to_viradr((adr_t)smbsp), (void *)kmbsp, sizeof(machbstart_t));return;}//平台初始化函数void init_halplaltform(){//复制机器信息结构init_machbstart();//初始化图形显示驱动init_bdvideo();return;}
这个代码中别的地方很好理解,就是kmachbsp你可能会有点奇怪,它是个结构体变量,结构体类型是machbstart_t,这个结构和二级引导器所使用的一模一样。
同时,它还是一个hal层的全局变量,我们想专门有个文件定义所有hal层的全局变量,于是我们在Cosmos/hal/x86/下建立一个halglobal.c文件,写上如下代码。
//全局变量定义变量放在data段#define HAL_DEFGLOB_VARIABLE(vartype,varname) \EXTERN __attribute__((section(".data"))) vartype varnameHAL_DEFGLOB_VARIABLE(machbstart_t,kmachbsp);
前面的EXTERN,在halglobal.c文件中定义为空,而在其它文件中定义为extern,告诉编译器这是外部文件的变量,避免发生错误。
下面,我们在Cosmos/hal/x86/下的bdvideo.c文件中,写好init_bdvideo函数。
void init_bdvideo(){dftgraph_t *kghp = &kdftgh;//初始化图形数据结构,里面放有图形模式,分辨率,图形驱动函数指针init_dftgraph();//初始bga图形显卡的函数指针init_bga();//初始vbe图形显卡的函数指针init_vbe();//清空屏幕 为黑色fill_graph(kghp, BGRA(0, 0, 0));//显示背景图片set_charsdxwflush(0, 0);hal_background();return;}
init_defgraph()函数初始了dftgraph_t结构体类型的变量kdftgh,我们在halglobal.c文件中定义这个变量,结构类型我们这样来定义。
typedef struct s_DFTGRAPH{u64_t gh_mode; //图形模式u64_t gh_x; //水平像素点u64_t gh_y; //垂直像素点u64_t gh_framphyadr; //显存物理地址u64_t gh_fvrmphyadr; //显存虚拟地址u64_t gh_fvrmsz; //显存大小u64_t gh_onepixbits; //一个像素字占用的数据位数u64_t gh_onepixbyte;u64_t gh_vbemodenr; //vbe模式号u64_t gh_bank; //显存的bank数u64_t gh_curdipbnk; //当前banku64_t gh_nextbnk; //下一个banku64_t gh_banksz; //bank大小u64_t gh_fontadr; //字库地址u64_t gh_fontsz; //字库大小u64_t gh_fnthight; //字体高度u64_t gh_nxtcharsx; //下一字符显示的x坐标u64_t gh_nxtcharsy; //下一字符显示的y坐标u64_t gh_linesz; //字符行高pixl_t gh_deffontpx; //默认字体大小u64_t gh_chardxw;u64_t gh_flush;u64_t gh_framnr;u64_t gh_fshdata; //刷新相关的dftghops_t gh_opfun; //图形驱动操作函数指针结构体}dftgraph_t;typedef struct s_DFTGHOPS{//读写显存数据size_t (*dgo_read)(void* ghpdev,void* outp,size_t rdsz);size_t (*dgo_write)(void* ghpdev,void* inp,size_t wesz);sint_t (*dgo_ioctrl)(void* ghpdev,void* outp,uint_t iocode);//刷新void (*dgo_flush)(void* ghpdev);sint_t (*dgo_set_bank)(void* ghpdev, sint_t bnr);//读写像素pixl_t (*dgo_readpix)(void* ghpdev,uint_t x,uint_t y);void (*dgo_writepix)(void* ghpdev,pixl_t pix,uint_t x,uint_t y);//直接读写像素pixl_t (*dgo_dxreadpix)(void* ghpdev,uint_t x,uint_t y);void (*dgo_dxwritepix)(void* ghpdev,pixl_t pix,uint_t x,uint_t y);//设置x,y坐标和偏移sint_t (*dgo_set_xy)(void* ghpdev,uint_t x,uint_t y);sint_t (*dgo_set_vwh)(void* ghpdev,uint_t vwt,uint_t vhi);sint_t (*dgo_set_xyoffset)(void* ghpdev,uint_t xoff,uint_t yoff);//获取x,y坐标和偏移sint_t (*dgo_get_xy)(void* ghpdev,uint_t* rx,uint_t* ry);sint_t (*dgo_get_vwh)(void* ghpdev,uint_t* rvwt,uint_t* rvhi);sint_t (*dgo_get_xyoffset)(void* ghpdev,uint_t* rxoff,uint_t* ryoff);}dftghops_t;//刷新显存void flush_videoram(dftgraph_t *kghp){kghp->gh_opfun.dgo_flush(kghp);return;}
不难发现,我们正是把这些实际的图形驱动函数的地址填入了这个结构体中,然后通过这个结构体,我们就可以调用到相应的函数了。
因为写这些函数都是体力活,我已经帮你搞定了,你直接使用就可以。上面的flush_videoram函数已经证明了这一想法。
来,我们测试一下,看看结果,我们图形驱动程序初始化会显示背景图片——background.bmp,这是在打包映像文件时包含进去的,你自己可以随时替换,只要是满足1024*768,24位的位图文件就行了。
下面我们要把这些函数调用起来:
//在halinit.c文件中void init_hal(){init_halplaltform();return;}//在hal_start.c文件中void hal_start(){init_hal();//初始化hal层,其中会调用初始化平台函数,在那里会调用初始化图形驱动for(;;);return;}
接下来,让我们一起make vboxtest,应该很有成就感。一幅风景图呈现在我们面前,上面有Cosmos的版本、编译时间、CPU工作模式,内存大小等数据。这相当一个我们Cosmos的水印信息。
首先,我们在Cosmos/hal/x86/下建立一个halmm.c文件,用于初始化内存,为了后面的内存管理器作好准备。
hal层的内存初始化比较容易,只要向内存管理器提供内存空间布局信息就可以。
你可能在想,不对啊,明明我们在二级引导器中已经获取了内存布局信息,是的,但Cosmos的内存管理器需要保存更多的信息,最好是顺序的内存布局信息,这样可以增加额外的功能属性,同时降低代码的复杂度。
不难发现,BIOS提供的结构无法满足前面这些要求。不过我们也有办法解决,只要以BIOS提供的结构为基础,设计一套新的数据结构就搞定了。这个结构可以这样设计。
#define PMR_T_OSAPUSERRAM 1#define PMR_T_RESERVRAM 2#define PMR_T_HWUSERRAM 8#define PMR_T_ARACONRAM 0xf#define PMR_T_BUGRAM 0xff#define PMR_F_X86_32 (1<<0)#define PMR_F_X86_64 (1<<1)#define PMR_F_ARM_32 (1<<2)#define PMR_F_ARM_64 (1<<3)#define PMR_F_HAL_MASK 0xfftypedef struct s_PHYMMARGE{spinlock_t pmr_lock;//保护这个结构是自旋锁u32_t pmr_type; //内存地址空间类型u32_t pmr_stype;u32_t pmr_dtype; //内存地址空间的子类型,见上面的宏u32_t pmr_flgs; //结构的标志与状态u32_t pmr_stus;u64_t pmr_saddr; //内存空间的开始地址u64_t pmr_lsize; //内存空间的大小u64_t pmr_end; //内存空间的结束地址u64_t pmr_rrvmsaddr;//内存保留空间的开始地址u64_t pmr_rrvmend; //内存保留空间的结束地址void* pmr_prip; //结构的私有数据指针,以后扩展所用void* pmr_extp; //结构的扩展数据指针,以后扩展所用}phymmarge_t;
有些情况下内核要另起炉灶,不想把所有的内存空间都交给内存管理器去管理,所以要保留一部分内存空间,这就是上面结构中那两个pmr_rrvmsaddr、pmr_rrvmend字段的作用。
有了数据结构,我们还要写代码来操作它:
u64_t initpmrge_core(e820map_t *e8sp, u64_t e8nr, phymmarge_t *pmargesp){u64_t retnr = 0;for (u64_t i = 0; i < e8nr; i++){//根据一个e820map_t结构建立一个phymmarge_t结构if (init_one_pmrge(&e8sp[i], &pmargesp[i]) == FALSE){return retnr;}retnr++;}return retnr;}void init_phymmarge(){machbstart_t *mbsp = &kmachbsp;phymmarge_t *pmarge_adr = NULL;u64_t pmrgesz = 0;//根据machbstart_t机器信息结构计算获得phymmarge_t结构的开始地址和大小ret_phymmarge_adrandsz(mbsp, &pmarge_adr, &pmrgesz);u64_t tmppmrphyadr = mbsp->mb_nextwtpadr;e820map_t *e8p = (e820map_t *)((adr_t)(mbsp->mb_e820padr));//建立phymmarge_t结构u64_t ipmgnr = initpmrge_core(e8p, mbsp->mb_e820nr, pmarge_adr);//把phymmarge_t结构的地址大小个数保存machbstart_t机器信息结构中mbsp->mb_e820expadr = tmppmrphyadr;mbsp->mb_e820exnr = ipmgnr;mbsp->mb_e820exsz = ipmgnr * sizeof(phymmarge_t);mbsp->mb_nextwtpadr = PAGE_ALIGN(mbsp->mb_e820expadr + mbsp->mb_e820exsz);//phymmarge_t结构中地址空间从低到高进行排序,我已经帮你写好了phymmarge_sort(pmarge_adr, ipmgnr);return;}
结合上面的代码,你会发现这是根据e820map_t结构数组,建立了一个phymmarge_t结构数组,init_one_pmrge函数正是把e820map_t结构中的信息复制到phymmarge_t结构中来。理解了这个原理,即使不看我的,你自己也会写。
下面我们把这些函数,用一个总管函数调动起来,这个总管函数叫什么名字好呢?当然是init_halmm,如下所示。
void init_halmm(){init_phymmarge();//init_memmgr();return;}
这里init_halmm函数中还调用了init_memmgr函数,这个正是这我们内存管理器初始化函数,我会在内存管理的那节课展开讲。而init_halmm函数将要被init_hal函数调用。
什么是中断呢?为了帮你快速理解,我们先来看两种情景:
在以上两种情景中,虽然不十分恰当,但都是在做一件事时,因为一些原因而要切换到另一件事上。其实计算机中的CPU也是一样,在做一件事时,因为一些原因要转而做另一件事,于是中断产生了……
根据原因的类型不同,中断被分为两类。
异常,这是同步的,原因是错误和故障,就像汽车引擎坏了。不修复错误就不能继续运行,所以这时,CPU会跳到这种错误的处理代码那里开始运行,运行完了会返回。
为啥说它是同步的呢?这是因为如果不修改程序中的错误,下次运行程序到这里同样会发生异常。
中断,这是异步的,我们通常说的中断就是这种类型,它是因为外部事件而产生的,就好像旅游时女朋友来电话了。通常设备需要CPU关注时,会给CPU发送一个中断信号,所以这时CPU会跳到处理这种事件的代码那里开始运行,运行完了会返回。
由于不确定何种设备何时发出这种中断信号,所以它是异步的。
在x86 CPU上,最多支持256个中断,还记得前面所说的中断表和中断门描述符吗,这意味着我们要准备256个中断门描述符和256个中断处理程序的入口。
下面我们来定义它,如下所示:
typedef struct s_GATE{u16_t offset_low; /* 偏移 */u16_t selector; /* 段选择子 */u8_t dcount; /* 该字段只在调用门描述符中有效。如果在利用调用门调用子程序时引起特权级的转换和堆栈的改变,需要将外层堆栈中的参数复制到内层堆栈。该双字计数字段就是用于说明这种情况发生时,要复制的双字参数的数量。*/u8_t attr; /* P(1) DPL(2) DT(1) TYPE(4) */u16_t offset_high; /* 偏移的高位段 */u32_t offset_high_h;u32_t offset_resv;}__attribute__((packed)) gate_t;//定义中断表HAL_DEFGLOB_VARIABLE(gate_t,x64_idt)[IDTMAX];
说到这里你会发现,中断表其实是个gate_t结构的数组,由CPU的IDTR寄存器指向,IDTMAX为256。
但是光有数组还不行,还要设置其中的数据,下面我们就来设计这个函数,建立一个文件halsgdidt.c,在其中写一个函数,代码如下。
//vector 向量也是中断号//desc_type 中断门类型,中断门,陷阱门//handler 中断处理程序的入口地址//privilege 中断门的权限级别void set_idt_desc(u8_t vector, u8_t desc_type, inthandler_t handler, u8_t privilege){gate_t *p_gate = &x64_idt[vector];u64_t base = (u64_t)handler;p_gate->offset_low = base & 0xFFFF;p_gate->selector = SELECTOR_KERNEL_CS;p_gate->dcount = 0;p_gate->attr = (u8_t)(desc_type | (privilege << 5));p_gate->offset_high = (u16_t)((base >> 16) & 0xFFFF);p_gate->offset_high_h = (u32_t)((base >> 32) & 0xffffffff);p_gate->offset_resv = 0;return;}
上面的代码,正是按照要求,把这些数据填入中断门描述符中的。有了中断门之后,还差中断处理程序,中断处理程序只负责这三件事:
1.保护CPU 寄存器,即中断发生时的程序运行的上下文。
2.调用中断处理程序,这个程序可以是修复异常的,可以是设备驱动程序中对设备响应的程序。
3.恢复CPU寄存器,即恢复中断时程序运行的上下文,使程序继续运行。
以上这些操作又要用汇编代码才可以编写,我觉得这是内核中最重要的部分,所以我们建立一个文件,并用kernel.asm命名。
我们先来写好完成以上三个功能的汇编宏代码,避免写256遍同样的代码,代码如下所示。
//保存中断后的寄存器%macro SAVEALL 0push raxpush rbxpush rcxpush rdxpush rbppush rsipush rdipush r8push r9push r10push r11push r12push r13push r14push r15xor r14,r14mov r14w,dspush r14mov r14w,espush r14mov r14w,fspush r14mov r14w,gspush r14%endmacro//恢复中断后寄存器%macro RESTOREALL 0pop r14mov gs,r14wpop r14mov fs,r14wpop r14mov es,r14wpop r14mov ds,r14wpop r15pop r14pop r13pop r12pop r11pop r10pop r9pop r8pop rdipop rsipop rbppop rdxpop rcxpop rbxpop raxiretq%endmacro//保存异常下的寄存器%macro SAVEALLFAULT 0push raxpush rbxpush rcxpush rdxpush rbppush rsipush rdipush r8push r9push r10push r11push r12push r13push r14push r15xor r14,r14mov r14w,dspush r14mov r14w,espush r14mov r14w,fspush r14mov r14w,gspush r14%endmacro//恢复异常下寄存器%macro RESTOREALLFAULT 0pop r14mov gs,r14wpop r14mov fs,r14wpop r14mov es,r14wpop r14mov ds,r14wpop r15pop r14pop r13pop r12pop r11pop r10pop r9pop r8pop rdipop rsipop rbppop rdxpop rcxpop rbxpop raxadd rsp,8iretq%endmacro//没有错误码CPU异常%macro SRFTFAULT 1push _NOERRO_CODESAVEALLFAULTmov r14w,0x10mov ds,r14wmov es,r14wmov fs,r14wmov gs,r14wmov rdi,%1 ;rdi, rsimov rsi,rspcall hal_fault_allocatorRESTOREALLFAULT%endmacro//CPU异常%macro SRFTFAULT_ECODE 1SAVEALLFAULTmov r14w,0x10mov ds,r14wmov es,r14wmov fs,r14wmov gs,r14wmov rdi,%1mov rsi,rspcall hal_fault_allocatorRESTOREALLFAULT%endmacro//硬件中断%macro HARWINT 1SAVEALLmov r14w,0x10mov ds,r14wmov es,r14wmov fs,r14wmov gs,r14wmov rdi, %1mov rsi,rspcall hal_intpt_allocatorRESTOREALL%endmacro
别看前面的代码这么长,其实最重要的只有两个指令:push、pop,这两个正是用来压入寄存器和弹出寄存器的,正好可以用来保存和恢复CPU所有的通用寄存器。
有的CPU异常,CPU自动把异常码压入到栈中,而有的CPU异常没有异常码,为了统一,我们对没有异常码的手动压入一个常数,维持栈的平衡。
有了中断异常处理的宏,我们还要它们变成中断异常的处理程序入口点函数。汇编函数其实就是一个标号加一段汇编代码,C编译器把C语言函数编译成汇编代码后,也是标号加汇编代码,函数名就是标号。
下面我们在kernel.asm中写好它们:
//除法错误异常 比如除0exc_divide_error:SRFTFAULT 0//单步执行异常exc_single_step_exception:SRFTFAULT 1exc_nmi:SRFTFAULT 2//调试断点异常exc_breakpoint_exception:SRFTFAULT 3//溢出异常exc_overflow:SRFTFAULT 4//段不存在异常exc_segment_not_present:SRFTFAULT_ECODE 11//栈异常exc_stack_exception:SRFTFAULT_ECODE 12//通用异常exc_general_protection:SRFTFAULT_ECODE 13//缺页异常exc_page_fault:SRFTFAULT_ECODE 14hxi_exc_general_intpfault:SRFTFAULT 256//硬件1~7号中断hxi_hwint00:HARWINT (INT_VECTOR_IRQ0+0)hxi_hwint01:HARWINT (INT_VECTOR_IRQ0+1)hxi_hwint02:HARWINT (INT_VECTOR_IRQ0+2)hxi_hwint03:HARWINT (INT_VECTOR_IRQ0+3)hxi_hwint04:HARWINT (INT_VECTOR_IRQ0+4)hxi_hwint05:HARWINT (INT_VECTOR_IRQ0+5)hxi_hwint06:HARWINT (INT_VECTOR_IRQ0+6)hxi_hwint07:HARWINT (INT_VECTOR_IRQ0+7)
为了突出重点,这里没有全部展示代码 ,你只用搞清原理就行了。那有了中断处理程序的入口地址,下面我们就可以在halsgdidt.c文件写出函数设置中断门描述符了,代码如下。
void init_idt_descriptor(){//一开始把所有中断的处理程序设置为保留的通用处理程序for (u16_t intindx = 0; intindx <= 255; intindx++){set_idt_desc((u8_t)intindx, DA_386IGate, hxi_exc_general_intpfault, PRIVILEGE_KRNL);}set_idt_desc(INT_VECTOR_DIVIDE, DA_386IGate, exc_divide_error, PRIVILEGE_KRNL);set_idt_desc(INT_VECTOR_DEBUG, DA_386IGate, exc_single_step_exception, PRIVILEGE_KRNL);set_idt_desc(INT_VECTOR_NMI, DA_386IGate, exc_nmi, PRIVILEGE_KRNL);set_idt_desc(INT_VECTOR_BREAKPOINT, DA_386IGate, exc_breakpoint_exception, PRIVILEGE_USER);set_idt_desc(INT_VECTOR_OVERFLOW, DA_386IGate, exc_overflow, PRIVILEGE_USER);//篇幅所限,未全部展示set_idt_desc(INT_VECTOR_PAGE_FAULT, DA_386IGate, exc_page_fault, PRIVILEGE_KRNL);set_idt_desc(INT_VECTOR_IRQ0 + 0, DA_386IGate, hxi_hwint00, PRIVILEGE_KRNL);set_idt_desc(INT_VECTOR_IRQ0 + 1, DA_386IGate, hxi_hwint01, PRIVILEGE_KRNL);set_idt_desc(INT_VECTOR_IRQ0 + 2, DA_386IGate, hxi_hwint02, PRIVILEGE_KRNL);set_idt_desc(INT_VECTOR_IRQ0 + 3, DA_386IGate, hxi_hwint03, PRIVILEGE_KRNL);//篇幅所限,未全部展示return;}
上面的代码已经很明显了,一开始把所有中断的处理程序设置为保留的通用处理程序,避免未知中断异常发生了CPU无处可去,然后对已知的中断和异常进一步设置,这会覆盖之前的通用处理程序,这样就可以确保万无一失。
下面我们把这些代码整理一下,安装到具体的调用路径上,让上层调用者调用到就好了。
我们依然在halintupt.c文件中写上init_halintupt()函数:
void init_halintupt(){init_idt_descriptor();init_intfltdsc();return;}
到此为止,CPU体系层面的中断就初始化完成了。你会发现,我们在init_halintupt()函数中还调用了init_intfltdsc()函数,这个函数是干什么的呢?请往下看。
我们先来设计一下Cosmos的中断处理框架,后面我们把中断和异常统称为中断,因为它们的处理方式相同。
前面我们只是解决了中断的CPU相关部分,而CPU只是响应中断,但是并不能解决产生中断的问题。
比如缺页中断来了,我们要解决内存地址映射关系,程序才可以继续运行。再比如硬盘中断来了,我们要读取硬盘的数据,要处理这问题,就要写好相应的处理函数。
因为有些处理是内核所提供的,而有些处理函数是设备驱动提供的,想让它们和中断关联起来,就要好好设计中断处理框架了。
下面我们来画幅图,描述中断框架的设计:
可以看到,中断、异常分发器的左侧的东西我们已经处理完成,下面需要写好中断、异常分发器和中断异常描述符。
我们先来搞定中断异常描述,结合框架图,中断异常描述也是个表,它在C语言中就是个结构数组,让我们一起来写好这个数组:
typedef struct s_INTFLTDSC{spinlock_t i_lock;u32_t i_flg;u32_t i_stus;uint_t i_prity; //中断优先级uint_t i_irqnr; //中断号uint_t i_deep; //中断嵌套深度u64_t i_indx; //中断计数list_h_t i_serlist; //也可以使用中断回调函数的方式uint_t i_sernr; //中断回调函数个数list_h_t i_serthrdlst; //中断线程链表头uint_t i_serthrdnr; //中断线程个数void* i_onethread; //只有一个中断线程时直接用指针void* i_rbtreeroot; //如果中断线程太多则按优先级组成红黑树list_h_t i_serfisrlst;uint_t i_serfisrnr;void* i_msgmpool; //可能的中断消息池void* i_privp;void* i_extp;}intfltdsc_t;
上面结构中,记录了中断了优先级。因为有些中断可以稍后执行,而有的中断需要紧急执行,所以要设计一个优先级。其中还有中断号,中断计数等统计信息。
中断可以由线程的方式执行,也可以是一个回调函数,该函数的地址放另一个结构体中,这个结构体我已经帮你写好了,如下所示。
typedef drvstus_t (*intflthandle_t)(uint_t ift_nr,void* device,void* sframe); //中断处理函数的指针类型typedef struct s_INTSERDSC{list_h_t s_list; //在中断异常描述符中的链表list_h_t s_indevlst; //在设备描述描述符中的链表u32_t s_flg;intfltdsc_t* s_intfltp; //指向中断异常描述符void* s_device; //指向设备描述符uint_t s_indx;intflthandle_t s_handle; //中断处理的回调函数指针}intserdsc_t;
如果内核或者设备驱动程序要安装一个中断处理函数,就要先申请一个intserdsc_t结构体,然后把中断函数的地址写入其中,最后把这个结构挂载到对应的intfltdsc_t结构中的i_serfisrlst链表中。
你可能要问了,为什么不能直接把中断处理函数放在intfltdsc_t结构中呢,还要多此一举搞个intserdsc_t结构体呢?
这是因为我们的计算机中可能有很多设备,每个设备都可能产生中断,但是中断控制器的中断信号线是有限的。你可以这样理解:中断控制器最多只能产生几十号中断号,而设备不止几十个,所以会有多个设备共享一根中断信号线。
这就导致一个中断发生后,无法确定是哪个设备产生的中断,所以我们干脆让设备驱动程序来决定,因为它是最了解设备的。
这里我们让这个intfltdsc_t结构上的所有中断处理函数都依次执行,查看是不是自己的设备产生了中断,如果是就处理,不是则略过。
好,明白了这两个结构之后,我们就要开始初始化了。首先是在halglobal.c文件定义intfltdsc_t结构。
//定义intfltdsc_t结构数组大小为256HAL_DEFGLOB_VARIABLE(intfltdsc_t,machintflt)[IDTMAX];
下面我们再来实现中断、异常分发器函数,如下所示。
//中断处理函数void hal_do_hwint(uint_t intnumb, void *krnlsframp){intfltdsc_t *ifdscp = NULL;cpuflg_t cpuflg;//根据中断号获取中断异常描述符地址ifdscp = hal_retn_intfltdsc(intnumb);//对断异常描述符加锁并中断hal_spinlock_saveflg_cli(&ifdscp->i_lock, &cpuflg);ifdscp->i_indx++;ifdscp->i_deep++;//运行中断处理的回调函数hal_run_intflthandle(intnumb, krnlsframp);ifdscp->i_deep--;//解锁并恢复中断状态hal_spinunlock_restflg_sti(&ifdscp->i_lock, &cpuflg);return;}//异常分发器void hal_fault_allocator(uint_t faultnumb, void *krnlsframp){//我们的异常处理回调函数也是放在中断异常描述符中的hal_do_hwint(faultnumb, krnlsframp);return;}//中断分发器void hal_hwint_allocator(uint_t intnumb, void *krnlsframp){hal_do_hwint(intnumb, krnlsframp);return;}
前面的代码确实是按照我们的中断框架设计实现的,下面我们去实现hal_run_intflthandle函数,它负责调用中断处理的回调函数。
void hal_run_intflthandle(uint_t ifdnr, void *sframe){intserdsc_t *isdscp;list_h_t *lst;//根据中断号获取中断异常描述符地址intfltdsc_t *ifdscp = hal_retn_intfltdsc(ifdnr);//遍历i_serlist链表list_for_each(lst, &ifdscp->i_serlist){//获取i_serlist链表上对象即intserdsc_t结构isdscp = list_entry(lst, intserdsc_t, s_list);//调用中断处理回调函数isdscp->s_handle(ifdnr, isdscp->s_device, sframe);}return;}
上述代码已经很清楚了,循环遍历intfltdsc_t结构中,i_serlist链表上所有挂载的intserdsc_t结构,然后调用intserdsc_t结构中的中断处理的回调函数。
我们Cosmos链表借用了Linux所用的链表,代码我已经帮你写好了,放在了list.h和list_t.h文件中,请自行查看。
我们把CPU端的中断搞定了以后,还有设备端的中断,这个可以交给设备驱动程序,但是CPU和设备之间的中断控制器,还需要我们出面解决。
多个设备的中断信号线都会连接到中断控制器上,中断控制器可以决定启用或者屏蔽哪些设备的中断,还可以决定设备中断之间的优先线,所以它才叫中断控制器。
x86平台上的中断控制器有多种,最开始是8259A,然后是IOAPIC,最新的是MSI-X。为了简单的说明原理,我们选择了8259A中断控制器。
8259A在任何x86平台上都可以使用,x86平台使用了两片8259A芯片,以级联的方式存在。它拥有15个中断源(即可以有15个中断信号接入)。让我们看看8259A在系统上的框架图:
上面直接和CPU连接的是主8259A,下面的是从8259A,每一个8259A芯片都有两个I/O端口,我们可以通过它们对8259A进行编程。主8259A的端口地址是0x20,0x21;从8259A的端口地址是0xA0,0xA1。
下面我们来做代码初始化,我们程序员可以向8259A写两种命令字: ICW和OCW;ICW这种命令字用来实现8259a芯片的初始化。而OCW这种命令用来向8259A发布命令,以对其进行控制。OCW可以在8259A被初始化之后的任何时候被使用。
我已经把代码定好了,放在了8259.c文件中,如下所示:
void init_i8259(){//初始化主从8259aout_u8_p(ZIOPT, ICW1);out_u8_p(SIOPT, ICW1);out_u8_p(ZIOPT1, ZICW2);out_u8_p(SIOPT1, SICW2);out_u8_p(ZIOPT1, ZICW3);out_u8_p(SIOPT1, SICW3);out_u8_p(ZIOPT1, ICW4);out_u8_p(SIOPT1, ICW4);//屏蔽全部中断源out_u8_p(ZIOPT1, 0xff);out_u8_p(SIOPT1, 0xff);return;}
如果你要了解8259A的细节,就是上述代码中为什么要写入这些数据,你可以自己在Intel官方网站上搜索8259A的数据手册,自行查看。
这里你只要在init_halintupt()函数的最后,调用这个函数就行。你有没有想过,既然我们是研究操作系统不是要写硬件驱动,为什么要在初始化中断控制器后,屏蔽所有的中断源呢?因为我们Cosmos在初始化阶段还不能处理中断。
到此,我们的Cosmos的hal层初始化就结束了。关于内存管理器的初始化,我会在内存管理模块讲解,你先有个印象就行。
hal层的初始化已经完成,按照前面的设计,我们的Cosmos还有内核层,我们下面就要进入到内核层,建立一个文件,写上一个函数,作为本课程的结尾。
但是这个函数是个空函数,目前什么也不做,它是为Cosmos内核层初始化而存在的,但是由于课程只进行到这里,所以我只是写个空函数,为后面的课程做好准备。
由于内核层是从hal层进入的,必须在hal_start()函数中被调用,所以在此完成这个函数——init_krl()。
void init_krl(){//禁止函数返回die(0);return;}
下面我们在hal_start()函数中调用它就行了,如下所示
void hal_start(){//初始化Cosmos的hal层init_hal();//初始化Cosmos的内核层init_krl();return;}
从上面的代码中,不难发现Cosmos的hal层初始化完成后,就自动进入了Cosmos内核层的初始化。至此本课程已经结束。
写一个C函数是容易的,但是写操作系统的第一个C函数并不容易,好在我们一路坚持,没有放弃,才取得了这个阶段性的胜利。但温故而知新,对学过的东西要学而时习之,下面我们来回顾一下本课程的重点。
1.Cosmos的第一个C函数产生了,它十分简单但极其有意义,它的出现标志着C语言的运行环境已经完善。从此我们可以用C语言高效地开发操作系统了,由爬行时代进入了跑步前行的状态,可喜可贺。
2.第一个C函数,干的第一件重要工作就是**调用hal层的初始化函数。**这个初始化函数首先初始化了平台,初始化了机器信息结构供内核的其它代码使用,还初始化了我们图形显示驱动、显示了背景图片;其次是初始化了内存管理相关的数据结构;接着初始了中断,中断处理框架是两层,所以最为复杂;最后初始化了中断控制器。
3.当hal层初始化完成了,我们就进入了内核层,由于到了课程的尾声,我们先暂停在这里。
在这节课里我帮你写了很多代码,那些代码非常简单和枯燥,但是必须要有它们才可以。综合我们前面讲过的知识,我相信你有能力看懂它们。
请你梳理一下,Cosmos hal层的函数调用关系。
欢迎你在留言区跟我交流互动,也欢迎把这节课转发给你的朋友和同事。
好,我是LMOS,咱们下节课见!