xv6学习笔记(3):中断处理和系统调用
xv6学习笔记(3):中断处理和系统调用
1. tvinit函数
这个函数位于main函数内
表明了就是设置idt表
void
tvinit(void)
{
int i;
for(i = 0; i < 256; i++)
SETGATE(idt[i], 0, SEG_KCODE<<3, vectors[i], 0);
SETGATE(idt[T_SYSCALL], 1, SEG_KCODE<<3, vectors[T_SYSCALL], DPL_USER);
initlock(&tickslock, "time");
}
1. SETGATE函数
这里的setgate是一个宏定义是用来设置idt表的
#define SETGATE(gate, istrap, sel, off, d) \
{ \
(gate).off_15_0 = (uint)(off) & 0xffff; \
(gate).cs = (sel); \
(gate).args = 0; \
(gate).rsv1 = 0; \
(gate).type = (istrap) ? STS_TG32 : STS_IG32; \
(gate).s = 0; \
(gate).dpl = (d); \
(gate).p = 1; \
(gate).off_31_16 = (uint)(off) >> 16; \
}
下面是函数参数的说明
Sel : 表示对于中断处理程序代码所在段的段选择子
off:表示中断处理程序代码的段内偏移
(gate).gd_off_15_0 : 存储偏移值的低16位
(gate).gd_off_31_16 : 存储偏移值的高16位
(gate).gd_sel : 存储段选择子
(gate).gd_dpl : dpl 表示该段对应的
熟悉了这些之后参考intel的开发手册找一下istrap的值,这里注意系统调用的dpl = 3不然我们无法从用户模式进去
这里只要按照上述宏定义的格式书写就好,而且这里的中断处理函数我们都不用关心怎么实现,只用给他一个占位符。
可以发现这里就是这是IDT表格了
2. idtinit函数
void
idtinit(void)
{
lidt(idt, sizeof(idt));
}
这里就是调用lidt函数
static inline void
lidt(struct gatedesc *p, int size)
{
volatile ushort pd[3];
pd[0] = size-1;
pd[1] = (uint)p;
pd[2] = (uint)p >> 16;
asm volatile("lidt (%0)" : : "r" (pd));
}
这个函数最后会调用lidt这个汇编代码

而lidt这个汇编代码做的事情就是把pd加载到GDTR。
也就是有对应的IDT表的基地址 和 IDT表的大小
CS寄存器存储的是内核代码段的段编号SEG_KCODE,offset部分存储的是vector[i]的地址。在XV6系统中,所有的vector[i]地址均指向trapasm.S中的alltraps函数。
2. XV6中断处理过程
1. 中断例子
当XV6的遇到中断志龙,首先CPU硬件会发现这个错误,触发中断处理机制。在中断处理机制中,硬件会执行如下步骤:下面的过程我们成为保护现场xv6官方文档
- 从IDT 中获得第 n 个描述符,n 就是 int 的参数。
- 检查CS的域 CPL <= DPL,DPL 是描述符中记录的特权级。
- 如果目标段选择符的 PL < CPL,就在 CPU 内部的寄存器中保存ESP和SS的值。
- 从一个任务段描述符中加载SS和ESP。
- 将SS压栈。
- 将ESP压栈。
- 将EFLAGS压栈。
- 将CS压栈。
- 将EIP压栈。
- 清除EFLAGS的一些位。
- 设置CS和EIP为描述符中的值。
此时,由于CS已经被设置为描述符中的值(SEG_KCODE),所以此时已经进入了内核态,并且EIP指向了trapasm.S中alltraps函数的开头。在alltrap函数中,系统将用户寄存器压栈,构建Trap Frame,并且设置数据寄存器段为内核数据段,然后跳转到trap.c中的trap函数。

