天天看點

程序管理(八)--建立程序fork1 _do_fork函數分析2 copy_process函數分析3. wake_up_new_task喚醒新程序流程4. 總結

在最新的版本的POSIX标準中,定義了程序建立和終止的操作,程序建立包括fork()和execve(),程序終止包括wait(),waitpid(),kill()以及exit()。Linux系統為了提高效率,把POSIX标準的fork()擴充為vfork和clone。

前面一章我們學習了用GCC将一個最簡單的程式(如hello world程式)編譯成ELF檔案,在shell提示符下輸入該可執行檔案并且按回車後,這個程式就開始執行了。起始這裡shell會調用fork()來建立一個新程序,然後調用execve()來執行這個新程式。該函數負責讀取可執行檔案,将其裝入子程序的位址空間并開始執行,這時候父子程序開始分道揚镳。

這一節,我們就來看一看,fork系統調用的實作,建立程序這個動作在核心裡都做了什麼事情。

1 _do_fork函數分析

在核心中,fork()、vfork()和clone()系統調用通過_do_fork()函數實作,_do_fork()函數實作在kernel/fork.c檔案中

long _do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr,
	      unsigned long tls)
           

_do_fork函數有6個參數,具體的含義如下:

參數 說明
clone_flags 建立程序的标志位集合,常見的标志位如下所示
stack_start 使用者态棧的起始位址
stack_size 使用者态棧的大小,通常設定為0
parent_tidptr和child_tidptr 指向使用者空間中位址的兩個指針,分别指向父、子程序的ID
tls 傳遞線程本地存儲

常見的标志位,選取其中常用的幾個

标志位 含義
CLONE_VM 父、子程序共享程序位址空間
CLONE_FS 父、子程序共享檔案系統資訊
CLONE_FILES 父、子程序共享打開的檔案
CLONE_SIGHAND 父、子程序共享信号處理函數以及被阻塞的信号
CLONE_VFORK 在建立子程序時啟用Linux核心的完成量機制,wait_for_completion會使父程序進入睡眠狀态,直到子程序調用execve或exit釋放記憶體
CLONE_IO 複制I/O上下文
CLONE_PTRACE 父程序被跟蹤、子程序也會被跟蹤

_do_fork()函數主要是調用copy_process函數來建立子程序的task_struct資料結構,以及從父程序複制必要的内容到子程序的task_struct資料結構中,完成子程序的建立,如下圖所示

程式管理(八)--建立程式fork1 _do_fork函數分析2 copy_process函數分析3. wake_up_new_task喚醒新程式流程4. 總結

第一步、檢查子程序是否允許被跟蹤

如果父程序正在被跟蹤(即current->ptrace不為0時),檢查debugger程式是否想跟蹤子程序,并且子程序不是核心程序(CLONE_UNTRACED未設定)那麼就設定CLONE_PTRACE标志,即子程序也被跟蹤

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

第二步、複制程序描述符,傳回的是新的程序描述符的位址

調用copy_process函數建立一個新的子程序,如果成功就傳回子程序的task_struct

p = copy_process(clone_flags, stack_start, stack_size,
			 child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
	add_latent_entropy();
           

第三步、初始化完成量

對于vfork建立的子程序,首先要保證子程序先運作,子程序調用exec()或exit()之後,才可以排程,運作父程序,是以這裡使用了一個vfork_done的完成量達到該目的。

struct completion vfork;
		struct pid *pid;

		trace_sched_process_fork(current, p);
//1. 由子程序的task_struct資料結構來擷取PID
		pid = get_task_pid(p, PIDTYPE_PID);
//2. pid_vnr擷取虛拟的PID,即從目前指令空間内部看到的PID
		nr = pid_vnr(pid);

		if (clone_flags & CLONE_PARENT_SETTID)
			put_user(nr, parent_tidptr);
//3. init_completion初始化完成量
		if (clone_flags & CLONE_VFORK) {
			p->vfork_done = &vfork;
			init_completion(&vfork);
			get_task_struct(p);
		}
           

第四步、喚醒新程序

wake_up_new_task函數用于喚醒新建立的程序,也就是把程序加入就緒隊列裡并接受排程、運作。

第五步、等待子程序完成

對于使用vfork(),wait_for_vfork_done函數等待子程序調用exec()或exit()

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

		if (clone_flags & CLONE_VFORK) {
			if (!wait_for_vfork_done(p, &vfork))
				ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
		}

           

第六步、傳回子程序的ID

在父程序傳回使用者空間時,其傳回子程序的ID,子程序傳回使用者空間時,其傳回值為0。

do_fork函數執行後就存在兩個程序,而且每個程序都會從 _do_fork函數的傳回處執行。程式可以通過fork的傳回值來區分父、子程序

  • 父程序,傳回新建立的子程序的ID
  • 子程序,傳回0

其處理流程如下圖所示

程式管理(八)--建立程式fork1 _do_fork函數分析2 copy_process函數分析3. wake_up_new_task喚醒新程式流程4. 總結

2 copy_process函數分析

copy_process函數使fork的核心函數,它會建立新程序的描述符,以及新程序執行所需要的其他資料結構,我們主要來看看這個具體做了些什麼?

第一步、标志位檢查

// 1. CLONE_NEWS表明父子程序不共享mount的命名空間,每個程序可以擁有屬于自己的mount空間
	if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
		return ERR_PTR(-EINVAL);
// 2. CLONE_NEWUSER表示子程序要建立新的user命名空間,USER指令空間用于管理USER ID和Group ID的映射,起到隔離的作用
	if ((clone_flags & (CLONE_NEWUSER|CLONE_FS)) == (CLONE_NEWUSER|CLONE_FS))
		return ERR_PTR(-EINVAL);

	/*
	 * Thread groups must share signals as well, and detached threads
	 * can only be started up within the thread group.
	 */
// 3. CLONE_THREAD表示父子程序在同一個線程組裡,POSIX标準規定在一個程序的内部,多個線程共享一個PID,但是linux為每個線程和程序都配置設定了PID
	if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
		return ERR_PTR(-EINVAL);

	/*
	 * Shared signal handlers imply shared VM. By way of the above,
	 * thread groups also imply shared VM. Blocking this case allows
	 * for various simplifications in other code.
	 */
// 4. CLONE_SIGHAND表明父子程序共享相同的信号處理表,CLONE_VM表明父子程序共享記憶體空間
	if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
		return ERR_PTR(-EINVAL);

	/*
	 * Siblings of global init remain as zombies on exit since they are
	 * not reaped by their parent (swapper). To solve this and to avoid
	 * multi-rooted process trees, prevent global and container-inits
	 * from creating siblings.
	 */
// 5. CLONE_PARENT表明新建立的程序是兄弟關系,而不是父子關系,他們擁有相同的父程序
	if ((clone_flags & CLONE_PARENT) &&
				current->signal->flags & SIGNAL_UNKILLABLE)
		return ERR_PTR(-EINVAL);

	/*
	 * If the new process will be in a different pid or user namespace
	 * do not allow it to share a thread group with the forking task.
	 */
// 6. CLONE_NEWPID表明建立一個新的PID命名空間
	if (clone_flags & CLONE_THREAD) {
		if ((clone_flags & (CLONE_NEWUSER | CLONE_NEWPID)) ||
		    (task_active_pid_ns(current) !=
				current->nsproxy->pid_ns_for_children))
			return ERR_PTR(-EINVAL);
	}
           

第二步、配置設定一個task_struct資料結構

dup_task_struct()為新程序配置設定一個task_struct資料結構,後續補充這個函數做了些什麼?

retval = security_task_create(clone_flags);
	if (retval)
		goto fork_out;

	retval = -ENOMEM;
	p = dup_task_struct(current, node);
	if (!p)
		goto fork_out;
           

第三步、複制父程序

user資料結構中的processes成員記錄了該使用者的程序數,這裡檢查程序數是否超過了程序資源的限制RLIMIT_NPROC

ftrace_graph_init_task(p);

	rt_mutex_init_task(p);

#ifdef CONFIG_PROVE_LOCKING
	DEBUG_LOCKS_WARN_ON(!p->hardirqs_enabled);
	DEBUG_LOCKS_WARN_ON(!p->softirqs_enabled);
#endif
	retval = -EAGAIN;
// 1. 檢查程序數是否超過限制,由作業系統定義
	if (atomic_read(&p->real_cred->user->processes) >=
			task_rlimit(p, RLIMIT_NPROC)) {
		if (p->real_cred->user != INIT_USER &&
		    !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN))
			goto bad_fork_free;
	}
	current->flags &= ~PF_NPROC_EXCEEDED;
