分析Linux內核創建一個新進程的過程


前言說明

本篇為網易雲課堂Linux內核分析課程的第六周作業,本次作業我們將具體來分析fork系統調用,來分析Linux內核創建新進程的過程


關鍵詞:fork, 系統調用進程


運行環境:*

  • Ubuntu 14.04 LTS x64
  • gcc 4.9.2
  • gdb 7.8
  • vim 7.4 with vundle

分析

分析方法說明

  • PCB包含了一個進程的重要運行信息,所以我們將圍繞在創建一個新進程時,如何來建立一個新的PCB的這一個過程來進行分析,在Linux系統中,PCB主要是存儲在一個叫做task_struct這一個結構體中,創建新進程僅能通過fork,clone,vfork等系統調用的形式來進行
  • 不管是fork,還是clonevfork,他們都是通過do_fork來創建進程
  • 接下來我將通過精簡版的do_fork代碼,和do_fork中關鍵的過程來進行分析說明

do_fork()

long do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr)
{
	struct task_struct *p; //進程描述符結構體指針
	int trace = 0;
	long nr; //總的pid數量

	/*
	 * Determine whether and which event to report to ptracer.  When
	 * called from kernel_thread or CLONE_UNTRACED is explicitly
	 * requested, no event is reported; otherwise, report if the event
	 * for the type of forking is enabled.
	 */
	if (!(clone_flags & CLONE_UNTRACED)) {
		if (clone_flags & CLONE_VFORK)
			trace = PTRACE_EVENT_VFORK;
		else if ((clone_flags & CSIGNAL) != SIGCHLD)
			trace = PTRACE_EVENT_CLONE;
		else
			trace = PTRACE_EVENT_FORK;

		if (likely(!ptrace_event_enabled(current, trace)))
			trace = 0;
	}

	// 復制進程描述符,返回創建的task_struct的指針
	p = copy_process(clone_flags, stack_start, stack_size,
			 child_tidptr, NULL, trace);
	/*
	 * Do this prior waking up the new thread - the thread pointer
	 * might get invalid after that point, if the thread exits quickly.
	 */
	if (!IS_ERR(p)) {
		struct completion vfork;
		struct pid *pid;

		trace_sched_process_fork(current, p);

		// 取出task結構體內的pid
		pid = get_task_pid(p, PIDTYPE_PID);
		nr = pid_vnr(pid);

		if (clone_flags & CLONE_PARENT_SETTID)
			put_user(nr, parent_tidptr);

		// 如果使用的是vfork,那么必須采用某種完成機制,確保父進程后運行
		if (clone_flags & CLONE_VFORK) {
			p->vfork_done = &vfork;
			init_completion(&vfork);
			get_task_struct(p);
		}

		// 將子進程添加到調度器的隊列,使得子進程有機會獲得CPU
		wake_up_new_task(p);

		/* forking complete and child started to run, tell ptracer */
		if (unlikely(trace))
			ptrace_event_pid(trace, pid);

		// 如果設置了 CLONE_VFORK 則將父進程插入等待隊列,並掛起父進程直到子進程釋放自己的內存空間
		// 保證子進程優先於父進程運行
		if (clone_flags & CLONE_VFORK) {
			if (!wait_for_vfork_done(p, &vfork))
				ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
		}

		put_pid(pid);
	} else {
		nr = PTR_ERR(p);
	}
	return nr;
}
  • 對於do_fork中比較重要的過程,我已經注釋加以說明,這里我抽象的加以把過程總結一邊

    • 通過copy_process來復制進程描述符,返回新創建的子進程的task_struct的指針(即PCB指針)
    • 將新創建的子進程放入調度器的隊列中,讓其有機會獲得CPU,並且要確保子進程要先於父進程運行,
    • 這里為什么要確保子進程先於父進程運行呢?,答案是在Linux系統中,有一個叫做copy_on_write技術(寫時拷貝技術),該技術的作用是創建新進程時可以減少系統開銷,具體該技術的細節請各位Google之,這里子進程先於父進程運行可以保證寫時拷貝技術發揮其作用
  • 這里有一個重點的地方需要說明,在使用get_pid系統調用時,返回的並不是進程的pid,而是線程的tgid,tgid指的是一個線程組當時領頭的進程的pid

  • do_fork中,copy_process函數是比較重要的,其作用是創建進程描述符以及子進程所需要的其他所有數據結構為子進程准備運行環境,下面我將深入copy_process中來詳細分析


