day11 system_call之fork

2018-12-12  本文已影响0人  柯基是只dog

用户程序要使用内核功能,唯一的途径是使用系统调用,cpu执行到该指令时,会切换到内核态去执行。

  // 设置系统调用中断门。
    set_system_gate (0x80, &system_call);

;//// int 0x80 --linux 系统调用入口点(调用中断int 0x80,eax 中是调用号)。
align 4
_system_call:
    cmp eax,nr_system_calls-1 ;// 调用号如果超出范围的话就在eax 中置-1 并退出。
    ja bad_sys_call
    push ds ;// 保存原段寄存器值。
    push es
    push fs
    push edx ;// ebx,ecx,edx 中放着系统调用相应的C 语言函数的调用参数。
    push ecx ;// push %ebx,%ecx,%edx as parameters
    push ebx ;// to the system call
    mov edx,10h ;// set up ds,es to kernel space
    mov ds,dx ;// ds,es 指向内核数据段(全局描述符表中数据段描述符)。
    mov es,dx
    mov edx,17h ;// fs points to local data space
    mov fs,dx ;// fs 指向局部数据段(局部描述符表中数据段描述符)。
;// 下面这句操作数的含义是:调用地址 = _sys_call_table + %eax * 4。参见列表后的说明。
;// 对应的C 程序中的sys_call_table 在include/linux/sys.h 中,其中定义了一个包括72 个
;// 系统调用C 处理函数的地址数组表。
    call [_sys_call_table+eax*4]
    push eax ;// 把系统调用号入栈。
    mov eax,_current ;// 取当前任务(进程)数据结构地址??eax。
;// 下面97-100 行查看当前任务的运行状态。如果不在就绪状态(state 不等于0)就去执行调度程序。
;// 如果该任务在就绪状态但counter[??]值等于0,则也去执行调度程序。
    cmp dword ptr [state+eax],0 ;// state
    jne reschedule
    cmp dword ptr [counter+eax],0 ;// counter
    je reschedule
;// 以下这段代码执行从系统调用C 函数返回后,对信号量进行识别处理。
ret_from_sys_call:
;// 首先判别当前任务是否是初始任务task0,如果是则不必对其进行信号量方面的处理,直接返回。
;// 103 行上的_task 对应C 程序中的task[]数组,直接引用task 相当于引用task[0]。
    mov eax,_current ;// task[0] cannot have signals
    cmp eax,_task
    je l1 ;// 向前(forward)跳转到标号l1。
;// 通过对原调用程序代码选择符的检查来判断调用程序是否是超级用户。如果是超级用户就直接
;// 退出中断,否则需进行信号量的处理。这里比较选择符是否为普通用户代码段的选择符0x000f
;// (RPL=3,局部表,第1 个段(代码段)),如果不是则跳转退出中断程序。
    cmp word ptr [R_CS+esp],0fh ;// was old code segment supervisor ?
    jne l1
;// 如果原堆栈段选择符不为0x17(也即原堆栈不在用户数据段中),则也退出。
    cmp word ptr [OLR_DSS+esp],17h ;// was stack segment = 0x17 ?
    jne l1
;// 下面这段代码(109-120)的用途是首先取当前任务结构中的信号位图(32 位,每位代表1 种信号),
;// 然后用任务结构中的信号阻塞(屏蔽)码,阻塞不允许的信号位,取得数值最小的信号值,再把
;// 原信号位图中该信号对应的位复位(置0),最后将该信号值作为参数之一调用do_signal()。
;// do_signal()在(kernel/signal.c,82)中,其参数包括13 个入栈的信息。
    mov ebx,[signal+eax] ;// 取信号位图??ebx,每1 位代表1 种信号,共32 个信号。
    mov ecx,[blocked+eax] ;// 取阻塞(屏蔽)信号位图??ecx。
    not ecx ;// 每位取反。
    and ecx,ebx ;// 获得许可的信号位图。
    bsf ecx,ecx ;// 从低位(位0)开始扫描位图,看是否有1 的位,
;// 若有,则ecx 保留该位的偏移值(即第几位0-31)。
    je l1 ;// 如果没有信号则向前跳转退出。
    btr ebx,ecx ;// 复位该信号(ebx 含有原signal 位图)。
    mov dword ptr [signal+eax],ebx ;// 重新保存signal 位图信息??current->signal。
    inc ecx ;// 将信号调整为从1 开始的数(1-32)。
    push ecx ;// 信号值入栈作为调用do_signal 的参数之一。
    call _do_signal ;// 调用C 函数信号处理程序(kernel/signal.c,82)
    pop eax ;// 弹出信号值。
