Unix標準的複製進程的系統調用時fork(即分叉),可是Linux,BSD等操做系統並不止實現這一個,確切的說linux實現了三個,fork,vfork,clone(確切說vfork創造出來的是輕量級進程,也叫線程,是共享資源的進程)node
系統調用 | 描述 |
---|---|
fork | fork創造的子進程是父進程的完整副本,複製了父親進程的資源,包括內存的內容task_struct內容 |
vfork | vfork建立的子進程與父進程共享數據段,並且由vfork()建立的子進程將先於父進程運行 |
clone | Linux上建立線程通常使用的是pthread庫 實際上linux也給咱們提供了建立線程的系統調用,就是clone |
fork, vfork和clone的系統調用的入口地址分別是sys_fork, sys_vfork和sys_clone, 而他們的定義是依賴於體系結構的, 由於在用戶空間和內核空間之間傳遞參數的方法因體系結構而異linux
系統調用的參數傳遞c#
系統調用的實現與C庫不一樣, 普通C函數經過將參數的值壓入到進程的棧中進行參數的傳遞。因爲系統調用是經過中斷進程從用戶態到內核態的一種特殊的函數調用,沒有用戶態或者內核態的堆棧能夠被用來在調用函數和被調函數之間進行參數傳遞。系統調用經過CPU的寄存器來進行參數傳遞。在進行系統調用以前,系統調用的參數被寫入CPU的寄存器,而在實際調用系統服務例程以前,內核將CPU寄存器的內容拷貝到內核堆棧中,實現參數的傳遞。數據結構
所以不一樣的體系結構可能採用不一樣的方式或者不一樣的寄存器來傳遞參數,而上面函數的任務就是從處理器的寄存器中提取用戶空間提供的信息, 並調用體系結構無關的_do_fork(或者早期的do_fork)函數, 負責進程的複製架構
即不一樣的體系結構可能須要採用不一樣的方式或者寄存器來存儲函數調用的參數, 所以linux在設計系統調用的時候, 將其劃分紅體系結構相關的層次和體系結構無關的層次, 前者複雜提取出依賴與體系結構的特定的參數, 後者則依據參數的設置執行特定的真正操做app
The commit 3033f14ab78c32687 (「clone: support passing tls argument via C
rather than pt_regs magic」) introduced _do_fork() that allowed to pass
@tls parameter.electron
linux2.5.32之後, 添加了TLS(Thread Local Storage)機制, clone的標識CLONE_SETTLS接受一個參數來設置線程的本地存儲區。sys_clone也所以增長了一個int參數來傳入相應的點tls_val。sys_clone經過do_fork來調用copy_process完成進程的複製,它調用特定的copy_thread和copy_thread把相應的系統調用參數從pt_regs寄存器列表中提取出來,可是會致使意外的狀況。學習
only one code path into copy_thread can pass the CLONE_SETTLS flag, and
that code path comes from sys_clone with its architecture-specific
argument-passing order.ui
所以linux-4.2以後選擇引入一個新的CONFIG_HAVE_COPY_THREAD_TLS,和一個新的COPY_THREAD_TLS接受TLS參數爲額外的長整型(系統調用參數大小)的爭論。改變sys_clone的TLS參數unsigned long,並傳遞到copy_thread_tls。
/* http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L2646 */ extern long _do_fork(unsigned long, unsigned long, unsigned long, int __user *, int __user *, unsigned long); extern long do_fork(unsigned long, unsigned long, unsigned long, int __user *, int __user *); /* linux2.5.32之後, 添加了TLS(Thread Local Storage)機制, 在最新的linux-4.2中添加了對CLONE_SETTLS 的支持 底層的_do_fork實現了對其的支持, dansh*/ #ifndef CONFIG_HAVE_COPY_THREAD_TLS /* For compatibility with architectures that call do_fork directly rather than * using the syscall entry points below. */ long do_fork(unsigned long clone_flags, unsigned long stack_start, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr) { return _do_fork(clone_flags, stack_start, stack_size, parent_tidptr, child_tidptr, 0); } #endif
咱們會發現,新版本的系統中clone的TLS設置標識會經過TLS參數傳遞, 所以_do_fork替代了老版本的do_fork。
老版本的do_fork只有在以下狀況纔會定義
參數 | 描述 |
---|---|
clone_flags | 與clone()參數flags相同, 用來控制進程複製過的一些屬性信息, 描述你須要從父進程繼承那些資源。該標誌位的4個字節分爲兩部分。最低的一個字節爲子進程結束時發送給父進程的信號代碼,一般爲SIGCHLD;剩餘的三個字節則是各類clone標誌的組合(本文所涉及的標誌含義詳見下表),也就是若干個標誌之間的或運算。經過clone標誌能夠有選擇的對父進程的資源進行復制; |
stack_start | 與clone()參數stack_start相同, 子進程用戶態堆棧的地址 |
regs | 是一個指向了寄存器集合的指針, 其中以原始形式, 保存了調用的參數, 該參數使用的數據類型是特定體系結構的struct pt_regs,其中按照系統調用執行時寄存器在內核棧上的存儲順序, 保存了全部的寄存器, 即指向內核態堆棧通用寄存器值的指針,通用寄存器的值是在從用戶態切換到內核態時被保存到內核態堆棧中的(指向pt_regs結構體的指針。當系統發生系統調用,即用戶進程從用戶態切換到內核態時,該結構體保存通用寄存器中的值,並被存放於內核態的堆棧中) |
stack_size | 用戶狀態下棧的大小, 該參數一般是沒必要要的, 總被設置爲0 |
parent_tidptr | 與clone的ptid參數相同, 父進程在用戶態下pid的地址,該參數在CLONE_PARENT_SETTID標誌被設定時有意義 |
child_tidptr | 與clone的ctid參數相同, 子進程在用戶太下pid的地址,該參數在CLONE_CHILD_SETTID標誌被設定時有意義 |
其中clone_flags以下表所示
不一樣體系結構下的fork實現sys_fork主要是經過標誌集合區分, 在大多數體系結構上, 典型的fork實現方式與以下
早期實現:
架構 | 實現 |
---|---|
arm | arch/arm/kernel/sys_arm.c, line 239 |
i386 | arch/i386/kernel/process.c, line 710 |
x86_64 | arch/x86_64/kernel/process.c, line 706 |
asmlinkage long sys_fork(struct pt_regs regs) { return do_fork(SIGCHLD, regs.rsp, ®s, 0); }
新版本:
#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
咱們能夠看到惟一使用的標誌是SIGCHLD。這意味着在子進程終止後將發送信號SIGCHLD信號通知父進程,
因爲寫時複製(COW)技術, 最初父子進程的棧地址相同, 可是若是操做棧地址閉並寫入數據, 則COW機制會爲每一個進程分別建立一個新的棧副本
若是do_fork成功, 則新建進程的pid做爲系統調用的結果返回, 不然返回錯誤碼
早期實現
架構 | 實現 |
---|---|
arm | arch/arm/kernel/sys_arm.c, line 254 |
i386 | arch/i386/kernel/process.c, line 737 |
x86_64 | arch/x86_64/kernel/process.c, line 728 |
asmlinkage long sys_vfork(struct pt_regs regs) { return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.rsp, ®s, 0); }
新版本
#ifdef __ARCH_WANT_SYS_VFORK SYSCALL_DEFINE0(vfork) { return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0, 0, NULL, NULL, 0); } #endif
能夠看到sys_vfork的實現與sys_fork只是略微不一樣, 前者使用了額外的標誌CLONE_VFORK | CLONE_VM
早期實現
架構 | 實現 |
---|---|
arm | arch/arm/kernel/sys_arm.c, line 247 |
i386 | arch/i386/kernel/process.c, line 715 |
x86_64 | arch/x86_64/kernel/process.c, line 711 |
sys_clone的實現方式與上述系統調用相似, 但實際差異在於do_fork以下調用
casmlinkage int sys_clone(struct pt_regs regs) { /* 註釋中是i385下增長的代碼, 其餘體系結構無此定義 unsigned long clone_flags; unsigned long newsp; clone_flags = regs.ebx; newsp = regs.ecx;*/ if (!newsp) newsp = regs.esp; return do_fork(clone_flags, newsp, ®s, 0); }
新版本
#ifdef __ARCH_WANT_SYS_CLONE #ifdef CONFIG_CLONE_BACKWARDS SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp, int __user *, parent_tidptr, unsigned long, tls, 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, unsigned long, tls) #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, unsigned long, tls) #else SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp, int __user *, parent_tidptr, int __user *, child_tidptr, unsigned long, tls) #endif { return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls); } #endif
咱們能夠看到sys_clone的標識再也不是硬編碼的, 而是經過各個寄存器參數傳遞到系統調用, 於是咱們須要提取這些參數。
另外,clone也再也不復制進程的棧, 而是能夠指定新的棧地址, 在生成線程時, 可能須要這樣作, 線程可能與父進程共享地址空間, 可是線程自身的棧可能在另一個地址空間
另外還指令了用戶空間的兩個指針(parent_tidptr和child_tidptr), 用於與線程庫通訊
_do_fork和do_fork在進程的複製的時候並無太大的區別, 他們就只是在進程tls複製的過程當中實現有細微差異
全部進程複製(建立)的fork機制最終都調用了kernel/fork.c中的_do_fork(一個體繫結構無關的函數),
_do_fork以調用copy_process開始, 後者執行生成新的進程的實際工做, 並根據指定的標誌複製父進程的數據。在子進程生成後, 內核必須執行下列收尾操做:
對比,咱們從《深刻linux內核架構》中找到了早期的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; /* * 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; } /* 複製進程描述符,copy_process()的返回值是一個 task_struct 指針 */ p = copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace, tls); /* * 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); /* 獲得新建立的進程的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; }
對比,咱們從《深刻linux內核架構》中找到了早期的do_fork流程圖,基本一致,能夠用來參考學習和對比
主要的區別其實就是最後的copy_thread更改爲爲copy_thread_tls
/* * This creates a new process as a copy of the old one, * but does not actually start it yet. * * It copies the registers, and all the appropriate * parts of the process environment (as per the clone * flags). The actual kick-off is left to the caller. */ 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) { int retval; struct task_struct *p; retval = security_task_create(clone_flags); if (retval) goto fork_out; // 複製當前的 task_struct retval = -ENOMEM; p = dup_task_struct(current); if (!p) goto fork_out; 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; 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; 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. */ //檢查進程數是否超過 max_threads 由內存大小決定 retval = -EAGAIN; if (nr_threads >= max_threads) goto bad_fork_cleanup_count; 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); // 初始化 CPU 定時器 posix_cpu_timers_init(p); // ...... /* Perform scheduler related setup. Assign this task to a CPU. 初始化進程數據結構,並把進程狀態設置爲 TASK_RUNNING */ retval = sched_fork(clone_flags, p); if (retval) goto bad_fork_cleanup_policy; retval = perf_event_init_task(p); /* 複製全部進程信息,包括文件系統、信號處理函數、信號、內存管理等 形式相似於copy_xxx的形式 */ 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; /* 初始化子進程內核棧 linux-4.2新增處理TLS 以前版本是 retval = copy_thread(clone_flags, stack_start, stack_size, p); */ retval = copy_thread_tls(clone_flags, stack_start, stack_size, p, tls); if (retval) goto bad_fork_cleanup_io; /* 爲新進程分配新的pid */ 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_io; } } /* 設置子進程的pid */ /* ok, now we should be set up.. */ 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; } p->nr_dirtied = 0; p->nr_dirtied_pause = 128 >> (PAGE_SHIFT - 10); p->dirty_paused_when = 0; p->pdeath_signal = 0; INIT_LIST_HEAD(&p->thread_group); p->task_works = NULL; /* * Make it visible to the rest of the system, but dont wake it up yet. * Need tasklist lock for parent etc handling! */ write_lock_irq(&tasklist_lock); /* 調用fork的進程爲其父進程 */ /* CLONE_PARENT re-uses the old parent */ if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) { p->real_parent = current->real_parent; p->parent_exec_id = current->parent_exec_id; } else { p->real_parent = current; p->parent_exec_id = current->self_exec_id; } spin_lock(¤t->sighand->siglock); // ...... return p; }
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 節點,包含進程的內核棧,ti 爲棧底 ti = alloc_thread_info_node(tsk, node); if (!ti) goto free_tsk; //將棧底的值賦給新節點的棧 tsk->stack = ti; //…… return tsk; }
union thread_union { struct thread_info thread_info; unsigned long stack[THREAD_SIZE/sizeof(long)]; };
int sched_fork(unsigned long clone_flags, struct task_struct *p) { unsigned long flags; int cpu = get_cpu(); __sched_fork(clone_flags, p); // 將子進程狀態設置爲 TASK_RUNNING p->state = TASK_RUNNING; // …… // 爲子進程分配 CPU set_task_cpu(p, cpu); put_cpu(); return 0; }
咱們能夠看到sched_fork大體完成了兩項重要工做,
咱們能夠看到linux-4.2以後增長了copy_thread_tls函數和CONFIG_HAVE_COPY_THREAD_TLS宏
可是若是未定義CONFIG_HAVE_COPY_THREAD_TLS宏默認則使用copy_thread同時將定義copy_thread_tls爲copy_thread
單獨將這個函數是由於這個複製操做與其餘操做都不相同, 這是一個特定於體系結構的函數,用於複製進程中特定於線程(thread-special)的數據, 重要的就是填充task_struct->thread的各個成員,這是一個thread_struct類型的結構, 其定義是依賴於體系結構的。它包含了全部寄存器(和其餘信息),內核在進程之間切換時須要保存和恢復的進程的信息。
該函數用於設置子進程的執行環境,如子進程運行時各CPU寄存器的值、子進程的內核棧的起始地址(指向內核棧的指針一般也是保存在一個特別保留的寄存器中)
#ifdef CONFIG_HAVE_COPY_THREAD_TLS extern int copy_thread_tls(unsigned long, unsigned long, unsigned long, struct task_struct *, unsigned long); #else extern int copy_thread(unsigned long, unsigned long, unsigned long, struct task_struct *); /* Architectures that haven't opted into copy_thread_tls get the tls argument * via pt_regs, so ignore the tls argument passed via C. */ static inline int copy_thread_tls( unsigned long clone_flags, unsigned long sp, unsigned long arg, struct task_struct *p, unsigned long tls) { return copy_thread(clone_flags, sp, arg, p); } #endif
下面咱們來看32位架構的copy_thread_tls函數,他與原來的copy_thread變更並不大, 只是多了後面TLS的設置信息
int copy_thread_tls(unsigned long clone_flags, unsigned long sp, unsigned long arg, struct task_struct *p, unsigned long tls) { 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; /* 子進程ip 設置爲ret_from_fork,所以子進程從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; 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); } err = 0; /* * Set a new TLS for the child thread? * 爲進程設置一個新的TLS */ if (clone_flags & CLONE_SETTLS) err = do_set_thread_area(p, -1, (struct user_desc __user *)tls, 0); if (err && p->thread.io_bitmap_ptr) { kfree(p->thread.io_bitmap_ptr); p->thread.io_bitmap_max = 0; } return err; }
copy_thread_tls 這段代碼爲咱們解釋了兩個至關重要的問題!
fork, vfork和clone的系統調用的入口地址分別是sys_fork, sys_vfork和sys_clone, 而他們的定義是依賴於體系結構的, 而他們最終都調用了_do_fork(linux-4.2以前的內核中是do_fork),在_do_fork中經過copy_process複製進程的信息,調用wake_up_new_task將子進程加入調度器中
fork系統調用對應的kernel函數是sys_fork,此函數簡單的調用kernel函數_do_fork。一個簡化版的_do_fork執行以下:
簡化的copy_process()流程
簡單的說,copy_process()就是將父進程的運行環境複製到子進程並對某些子進程特定的環境作相應的調整。
此外應用程序使用系統調用exit()來結束一個進程,此係統調用接受一個退出緣由代碼,父進程可使用wait()系統調用來獲取此代碼,從而知道子進程退出的緣由。對應到kernel,此係統調用sys_exit_group(),它的基本流程以下:
do_exit()完成線程退出的任務,其主要功能是將線程佔用的系統資源釋放,do_exit()的基本流程以下:
進程的建立到執行過程以下圖所示