OSTEP:6 Mechanism:Limited Direct Execution

Mechanism:Limited Direct Execution

受限制的操作

进程跑在我们的操作系统上,总不能让他为所欲为吧,cpu提供给了我们保护模式,从而有了用户模式和内核模式,我们的操作系统运行在内核模式,可以访问机器全部资源,用户进程运行在用户模式,受到限制,但是用户程序有时必须做一些特权操作,操作系统就必须提供给他们一些接口:系统调用。

异常是异常控制流的一种形式,它一部分由硬件实现,一部分由操作系统实现。陷阱是有意的异常,陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口系统调用,在 x86-64系统上,系统调用是通过一条称为 syscall的陷阱指令来提供的

加电开机后,处于内核模式,操作系统内核会告诉硬件我们的陷阱处理程序(陷阱表)在哪,执行陷阱处理程序前,处理器会把进程的一些寄存器值等压到该进程内核栈(在我们32位程序进程说的高1G内核空间里,在x86的32位机器上内核栈大小可以为4KB或8KB),执行完陷阱返回时再弹出

在进程之间切换

操作系统如何重新获得 CPU 的控制权

协作方式:等待系统调用

程序主动进行系统调用或其他类型的异常来重新获取控制权

非协作方式:操作系统进行控制

协作方式对于一些只循环的程序无能为力,那就用硬件中断

利用时钟设备,每隔几毫秒产生一次中断,中断处理程序前,处理器也会把一些当前进程信息压入其内核栈

保存和恢复上下文

接下来就是调度程序根据调度策略决定怎么跑路Zzz

决定切换的话,会进行上下文切换(context switch)

假如由A切换到B

A的寄存器首先被硬件保存到该进程的内核栈,进入内核态,操作系统切换时,调用switch再次把寄存器保存到操作系统维持的A的进程结构体的内存里,然后把要调度B进程结构体的内存里的值恢复到寄存器,这个时候栈指针已经变为了B的内核栈,从内核栈恢复寄存器就是恢复B的寄存器了,就可以把之前硬件保存的B进程的东西恢复,回到用户模式执行B的指令

xv6上下文切换源码

书里的代码有错误,去找了波源码 https://github.com/mit-pdos/xv6-public

#   void swtch(struct context *old, struct context *new);
#
# Save current register context in old
# and then load register context from new.

.globl swtch
swtch:
# Save old registers
movl 4(%esp), %eax

popl 0(%eax) # %eip
movl %esp, 4(%eax)
movl %ebx, 8(%eax)
movl %ecx, 12(%eax)
movl %edx, 16(%eax)
movl %esi, 20(%eax)
movl %edi, 24(%eax)
movl %ebp, 28(%eax)

# Load new registers
movl 4(%esp), %eax # not 8(%esp) - popped return address above

movl 28(%eax), %ebp
movl 24(%eax), %edi
movl 20(%eax), %esi
movl 16(%eax), %edx
movl 12(%eax), %ecx
movl 8(%eax), %ebx
movl 4(%eax), %esp
pushl 0(%eax) # %eip

ret
// Saved registers for kernel context switches.
// Don't need to save all the %fs etc. segment registers,
// because they are constant across kernel contexts.
// Save all the regular registers so we don't need to care
// which are caller save, but not the return register %eax.
// (Not saving %eax just simplifies the switching code.)
// The layout of context must match code in swtch.S.
struct context {
int eip;
int esp;
int ebx;
int ecx;
int edx;
int esi;
int edi;
int ebp;
};

根据函数声明和32位压栈规则,栈刚开始是这样的

oldA的内核栈
+--------------+
| |
+--------------+ <-- %esp
| ret(A) |
+--------------+ <-- %esp+4
| old(*) |
+--------------+ <-- %esp+8
| new(*) |
+--------------+

movl 4(%esp), %eax把old(*)给eax

popl 0(%eax)把eip寄存器给到old->eip

oldA的内核栈
+--------------+
| |
+--------------+ <-- %esp
| old(*) |
+--------------+
| new(*) |
+--------------+

后面一直到movl %ebp, 28(%eax)把old的context结构体里内容填满

movl 4(%esp), %eax把新的context地址给到eax

一直到movl 4(%eax), %esp把新context里的内容恢复到寄存器

可以看的栈帧寄存器ebp,esp都变了,切换到了B内核栈

pushl 0(%eax) # %eip把newb->eip压栈

NewB的内核栈
+--------------+
| |
+--------------+ <-- %esp
| ret(B) |
+--------------+
| |
+--------------+
| |
+--------------+
新版

其实上面是比较早版本的swtch,最新的已经变了

//PAGEBREAK: 17
// Saved registers for kernel context switches.
// Don't need to save all the segment registers (%cs, etc),
// because they are constant across kernel contexts.
// Don't need to save %eax, %ecx, %edx, because the
// x86 convention is that the caller has saved them.
// Contexts are stored at the bottom of the stack they
// describe; the stack pointer is the address of the context.
// The layout of the context matches the layout of the stack in swtch.S
// at the "Switch stacks" comment. Switch doesn't save eip explicitly,
// but it is on the stack and allocproc() manipulates it.
struct context {
uint edi;
uint esi;
uint ebx;
uint ebp;
uint eip;
};
# Context switch
#
# void swtch(struct context **old, struct context *new);
#
# Save the current registers on the stack, creating
# a struct context, and save its address in *old.
# Switch stacks to new and pop previously-saved registers.

.globl swtch
swtch:
movl 4(%esp), %eax
movl 8(%esp), %edx

# Save old callee-saved registers
pushl %ebp
pushl %ebx
pushl %esi
pushl %edi

# Switch stacks
movl %esp, (%eax)
movl %edx, %esp

# Load new callee-saved registers
popl %edi
popl %esi
popl %ebx
popl %ebp
ret
oldA的内核栈
+--------------+
| |
+--------------+ <-- %esp
| ret(A) |
+--------------+ <-- %esp+4
| old(**) |
+--------------+ <-- %esp+8
| new(*) |
+--------------+

movl 4(%esp), %eax movl 8(%esp), %edx

old(**)给到eax

new(*)给到edx

然后直接把context push到栈上

oldA的内核栈
+--------------+
| |
+--------------+<-- %esp
| %edi |
+--------------+
| %esi |
+--------------+
| %ebx |
+--------------+
| %ebp |
+--------------+
| ret(A) |
+--------------+ <--%eax
| old(**) |
+--------------+ <--%edx
| new(*) |
+--------------+

movl %esp, (%eax)

直接把当前esp指向的地址写到oldA的context的地址,就是把栈上的这些当作context结构体,没去再去单独申请内存,为了提高效率??

movl %edx, %esp

把栈切换到NewB的内核栈,由于之前B也被切换过所以他的栈顶也是context结构体

NewB的内核栈
+--------------+
| |
+--------------+<-- %esp
| %edi |
+--------------+
| %esi |
+--------------+
| %ebx |
+--------------+
| %ebp |
+--------------+
| ret(B) |
+--------------+

pop后ret就可以切换到B指令执行

并发问题!!!

系统调用时发生时钟中断?

中断处理时发生中断?

简单的在中断时禁止中断可能会导致中断丢失?