郭垚 原創做品轉載請註明出處 《Linux內核分析》MOOC課程http://mooc.study.163.com/course/USTC-1000029000node
【學習視頻時間:1小時 撰寫博客時間:2小時】編程
【學習內容:進程建立的過程、使用gdb跟蹤分析內核處理函數sys_clone】數據結構
1. 進程控制塊PCB——task_structdom
爲了管理進程,內核必須對每一個進程進行清晰的描述,進程描述符提供了內核所需瞭解的進程信息。函數
struct task_struct{ volatile long state; //進程狀態,-1表示不可執行,0表示可執行,大於1表示中止 void *stack; //內核堆棧 atomic_t usage; unsigned int flags; //進程標識符 unsigned int ptrace; …… }
struct task_ struct數據結構很龐大學習
Linux進程狀態轉換圖this
1242#ifdef CONFIG_SMP //條件編譯,多處理器會用到 1243 struct llist_node wake_entry; 1244 int on_cpu; 1245 struct task_struct *last_wakee; 1246 unsigned long wakee_flips; 1247 unsigned long wakee_flip_decay_ts; 1248 1249 int wake_cpu; 1250#endif 1251 int on_rq; 1252 1253 int prio, static_prio, normal_prio; 1254 unsigned int rt_priority; //與優先級相關 1255 const struct sched_class *sched_class; 1256 struct sched_entity se; 1257 struct sched_rt_entity rt; …… 1295 struct list_head tasks; //進程鏈表 1296#ifdef CONFIG_SMP 1297 struct plist_node pushable_tasks; 1298 struct rb_node pushable_dl_tasks; 1299#endif
Linux的狀態圖atom
1301 struct mm_struct *mm, *active_mm; //內存管理進程的地址空間 1302#ifdef CONFIG_COMPAT_BRK 1303 unsigned brk_randomized:1; 1304#endif 1305 /* per-thread vma caching */ 1306 u32 vmacache_seqnum; 1307 struct vm_area_struct *vmacache[VMACACHE_SIZE]; 1308#if defined(SPLIT_RSS_COUNTING) 1309 struct task_rss_stat rss_stat; 1310#endif 1311/* task state */ 1312 int exit_state; //任務的狀態 1313 int exit_code, exit_signal; 1314 int pdeath_signal; /* The signal sent when the parent dies */ 1315 unsigned int jobctl; /* JOBCTL_*, siglock protected */ 1316 1317 /* Used for emulating ABI behavior of previous Linux versions */ 1318 unsigned int personality; 1319 1320 unsigned in_execve:1; /* Tell the LSMs that the process is doing an 1321 * execve */ 1322 unsigned in_iowait:1; …… 1337 //進程的父子關係 1338 * pointers to (original) parent process, youngest child, younger sibling, 1339 * older sibling, respectively. (p->father can be replaced with 1340 * p->real_parent->pid) 1341 */ 1342 struct task_struct __rcu *real_parent; /* real parent process */ 1343 struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */ 1344 /* 1345 * children/sibling forms the list of my natural children 1346 */ 1347 struct list_head children; /* list of my children */ 1348 struct list_head sibling; /* linkage in my parent's children list */ 1349 struct task_struct *group_leader; /* threadgroup leader */
1356 struct list_head ptraced; //調試 1357 struct list_head ptrace_entry; 1358 1359 /* PID/PID hash table linkage. */ 1360 struct pid_link pids[PIDTYPE_MAX]; 1361 struct list_head thread_group; 1362 struct list_head thread_node; //將進程鏈表鏈接起來 …… 1411/* 與當前任務CPU狀態相關,對進程上下文切換有關鍵性做用 */ 1412 struct thread_struct thread; 1413/* filesystem information */ 1414 struct fs_struct *fs; 1415/* 打開文件描述符列表 */ 1416 struct files_struct *files; 1417/* namespaces */ 1418 struct nsproxy *nsproxy; 1419/* 信號處理 */ 1420 struct signal_struct *signal; 1421 struct sighand_struct *sighand; 1422 1423 sigset_t blocked, real_blocked; 1424 sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */ 1425 struct sigpending pending; …… 1598 struct pipe_inode_info *splice_pipe; 1599 //與管道相關 1600 struct page_frag task_frag; 1601 1602#ifdef CONFIG_TASK_DELAY_ACCT 1603 struct task_delay_info *delays; 1604#endif 1605#ifdef CONFIG_FAULT_INJECTION 1606 int make_it_fail; 1607#endif
回顧:spa
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) //pid == 0和下面的else都會被執行到(一個是在父進程中即pid ==0的狀況,一個是在子進程中,即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"); } }
Linux中建立進程一共有三個函數:
這裏值得注意的是,Linux中得線程是經過模擬進程實現的,較新的內核使用的線程庫通常都是NPTL。
複製一個PCB——task_struct
err = arch_dup_task_struct(tsk, orig);
給新進程分配一個新的內核堆棧
ti = alloc_ thread_ info_ node(tsk, node); tsk->stack = ti; setup_ thread_ stack(tsk, orig); //這裏只是複製thread_ info,而非複製內核堆棧
經過以前的學習,咱們知道fork是經過觸發0x80中斷,陷入內核,來使用內核提供的提供調用:
SYSCALL_DEFINE0(fork) { return do_fork(SIGCHLD, 0, 0, NULL, NULL); } #endif SYSCALL_DEFINE0(vfork) { return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0, 0, NULL, NULL); } SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp, int __user *, parent_tidptr, int __user *, child_tidptr, int, tls_val) { return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr); }
經過以上精簡後的代碼,咱們能夠看出,fork、vfork和clone這三個函數最終都是經過do_ fork函數實現的。
追蹤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; // ... // 複製進程描述符,返回建立的task_struct的指針 p = copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace); if (!IS_ERR(p)) { struct completion vfork; struct pid *pid; trace_sched_process_fork(current, p); // 取出task結構體內的pid pid = get_task_pid(p, PIDTYPE_PID); nr = pid_vnr(pid); if (clone_flags & CLONE_PARENT_SETTID) put_user(nr, parent_tidptr); // 若是使用的是vfork,那麼必須採用某種完成機制,確保父進程後運行 if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); get_task_struct(p); } // 將子進程添加到調度器的隊列,使得子進程有機會得到CPU wake_up_new_task(p); // ... // 若是設置了 CLONE_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; }
由以上代碼能夠看出,do_ fork大概作了以下幾件事:
拋開vfork,進程建立的大部分過程都在copy_ process函數中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; // 分配一個新的task_struct,此時的p與當前進程的task,僅僅是stack地址不一樣 p = dup_task_struct(current); // 檢查該用戶的進程數是否超過限制 if (atomic_read(&p->real_cred->user->processes) >= task_rlimit(p, RLIMIT_NPROC)) { // 檢查該用戶是否具備相關權限,不必定是root if (p->real_cred->user != INIT_USER && !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN)) goto bad_fork_free; } retval = -EAGAIN; // 檢查進程數量是否超過 max_threads,後者取決於內存的大小 if (nr_threads >= max_threads) goto bad_fork_cleanup_count; // 初始化自旋鎖 // 初始化掛起信號 // 初始化定時器 // 完成對新進程調度程序數據結構的初始化,並把新進程的狀態設置爲TASK_RUNNING retval = sched_fork(clone_flags, p); // ..... // 複製全部的進程信息 // copy_xyz // 初始化子進程的內核棧 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號 pid = alloc_pid(p->nsproxy->pid_ns_for_children); if (!pid) goto bad_fork_cleanup_io; } /* ok, now we should be set up.. */ // 設置子進程的pid p->pid = pid_nr(pid); // 若是是建立線程 if (clone_flags & CLONE_THREAD) { p->exit_signal = -1; // 線程組的leader設置爲當前線程的leader p->group_leader = current->group_leader; // tgid是當前線程組的id,也就是main進程的pid 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; // tgid和pid相同 p->tgid = p->pid; } if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) { // 若是是建立線程,那麼同一線程組內的全部線程、進程共享parent p->real_parent = current->real_parent; p->parent_exec_id = current->parent_exec_id; } else { // 若是是建立進程,當前進程就是子進程的parent p->real_parent = current; p->parent_exec_id = current->self_exec_id; } // 將pid加入PIDTYPE_PID這個散列表 attach_pid(p, PIDTYPE_PID); // 遞增 nr_threads的值 nr_threads++; // 返回被建立的task結構體指針 return p; }
根據這份精簡代碼,我總結出copy_process的大致流程:
一個新建立的子進程,(當它得到CPU以後)是從哪一行代碼進程執行的?
這涉及子進程的內核堆棧數據狀態和task_ struct中thread記錄的sp和ip的一致性問題,這是在copy_ thread in copy_ process設定的
*childregs = *current_pt_regs(); //複製內核堆棧 childregs->ax = 0; //子進程的fork返回0 p->thread.sp = (unsigned long) childregs; //調度到子進程時的內核棧頂 p->thread.ip = (unsigned long) ret_from_fork; //調度到子進程時的第一條指令地址
1. 更新menu,刪除test_fork.c和test.c文件,從新執行make rootfs
2. 編譯內核查看fork命令
3. 啓動gdb調試,並對主要的函數設置斷點
執行一個fork,會發現只輸出一個fork的命令描述,後面並無執行,由於它停在了sys_ clone這個位置。
4. 特別關注新進程是從哪裏開始執行的?爲何從哪裏能順利執行下去?即執行起點與內核堆棧如何保證一致。
答:ret_ from_ fork決定了新進程的第一條指令地址。子進程從ret_ from_ fork處開始執行。由於在ret_ from_ fork以前,也就是在copy_ thread()函數中* childregs = * current_ pt_ regs();該句將父進程的regs參數賦值到子進程的內核堆棧。* childregs的類型爲pt_ regs,裏面存放了SAVE_ ALL中壓入棧的參數,所以在以後的RESTORE ALL中能順利執行下去。
Linux中全部的進程建立都是基於複製的方式,Linux經過複製父進程來建立一個新進程,經過調用do_ fork來實現。而後對子進程作一些特殊的處理。而Linux中的線程,又是一種特殊的進程。根據代碼的分析,do_ fork中,copy_ process管子進程運行的準備,wake_ up_ new_ task做爲子進程forking的完成。fork()函數最大的特色就是被調用一次,返回兩次。