alltraps继续压入寄存器保存现场,得到trapframe结构体,trapframe结构体如图所示,其中oesp没有用处,这是pushal指令统一压栈的。
.globl alltraps
alltraps:
# Build trap frame.
pushl %ds
pushl %es
pushl %fs
pushl %gs
pushal
这里的pushal就是压入所有通用寄存器
在这之后重新设置段寄存器,进入内核态,压入当前栈esp,然后调用C函数trap处理中断,在trap返回时,弹出esp
# Set up data segments.
movw $(SEG_KDATA<<3), %ax
movw %ax, %ds
movw %ax, %es
# Call trap(tf), where tf=%esp
pushl %esp
call trap
trap函数是通过tf->trapno来进行逻辑分支处理的。下面介绍一下系统调用的处理。
系统调用
当tr->trapno是 T_SYSCALL的时候,内核调用syscall函数。
if(tf->trapno == T_SYSCALL){
if(myproc()->killed)
exit();
myproc()->tf = tf;
syscall();
if(myproc()->killed)
exit();
return;
}
这是syscalls的对应数组嗷
extern int sys_chdir(void);
extern int sys_close(void);
extern int sys_dup(void);
extern int sys_exec(void);
extern int sys_exit(void);
extern int sys_fork(void);
extern int sys_fstat(void);
extern int sys_getpid(void);
extern int sys_kill(void);
extern int sys_link(void);
extern int sys_mkdir(void);
extern int sys_mknod(void);
extern int sys_open(void);
extern int sys_pipe(void);
extern int sys_read(void);
extern int sys_sbrk(void);
extern int sys_sleep(void);
extern int sys_unlink(void);
extern int sys_wait(void);
extern int sys_write(void);
extern int sys_uptime(void);
static int (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
[SYS_wait] sys_wait,
[SYS_pipe] sys_pipe,
[SYS_read] sys_read,
[SYS_kill] sys_kill,
[SYS_exec] sys_exec,
[SYS_fstat] sys_fstat,
[SYS_chdir] sys_chdir,
[SYS_dup] sys_dup,
[SYS_getpid] sys_getpid,
[SYS_sbrk] sys_sbrk,
[SYS_sleep] sys_sleep,
[SYS_uptime] sys_uptime,
[SYS_open] sys_open,
[SYS_write] sys_write,
[SYS_mknod] sys_mknod,
[SYS_unlink] sys_unlink,
[SYS_link] sys_link,
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
};
这里的systemcall函数利用eax寄存器获得系统调用号。最后的返回值也利用eax寄存器返回
如果系统调用号合理的话,返回值就是对应系统调用函数产生的返回值
void
syscall(void)
{
int num;
struct proc *curproc = myproc();
num = curproc->tf->eax;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
curproc->tf->eax = syscalls[num]();
} else {
cprintf("%d %s: unknown sys call %d\n",
curproc->pid, curproc->name, num);
curproc->tf->eax = -1;
}
}
下面是对于除0的处理。
if(myproc() == 0 || (tf->cs&3) == 0){
// In kernel, it must be our mistake.
cprintf("unexpected trap %d from cpu %d eip %x (cr2=0x%x)\n",
tf->trapno, cpuid(), tf->eip, rcr2());
panic("trap");
}
// In user space, assume process misbehaved.
cprintf("pid %d %s: trap %d err %d on cpu %d "
"eip 0x%x addr 0x%x--kill proc\n",
myproc()->pid, myproc()->name, tf->trapno,
tf->err, cpuid(), tf->eip, rcr2());
myproc()->killed = 1;
根据触发中断的是内核态还是用户进程,执行不同的处理。如果是用户进程出错了,那么系统会杀死这个用户进程;如果是内核进程出错了,那么在输出一段错误信息后,整个系统进入死循环。
如果是一个可以修复的错误,比如页错误,那么系统会在处理完后返回trap()函数进入trapret()函数,在这个函数中恢复进程的执行上下文,让整个系统返回到触发中断的位置和状态。
2. 系统调用全过程
首先在文件user.h中存储了提供的系统调用,这里以exec这个系统调用为例,考察在用户态执行的整个流程。
// system calls
int fork(void);
int exit(void) __attribute__((noreturn));
int wait(void);
int pipe(int*);
int write(int, const void*, int);
int read(int, void*, int);
int close(int);
int kill(int);
// .......
1. 考虑系统调用号如何传递
这里需要去看一下usys.S和反汇编一下usys.o
1. 首先看去看usys.S
可以发现这里定义了一个宏定义就是根据传递过来的系统调用名称把系统调用号传递到%eax寄存器中
随后触发int中断陷入内核态
#include "syscall.h"
#include "traps.h"
#define SYSCALL(name) \
.globl name; \
name: \
movl $SYS_ ## name, %eax; \
int $T_SYSCALL; \
ret
SYSCALL(fork)
SYSCALL(exit)
SYSCALL(wait)
SYSCALL(pipe)
SYSCALL(read)
SYSCALL(write)
SYSCALL(close)
SYSCALL(kill)
SYSCALL(exec)
SYSCALL(open)
SYSCALL(mknod)
SYSCALL(unlink)
SYSCALL(fstat)
SYSCALL(link)
SYSCALL(mkdir)
SYSCALL(chdir)
SYSCALL(dup)
SYSCALL(getpid)
SYSCALL(sbrk)
SYSCALL(sleep)
SYSCALL(uptime)
2. 在看usys.o
我们这里反汇编一下usys.o

