免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 1040 | 回复: 0
打印 上一主题 下一主题

FreeBSD 5内核源代码分析之系统调用过程 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2006-07-12 11:53 |只看该作者 |倒序浏览
FreeBSD 5内核源代码分析之系统调用过程
by wheelz
--------------------------
系统调用开始于用户程序,接着到达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)
代码:
(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
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的东西。


本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u/6180/showart_139518.html
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP