实现系统调用

优质
小牛编辑
129浏览
2023-12-01

实现系统调用

目前,我们实现 sys_read sys_writesys_exit 三个简单的系统调用。通过学习它们的实现,更多的系统调用也并没有多难。

用户程序中调用系统调用

在用户程序中实现系统调用比较容易,就像我们之前在操作系统中使用 sbi_call 一样,只需要符合规则传递参数即可。而且这一次我们甚至不需要参考任何标准,每个人都可以为自己的操作系统实现自己的标准。

例如,在实验指导中,系统调用的编号使用了 musl 中的编码和参数格式。但实际上,在实现操作系统的时候,编码和参数格式都可以随意调整,只要在用户程序中的调用和操作系统中的解释相符即可。

代码示例

// musl 中的 sys_read 调用格式
llvm_asm!("ecall" :
    "={x10}" (/* 返回读取长度 */) :
    "{x10}" (/* 文件描述符 */),
    "{x11}" (/* 读取缓冲区 */),
    "{x12}" (/* 缓冲区长度 */),
    "{x17}" (/* sys_read 编号 63 */) ::
);
// 一种可能的 sys_read 调用格式
llvm_asm!("ecall" :
    "={x10}" (/* 现在的时间 */),
    "={x11}" (/* 今天的天气 */),
    "={x12}" (/* 读取一个字符 */) :
    "{x20}" (/* sys_read 编号 0x595_7ead */) ::
);

实验指导提供了第一种无趣的系统调用格式。

避免忙等待

在常见操作系统中,一些延迟非常大的操作,例如文件读写、网络通讯,都可以使用异步接口来进行。但是为了实现更加简便,我们的读写系统调用都是阻塞的。在 sys_read 中,使用了 loop 来保证仅当成功读取字符时才返回。

此时,如果用户程序需要获取从控制台输入的字符,但是此时并没有任何字符到来。那么,程序将被阻塞,而操作系统的职责就是尽量减少线程执行无用阻塞占用 CPU 的时间,而是将这段时间分配给其他可以执行的线程。具体的做法,将会在后面条件变量的章节讲述。

操作系统中实现系统调用

在操作系统中,系统调用的实现和中断处理一样,有同样的入口,而针对不同的参数设置不同的处理流程。为了简化流程,我们不妨把系统调用的处理结果分为三类:

  • 返回一个数值,程序继续执行
  • 程序进入等待
  • 程序将被终止

系统调用的处理流程

  • 首先,从相应的寄存器中取出调用代号和参数
  • 根据调用代号,进入不同的处理流程,得到处理结果
    • 返回数值并继续执行:
      • 返回值存放在 x10 寄存器,sepc += 4,继续此 context 的执行
    • 程序进入等待
      • 同样需要更新 x10sepc,但是需要将当前线程标记为等待,切换其他线程来执行
    • 程序终止
      • 不需要考虑系统调用的返回,直接删除线程

具体的调用实现

那么具体该如何实现读 / 写系统调用呢?这里我们会利用文件的统一接口 INode,使用其中的 read_at()write_at() 接口即可。下一节就将讲解如何处理文件描述符。