Linux核心分析第七次作業
阿新 • • 發佈:2018-11-25
分析Linux核心建立一個新程序的過程
Linux中建立程序一共有三個函式:
1. fork,建立子程序
2. vfork,與fork類似,但是父子程序共享地址空間,而且子程序先於父程序執行。
3. clone,主要用於建立執行緒
程序建立的大概過程
通過之前的學習,我們知道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大概做了這麼幾件事情:
1. 呼叫copy_process,將當期程序複製一份出來為子程序,並且為子程序設定相應地上下文資訊。
2. 初始化vfork的完成處理資訊(如果是vfork呼叫)
3. 呼叫wake_up_new_task,將子程序放入排程器的佇列中,此時的子程序就可以被排程程序選中,得以執行。
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) { 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的大體流程:
1. 檢查各種標誌位
2. 呼叫dup_task_struct複製一份task_struct結構體,作為子程序的程序描述符。
3. 檢查程序的數量限制。
4. 初始化定時器、訊號和自旋鎖。
5. 初始化與排程有關的資料結構,呼叫了sched_fork,這裡將子程序的state設定為TASK_RUNNING。
6. 複製所有的程序資訊,包括fs、訊號處理函式、訊號、記憶體空間(包括寫時複製)等。
7. 呼叫copy_thread,這又是關鍵的一步,這裡設定了子程序的堆疊資訊。
8. 為子程序分配一個pid
9. 設定子程序與其他程序的關係,以及pid、tgid等。這裡主要是對執行緒做一些區分。
進一步追蹤dup_task_struct簡化後的程式碼如下:
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結點,其實內部分配了一個union,包含程序的核心棧
// 此時ti的值為棧底,在x86下為union的高地址處。
ti = alloc_thread_info_node(tsk, node);
if (!ti)
goto free_tsk;
err = arch_dup_task_struct(tsk, orig);
if (err)
goto free_ti;
// 將棧底的值賦給新結點的stack
tsk->stack = ti;
// ...
// 返回新申請的結點
return tsk;
}
dup_task_struct的程式碼要結合一個聯合體的定義來分析
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
這個聯合體的定義非常關鍵。我們知道x86體系結構的棧空間,按照從高到低的方式增長。而C中的結構體,是按從低到高的方式使用。
這樣我們可以宣告一個聯合體,低地址用作thread_info,高地址用作棧底。
這樣做還有一個好處,就是thread_info中存放著一個task_struct的指標,這樣我們根據棧底地址就可以通過thread_info快速定位到程序對應的task_struct指標。
上面的dup_task_struct中,我們:
1. 先呼叫alloc_task_struct_node分配一個task_struct結構體。
2. 呼叫alloc_thread_info_node,分配了一個union,注意,這裡不僅僅分配了一個thread_info結構體,還分配了一個stack陣列。返回值為ti,實際上就是棧底。
3. tsk->stack = ti;這句話,就是將棧底的地址賦給task的stack變數。
所以,最後為子程序分配了核心棧空間。
執行完dup_task_struct之後,子程序和父程序的task結構體,除了stack指標之外,完全相同!
進一步追蹤copy_thread函式上面的copy_process中,我們提到copy_thread函式為子程序準備了上下文堆疊資訊。程式碼如下:
// 初始化子程序的核心棧
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;
}
我們看到,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暫存器。
從上面的流程中,我們看出,子程序複製了父程序的上下文資訊,僅僅對某些地方做了改動,執行邏輯和父程序完全一致。
另外,我們得出結論,子程序從ret_from_fork處開始執行。
新程序的執行
1. dup_task_struct中為其分配了新的堆疊
2. copy_process中呼叫了sched_fork,將其置為TASK_RUNNING
3. copy_thread中將父程序的暫存器上下文複製給子程序,這是非常關鍵的一步,這裡保證了父子程序的堆疊資訊是一致的。
4. 將ret_from_fork的地址設定為eip暫存器的值,這是子程序的第一條指令。