内容简介:System calls are a way for unprivileged, user applications to request services from the kernel. In the RISC-V architecture, we invoke the call using theWe have to set up our convention for handling system calls. We can use a convention that already exists,
Video
https://www.youtube.com/watch?v=6GW_jgkdGPw
Overview
System calls are a way for unprivileged, user applications to request services from the kernel. In the RISC-V architecture, we invoke the call using the ecall
instruction. This will cause the CPU to halt what it's doing, elevate privilege modes, and then jump to whatever function handler is stored in the mtvec
(machine trap vector) register. Remember, this is the "funnel" where all traps are handled, including our system calls.
We have to set up our convention for handling system calls. We can use a convention that already exists, so we can interface with a library, such as newlib. But, let's make this ours! We get to say what the system call numbers are, and where they will be when we execute a system call.
System Call Procedure
We usually only need to execute a system call if we're in a lower privilege mode. If we're in the kernel, we already have access to most of the privileged systems, which precludes us from actually needing to go into a system call.
Our system call comes to us through synchronous trap #8, which is the cause for a user-mode ecall. So, in our #8 handler, we forward the data over to our syscall handler. We'll be completely in Rust. We also need to be able to manipulate the program counter. Think of it, our exit
system call must be able to move on to another process, so we manipulate that through the mepc
(machine exception program counter) register.
Rust System Calls
As always, make sure you import your system call code with the following.
pub mod syscall;
This will go in your lib.rs
file.
Ordering and Numbering
Several libraries already have a certain order they expect your system calls to be in. However, we will be creating our own "C library" for our simple applications. Therefore, we're good to go as long as we're consistent.
Think about how we can do this. Whenever we execute the ecall instruction, the CPU elevates privileges and jumps to a trap vector. How do we send data along with it? The ARM architecture allows you to encode a number into their svc
(supervisor call) instruction. However, many operating systems forego that implementation. So, what do we do instead?
The answer is: registers. We have a ton of registers in the RISC-V architecture, so we aren't limited nearly as much as we were in the x86 days. For our system call convention, we will put the number of the system call into the first argument register, a0
. Subsequent parameters will go in a1, a2, a3, ..., a7. Then, we will return anything back using the same a0 register.
This is the same calling convention for regular functions, so it will interface well with what we already know. In RISC-V, we can use the pseudoinstruction call
to make a normal function call, or ecall
to make a supervisor call. Consistency is cey, or Konsistency is Key. Hmm.. Consistency is key isn't consistently sounding the 'k' :(.
Implementing System Calls
We redirect synchronous cause #8 to our system call Rust code.
8 => {
// Environment (system) call from User mode
println!("E-call from User mode! CPU#{} -> 0x{:08x}", hart, epc);
return_pc = do_syscall(return_pc, frame);
},
Most operating systems build a table with function pointers, but I'm using Rust's match
statement here. I haven't done any performance calculations, but I don't think one has a distinct advantage over another. Again, don't quote me, I haven't actually tested it.
As you can see, we receive a new program counter, which is the address of the instruction we want to execute when we return. The system call function must at least move this by 4 because the ecall
instruction is what's actually causing the synchronous interrupt. If we didn't move the program counter, we'd keep executing the ecall instruction over and over again. Fortunately, unlike x86, all instructions are 32-bits except for the 16-bit compressed instructions, but ecall
is always 32-bit because it doesn't have a compressed form.
Each mode has a different ecall cause. If we make an ecall in machine mode (the most privileged mode), we get cause #11. Otherwise, if we make an ecall in supervisor mode (typically the kernel mode), we get cause #9. So, we can split off what system calls mean what given the privilege mode. I'm going to keep it easy and konsistent (consistent?) by linking all three to the same system call function.
Let There Be System Calls!
Let's take a look at the code in Rust.
pub fn do_syscall(mepc: usize, frame: *mut TrapFrame) -> usize {
let syscall_number;
unsafe {
// A0 is X10, so it's register number 10.
syscall_number = (*frame).regs[10];
}
match syscall_number {
0 => {
// Exit
println!("You called the exit system call!");
mepc + 4
},
_ => {
print!("Unknown syscall number {}", syscall_number);
mepc + 4
}
}
The very first thing we need is the value of A0, which is the system call number. Since this is stored in the context during the trap handler phase, we can just retrieve it directly from memory. We have to put this in an unsafe
context because we're dereferencing a raw pointer, which may or may not be an accurate memory address. Since Rust cannot guarantee that it is, we're required to put this in an unsafe block.
This might be interesting to non-Rustaceans.
let syscall_number;
unsafe {
// A0 is X10, so it's register number 10.
syscall_number = (*frame).regs[10];
}
I'm creating a variable called syscall_number
, but since I cannot get its value until I'm inside of an unsafe block, it is just a placeholder. In fact, Rust doesn't put a type to it until we give it a value. You'll notice that I haven't put any constraints on the variable type, so I'm letting Rust decide.
Why did I do this? The unsafe block creates a new block, and thus, it creates a new scope. However, I want syscall_number
to contain an immutable value outside of the unsafe context. This is why I decided to do it this way. Technically, I can constrain the data type by using let syscall_number: u64;
, but it's not required since Rust will evaluate the data type whenever we get around to setting the variable equal to something.
Where do we go from here?
We will write the scheduler so that our system calls can actually do stuff--yes, stuff in the most technically accurate way possible! For example, we may need to postpone a process until after a certain amount of time (sort of like how sleep()
works), or we need to close the process (much like how exit()
) works. What about writing to the console--yep, we need that one too.
So, next, we will add processes and the system calls necessary as we reach them. We aren't making any future predictions, which might be dangerous, but we're implementing our OS as we find an unworkable solution. Hopefully, this will give you an appreciation for how operating systems are implemented to be all things to all applications!
Table of Contents →Chapter 6 → (Chapter 7) → Chapter 8
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
个体与交互
Ken Howard、Barry Rogers / 贾永娜、张凯峰 / 机械工业出版社华章公司 / 2012-3-20 / 45.00元
对敏捷软件开发的关注重点,通常都集中在“机制”方面,即过程和工具。“敏捷宣言”认为,个体与交互的价值要高于过程和工具,但这一点很容易被遗忘。在敏捷开发中,如果你重新将注意力放在人的方面,将会收获巨大利益。 本书展示了如何解决敏捷团队在实际项目中遭遇的问题。同时,本书也是很有实用价值的敏捷用户指南,其中包含的故事、最佳实践方法、经验以及技巧均可应用到实际项目当中。通过逐步实践,你将学会如何让团......一起来看看 《个体与交互》 这本书的介绍吧!