初始化栈的代码_操作系统习笔记(2) -- 内核初始化和进程运行和切换

操作系统习笔记(2) -- 内核初始化和进程运行和切换

前文:

操作系统习笔记(1) -- 一个操作系统的引导​mp.weixin.qq.com
535521656e3c6dfe1986140082a19f0c.png
苏畅:操作系统习笔记(1) -- 一个操作系统的引导​zhuanlan.zhihu.com

本文是操作系统学习笔记系列的第二章, 来研究内核的 init 进程的初始化和 进程的运行和切换. (内容太多了, 没看懂, 没能做到日更...

背景知识

crX 寄存器 控制寄存器

c7129d876d03114e778af4a8a0203dd2.png

用到的是这些:

  1. cr0 的 PG 位是表示是否开启分页 PE 表示是否保护模式, WP 是写保护, 是否允许 0 级程序向用户级程序的只读页面进行写操作, 别的是一些协处理器的控制位
  2. cr2 是当分页异常的时候, 把引起分页异常的线性地址放进去供操作系统处理
  3. cr3 是目录页表的起始地址. 不同的拥有独立虚拟空间的进程切换的时候, 要对这个做出修改.

分页

关于分页, 比较重要的是这张图:

f947ee84bd60ae8b9d3071cce160834f.png
  1. cr3 指向了一个页目录, 不同进程有自己的页目录
  2. 每个页目录可以指向一个页表项目, 进程创建的时候, 要自己添加这些页表啊项目, 格式参考上图

调用约定

这是 c语言函数调用的时候的内存布局和寄存器值.

15e895ae2eae36d6766a0eaf89a41925.png

esp 指向了栈帧的 return 地址

通过 pop 和 push 可以来操控 esp 的值,

  • call 的时候, 会保存当前的eip 到当前栈帧的上 , 并且把 eip 指向函数开始的地方
  • ret 的时候, 会根据 esp 指向的地址, 返回去

代码分析

初始化 init 进程

代码进入到 entry.S ,

  1. 先进行了一系列页表相关的配置:
  • 操作 cr4 开启超级页功能

movl %cr4, %eax orl $(CR4_PSE), %eax movl %eax, %cr4 # 开启超级页

    • 设置一个初始的页目录

movl $(V2P_WO(entrypgdir)), %eax movl %eax, %cr3
初始页面就是内核进程的页表 entrypgdir,
__attribute__((__aligned__(PGSIZE))) pde_t entrypgdir[NPDENTRIES] = { // Map VA's [0, 4MB) to PA's [0, 4MB) [0] = (0) | PTE_P | PTE_W | PTE_PS, // Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB) [KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS, };
代码写的抽象, 可以看出来是把内核进程的虚拟地址从 0-4mb 映射到 物理内存的 0-4m

    • 开启分页 和 写保护

movl %cr0, %eax orl $(CR0_PG|CR0_WP), %eax # 开启分页 和写保护 movl %eax, %cr0

    • 分配给 main 一个栈帧, 进入 c 语言处理

movl $(stack + KSTACKSIZE), %esp # 设置main 的栈 mov $main, %eax jmp *%eax .comm stack, KSTACKSIZE # 在 bss 区申请一个 stack s
esp 是栈顶指针寄存器, 把 esp 指向栈顶. 强行在内存这个位置进行main 的调用, 其实就是内核的代码段上面.

完成这个过程后, 内存就成了这个样子

a3948eded5e66d3bfc2ab2caf92aa46b.png

0-4m 映射过去, 并且stack 开始进行调用 main

  1. main 中 先进行了一些重要的操作, 比如内存分配器的初始化(kinit), 更精细的内核分页(kvmalloc), 不过我们不太关心这个, 直接看 inituser:
  • 首先那一个新的进程结构, 和用连接器定义好的符号, 找到 initcode.S 的 代码地址和 大小.
    struct proc *p; extern char _binary_initcode_start[], _binary_initcode_size[]; // 连接器定义的符号 p = allocproc();
  • 然后初始化内存, 把虚拟空间 0 地址和 initcode.S的代码地址对应
    if((p->pgdir = setupkvm()) == 0) // 申请一个内核页目录 .... inituvm(p->pgdir, _binary_initcode_start, (int)_binary_initcode_size);
    稍微跟进去看一下
    void inituvm(pde_t *pgdir, char *init, uint sz) { char *mem; if(sz >= PGSIZE) panic("inituvm: more than a page"); mem = kalloc(); memset(mem, 0, PGSIZE); mappages(pgdir, 0, PGSIZE, V2P(mem), PTE_W|PTE_U); // 核心代码就是这一句 memmove(mem, init, sz); }
    核心代码就是
    mappages(pgdir, 0, PGSIZE, V2P(mem), PTE_W|PTE_U);
    对应 虚拟空间 0地址的映射的页
  • 接下来初始化内核栈保存点, trapframe, 系统调用的信息和执行过程会保存在这里.
    memset(p->tf, 0, sizeof(*p->tf)); p->tf->cs = (SEG_UCODE << 3) | DPL_USER; p->tf->ds = (SEG_UDATA << 3) | DPL_USER; p->tf->es = p->tf->ds; p->tf->ss = p->tf->ds; p->tf->eflags = FL_IF; p->tf->esp = PGSIZE; p->tf->eip = 0; // beginning of initcode.S
    先不管这里.
  • 最后让他运行
    p->state = RUNNABLE;

这时候 init 进程就准备好运行了.

  1. 接下来 main 中, 开启调度, 开始了操作系统的主循环.
    调用过程: main-> mpmain -> scheduler
    开始调度运行所有进程. 不过我们先不管他怎么运行的, 我们先看看 init 里干了啥.

init 干了啥

进来以后先执行 initcode.S,

start:
  pushl $argv
  pushl $init
  pushl $0  // where caller pc would be
  movl $SYS_exec, %eax
  int $T_SYSCALL

看的出是进行了一个系统调用, 执行了

exec("init", 0)

我们先不管系统调用之类的中断是怎么触发的, 可以先理解成一个事件模型.

看看 sys_exec 干了啥,

int 

他处理完参数就调用 exec 了,

我们看看 exec:

  1. 首先 读取 elf 文件并分配一个页表:
if
  1. 给每个段用 allocuvm 分配一个映射的内存, 并且不断读取 elf
for
  1. 分配俩个页, 第一个clearpteu标记成不可用
sz 
  1. 参数通过 ustack 放到 用户栈中
for

0 是main 函数的返回地址, 之后是参数, 这是因为栈帧的结构.

fb9246ab4a8cd0d6893778e59695ed7e.png
  1. 接下来只要把刚刚从 elf 读到的信息, 构造的寄存器信息, main 入口的信息, 写到当前进程里就行
// Commit to the user image.  

exec 就执行完了. 那他执行的 init 是干啥的呢? 我们看看 init.c

int

这个代码太简单了, 搞出一个子进程, 执行 sh, 程序后退出, 另一个作为一个僵尸进程一直后台死循环跑的. 这时候, 操作系统的初始化就完成了.

进程怎么切换

我们看看进程怎么切换, 找到: proc.c 的 scheduler,

  1. 找到一个状态为可运行的进程
for
  1. 设置当前cpu 运行的进程, 改变状态 和 cr3 寄存器指向的页目录
c
  1. 进入 switch, switch 的思路是把会改变的寄存器, 放到自己的栈帧上, 然后直接切换栈帧, 从之前保存好的栈帧里恢复寄存器的值, switch 代码可以分几个部分
  • 解析出 switch 的参数 根据调用约定
movl 4(%esp), %eax // old
  movl 8(%esp), %edx // new
  • 保存当前寄存器的值到当前栈帧上
# Save old callee-saved registers
  • 换栈, 换成 new
movl %esp, (%eax)
movl %edx, %esp
  • 因为这时候 esp 寄存器的值变了, pop 的时候恢复的就是新的 esp 寄存器的位置, 根据保存的顺序,也就是 context 定义的顺序,
struct 

我们连着 pop 4 次 , 把对应寄存器的值恢复, 此时, esp 就指向了 eip, 调用 ret, 就可以跳到 eip 执行了.

  • 在别处 调用 sched方法, 可以恢复到内核的栈帧
void sched(void) {
  ....
  swtch(&p->context, mycpu()->scheduler);
}
  • 接着执行 scheduler里的 switchkvm 让cr3 寄存器指向内核的页目录

我们总结一下, 进程切换的关键是:

  1. 切换 cr3 寄存器所指向的页目录
  2. 把自己会变的信息放在一个栈帧上, 并把这个栈帧和 c 语言里定义的 context信息同步起来.

怎么执行到 main

我们发现, 初始化的进程的 context 的 eip 指向了 forket, main 是被 tf 所指向的, 那么怎么运行 tf 指向的 entry 呢?

答案在 proc.c 的 allocproc里:

sp 

我们画一个 sp 的图

182dad35270a3621bdad784f7be49b6b.png

很直观, forket 执行完了以后, 返回到的地方正好是trapret,

trapret的思路和 switch 像极了,

我们对应着 trapframe 的结构来看

// hardware and by trapasm.S, and passed to trap().

我们看 trapret

  • 弹出通用寄存器
    popal
  • 弹出 gs fs 用 l 来弹 一弹弹俩
popl %gs
  popl %fs
  popl %es
  popl %ds
  • 略过其他数据
addl $0x8, %esp  # trapno and errcode
  • esp 指向了 eip 了这时候 ret 一发 直接进入用户定义的程序中
iret

好了 , 至此我们已经搞懂了进程是怎么诞生和切换的了.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值