linux0.11操作系统源码剖析fork.c

fork() 用于创建 一个新的进程,一次调用两次返回。父进程返回子进程的PID 子进程是 0.
fork() 采用写时复制,也就是 创建的时候 就复制了页表,并没有实际的内存空间,子进程这个时候和父亲共享内存,但是子进程只有 读的权限,当修改的时候 才复制一份。
知乎大佬的nettle的回答:
linux下的fork()函数

  1. 传统的fork()系统调用直接把所有的资源复制给新创建的进程.linux的fork()使用写时拷贝(copy-on-write)页实现.写时拷贝是一种可以推迟甚至免除拷贝数据的技术,内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝. 只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝,也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读的方式共享.这种技术使地址空间上的页的拷贝被推迟到实发生写入的时候才进行.在页跟本不会被写入的情况下(比如:fork()后立即调用exec())它们就无需复制了.
  2. linux通过系统调用clone()来实现fork().然后由clone()来调用do_fork().
    附:
    linux下fork()函数的实现:
    linux通过clone()系统调用实现fork()。这个调用通过一系列的参数标志来指明父,子进程需要共享的资源。fork(),vfork()和__clone()库函数都根据各自需要的参数标志去调用clone().然后由clone()去调用do_fork(). do_frok完成了创建中的大部分工作,它的定义在ker/frok.c文件中。该函数调用copy_process()的函数,然后让进程开始运行。copy_process()函数完成的工作很有意思:
    1. 调用dup_task_struct()为新进程创建一个内核栈,thread_info结构和task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符是完全相同的。
    2. 检查新创建的这个子进程后,当前用户所拥有的进程数目没有超出给它分配的资源的限制。
    3. 现在,子进程着手使自己与父进程区别开来。进程描述符内的许多成员都要被清0或者设为初始值。进程描述符的成员值并不是继承而来的,而主要是统计信息。进程描述符中的大多数数据都是共享的.
    4. 接下来,子进程的状态被设置为TASK_UNINTERRUPTIBLE以保证它不会投入运行。
    5. copy_process()调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV的标志被清0.表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。
    6. 调用get_pid()为新进程获取一个有效的PID。
    7. 根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件,文件系统信息,信号处理函数,进程地址空间和命名空间等。再一半情况下,这些资源会被给定进程的所有线程共享;否则,这些资源对每个进程是不同的,因此被拷贝到了这里。
    8. 让父进程和子进程平分剩余的时间片。
    9. 最后,copy_process()做扫尾工作并返回一个指向子进程的指针。 再回到do_fork()函数,如果copy_process()函数返回成功,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行。因为一半子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能会开始向地址空间写入。

可以看看这篇大佬的博客

对于.Linux 2.6.38 下的 fork() ,vfork(),clone

asmlinkage int sys_fork(unsigned long r4, unsigned long r5,
			unsigned long r6, unsigned long r7,
			struct pt_regs __regs)
{
#ifdef CONFIG_MMU
	struct pt_regs *regs = RELOC_HIDE(&__regs, 0);
	return do_fork(SIGCHLD, regs->regs[15], regs, 0, NULL, NULL);
#else
	/* fork almost works, enough to trick you into looking elsewhere :-( */
	return -EINVAL;
#endif
}
asmlinkage int sys_vfork(unsigned long r4, unsigned long r5,
			 unsigned long r6, unsigned long r7,
			 struct pt_regs __regs)
{
	struct pt_regs *regs = RELOC_HIDE(&__regs, 0);
	return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->regs[15], regs,
		       0, NULL, NULL);
}

asmlinkage int sys_clone(unsigned long clone_flags, unsigned long newsp,
			 unsigned long parent_tidptr,
			 unsigned long child_tidptr,
			 struct pt_regs __regs)
{
	struct pt_regs *regs = RELOC_HIDE(&__regs, 0);
	if (!newsp)
		newsp = regs->regs[15];
	return do_fork(clone_flags, newsp, regs, 0,
			(int __user *)parent_tidptr,
			(int __user *)child_tidptr);
}

LINUX0.11 fork.c源码

