FreeBSD 5内核源代码分析之系统调用过程

转自: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, &params); 

如果该进程有自己的系统调用准备函数,则调用之。事实上,所谓的系统调用准备函数, 
其作用应该就是对用户传进来的参数进行解释。如果没有准备函数,则内核做缺省处理,如下: 

代码:

   } 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 次编辑
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值