《Linux内核设计的艺术》总结: 进程1的创建

来源:互联网 发布:python的idle打不开 编辑:程序博客网 时间:2024/05/22 06:19
现在操作系统已经有了第一个用户进程0,进程0是一个idle进程,现在它要去创建进程1
1. 通过系统调用fork创建进程1.
if (!fork()) {/* we count on this going ok */init();}for(;;) pause();


fork系统调用会创建子进程,并和父进程共享代码段,它有两个返回值。在父进程中返回子进程的pid,每次创建一个进程系统会将全局变量last_pid加1并将它作为新的进程的pid,而在子进程中则返回0。不同的返回值可以控制父子进程不同的执行路径。所以父进程在调用fork之后会无限循环执行pause,而子进程则去执行init函数。
    fork的系统调用展开后如下所示:
int fork(){ long __res; __asm__ volatile ("int $0x80" : "=a" (__res) : "0" (__NR_##name)); if (__res >= 0) return (type) __res; errno = -__res; return -1; }

1.1 fork通过int 0x80产生一个中断,跳转到内核态执行内核代码,并将__NR_FORK放到eax寄存器中,内核会调到0x80对应的系统调用总入口函数_system_call,并根据eax去执行具体的系统调用代码。前面说到中断会切换堆栈为进程0的内核栈,并自动的将寄存器SS(用户栈栈底)、ESP(用户栈栈顶)、EFLAGS(标志位)、CS(代码段)、EIP(下条指令的地址,现在是“if (_res >= 0)”,这个值在后面还会用到)依次入栈,所以现在进程0的内核栈中有5个值。
1.2 _system_call是一段汇编程序,它首先会依次将ds、es、fs、edx、ecx、ebx6个寄存器值入栈,所以现在进程0的内核栈中有11个值。接下来调用call _sys_call_table(,%eax,4)。该语句去调用_sys_call_table数组中对应eax的项_sys_fork,因为本身也是一个函数调用,会将eip压栈,所以现在进程0的内核栈中有12个值。
1.3 _sys_fork首先call _find_empty_process,该调用是从内核的struct task_struct*类型的task数组中找到一个可用项来存储新进程的task_struct,现在task中第0项是进程0,其他均为NULL,所以会选择第1项存储新进程的task_struct地址,并将last_pid加1变为1。
1.4 接下来_sys_fork依次吧gs、esi、edi、ebp、eax五个寄存器入栈,现在进程0的内核栈中有17个值,如下所示:

这17个值都会被用作调用_copy_process时的参数值。
1.5 _sys_fork继续调用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();//申请页面作为新的进程的task_struct和内核栈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;//设置子进程的pidp->father = current->pid;//设置子进程的父进程p->counter = p->priority;//counter表示时间片大小p->signal = 0;//signal表示当前进程的信号标志位,清空p->alarm = 0;//报警定时值p->leader = 0;//会话组长p->utime = p->stime = 0;//用户态和内核态的运行时间p->cutime = p->cstime = 0;//子进程用户态和内核态的运行时间p->start_time = jiffies;//启动时间p->tss.back_link = 0;//接下来设置进程的tss结构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;//置为就绪态,可以准备调度return last_pid;//将子进程的pid返回}

copy_progress首先通过get_free_page申请一个内存页面,页面头部低地址是task_struct结构,高地址是栈顶,并向下增长。get_free_page的申请原则是从高地址到低地址遍历页面管理数组mem_map,找到第一个页面计数为0的页面,由于现在主内存还未被使用,16MB内存空间的最后一个页面会被作为新的页面。

注:2.6内核以前task_struct和内核栈共用一个或两个页面,在之后的版本里,两者分开了,并引入了新的结构struct thread_info取代task_struct的位置:新版本在dup_task_struct函数中,task_struct首先通过slab分配1K多内存,然后再通过slab分配一个内核栈大小的内存,和之前版本的结构类似,内存底部刚好是thread_info的首地址,内存末端当做栈顶,向下增长,thread_info有成员指向task_struct,task_struct中也有成员指向内核栈,如下图所示:


copy_process函数接下来对子进程做了一些初始化,如:
p->tss.esp0 = PAGE_SIZE + (long) p;
就是把子进程的内核栈栈顶设置为刚刚申请的内存页面的地址最高端处,堆栈向下增长。每次进程从用户态切换到内核态都是用进程程序控制块的tss中的esp0字段设置进程在内核栈中的地址,因为esp0的值不会再被改变,每次的栈顶都是内存页面的最高端,所以说每次切换至内核态内存栈都是空的。
其中有两个最重要的语句是:
p->tss.eax = 0;p->tss.eip = eip;
这里的eip是int 0x80的下一条指令,也就是"if (_res >= 0)",设置eip可以让子进程在一开始被调度时就和父进程的执行路径一致,而把eax置为0也就是返回值置为0可以使得父子进程不同的返回值(父进程为子进程的pid,子进程为0),从而有不同的执行路径。
    copy_process调用copy_mem设置进程1的分页管理:设置进程1的线性地址空间中的代码段、数据段;为进程1创建第一个页表并设置对应的页目录项。

Linux0.11中,最多只有64个进程,task数组大小也定义成了64,每个进程与一个task数组中的项一一对应。Linux0.11内存物理地址0处开始放着一页页目录表和四页页表。这是系统的唯一的一个页目录表,被所有进程共享,其后的四页页表正好映射16M物理内存,是内核和进程0的页表。任务号为nr的进程,对应页目录的第nr*16至nr*16+15一共16个目录项,代码段线性基址是nr*64MB,段长64MB。这样4G的线性空间由64个进程共享,每个进程分到64M。比如进程3,它在页目录中占有的页目录项就是48-63项,线性地址空间是192MB-256MB:

注:进程寻址都是通过线性地址映射到物理地址。逻辑地址是分段式的形式(段基址:段偏移),在不同机器上用相同的编译器生成的逻辑地址相同;线性地址是分页式的地址。一个程序代码如果有两个进程,那么它们的逻辑地址是相同的,而由于段基址的不同生成的线性地址不同;即使生成相同的线性地址,因为不同的任务有不同的页目录表和页表,真正的物理地址也不相同,所以不会造成内存访问冲突。
copy_process函数然后子进程共享父进程的文件目录等信息,共享的本质就是将对应资源的计数器加1。
copy_process函数设置进程1在GDT中的表项。
copy_process函数将进程置为就绪态,这时候进程1就可以参与任务调度了。
copy_process函数最后将子进程的pid返回,这时候还是进程0在执行,它最终会返回main中,
2. 进程0进入for循环,通过pause再次进行系统调用。
2.1 pause系统调用最终会进入sys_pause中,将自身置为可中断状态并执行schedule进行任务调度。由于现在只有进程1是就绪态,系统会开始执行进程1,注意此时还处在系统调用模式中。
2.2 进程1的最开始执行地址就是前面说的的eip,也就是"if (_res >= 0)",由于进程1的eax被设置为0,在main中的fork返回值为0,进程1和进程0执行不同的代码路径----调用init函数。
1 0
原创粉丝点击