您的位置:首页 > 运维架构 > Linux

对linux 0.11版本内核中进程创建fork()的理解

2014-05-14 13:41 627 查看
        首先来看一下fork的系统调用,源码如下:

_sys_fork:
call _find_empty_process #这个函数用来取得一个pid,假如是负数直接退出
testl %eax,%eax
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call _copy_process #这个函数用来复制进程
addl $20,%esp #前面压了5个参数,每个4字节,共20字节,这里的意思就是丢弃这些压栈的值
1: ret        总的来说,看到这里我觉得大致能想到过程应该是取得一个pid,然后复制修改父进程的进程描述符,再复制页表,设置掉GDT,LDT。find_empty_process()源码如下:
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++)
if (!task[i])
return i;
return -EAGAIN;
}
        分析:其实就是last_pid不断增一,然后判断该进程号有没有被任何进程使用过,有的话重复该过程,没有的话继续执行,再找到一个空闲的task项,返回。其中last_pid是一个全局变量,记录当前能分配的pid,不用返回。copy_process()的代码比较长,就直接在代码中利用注释去解释,下面是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;

/*利用get_free_page()申请一页空间也就是4K,这块内存用来存储进程描述符,nr就是前面的函数返回的空闲的任务号,将这个任务号指向新分配的进程描述符,再将当前的任务描述符的内容复制到新分配的内存中,现在子进程的描述符与父进程的完全相同*/
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; //获得子进程的pid
p->father = current->pid; //父进程为当前进程
p->counter = p->priority; //时间片为优先级的值
p->signal = 0; //清空信号位图
p->alarm = 0; //清空报警定时
p->leader = 0; //会话的领导地位不能与父进程相同
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->tss.ss0 = 0x10; //内核态堆栈选择符,与数据段相同
p->tss.eip = eip;
p->tss.eflags = eflags;
p->tss.eax = 0; //子进程的返回值为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(nr)就是找出该任务号在GDT中的位置,也就是选择符,GDT的结构在之前的一篇博文中提到过
p->tss.trace_bitmap = 0x80000000;

if (last_task_used_math == current) //对协处理器了解较少,这里大概的意思就是父进程使用过了协处理器,就要保存上下文
__asm__("clts ; fnsave %0"::"m" (p->tss.i387));

/*接下来要调用copy_mem()函数来重新设置子进程代码段和数据段,并复制页表,具体代码后面来分析*/
if (copy_mem(nr,p)) {
task[nr] = NULL;
free_page((long) p);
return -EAGAIN;
}

/*假如文件时打开的,那该文件的系统文件打开表增一,父子进程能共享这些文件打开表,另外pwd等等这些引用都要增一*/
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++;

/*gdt的前四个分别是null,内核代码段,内核数据段,系统调用段,加上FIRST_TSS_ENTRY 就是跳过前面四个段,接下来就是任务0的TSS段和LDT段,任务1的TSS和LDT,以此类推,所以加上(nr<<1)就是指向了第nr个任务在GDT中的内容,第一句话设置了该任务的tss在GDT中的描述符,第二句设置了ldt在GDT的描述符,之后使任务变为可运行,然后退出*/
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;
}         接下来看一下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;

/*0x0f是LDT中代码段的选择符,0x17是数据段的描述符,通过选择符来找到描述符进而找到段限长*/
code_limit=get_limit(0x0f);0x0f是代码段的选择符,通过选择符
data_limit=get_limit(0x17);

/*ldt[1]指的是代码段的描述符,ldt[2]指的是数据段的描述符,通过描述符得到段基址*/
old_code_base = get_base(current->ldt[1]);
old_data_base = get_base(current->ldt[2]);

/*linux 0.11里面数据段和代码段不分开,也就是基址和限长相同,不相同就是出错了 */
if (old_data_base != old_code_base)
panic("We don't support separate I&D");
if (data_limit < code_limit)
panic("Bad data_limit");

/*0x4000000代表一个64M的空间,每个任务在线性地址空间中占据64M,其起始地址就是nr * 0x4000000,这个地址也就是子进程的段基址 */
new_data_base = new_code_base = nr * 0x4000000;
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;
}          总结下fork()的整个流程,一开始要得到一个pid和空闲的任务号,再调用copy_process来创建子进程任务结构的空间,拷贝父进程的内容到这块新申请的空间中,之后要修改进程描述符中的内容,包括pid,统计信息,信号量等,还要修改子进程的tss段指向内容的地址,包括里面的eax,内核堆栈指针,gdt中对子进程ldt的索引号。之后要设置子进程LDT中子进程代码段和数据段的基址和限长,同时拷贝父进程有关代码段数据段的页表,再对父进程中打开的文件表增一等,最后设置gdt中对子进程tss和ldt的描述符。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息