你管这叫操作系统源码(九)

通过fork看一次系统调用

有了前两篇文章的铺垫,我们终于可以回到主流程看看fork函数了。这个fork函数稍稍绕了点,看如下代码:

static _inline _syscall0(int,fork)

#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
    : "=a" (__res) \
    : "0" (__NR_##name)); \
if (__res >= 0) \
    return (type) __res; \
errno = -__res; \
return -1; \
}

别急,我把它变成稍稍能看得懂的样子,就是这样:

#define _syscall0(type,name) \
type name(void) \
{ \
    volatile long __res; \
    _asm { \
        _asm mov eax,__NR_##name \
        _asm int 80h \
        _asm mov __res,eax \
    } \
    if (__res >= 0) \
        return (type) __res; \
    errno = -__res; \
    return -1; \
}

所以,把宏定义都展开,其实就相当于定义了一个函数

int fork(void) {
     volatile long __res;
    _asm {
        _asm mov eax,__NR_fork
        _asm int 80h
        _asm mov __res,eax
    }
    if (__res >= 0)
        return (void) __res;
    errno = -__res;
    return -1;
}

具体看一下 fork 函数里面的代码,又是讨厌的内联汇编,不过上面我已经变成好看一点的样子了。关键指令就是一个 0x80 号软中断的触发,int 80h。其中还有一个 eax 寄存器里的参数是 __NR_fork,这也是个宏定义,值是 2。

OK,还记得 0x80 号中断的处理函数么?这个是我们在系列之六中 进程调度初始化 sched_init 里面设置的:

set_system_gate(0x80, &system_call);

看这个 system_call 的汇编代码,我们发现这么一行。

_system_call:
    ...
    call [_sys_call_table + eax*4]
    ...

刚刚那个值就用上了,eax 寄存器里的值是 2,所以这个就是在这个 sys_call_table 表里找下标 2 位置处的函数,然后跳转过去。那我们接着看 sys_call_table 是个啥。

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
};

看到没,就是各种函数指针组成的一个数组,说白了就是个系统调用函数表。那下标 2 位置处是啥?从第零项开始数,第二项就是 sys_fork 函数!至此,我们终于找到了 fork 函数,通过系统调用这个中断,最终走到内核层面的函数是什么,就是 sys_fork

_sys_fork:
    call _find_empty_process
    testl %eax,%eax
    js 1f
    push %gs
    pushl %esi
    pushl %edi
    pushl %ebp
    pushl %eax
    call _copy_process
    addl $20,%esp
1:  ret

至于这个函数是什么,我们下节再说。

小结

从这讲的探索我们也可以看出,操作系统通过系统调用,提供给用户态可用的功能,都暴露在 sys_call_table 里了。

系统调用统一通过 int 0x80 中断来进入,具体调用这个表里的哪个功能函数,就由 eax 寄存器传过来,这里的值是个数组索引的下标,通过这个下标就可以找到在 sys_call_table 这个数组里的具体函数。同时也可以看出,用户进程调用内核的功能,可以直接通过写一句 int 0x80 汇编指令,并且给 eax 赋值,当然这样就比较麻烦。所以也可以直接调用 fork 这样的包装好的方法,而这个方法里本质也是 int 0x80 以及 eax 赋值而已。
ch15-1

那我们再多说两句,刚刚定义 fork 的系统调用模板函数时,用的是 syscall0,其实这个表示参数个数为 0,也就是 sys_fork 函数并不需要任何参数。所以其实,在unistd.h头文件里,还定义了 syscall0 ~ syscall3 一共四个宏:

#define _syscall0(type,name)
#define _syscall1(type,name,atype,a)
#define _syscall2(type,name,atype,a,btype,b)
#define _syscall3(type,name,atype,a,btype,b,ctype,c)

看都能看出来,其实 syscall1 就表示有一个参数syscall2 就表示有两个参数。那这些参数放在哪里了呢?总得有个约定的地方吧?

我们看一个今后要讲的重点函数,execve,是一个通常和 fork 在一起配合的变身函数,在之后的进程 1 创建进程 2 的过程中,就是这样玩的:

