A toy OS, 32bit finally implements a simple interactive shell 
TO-DO List:
There are two hard disks in total, the system itself is installed on the main disk, and the boot mode of MBR is adopted, and the process of MBR->Boot Loader->Kernnel is used.
MBR is located in 1 sector starting LBA 0号扇区
Boot Loader is located in 4 sectors starting with LBA 2号扇区of the disk
Kernel is located within 200 sectors starting with disk LBA 9号扇区
The file system is implemented on the slave disk. This may not be very reasonable. If the file system is based on the logic of the commercial system, it should be implemented now, and then the operating system is installed in the corresponding partition.
Memory paging, one page is 4Kb
Memory management adopts bitmap management, and is distinguished by size when allocating memory. If it is greater than 1024 bytes, it is allocated directly by page.
If it is less than 1024 bytes, on the basis of allocating arena by page, use the idle blockchain in arena for allocation and control.
For convenience, although the paging mechanism is enabled, the switching function of memory pages and disks is not implemented.

PCB is 1 page size
The core essence of thread scheduling is to switch PCB by controlling the ESP pointer switching by clock interrupt. The priority is reflected in the length of the running time slice of each thread.
The implementation of the process is based on threads, and the selection of TSS is based on Linux, using single TSS to backup level 0 stacks and level 0 stack pointers. The biggest difference from a thread is that the process has a page table address in the PCB, which is the biggest difference between a process and a thread. The process truly has its own independent virtual memory space.
There is no efficient algorithm in scheduling, just use queue loop scheduling
Implementation of idle thread
The implementation of the idle thread is very simple. When the first time you get the schedule, you block it and give it out of the CPU. When the scheduler executes the dispatch again, if no ready thread or process is found in the ready queue, the idle thread will be awakened. At this time, the idle thread suspends the CPU through hlt . When the time slice is used up and the CPU has not found a ready process or thread, continue to replace the idle thread with the CPU. At this time, the idle will continue to block itself and then start repeating the above scheduling process.
// 空载任务
static void idle ( void * arg ) {
while ( 1 ) {
thread_block ( TASK_BLOCKED );
asm volatile ( "sti; hlt" : : : "memory" );
}
}Process fork
The process's fork first copy the PCB of the current process, and then create a new page table through the virtual pool bitmap of the current process. The correspondence of the virtual address is exactly the same as in the original process. Finally, a interrupt site is forged and the child process is added to the scheduling queue and wait for the scheduling to be executed. In the forged interrupt site, the eax in the PCB of the child process is modified to 0, which means that the return value of fork is 0 in the new process, while the eax in the PCB of the parent process remains unchanged, representing the pid of the child process. The parent process returns through the end of the system call, while the child process returns directly through the interrupt exit function.
Process exec
The implementation of exec first loads the elf file from disk to memory, then changes the process name in the PCB of the current process, and puts the parameters required by the process to be executed into the agreed register, and modifies eip to the entry point of elf, forgery of the interrupt site, and executes the new process immediately by directly calling the interrupt exit function intr_exit.
Among them, ELF's Entry Point is implemented by implementing an extremely simple CRT itself, which gives a _start entry and pushes the agreed parameter registers into the 3-level stack, and calls the main function of the external command to realize parameter passing.
[bits 32]
extern main
extern exit
; 这是一个简易版的CRT
; 如果链接时候ld不指定-e main的话,那ld默认会使用_start来充当入口
; 这里的_start的简陋实现,充当了exec调用的进程从伪造的中断中返回时的入口地址
; 通过这个_start, 压入了在execv中存放用户进程参数的两个寄存器。然后call 用户进程main来实现了向用户进程传递参数
section .text
global _start
_start:
;下面这两个要和 execv 中 load 之后指定的寄存器一致
push ebx ;压入 argv
push ecx ;压入 argc
call main
; 压入main的返回值
push eax
call exit ; 不再返回,直接调度别的进程了,这个进程直接被回收了Process wait
After fork and executing a local command, in order not to appear, the parent process needs to end in the local wait child process.
The implementation here is to enter the sys_wait system call, traverse the entire process queue, find the process whose parent process is its own suspended state, then obtain the return value in its pcb, recycle the pcb and page directory tables, and remove it from the scheduling queue. If no pending child process is found after traversal, block itself and wait for the child process to wake up.
Process exit
During execution, external commands are actually wrapped by a simple CRT created by themselves. The main of the simple CRT call external command will obtain its return value at the end, pass it to exit, and then call exit
The implementation here mainly does three things:
The entire process of loading external commands and executing them
First, the external command needs to provide an int main(int argc, char **argv) function. When linking, you need to bring a homemade simple CRT start.o , and finally write the compiled external commands to the file system.
When an external command is to be executed, the current process forks a process, execv in the new process, the current process executes wait, passes an address to accept the return value of the child process, and then blocks and waits for the new process to return. The new process loads external commands from the file system to memory in exevc, and changes the relevant content in pcb to the information of external commands, and modifies the eip in the pcb interrupt stack to the CRT's _start entry (at this time the new process has completely replaced itself with the external command process to be executed), and finally uses the interrupt exit function intr_exit to fake the interrupt exit and enter the CRT and then enter the main of the external command. When the external command main execution ends and returns, CRT calls exit through the main return value, put the main return value in sys_exit into the corresponding position of the process pcb, recycle all resources except pcb and page directory table, and then wake up the parent process and block the child process. After the parent process is awakened, the system call sys_wait gets the return value from the pcb of the pcb of the pcb of the pcb of the pcb of the pcb of the pcb of the pcb of the pcb of the pcb of the pcb of the pcb, cleans up the page directory table of the pcb of the child process, removes the child process from the dispatch queue and the global queue, and returns the child process pid. At this point, the child process has been executed and is completely recycled.
The file system implementation imitates the inode of Unix-like systems
The partition limits the number of inodes 4096. The CPU operates the hard disk according to the size of the block (cluster), and one block is set to a sector, with 512 bytes.
Inode supports 12 direct blocks and 1 first-level indirect table. One block is 512 bytes in a sector, so a single file supports up to 140 * 512 bytes.
inode structure

