linux0.11源码分析-fork进程

1,011 阅读7分钟

fork函数先从当前任务表(task)里找到一个任务号(进程pid),如果可以找到,就会复制当前进程current结构体的数据(task_struct),然后复制进程页表项,将RW置位0,为以后写时复制做准备。子进程与父进程共享内存。然后处理信号。切换进程后,CPU会自动的加载每个 task_struct中的TSS数据,并且保存前一个进程的CPU状态到TSS中。进程fork后,就等着调度了。注意,子进程在初始化的时候往eax寄存器中存进去了0,eax用于函数返回值。也就是说子进程会返回0,而父进程会返回自己的pid。

fork函数的定义

init/main.c

static inline _syscall0(int,fork)

include/unistd.h

#define __NR_fork	2


#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 fork(void)
{
long __res;
__asm__ volatile ("int $0x80" \
	: "=a" (__res) \
	: "0" (__NR_fork)); \
if (__res >= 0) \
	return (int) __res; \
errno = -__res; \
return -1; \
}

"0" (__NR_fork))表示表示沿用上一次的约束,就是把__NR_fork的值2放到eax寄存器中。"=a" (__res)表示的是__res与eax绑定。函数的返回结果会在eax里面,也就正在__res里。

使用int 0x80 中断后就会调用system_call函数,然会system_call会根据传递进来的函数索引从系统调用表sys_call_table中找到对应的函数,从而执行。

kernel/sched.c 在sched.c文件中设置了0x80号中断的处理程序。


void sched_init(void)
{
	...
	set_system_gate(0x80,&system_call);
	...
}

include/linux/sys.h

fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork,... };

nr_system_calls = 72 # Linux 0.11 版本内核中的系统共调用总数。

system_call:
	cmpl $nr_system_calls-1,%eax    # 调用号如果超出范围的话就在eax中置-1并退出
	ja bad_sys_call                 # 中断返回,返回-1
	push %ds                        # 保存原段寄存器值
	push %es
	push %fs
	
        pushl %edx              # ebx,ecx,edx 中放着系统调用相应的C 语言函数的调用参数。
	pushl %ecx		
	pushl %ebx		
    
	movl $0x10,%edx		# 0x10即0001 0000 选择子,进入内核空间。
	mov %dx,%ds             
	mov %dx,%es
    
	movl $0x17,%edx		#  0001 0111,RPL=3 ,TI=1,index=2,表示LDT中的用户数据段
	mov %dx,%fs

	call sys_call_table(,%eax,4)        # 这里就是调用sys_fork函数
	pushl %eax                          # 把系统调用返回值入栈
    
# 下面几行查看当前任务的运行状态。如果不在就绪状态(state != 0)就去执行调度程序。如果该
# 任务在就绪状态,但其时间片已用完(counter = 0),则也去执行调度程序。例如当后台进程组中的
# 进程执行控制终端读写操作时,那么默认条件下该后台进程组所有进程会收到SIGTTIN或SIGTTOU
# 信号,导致进程组中所有进程处于停止状态。而当前进程则会立刻返回。
	movl current,%eax       # 取当前任务(进程)数据结构地址→eax
	cmpl $0,state(%eax)	# state,如果不在就绪状态(state != 0)就去执行调度程序,则重新执行调度程序
	jne reschedule
	cmpl $0,counter(%eax)		# counter,时间片已用完,则重新执行调度程序
	je reschedule
    
    
#对信号进行识别处理
...
.align 2
sys_fork:
	call find_empty_process
	testl %eax,%eax             # 在eax中返回进程号pid。若返回负数则退出。
	js 1f     #向前跳转到1处,就是返回
	push %gs
	pushl %esi
	pushl %edi
	pushl %ebp
	pushl %eax
	call copy_process
	addl $20,%esp               # 丢弃这里所有压栈内容。
1:	ret

kernel/sched.c

struct task_struct * task[NR_TASKS] = {&(init_task.task), }; // 定义任务指针数组  NR_TASKS =64