void init(void) {
    ...
    if (!(pid=fork())) {
        ...
        execve("/bin/sh",argv_rc,envp_rc);
        ...
    }
}

当然我们的重点不是研究这个函数的作用,仅仅把它当做研究 syscall3 的一个例子,因为它的宏定义就是 syscall3

execve("/bin/sh",argv_rc,envp_rc);

_syscall3(int,execve,const char *,file,char **,argv,char **,envp)

#define _syscall3(type,name,atype,a,btype,b,ctype,c) \
type name(atype a,btype b,ctype c) { \
    volatile long __res; \
    _asm { \
        _asm mov eax,__NR_##name \
        _asm mov ebx,a \
        _asm mov ecx,b \
        _asm mov edx,c \
        _asm int 80h \
        _asm mov __res,eax\
    } \
    if (__res >= 0) \
        return (type) __res; \
    errno = -__res; \
    return -1; \
}

可以看出,参数 a 被放在了 ebx 寄存器,参数 b 被放在了 ecx 寄存器,参数 c 被放在了 edx 寄存器。我们再打开 system_call 的代码,刚刚我们只看了它的关键一行,就是去系统调用表里找函数。再看看全貌:

_system_call:
    cmpl $nr_system_calls-1,%eax
    ja bad_sys_call
    push %ds
    push %es
    push %fs
    pushl %edx
    pushl %ecx      # push %ebx,%ecx,%edx as parameters
    pushl %ebx      # to the system call
    movl $0x10,%edx     # set up ds,es to kernel space
    mov %dx,%ds
    mov %dx,%es
    movl $0x17,%edx     # fs points to local data space
    mov %dx,%fs
    call _sys_call_table(,%eax,4)
    pushl %eax
    movl _current,%eax
    cmpl $0,state(%eax)     # state
    jne reschedule
    cmpl $0,counter(%eax)       # counter
    je reschedule
ret_from_sys_call:
    movl _current,%eax      # task[0] cannot have signals
    cmpl _task,%eax
    je 3f
    cmpw $0x0f,CS(%esp)     # was old code segment supervisor ?
    jne 3f
    cmpw $0x17,OLDSS(%esp)      # was stack segment = 0x17 ?
    jne 3f
    movl signal(%eax),%ebx
    movl blocked(%eax),%ecx
    notl %ecx
    andl %ebx,%ecx
    bsfl %ecx,%ecx
    je 3f
    btrl %ecx,%ebx
    movl %ebx,signal(%eax)
    incl %ecx
    pushl %ecx
    call _do_signal
    popl %eax
3:  popl %eax
    popl %ebx
    popl %ecx
    popl %edx
    pop %fs
    pop %es
    pop %ds
    iret

又被吓到了是不是?别怕,我们只关注压栈的情况,还记不记得在系列之七从内核态到用户态 中,我们聊到触发了中断后,CPU 会自动帮我们做如下压栈操作:

ch14-4因为 system_call 是通过 int 80h 这个软中断进来的,所以也属于中断的一种,具体说是属于特权级发生变化的,且没有错误码情况的中断,所以在这之前栈已经被压了 SS、ESP、EFLAGS、CS、EIP 这些值。接下来 system_call 又压入了一些值,具体说来有 ds、es、fs、edx、ecx、ebx、eax

如果你看源码费劲,得不出我上述结论,那你可以看 system_call.s 上面的注释,Linus 作者已经很贴心地给你写出了此时的堆栈状态:

/*
 * Stack layout in 'ret_from_system_call':
 *
 *   0(%esp) - %eax
 *   4(%esp) - %ebx
 *   8(%esp) - %ecx
 *   C(%esp) - %edx
 *  10(%esp) - %fs
 *  14(%esp) - %es
 *  18(%esp) - %ds
 *  1C(%esp) - %eip
 *  20(%esp) - %cs
 *  24(%esp) - %eflags
 *  28(%esp) - %oldesp
 *  2C(%esp) - %oldss
 */

看,就是 CPU 中断压入的 5 个值,加上 system_call 手动压入的 7 个值。所以之后,中断处理程序如果有需要的话,就可以从这里取出它想要的值,包括 CPU 压入的那五个值,或者 system_call 手动压入的 7 个值。

