目前,我们实现 sys_read
sys_write
和 sys_exit
三个简单的系统调用。通过学习它们的实现,更多的系统调用也并没有多难。
在用户程序中实现系统调用比较容易,就像我们之前在操作系统中使用 sbi_call
一样,只需要符合规则传递参数即可。而且这一次我们甚至不需要参考任何标准,每个人都可以为自己的操作系统实现自己的标准。
例如,在实验指导中,系统调用的编号使用了 musl 中的编码和参数格式。但实际上,在实现操作系统的时候,编码和参数格式都可以随意调整,只要在用户程序中的调用和操作系统中的解释相符即可。
{% label %}代码示例{% endlabel %}
// 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
的执行
- 返回值存放在
- 程序进入等待
- 同样需要更新
x10
和sepc
,但是需要将当前线程标记为等待,切换其他线程来执行
- 同样需要更新
- 程序终止
- 不需要考虑系统调用的返回,直接删除线程
- 返回数值并继续执行:
那么具体该如何实现读 / 写系统调用呢?这里我们会利用文件的统一接口 INode
,使用其中的 read_at()
和 write_at()
接口即可。下一节就将讲解如何处理文件描述符。