以fork为例子它把系统调用号1传递给了eax寄存器
3.执行系统调用函数
随后在syscall.c中到syscall函数
在这里利用系统调用号获取对应的系统调用函数
void
syscall(void)
{
int num;
struct proc *curproc = myproc();
num = curproc->tf->eax;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
curproc->tf->eax = syscalls[num]();
} else {
cprintf("%d %s: unknown sys call %d\n",
curproc->pid, curproc->name, num);
curproc->tf->eax = -1;
}
}
以exec为例子就是执行这个函数sys_exec进行系统调用处理
2. 系统调用函数执行
经过上面的一顿分析,最后exec系统调用会进入这里进行执行
int
sys_exec(void)
{
char *path, *argv[MAXARG];
int i;
uint uargv, uarg;
if(argstr(0, &path) < 0 || argint(1, (int*)&uargv) < 0){
return -1;
}
memset(argv, 0, sizeof(argv));
for(i=0;; i++){
if(i >= NELEM(argv))
return -1;
if(fetchint(uargv+4*i, (int*)&uarg) < 0)
return -1;
if(uarg == 0){
argv[i] = 0;
break;
}
if(fetchstr(uarg, &argv[i]) < 0)
return -1;
}
return exec(path, argv);
}
对于exec而言,exec需要一个可执行文件的路径和需要执行的参数。而获取参数和路径的函数下面来介绍一下
1. argstr函数
可以发现这个函数调用了argint函数以及fetchstr()函数