比如 sys_execve 这个中断处理函数,一开始就取走了位于栈顶 0x1C 位置处的 EIP 的值:

EIP = 0x1C
_sys_execve:
    lea EIP(%esp),%eax
    pushl %eax
    call _do_execve
    addl $4,%esp
    ret

随后在 do_execve 函数中,又通过 C 语言函数调用的约定,取走了 filename,argv,envp 等参数:

int do_execve(
        unsigned long * eip,
        long tmp,
        char * filename,
        char ** argv,
        char ** envp) {
    ...
}

具体这个函数的详细流程和作用,将会在之后的 shell 程序装载章节讲到。本篇你只需要记住一次系统调用的流程和原理,就可以了,把小结已开始那张图印在脑子里,之后很多函数都会像今天的 fork 一样,走一遍系统调用的流程,到时候我就不再展开了。

fork中进程基本信息的复制

本节讲 fork 函数的原理,实际上就是sys_fork函数干了啥。还是个汇编代码,但我们要关注的地方不多:

_sys_fork:
    call _find_empty_process
    testl %eax,%eax
    js 1f
    push %gs
    pushl %esi
    pushl %edi
    pushl %ebp
    pushl %eax
    call _copy_process
    addl $20,%esp
1:  ret

其实就是调用了两个函数。我们先从方法名直接翻译一下,猜猜意思。先是find_empty_process,就是找到空闲的进程槽位。然后 copy_process,就是复制进程。那妥了,这个方法的意思非常简单,因为存储进程的数据结构是一个 task[64] 数组,这个是在之前系列之六中 进程调度初始化 sched_init 函数的时候设置的

ch13-2

就是先在这个数组中找一个空闲的位置,准备存一个新的进程的结构 task_struct,这个结构在上篇如果让你来设计进程调度 也简单说过了

struct task_struct {
    long state;
    long counter;
    long priority;
    ...
    struct tss_struct tss;
}

这个结构各个字段具体赋什么值呢?通过 copy_process 这个名字我们知道,就是复制原来的进程,也就是当前进程。当前只有一个进程,就是数组中位置 0 处的 init_task.init,也就是零号进程,那自然就复制它咯。好了,以上只是我们的猜测,有了猜测再看代码会非常轻松,我们一个个函数看。

先来 find_empty_process

long last_pid = 0;

int find_empty_process(void) {
    int i;
    repeat:
        if ((++last_pid)<0) last_pid=1;
        for(i=0 ; i<64 ; i++)
            if (task[i] && task[i]->pid == last_pid) goto repeat;
    for(i=1 ; i<64; i++)
        if (!task[i])
            return i;
    return -EAGAIN;
}

一共三步,很简单。

第一步,判断 ++last_pid 是不是小于零了,小于零说明已经超过 long 的最大值了,重新赋值为 1,起到一个保护作用,这没什么好说的。

第二步,一个 for 循环,看看刚刚的 last_pid 在所有 task[] 数组中,是否已经被某进程占用了。如果被占用了,那就重复执行,再次加一,然后再次判断,直到找到一个 pid 号没有被任何进程用为止。

第三步,又是个 for 循环,刚刚已经找到一个可用的 pid 号了,那这一步就是再次遍历这个 task[] 试图找到一个空闲项,找到了就返回素组索引下标。

最终,这个方法就返回 task[] 数组的索引,表示找到了一个空闲项,之后就开始往这里塞一个新的进程吧。由于我们现在只有 0 号进程,且 task[] 除了 0 号索引位置,其他地方都是空的,所以这个方法运行完,last_pid 就是 1,也就是新进程被分配的 pid 就是 1,然后即将要加入的 task[] 数组的索引位置,也是 1。好的,那我们接下来就看,怎么构造这个进程结构,塞到这个 1 索引位置的 task[] 中?