find_empty_process从全局的task任务表里找到一个数组索引,即进程号。

kernel/fork.c


long last_pid=0; 


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;
}
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;	/* 当前进程的结构数据复制给p */
    
	p->state = TASK_UNINTERRUPTIBLE;  // 先将进程的状态置为不可中断等待状态,以防止内核调度其执行
	p->pid = last_pid;              // 新进程号。
	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;        // 进程开始运行时间(当前时间滴答数)
	p->tss.back_link = 0;
	p->tss.esp0 = PAGE_SIZE + (long) p;     // 任务内核态栈指针。由于系统给任务结构p分配了1页新内存,所以(PAGE_SIZE+(long)p)让esp0正好指向该页顶端
	p->tss.ss0 = 0x10;                      // 内核态栈的段选择符(与内核数据段相同)
	p->tss.eip = eip;                       // 指令代码指针
	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;                // 段寄存器仅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);                  // 任务局部表描述符的选择符(LDT描述符在GDT中)
	p->tss.trace_bitmap = 0x80000000;       // 高16位有效
    
	if (last_task_used_math == current)
		__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
        
    // 接下来复制进程页表。即在线性地址空间中设置新任务代码段和数据段描述符中的基址和限长,
    // 并复制页表。如果出错(返回值不是0),则复位任务数组中相应项并释放为该新任务分配的用于
    // 任务结构的内存页。
	if (copy_mem(nr,p)) {
		task[nr] = NULL;
		free_page((long) p);
		return -EAGAIN;
	}
    
    // 如果父进程中有文件是打开的,则将对应文件的打开次数增1,因为这里创建的子进程会与父
    // 进程共享这些打开的文件。将当前进程(父进程)的pwd,root和executable引用次数均增1.
    // 与上面同样的道理,子进程也引用了这些i节点。
	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;	/* 任务已经准备好,等待调度 */
	return last_pid;
}

get_free_page的函数分析这里有

由于Linux系统采用了写时复制(copy on write)技术,copy_mem仅为新进程设置自己的页目录表项和页表项,而没有实际为新进程分配物理内存页面。此时新进程与其父进程共享所有内存页面。 系统设置全局描述符表GDT中的分段描述符项数最大为256,其中2项空闲,2项系统使用,每个进程使用两项。因此,此时系统可以最多容纳(256-4)/2 +1 =127个任务,并且虚拟地址范围是((256-4)/2)*64MB =4G。 4G正好与CPU的线性地址空间范围或物理地址空间范围相同,因此在0.11内核中比较容易混淆三种地址概念, 从Linux内核0.99版以后,对内存空间的使用方式发生了变化。每个进程可以单独享用整个4G的地址空间范围。

kernel/fork.c

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 即 0000 1111 ,表示RPL=3 TI=1(在LDT中),索引1,就是代码段
	data_limit=get_limit(0x17); // 0x17 即 0000 10111,表示RPL=3 TI=1(在LDT中),索引2,就是数据段
	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; //新基址=任务号*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)) {
		printk("free_page_tables: from copy_mem\n");
		free_page_tables(new_data_base,data_limit);
		return -ENOMEM;
	}
	return 0;
}

include/linux/sched.h

#define get_limit(segment) ({ \
unsigned long __limit; \
__asm__("lsll %1,%0\n\tincl %0":"=r" (__limit):"r" (segment)); \
__limit;})

#define set_base(ldt,base) _set_base( ((char *)&(ldt)) , (base) )
#define get_base(ldt) _get_base( ((char *)&(ldt)) )

#define _set_base(addr,base)  \
__asm__ ("push %%edx\n\t" \
	"movw %%dx,%0\n\t" \
	"rorl $16,%%edx\n\t" \
	"movb %%dl,%1\n\t" \
	"movb %%dh,%2\n\t" \
	"pop %%edx" \
	::"m" (*((addr)+2)), \
	 "m" (*((addr)+4)), \
	 "m" (*((addr)+7)), \
	 "d" (base) \
	)
    