这里的(myproc()->tf->esp) + 4 + 4*n就是获取上述栈帧里存储的第几个参数
Eg: n = 0 时候就说获取edi寄存器的参数我们以exec为例子第一个参数使用edi寄存器传递的因此就是获取可执行文件的路径的地址
而真正的字符串还要利用fetchstr函数获取
int
argstr(int n, char **pp)
{
int addr;
if(argint(n, &addr) < 0)
return -1;
return fetchstr(addr, pp);
}
2. argint函数
int
argint(int n, int *ip)
{
return fetchint((myproc()->tf->esp) + 4 + 4*n, ip);
}
3. fetchint函数
// Fetch the int at addr from the current process.
int
fetchint(uint addr, int *ip)
{
struct proc *curproc = myproc();
if(addr >= curproc->sz || addr+4 > curproc->sz)
return -1;
*ip = *(int*)(addr);
return 0;
}
4. fetchstr函数
int
fetchstr(uint addr, char **pp)
{
char *s, *ep;
struct proc *curproc = myproc();
if(addr >= curproc->sz)
return -1;
*pp = (char*)addr;
ep = (char*)curproc->sz;
for(s = *pp; s < ep; s++){
if(*s == 0)
return s - *pp;
}
return -1;
}
3. 真正系统调用的执行
而构建好参数之后最后sys_exec实际上会调用exec(path, argv);函数
而exec函数还是比较复杂的这里简单分析一下即可。
- 根据提供的path获取文件信息读入到
inode中 - 然后把inode信息解析到elf头中
int
exec(char *path, char **argv)
{
char *s, *last;
int i, off;
uint argc, sz, sp, ustack[3+MAXARG+1];
struct elfhdr elf;
struct inode *ip;
struct proghdr ph;
pde_t *pgdir, *oldpgdir;
struct proc *curproc = myproc();
begin_op();
if((ip = namei(path)) == 0){
end_op();
cprintf("exec: fail\n");
return -1;
}
ilock(ip);
pgdir = 0;
// Check ELF header
if(readi(ip, (char*)&elf, 0, sizeof(elf)) != sizeof(elf))
goto bad;
if(elf.magic != ELF_MAGIC)
goto bad;
- 这里会给每一个进程分配一个内核页表然后在返回用户空间之前把它copy到用户空间
- 然后按照elf段把它分配memory然后加载到内存,分配和加载分别通过
allocuvm和loaduvm函数实现
if((pgdir = setupkvm()) == 0)
goto bad;
// Load program into memory.
sz = 0;
for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
if(readi(ip, (char*)&ph, off, sizeof(ph)) != sizeof(ph))
goto bad;
if(ph.type != ELF_PROG_LOAD)
continue;
if(ph.memsz < ph.filesz)
goto bad;
if(ph.vaddr + ph.memsz < ph.vaddr)
goto bad;
if((sz = allocuvm(pgdir, sz, ph.vaddr + ph.memsz)) == 0)
goto bad;
if(ph.vaddr % PGSIZE != 0)
goto bad;
if(loaduvm(pgdir, (char*)ph.vaddr, ip, ph.off, ph.filesz) < 0)
goto bad;
}
算了这里先看一下分配和加载分别是如何做的‘
allocuvm函数
这个函数就是逐页为每一段分配页表并做对应的映射。
int
allocuvm(pde_t *pgdir, uint oldsz, uint newsz)
{
char *mem;
uint a;
if(newsz >= KERNBASE)
return 0;
if(newsz < oldsz)
return oldsz;
a = PGROUNDUP(oldsz);
for(; a < newsz; a += PGSIZE){
mem = kalloc();
if(mem == 0){
cprintf("allocuvm out of memory\n");
deallocuvm(pgdir, newsz, oldsz);
return 0;
}
memset(mem, 0, PGSIZE);
if(mappages(pgdir, (char*)a, PGSIZE, V2P(mem), PTE_W|PTE_U) < 0){
cprintf("allocuvm out of memory (2)\n");
deallocuvm(pgdir, newsz, oldsz);
kfree(mem);
return 0;
}
}
return newsz;
}
loaduvm函数
这里加载到了内核的高地址区域(说实话现在还不懂为啥要这样做。后面慢慢来吧
// Load a program segment into pgdir. addr must be page-aligned
// and the pages from addr to addr+sz must already be mapped.
int
loaduvm(pde_t *pgdir, char *addr, struct inode *ip, uint offset, uint sz)
{
uint i, pa, n;
pte_t *pte;
if((uint) addr % PGSIZE != 0)
panic("loaduvm: addr must be page aligned");
for(i = 0; i < sz; i += PGSIZE){
if((pte = walkpgdir(pgdir, addr+i, 0)) == 0)
panic("loaduvm: address should exist");
pa = PTE_ADDR(*pte);
if(sz - i < PGSIZE)
n = sz - i;
else
n = PGSIZE;
if(readi(ip, P2V(pa), offset+i, n) != n)
return -1;
}
return 0;
}
- 这里用来构建参数
- 然后为返回用户空间做准备
- 这里把
curproc->tf->eip = elf.entry;这样就设置好了所需要执行函数的入口地址
iunlockput(ip);
end_op();
ip = 0;
// Allocate two pages at the next page boundary.
// Make the first inaccessible. Use the second as the user stack.
sz = PGROUNDUP(sz);
if((sz = allocuvm(pgdir, sz, sz + 2*PGSIZE)) == 0)
goto bad;
clearpteu(pgdir, (char*)(sz - 2*PGSIZE));
sp = sz;
// Push argument strings, prepare rest of stack in ustack.
for(argc = 0; argv[argc]; argc++) {
if(argc >= MAXARG)
goto bad;
sp = (sp - (strlen(argv[argc]) + 1)) & ~3;
if(copyout(pgdir, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
goto bad;
ustack[3+argc] = sp;
}
ustack[3+argc] = 0;
ustack[0] = 0xffffffff; // fake return PC
ustack[1] = argc;
ustack[2] = sp - (argc+1)*4; // argv pointer
sp -= (3+argc+1) * 4;
if(copyout(pgdir, sp, ustack, (3+argc+1)*4) < 0)
goto bad;
// Save program name for debugging.
for(last=s=path; *s; s++)
if(*s == '/')
last = s+1;
safestrcpy(curproc->name, last, sizeof(curproc->name));
// Commit to the user image.
oldpgdir = curproc->pgdir;
curproc->pgdir = pgdir;
curproc->sz = sz;
curproc->tf->eip = elf.entry; // main
curproc->tf->esp = sp;
switchuvm(curproc);
freevm(oldpgdir);
return 0;
bad:
if(pgdir)
freevm(pgdir);
if(ip){
iunlockput(ip);
end_op();
}
return -1;
}
参考
xv6学习笔记(3):中断处理和系统调用的更多相关文章
- xv6学习笔记(4) : 进程调度
xv6学习笔记(4) : 进程 xv6所有程序都是单进程.单线程程序.要明白这个概念才好继续往下看 1. XV6中进程相关的数据结构 在XV6中,与进程有关的数据结构如下 // Per-process ...
- XV6学习笔记(1) : 启动与加载
XV6学习笔记(1) 1. 启动与加载 首先我们先来分析pc的启动.其实这个都是老生常谈了,但是还是很重要的(也不知道面试官考不考这玩意), 1. 启动的第一件事-bios 首先启动的第一件事就是运行 ...
- XV6学习笔记(2) :内存管理
XV6学习笔记(2) :内存管理 在学习笔记1中,完成了对于pc启动和加载的过程.目前已经可以开始在c语言代码中运行了,而当前已经开启了分页模式,不过是两个4mb的大的内存页,而没有开启小的内存页.接 ...
- xv6学习笔记(5) : 锁与管道与多cpu
xv6学习笔记(5) : 锁与管道与多cpu 1. xv6锁结构 1. xv6操作系统要求在内核临界区操作时中断必须关闭. 如果此时中断开启,那么可能会出现以下死锁情况: 进程A在内核态运行并拿下了p ...
- 20135202闫佳歆--week4 系统调用(上)--学习笔记
此为个人笔记存档 week 4 系统调用(上) 一.用户态.内核态和中断处理过程 用户通过库函数与系统调用联系起来. 1.内核态 在高执行级别下,代码可以执行特权指令,访问任意的物理地址. 2.用户态 ...
- 20135202闫佳歆--week5 系统调用(下)--学习笔记
此为个人笔记存档 week 5 系统调用(下) 一.给MenuOS增加time和time-asm命令 这里老师示范的时候是已经做好的了: rm menu -rf 强制删除 git clone http ...
- 《Linux内核分析》第八周学习笔记
<Linux内核分析>第八周学习笔记 进程的切换和系统的一般执行过程 郭垚 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163 ...
- 《Linux内核分析》第五周学习笔记
<Linux内核分析>第五周学习笔记 扒开系统调用的三层皮(下) 郭垚 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.c ...
- 《Linux内核分析》第四周学习笔记
<Linux内核分析>第四周学习笔记 扒开系统调用的三层皮(上) 郭垚 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.c ...
随机推荐
- PHP解决并发问题的几种实现(转)
对于商品抢购等并发场景下,可能会出现超卖的现象,这时就需要解决并发所带来的这些问题了 在PHP语言中并没有原生的提供并发的解决方案,因此就需要借助其他方式来实现并发控制. 方案一:使用文件锁排它锁 ...
- ctf常见编码形式(罗师傅)
https://zhuanlan.zhihu.com/p/30323085 这是原链接 ASCII编码 •ASCII编码大致可以分作三部分组成: •第一部分是:ASCII非打印控制字符(参详ASCII ...
- ESP32音频输入-MAX4466,MAX9814,SPH0645LM4H,INMP441(翻译)
有几种方法可以将模拟音频数据输入到ESP32中. 直接从内置的模数转换器(ADC)读取 这对于一次性读取很有用,但不适用于高采样率. 使用I2S通过DMA读取内置ADC 适用于模拟麦克风,例如MAX4 ...
- java基础---java8后新特性
1. java9 新特性 模块化的使用 减少内存的开销. 可简化各种类库和大型应用的开发和维护. 安全性,可维护性,提高性能. 在 module-info.java 文件中,我们可以用新的关键词mod ...
- 物理机连接虚拟机中的数据库及Windows添加防火墙允许端口详细操作步骤
公司项目中因为会使用到SQL server数据库,但是自己电脑无论安装2008R2或者2014版本都不成功,我想可能是和之前安装的一些Windows的软件存在冲突. 于是便单独创建了一台虚拟机,在虚拟 ...
- c++中的静态成员
引言 有时候需要类的一些成员与类本身相关联,而不是与类的每个对象相关联.比如类的所有对象都要共享的变量,这个时候我们就要用到类的静态成员. 声明类的静态成员 声明静态成员的方法是使用static关键字 ...
- Appium -- adb monkey操作(一)
1.Monkey简介在Android的官方自动化测试领域有一只非常著名的"猴子"叫Monkey,这只"猴子"一旦启动,就会让被测的Android应用程序像猴子一 ...
- Requests方法 -- 参数关联
一.删除草稿箱1.参数这篇https://www.cnblogs.com/Teachertao/p/11144726.html 2.删除刚才保存的草稿 3.用 fiddler 抓包,抓到删除帖子的请求 ...
- 在使用TCP协议进行消息发送时,对消息分帧
成帧与解析 阅读 <java TCP/IP Socket 编程>第三章笔记 成帧技术(frame)是解决如何在接收端定位消息的首尾位置的问题.在进行数据收发时,必须指定消息接收者如何确定何 ...
- vue3如何编写挂载DOM的插件
vue3 跟 vue2 相比,多了一个 app 的概念,vue3 项目的创建也变成了 // main.jsimport { createApp } from 'vue' import App from ...