来看 copy_process

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;


    p = (struct task_struct *) get_free_page();
    if (!p)
        return -EAGAIN;
    task[nr] = p;
    *p = *current;  /* NOTE! this doesn't copy the supervisor stack */
    p->state = TASK_UNINTERRUPTIBLE;
    p->pid = last_pid;
    p->father = current->pid;
    p->counter = p->priority;
    p->signal = 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;
    p->tss.back_link = 0;
    p->tss.esp0 = PAGE_SIZE + (long) p;
    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;
    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);
    p->tss.trace_bitmap = 0x80000000;
    if (last_task_used_math == current)
        __asm__("clts ; fnsave %0"::"m" (p->tss.i387));
    if (copy_mem(nr,p)) {
        task[nr] = NULL;
        free_page((long) p);
        return -EAGAIN;
    }
    for (i=0; i<NR_OPEN;i++)
        if (f=p->filp[i])
            f->f_count++;
    if (current->pwd)
        current->pwd->i_count++;
    if (current->root)
        current->root->i_count++;
    if (current->executable)
        current->executable->i_count++;
    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;
}

艾玛,这也太多了!别急,大部分都是 tss 结构的复制,以及一些无关紧要的分支,看我简化下:

int copy_process(int nr, ...) {
    struct task_struct p = 
        (struct task_struct *) get_free_page();
    task[nr] = p;
    *p = *current;

    p->state = TASK_UNINTERRUPTIBLE;
    p->pid = last_pid;
    p->counter = p->priority;
    ..
    p->tss.edx = edx;
    p->tss.ebx = ebx;
    p->tss.esp = esp;
    ...
    copy_mem(nr,p);
    ...
    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;
    return last_pid;
}

这个函数本来就是 fork 的难点了,所以我们慢慢来。首先 get_free_page 会在主内存末端申请一个空闲页面,还记得我们之前在系列之四主内存初始化 mem_init 里是怎么管理内存的吧?

ch11-1那 get_free_page 这个函数就很简单了,就是遍历 mem_map[] 这个数组,找出值为零的项,就表示找到了空闲的一页内存。然后把该项置为 1,表示该页已经被使用。最后,算出这个页的内存起始地址,返回。然后,拿到的这个内存起始地址,就给了 task_struct 结构的p:

int copy_process(int nr, ...) {
    struct task_struct p = 
        (struct task_struct *) get_free_page();
    task[nr] = p;
    *p = *current;
    ...
}

于是乎,一个进程结构 task_struct 就在内存中有了一块空间,但此时还没有赋值具体的字段。别急。

首先将这个 p 记录在进程管理结构 task[] 中。然后下一句 *p = *current很简单,就是把当前进程,也就是 0 号进程的 task_struct 的全部值都复制给即将创建的进程 p,目前它们两者就完全一样了。嗯,这就附上值了,就完全复制之前的进程的 task_struct 而已,很粗暴。最后的内存布局的效果就是这样:
ch13-2改

然后,进程 1 和进程 0 目前是完全复制的关系,但有一些值是需要个性化处理的,下面的代码就是把这些不一样的值覆盖掉:

int copy_process(int nr, ...) {
    ...
    p->state = TASK_UNINTERRUPTIBLE;
    p->pid = last_pid;
    p->counter = p->priority;
    ..
    p->tss.edx = edx;
    p->tss.ebx = ebx;
    p->tss.esp = esp;
    ...
    p->tss.esp0 = PAGE_SIZE + (long) p;
    p->tss.ss0 = 0x10;
    ...
}

不一样的值,一部分是 statepidcounter 这种进程的元信息,另一部分是 tss 里面保存的各种寄存器的信息,即上下文。这里有两个寄存器的值的赋值有些特殊,就是 ss0 和 esp0,这个表示 0 特权级也就是内核态时的 ss:esp 的指向。根据代码我们得知,其含义是将代码在内核态时使用的堆栈栈顶指针指向进程 task_struct 所在的 4K 内存页的最顶端,而且之后的每个进程都是这样被设置的:
ch15-2

好了,进程槽位的申请,以及基本信息的复制,就讲完了,就是内存中找个地方存一个 task_struct 结构的东东,并添加到 task[] 数组里的空闲位置处,这个东东的具体字段赋值的大部分都是复制原来进程的。接下来将是进程页表和段表的复制,这将会决定进程之间的内存规划问题,很是精彩,也是 fork 真正的难点所在。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值