1. 程式人生 > >Linux程序控制--程序建立

Linux程序控制--程序建立

Linux程序建立

linux下建立程序的方式有三種,通過fork vfork clone系統呼叫實現程序的建立

1. fork

fork函式用於建立一個新的程序,其建立的程序和當前程序為父關係,子程序建立自己的task_struct 之後初始化子程序的互斥變數、cpu定時器、自旋鎖、掛起訊號、程序資料結構等並且設定程序狀態, 然後子程序複製父程序的各項資訊(包括檔案系統、訊號處理函式、訊號、記憶體管理等)但是拷貝父程序的mm_struct時採用寫時拷貝(後面詳解)。之後會為子程序設定核心棧(thread_info) 和程序的pid 以及與呼叫程序的關係設定,呼叫 wake_up_new_task 將子程序加入排程器,為之分配 CPU;

使用fork()建立的子程序和和父程序通過寫時拷貝共享同一虛擬地址空間,子程序和父程序各自擁有不同的核心棧,子程序共享父程序的時間片。

寫時複製(copy _on_write) 子程序與父程序共享同一頁幀,不是複製,並且將子程序和父程序對該頁幀的許可權均設定成只讀(頁幀被保護),當有任意一方需要進行寫入時會觸發出錯異常(page_fault int14)中斷,此時系統會為其重新分配一個新的頁幀並且複製之前頁幀內容標記可寫, 然後返回繼續執行寫操作,此時原來的頁幀仍然被防寫當,再有程序對其程序寫操作時,核心檢查寫程序是否是這個頁幀的唯一屬主,如果是,就把這個頁幀標記為對這個程序是可寫的。 另外Linux下c++標準庫中string類也使用了

寫時拷貝

2. vfork

vfork也是用於建立一個新的程序,建立過程和fork相似,不過在vfork進行拷貝父程序mm_struct時進行的是指標值拷貝也就是說父子程序指向同一程序地址空間,不過vfork建立的子程序同樣會為其建立自己的核心棧,另外vfork 需要(設定CLONE_VFORK和ptrace標誌)初始化完成處理資訊,還有父程序會阻塞等待。vfork()保證子程序先排程執行。因為父程序和子程序指向同一程序地址空間則使用vfork函式後會立即使用exec函式進行替換,當程序呼叫一種exec函式時,該程序完全由新程式代換,而新程式則從其main函式開始執行,因為呼叫exec並不建立新程序,所以前後的程序id 並未改變,exec只是用另一個新程式替換了當前程序的正文,資料,堆和棧段。 如果在程序不去替換子程序,子程序繼續執行將可能破壞父程序的資料結構或棧而出錯。

3. fork和vfork呼叫流程

fork, vfork和clone的系統呼叫的入口地址分別是sys_fork, sys_vfork和sys_clone 以下程式碼均來自Linux-4.5

sys_fork()實現

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

sys_vfork()實現

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

fork和vfork實現最後都呼叫了_do_fork函式區別在於引數不同,其中vfork函式clone_flags引數多了兩個屬性,CLONE_VFORK 和CLONE_VM 接著看_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,
      unsigned long tls)
{
    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, tls);

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

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

    //如果呼叫的 vfork()方法,初始化 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);

   // 如果是 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;
}

其主要完成

  1. 呼叫 copy_process 為子程序複製出一份程序資訊
  2. 如果是 vfork(設定了CLONE_VFORK和ptrace標誌)初始化完成處理資訊
  3. 呼叫 wake_up_new_task 將子程序加入排程器,為之分配 CPU
  4. 如果是 vfork,父程序加入等待佇列,待子程序完成 exec 替換自己的地址空間,或者子程序結束 另外檢視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,
                    unsigned long tls)
{
	// 函式實現很長就不具體貼瞭如果要檢視可以在點上面連結
	// 這個函式的主要功能
	// 呼叫 dup_task_struct 複製當前的 task_struct
	// 檢查程序數是否超過限制
	// 初始化自旋鎖、掛起訊號、CPU 定時器等
	// 呼叫 sched_fork 初始化程序資料結構,並把程序狀態設定為 TASK_RUNNING
	// 複製所有程序資訊,包括檔案系統、訊號處理函式、訊號、記憶體管理等
	// 呼叫 copy_thread_tls 初始化子程序核心棧
	// 為新程序分配並設定新的 pid
	//...
	//在這省略了函式的其他實現主要看程序的資訊中記憶體地址空間的拷貝這塊的
    //複製所有程序資訊,包括檔案系統、訊號處理函式、訊號、記憶體管理等
       形式類似於copy_xxx的形式 */
    //...
    retval = copy_mm(clone_flags, p);
    if (retval)
        goto bad_fork_cleanup_signal;
}
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{
	//...
	//當vfork呼叫時其clone_flags擁有CLONE_VM屬性即其實現值拷貝
	if (clone_flags & CLONE_VM) {
		atomic_inc(&oldmm->mm_users);
		mm = oldmm;
		goto good_mm;
	}

	retval = -ENOMEM;
	//fork呼叫dup_mm->dup_mmap->copy_page_range->copy_pud_range->
	//copy_pmd_range->copy_pte_range->copy_one_pte實現寫時複製
	mm = dup_mm(tsk);
	if (!mm)
		goto fail_nomem;

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

fail_nomem:
	return retval;
}

最後fork() vfork() 函式執行完之後在以後的程序切換中,排程程式繼續完善子程序:把子程序描述符thread(即核心棧中的值,因為父子程序核心棧不同,故可以保留不同的值以供esp暫存器讀取因此父子程序返回不同的值)欄位的值(TSS值)裝入幾個CPU暫存器。特別是把thread.esp裝入esp暫存器,把函式ret_from_fork()的地址裝入eip暫存器。這個組合語言函式呼叫schedule_tail()函式,用存放在棧中的值再裝入所有暫存器,並強迫CPU返回到使用者態。這樣,eax暫存器就裝過兩個值,一個是子程序的值0,一個是父程序的值——子程序的PID。

  1 #include <unistd.h>
  2 #include <stdio.h>
  3 #include <string.h>
  4 #include <errno.h>
  5 int main()
  6 {
  7     int val = 100;
  8     pid_t pid = fork();
  9     if(pid < 0)
 10     {
 11         perror("creat process error");
 12     }
 13     else if(pid == 0)  //子程序返回0
 14     {
 15         val = 1;
 16         printf("this is child[pid:%d] val:%d\n", getpid(), val);
 17     }
 18     else if(pid > 0)  //父程序返回子程序的pid
 19     {
 20         printf("this is parent[pid:%d] val:%d\n", getpid(), val);
 21     }
 22     sleep(1);
 23     return 0;
 24 }