服务器 频道

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

    【IT168 服务器学院】注:由于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
    MEX99vCOUNT
    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
    W99vNESS_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的东西。

0
相关文章