copy_process

/*
	創建進程描述符以及子進程所需要的其他所有數據結構
	為子進程准備運行環境
*/
static struct task_struct *copy_process(unsigned long clone_flags,
					unsigned long stack_start,
					unsigned long stack_size,
					int __user *child_tidptr,
					struct pid *pid,
					int trace)
{
    ...
	int retval;
	struct task_struct *p;

    ...
	// 分配一個新的task_struct,此時的p與當前進程的task,僅僅是stack地址不同
	p = dup_task_struct(current);
	if (!p)
		goto fork_out;

	···
	
	retval = -EAGAIN;
	// 檢查該用戶的進程數是否超過限制
	if (atomic_read(&p->real_cred->user->processes) >=
			task_rlimit(p, RLIMIT_NPROC)) {
		// 檢查該用戶是否具有相關權限,不一定是root
		if (p->real_cred->user != INIT_USER &&
		    !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
			goto bad_fork_free;
	}
	current->flags &= ~PF_NPROC_EXCEEDED;

	retval = copy_creds(p, clone_flags);
	if (retval < 0)
		goto bad_fork_free;

	/*
	 * If multiple threads are within copy_process(), then this check
	 * triggers too late. This doesn't hurt, the check is only there
	 * to stop root fork bombs.
	 */
	retval = -EAGAIN;
	// 檢查進程數量是否超過 max_threads,后者取決於內存的大小
	if (nr_threads >= max_threads)
		goto bad_fork_cleanup_count;

	if (!try_module_get(task_thread_info(p)->exec_domain->module))
		goto bad_fork_cleanup_count;

	delayacct_tsk_init(p);	/* Must remain after dup_task_struct() */
	p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);
	// 表明子進程還沒有調用exec系統調用
	p->flags |= PF_FORKNOEXEC;
	INIT_LIST_HEAD(&p->children);
	INIT_LIST_HEAD(&p->sibling);
	rcu_copy_process(p);
	p->vfork_done = NULL;

	// 初始化自旋鎖
	spin_lock_init(&p->alloc_lock);

	// 初始化掛起信號
	init_sigpending(&p->pending);

	// 初始化定時器
	p->utime = p->stime = p->gtime = 0;
	p->utimescaled = p->stimescaled = 0;
#ifndef CONFIG_VIRT_CPU_ACCOUNTING_NATIVE
	p->prev_cputime.utime = p->prev_cputime.stime = 0;
#endif
#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN
	seqlock_init(&p->vtime_seqlock);
	p->vtime_snap = 0;
	p->vtime_snap_whence = VTIME_SLEEPING;
#endif

    ...

#ifdef CONFIG_DEBUG_MUTEXES
	p->blocked_on = NULL; /* not blocked yet */
#endif
#ifdef CONFIG_BCACHE
	p->sequential_io	= 0;
	p->sequential_io_avg	= 0;
#endif

	/* Perform scheduler related setup. Assign this task to a CPU. */
	
	// 完成對新進程調度程序數據結構的初始化,並把新進程的狀態設置為TASK_RUNNING
	// 同時將thread_info中得preempt_count置為1,禁止內核搶占
	retval = sched_fork(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_policy;

	retval = perf_event_init_task(p);
	if (retval)
		goto bad_fork_cleanup_policy;
	retval = audit_alloc(p);
	if (retval)
		goto bad_fork_cleanup_perf;
	/* copy all the process information */

	// 復制所有的進程信息
	shm_init_task(p);
	retval = copy_semundo(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_audit;
	retval = copy_files(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_semundo;
		
	...

	// 初始化子進程的內核棧
	retval = copy_thread(clone_flags, stack_start, stack_size, p);
	if (retval)
		goto bad_fork_cleanup_io;

	if (pid != &init_struct_pid) {
		retval = -ENOMEM;
		// 這里為子進程分配了新的pid號
		pid = alloc_pid(p->nsproxy->pid_ns_for_children);
		if (!pid)
			goto bad_fork_cleanup_io;
	}

	...
	
	// 清除子進程thread_info結構的 TIF_SYSCALL_TRACE,防止 ret_from_fork將系統調用消息通知給調試進程
	clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);
#ifdef TIF_SYSCALL_EMU
	clear_tsk_thread_flag(p, TIF_SYSCALL_EMU);
#endif
	clear_all_latency_tracing(p);

	/* ok, now we should be set up.. */
	
	// 設置子進程的pid
	p->pid = pid_nr(pid);
	
	// 如果是創建線程
	if (clone_flags & CLONE_THREAD) {
		p->exit_signal = -1;
		
		// 線程組的leader設置為當前線程的leader
		p->group_leader = current->group_leader;
		
		// tgid是當前線程組的id,也就是main進程的pid
		p->tgid = current->tgid;
	} else {
		if (clone_flags & CLONE_PARENT)
			p->exit_signal = current->group_leader->exit_signal;
		else
			p->exit_signal = (clone_flags & CSIGNAL);
			
		// 創建的是進程,自己是一個單獨的線程組
		p->group_leader = p;
		
		// tgid和pid相同
		p->tgid = p->pid;
	}

    ...
    
	if (likely(p->pid)) {
		ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);

		init_task_pid(p, PIDTYPE_PID, pid);
		if (thread_group_leader(p)) {

			...
			
			// 將pid加入散列表
			attach_pid(p, PIDTYPE_PGID);
			attach_pid(p, PIDTYPE_SID);
			__this_cpu_inc(process_counts);
		} else {

			...

		}
		// 將pid加入PIDTYPE_PID這個散列表
		attach_pid(p, PIDTYPE_PID);
		// 遞增 nr_threads的值
		nr_threads++;
	}

	total_forks++;
	spin_unlock(&current->sighand->siglock);
	syscall_tracepoint_update(p);
	write_unlock_irq(&tasklist_lock);

	...

	// 返回被創建的task結構體指針
	return p;
	
    ...
    
}

同樣,這里關鍵部分我已經有了注釋我針對幾個關鍵問題進行說明:

  • copy_process的參數與do_fork的參數類型一模一樣,參數也是幾乎相同,除了struct pid那里是空
  • dup_task_struct這個函數,會分配一個新的task_struct給子進程,但是這個task_struct是未初始化的,下面我將具體說說這個dup_task_struct:
  • copy_process中會進行各種各樣的初始化和信息檢查,比如初始化自旋鎖,初始化堆棧信息等等,同時會把新創建的子進程運行狀態置為TASK_RUNNING(這里應該是就緒態)
  • copy_process中,會通過copy_thread來初始化子進程的內核棧,下面也會進行具體說明

dup_task_struct

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
	struct task_struct *tsk;
	struct thread_info *ti;
	int node = tsk_fork_get_node(orig);
	int err;

	// 分配一個task_struct結點
	tsk = alloc_task_struct_node(node);
	if (!tsk)
		return NULL;

	// 分配一個thread_info結點,其實內部分配了一個union,包含進程的內核棧
	// 此時ti的值為棧底,在x86下為union的高地址處。
	ti = alloc_thread_info_node(tsk, node);
	if (!ti)
		goto free_tsk;

	err = arch_dup_task_struct(tsk, orig);
	if (err)
		goto free_ti;

	// 將棧底的值賦給新結點的stack
	tsk->stack = ti;
    
    ...

	/*
	 * One for us, one for whoever does the "release_task()" (usually
	 * parent)
	 */
	// 將進程描述符的使用計數器置為2
	atomic_set(&tsk->usage, 2);
#ifdef CONFIG_BLK_DEV_IO_TRACE
	tsk->btrace_seq = 0;
#endif
	tsk->splice_pipe = NULL;
	tsk->task_frag.page = NULL;

	account_kernel_stack(ti, 1);

	// 返回新申請的結點
	return tsk;

free_ti:
	free_thread_info(ti);
free_tsk:
	free_task_struct(tsk);
	return NULL;
}
  • 這其中有個比較重要的結構struct thread_info,但是在內部分配時,其實是一個union(聯合體),這個union包括了一個內核堆棧,其結構如下圖(圖來自Understanding Linux kernel 3th)