File system layout

The correspondence between file descriptor and inode

The implementation of the pipeline depends on the file structure in the file system. Its essence is to replace the inode that the file structure should originally correspond to with a ring buffer space in the kernel space.
// 因为管道也是当作文件来对待,因此file结构体在针对真实文件和管道是有不同的意义
struct file {
// 文件操作的偏移指针, 当是管道是表示管道打开的次数
uint32_t fd_pos ;
// 文件的操作标志,当是管道是一个固定值0xFFFF
uint32_t fd_flag ;
// 对应的inode指针,当是管道时指向管道的环形缓冲区
struct inode * fd_inode ;
}; Because the kernel space is shared, communication between different processes can be achieved through read and write pipelines. The read and write of the pipeline is encapsulated in sys_write and sys_read , so there is no difference between operating the pipeline and operating ordinary files.
The essence of redirection is to change the address of the corresponding global descriptor table in the pcb file descriptor table, and then the operation of reading and writing the corresponding file descriptor is pointed to the new file.
The implementation of the pipeline character | in the shell is achieved by redirecting standard input and standard output to the pipeline.
int32_t sys_read ( int32_t fd , void * buf , uint32_t count ) {
if ( fd == stdin_no ) {
if ( is_pipe ( fd )) {
// 从已经重定向好管道中读
ret = pipe_read ( fd , buf , count );
} else {
// 从键盘获取输入
}
} else if ( is_pipe ( fd )) {
// 读管道
ret = pipe_read ( fd , buf , count );
} else {
// 读取普通文件
}
return ret ;
}
int32_t sys_write ( int32_t fd , const void * buf , uint32_t count ) {
if ( fd == stdout_no ) {
if ( is_pipe ( fd )) {
// 向已经重定向好管道中写入
return pipe_write ( fd , buf , count );
} else {
// 向控制台输出内容
}
} else if ( is_pipe ( fd )) {
// 写管道
return pipe_write ( fd , buf , count );
} else {
// 向普通文件写入
}
}