學習內容:分析Linux內核建立一個新進程的過程node
分析fork函數對應的內核處理過程sys_clone,理解建立一個新進程如何建立和修改task_struct數據結構;git
使用gdb跟蹤分析一個fork系統調用內核處理函數sys_clone ,驗證對Linux系統建立一個新進程的理解,github
特別關注新進程是從哪裏開始執行的?爲何從哪裏能順利執行下去?即執行起點與內核堆棧如何保證一致。c#
一.進程分析數組
(一)進程控制塊PCB——task_structbash
對於一個進程來講,PCB就好像是他的記帳先生,當一個進程被建立時PCB就被分配,而後有關進程的全部信息就全都存儲在PCB中,例如,打開的文件,頁表基址寄存器,進程號等等。在linux中PCB是用結構task_struct來表示的,咱們首先來看一下task_struct的組成(代碼位於linux/include/linux/Sched.h)markdown
代碼以下:session
struct task_struct { long state; //表示進程的狀態,-1表示不可執行,0表示可執行,>0表示中止
long counter;/* 運行時間片,以jiffs遞減計數 */
long priority; /* 運行優先數,開始時,counter = priority,值越大,表示優先數越高,等待時間越長. */
long signal;/* 信號.是一組位圖,每個bit表明一種信號. */
struct sigaction sigaction[32]; /* 信號響應的數據結構, 對應信號要執行的操做和標誌信息 */
long blocked; /* 進程信號屏蔽碼(對應信號位圖) */
/* various fields */
int exit_code; /* 任務執行中止的退出碼,其父進程會取 */ unsigned long start_code,end_code,end_data,brk,start_stack;/* start_code代碼段地址,end_code代碼長度(byte), end_data代碼長度+數據長度(byte),brk總長度(byte),start_stack堆棧段地址 */
long pid,father,pgrp,session,leader;/* 進程號,父進程號 ,父進程組號,會話號,會話頭(發起者)*/ unsigned short uid,euid,suid;/* 用戶id 號,有效用戶 id 號,保存用戶 id 號*/ unsigned short gid,egid,sgid;/* 組標記號 (組id),有效組 id,保存的組id */
long alarm;/* 報警定時值 (jiffs數) */
long utime,stime,cutime,cstime,start_time;/* 用戶態運行時間 (jiffs數), 系統態運行時間 (jiffs數),子進程用戶態運行時間,子進程系統態運行時間,進程開始運行時刻 */ unsigned short used_math;/* 是否使用了協處理器 */
/* file system info */
int tty; /* 進程使用tty的子設備號. -1表示設有使用 */ unsigned short umask; /* 文件建立屬性屏蔽位 */
struct m_inode * pwd; /* 當前工做目錄 i節點結構 */
struct m_inode * root; /* 根目錄i節點結構 */
struct m_inode * executable;/* 執行文件i節點結構 */ unsigned long close_on_exec; /* 執行時關閉文件句柄位圖標誌. */
struct file * filp[NR_OPEN]; /* 文件結構指針表,最多32項. 表項號便是文件描述符的值 */
struct desc_struct ldt[3]; /* 任務局部描述符表.0-空,1-cs段,2-Ds和Ss段 */
struct tss_struct tss; /* 進程的任務狀態段信息結構 */ };
PCB task_struct中包含數據結構
進程狀態
進程打開的文件
進程優先級信息
理解這一個過程能夠用一個想象的框架:Linux經過複製父進程來建立一個新進程,複製一個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、vfork和clone三個系統調用均可以建立一個新進程,並且都是經過調用do_fork來實現進程的建立;
1. fork,建立子進程
2. vfork,與fork相似,可是父子進程共享地址空間,並且子進程先於父進程運行。
3. clone,主要用於建立線程
這三個代碼分別是:
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); }
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的事情:
1. 調用copy_process,將當期進程複製一份出來爲子進程,而且爲子進程設置相應地上下文信息。
2. 初始化vfork的完成處理信息(若是是vfork調用)
3. 調用wake_up_new_task,將子進程放入調度器的隊列中,此時的子進程就能夠被調度進程選中,得以運行。
4. 若是是vfork調用,須要阻塞父進程,知道子進程執行exec。
理解這一個過程提供一個想象的框架:Linux經過複製父進程來建立一個新進程,複製一個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();函數返回了兩次,即在父子進程中各返回一次,父進程從系統調用中返回比較容易理解,子進程從系統調用中返回,那它在系統調用處理過程當中的哪裏開始執行的呢?這就涉及子進程的內核堆棧數據狀態和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)copy_process函數:在進程建立的do_fork函數中調用,主要完成進程數據結構,各類資源的初始化。初始化方式能夠從新分配,也能夠共享父進程資源,
大致流程:
1. 檢查各類標誌位 2. 調用dup_task_struct複製一份task_struct結構體,做爲子進程的進程描述符。 3. 檢查進程的數量限制。 4. 初始化定時器、信號和自旋鎖。 5. 初始化與調度有關的數據結構,調用了sched_fork,這裏將子進程的state設置爲TASK_RUNNING。 6. 複製全部的進程信息,包括fs、信號處理函數、信號、內存空間(包括寫時複製)等。 7. 調用copy_thread,這又是關鍵的一步,這裏設置了子進程的堆棧信息。 8. 爲子進程分配一個pid9. 設置子進程與其餘進程的關係,以及pid、tgid等
關鍵地方:
tsk = alloc_task_struct_node(node);//爲task_struct開闢內存
ti = alloc_thread_info_node(tsk, node);//ti指向thread_info的首地址,同時也是系統爲新進程分配的兩個連續頁面的首地址。
err = arch_dup_task_struct(tsk, orig);//複製父進程的task_struct信息到新的task_struct裏, (dst = src;)
tsk->stack = ti;task的對應棧 setup_thread_stack(tsk, orig);//初始化thread info結構
set_task_stack_end_magic(tsk);//棧結束的地址設置數據爲棧結束標示(for overflow detection)
代碼以下:
/* 建立進程描述符以及子進程所須要的其餘全部數據結構 爲子進程準備運行環境 */
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_thread的流程以下:
1. 獲取子進程寄存器信息的存放位置 2. 對子進程的thread.sp賦值,未來子進程運行,這就是子進程的esp寄存器的值。 3. 若是是建立內核線程,那麼它的運行位置是ret_from_kernel_thread,將這段代碼的地址賦給thread.ip,以後準備其餘寄存器信息,退出。 4. 將父進程的寄存器信息複製給子進程。 5. 將子進程的eax寄存器值設置爲0,因此fork調用在子進程中的返回值爲0。 6. 子進程從ret_from_fork開始執行,因此它的地址賦給thread.ip,也就是未來的eip寄存器。
// 初始化子進程的內核棧
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; // 棧頂 空棧
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; // 子進程從ret_from_fork開始執行
p->thread.ip = (unsigned long) ret_from_fork; task_user_gs(p) = get_user_gs(current_pt_regs()); return err; }
從流程中看出,子進程複製了父進程的上下文信息,僅僅對某些地方作了改動,運行邏輯和父進程徹底一致。
子進程從ret_from_fork處開始執行。
流程以下:
1.先調用分配一個結構體。alloc_task_struct_nodetask_struct
2.調用,分配了一個union。這裏分配了一個結構體,還分配了一個stack數組。返回值爲ti,實際上就是棧底。alloc_thread_info_nodethread_info
將棧底的地址賦給task的stack變量。3.tsk->stack = ti
4.最後爲子進程分配了內核棧空間。
5.執行完以後,子進程和父進程的task結構體,除了stack指針以外,徹底相同。dup_task_struct
(三)新進程的執行
新進程從ret_from_fork處開始執行,子進程的運行是由這幾處保證的:
子進程系統調用處理過程1. dup_task_struct中爲其分配了新的堆棧 2. copy_process中調用了sched_fork,將其置爲TASK_RUNNING 3. copy_thread中將父進程的寄存器上下文複製給子進程,這是很是關鍵的一步,這裏保證了父子進程的堆棧信息是一致的。 4. 將ret_from_fork的地址設置爲eip寄存器的值,這是子進程的第一條指令。
(四)
*childregs = *current_pt_regs(); //複製內核堆棧
childregs->ax = 0; //子進程的fork返回0的緣由
p->thread.sp = (unsigned long) childregs; //調度到子進程時的內核棧頂
p->thread.ip = (unsigned long) ret_from_fork; //調度到子進程時的第一條指令地址
實踐:使用gdb跟蹤分析一個fork系統調用內核處理函數sys_clone
啓動MenuOS和gdb調試
cd LinuxKernel rm menu -rf git clone https://github.com/mengning/menu.git
cd menu mv test_fork.c test.c make rootfs qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
在新窗口中啓動調試
gdb
file linux-3.18.6/vmlinux
target remote:1234
實驗截圖以下:
4、總結
能夠將上面繁瑣的進程建立過程總結爲一下的幾步:
一、調用fork()函數引起0x80中斷
二、調用sys_fork
三、經過find_empty_process爲新進程分配一個進程號
四、經過copy_process函數使子進程複製父進程的資源,並進行一些個性化設置後,返回進程號。