“主”函数的编写

关于“主”函数的一些说明

通常我们的 C 语言程序都会有一个主函数(main 函数)作为程序的入口,在这里我们也先写一个 main 函数作为我们的内核的“主”函数,之所以在“主”函数上加引号,是因为实际上这个函数并不是整个系统真正的入口,而 main 的名字也是可以任意取的,这一细节后续还会再提到,在这里还是遵循我们的习惯,将它取名为 main。

相关函数的介绍

在编写操作系统时是有很多地方与编写普通程序有很大的不同的,首先就是写代码的时候无法使用一些常用的头文件,如 stdio.h, stdlib.h 等,因为这些头文件是依赖于系统的实现的,换句话说,这些头文件的功能并不是 C 语言或编译器天生就具备的,而是由操作系统提供了底层支持,由操作系统完成了内部的一些函数和定义。

既然不能使用这些头文件,我们就无法使用 printf, scanf, puts, gets, putchar, getchar 之类的函数。好在,sbi 为我们提供了一些字符输入输出的环境调用功能,但是需要我们使用汇编语言来调用,因此我们会对这些功能做 C 语言的函数封装,这样我们就能借助我们自己封装的 C 语言函数使用它们了。

main 函数中,我们使用了这几个封装:

  • sbi_probe_extension
    • 探测 SBI 扩展的可用情况
  • sbi_console_putchar
    • 打印字符
  • sbi_get_impl_id
    • 获取当前的 SBI(Supervisor 二进制接口)实现 ID

同样我们也自己定义了一个符合 sbi 标准的结构体类型struct sbiret,用于表示一些 sbi 环境调用的返回值。在 main 函数中,主要是 sbi_probe_extension 函数使用了这个结构体,返回的值中若成员 value 的值不为0,则表示功能扩展可用,这些标准都会在后续介绍到,此处暂且不提。

另外我们又对 sbi_console_putchar 做了进一步封装,封装后函数名为 kputs,可以直接输出字符串,并在最后输出换行,封装代码如下:

static void
kputs(const char *msg)
{
    for ( ; *msg != '\0'; ++msg )
    {
        sbi_console_putchar(*msg);
    }
    sbi_console_putchar('\n');
}

“主”函数代码与功能介绍

在这里给出 main 函数的代码:

int 
main()
{
    struct sbiret ret;
    ret = sbi_probe_extension(TIMER_EXTENTION);
    if (ret.value != 0)
        kputs("TIMER_EXTENTION: available");
    else 
        kputs("TIMER_EXTENTION: unavailable");

    ret = sbi_probe_extension(SHUT_DOWN_EXTENTION);
    if (ret.value != 0)
        kputs("SHUT_DOWN_EXTENTION: available");
    else 
        kputs("SHUT_DOWN_EXTENTION: unavailable");

    ret = sbi_probe_extension(HART_STATE_EXTENTION);
    if (ret.value != 0)
        kputs("HART_STATE_EXTENTION: available");
    else 
        kputs("HART_STATE_EXTENTION: unavailable");

    ret = sbi_get_impl_id();
    switch (ret.value)
    {
        case BERKELY_BOOT_LOADER:
            kputs("Implemention ID: Berkely boot loader");
            break;
        case OPENSBI:
            kputs("Implemention ID: OpenSBI");
            break;
        case XVISOR:
            kputs("Implemention ID: XVISOR");
            break;
        case KVM:
            kputs("Implemention ID: KVM");
            break;
        case RUSTSBI:
            kputs("Implemention ID: RustSBI");
            break;
        case DIOSIX:
            kputs("Implemention ID: DIOSIX");
            break;
        default:
            kputs("Implemention ID: Unkonwn");
    }
    kputs("Hello LZU OS");
    while (1)
        ; /* infinite loop */
    return 0;
}

根据前面的说明,我们很容易读通这个程序,它所做的不过就是检测TIMER_EXTENTION(时钟)、SHUT_DOWN_EXTENTION(关机)、HART_STATE_EXTENTION(硬件级线程,这一拓展我们实际并不会使用,只是单纯的检测一下)这几个 sbi 扩展是否可用 (available) ,检测当前 sbi 的实现,最后输出"Hello LZU OS",随后陷入死循环

小知识:HART 硬件级线程

hart 是硬件线程(hardware thread)的缩略形式。 我们用该术语将它们与大多数程序员熟悉的软件线程区分开来。软件线程在 harts 上进行分时复用。 大多数处理器核都只有一个hart。
—— 摘自《RISC-V 手册》

如果一个部件包含了一个独立的取指令单元,则该部件被称为核心(core)。一个RiscV兼容的核心能够通过多线程技术(或者说超线程技术)支持多个RiscV兼容硬件线程(harts),harts这儿就是指硬件线程, hardware thread的意思。所谓超线程技术,就是在一个硬件核中,实现多份硬件线程,每个硬件线程都有自己独立的寄存器组等上下文资源,但大多数的运算资源都被所有线程复用,因此面积效率很高。超线程最早出现是在Intel的处理器中。
—— 摘自 Risc-V简要概括 ( archive.is互联网存档 )

简而言之,硬件级线程和现在常见的 x86 处理器上的“m 核 n 线程”中的“n 线程”对应,若处理器设计时没有做超线程,那么一个核心就是一个硬件及线程,若有做超线程,那么某些核心可以有多个线程。

为什么 main 函数到最后是陷入死循环而不是退出呢?我们在讲完完整的启动流程后再来回顾这个问题。

results matching ""

    No results matching ""