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官方文档

  1. 从IDT 中获得第 n 个描述符,n 就是 int 的参数。
  2. 检查CS的域 CPL <= DPL,DPL 是描述符中记录的特权级。
  3. 如果目标段选择符的 PL < CPL,就在 CPU 内部的寄存器中保存ESP和SS的值。
  4. 从一个任务段描述符中加载SS和ESP。
  5. 将SS压栈。
  6. 将ESP压栈。
  7. 将EFLAGS压栈。
  8. 将CS压栈。
  9. 将EIP压栈。
  10. 清除EFLAGS的一些位。
  11. 设置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函数还是比较复杂的这里简单分析一下即可。

  1. 根据提供的path获取文件信息读入到inode
  2. 然后把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;
  1. 这里会给每一个进程分配一个内核页表然后在返回用户空间之前把它copy到用户空间
  2. 然后按照elf段把它分配memory然后加载到内存,分配和加载分别通过allocuvmloaduvm函数实现
  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;
}
  1. 这里用来构建参数
  2. 然后为返回用户空间做准备
  3. 这里把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;
}

参考

博客1

博客2

xv6中文文档

xv6学习笔记(3):中断处理和系统调用的更多相关文章

  1. xv6学习笔记(4) : 进程调度

    xv6学习笔记(4) : 进程 xv6所有程序都是单进程.单线程程序.要明白这个概念才好继续往下看 1. XV6中进程相关的数据结构 在XV6中,与进程有关的数据结构如下 // Per-process ...

  2. XV6学习笔记(1) : 启动与加载

    XV6学习笔记(1) 1. 启动与加载 首先我们先来分析pc的启动.其实这个都是老生常谈了,但是还是很重要的(也不知道面试官考不考这玩意), 1. 启动的第一件事-bios 首先启动的第一件事就是运行 ...

  3. XV6学习笔记(2) :内存管理

    XV6学习笔记(2) :内存管理 在学习笔记1中,完成了对于pc启动和加载的过程.目前已经可以开始在c语言代码中运行了,而当前已经开启了分页模式,不过是两个4mb的大的内存页,而没有开启小的内存页.接 ...

  4. xv6学习笔记(5) : 锁与管道与多cpu

    xv6学习笔记(5) : 锁与管道与多cpu 1. xv6锁结构 1. xv6操作系统要求在内核临界区操作时中断必须关闭. 如果此时中断开启,那么可能会出现以下死锁情况: 进程A在内核态运行并拿下了p ...

  5. 20135202闫佳歆--week4 系统调用(上)--学习笔记

    此为个人笔记存档 week 4 系统调用(上) 一.用户态.内核态和中断处理过程 用户通过库函数与系统调用联系起来. 1.内核态 在高执行级别下,代码可以执行特权指令,访问任意的物理地址. 2.用户态 ...

  6. 20135202闫佳歆--week5 系统调用(下)--学习笔记

    此为个人笔记存档 week 5 系统调用(下) 一.给MenuOS增加time和time-asm命令 这里老师示范的时候是已经做好的了: rm menu -rf 强制删除 git clone http ...

  7. 《Linux内核分析》第八周学习笔记

    <Linux内核分析>第八周学习笔记 进程的切换和系统的一般执行过程 郭垚 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163 ...

  8. 《Linux内核分析》第五周学习笔记

    <Linux内核分析>第五周学习笔记 扒开系统调用的三层皮(下) 郭垚 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.c ...

  9. 《Linux内核分析》第四周学习笔记

    <Linux内核分析>第四周学习笔记 扒开系统调用的三层皮(上) 郭垚 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.c ...

随机推荐

  1. Redis和Memcached到底有什么区别?

    前言 我们都知道,Redis和Memcached都是内存数据库,它们的访问速度非常之快.但我们在开发过程中,这两个内存数据库,我们到底要如何选择呢?它们的优劣都有哪些?为什么现在看Redis要比Mem ...

  2. MySQL索引类型总结和使用技巧以及注意事项 (转)

      在数据库表中,对字段建立索引可以大大提高查询速度.假如我们创建了一个 mytable表:  代码如下: CREATE TABLE mytable(   ID INT NOT NULL,    us ...

  3. SQL关联子查询

    SQL关联子查询执行顺序: 1.先取到主查询中的相关数据,一次取一行主查询的数据 2.然后传入子查询,进行子查询 3.最后做主查询where筛选,注意子查询的where条件同样需要加在主查询后 参考: ...

  4. ELK处理Spring Boot 日志,妙!

    在排查线上异常的过程中,查询日志总是必不可缺的一部分.现今大多采用的微服务架构,日志被分散在不同的机器上,使得日志的查询变得异常困难. 工欲善其事,必先利其器.如果此时有一个统一的实时日志分析平台,那 ...

  5. varnish配置语言(2)

    目录 1. Backend servers 2. 多个后端 3. Varnish 中的后端服务器和虚拟主机 4. 调度器 5. 健康检查 6. Hashing 7. 优雅模式 Grace mode 和 ...

  6. linux挂载光驱

    挂载光驱到linux中.linux的镜像盘中有安装oracle的所有的软件包,可以会用yum一键安装. 1.此时的linux的界面显示光驱图标 2.挂载 因为光盘里面的文件是只读模式的,yum安装时不 ...

  7. SA20225394 舒蔚 高级软件工程实验总结

    [实验]: 一.编程神器Visual Studio Code 配置好Visual Studio Code这一强大而又轻量的编辑器.其中有版本控制+代码补全+自动加载依赖,并且可以根据自己的需要添加插件 ...

  8. 「CF555E」 Case of Computer Network

    「CF555E」 Case of Computer Network 传送门 又是给边定向的题目(马上想到欧拉回路) 然而这个题没有对度数的限制,你想歪了. 然后又开始想一个类似于匈牙利的算法:我先跑, ...

  9. iPhone X适配方案

    iPhone X适配方案 https://github.com/Wscats/iPhone-X 绝对长度单位 英寸 厘米 毫米 磅 pc inch cm mm pt pica 相对长度单位 是网页设计 ...

  10. c语言字符串存储方式

    #include <stdio.h> // C 语言中,任何数据类型都不可以直接存储一个字符串.那么字符串如何存储? //在 C 语言中,字符串有两种存储方式,一种是通过字符数组存储,另一 ...