在上一篇里,我们分析了linux0.12进程调度的有关设计,这里我们进程生成,执行以及销毁的主要函数做一下具体分析。可以达到加深理解的作用
1.fork系统调用分析
我们分析一下典型的fork程序,如下代码:
if ((pid=fork()) ==0) {
//new process
child_do()
}else{
//parent process
parent_do()
}
我们知道,CPU中的寄存器CS:EIP控制下一条要执行的指令。CPU会在执行当前指令的同时,将下一条即将执行的指令装载到CS:EIP中。其中CS表示代码段,EIP表示(Instruction Pointer)。形象的说就是:吃着碗里看着锅里。 所有的进程或者任务都是由CPU调度,大家共享CPU时间。
我们可以看到,fork系统调用实际上是
c
static inline _syscall0(int,fork)
而系统调用实际上是int 0x80。
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name)); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
由此可见,在父任务中进行中断调用int 0x80时,被CPU装载进EIP的下一条即将执行的指令是 mov %eax, _res
。fork系统调用会创建出新的一个可以被CPU调度的子任务(task_struct),该子任务的线性地址空间为64M*nr ~64M*(nr+1),同时因为使用copy-on-write技术,子任务的线性地址空间指向父任务的物理地址,并且线性地址空间的分布和父任务如出一辙。从LIB库,环境变量和参数,用户进程栈到用户程序txt-data-bss等都一致,不一样的地方一个是子任务TSS中内核态栈指针指向自己的task_struct的顶端,另一个则是子任务TSS中eax被赋值为0. 如果子任务被CPU调度到,CPU首先会从TSS中回复子任务的上下文,包括初始化所有寄存器值,(eax=0),然后指令
mov %eax, _res
被执行,从而返回了0。于是可以知道当前执行的任务是新创建出的子任务,从而执行child_do()函数。这里这里子任务的代码段也指向父任务的代码段(通过页目录表和页表,页表映射到和父任务相同的物理地址)。
这里父任务代码陷入系统调用fork,引起了用户态-内核态的转换,用户态代码的CS:EIP,SS:ESP, EFLAGS等悉数被保存到父任务的内核栈上。 执行完sys_fork()函数,从中断返回,又从内核态切换到用户态,其中eax保留的是父任务的eax!=0,代码段是父任务当前线性空间上,于是父进程继而执行parent_do()函数。
//返回的是实际物理地址 用来存放task_struct数据
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->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; //内核态栈指针设为当前task_struct所在页的顶端
p->tss.ss0 = 0x10; //内核代码区间
p->tss.eip = eip; //调用fork()的用户态进程下一条即将执行的指令 (应该是mov %eax, _res)
p->tss.eflags = eflags; //调用fork()的用户态进程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;
2.exec系统调用分析
exec调用的过程如下
- 首先会释放掉fork时从父进程拷贝过来的旧页表并将旧的线性空间地址dir置0。
- 其次会拷贝程序的环境变量和参数(这些都会驻留到主存上,并加到内存页表中),并重新设置当前用户进程LDT的code base/limit 和data base/limit。
- 之后初始化当前进程的stack,将argc, argv **, envp**初始化,其中argv和envp指向后面的环境变量和参数区域。
- 最后,对当前进程的值包括executable, signal, brk, end_data, end_code, start_stack等初始化,将中断返回的eip改成新进程可执行文件的entry,将中断返回的ESP修改成当前任务的线性地址空间内的stack。
由于进程的CS,DS,SS等都已经设置为nr*64M,从中断返回后就将执行新文件的代码。值得注意的是,将argc, argv **, envp**初始化时,当前stack会引发页不可用的中断,内存管理程序会分配新一页数据使用。
0x80系统中断发生时内核栈
old SS
old ESP
old EFLAGS
old CS
old EIP —int iret
DS — system_call
ES
FS
EAX
EDX (char ** envp)
ECX (char ** argv)
EBX (char * filename)
next EIP
old EIP address