Winter break is coming to an end before you know it. I believe most people have already started their new semester, so let me first wish you all a happy back-to-school season (doge). I will still make time to continue working on PA after school starts, and will continue to introduce the concepts involved in PA from a theoretical perspective in these articles. The update speed might be slightly slower, but I suppose nobody cares anyway I hope everyone can understand.
The Simplest Operating System
In the last PA, we completed the event handling system. Many events can occur in our computer, such as errors, exceptions, etc. After these events occur, our computer will memorize the state at the moment the event occurred (such as the contents of the General Registers (GR), etc.), and from that, analyze the type of event and the parameters required for handling it.
Recall: Event Handling Process
In the RISC-V architecture, there is a set of dedicated System Registers (SR) for event handling. Among them, there is the system register
mcauseused to store the cause of the event,mtvecused to store the location of the event handler function,mepcused to store the PC value when the event occurred…Meanwhile, during an event, the operating system may need some parameters to correctly handle the event. Therefore, RISC-V defines some general registers specifically for storing event-related parameters and return values.
With such an event handling process, our application programs can communicate with the operating system, and acquire information from the operating system!
Therefore, we need to introduce the simplest operating system: Nanos-lite. The program execution processes we described previously were all on bare metal, utilizing all the resources of the NEMU hardware system (currently Nanos-lite is also a program running on bare metal). Now, with the introduction of an OS, our programs become entities running on top of the OS, capable of utilizing only the portion of resources allocated by the OS. Consequently, the principles of program execution must change accordingly.
In our operating system, our applications are stored in the form of binary executable programs (like .exe files in Windows, .elf files in Linux). When these executables are run, the information within them (instructions, data, etc.) is loaded into a specific memory space designated by the OS, and the PC is pointed to the first instruction in this file.
This might not look any different from before, but it is truly an epoch-making change in computer history. Our previous way of execution is akin to feeding a punch card with a program on it (what a vintage vibe) directly into the machine to run; the current execution method supports batch processing systems, capable of running multiple programs continuously. After adding virtualization and concurrency later, we will arrive at a system similar to modern operating systems!
ELF files provide two perspectives for organizing an executable file: one is the section perspective oriented towards the linking process, which provides information for linking and relocation (such as the symbol table); the other is the segment perspective oriented towards execution, providing information for loading the executable. Through the readelf command, we can also see the mapping relationship between sections and segments: a segment may consist of 0 or multiple sections, but a section might not be included in any segment.
We only care about how to load the program right now, so we will focus on the segment perspective. ELF uses a program header table to manage segments. An entry in the program header table describes all attributes of a segment, including type, virtual address, flags, alignment, as well as the in-file offset and segment size. Based on this information, we know which bytes of the executable need to be loaded. Simultaneously, we can see that loading an executable does not mean loading everything it contains; we only need to load contents relevant to runtime. For example, debugging information and symbol tables do not need to be loaded. We can determine if a segment needs to be loaded by checking if its Type attribute is PT_LOAD.
1 +-------+---------------+-----------------------+ 2 | |...............| | 3 | |...............| | ELF file 4 | |...............| | 5 +-------+---------------+-----------------------+ 6 0 ^ | 7 |<------+------>| 8 | | | 9 | | 10 | +----------------------------+ 11 | | 12Type | Offset VirtAddr PhysAddr |FileSiz MemSiz Flg Align 13LOAD +-- 0x001000 0x03000000 0x03000000 +0x1d600 0x27240 RWE 0x1000 14 | | | 15 | +-------------------+ | 16 | | | 17 | | | | | 18 | | | | | 19 | | +-----------+ --- | 20 | | |00000000000| ^ | 21 | | --- |00000000000| | | 22 | | ^ |...........| | | 23 | | | |...........| +------+ 24 | +--+ |...........| | 25 | | |...........| | 26 | v |...........| v 27 +-------> +-----------+ --- 28 | | 29 | | 30 Memory
System Calls
Some functionalities of the runtime environment require hardware resources, mapping physical memory needs physical memory, updating the screen needs the frame buffer. In PA2, our computer system was monopolized by a single program; it could play however it wanted, and if it broke something, it was only its own problem. However, in modern computer systems, there might be multiple programs concurrently or even simultaneously using the resources in the computer system. If every program directly used these resources without knowing each other’s usage status, the whole system would quickly fall into chaos: e.g., I overwritten your display, you overwrote my memory space…
Therefore, there needs to be a role to uniformly manage the resources in the system: programs can no longer use resources arbitrarily; when they need to use them, they need to submit a request to the resource manager. Since the operating system sits at a high privilege level, enjoying supreme rights, it naturally needs to fulfill corresponding obligations: as the resource manager managing all resources in the system, the OS also needs to provide corresponding services for user programs. These services must be presented in a unified interface, and user programs can only request services through this interface.
This interface is the system call. System calls divide the entire runtime environment into two parts: one part is the OS internal (kernel space), and the other part is the user space. Functions that access system resources will be implemented in the kernel space, while the user space retains some functions that do not require system resources, as well as the system call interfaces used to request system resource-related services.
Under this model, user programs can only dutifully “calculate” within the user space. Any task beyond pure computational abilities needs to be requested from the OS via system calls. If a user program attempts any illegal operations, the CPU will throw an exception signal to the OS, causing the illegal operation instruction to “fail” and leaving it to the OS to handle.
The Process of System Calls
System calls are the way applications communicate with the operating system, and they are also a special type of event. When an application triggers a system call event, it stores the system call parameters in the general registers mentioned above. During the event response process, the event response function defined by the OS will be called, within which the system call handler function is invoked. The system call function will provide the application with corresponding functionalities based on the parameters stored in the general registers, thereby achieving the purpose of providing services to the application.
To be continued…