// 从地址addr 处段描述符中取段基地址
static inline unsigned long _get_base(char * addr)
{
         unsigned long __base;
         __asm__("movb %3,%%dh\n\t"  
                 "movb %2,%%dl\n\t"
                 "shll $16,%%edx\n\t"
                 "movw %1,%%dx"
                 :"=&d" (__base)
                 :"m" (*((addr)+2)),
                  "m" (*((addr)+4)),
                  "m" (*((addr)+7)));
         return __base;
}


指令lsl 是Load Segment Limit 缩写。它从指定选择子中取出分散的限长比特位拼成完整的段限长值放入指定寄存器中。所得的段限长是实际字节数减1,因此这里还需要加1 后才返回。

movb %3,%%dh 取[addr+7]处基址高16位的高8位(位31-24) -> dh

"movb %2,%%dl 取[addr+4]处基址高16 位的低8 位(位23-16)-> dh

shll $16,%%edx 基地址高16 位移到edx 中高16 位处

movw %1,%%dx取[addr+2]处基址低16 位(位15-0)-> dx

mm/memory.c

int copy_page_tables(unsigned long from,unsigned long to,long size)
{
	unsigned long * from_page_table;
	unsigned long * to_page_table;
	unsigned long this_page;
	unsigned long * from_dir, * to_dir;
	unsigned long nr;

  
	if ((from&0x3fffff) || (to&0x3fffff)) // 4MB边界上,因为一个页可以管理4MB的内存
		panic("copy_page_tables called with wrong alignment");
        
        //目录项指针
	from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
	to_dir = (unsigned long *) ((to>>20) & 0xffc);
       
       //计算要复制的内存块占用的页表数(也即目录项数)
	size = ((unsigned) (size+0x3fffff)) >> 22;
        
        //对每个占用的页表依次进行复制操作
	for( ; size-->0 ; from_dir++,to_dir++) {
		if (1 & *to_dir) //如果目的目录项指定的页表已经存在(P=1),则出错
			panic("copy_page_tables: already exist");
		if (!(1 & *from_dir))  //如果此源目录项未被使用,则不用复制对应页表,跳过
			continue;
               
               //取当前源目录项中页表的地址 -> from_page_table
		from_page_table = (unsigned long *) (0xfffff000 & *from_dir); 
        
		if (!(to_page_table = (unsigned long *) get_free_page()))
			return -1;	/* Out of memory, see freeing */
       
                //设置目的目录项信息。7 是标志信息,表示(Usr, R/W, Present)
		*to_dir = ((unsigned long) to_page_table) | 7; 
                //针对当前处理的页表,设置需复制的页面数。如果是在内核空间,则仅需复制头160 页(640KB),否则需要复制一个页表中的所有1024 页面
		nr = (from==0)?0xA0:1024;
         
                //对于当前页表,开始复制指定数目nr 个内存页面。
		for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
			this_page = *from_page_table;
			if (!(1 & this_page)) //如果当前源页面没有使用,则不用复制
				continue;
			this_page &= ~2;  //复位页表项中R/W 标志(置0)。写时复制
                       
                       //将该页表项复制到目的页表中
			*to_page_table = this_page;
          
                       //如果该页表项所指页面的地址在1MB 以上,则需要设置内存页面映射数组mem_map[],于是计算
                       //页面号,并以它为索引在页面映射数组相应项中增加引用次数。而对于位于1MB 以下的页面,说明
                       //是内核页面,因此不需要对mem_map[]进行设置。因为mem_map[]仅用于管理主内存区中的页面使用情况
			if (this_page > LOW_MEM) {
				*from_page_table = this_page; // 令源页表项也只读
				this_page -= LOW_MEM;
				this_page >>= 12;
				mem_map[this_page]++;
			}
		}
	}
	invalidate(); //刷新页变换高速缓冲
	return 0;
}