徐晨 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
本次的作业希望我们能够讲自己的系统调用放入menuOS中,用GDB进行观察,并分析一下系统调用中一段关键代码做了那些工作。我们首先向menuOS中加入自己的系统调用。这里我加入的是getpid调用,如下图所示。
我们尝试debug一下
我们看到我们可以跟踪到系统函数sys_getpid,但是往下gdb就无法跟踪到后面的汇编代码中去了。
那么我们来简单分析一下从system_call开始到iret结束之间的整个过程。这段代码是系统调用在内核态执行的很重要的一段代码,内核在set_system_trap_gate(SYSCALL_VECTOR, &system_call)中绑定了system_call和中断向量SYSCALL_VECTOR以后,一但我们执行int 0x80系统就会立即跳转到systemcall的位置(entry_32.S)。这中间我们经历了上下文环境的保存,系统调用服务程序的执行,现场的恢复,以及从内核态回到用户态的一系列过程。iret这条命令实际上是上下文切换的最后一条命令,完成后我们也就从内核态回到了用户态。
.macro INTERRUPT_RETURN
iret
.endm
.macro SAVE_ALL
...
.macro RESTORE_INT_REGS #宏定义
...
.endm
ENTRY(system_call) #开始
SAVE_ALL
syscall_call:
call *sys_call_table(,%eax,4)<span style="white-space:pre"> </span>#根据系统调用号来查找处理程序
movl %eax, PT_EAX(%esp) ; store the return value
syscall exit:
testl $_TIF_ALLWORK_MASK, %ecx 是否由需要处理的工作
jne syscall_exit_work
restore_all: #保存现场
RESTORE_INT_REGS
irq_return:<span style="white-space:pre"> </span> #返回
INTERRUPT_RETURN
ENDPROC(system_call)
syscall_exit_work:
testl $_TIF_WORK_SYSCALL_EXIT, %ecx
jz work_pending
END(syscall_exit_work)
work_pending:
testb $_TIF_NEED_RESCHED, %cl #是否需要调度
jz work_notifysig
work_resched:
call schedule
jz restore_all
work_notifysig: #处理挂起的信号
... ; deal with pending signals
END(work_pending)
由于这段代码有些复杂,所以我们从 孟老师提供的简化伪代码入手分析,内核执行int 0x80后系统跳转到system_call入口,首先系统进行保存现场的工作(包括将用户态进程的各寄存器保存在内核堆栈上),之后系统执行sys_call_table,该函数通过系统调用号来查找实际的系统调用函数,当该函数返回时,系统保存其返回值,然后到syscall_exit_work,这里会有一个判断,即此时系统是否还有没有完成的额外工作,如果有的话,首先进入work_pending,这里是检查是否有等待调度的进程,因为这里是内核设置的进程调度的一个时机。该段程序循环调度需要调度的进程,如果已经没有需要调度的进程,紧接着需要检查是否有挂起的信号(比如进程间通信的一些信号)等待处理。处理完毕后,本次系统调用也就接近了尾声,此时恢复上下文,然后将CPU控制权交给用户态。至此该段代码执行完毕。我们总结成一张流程图的话,应该如下图所示。
通过上面的分析我们可以得出,系统调用实际上是一种特殊的中断。和一般的中断一样,它需要进行现场的保存,用户态到内核态的切换,现场的恢复,内核态到用户态的切换这么几个步骤。
但和一般的中断相比,系统调用又有所不同。这个中断不是由硬件触发的(如键盘按键),而是由封装在C库中的系统调用函数通过调用int 0x80进入的。这个指令是一条陷入指令,它是我们正在执行的代码引起的,而不是外界与当前代码无关的中断源引起的。
这次的分析就到这里,希望对读者有所帮助。