fork剖析(二)精益求精方显英雄本色

static task_t *copy_process(unsigned long clone_flags,
				 unsigned long stack_start,
				 struct pt_regs *regs,
				 unsigned long stack_size,
				 int __user *parent_tidptr,
				 int __user *child_tidptr,
				 int pid)
{

copy_process do_fork 的核心代码具体代码参考linux源代码具体只做主要函数的剖析

这个函数是用来产生子进程的task_struct 俗称pcb的结构体

主要函数之前是对clone_flags进行标志检查

第一个函数 p = dup_task_struct(current); current指的是父进程的thread_info->task_struct 去初始化子进程的task_struct

dup_tast_struct 中的

第一个函数时prepare_to_copy()会将current进程的寄存器保存在thread_info 中等待后面将其复制给子进程

第二个函数时tsk = alloc_task_struct();具体去分配tsk结构体的空间(从高速缓存上)一般分配两个页面总共8kb

具体分析入戏taks_struct占1kb 其余7kb都作为内核栈去处理

第三个函数ti = alloc_thread_info(tsk);分配thread_info 结构体内存(从高速缓存上)并且分配内核栈一般为8kb

*ti = *orig->thread_info;
*tsk = *orig;
tsk->thread_info = ti;
t i->task = tsk;

将current进程的thread_info赋给ti task_struct 赋给 tsk  tsk->thread_info 和 ti->task=tsk;让子进程和父进程拥有相同的进程运行环境

两个结构体中都有一个指向对方的指针;

atomic_set(&tsk->usage,2);//设置他的usage为2表示他为活动属性

dup_task_struct 函数完成 返回生成的task_struct 因为内里有个指向thread_info的指针所以不用管;

回到copy_process 函数

接下来就是对对user的几个函数

第一个函数atomic_read(&p->user->processes) 对用户的processes进行限制

第二个函数atomic_inc(&p->user->__count);增加user结构体中的个数

* 检查系统中的进程数量(nr_threads)是否超过max_threads
 max_threads的缺省值是由系统内存容量决定的。总的原则是:所有的thread_info描述符和内核栈所占用的空间
* 不能超过物理内存的1/8。不过,系统管理可以通过写/proc/sys/kernel/thread-max文件来改变这个值。
 */
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;

/*** 更新从父进程复制到tsk_flags字段中的一些标志。

* 首先清除PF_SUPERPRIV。该标志表示进程是否使用某种超用户权限。

* 然后设置PF_FORKNOEXEC标志。它表示子进程还没有发出了execve系统调用。
*/

copy_flags(clone_flags, p);复制current的clone_flaes给新的pid

/**
* 如果CLONE_PARENT_SETTID标志被设置,就将子进程的PID复制到参数parent_tidptr指向的用户态变量中。
 * xie.baoyou:想想我们常常调用的pid = fork()语句吧。
*/
if (clone_flags & CLONE_PARENT_SETTID) 子进程的pid复制到父进程的parent_tiptr;
if (put_user(p->pid, parent_tidptr))
goto bad_fork_cleanup;

初始化新pid中的东西

INIT_LIST_HEAD(&p->children);//显而易见每个进程的子进程都会属于一个进程通过链表进行连接
INIT_LIST_HEAD(&p->sibling);//所有兄弟进程都挂在在这个链表上

然后初始化自旋锁其他也进行初始化

set_task_cpu(p, smp_processor_id()); 初始化子进程的cpu字段

接下来就是几个重要的拷贝函数了

复制所有进程信息,包括文件系统、信号处理函数、信号、内存管理等
   形式类似于copy_xxx的形式 

if ((retval = copy_semundo(clone_flags, p)))//拷贝信号量
goto bad_fork_cleanup_audit;
if ((retval = copy_files(clone_flags, p)))调用copy_files函数拷贝父进程打开的文件描述符
goto bad_fork_cleanup_semundo;
if ((retval = copy_fs(clone_flags, p)))//拷贝文件系统也就是当前的pwd文件的结构信息
goto bad_fork_cleanup_files;
if ((retval = copy_sighand(clone_flags, p)))//拷贝信号处理函数
goto bad_fork_cleanup_fs;
if ((retval = copy_signal(clone_flags, p)))//拷贝信号
goto bad_fork_cleanup_sighand;
if ((retval = copy_mm(clone_flags, p)))调用copy_mm函数处理与新进程的内存问题
goto bad_fork_cleanup_signal;
if ((retval = copy_keys(clone_flags, p)))复制认证信息 if((retval=copy_keys(clone_flags,p)))
goto bad_fork_cleanup_mm;
if ((retval = copy_namespace(clone_flags, p)))//拷贝命名空间
goto bad_fork_cleanup_keys;

retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);//

