linux0.11内核分析之系统调用
概述
操心系统的核心是内核,拥有访问受保护的内存空间以及底层硬件设备的权限。为了使用户进程不能直接操作内核,操作系统将进程状态划分为高权限的核心态与低权限的用户态,而系统调用接口就是linux0.11系统中用户进程使用核心态功能的门户。
系统调用相关源码分析
1、pause
因为用户pause函数的定义在libc中,所以下面的代码可能会有一点晦涩
// 以下为pause.c内容
#include <signal.h>
#include <unistd.h>
#include <sysdep-cancel.h>
/* Suspend the process until a signal arrives.
This always returns -1 and sets errno to EINTR. */
int
__libc_pause (void)
{
sigset_t set;
int rc =
SYSCALL_CANCEL (rt_sigprocmask, SIG_BLOCK, NULL, &set, _NSIG / 8);
if (rc == 0)
rc = SYSCALL_CANCEL (rt_sigsuspend, &set, _NSIG / 8);
return rc;
}
weak_alias (__libc_pause, pause)
//以下为syscall cancel内容
#define SYSCALL_CANCEL(...) \
({ \
long int sc_ret; \
if (SINGLE_THREAD_P) \
sc_ret = __SYSCALL_CALL (__VA_ARGS__); \
else \
{ \
int sc_cancel_oldtype = LIBC_CANCEL_ASYNC (); \
sc_ret = __SYSCALL_CALL (__VA_ARGS__); \
LIBC_CANCEL_RESET (sc_cancel_oldtype); \
} \
sc_ret; \
})
看起来有点复杂,但实际上作用几乎只有一个,那就是把sys_pause系统调用号压入eax,随后调用system_call
2、system_call
bad_sys_call:
movl $-1,%eax
iret
.align 4
reschedule:
pushl $ret_from_sys_call
jmp schedule
.align 4
system_call:
cmpl $nr_system_calls-1,%eax
ja bad_sys_call
push %ds
push %es
push %fs
pushl %edx
pushl %ecx # push %ebx,%ecx,%edx as parameters
pushl %ebx # to the system call
movl $0x10,%edx # set up ds,es to kernel space
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs points to local data space
mov %dx,%fs
call sys_call_table(,%eax,4)
pushl %eax
movl current,%eax
cmpl $0,state(%eax) # state
jne reschedule
cmpl $0,counter(%eax) # counter
je reschedule
ret_from_sys_call:
movl current,%eax # task[0] cannot have signals
cmpl task,%eax
je 3f
cmpw $0x0f,CS(%esp) # was old code segment supervisor ?
jne 3f
cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ?
jne 3f
movl signal(%eax),%ebx
movl blocked(%eax),%ecx
notl %ecx
andl %ebx,%ecx
bsfl %ecx,%ecx
je 3f
btrl %ecx,%ebx
movl %ebx,signal(%eax)
incl %ecx
pushl %ecx
call do_signal
popl %eax
3: popl %eax
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds
iret
在内核调用系统函数的时候,会用一个陷入指令“INT 0x80”引发一个内部中断,随后调用system_call函数来进行处理(系统函数的调用),在system_call中,进程的状态从用户态转换为核心态,system_call函数根据保存在esp寄存器中的系统调用号到sys_call_table中寻找对应的系统调用函数并执行。
分析system_call函数的行为:
-
将eax寄存器中的系统调用号与定义的最大系统调用号(nr_system_calls)进行比较,如果eax超出系统调用号的范围,则认为该系统调用无效,跳转至bad_sys_call,随即iret返回用户态
-
如果系统调用号有效,则将当前用户进程的段寄存器ds、es、fs压栈保存,方便调用完成后恢复用户进程状态
-
把edx、ecx、ebx寄存器保存的值压入栈中作为系统调用的参数(参数通过栈传递)
-
设定内核态进程的段寄存器
-
调用sys_call_table中系统调用号对应的函数指针
-
调用完成后进行进程调度(进程调度的知识可以看这篇文章的system_call部分)
-
完成进程调度后,在do_signal中侦测内核信息并处理*(比如alarm定时器的时间信号)*
-
随后恢复用户进程栈并返回到用户态
3、内核态系统调用函数
此处同样以常见的sys_pause函数为例
int sys_pause(void)
{
current->state = TASK_INTERRUPTIBLE;
schedule();
return 0;
}
实际上,系统调用函数在linux0.11内核中的实现并不算复杂,如上面的sys_pause函数,只是将当前进程的状态设为可中断的睡眠态,然后执行一次进程调度
之所以要大费周章地通过多层调用及状态转换来完成这个功能,就是因为系统调用功能中直接对当前进程的运行状态进行了改变,并且进行了进程调度。为了防止用户误操作破坏操作系统的稳定性,系统定义了内核态和用户态并将权限进行了分离 (试想用户误操作把所有进程的运行状态变成了不可中断的睡眠态,导致整个系统瘫痪的美丽场景)
系统调用流程图
千言万语不如一张图,请诸君自取
强行总结
其实整个系统调用就是一个权限分离的小把戏,把内核态的核心资源保护起来,并提供一些接口让用户得以在制定的规则下使用内核资源又不破坏操作系统核心的稳定性,理解了这个流程之后,接下来对于外部设备的访问会更加的得心应手