《第二篇 linux 0.12 fork系统调用分析》


  • sys_fork函数的进入

如何从用户态进入系统态,并执行sys_call对应的功能函数,请参考《linux 0.12 系统调用(int 0x80)详解》。

通过int 0x80,CPU在执行此指令时,会读取中断号为0x80的陷阱门描述符,获取CS:EIP,并比对DPL,然后调用sys_call函数。在sys_call函数中,根据EAX中的功能号__NR_fork进入sys_fork函数。

#define __NR_fork    2

  • sys_fork的实现
调用find_empty_process,即从全局数组变量struct task_struct * task[NR_TASKS]中找空闲项,如果找到,则返回值此空闲项的下标值。此数组项,即是当前进程使用的任务结构。同时设置当前进程的进程号为last_pid。
_sys_fork:
    call _find_empty_process
    testl %eax,%eax  //eax保存的是任务数组下标值,
    js 1f
    push %gs
    pushl %esi
    pushl %edi
    pushl %ebp
    pushl %eax
    call _copy_process
    addl $20,%esp
1:    ret

  • copy_process的分析
在调用copy_process时,内核的栈与copy_process函数参数对应关系:
44(%esp)    %oldss    long ss       
40(%esp)    %oldesp    long esp     
3c(%esp)    %eflags    long eflags     
38(%esp)    %cs    long cs     
34(%esp)    %eip    long eip     
30(%esp)    %ds    long ds     
2C(%esp)    %es    long es     
28(%esp)    %fs    long fs     
24(%esp)    Old %eax    long orig_eax    -1 if not system call
20(%esp)    %edx    long edx     
1C(%esp)    %ecx    long ecx     
18(%esp)    %ebx    long ebx     
14(%esp)    %eax    long none     
10(%esp)    %gs    long gs     
 C(%esp)    %esi    long esi     
 8(%esp)    %edi    long edi     
 4(%esp)    %ebp    long ebp     
 0(%esp)    %eax    int nr     

int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
        long ebx,long ecx,long edx, long orig_eax,
        long fs,long es,long ds,
        long eip,long cs,long eflags,long esp,long ss)

//此时,CPU仍然在父进程的进程空间中执行,即将对子进程进行初始化工作,调用get_free_page获取一页物理内存,此页内存,并没有映射到进程的虚拟地址空间。然后,将子进程的任务结构指向申请到的内存页的页内偏移地址0处。堆栈指令指向物理内存页的结束处。
    p = (struct task_struct *) get_free_page(); 
    if (!p)    return -EAGAIN;
    task[nr] = p;
    *p = *current;            //复制父进程的任务数据结构。父子进程的区别在那里,决定于这个数据结构了
    p->state = TASK_UNINTERRUPTIBLE;  //设置子进程为不可中断等待状态,以防止内核调试执行,以便配置子进程任务数据结构
    p->pid = last_pid;            //设置子进程的进程ID
    p->counter = p->priority;     //设置时间片值
    p->signal = 0;        //复位新进程的信号位图、报警定时器、会话领导标志
    p->alarm = 0;
    p->leader = 0;          /* process leadership doesn't inherit */
    p->utime = p->stime = 0; //用户态及核心态时间置为0
    p->cutime = p->cstime = 0;//子进程的用户态及核心态时间置为0
    p->start_time = jiffies; //进程开始运行时间(当前的滴答数)



//TSS段初始化,被填入子进程TSS段的内容就是当前进程(父进程)从用户态进入内核态(fork系统调用)时的进程状态。使得子进程的TSS段的内容与父进程进入系统调用时是一样的。不一样的是,内核态堆栈段选择符,内核态堆栈指针,fork函数的返回值,还有子进程的LDT。
    p->tss.back_link = 0;
    p->tss.esp0 = PAGE_SIZE + (long) p;  //设置子进程的内核态堆栈指针,指向物理页面的页结束处
    p->tss.ss0 = 0x10;                       //指向内核态堆栈段的段选择符
    p->tss.eip = eip;                     //子进程的EIP同你进程一样,所以两次fork返回时,父子进程都从这条指令开始执行
    p->tss.eflags = eflags;
    p->tss.eax = 0;                        //子进程返回时的返回值,即fork系统调用子进程返回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);    //关于LDT,参考如下分析。
    p->tss.trace_bitmap = 0x80000000;
宏:#define _LDT(n) ((((unsigned long) n)<<4)+(FIRST_LDT_ENTRY<<3))
每一个进程,在GDT表中占有两项描述符,也仅占两项,一个是LDT描述符(指向本进程的LDT表),一个是TSS描述符(指向本进程的TSS段)。p->tss.ldt的内容是本进程的TSS段选项符,其值为_LDT(n)。
宏_LDT(n)的参数为任务号,分别是左移4位或3位,因此底3位(TI与RPL)为零,即此描述符是GDT中,索引值为_LDT(n)中的索引值。
因此通过p->tss.ldt即可找到本进程的LDT描述符,通过LDT描述符即可找到本进程的LDT表。

//调用copy_mem函数设置LDT表的内容及子程度的页表。
    if (copy_mem(nr,p)) {
        task[nr] = NULL;
        free_page((long) p);
        return -EAGAIN;
    }
//设置子进程在GDT表中的TSS、LDT段描述符,而LDT表中的代码段、数据段选择符中的内容,已经在copy_mem函数中填充好了。
    set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
    set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));

//注意,此时设置子进度为就绪状态,当子进程被调度时,执行的第一条指令是p->tss.eip = eip;,其返回值是p->tss.eax = 0;    就这就一次fork调用,两次返回中的子进程返回。
    p->state = TASK_RUNNING;    /* do this last, just in case */

//父进程返回,返回值是子进程ID。一次调用,两次返回,虽然执行的是相同代码,但是是在不同的进程地址空间。原因是,子进程完全复制了父进程的地址空间,使得父子进程都映射相同的物理地址空间

return last_pid;
}

4.    copy_ mem的分析
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)
        panic("We don't support separate I&D");
    if (data_limit < code_limit)
        panic("Bad data_limit");

//设置子进程的代码段、数据段基地址,当这两个基地址设置好后,后续在执行execvp加载并运行新的程序文件时,也无须重新设置,因为被加载程序的起始地址是从0开始的,当运行新程序文件时,基地址加上新程序文件的地址,仍然在本进程的虚拟地址空间中。
    new_data_base = new_code_base = nr * TASK_SIZE;
    p->start_code = new_code_base;
    set_base(p->ldt[1],new_code_base);  //设置进程的代码段基地址
    set_base(p->ldt[2],new_data_base);     //设置进程的数据段基地址
    
//为子进程申请物理内存页保存父进程的页表,此函数执行后,父子进程分别有各有独立页表,且页表内容一样,同时设置r/w为共读,因此就实现了父子进程共享代码与数据。
    if (copy_page_tables(old_data_base, new_data_base, data_limit))
{
        free_page_tables(new_data_base,data_limit);
        return -ENOMEM;
    }
    return 0;
}

你可能感兴趣的:(linux,0.12)