//2. 複制父程序
	retval = copy_creds(p, clone_flags);
	if (retval < 0)
		goto bad_fork_free;

           

第四步、初始化task_stcut

//初始化子程序描述符中的list_head資料結構和自旋鎖,并為與挂起信号、定時器及時間統計表相關的幾個字段賦初值。
	delayacct_tsk_init(p);	/* Must remain after dup_task_struct() */
	p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);
	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;
	prev_cputime_init(&p->prev_cputime);
           

第五步、初始化程序排程相關的資料結構

sched_fork函數初始化與程序排程相關的資料結構,排程實體用sched_entity資料結構來抽象,每個程序或線程都是一個排程實體。

/* Perform scheduler related setup. Assign this task to a CPU. */
	retval = sched_fork(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_policy;
           

第六步、初始化task_struct結構的其他資料結構

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_fs(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_files;
	retval = copy_sighand(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_fs;
	retval = copy_signal(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_sighand;
	retval = copy_mm(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_signal;
	retval = copy_namespaces(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_mm;
	retval = copy_io(clone_flags, p);
	if (retval)
		goto bad_fork_cleanup_namespaces;
	retval = copy_thread_tls(clone_flags, stack_start, stack_size, p, tls);
	if (retval)
		goto bad_fork_cleanup_io;

	if (pid != &init_struct_pid) {
		pid = alloc_pid(p->nsproxy->pid_ns_for_children);
		if (IS_ERR(pid)) {
			retval = PTR_ERR(pid);
			goto bad_fork_cleanup_thread;
		}
	}
           
  • copy_files 主要用于複制一個程序打開的檔案資訊。這些資訊用一個結構 files_struct 來維護,每個打開的檔案都有一個檔案描述符。在 copy_files 函數裡面調用 dup_fd,在這裡面會建立一個新的 files_struct,然後将所有的檔案描述符數組 fdtable 拷貝一份。
  • copy_fs 主要用于複制一個程序的目錄資訊。這些資訊用一個結構 fs_struct 來維護。一個程序有自己的根目錄和根檔案系統 root,也有目前目錄 pwd 和目前目錄的檔案系統,都在 fs_struct 裡面維護。copy_fs 函數裡面調用 copy_fs_struct,建立一個新的 fs_struct,并複制原來程序的 fs_struct。
  • copy_sighand 會配置設定一個新的 sighand_struct。這裡最主要的是維護信号處理函數,在 copy_sighand 裡面會調用 memcpy,将信号處理函數 sighand->action 從父程序複制到子程序。
  • init_sigpending 和 copy_signal 用于初始化,并且複制用于維護發給這個程序的信号的資料結構。copy_signal 函數會配置設定一個新的 signal_struct,并進行初始化。
  • 程序都自己的記憶體空間,用 mm_struct 結構來表示。copy_mm 函數中調用 dup_mm,配置設定一個新的 mm_struct 結構,調用 memcpy 複制這個結構。dup_mmap 用于複制記憶體空間中記憶體映射的部分。前面講系統調用的時候,我們說過,mmap 可以配置設定大塊的記憶體,其實 mmap 也可以将一個檔案映射到記憶體中,友善可以像讀寫記憶體一樣讀寫檔案,這個在記憶體管理那節我們講。
  • copy_namespace函數複制父程序的命名位址空間
  • copy_io函數複制父程序與I/O相關的内容
  • copy_thread_tls函數複制父程序的核心堆資訊

第七步、配置設定ID

開始配置設定 pid,設定 tid,group_leader,并且建立程序之間的親緣關系。

p->pid = pid_nr(pid);
	if (clone_flags & CLONE_THREAD) {
		p->exit_signal = -1;
		p->group_leader = current->group_leader;
		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;
		p->tgid = p->pid;
	}
           
  • pid_nr配置設定一個全局的PID,這個全局的PID是從init程序的命名空間的家督來看,是一個虛拟的PID
  • 設定group_leader和TGID

第八步、傳回程序描述符

配置設定task_struct,并完成各項的初始化後,就傳回子程序的描述符。

到此,copy_process函數的處理流程完畢,其處理流程如下圖所示

程式管理(八)--建立程式fork1 _do_fork函數分析2 copy_process函數分析3. wake_up_new_task喚醒新程式流程4. 總結

3. wake_up_new_task喚醒新程序流程

用copy_process來拷貝出一個新的程序pcb,然後調用wake_up_new_task将新的程序放入運作隊列并喚醒該程序。同時新任務剛剛建立,有沒有機會搶占别人,獲得 CPU 呢?

void wake_up_new_task(struct task_struct *p)
{
	struct rq_flags rf;
	struct rq *rq;
//1. 需要将程序的狀态設定為 TASK_RUNNING
	raw_spin_lock_irqsave(&p->pi_lock, rf.flags);
	p->state = TASK_RUNNING;
#ifdef CONFIG_SMP
	/*
	 * Fork balancing, do it here and not earlier because:
	 *  - cpus_allowed can change in the fork path
	 *  - any previously selected cpu might disappear through hotplug
	 *
	 * Use __set_task_cpu() to avoid calling sched_class::migrate_task_rq,
	 * as we're not fully set-up yet.
	 */
//2.這個函數會根據新建立的這個線程所屬的排程類去執行不同的select_task_rq。
	__set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));
#endif
	rq = __task_rq_lock(p, &rf);
	post_init_entity_util_avg(&p->se);

	activate_task(rq, p, 0);
	p->on_rq = TASK_ON_RQ_QUEUED;
	trace_sched_wakeup_new(p);
	check_preempt_curr(rq, p, WF_FORK);
#ifdef CONFIG_SMP
	if (p->sched_class->task_woken) {
		/*
		 * Nothing relies on rq->lock after this, so its fine to
		 * drop it.
		 */
		lockdep_unpin_lock(&rq->lock, rf.cookie);
		p->sched_class->task_woken(rq, p);
		lockdep_repin_lock(&rq->lock, rf.cookie);
	}
#endif
	task_rq_unlock(rq, p, &rf);
}
           

activate_task 函數中會調用 enqueue_task,就會涉及到排程相關的流程,該内容在排程中進行學習。

static inline void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
{
	update_rq_clock(rq);
	if (!(flags & ENQUEUE_RESTORE))
		sched_info_queued(rq, p);
	p->sched_class->enqueue_task(rq, p, flags);
}
           

子程序建立後,肯定要加入到CPU的執行隊列中,這樣才有可能被執行,這是調用wake_up_new_task()來實作的。這是排程器與程序建立的第二個邏輯互動時機,核心會調用排程器類的task_new函數(sched_class結構中),将新程序加入到相應類的就緒隊列。

至此,建立使用者程序的過程就完成了。其主要的要點如下:

  1. 每個程序需要有一個核心棧,不管是4K還是8KB,這個核心棧需要包含兩部分,一個是task_struct資料結構,另外一個是核心棧
  2. 繼承父程序的task_struct資料結構,然後進行調整
  3. 設定程序空間的棧
  4. 拷貝父程序的程序位址空間給子程序
  5. 将子程序喚醒,設定到就緒隊列中,初始化排程相關的,然後等待排程器進行排程

4. 總結

fork, vfork和clone的系統調用的入口位址分别是sys_fork, sys_vfork和sys_clone, 而他們的定義是依賴于體系結構的, 而他們最終都調用了_do_fork,在_do_fork中通過copy_process複制程序的資訊,調用wake_up_new_task将子程序加入排程器中,其主要的工作内容如下:

  • copy_process()函數會做fork的大部分事情,它主要完成講父程序的運作環境複制到新的子程序,比如信号處理、檔案描述符和程序的代碼資料等,初始化程序控制塊中的所有成員,其處理流程如下
    程式管理(八)--建立程式fork1 _do_fork函數分析2 copy_process函數分析3. wake_up_new_task喚醒新程式流程4. 總結
  • wake_up_new_task()。計算此程序的優先級和其他排程參數,将新的程序加入到程序排程隊列并設此程序為可被排程的,以後這個程序可以被程序排程子產品排程執行。

繼續閱讀