1. 程式人生 > >《Linux作業系統分析》之分析Linux核心建立一個新程序的過程

《Linux作業系統分析》之分析Linux核心建立一個新程序的過程

本篇文章通過fork函式的呼叫,來說明在Linux系統中建立一個新程序需要經歷的過程。

相關知識:

首先關於這篇文章會介紹一些用到的知識。

一、程序、輕量級程序、執行緒

程序是程式執行的一個例項。程序的目的就是擔當分配系統資源的實體。

兩個輕量級程序基本可以共享一些資源,比如資料空間、開啟的檔案等。只要其中的一個修改共享資源,另一個就立馬檢視這種修改。

執行緒可以由核心獨立的排程。執行緒之間可以通過簡單地共享同一記憶體地址、同一開啟檔案集等來訪問相同的應用程式資料結構集。

二、程序描述符

程序描述符都是task_struc型別結構。它包含了與程序相關的所有資訊。


三、程序狀態

狀態以及之間的轉換關係如圖所示:


四、fork、vfork和clone

fork()函式複製時將父程序的所有資源都通過複製資料結構進行復制,然後傳遞給子程序,故fork()函式不帶引數;clone()函式則是將部分父程序的資源的資料結構進行復制,複製哪些資源是可選擇的,通過引數設定,故clone()函式帶引數。fork()可以看出是完全版的clone(),而clone()克隆的只是fork()的一部分。為了提高系統的效率,後來的Linux設計者又增加了一個系統呼叫vfork()。vfork()所建立的不是程序而是執行緒,它所複製的是除了任務結構體和系統堆疊之外的所有資源的資料結構,而任務結構體和系統堆疊是與父程序共用的。

分析過程:

我們將Ubuntu中的menuOS進行更新

git clone https://github.com/mengning/menu.git
然後重新make rootfs。結果如圖:

在menuOS中添加了fork函式,可以建立程序:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
    int pid;
    /* fork another process */
    pid = fork();
    if (pid < 0) 
    { 
        /* error occurred */
        fprintf(stderr,"Fork Failed!");
        exit(-1);
    } 
    else if (pid == 0) 
    {
        /* child process */
        printf("This is Child Process!\n");
    } 
    else 
    {  
        /* parent process  */
        printf("This is Parent Process!\n");
        /* parent will wait for the child to complete*/
        wait(NULL);
        printf("Child Complete!\n");
    }
}
然後在gdb中進行除錯,過程與幾篇部落格相同,請參考部落格:《Linux作業系統分析》之跟蹤分析Linux核心的啟動過程。設定的斷點如圖:

追蹤除錯結果如下:


通過上面斷點的出現位置,我們知道一次程序的建立大致是這樣的一個流程:

sys_clone—>do_fork—>copy_process—>dup_task_struct—>copy_thread—>ret_from_fork

我們大概看一下每個函式的執行:

先看fork函式:

#ifdef __ARCH_WANT_SYS_FORK
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
    return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
    /* can not support in nommu mode */
    return -EINVAL;
#endif
}
#endif

//vfork
#ifdef __ARCH_WANT_SYS_VFORK
SYSCALL_DEFINE0(vfork)
{
    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
            0, NULL, NULL);
}
#endif

//clone
#ifdef __ARCH_WANT_SYS_CLONE
#ifdef CONFIG_CLONE_BACKWARDS
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
         int __user *, parent_tidptr,
         int, tls_val,
         int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
         int __user *, parent_tidptr,
         int __user *, child_tidptr,
         int, tls_val)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
        int, stack_size,
        int __user *, parent_tidptr,
        int __user *, child_tidptr,
        int, tls_val)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
         int __user *, parent_tidptr,
         int __user *, child_tidptr,
         int, tls_val)
