Skip to content
GitLab
Projects Groups Topics Snippets
  • /
  • Help
    • Help
    • Support
    • Community forum
    • Submit feedback
    • Contribute to GitLab
  • Sign in
  • P project210-USTC
  • Project information
    • Project information
    • Activity
    • Labels
    • Members
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributor statistics
    • Graph
    • Compare revisions
  • Issues 0
    • Issues 0
    • List
    • Boards
    • Service Desk
    • Milestones
  • Packages and registries
    • Packages and registries
    • Package Registry
    • Terraform modules
  • Monitor
    • Monitor
    • Incidents
  • Analytics
    • Analytics
    • Value stream
    • Repository
  • Wiki
    • Wiki
  • Snippets
    • Snippets
  • Activity
  • Graph
  • Create a new issue
  • Commits
  • Issue Boards
Collapse sidebar
  • 叫什么名字好呢队
  • project210-USTC
  • Wiki
  • Home
  • 1.简单的OS宏观理解

1.简单的OS宏观理解 · Changes

Page history
Update 1.简单的OS宏观理解 authored Aug 13, 2023 by lqzzy's avatar lqzzy
Hide whitespace changes
Inline Side-by-side
home/1.简单的OS宏观理解.md 0 → 100644
View page @ d6bae7a3
# 简单的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
Clone repository
  • home
    • 1.简单的OS宏观理解
    • 2.内存管理
    • 3.中断
    • 4.调度
    • 5.文件系统