/*
 *  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;
进程空间区域写前验证函数。
//对于80386CPU,在执行特权级0代码时不会理会用户空间中的页面是否是页保护的,因此
//在执行内核代码时用户空间中数据页面保护标志起不了作用,写时复制机制也就失去了作用。
//verify_area)函数就用于此目的。但对于80486或后来的CPU,其控制寄存器CRO中有一个
//写保护标志WP(位16),内核可以通过设置该标志来禁止特权级0的代码向用户空间只读
//页面执行写数据,否则将导致发生写保护异常。从而486以上CPU可以通过设置该标志来达
//到本函数的目的。
//该函数对当前进程逻辑地址从addr到addr+size这一段范围以页为单位执行写操作前
//的检测操作。由于检测判断是以页面为单位进行操作,因此程序首先需要找出addr所在页
//面开始地址start,然后start加上进程数据段基址,使这个start变换成CPU4G线性空
//间中的地址。最后循环调用write_verify()对指定大小的内存空间进行写前验证。若页面
//是只读的,则执行共享检验和复制页面操作(写时复制)。

void verify_area(void * addr,int size)
{
	unsigned long start;
	//调整start 为 所在页左边界 开始位置,并 更改size
	//简单来说 就是设置成 4K 的倍数,然后每次循环验证
	start = (unsigned long) addr;
	size += start & 0xfff;
	//低12位置零
	start &= 0xfffff000;
	//加上数据段在线性空间中的起始基址
	start += get_base(current->ldt[2]);
	while (size>0) {
	    //每次变 4096=4K
		size -= 4096;
		write_verify(start);
		start += 4096;
	}
}

//复制内存页表。
//参数nr是新任务号;p是新任务数据结构指针。该函数为新任务在线性地址空间中设置代码
//段和数据段基址、限长,并复制页表。由于Linux系统采用了写时复制(copy on write)
//技术,因此这里仅为新进程设置自己的页目录表项和页表项,而没有实际为新进程分配物理
// 内存页面。此时新进程与其父进程共享所有内存页面。操作成功返回0,否则返回出错号。
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;
//首先取当前进程局部描述符表中代码段描述符和数据段描述符项中的段限长(字节数)。
//0x0f是代码段选择符;0x17是数据段选择符。然后取当前进程代码段和数据段在线性地址
// 空间中的基地址。由于Linux0.11内核还不支持代码和数据段分立的情况,因此这里需要
//检查代码段和数据段基址和限长是否都分别相同。否则内核显示出错信息,并停止运行。
//get_limit()和get base()定义在include/linux/sched.h第226行处。
	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");
//然后设置创建中的新进程在线性地址空间中的基地址等于(64MB*其任务号),并用该值
//设置新进程局部描述符表中段描述符中的基地址。接着设置新进程的页目录表项和页表项,
//即复制当前进程(父进程)的页目录表项和页表项。
//正常情况下copy_page_tables)返回0,否则表示出错,则释放刚申请的页表项。
	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.
 */
/*0K,下面是主要的fork子程序。它复制系统进程信息(task[n])
*并且设置必要的寄存器。它还整个地复制数据段。
 * */
//复制进程。
//该函数的参数是进入系统调用中断处理过程(system_call.s)开始,直到调用本系统调用处理
//过程(systemcall.s第208行)和调用本函数前时(system_call.s第217行)逐步压入栈的
//各寄存器的值。这些在system_cal1.s程序中逐步压入栈的值(参数)包括:
//①CPU执行中断指令压入的用户栈地址ss和esp、标志寄存器eflags和返回地址cs和eip;
//②第83-88行在刚进入system_cal1时压入栈的段寄存器ds、es、fs和edx、ecx、ebx;
//③第94行调用syscall_table中sys fork函数时压入栈的返回地址(用参数none表示);
//④第212--216行在调用copy process()之前压入栈的gs、esi、edi、ebp和eax(nr)值。
//其中参数nr是调用find empty 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;

	*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;


    //再修改任务状态段TSS数据。由于系统给任务结构p分配了1页新
//内存,所以(PAGE_SIZE+(1ong)p)让esp0正好指向该页顶端。ss0:esp0用作程序
	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;
    //如果当前任务使用了协处理器,就保存其上下文。汇编指令clts用于清除控制寄存器CRO
//中的任务已交换(TS)标志。每当发生任务切换,CPU都会设置该标志。该标志用于管理
//数学协处理器:如果该标志置位,那么每个ESC指令都会被捕获(异常7)。如果协处理
//器存在标志MP也同时置位的话,那么WAIT指令也会捕获。因此,如果任务切换发生在一//个ESC指令开始执行之后,则协处理器中的内容就可能需要在执行新的ESC指令之前保存
//起来。捕获处理句柄会保存协处理器的内容并复位TS标志。指令fnsave用于把协处理器
//的所有状态保存到目的操作数指定的内存区域中(tss.i387)。
	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++;
    //随后在GDT表中设置新任务TSS段和LDT段描述符项。这两个段的限长均被设置成104
//字节。set tss desc()和set 1dt desc()的定义参见include/asm/system.h文件
//52-66行代码。“gdt+(nr<<1)+FIRST_TSS ENTRY”是任务nr的TSS描述符项在全局
//表中的地址。因为每个任务占用GDT表中2项,因此上式中要包括’(nr<<1)'。//程序然后把新进程设置成就绪态。另外在任务切换时,任务寄存器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;
}
//为新进程取得不重复的进程号last pid。函数返回在任务数组中的任务号(数组项)。
int find_empty_process(void)
{
	int i;
//首先获取新的进程号。如果last_pid增1后超出进程号的正数表示范围,则重新从1开始
//使用pid号。然后在任务数组中搜索刚设置的pid号是否已经被任何任务使用。如果是则
//跳转到函数开始处重新获得一个pid号。接着在任务数组中为新任务寻找一个空闲项,并
//返回项号。last pid是一个全局变量,不用返回。如果此时任务数组中64个项已经被全
//部占用,则返回出错码。
	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;
}

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值