概述
转自:https://www.freebsdchina.org/forum/viewtopic.php?t=11068
by wheelz
--------------------------
注:由于code是BBCode的关键字,在某些地方将程序中的变量code改写为_code
系统调用开始于用户程序,接着到达libc进行参数的包装,然后调用内核提供的机制进入内核。
内核提供的系统调用进入内核的方式有几种,包括lcall $X, y方式和
int 0x80方式。其实现都在sys/i386/i386/exception.s中。
我们看最常见的int 0x80入口。
1,int 0x80中断向量的初始化。
------------------
在i386CPU的初始化过程中,会调用函数init386() /*XXX*/
其中有:
代码: |
(sys/i386/i386/machdep.c) ----------------------------------- setidt(IDT_SYSCALL, &IDTVEC(int0x80_syscall), SDT_SYS386TGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL)); ----------------------------------- |
在这里设置好int80的中断向量表。
代码: |
(sys/i386/include/segments.h) --------------------------------- #define IDT_SYSCALL 0x80 /* System Call Interrupt Vector */ #define SDT_SYS386TGT 15 /* system 386 trap gate */ #define SEL_UPL 3 /* user priority level */ #define GSEL(s,r) (((s)<<3) | r) /* a global selector */ #define GCODE_SEL 1 /* Kernel Code Descriptor */ #define SEL_KPL 0 /* kernel priority level */ ---------------------------------- |
代码: |
(sys/i386/i386/machdep.c) ----------------------------------- void setidt(idx, func, typ, dpl, selec) int idx; inthand_t *func; int typ; int dpl; int selec; { struct gate_descriptor *ip; ip = idt + idx; ip->gd_looffset = (int)func; ip->gd_selector = selec; ip->gd_stkcpy = 0; ip->gd_xx = 0; ip->gd_type = typ; ip->gd_dpl = dpl; ip->gd_p = 1; ip->gd_hioffset = ((int)func)>>16 ; } ------------------------------------ |
2,int0x80_syscall
------------------
系统调用的入口是int0x80_syscall,在sys/i386/i386/exception.s中。
它其实是一个包装函数,用汇编写成,其目的是为调用C函数syscall()做准备。
代码: |
void syscall(frame) struct trapframe frame; |
由于系统调用最终是要调用syscall()这个函数,
因此需要为它准备一个调用栈,包括参数frame,其类型为struct trapframe
代码: |
/* * Exception/Trap Stack Frame */ struct trapframe { int tf_fs; int tf_es; int tf_ds; int tf_edi; int tf_esi; int tf_ebp; int tf_isp; int tf_ebx; int tf_edx; int tf_ecx; int tf_eax; int tf_trapno; /* below portion defined in 386 hardware */ int tf_err; int tf_eip; int tf_cs; int tf_eflags; /* below only when crossing rings (e.g. user to kernel) */ int tf_esp; int tf_ss; }; |
这个trapframe实际上就是保存在核心栈上的用户态寄存器的状态,当从
系统调用返回时,需要从这里恢复寄存器等上下文内容。同时,它又是
函数syscall()的参数,这样在syscall()函数里面就可以方便地操纵返回后
的用户进程上下文状态。
我们来看具体的int0x80_syscall。
代码: |
/* * Call gate entry for FreeBSD ELF and Linux/NetBSD syscall (int 0x80) * * Even though the name says 'int0x80', this is actually a TGT (trap gate) * rather then an IGT (interrupt gate). Thus interrupts are enabled on * entry just as they are for a normal syscall. */ SUPERALIGN_TEXT IDTVEC(int0x80_syscall) pushl $2 /* sizeof "int 0x80" */ |
对照struct trapframe可知,此句赋值frame->tf_err=2,记录int 0x80指令的长度,
因为有可能系统调用需要重新执行(系统调用返回ERESTART的话内核会自动重新执行),
需要%eip的值减去int 0x80的指令长度。
代码: |
subl $4,%esp /* skip over tf_trapno */ pushal pushl %ds pushl %es pushl %fs |
对照struct trapframe又可知,此时syscall(frame)的参数在堆栈上已经构造好。
代码: |
mov $KDSEL,%ax /* switch to kernel segments */ mov %ax,%ds mov %ax,%es mov $KPSEL,%ax mov %ax,%fs |
切换到内核数据段,并将%fs设置好,%fs指向一个per cpu的段,内存CPU相关的数据,
比如当前线程的pcb和struct thread指针。
代码: |
FAKE_MCOUNT(13*4(%esp)) call syscall MEXITCOUNT jmp doreti |
调用syscall()函数。syscall()返回后,
将转到doreti(也在sys/i386/i386/exception.s中),判断是否可以执行AST,
最后结束整个系统调用。
3,syscall()函数
---------------
我们接着看syscall()函数
代码: |
/* * syscall - system call request C handler * * A system call is essentially treated as a trap. */ void syscall(frame) struct trapframe frame; { caddr_t params; struct sysent *callp; struct thread *td = curthread; struct proc *p = td->td_proc; register_t orig_tf_eflags; u_int sticks; int error; int narg; int args[8]; u_int code; /* * note: PCPU_LAZY_INC() can only be used if we can afford * occassional inaccuracy in the count. */ PCPU_LAZY_INC(cnt.v_syscall); #ifdef DIAGNOSTIC if (ISPL(frame.tf_cs) != SEL_UPL) { mtx_lock(&Giant); /* try to stabilize the system XXX */ panic("syscall"); /* NOT REACHED */ mtx_unlock(&Giant); } #endif sticks = td->td_sticks; td->td_frame = &frame; if (td->td_ucred != p->p_ucred) cred_update_thread(td); |
如果进程的user credential发生了改变,更新线程的相应指针。
代码: |
if (p->p_flag & P_SA) thread_user_enter(p, td); |
如果进程的线程模型采用scheduler activation,则需要通知用户态的线程manager
(FIXME)
代码: |
(sys/sys/proc.h) #define P_SA 0x08000 /* Using scheduler activations. */ |
代码: |
params = (caddr_t)frame.tf_esp + sizeof(int); code = frame.tf_eax; |
params指向用户传递的系统调用参数。code指示是何种系统调用,后面还有描述。
代码: |
orig_tf_eflags = frame.tf_eflags; if (p->p_sysent->sv_prepsyscall) { /* * The prep code is MP aware. */ (*p->p_sysent->sv_prepsyscall)(&frame, args, &code, ¶ms); |
如果该进程有自己的系统调用准备函数,则调用之。事实上,所谓的系统调用准备函数,
其作用应该就是对用户传进来的参数进行解释。如果没有准备函数,则内核做缺省处理,如下:
代码: |
} else { /* * Need to check if this is a 32 bit or 64 bit syscall. * fuword is MP aware. */ if (code == SYS_syscall) { /* * Code is first argument, followed by actual args. */ code = fuword(params); params += sizeof(int); } else if (code == SYS___syscall) { /* * Like syscall, but code is a quad, so as to maintain * quad alignment for the rest of the arguments. */ code = fuword(params); params += sizeof(quad_t); } } |
如果该进程没有自己的系统调用准备函数,即缺省情况,则根据系统调用是32位还是64位,
得到相应的具体系统号,并相应调整指向用户参数的指针。
SYS_syscall对应32位方式,
SYS___syscall对应64位方式。
函数fuword()意为fetch user word,即从用户空间拷贝一个word到内核空间来。其定义在
sys/i386/i386/support.s中,其实现与copyin()类似,我们略过。
此时,具体的系统调用号已经在变量code中了。
代码: |
if (p->p_sysent->sv_mask) code &= p->p_sysent->sv_mask; |
对系统调用号做一些调整和限制。
代码: |
if ( code >= p->p_sysent->sv_size) callp = &p->p_sysent->sv_table[0]; else callp = &p->p_sysent->sv_table[_code]; |
得到系统调用的函数入口。
代码: |
narg = callp->sy_narg & SYF_ARGMASK; |
得到该系统调用的参数个数。
代码: |
/* * copyin and the ktrsyscall()/ktrsysret() code is MP-aware */ if (params != NULL && narg != 0) error = copyin(params, (caddr_t)args, (u_int)(narg * sizeof(int))); else error = 0; |
将参数从用户态拷贝到内核态的args中。
代码: |
#ifdef KTRACE if (KTRPOINT(td, KTR_SYSCALL)) ktrsyscall(code, narg, args); #endif /* * Try to run the syscall without Giant if the syscall * is MP safe. */ if ((callp->sy_narg & SYF_MPSAFE) == 0) mtx_lock(&Giant); |
如果该系统调用不是MP安全的,则获取全局锁。
代码: |
if (error == 0) { td->td_retval[0] = 0; td->td_retval[1] = frame.tf_edx; STOPEVENT(p, S_SCE, narg); PTRACESTOP_SC(p, td, S_PT_SCE); error = (*callp->sy_call)(td, args); } |
调用具体的系统调用。
这里,之所以要间接地使用一个系统调用函数表,是因为模拟其他操作系统的
需要。同一个系统调用在不同的操作系统里"系统调用号"是不同的,当运行其他
操作系统的应用程序时,因为其编译结果是用其他操作系统的"系统调用号",
此时需要转换到相应的FreeBSD的"系统调用号"上来,使用系统调用函数表就可以
方便地作到这一点。
代码: |
switch (error) { case 0: frame.tf_eax = td->td_retval[0]; frame.tf_edx = td->td_retval[1]; frame.tf_eflags &= ~PSL_C; break; |
Great,调用成功,设置返回值,并清除carry bit,用户态的libc要根据carry bit
判断系统调用是否成功。
代码: |
case ERESTART: /* * Reconstruct pc, assuming lcall $X,y is 7 bytes, * int 0x80 is 2 bytes. We saved this in tf_err. */ frame.tf_eip -= frame.tf_err; break; |
系统调用返回ERESTART,内核要尝试重新执行系统调用,因此需要将返回用户空间后的
%eip后退,具体后退几个字节,跟系统调用的进入方式有关,如果是通过int 0x80进入的,
由于int 0x80指令的长度为两个字节,因此回退2字节,如果是通过lcall $X,y方式进入
内核的,由于lcall $X,y指令的长度为7个字节,因此回退7字节。具体几个字节,在刚进入
时已经压到堆栈上了(前述pushl $2即是)。
代码: |
case EJUSTRETURN: break; default: if (p->p_sysent->sv_errsize) { if (error >= p->p_sysent->sv_errsize) error = -1; /* XXX */ else error = p->p_sysent->sv_errtbl[error]; } frame.tf_eax = error; frame.tf_eflags |= PSL_C; break; } |
如果系统调用返回其他错误的话,则在进程的一个错误对应表中转换错误号。
并设置carry bit,以便libc知道。
代码: |
/* * Release Giant if we previously set it. */ if ((callp->sy_narg & SYF_MPSAFE) == 0) mtx_unlock(&Giant); |
释放全局锁。
代码: |
/* * Traced syscall. */ if ((orig_tf_eflags & PSL_T) && !(orig_tf_eflags & PSL_VM)) { frame.tf_eflags &= ~PSL_T; trapsignal(td, SIGTRAP, 0); } |
处理Traced系统调用。
代码: |
/* * Handle reschedule and other end-of-syscall issues */ userret(td, &frame, sticks); |
做一些调度处理等,后面另分析。
代码: |
#ifdef KTRACE if (KTRPOINT(td, KTR_SYSRET)) ktrsysret(code, error, td->td_retval[0]); #endif /* * This works because errno is findable through the * register set. If we ever support an emulation where this * is not the case, this code will need to be revisited. */ STOPEVENT(p, S_SCX, code); PTRACESTOP_SC(p, td, S_PT_SCX); #ifdef DIAGNOSTIC cred_free_thread(td); #endif WITNESS_WARN(WARN_PANIC, NULL, "System call %s returning", (code >= 0 && code < SYS_MAXSYSCALL) ? syscallnames[_code] : "???"); mtx_assert(&sched_lock, MA_NOTOWNED); mtx_assert(&Giant, MA_NOTOWNED); } |
4, userret()函数
-----------------
简要地看一下userret()函数。
代码: |
/* * Define the code needed before returning to user mode, for * trap and syscall. * * MPSAFE */ void userret(td, frame, oticks) struct thread *td; struct trapframe *frame; u_int oticks; { struct proc *p = td->td_proc; CTR3(KTR_SYSC, "userret: thread %p (pid %d, %s)", td, p->p_pid, p->p_comm); #ifdef INVARIANTS /* Check that we called signotify() enough. */ PROC_LOCK(p); mtx_lock_spin(&sched_lock); if (SIGPENDING(td) && ((td->td_flags & TDF_NEEDSIGCHK) == 0 || (td->td_flags & TDF_ASTPENDING) == 0)) printf("failed to set signal flags properly for ast()n"); mtx_unlock_spin(&sched_lock); PROC_UNLOCK(p); #endif /* * Let the scheduler adjust our priority etc. */ sched_userret(td); |
调度器处理。
代码: |
/* * We need to check to see if we have to exit or wait due to a * single threading requirement or some other STOP condition. * Don't bother doing all the work if the stop bits are not set * at this time.. If we miss it, we miss it.. no big deal. */ if (P_SHOULDSTOP(p)) { PROC_LOCK(p); thread_suspend_check(0); /* Can suspend or kill */ PROC_UNLOCK(p); } |
是否需要停住?系统的某些时候只允许单个线程运行。
代码: |
/* * Do special thread processing, e.g. upcall tweaking and such. */ if (p->p_flag & P_SA) { thread_userret(td, frame); } |
又是scheduler activation的东西,通知用户态的thread manager。
(FIXME)
代码: |
/* * Charge system time if profiling. */ if (p->p_flag & P_PROFIL) { quad_t ticks; mtx_lock_spin(&sched_lock); ticks = td->td_sticks - oticks; mtx_unlock_spin(&sched_lock); addupc_task(td, TRAPF_PC(frame), (u_int)ticks * psratio); } } |
最后是profiling的东西。
最后进行编辑的是 wheelz on Tue 2004-05-11 09:42:44, 总计第 1 次编辑
最后
以上就是淡然火车为你收集整理的FreeBSD 5内核源代码分析之系统调用过程的全部内容,希望文章能够帮你解决FreeBSD 5内核源代码分析之系统调用过程所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复