如何从用户态进入系统态,并执行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
调用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函数参数对应关系:
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;
}