l1: pop eax
    pop ebx
    pop ecx
    pop edx
    pop fs
    pop es
    pop ds
    iretd

  1. 把ds和es指向0x10,也就是内核的数据段
  2. 把fs指向0x17,也就是用户态的数据段
  3. 系统调用号是放在eax里的,当把一些寄存器入栈后就调用对应的系统调用程序
  4. cpu在进入system_call之前会自动完成一些工作,就是把cs,eip,eflags,esp,ss先入栈,所以我们可以看到一些c方法的参数有那么多,这就是用了栈上的数据,比如fork的copy
// 复制进程。
int copy_process (int nr, long ebp, long edi, long esi, long gs, long none,
                  long ebx, long ecx, long edx,
                  long fs, long es, long ds,
                  long eip, long cs, long eflags, long esp, long ss)
{

系统调用表

// 系统调用函数指针表。用于系统调用中断处理程序(int 0x80),作为跳转表。
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
  sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
  sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
  sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
  sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
  sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
  sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
  sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
  sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
  sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
  sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
  sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
  sys_setreuid, sys_setregid
};

这就是定义对应调用号与处理函数的地方了,我们可以先找一个看一看,比如说fork,它在数组里的下标是2,所以fork的调用号是2,我们找到他的入口代码

align 4
_sys_fork:
    call _find_empty_process ;// 调用find_empty_process()(kernel/fork.c,135)。
    test eax,eax
    js l2
    push gs
    push esi
    push edi
    push ebp
    push eax
    call _copy_process ;// 调用C 函数copy_process()(kernel/fork.c,68)。
    add esp,20 ;// 丢弃这里所有压栈内容。
l2: ret

find_empty_process是c代码,是找一个可以使用的pid

int find_empty_process (void)
{
    int i;

repeat:
    if ((++last_pid) < 0)
        last_pid = 1;
    for (i = 0; i < NR_TASKS; i++)
        if (task[i] && task[i]->pid == last_pid)
            goto repeat;
    for (i = 1; i < NR_TASKS; i++)  // 任务0 排除在外。
        if (!task[i])
            return i;
    return -EAGAIN;
}

如果找到了pid,则进入copy_process,该方法的参数刚才已经分析过了,前面的nr,ebp,edi,esi,gs是在sys_fork中入栈的,none这个比较有趣,是sys_fork中call copy_process时,cpu自动把下一条指令也就是返回点压入栈的。然后后面ebx都是在system_call中入栈的

// 复制进程。
int copy_process (int nr, long ebp, long edi, long esi, long gs, long none,
                  long ebx, long ecx, long edx,
                  long fs, long es, long ds,
                  long eip, long cs, long eflags, long esp, long ss)
{
    struct task_struct *p;
    int i;
    struct file *f;
    struct i387_struct *p_i387;

    p = (struct task_struct *) get_free_page ();    // 为新任务数据结构分配内存。
    if (!p)         // 如果内存分配出错,则返回出错码并退出。
        return -EAGAIN;
    task[nr] = p;           // 将新任务结构指针放入任务数组中。
// 其中nr 为任务号,由前面find_empty_process()返回。
    *p = *current;      /* NOTE! this doesn't copy the supervisor stack */
/* 注意!这样做不会复制超级用户的堆栈 (只复制当前进程内容)。*/ 
    p->state = TASK_UNINTERRUPTIBLE;    // 将新进程的状态先置为不可中断等待状态。
    p->pid = last_pid;      // 新进程号。由前面调用find_empty_process()得到。
    p->father = current->pid;   // 设置父进程号。
    p->counter = p->priority;
    p->signal = 0;      // 信号位图置0。
    p->alarm = 0;
    p->leader = 0;      /* process leadership doesn't inherit */
/* 进程的领导权是不能继承的 */
    p->utime = p->stime = 0;    // 初始化用户态时间和核心态时间。
    p->cutime = p->cstime = 0;  // 初始化子进程用户态和核心态时间。
    p->start_time = jiffies;    // 当前滴答数时间。
// 以下设置任务状态段TSS 所需的数据(参见列表后说明)。
    p->tss.back_link = 0;
    p->tss.esp0 = PAGE_SIZE + (long) p; // 堆栈指针(由于是给任务结构p 分配了1 页
// 新内存,所以此时esp0 正好指向该页顶端)。
    p->tss.ss0 = 0x10;      // 堆栈段选择符(内核数据段)[??]。
    p->tss.eip = eip;       // 指令代码指针。
    p->tss.eflags = eflags; // 标志寄存器。
    p->tss.eax = 0;
    p->tss.ecx = ecx;
    p->tss.edx = edx;
    p->tss.ebx = ebx;
    p->tss.esp = esp;
    p->tss.ebp = ebp;
    p->tss.esi = esi;
    p->tss.edi = edi;
    p->tss.es = es & 0xffff;    // 段寄存器仅16 位有效。
    p->tss.cs = cs & 0xffff;
    p->tss.ss = ss & 0xffff;
    p->tss.ds = ds & 0xffff;
    p->tss.fs = fs & 0xffff;
    p->tss.gs = gs & 0xffff;
    p->tss.ldt = _LDT (nr); // 该新任务nr 的局部描述符表选择符(LDT 的描述符在GDT 中)。
    p->tss.trace_bitmap = 0x80000000;
// 如果当前任务使用了协处理器,就保存其上下文。
    p_i387 = &p->tss.i387;
    if (last_task_used_math == current)
    _asm{
        mov ebx, p_i387
        clts
        fnsave [p_i387]
    }
//    __asm__ ("clts ; fnsave %0"::"m" (p->tss.i387));
// 设置新任务的代码和数据段基址、限长并复制页表。如果出错(返回值不是0),则复位任务数组中
// 相应项并释放为该新任务分配的内存页。
    if (copy_mem (nr, p))
    {               // 返回不为0 表示出错。
        task[nr] = NULL;
        free_page ((long) p);
        return -EAGAIN;
    }
// 如果父进程中有文件是打开的,则将对应文件的打开次数增1。
    for (i = 0; i < NR_OPEN; i++)
        if (f = p->filp[i])
            f->f_count++;
// 将当前进程(父进程)的pwd, root 和executable 引用次数均增1。
    if (current->pwd)
        current->pwd->i_count++;
    if (current->root)
        current->root->i_count++;
    if (current->executable)
        current->executable->i_count++;
// 在GDT 中设置新任务的TSS 和LDT 描述符项,数据从task 结构中取。
// 在任务切换时,任务寄存器tr 由CPU 自动加载。
    set_tss_desc (gdt + (nr << 1) + FIRST_TSS_ENTRY, &(p->tss));
    set_ldt_desc (gdt + (nr << 1) + FIRST_LDT_ENTRY, &(p->ldt));
    p->state = TASK_RUNNING;    /* do this last, just in case */
/* 最后再将新任务设置成可运行状态,以防万一 */
    return last_pid;        // 返回新进程号(与任务号是不同的)。
}

流程

  1. 上来调动get_free_page为进程描述块申请一页内存保存,然后存入task进程数组中
  2. 然后取当前进程(就是fork的父进程),用父进程的属性对新申请的描述块赋值
  3. 然后对一些属性修改,几个重要的是state改成不可中断等待,设置了tss中esp0和ss0(cpu在由于中断陷入内核态时候cpu会从tr寄存器也就是tss取出esp0和ss0,然后切换成内核堆栈,在这儿linux进程的内核堆栈指针放在该页的最后)。还把tss中的eax改成了0,我们知道eax是当做返回的寄存器,这样在子进程从fork返回的时候,返回的值是0,而父进程返回的是大于0的子进程pid。然后把tss中ldt改成当前进程的索引号。
  4. 继承所有父进程打开的fd,并且对这些fd指向的flip的f_count加1。对进程的pwd,root,executable的引用节点都+1。
  5. 最后重新设置在gdt表中的该进程的ldt和tss

因为linux0.11版本对进程的段划分很简单,所以计算新的逻辑地址很简单,只是取出原来的,然后加上对应进程的其实段地址。最后调用copy_page_tables,复制原进程的页表项

int copy_mem (int nr, struct task_struct *p)
{
    unsigned long old_data_base, new_data_base, data_limit;
    unsigned long old_code_base, new_code_base, code_limit;

    code_limit = get_limit (0x0f);  // 取局部描述符表中代码段描述符项中段限长。
    data_limit = get_limit (0x17);  // 取局部描述符表中数据段描述符项中段限长。
    old_code_base = get_base (current->ldt[1]); // 取原代码段基址。
    old_data_base = get_base (current->ldt[2]); // 取原数据段基址。
    if (old_data_base != old_code_base) // 0.11 版不支持代码和数据段分立的情况。
        panic ("We don't support separate I&D");
    if (data_limit < code_limit)    // 如果数据段长度 < 代码段长度也不对。
        panic ("Bad data_limit");
    new_data_base = new_code_base = nr * 0x4000000; // 新基址=任务号*64Mb(任务大小)。
    p->start_code = new_code_base;
    set_base (p->ldt[1], new_code_base);    // 设置代码段描述符中基址域。
    set_base (p->ldt[2], new_data_base);    // 设置数据段描述符中基址域。
    if (copy_page_tables (old_data_base, new_data_base, data_limit))
    {               // 复制代码和数据段。
        free_page_tables (new_data_base, data_limit);   // 如果出错则释放申请的内存。
        return -ENOMEM;
    }
    return 0;
}

最后我们看到方法返回的是last_pid,c方法返回后是放在eax里的,system_call最后返回用户态的时候是pop出了eax,所以我们的父进程从该点返回后,fork返回的值就是子进程的pid,现在我们就清楚了为什么我们写fork的时候会有两次返回,而父进程和子进程返回的值不同的原因了

上一篇下一篇

猜你喜欢

热点阅读