在三种情况下,用户程序需要陷入内核。
- 系统调用。来自用户程序主动调用请求内核服务。
- 异常。来自用户程序本身的错误。
- 中断。来自外部程序的中断信号。
x86硬件机制
在x86中,有四种特权等级,0到3,而操作系统只使用0和3级别,分别代表内核级和用户级。
在x86中中断向量是由IDT(中断描述符表)维护的。在x86中如果要进行系统调用,需要使用int n指令,其中n代表在IDT中的中断向量的索引,int指令主要进行保护现场和跳转到中断向量的操作。
系统调用过程
在main函数中调用了tvinit函数来进行IDT的初始化。
int
main(void)
{
...
tvinit(); // trap vectors
...
}
在tvinit中可以看到操作系统设置了256个entry,特别的是设置了专门的系统调用的entry,且其特权级别是USER,其他的所有entry特权级都是内核级,这说明用户程序只有系统调用这一种权限。
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");
}
其中的vector表中的每一个vector定义如下。
vector0:
pushl $0
pushl $0
jmp alltraps
.globl vector1
而alltrap定义如下。这里就是进一步保存现场,最后就形成了一个前述文章中提到的一个trapframe,再接着调用trap过程,而当trap执行完之后,就会接着接着执行trapret过程,这里主要就是恢复现场。
# vectors.S sends all traps here.
.globl alltraps
alltraps:
# Build trap frame.
pushl %ds
pushl %es
pushl %fs
pushl %gs
pushal
# 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
addl $4, %esp
接着再看trap函数。可以看到这个函数接收的是一个trapframe,也就是前述过程所保存的现场。接着它会根据其中的trapno来判断trap的类型,时系统调用还是中断,如果是系统调用的话就会调用syscall函数。
void
trap(struct trapframe *tf)
{
if(tf->trapno == T_SYSCALL){
if(myproc()->killed)
exit();
myproc()->tf = tf;
syscall();
if(myproc()->killed)
exit();
return;
}
switch(tf->trapno){
case T_IRQ0 + IRQ_TIMER:
if(cpuid() == 0){
acquire(&tickslock);
ticks++;
wakeup(&ticks);
release(&tickslock);
}
lapiceoi();
break;
case T_IRQ0 + IRQ_IDE:
ideintr();
lapiceoi();
break;
case T_IRQ0 + IRQ_IDE+1:
// Bochs generates spurious IDE1 interrupts.
break;
case T_IRQ0 + IRQ_KBD:
kbdintr();
lapiceoi();
break;
case T_IRQ0 + IRQ_COM1:
uartintr();
lapiceoi();
break;
case T_IRQ0 + 7:
case T_IRQ0 + IRQ_SPURIOUS:
cprintf("cpu%d: spurious interrupt at %x:%x\n",
cpuid(), tf->cs, tf->eip);
lapiceoi();
break;
//PAGEBREAK: 13
default:
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;
}
// Force process exit if it has been killed and is in user space.
// (If it is still executing in the kernel, let it keep running
// until it gets to the regular system call return.)
if(myproc() && myproc()->killed && (tf->cs&3) == DPL_USER)
exit();
// Force process to give up CPU on clock tick.
// If interrupts were on while locks held, would need to check nlock.
if(myproc() && myproc()->state == RUNNING &&
tf->trapno == T_IRQ0+IRQ_TIMER)
yield();
// Check if the process has been killed since we yielded
if(myproc() && myproc()->killed && (tf->cs&3) == DPL_USER)
exit();
}
接着就来到了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;
}
}
系统调用表定义如下。
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,
};
比如其中的sys_exec,用户进程如果要进行exec的话,必须要通过系统调用的形式,也就是通过系统调用表找到sys_exec,而在sys_exec函数中,实际就是调用了之前分析过的exec函数,而内核中就可以直接调用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);
}
中断过程
由于xv6构建在多核系统上,所以不使用PIC的方式处理中断。而是使用分离的IOAPIC和local APIC,前者面向设备端,后者面向CPU端。比如在如下代码中xv6通过ioapic将键盘中断导向了CPU0。
void
consoleinit(void)
{
initlock(&cons.lock, "console");
devsw[CONSOLE].write = consolewrite;
devsw[CONSOLE].read = consoleread;
cons.locking = 1;
ioapicenable(IRQ_KBD, 0);
}
在main函数中主要有lapicinit来用来初始化local apic,有ioapicinit来初始化io apic,有picinit来禁止pic。比如在lapicinit中初始化定时器中断。这里是硬件上的初始化,即保证CPU能接受到定时器中断信号。
// The timer repeatedly counts down at bus frequency
// from lapic[TICR] and then issues an interrupt.
// If xv6 cared more about precise timekeeping,
// TICR would be calibrated using an external time source.
lapicw(TDCR, X1);
lapicw(TIMER, PERIODIC | (T_IRQ0 + IRQ_TIMER));
lapicw(TICR, 10000000);
定时器中断的处理和系统调用的处理类似。定时器中断在vector中的号是32,而系统调用是64。而定时器中断的处理过程也就是将tick增加并且调用wakeup函数,而在wakeup函数中可能会发生进程切换。
case T_IRQ0 + IRQ_TIMER:
if(cpuid() == 0){
acquire(&tickslock);
ticks++;
wakeup(&ticks);
release(&tickslock);
}
lapiceoi();
break;
磁盘驱动
磁盘将每512个字节叫做一个扇区(sector)。而xv6中所使用的磁盘读取单位(block)也为一个扇区。xv6使用如下一个结构体来管理一个block。
struct buf {
int flags;
uint dev;
uint blockno;
struct sleeplock lock;
uint refcnt;
struct buf *prev; // LRU cache list
struct buf *next;
struct buf *qnext; // disk queue
uchar data[BSIZE];
};
其中的flags字段包括如下两种值。其中B_VALID表示数据已经从磁盘读进入了内存,而B_DIRTY表示当前内存中的数据需要写入到磁盘上。
#define B_VALID 0x2 // buffer has been read from disk
#define B_DIRTY 0x4 // buffer needs to be written to disk
在main函数中调用了ideinit函数来初始化磁盘驱动。
ideinit(); // disk
而在ideinit中,首先调用了ioapicenable函数来开启IRQ_IDE中断。
void
ideinit(void)
{
int i;
initlock(&idelock, "ide");
ioapicenable(IRQ_IDE, ncpu - 1);
idewait(0);
// Check if disk 1 is present
outb(0x1f6, 0xe0 | (1<<4));
for(i=0; i<1000; i++){
if(inb(0x1f7) != 0){
havedisk1 = 1;
break;
}
}
// Switch back to disk 0.
outb(0x1f6, 0xe0 | (0<<4));
}
接着调用了idewait函数来等待磁盘进入能够接收CPU命令的状态。而在idewait中通过IO端口0x1f7来读取当前磁盘的状态。idewait函数一直轮询端口的状态值并一直等到IDE_BSY为0且IDE_DRDY为1才返回。而此时表示磁盘已经准备好接收CPU命令开始工作了。
// Wait for IDE disk to become ready.
static int
idewait(int checkerr)
{
int r;
while(((r = inb(0x1f7)) & (IDE_BSY|IDE_DRDY)) != IDE_DRDY)
;
if(checkerr && (r & (IDE_DF|IDE_ERR)) != 0)
return -1;
return 0;
}
而对上述struct buf进行操作的主要函数就是iderw,这个函数主要的功能是同步struct buf和磁盘之间的数据,比如当B_DIRTY被置位的时候,它会将buf写到磁盘上,而当B_VALID被置位的时候,它就会从磁盘上读取最新的数据来更新struct buf。
//PAGEBREAK!
// Sync buf with disk.
// If B_DIRTY is set, write buf to disk, clear B_DIRTY, set B_VALID.
// Else if B_VALID is not set, read buf from disk, set B_VALID.
void
iderw(struct buf *b)
{
struct buf **pp;
if(!holdingsleep(&b->lock))
panic("iderw: buf not locked");
if((b->flags & (B_VALID|B_DIRTY)) == B_VALID)
panic("iderw: nothing to do");
if(b->dev != 0 && !havedisk1)
panic("iderw: ide disk 1 not present");
acquire(&idelock); //DOC:acquire-lock
// Append b to idequeue.
b->qnext = 0;
for(pp=&idequeue; *pp; pp=&(*pp)->qnext) //DOC:insert-queue
;
*pp = b;
// Start disk if necessary.
if(idequeue == b)
idestart(b);
// Wait for request to finish.
while((b->flags & (B_VALID|B_DIRTY)) != B_VALID){
sleep(b, &idelock);
}
release(&idelock);
}
注意到当对磁盘进行读写的时候,必须要考虑到磁盘和CPU之间的速度差异,如果在读写磁盘的时候一直循环等待显然耗费了大量的CPU资源。所以可以看到iderw对struct buf的读写使用了队列的机制,每次只处理队头的读写请求(这是由于磁盘每次只能处理一个请求,即进行一次读或写),且在读写过程中进入sleep状态,这样其他进程就可以在读写的时间内继续工作。
其中的idestart函数就是通过IO端口向磁盘控制器发出读写请求。
if(b->flags & B_DIRTY){
outb(0x1f7, write_cmd);
outsl(0x1f0, b->data, BSIZE/4);
} else {
outb(0x1f7, read_cmd);
}
最后当磁盘完成读写请求后,会产生一个中断,而处理这个中断的函数就是ideintr函数。当发出的操作是读时,产生的中断信号会告知此时磁盘数据已经ready可以直接读到内存了,而在ideintr中就会进行读取过程。当发出的操作是写时,产生的中断信号就会告知此时数据已经全部写入磁盘了。最后ideintr会唤醒等待的进程并继续发送队列中的下一个磁盘请求。
// Interrupt handler.
void
ideintr(void)
{
struct buf *b;
// First queued buffer is the active request.
acquire(&idelock);
if((b = idequeue) == 0){
release(&idelock);
return;
}
idequeue = b->qnext;
// Read data if needed.
if(!(b->flags & B_DIRTY) && idewait(1) >= 0)
insl(0x1f0, b->data, BSIZE/4);
// Wake process waiting for this buf.
b->flags |= B_VALID;
b->flags &= ~B_DIRTY;
wakeup(b);
// Start disk on next buf in queue.
if(idequeue != 0)
idestart(idequeue);
release(&idelock);
}