/**
* 调用copy_thread,用发出clone系统调用时CPU寄存器的值(它们保存在父进程的内核栈中)
* 来初始化子进程的内核栈。不过,copy_thread把eax寄存器对应字段的值(这是fork和clone系统调用在子进程中的返回值)
* 强行置为0。子进程描述符的thread.esp字段初始化为子进程内核栈的基地址。ret_from_fork的地址存放在thread.eip中。
* 如果父进程使用IO权限位图。则子进程获取该位图的一个拷贝。
* 最后,如果CLONE_SETTLS标志被置位,则子进程获取由CLONE系统调用的参数tls指向的用户态数据结构所表示的TLS段。
*/
retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);

具体一个一个解析

先从这个函数解析

copy_mm(clone_flags, p)/**
 * 当创建一个新的进程时,内核调用copy_mm函数,
 * 这个函数通过建立新进程的所有页表和内存描述符来创建进程的地址空间。
 * 通常,每个进程都有自己的地址空间,但是轻量级进程共享同一地址空间,即允许它们对同一组页进行寻址。

通过flags判断是不是创建的是共享内存的轻量级进程

static int copy_mm(unsigned long clone_flags, struct task_struct * tsk)
{
	struct mm_struct * mm, *oldmm;
	int retval;

	tsk->min_flt = tsk->maj_flt = 0;
	tsk->nvcsw = tsk->nivcsw = 0;

	tsk->mm = NULL;
	tsk->active_mm = NULL;

	/*
	 * Are we cloning a kernel thread?
	 *
	 * We need to steal a active VM for that..
	 */
	oldmm = current->mm;
	/**
	 * 内核线程??
	 */
	if (!oldmm)
		return 0;

	/**
	 * 指定了CLONE_VM标志,表示创建线程。
	 */
	if (clone_flags & CLONE_VM) {
		/**
		 * 新线程共享父进程的地址空间,所以需要将mm_users加一。
		 */
		atomic_inc(&oldmm->mm_users);
		mm = oldmm;
		/*
		 * There are cases where the PTL is held to ensure no
		 * new threads start up in user mode using an mm, which
		 * allows optimizing out ipis; the tlb_gather_mmu code
		 * is an example.
		 */
		/**
		 * 如果其他CPU持有进程页表自旋锁,就通过spin_unlock_wait保证在释放锁前,缺页处理程序不会结果。
		 * 实际上,这个锁除了保护页表,还必须禁止创建新的轻量级进程。因为它们共享mm描述符
		 */
		spin_unlock_wait(&oldmm->page_table_lock);
		/**
		 * 在good_mm中,将父进程的地址空间赋给子进程。
		 * 注意前面对mm的赋值,表示了新线程使用的mm
		 * 完了,就这么简单
		 */
		goto good_mm;
	}

	/**
	 * 没有CLONE_VM标志,就必须创建一个新的地址空间。
	 * 必须要有地址空间,即使此时并没有分配内存。
	 */
	retval = -ENOMEM;
	/**
	 * 分配一个新的内存描述符。把它的地址存放在新进程的mm中。
	 */
	mm = allocate_mm();
	if (!mm)
		goto fail_nomem;

	/* Copy the current MM stuff.. */
	/**
	 * 并从当前进程复制mm的内容。
	 */
	memcpy(mm, oldmm, sizeof(*mm));
	if (!mm_init(mm))
		goto fail_nomem;

	/**
	 * 调用依赖于体系结构的init_new_context。
	 * 对于80X86来说,该函数检查当前进程是否有定制的局部描述符表。
	 * 如果有,就复制一份局部描述符表并把它插入tsk的地址空间
	 */
	if (init_new_context(tsk,mm))
		goto fail_nocontext;

	/**
	 * dup_mmap不但复制了线程区和页表,也设置了mm的一些属性.
	 * 它也会改变父进程的私有,可写的页为只读的,以使写时复制机制生效。
	 */
	retval = dup_mmap(mm, oldmm);
	if (retval)
		goto free_pt;

	mm->hiwater_rss = mm->rss;
	mm->hiwater_vm = mm->total_vm;

good_mm:
	tsk->mm = mm;
	tsk->active_mm = mm;
	return 0;

free_pt:
	mmput(mm);
fail_nomem:
	return retval;

fail_nocontext:
	/*
	 * If init_new_context() failed, we cannot use mmput() to free the mm
	 * because it calls destroy_context()
	 */
	mm_free_pgd(mm);
free_mm(mm);
	return retval;
}

接下来的函数在看看

主要分析这个函数//实际上复制父进程的系统空间堆栈 

	retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);


int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
	unsigned long unused,
	struct task_struct * p, struct pt_regs * regs)
{
	struct pt_regs * childregs;
	struct task_struct *tsk;
	int err;

	childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p->thread_info)) - 1;//
	*childregs = *regs;
	childregs->eax = 0;//将当前的寄存器设置为eax设置为0所以最后fork返回的是0
	childregs->esp = esp;

	p->thread.esp = (unsigned long) childregs;
	p->thread.esp0 = (unsigned long) (childregs+1);

	p->thread.eip = (unsigned long) ret_from_fork;

	savesegment(fs,p->thread.fs);
	savesegment(gs,p->thread.gs);

	tsk = current;
	if (unlikely(NULL != tsk->thread.io_bitmap_ptr)) {
		p->thread.io_bitmap_ptr = kmalloc(IO_BITMAP_BYTES, GFP_KERNEL);
		if (!p->thread.io_bitmap_ptr) {
			p->thread.io_bitmap_max = 0;
			return -ENOMEM;
		}
		memcpy(p->thread.io_bitmap_ptr, tsk->thread.io_bitmap_ptr,
			IO_BITMAP_BYTES);
	}

	/*
	 * Set a new TLS for the child thread?
	 */
	if (clone_flags & CLONE_SETTLS) {
		struct desc_struct *desc;
		struct user_desc info;
		int idx;

		err = -EFAULT;
		if (copy_from_user(&info, (void __user *)childregs->esi, sizeof(info)))
			goto out;
		err = -EINVAL;
		if (LDT_empty(&info))
			goto out;

		idx = info.entry_number;
		if (idx < GDT_ENTRY_TLS_MIN || idx > GDT_ENTRY_TLS_MAX)
			goto out;

		desc = p->thread.tls_array + idx - GDT_ENTRY_TLS_MIN;
		desc->a = LDT_entry_a(&info);
		desc->b = LDT_entry_b(&info);
	}

	err = 0;
 out:
	if (err && p->thread.io_bitmap_ptr) {
		kfree(p->thread.io_bitmap_ptr);
		p->thread.io_bitmap_max = 0;
	}
	return err;
}

其他函数不做解释后边在进行剖析:

sched_fork(p);

调用sched_fork完成对新进程调度程序数据结构的初始化。
该函数把新进程的状态置为TASK_RUNNING,并把thread_info结构的preempt_count字段设置为1,
从而禁止抢占。
 此外,为了保证公平调度,父子进程共享父进程的时间片。fork完之后先共享父进程的时间片

 *set_task_cpu(p, smp_processor_id()); 初始化子线程的cpu字段。

void fastcall sched_fork(task_t *p)
{
	/*
	 * We mark the process as running here, but have not actually
	 * inserted it onto the runqueue yet. This guarantees that
	 * nobody will actually run it, and a signal or other external
	 * event cannot wake it up and insert it on the runqueue either.
	 */
	p->state = TASK_RUNNING;//将子进程打的state设置为TASK_RUNGING
	INIT_LIST_HEAD(&p->run_list);
	p->array = NULL;
	spin_lock_init(&p->switch_lock);
#ifdef CONFIG_SCHEDSTATS
	memset(&p->sched_info, 0, sizeof(p->sched_info));
#endif
#ifdef CONFIG_PREEMPT
	/*
	 * During context-switch we hold precisely one spinlock, which
	 * schedule_tail drops. (in the common case it's this_rq()->lock,
	 * but it also can be p->switch_lock.) So we compensate with a count
	 * of 1. Also, we want to start with kernel preemption disabled.
	 */
	p->thread_info->preempt_count = 1;
#endif
	/*
	 * Share the timeslice between parent and child, thus the
	 * total amount of pending timeslices in the system doesn't change,
	 * resulting in more scheduling fairness.
	 */
	local_irq_disable();
	p->time_slice = (current->time_slice + 1) >> 1;
	/*
	 * The remainder of the first timeslice might be recovered by
	 * the parent if the child exits early enough.
	 */
	/**
	 * 因为子进程没有用完它的时间片(现在还没有运行,并且子进程的时间片总是大于0,当然就没有用完了)
	 * 这样,设置first_time_slice,如果子进程在它的第一个时间片内终止或者执行新的程序,就把子进程的剩余时间奖励给父进程。
	 */
	p->first_time_slice = 1;
	current->time_slice >>= 1;
	p->timestamp = sched_clock();
	/**
	 * 如果当前进程的时间片为1,那么这一个tick将被分给子进程,而当前进程的时间片变成1.
	 */
	if (unlikely(!current->time_slice)) {
		/*
		 * This case is rare, it happens when the parent has only
		 * a single jiffy left from its timeslice. Taking the
		 * runqueue lock is not a problem.
		 */
		/**
		 * 强制将time_slice设置成1,然后调用scheduler_tick递减为0.
		 */
		current->time_slice = 1;
		preempt_disable();
		scheduler_tick();
		local_irq_enable();
		preempt_enable();
	} else
		local_irq_enable();
}







  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值