copy_thread

// 初始化子進程的內核棧
int copy_thread(unsigned long clone_flags, unsigned long sp,
	unsigned long arg, struct task_struct *p)
{

	// 取出子進程的寄存器信息
	struct pt_regs *childregs = task_pt_regs(p);
	struct task_struct *tsk;
	int err;

	// 棧頂 空棧
	p->thread.sp = (unsigned long) childregs;
	p->thread.sp0 = (unsigned long) (childregs+1);
	memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));

	// 如果是創建的內核線程
	if (unlikely(p->flags & PF_KTHREAD)) {
		/* kernel thread */
		memset(childregs, 0, sizeof(struct pt_regs));
		// 內核線程開始執行的位置
		p->thread.ip = (unsigned long) ret_from_kernel_thread;
		task_user_gs(p) = __KERNEL_STACK_CANARY;
		childregs->ds = __USER_DS;
		childregs->es = __USER_DS;
		childregs->fs = __KERNEL_PERCPU;
		childregs->bx = sp;	/* function */
		childregs->bp = arg;
		childregs->orig_ax = -1;
		childregs->cs = __KERNEL_CS | get_kernel_rpl();
		childregs->flags = X86_EFLAGS_IF | X86_EFLAGS_FIXED;
		p->thread.io_bitmap_ptr = NULL;
		return 0;
	}

	// 將當前進程的寄存器信息復制給子進程
	*childregs = *current_pt_regs();
	// 子進程的eax置為0,所以fork的子進程返回值為0
	childregs->ax = 0;
	if (sp)
		childregs->sp = sp;

	// 子進程從ret_from_fork開始執行
	p->thread.ip = (unsigned long) ret_from_fork;
	task_user_gs(p) = get_user_gs(current_pt_regs());

	p->thread.io_bitmap_ptr = NULL;
	tsk = current;
	err = -ENOMEM;

	// 如果父進程使用IO權限位圖,那么子進程獲得該位圖的一個拷貝
	if (unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) {
		p->thread.io_bitmap_ptr = kmemdup(tsk->thread.io_bitmap_ptr,
						IO_BITMAP_BYTES, GFP_KERNEL);
		if (!p->thread.io_bitmap_ptr) {
			p->thread.io_bitmap_max = 0;
			return -ENOMEM;
		}
		set_tsk_thread_flag(p, TIF_IO_BITMAP);
	}

	...
	
	return err;
}
  • 這里的過程基本就是給新的進程的各種運行時狀態進行初始化,比如寄存器信息(通過父進程的寄存器信息來初始化,但是eip會是個例外,eip將會取決於最后子進程將會從哪里開始執行),棧會被置空未初始化狀態
  • 在代碼中,有兩段這樣的代碼p->thread.ip = (unsigned long) ret_from_kernel_thread; p->thread.ip = (unsigned long) ret_from_fork;這里表面了在fork完成之后,新進程將會在哪里開始執行,如果新的創建的新的線程是內核線程,那么將會從ret_from_kernel_thread開始執行,但是如果是普通的用戶態線程,則將會從p->thread.ip = (unsigned long) ret_from_fork開始執行.

總結

通過實驗和fork系統調用的分析,讓我認識到除了Linux系統中最開始啟動時,創建的第一個始祖進程外,從init進程開始,其他所有的進程的創建方式均是通過forkclone,vfork的方式,而他們又能夠有歸結到do_fork,就想孟寧老師說得道生一,一生二,二生三這樣.

實驗截圖


參考資料

  • Understanding The Linux Kernel, the 3rd edtion
  • Linux內核設計與實現,第三版,Robert Love, 機械工業出版社

署名信息

吳欣偉 原創作品轉載請注明出處:《Linux內核分析》MOOC課程:http://mooc.study.163.com/course/USTC-1000029000


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM