程序建立
Unix 中關于程序的建立分為2個步驟:fork() 和 exec() (這裡 exec 指 exec一系列函數,因為核心實作了多個函數,比如 execv 等)。
- fork() 調用是通過拷貝目前程序來建立子程序。此時父子程序的差別在于pid(本程序号),ppid(父程序号)和一些資源和統計量。
- exec() 函數用于加載可執行檔案開始運作。
以上2個函數完成了程序的建立過程。
建立程序(線程)的方式有3種:fork()、vfork()、clone()。
fork 原理
有關 fork 的系統調用如下
#include <unistd.h>
pid_t fork(void);
fork() 系統調用傳回資訊,具體描述如下:
- 傳回值為-1時,表示建立失敗。
- 傳回值為0時,傳回到新建立的子程序。
- 傳回值大于0時,傳回父程序或調用者。該值為新建立的子程序的程序ID。
當使用者調用 fork()時,會進入系統調用 sys_fork()
int sys_fork(struct pt_regs *regs)
{
return do_fork(SIGCHLD, regs->sp, regs, 0, NULL, NULL);
}
從 do_fork 的第一個參數 clone_flags 可知,do_fork 隻傳遞了一個當子程序去世時向父程序發送的信号 SIGCHLD。
參數 clone_flags 有2部分組成,最低位元組為信号類型,用于表示子程序去世時向父程序發送的信号。高位表示的是資源和特性的标志位,比如:
#define CSIGNAL 0x000000ff
#define CLONE_VM 0x00000100
#define CLONE_FS 0x00000200
#define CLONE_FILES 0x00000400
#define CLONE_SIGHAND 0x00000800
#define CLONE_PTRACE 0x00002000
#define CLONE_VFORK 0x00004000
...
是以對于 fork,這一部分資源标志位全為0,表示對有關資源要進行複制而不是通過增加引用計數進行指針共享。
long do_fork(unsigned long clone_flags, ////資源标志
unsigned long stack_start, //子程序使用者态堆棧位址
struct pt_regs *regs, //寄存器集合指針
unsigned long stack_size, //使用者态下棧大小,該參數通常是不必要的,為0
int __user *parent_tidptr, //父程序在使用者态下的pid的位址
int __user *child_tidptr) //子程序在使用者态下pid的位址
{
struct task_struct *p;
int trace = 0;
long nr;
...
//複制子程序,為子程序複制出一份程序資訊
p = copy_process(clone_flags, stack_start, regs, stack_size,
child_tidptr, NULL);
if (!IS_ERR(p)) {
struct completion vfork;
//fork系統調用要傳回新程序的PID,如果設定了CLONE_NEWPID标志,fork操作可能建立了新的PID命名空間,此時要傳回發出fork系統調用的程序所在命名空間的程序ID
nr = (clone_flags & CLONE_NEWPID) ?
task_pid_nr_ns(p, current->nsproxy->pid_ns) :
task_pid_vnr(p);
...
//将任務放入運作隊列并将其喚醒
wake_up_new_task(p, clone_flags);
...
/* 如果是 vfork,将父程序加入至等待隊列,等待子程序完成 */
if (clone_flags & CLONE_VFORK) {
freezer_do_not_count();
wait_for_completion(&vfork);
freezer_count();
if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE)) {
current->ptrace_message = nr;
ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP);
}
}
} else {
nr = PTR_ERR(p);
}
//傳回子程序pid
return nr;
}
do_fork 的工作流程如下:
- 調用 copy_process 為子程序複制一份描述符資訊。
- 将子程序加入運作隊列并将其喚醒運作
- 若是調用 vfork() 則父程序等待子程序執行完成。
該函數的流程如下:
在 do_fork中,其核心處理程式為 copy_process,其實作如下
static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid)
{
int retval;
struct task_struct *p;
int cgroup_callbacks_done = 0;
/* CLONE_FS 不能與 CLONE_NEWNS 同時設定 */
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);
/* 建立線程時線程之間要共享信号處理函數 */
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);
/*
* 父子程序共享信号處理函數時必須共享記憶體位址空間
* 這就是為什麼fork出來的父子程序有其獨立的信号處理函數,因為他們的記憶體位址空間不同
*/
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);
...
retval = -ENOMEM;
//為新程序建立一個核心棧,此時父子程序的描述符完全相同
p = dup_task_struct(current);
rt_mutex_init_task(p);
retval = -EAGAIN;
/*
user指向user_struct。一個使用者常常有多個程序,是以使用者資訊不屬于某個專一的程序。屬于同一個
使用者的程序就可以通過指針user共享這些資訊。每個使用者隻有一個user_struct結構。結構中的__count,
對屬于該使用者的程序數量計數。當然,核心線程并不屬于某個使用者,自然task_struct中的user指引為0.
校驗該使用者擁有的程序數有沒有超過該使用者所擁有的程序數量的限制。
*/
if (atomic_read(&p->user->processes) >=
p->signal->rlim[RLIMIT_NPROC].rlim_cur) {
if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) &&
p->user != current->nsproxy->user_ns->root_user)
goto bad_fork_free;
}
//對該user_struct結構的引用計數加1;對該使用者所擁有的程序總數量加1
atomic_inc(&p->user->__count);
atomic_inc(&p->user->processes);
get_group_info(p->group_info);
//檢測系統中程序的總數量(所有使用者的程序數加系統的核心線程數)是否超過了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;
if (p->binfmt && !try_module_get(p->binfmt->module))
goto bad_fork_cleanup_put_domain;
p->did_exec = 0;
delayacct_tsk_init(p); /* Must remain after dup_task_struct() */
//将從do_fork()傳遞來的的clone_flags指派給子程序描述符中的對應字段
copy_flags(clone_flags, p);
/* 初始化子程序的子程序連結清單和兄弟程序連結清單為空 */
INIT_LIST_HEAD(&p->children);
INIT_LIST_HEAD(&p->sibling);
p->vfork_done = NULL;
/* 初始化配置設定鎖,此鎖用于保護配置設定記憶體,檔案,檔案系統等操作 */
spin_lock_init(&p->alloc_lock);
clear_tsk_thread_flag(p, TIF_SIGPENDING);
/* 信号清單初始化,此清單儲存被挂起的信号 */
init_sigpending(&p->pending);
//初始化其中的各個字段,使得子程序和父程序逐漸差別出來
p->utime = cputime_zero;
p->stime = cputime_zero;
...
//調用sched_fork函數執行排程器相關的設定,為這個新程序配置設定CPU,使得子程序的程序狀态為TASK_RUNNING。并禁止核心搶占
sched_fork(p, clone_flags);
/*複制程序的資源資訊比如打開的檔案、檔案系統資訊,信号處理函數、程序位址空間、指令空間等.
至于這些資訊時父子共享還是進行拷貝,根據clone_flags的具體取值來為子程序拷貝或共享父程序的某些資料結構
*/
security_task_alloc(p);
audit_alloc(p);
copy_semundo(clone_flags, p);
copy_files(clone_flags, p);
copy_fs(clone_flags, p);
/* 判斷是否設定 CLONE_SIGHAND ,如果是(線程必須為是),增加父進行的sighand引用計數,如果否(建立的必定是子程序),将父線程的sighand_struct複制到子程序中 */
copy_sighand(clone_flags, p);
/* 如果建立的是線程,直接傳回0,如果建立的是程序,則會将父程序的信号屏蔽和安排複制到子程序中 */
copy_signal(clone_flags, p);
/*
若clone_flags 不存在 CLONE_VM 時會對父程序 mm_struct 結構進行複制,對其的複制不僅局限于這個資料結構的複制,還包括了更深層次的資料結構的複制。
其中最重要的是vm_area_struct資料結構和頁面映射表的複制,這都是由dup_mmap()複制的
*/
copy_mm(clone_flags, p);
copy_keys(clone_flags, p);
copy_namespaces(clone_flags, p); /*
複制線程。通過copy_threads()函數更新子程序的核心棧和寄存器中的值,在之前的dup_task_struct()中隻是為子程序建立一個核心棧,
核心棧是空的,并沒有實際意義。
當父程序發出clone系統調用時,核心會将那個時候CPU中寄存器的值儲存在父程序的核心棧中。
這裡就是使用父程序核心棧中的值來更新子程序寄存器中的值。特别的,核心将子程序eax寄存器中的值強制指派為0,
這也就是為什麼使用fork()時子程序傳回值是0。另外,子程序的對應的thread_info結構中的esp字段會被初始化為子程序核心棧的基址
copy_thread函數會将父程序的thread_struct和核心棧資料複制到子程序中,并将子程序的傳回值置為0(x86傳回值儲存在eax中,arm儲存在r0中,即把eax或者r0所在的核心棧資料置為0)
copy_thread函數還會将子程序的eip寄存器值設定為ret_from_fork()的位址,即當子程序首次被調用就立即執行系統調用clone傳回。
是以應用層調用fork()函數後,子程序傳回0,父程序傳回子程序ID(傳回子程序ID在之後代碼中會實作)
*/
retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
if (retval)
goto bad_fork_cleanup_namespaces;
/* 判斷是不是init程序 */
if (pid != &init_struct_pid) {
retval = -ENOMEM;
/*配置設定pid
用alloc_pid函數為這個新程序配置設定一個pid,Linux系統内的pid是循環使用的,采用位圖方式來管理。
簡單的說,就是用每一位(bit)來标示該位所對應的pid是否被使用。
配置設定完畢後,判斷pid是否配置設定成功。成功則賦給p->pid
*/
pid = alloc_pid(task_active_pid_ns(p));
if (!pid)
goto bad_fork_cleanup_namespaces;
if (clone_flags & CLONE_NEWPID) {
retval = pid_ns_prepare_proc(task_active_pid_ns(p));
if (retval < 0)
goto bad_fork_free_pid;
}
}
/* 将子程序的PID設定為配置設定的PID在全局namespace中配置設定的值,在不同namespace中程序的PID不同,而p->pid儲存的是全局的namespace中所配置設定的PID */
p->pid = pid_nr(pid);
p->tgid = p->pid;
if (clone_flags & CLONE_THREAD)
p->tgid = current->tgid; /* 線程組的所有線程的tgid都一緻,使用getpid傳回的就是tgid */
/* 如果設定了CLONE_CHILD_SETTID則将task_struct中的set_child_tid指向使用者空間的child_tidptr,否則置空 */
p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL;
/* 如果設定了CLONE_CHILD_CLEARTID則将task_struct中的clear_child_tid指向使用者空間的child_tidptr,否則置空 */
p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr: NULL;
...
/*
* 如果共享VM或者vfork建立,信号棧清空
*/
if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM)
p->sas_ss_sp = p->sas_ss_size = 0;
clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);
/*parent_exec_id 表示父程序的執行域,p->self_exec_id 是本程序(子程序)的執行域*/
p->parent_exec_id = p->self_exec_id;
//exit_signal 為本程序執行exit()系統調用時向父程序發出的信号,
p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL);
//pdeath_signal 為要求父程序在執行exit()時向本程序發出的信号
p->pdeath_signal = 0;
p->exit_state = 0;
p->group_leader = p;
/*程序建立後必須處于某一組中,這是通過task_struct 結構中的隊列頭thread_group
與父程序連結起來,形成一個程序組(注意,thread 并不單指線程,核心代碼中經常用thread
通指所有的程序)*/
INIT_LIST_HEAD(&p->thread_group);
INIT_LIST_HEAD(&p->ptrace_children);
INIT_LIST_HEAD(&p->ptrace_list);
...
/* 将調用fork的程序為其父程序 */
if (clone_flags & (CLONE_PARENT|CLONE_THREAD))
/* 建立的是兄弟程序或者相同線程組線程,其父程序為父程序的父程序 */
p->real_parent = current->real_parent;
else
/* 建立的是子程序,父程序為父程序*/
p->real_parent = current;
p->parent = p->real_parent;
spin_lock(¤t->sighand->siglock);
/*
* 在fork之前,程序組和會話信号都需要送到父親結點,而在fork之後,這些信号需要送到父親和孩子結點。
* 如果我們在将新程序添加到程序組的過程中出現一個信号,而這個挂起信号會導緻目前程序退出(current),我們的子程序就不能夠被kill或者退出了
* 是以這裡要檢測父程序有沒有信号被挂起。
*/
recalc_sigpending();
if (signal_pending(current)) {
spin_unlock(¤t->sighand->siglock);
write_unlock_irq(&tasklist_lock);
retval = -ERESTARTNOINTR;
goto bad_fork_free_pid;
}
if (clone_flags & CLONE_THREAD) {
...
}
if (likely(p->pid)) {
add_parent(p);
if (unlikely(p->ptrace & PT_PTRACED))
__ptrace_link(p, current->parent);
if (thread_group_leader(p)) {
if (clone_flags & CLONE_NEWPID)
p->nsproxy->pid_ns->child_reaper = p;
p->signal->tty = current->signal->tty;
set_task_pgrp(p, task_pgrp_nr(current));
set_task_session(p, task_session_nr(current));
attach_pid(p, PIDTYPE_PGID, task_pgrp(current));
attach_pid(p, PIDTYPE_SID, task_session(current));
/* 将此程序task_struct加入到task連結清單中 */
list_add_tail_rcu(&p->tasks, &init_task.tasks);
__get_cpu_var(process_counts)++;
}
attach_pid(p, PIDTYPE_PID, pid);
/* 目前系統程序數加1 */
nr_threads++;
}
/* 已建立的程序數量加1 */
total_forks++;
spin_unlock(¤t->sighand->siglock);
write_unlock_irq(&tasklist_lock);
proc_fork_connector(p);
cgroup_post_fork(p);
/*傳回子程序的task_struct,通過sched_fork調用把子程序狀态設定為可運作狀态,但是子程序還沒有運作,
後續子程序的排程運作取決于schedule()排程程式
*/
return p;
//錯誤處理
...
}
copy_process 處理流程如下:
- 調用dup_task_struct為新程序建立一個核心棧、thread_info 和 task_struct 結構,這些資訊和父程序内容相同。此時父子程序的描述符時完全相同的。
- 對資源限制進行檢查,保證新建立子程序後,目前使用者所擁有的的程序數沒有超過給它配置設定的資源的限制。
- 對子程序的一些資訊開始設定初始值,主要是一些統計資訊等。
- 調用 sched_fork 函數執行排程器相關的設定,為這個新程序配置設定 CPU,把子程序的程序狀态為 TASK_RUNNING。
- 複制程序的資源資訊比如打開的檔案、檔案系統資訊,信号處理函數、程序位址空間、指令空間等。
- 調用 copy_thread 初始化子程序核心棧。
- 為新程序配置設定并設定新的 pid。
- 傳回 task_struct 程序描述符。
該函數的流程如下:
通過上述 copy_process 可知,子程序完全複制複制了父程序的一些資源資訊,如下圖
dup_task_struct 完成了子程序核心棧的建立
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
int err;
prepare_to_copy(orig);
//配置設定一塊task_struct
tsk = alloc_task_struct();
//配置設定一個 thread_info 節點,包含程序的核心棧,ti 為棧底
ti = alloc_thread_info(tsk);
//把父程序task_struct内容複制給子程序
*tsk = *orig;
//子程序的task_struct指向棧中的thread_info
tsk->stack = ti;
...
//把父程序thread_info複制給子程序的thread_info,然後子程序的thread_info指向子程序的task_struct
setup_thread_stack(tsk, orig);
...
return tsk;
}
dup_task_struct 為新程序建立一個核心棧、thread_info 和 task_struct 結構,這些結構中的資訊完全複制了父程序資訊,同時完成了 thread_info 和 task_struct 之間的關系,如下圖
在 copy_process 中通過 dup_task_struct 為子程序配置設定了描述結構并初始化,完成核心棧的低端資料的初始化,而用作核心堆棧的高端複制初始化由 copy_thread 來完成。
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;
//參數regs是儲存這個cpu進入核心前夕各個寄存器中的内容而形成的一個pt_regs結構
childregs = task_pt_regs(p); //指向核心棧的最高位址
*childregs = *regs; //把寄存器中的值存放到核心棧的最高位址
//對子程序的核心棧寄存器中的值進行調整
childregs->eax = 0; //将eax設定0,子程序被排程運作從系統調用傳回時,就傳回該值,這也就為什麼fork時子程序傳回的是0
/*
将esp設定成參數的esp,因為它決定了程序在使用者空間的堆棧位置.
對于clone()調用,參數esp是由調用者給定的。
對于fork() 和 vfork(), 參數esp來自do_fork()前夕的regs.esp, 是以實際上并未改變,還是執行父程序原來的使用者空間的堆棧
*/
childregs->esp = esp;
/*
task_struct 中的thread,它本身是一個thread_struct結構,裡面記錄的是程序在切換時的(系統空間)堆棧指針,
取指令位址(也即是傳回位址)等關鍵資訊。在複制時原封不動的複制了,但是子程序也有自己的核心空間堆棧,是以
也需要進行調整。
*/
//将thread.esp設定成子程序系統空間棧中pt_regs結構的其實位址,就好像該子程序以前曾運作過,而在進入核心以後正要傳回使用者空間時被切換了一樣
p->thread.esp = (unsigned long) childregs;
//esp0 指向子程序的系統空間堆棧的頂端。當一個程序被排程運作時,核心會将這個變量寫入TSS 的 esp0 字段,表示這個程序進入0級運作時其堆棧的位置
p->thread.esp0 = (unsigned long) (childregs+1);
//eip表示當程序下一次被切換進行運作時的切入點,類似于函數調用或中斷的傳回位址。将此位址設定為ret_from_fork,使得子程序在首次排程運作時從這開始
p->thread.eip = (unsigned long) ret_from_fork;
//把段寄存器gs的值儲存到p->thread.gs中
savesegment(gs,p->thread.gs);
//在父程序包含I/O通路許可權限位圖的情況下,使新建立程序繼承父程序的I/O通路許可權限位圖
...
/*
* Set a new TLS for the child thread?
*/
//在參數clone_flags包含CLONE_SETTLS标記的情況下,設定程序的TLS
...
return err;
}
通過 copy_thread 初始化子程序核心棧的高端位址,修改其中的寄存器,保證了子程序被排程運作傳回時能夠和父程序進行了區分。
我們知道應用程調用 fork() 會傳回2次,父程序傳回的是子程序的 id, 子程序傳回0,那子程序是怎麼傳回的呢?
在 copy_thread 函數将子程序的 eip 寄存器值設定為 ret_from_fork 的位址,同時将 eax 寄存器中的值指派為0(eax 記錄的就是函數傳回時的值)。
ENTRY(ret_from_fork)
CFI_STARTPROC
pushl %eax
CFI_ADJUST_CFA_OFFSET 4
call schedule_tail
GET_THREAD_INFO(%ebp)
popl %eax
CFI_ADJUST_CFA_OFFSET -4
pushl $0x0202 # Reset kernel eflags
CFI_ADJUST_CFA_OFFSET 4
popfl
CFI_ADJUST_CFA_OFFSET -4
jmp syscall_exit
CFI_ENDPROC
END(ret_from_fork)
當子程序被排程運作時,子程序進入 ret_from_fork,在調用完 schedule_tail 後調到 syscall_exit 結束系統調用傳回到使用者空間,使用者空間從 eax 寄存器中擷取傳回值0,也即是調用 fork 的傳回值。
寫時拷貝
在 fork() 建立程序的過程中, Linux 采用了寫時拷貝(copy-on-write)頁的技術,該技術就是一種可以延遲拷貝或免除拷貝的技術。
其原理就是先通過複制頁表項暫時共享這個實體記憶體頁。當從父程序複制頁表項時會把父程序的頁表項改成寫保護,然後把改成寫保護的頁表項設定到子程序的頁表中。這樣2個程序的頁面都變成“隻讀”的了。當不管父程序還是子程序企圖寫入該頁面時,都會引起一次頁面異常,而頁面異常處理程式會對此的反應是為其配置設定一個實體頁,并把内容真正的複制到新的實體頁面中。此時父子程序各自擁有自己的實體頁面,然後将這2個頁面表中相應的表項改成可寫。
寫時拷貝技術避免了在建立程序過程中進行大量根本就不會使用的資料進行拷貝而帶來的開銷。
fork() 的實際開銷就是複制父程序的頁表以及給子程序建立唯一的程序描述符的過程。
有關寫時拷貝技術原理如下
在 fork() 後子程序完全複制了父程序的頁表,但是沒有複制實體頁,這個時候兩個程序的虛拟位址和實體位址都是相同的,子程序和父程序使用同一份實體記憶體頁,這時的頁面标記時“隻讀”的。
當某個程序進行修改記憶體時,比如子程序進行修改記憶體操作,這個時候作業系統系統會把父程序的實體頁拷貝一份給子程序,同時修改頁表,子程序在新配置設定的實體頁中進行修改,這個時候父子實體記憶體也就分開了。
是以,在子程序複制父程序的位址空間和頁表後,父子程序都有獨立的mm_struct 和 各級頁表,且其值均相等。最關鍵的就是下表中紅色的部分,所有可寫的頁表項均設定為不可寫,當某個程序進行寫通路時,就會觸發缺頁異常中斷。
實作紅色部分屬性修改的函數調用流程如下:
copy_mm()
--> dup_mm()
--> dup_mmap()
--> copy_page_range()
--> copy_pud_range()
--> copy_pmd_range()
--> copy_pte_range()
--> copy_one_pte()
--> ptep_set_wrprotect()
static inline void
copy_one_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm,
pte_t *dst_pte, pte_t *src_pte, struct vm_area_struct *vma,
unsigned long addr, int *rss)
{
...
/*
* If it's a COW mapping, write protect it both
* in the parent and the child
*/
if (is_cow_mapping(vm_flags)) {
ptep_set_wrprotect(src_mm, addr, src_pte);
pte = pte_wrprotect(pte);
}
...
}
從代碼中可以看到,父子程序的頁表項均設定成了寫保護屬性。
vfork 原理
在linux中還有一種建立程序的方式,那就是vfork。
除了不拷貝父程序的頁表項外,vfork( ) 系統調用和 fork() 系統調用功能相同。子程序作為父程序的一個單獨的線程在它的位址空間裡運作,父程序被阻塞,直到子程序退出或者執行 execv()。
vfork 系統調用最終還是通過 do_fork 系統調用完成的,如下
int sys_vfork(long r10, long r11, long r12, long r13, long mof, long srp, struct pt_regs *regs)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, rdusp(), regs, 0, NULL, NULL);
}
在調用 do_fork 時 vfork 比 fork的 clone_flags 參數多了 CLONE_VFORK | CLONE_VM 标志,接下來通過這2個标志來進行分析。
- vfork 在調用 copy_process 時,由于存在 CLONE_VM 标志,是以在 拷貝 copy_mm 時子程序并不對父程序 mm_struct 結構進行複制,而是子程序指向父程序的 mm_struct結構進行共享。
- 在執行 do_fork 時,子程序的 vfork_done結構會指向一個特定的位址。
- 子程序先開始執行後,父程序不是馬上恢複執行,而是一直等待,直到子程序通過 vfork_done 指針向它發送信号。
- 在調用 mm_release() 時,該函數用于程序退出記憶體位址空間,并且檢查 vfork_done 是否為空,若不為空,則會向父程序發送信号。
- 回到 do_fork,父程序醒來并傳回。
若一切執行順利,子程序在新的位址空間裡運作而父程序也恢複了在原位址空間的運作。
由于子程序指向父程序的mm_struct結構,是以當子程序修改資料的時候父程序能夠感覺到。
建立線程
Linux 中實作線程的機制很特别。從核心的角度來看,并沒有線程的概念。Linux 把所有的線程當做程序來實作。線程僅僅被視為一個與其他程序共享某些資源的程序。每個線程都擁有唯一隸屬于自己的 task_struct。是以在核心中,它看起來像是一個普通的程序(隻是線程和其他一些程序共享某些資源,比如位址空間等)。
在使用者态中我們常用 pthread_create 來建立線程,而 pthread_create 在libc 庫中調用 create_thread(), 最終調用 clone()。
__pthread_create_2_1 ()
-->ALLOCATE_STACK () 配置設定線程棧空間
--> create_thread ()
--> __clone2 ()
從上述調用過程可以知道,線程在建立時候,通過 libc 庫建立了線程的棧,是以每個線程都有自己的私有棧。
在核心實作中,最終還是調用 do_fork。
int sys_clone(unsigned long clone_flags, unsigned long usp,
int __user *parent_tidp, void __user *child_threadptr,
int __user *child_tidp, int p6,
struct pt_regs *regs)
{
...
return do_fork(clone_flags, usp, regs, 0, parent_tidp, child_tidp);
}
線程的建立和普通程序的建立類似,隻不過在調用 clone() 的時候需要傳遞一些參數标志來指明需要共享的資源。
const int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM
| CLONE_SIGHAND | CLONE_THREAD
| CLONE_SETTLS | CLONE_PARENT_SETTID
| CLONE_CHILD_CLEARTID
| 0);
從上面的标志位可以知道,線程共享了父程序的位址空間、打開的檔案、檔案系統資訊、信号處理函數及被阻斷的信号等資訊。
exec調用
有關 exec 系列函數的調用,本文不再分析,可以參考文章 《Linux 可執行檔案程式載入和執行過程》。