Linux 内核--fork()函数创建进程
本文分析基于Linux 0.11内核,转载请表明出处http://blog.csdn.net/yming0221/archive/2011/06/05/6527337.aspx
Linux在move_to_user_mode()之后,进程0通过fork()产生子进程实际就是进程1(init进程)。
其中fork()是通过内嵌汇编的形式给出
- #define _syscall0(type,name) /
- type name(void) /
- { /
- long __res; /
- __asm__ volatile ( "int $0x80" / // 调用系统中断0x80。
- :"=a" (__res) / // 返回值??eax(__res)。
- :"" (__NR_##name)); / // 输入为系统中断调用号__NR_name。
- if (__res >= 0) / // 如果返回值>=0,则直接返回该值。
- return (type) __res; errno = -__res; / // 否则置出错号,并返回-1。
- return -1;}
这样使用int 0x80中断,调用sys_fork系统调用来创建进程。详细过程如下:
系统在sched.c中sched_init()函数最后设置系统调用中断门
set_system_gate (0x80, &system_call);
设置系统调用的中断号。
通过int 0x80调用sys_fork()
其使用汇编实现
系统将堆栈的内容入栈,然后执行call _sys_call_table(,%eax,4)
调用地址 = _sys_call_table + %eax * 4
然后真正调用sys_fork()
- _sys_fork:
- call _find_empty_process # 调用find_empty_process()(kernel/fork.c,135)。
- testl %eax,%eax
- js 1f
- push %gs
- pushl %esi
- pushl %edi
- pushl %ebp
- pushl %eax
- call _copy_process # 调用C 函数copy_process()(kernel/fork.c,68)。
- addl $20,%esp # 丢弃这里所有压栈内容。
- 1: ret
然后调用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++) // 任务0 排除在外。
- if (!task[i])
- return i;
- return -EAGAIN;
- }
该函数设置last_pid为最后可用不重复的pid号,然后返回task[]数组中空闲的项的index,存放在EAX中。
再将相应的寄存器 入栈,作为C函数的参数,调用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; // 将新任务结构指针放入任务数组中。
- // 其中nr 为任务号,由前面find_empty_process()返回。
- *p = *current; /* NOTE! this doesn't copy the supervisor stack */
- /* 注意!这样做不会复制超级用户的堆栈 */ (只复制当前进程内容)。
- p->state = TASK_UNINTERRUPTIBLE; // 将新进程的状态先置为不可中断等待状态。
- p->pid = last_pid; // 新进程号。由前面调用find_empty_process()得到。
- p->father = current->pid; // 设置父进程号。
- p->counter = p->priority;
- p->signal = 0; // 信号位图置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; // 当前滴答数时间。
- // 以下设置任务状态段TSS 所需的数据(参见列表后说明)。
- p->tss.back_link = 0;
- p->tss.esp0 = PAGE_SIZE + (long) p; // 堆栈指针(由于是给任务结构p 分配了1 页
- // 新内存,所以此时esp0 正好指向该页顶端)。
- 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; // 段寄存器仅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); // 该新任务nr 的局部描述符表选择符(LDT 的描述符在GDT 中)。
- p->tss.trace_bitmap = 0x80000000;
- // 如果当前任务使用了协处理器,就保存其上下文。
- if (last_task_used_math == current)
- __asm__ ("clts ; fnsave %0"::"m" (p->tss.i387));
- // 设置新任务的代码和数据段基址、限长并复制页表。如果出错(返回值不是0),则复位任务数组中
- // 相应项并释放为该新任务分配的内存页。
- if (copy_mem (nr, p))
- { // 返回不为0 表示出错。
- task[nr] = NULL;
- free_page ((long) p);
- return -EAGAIN;
- }
- // 如果父进程中有文件是打开的,则将对应文件的打开次数增1。
- for (i = 0; i < NR_OPEN; i++)
- if (f = p->filp[i])
- f->f_count++;
- // 将当前进程(父进程)的pwd, root 和executable 引用次数均增1。
- if (current->pwd)
- current->pwd->i_count++;
- if (current->root)
- current->root->i_count++;
- if (current->executable)
- current->executable->i_count++;
- // 在GDT 中设置新任务的TSS 和LDT 描述符项,数据从task 结构中取。
- // 在任务切换时,任务寄存器tr 由CPU 自动加载。
- 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; // 返回新进程号(与任务号是不同的)。
- }
这段代码的执行内容是:首先为进程分配内存,然后将新任务的指针放入上步查到的空闲task[]数组项中,然后复制父进程的内容后修改当前
进程的一部分属性和tss(任务状态段),最后设置新进程的代码段和数据段,限长,在GDT 中设置新任务的TSS 和LDT 描述符项,数据从task 结构中取。在任务切换时,任务寄存器tr 由CPU 自动加载。
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;
这样,新进程就创建完毕了。
copy_mem(int nr, struct task_struct *p)函数是为进程设置段基址,限长,并复制页表。下面是其代码
- // 设置新任务的代码和数据段基址、限长并复制页表。
- // 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); // 取局部描述符表中代码段描述符项中段限长。
- 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) // 0.11 版不支持代码和数据段分立的情况。
- 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; // 新基址=任务号*64Mb(任务大小)。
- 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;
- }
其中get_limit()函数是利用内嵌汇编取特定段描述符中段限长,其中用到指令lsll
- // 取段选择符segment 的段长值。
- // %0 - 存放段长值(字节数);%1 - 段选择符segment。
- #define get_limit(segment) ({ /
- unsigned long __limit; /
- __asm__( "lsll %1,%0/n/tincl %0": "=r" (__limit): "r" (segment)); /
- __limit;})
将指定描述符段的段限长返回,其中由于段限长是从0开始,所以在lsll之后需要增一。
至于ldt数据段描述符为什么是0x17,而ldt中代码段描述符是0x0f原因是段选择子的格式,一共16位,高13位表示描述符在描述符表的索引
[2]位表示这项是GDT还是LDT,0表示LDT;[1][0]表示RPL权限位。所以,0x17=0B0000 0000 0001 0111,其中10表示第二
项,0x0f=0B0000 0000 0000 1111,表示位于描述符表中的第一项。基地址在LDTR寄存器中。
get_base(addr)取描述符的中指向段的及地址,其宏定义如下:
- // 取局部描述符表中ldt 所指段描述符中的基地址。
- #define get_base(ldt) _get_base( ((char *)&(ldt)) )
- // 从地址addr 处描述符中取段基地址。功能与_set_base()正好相反。
- // edx - 存放基地址(__base);%1 - 地址addr 偏移2;%2 - 地址addr 偏移4;%3 - addr 偏移7。
- #define _get_base(addr) ({/
- unsigned long __base; /
- __asm__( "movb %3,%%dh/n/t" / // 取[addr+7]处基址高16 位的高8 位(位31-24)??dh。
- "movb %2,%%dl/n/t" / // 取[addr+4]处基址高16 位的低8 位(位23-16)??dl。
- "shll $16,%%edx/n/t" / // 基地址高16 位移到edx 中高16 位处。
- "movw %1,%%dx" / // 取[addr+2]处基址低16 位(位15-0)??dx。
- :"=d" (__base) / // 从而edx 中含有32 位的段基地址。
- :"m" (*((addr) + 2)), "m" (*((addr) + 4)), "m" (*((addr) + 7)));
- __base;
- }
- )
下图表示描述符格式:
copy_page_tables()函数复制页表,据说是内存管理中最复杂函数之一,以后研究,待续........