|
|
# 简单的OS宏观理解
|
|
|
|
|
|
想要理解操作系统,首先需要对操作系统有一个宏观的认识,这样才能更好的理解操作系统的细节。
|
|
|
|
|
|
在操作系统中,我们在shell里面输入命令,然后系统给出相应的响应,这是一个典型的操作系统的
|
|
|
交互过程。我们在这一节中,将结合代码,来解释这个过程是如何实现的。
|
|
|
|
|
|
在结合代码时,代码中会有暂时不需要理解的函数,后面会有详细的细节介绍。
|
|
|
|
|
|
## 需要掌握的概念
|
|
|
|
|
|
- 进程
|
|
|
- 调度
|
|
|
- exec系统调用
|
|
|
- fork系统调用
|
|
|
|
|
|
## main函数
|
|
|
|
|
|
我们的xv6的main函数如下:
|
|
|
|
|
|
```c
|
|
|
#include "types.h"
|
|
|
#include "param.h"
|
|
|
#include "memlayout.h"
|
|
|
#include "loongarch.h"
|
|
|
#include "defs.h"
|
|
|
#include "mm/pm.h"
|
|
|
#include "mm/kmalloc.h"
|
|
|
#include "hal/disk.h"
|
|
|
#include <printf.h>
|
|
|
|
|
|
__attribute__ ((aligned (16))) char stack0[4096 * NCPU];
|
|
|
volatile static int started = 0;
|
|
|
|
|
|
// start() jumps here in supervisor mode on all CPUs.
|
|
|
void
|
|
|
main()
|
|
|
{
|
|
|
if(cpuid() == 0){
|
|
|
consoleinit();
|
|
|
// uartinit();
|
|
|
printfinit();
|
|
|
kpminit(); // physical page allocator
|
|
|
vminit(); // create kernel page table
|
|
|
kmallocinit(); // small physical memory allocator
|
|
|
procinit();
|
|
|
trapinit();
|
|
|
apic_init();
|
|
|
extioi_init();
|
|
|
// disk_init();
|
|
|
binit();
|
|
|
// iinit();
|
|
|
// fileinit();
|
|
|
userinit();
|
|
|
__sync_synchronize();
|
|
|
started = 1;
|
|
|
printf("\nxv6 init successfully \n");
|
|
|
printsys();
|
|
|
} else {
|
|
|
while(started == 0)
|
|
|
;
|
|
|
__sync_synchronize();
|
|
|
printf("hart %d starting\n", cpuid());
|
|
|
}
|
|
|
|
|
|
scheduler();
|
|
|
}
|
|
|
```
|
|
|
|
|
|
main函数中的各种init,都是对系统的初始化,这些init函数的作用是什么,我们会在后面的章节中进行详细的介绍。
|
|
|
|
|
|
在这里我们主要看userinit和scheduler函数。
|
|
|
|
|
|
## userinit函数
|
|
|
|
|
|
```c
|
|
|
// Set up first user process.
|
|
|
void
|
|
|
userinit(void)
|
|
|
{
|
|
|
struct proc *p;
|
|
|
|
|
|
//创建一个进程
|
|
|
p = allocproc();
|
|
|
if(p == 0)
|
|
|
panic("userinit: allocproc");
|
|
|
initproc = p;
|
|
|
|
|
|
// allocate one user page and copy init's instructions
|
|
|
// and data into it.
|
|
|
|
|
|
//Load the user initcode into address 0 of pagetable,
|
|
|
uvminit(p->pagetable, initcode, sizeof(initcode));
|
|
|
p->sz = PGSIZE;
|
|
|
|
|
|
// prepare for the very first "return" from kernel to user.
|
|
|
p->trapframe->era = 0; // user program counter
|
|
|
p->trapframe->sp = PGSIZE; // user stack pointer
|
|
|
|
|
|
safestrcpy(p->name, "initcode", sizeof(p->name));
|
|
|
// p->cwd = namei("/");
|
|
|
|
|
|
p->state = RUNNABLE;
|
|
|
|
|
|
release(&p->lock);
|
|
|
}
|
|
|
```
|
|
|
|
|
|
userinit 函数的作用是初始化第一个用户进程,设置进程状态为 RUNNABLE。
|
|
|
|
|
|
uvminit在这里的作用是将initcode加载到进程的虚拟地址空间中,起始位置为0,这里的initcode是
|
|
|
一个数组,它的内容是一个汇编程序编译后的二进制代码。
|
|
|
|
|
|
然后在后面的代码中,我们可以看到,设置了trapframe中的era为0,这样做可以在调度器进行上下文
|
|
|
切换后把CPU的PC设置为0,这样就可以执行initcode了。
|
|
|
|
|
|
我们现在有了可以执行的第一个进程,但是我们还没有办法切换到这个进程中去执行,这就需要调度器了。
|
|
|
|
|
|
## scheduler函数
|
|
|
|
|
|
```c
|
|
|
void
|
|
|
scheduler(void)
|
|
|
{
|
|
|
struct proc *p;
|
|
|
struct cpu *c = mycpu();
|
|
|
|
|
|
c->proc = 0;
|
|
|
for(;;){
|
|
|
// Avoid deadlock by ensuring that devices can interrupt.
|
|
|
intr_on();
|
|
|
|
|
|
for(p = proc; p < &proc[NPROC]; p++) {
|
|
|
acquire(&p->lock);
|
|
|
if(p->state == RUNNABLE) {
|
|
|
// Switch to chosen process. It is the process's job
|
|
|
// to release its lock and then reacquire it
|
|
|
// before jumping back to us.
|
|
|
p->state = RUNNING;
|
|
|
c->proc = p;
|
|
|
swtch(&c->context, &p->context);
|
|
|
// Process is done running for now.
|
|
|
// It should have changed its p->state before coming back.
|
|
|
c->proc = 0;
|
|
|
}
|
|
|
release(&p->lock);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
这个函数负责系统的调度,它会遍历所有的进程,如果发现有进程处于RUNNABLE状态,
|
|
|
就会将其设置为RUNNING状态,然后调用swtch函数进行上下文切换,上下文切换的具体
|
|
|
机制我们会在后面的章节中进行详细的介绍。现在只需要知道调用swtch后CPU会运行对应的
|
|
|
进程。
|
|
|
|
|
|
scheduler的执行是一个死循环,不断地寻找着进程列表中可以执行(RUNNABLE)的进程
|
|
|
然后切换到该进程中去执行。
|
|
|
|
|
|
记得我们在userinit中设置了一个名为initcode的进程吗,它的状态是RUNNABLE,
|
|
|
那么我们的调度器接下来会切换到initcode进程中去执行。这也是我们系统中第一个
|
|
|
执行的进程。下面我们来看看initcode的代码。
|
|
|
|
|
|
## initcode代码
|
|
|
|
|
|
```asm
|
|
|
# Initial process that execs /init.
|
|
|
# This code runs in user space.
|
|
|
|
|
|
#include "syscall.h"
|
|
|
|
|
|
# exec(init, argv)
|
|
|
.globl start
|
|
|
start:
|
|
|
la $a0, init
|
|
|
la $a1, argv
|
|
|
li.d $a7, SYS_exec
|
|
|
syscall 0
|
|
|
|
|
|
# for(;;) exit();
|
|
|
exit:
|
|
|
li.d $a7, SYS_exit
|
|
|
syscall 0
|
|
|
bl exit
|
|
|
|
|
|
# char init[] = "/init\0";
|
|
|
init:
|
|
|
.string "/init\0"
|
|
|
|
|
|
# char *argv[] = { init, 0 };
|
|
|
.p2align 2
|
|
|
argv:
|
|
|
.long init
|
|
|
.long 0
|
|
|
|
|
|
```
|
|
|
|
|
|
这段汇编代码实现了一个最初的用户进程,它负责执行 /init 程序,然后进入一个无限循环,
|
|
|
不断地执行系统调用以实现进程的退出。
|
|
|
|
|
|
总的来说,第一个进程initcode的作用是执行init程序,然后退出。
|
|
|
|
|
|
## init程序
|
|
|
|
|
|
init是用户空间的代码,是事先编译好然后被放到文件系统的镜像中的。这种文件系统中的
|
|
|
可执行文件操作系统一般通过先fork然后exec的典型模式来执行。
|
|
|
|
|
|
而initcode直接调用了exec,让我们来看看init干了什么。
|
|
|
|
|
|
```c
|
|
|
// init: The initial user-level program
|
|
|
|
|
|
#include "types.h"
|
|
|
#include "fs/stat.h"
|
|
|
#include "spinlock.h"
|
|
|
#include "sleeplock.h"
|
|
|
#include "fs/fs.h"
|
|
|
#include "fs/file.h"
|
|
|
#include "user.h"
|
|
|
#include "fcntl.h"
|
|
|
|
|
|
char *argv[] = { "sh", 0 };
|
|
|
|
|
|
int
|
|
|
main(void)
|
|
|
{
|
|
|
int pid, wpid;
|
|
|
|
|
|
// if(open("console", O_RDWR) < 0){
|
|
|
// mknod("console", CONSOLE, 0);
|
|
|
// open("console", O_RDWR);
|
|
|
// }
|
|
|
open("/dev/console", O_RDWR);
|
|
|
dup(0); // stdout
|
|
|
dup(0); // stderr
|
|
|
|
|
|
for(;;){
|
|
|
// printf("init: starting sh\n");
|
|
|
pid = fork();
|
|
|
if(pid < 0){
|
|
|
printf("init: fork failed\n");
|
|
|
exit(1);
|
|
|
}
|
|
|
if(pid == 0){
|
|
|
exec("sh", argv);
|
|
|
printf("init: exec sh failed\n");
|
|
|
exit(1);
|
|
|
}
|
|
|
|
|
|
for(;;){
|
|
|
// this call to wait() returns if the shell exits,
|
|
|
// or if a parentless process exits.
|
|
|
wpid = wait((int *) 0);
|
|
|
if(wpid == pid){
|
|
|
// the shell exited; restart it.
|
|
|
break;
|
|
|
} else if(wpid < 0){
|
|
|
printf("init: wait returned an error\n");
|
|
|
exit(1);
|
|
|
} else {
|
|
|
// it was a parentless process; do nothing.
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
init 程序就是典型的fork/exec方式
|
|
|
|
|
|
在一个死循环中:
|
|
|
|
|
|
子进程操作:
|
|
|
|
|
|
if (pid < 0):如果 fork 失败(返回值小于 0),打印错误消息并退出程序。
|
|
|
if (pid == 0):如果是子进程,在子进程中执行以下操作:
|
|
|
exec("sh", argv);:通过系统调用 exec 执行名为 "sh" 的程序,传递 argv 作为参数。这会将当前进程替换为 Shell 进程。
|
|
|
如果 exec 失败,打印错误消息并退出子进程。
|
|
|
|
|
|
父进程操作:
|
|
|
|
|
|
在父进程中,继续执行以下操作:
|
|
|
通过系统调用 wait 等待任何子进程退出。wait 将会阻塞父进程,直到一个子进程退出。
|
|
|
如果等待的子进程 ID 等于之前创建的 Shell 进程的 ID,意味着 Shell 退出了,所以重新启动 Shell 进程。
|
|
|
如果等待返回一个错误(小于 0),打印错误消息并退出父进程。
|
|
|
|
|
|
循环重新开始,进入下一次迭代,重新创建一个新的 Shell 子进程。
|
|
|
|
|
|
调用exec执行sh的程序,那么sh也是文件系统中的一个编译好的可执行文件,我们来看看它的代码。
|
|
|
|
|
|
## sh程序
|
|
|
|
|
|
```c
|
|
|
int
|
|
|
main(void)
|
|
|
{
|
|
|
static char buf[100];
|
|
|
int fd;
|
|
|
|
|
|
// Ensure that three file descriptors are open.
|
|
|
while((fd = open("console", O_RDWR)) >= 0){
|
|
|
if(fd >= 3){
|
|
|
close(fd);
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Read and run input commands.
|
|
|
while(getcmd(buf, sizeof(buf)) >= 0){
|
|
|
if(buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' '){
|
|
|
// Chdir must be called by the parent, not the child.
|
|
|
buf[strlen(buf)-1] = 0; // chop \n
|
|
|
if(chdir(buf+3) < 0)
|
|
|
fprintf(2, "cannot cd %s\n", buf+3);
|
|
|
continue;
|
|
|
}
|
|
|
if(fork1() == 0)
|
|
|
runcmd(parsecmd(buf));
|
|
|
wait(0);
|
|
|
}
|
|
|
exit(0);
|
|
|
}
|
|
|
```
|
|
|
|
|
|
shell程序会调用getcmd等待你输入命令,然后根据你的命令采用fork/exec模式,让子进程去执行,
|
|
|
然后父进程等待子进程执行完毕,最后退出。
|
|
|
|
|
|
## 总结
|
|
|
|
|
|
结合上面的代码,我们可以看到,当我们的系统启动后,会创建一个initcode进程,它会执行
|
|
|
init程序,init程序会不断地exec sh程序,在每一个init 执行的sh程序中,它会等待你输入命令,然后根据你的命令采用fork/exec模式,
|
|
|
让子进程去执行,然后父进程等待子进程执行完毕,最后退出。
|
|
|
|
|
|
这样就完成了一个简单的操作系统的宏观理解。 |
|
|
\ No newline at end of file |