kernel源码(十五)fork.c
在system_call.s中,我们定义了_sys_fork标签
_sys_fork: call _find_empty_process //首先判断是否进程号已满.见本文后面对该函数的解释 testl %eax,%eax //eax与自身相与,为的是判断eax是否为0 js 1f //如果进程号已满,跳转到1处返回 push %gs //下面压入的值会在_copy_process中用到 pushl %esi pushl %edi pushl %ebp pushl %eax //新进程号,是find_empty_process中放入eax中的 call _copy_process 调用copy_process进行进程拷贝 addl $20,%esp //和pop指令弹出上述压入的5个值达到相同的效果 1: ret
这是创建新进程时调用的代码,这里面会调用fork.c的find_empty_process函数和copy_process函数,也就是本文将要介绍的部分。
源码
/* * linux/kernel/fork.c * * (C) 1991 Linus Torvalds */ /* * 'fork.c' contains the help-routines for the 'fork' system call * (see also system_call.s), and some misc functions ('verify_area'). * Fork is rather simple, once you get the hang of it, but the memory * management can be a bitch. See 'mm/mm.c': 'copy_page_tables()' */ #include <errno.h> #include <linux/sched.h> #include <linux/kernel.h> #include <asm/segment.h> #include <asm/system.h> extern void write_verify(unsigned long address); long last_pid=0; void verify_area(void * addr,int size) { unsigned long start; start = (unsigned long) addr; size += start & 0xfff; start &= 0xfffff000; start += get_base(current->ldt[2]); while (size>0) { size -= 4096; write_verify(start); start += 4096; } } 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"); 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; } /* * Ok, this is the main fork-routine. It copies the system process * information (task[nr]) and sets up the necessary registers. It * also copies the data segment in it's entirety. */ 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; } 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; }
进程空间区域的写前验证函数。
80386提供页级保护。在页目录表项和页表项有两个位R/W和U/S用于对页进行保护。R/W位指定该页是否可读写执行,但是R/W只在用户特权级下才起作用。因此,在请求特权级0下用户空间的代码页保护机制是不起作用的,通过fork创建的子进程的写时复制机制是不起作用的,varify_area的目的就是用来保护写时复制机制。
对80486,cr0有一个写保护标志,可以通过设置这个标志来禁止特权级的代码向用户空间只读页面执行写数据,否则就会发生一个写保护异常。80486通过这个来达到和varify_area函数相同的目的
void verify_area(void * addr,int size) { unsigned long start; start = (unsigned long) addr; //addr所在页面的开始地址 size += start & 0xfff; //0xfff为12位也就是4096字节也就是1页大小。start&0xfff表示start的低12位。这里重新计算size大小 start &= 0xfffff000; //start和0xfffff000相与,这样过滤掉不到1页的内容。上面这两行作用是重新调整start和size,以便后面按照页为单位进行内存验证 start += get_base(current->ldt[2]); //start调整成页面的边界值,get_base在其他文件中定义,后面会讲解。ldt[0]为代码段,ldt[1]为数据段 while (size>0) { size -= 4096; write_verify(start); //对start开始的一个页面进行验证,如果这个页面是不可写的,我们就复制这个页面,实现写时复制。 start += 4096; } }
复制内存页表,nr为新任务号,p是新任务的数据结构指针
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); //代码段描述符表的段限长。0x0f为段选择子,根据段选择子的结构我们可知,其代表用户段LDT,index=1.表示代码段。同样的道理0x17表示用户段LDT索引值为2,表示的是数据段 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"); new_data_base = new_code_base = nr * 0x4000000; //新的数据段基地址,新任务号*64M,即每个进程地址空间相距64M p->start_code = new_code_base; //新进程的start_code 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)) {//复制老进程的页目录表和页表到新进程中。也就是新进程共享父进程的代码段和数据段。正常情况下copy_page_table返回0,否则执行free_page_tables释放页目录项和页表项,返回出错信息 free_page_tables(new_data_base,data_limit); return -ENOMEM; } return 0; }
通过复制当前进程来创建新进程,返回新的进程号。
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, //这些参数都是进入系统调用中断处理过程(system_call.s中)逐步压入的,可参考https://www.cnblogs.com/zhenjingcool/p/16061213.html最下面介绍的系统调用过程中有关内核栈和用户栈的变化 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();//为新任务分配内存,申请一页内存(4096byte)用于存放进程的数据结构。get_free_page在memory.c中定义,用于申请一个内存页,并把内存映射表mem_map对应的这个内存页标记为已用 if (!p) return -EAGAIN; //如果分配内存失败,则返回错误码退出 task[nr] = p; //新任务的结构指针放到task数组中,nr为任务号 *p = *current; /* 当前任务的数据结构复制到刚申请的任务结构中,也就是说刚开始子进程和父进程有相同的数据 p->state = TASK_UNINTERRUPTIBLE; //下面是对复制过来的任务结构的内容进行修改,把进程状态置为不可中断的等待状态,防止内核调用它执行 p->pid = last_pid; //设置进程号pid p->father = current->pid; //设置父进程的进程号 p->counter = p->priority; //初始化进程运行时间片的值,一般为15个滴答 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; //下面是修改任务状态段(TSS)信息 p->tss.esp0 = PAGE_SIZE + (long) p; //ss0和esp0是程序在内核态执行的栈。也就是说任务数据结构和内核栈在同一个内存页中,内核栈从页顶向下生长。因为为p分配了1页内存,因此PAGE_SIZE+(long)p指向该内存页的顶端。 p->tss.ss0 = 0x10;//0001 0000,对应段选择子数据结构,LPR=0,tr=0,index=2,也就是内核态全局描述符表的第2项,是代码段。这里设置ss0被设置成内核段选择符。 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; //段寄存器仅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); //这是一个宏定义,在https://www.cnblogs.com/zhenjingcool/p/16031197.html中最后我们曾做过解释。_LDT(nr)表示的是第nr个进程ldt描述符在GDT中的地址(对照那篇博客的图看)。这里就是把GDT中本任务ldt段描述符的选择符保存在本任务的tss段中。当cpu切换任务时会自动从tss中把ldt段描述符的选择符加载到ldtr寄存器中 p->tss.trace_bitmap = 0x80000000; //高16位有效 if (last_task_used_math == current) //如果当前任务使用了协处理器 __asm__("clts ; fnsave %0"::"m" (p->tss.i387)); //清除控制寄存器cr0当中的任务已交换的标志。fnsave用于把协处理器的所有状态保存到p的tss.i387这个内存区域当中 if (copy_mem(nr,p)) {//复制进程的页表。也就是在线性地址空间当中设置新任务代码段和数据段描述符当中的基地址和限长。并且复制页表。正常情况下返回0 task[nr] = NULL; //如果出错,则复位任务数组task当中相应的项 free_page((long) p); //释放为新任务申请的内存页 return -EAGAIN; //返回出错信息 } for (i=0; i<NR_OPEN;i++) //如果父进程当中有文件是打开的 if (f=p->filp[i]) //对相应文件的打开次数增加1 f->f_count++; if (current->pwd)//把当前进程pwd、root、executable都增加1 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)); //在GDT中设置新任务的任务状态段tss的描述符项,set_tss_desc的定义在system.h当中。 set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt)); //在GDT中设置新任务的ldt的描述符项 p->state = TASK_RUNNING; /* 程序的状态置为就绪态 return last_pid; //返回新的进程号 }
其中设置tss段描述符的代码:
set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
其定义如下,其中gdt+(nr<<1)+FIRST_TSS_ENTRY表示任务nr的TSS描述符项在gdt中的地址。因为每个任务占gdt表中两项,因此nr<<1
这里还有个疑问,这里代码设置tss段描述符项和ldt描述符项到gdt中,好像gdt中的格式为[空,任务0代码段描述符,任务0数据段描述符,空,任务0的tss段描述符,任务0的ldt段描述符,任务1的tss段描述符,任务1的ldt段描述符,任务2的...]。为什么子进程只把tss和ldt段描述符放在gdt中?猜测是:子任务的代码段描述符项和数据段描述符项存放在了ldt中,需要待后续验证是否是这样
#define _set_tssldt_desc(n,addr,type) \ __asm__ ("movw $104,%1\n\t" \ //tss段和ldt段限长被设置成104字节(相比于代码段和数据段段限长64M,这里104字节(13K)显得非常小了) "movw %%ax,%2\n\t" \ "rorl $16,%%eax\n\t" \ "movb %%al,%3\n\t" \ "movb $" type ",%4\n\t" \ "movb $0x00,%5\n\t" \ "movb %%ah,%6\n\t" \ "rorl $16,%%eax" \ ::"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)), \ "m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7)) \ ) #define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89") //0x89=1000 1001.对应tss描述符中的P、DPL、TYPE位,表示0特权级tss #define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x82") //0x82=1000 0010.对应ldt描述符中的P、DPL、Type位,表示0特权级ldt
要理解上面的代码,需要对照tss和ldt的格式。可参考:https://blog.csdn.net/MJ_Lee/article/details/104419980/。
这个函数有17个参数,这些参数是从哪里来的呢?其来源有如下几个部分:
①系统调用是通过int 0x80实现的,其本质上是一个软中断,int指令会自动将5个用户态寄存器压入内核栈(原ss、原sp、原eflags、cs、eip)。和硬件中断类似,如下图黄色部分显式的寄存器。在https://www.cnblogs.com/zhenjingcool/p/15999402.html中我们介绍过硬件中断的处理过程
②在https://www.cnblogs.com/zhenjingcool/p/15999035.html的00 系统调用int 0x80这一小节,我们介绍过,int 0x80的中断处理函数是system_call.s中的_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
这里对ds、es、fs、edx、ecx、ebx进行了压栈操作,对应着copy_process函数的这几个参数。
③_sys_call中,调用了call sys_call_table(,%eax,4),对应的创建进程执行的是如下标号
_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
在这里又对gs、esi、edi、ebp、eax入栈,其中eax中存储的是find_empty_process函数返回的新进程号。
下面函数,取得一个不重复的进程号
int find_empty_process(void) { int i; repeat: if ((++last_pid)<0) last_pid=1; //如果last_pid超出进程号范围,则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; //如果没有空闲的进程号可用,则返回一个错误信息 }