#endif
{
    return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
#endif
我們看上面的程式碼知道三種方法建立的程序都是呼叫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;

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

	p = copy_process(clone_flags, stack_start, stack_size,
			 child_tidptr, NULL, trace);//複製程序描述符,copy_process()的返回值是一個 task_struct 指標。

	if (!IS_ERR(p)) {
		struct completion vfork;
		struct pid *pid;

		trace_sched_process_fork(current, p);

		pid = get_task_pid(p, PIDTYPE_PID);//得到新建立程序的pid
		nr = pid_vnr(pid);

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

		if (clone_flags & CLONE_VFORK) {
			p->vfork_done = &vfork;
			init_completion(&vfork);
			get_task_struct(p);
		}

		wake_up_new_task(p);//將子程序加入到排程器中
		/* forking complete and child started to run, tell ptracer */
		if (unlikely(trace))
			ptrace_event_pid(trace, pid);

		if (clone_flags & CLONE_VFORK) {//如果是 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;
}
在上面我們看到首先呼叫copy_process 為子程序複製出一份程序資訊,在呼叫 wake_up_new_task 將子程序加入排程器,為之分配 CPU。我們看一下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;
	
	//省略
	 
	retval = security_task_create(clone_flags);//security_task_create 的作用是對 clone_flags 的設定的標誌進 行安全檢查,檢視系統是否滿足程序建立所需要的儲存空間,使用者配額限制。
	if (retval)
		goto fork_out;

	retval = -ENOMEM;
	p = dup_task_struct(current);	//複製當前的 task_struct
	if (!p)
		goto fork_out;

	//省略
	 
	spin_lock_init(&p->alloc_lock);//初始化一些資源 
	init_sigpending(&p->pending);
	
	//省略 

	/* Perform scheduler related setup. Assign this task to a CPU. */
	retval = sched_fork(clone_flags, p);//初始化程序資料結構,並把程序狀態設定為 TASK_RUNNING
	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_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(clone_flags, stack_start, stack_size, p);//初始化子程序核心棧
	if (retval)
		goto bad_fork_cleanup_io;

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

	//省略 

	/* ok, now we should be set up.. */
	p->pid = pid_nr(pid); //設定子程序 pid 
	
	//省略 

	return p;
	//下面是各跳轉函式的定義處,省略 
}

在copy_process中會呼叫dup_task_struct(current);最終執行完dup_task_struct之後,子程序與父程序除了tsk->stack指標不同之外,其他全部都一樣,然後呼叫 sched_fork 初始化程序資料結構,並把程序狀態設定為 TASK_RUNNING,再接著呼叫copy_thread(clone_flags, stack_start, stack_size, p);。下面看下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;
	
	//省略
	 
	*childregs = *current_pt_regs();	//將當前暫存器資訊複製給子程序
	childregs->ax = 0;//子程序返回0 
	if (sp)
		childregs->sp = sp;

	p->thread.ip = (unsigned long) ret_from_fork;//將子程序的ip設定為 ret_from_fork,因此排程後子程序從ret_from_fork開始執行。 

	//省略
	 
	return err;
}
在上面我們知道子程序返回0的原因,以及將子程序ip 設定為ret_from_fork函式首地址,子程序將從ret_from_fork開始執行。
總結:

一、對於每個程序來說,Linux都把兩個不同的資料結構緊湊地存放在一個單獨為程序分配的儲存區域內:一個是核心態的程序堆疊,另一個是緊挨著程序描述符的小資料節後thread_info,叫做執行緒描述符。(資料型別是union,通常是8192位元組即兩個頁框)

union thread_union {
	struct thread_info thread_info;//<span style="font-family: Arial;">thread_info是52位元組長</span>
	unsigned long stack[THREAD_SIZE/sizeof(long)];
};

執行緒描述符駐留於這個記憶體區的開始,而棧從末端(高地址)開始增長。因為thread_info是52位元組長,所以核心棧能擴充套件到8140位元組。


二、當一個程序建立時,它與父程序相同。它接受父程序地址空間的一個(邏輯)拷貝,並從程序建立系統呼叫的下一條指令開始執行與父程序相同的程式碼。儘管父子程序可以共享程式程式碼的頁,但是它們各自有獨立的資料拷貝(棧和堆),因此子程序對一個記憶體單元的修改對父程序是不可見的(反之亦然)。

三、程序的建立過程大概就是這樣的:sys_clone—>do_fork—>copy_process—>dup_task_struct—>copy_thread—>ret_from_fork。

備註:

楊峻鵬 + 原創作品轉載請註明出處 + 《Linux核心分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000