分析 Linux 内核创建一个新进程的过程
1.实验过程
下载新menu替换原来的menu并make:
使用gdb设置断点并调试,调试过程如下图所示:
2.实验分析及总结
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;
//……
//复制进程描述符,copy_process()的返回值是一个 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);
//得到新创建的进程描述符中的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);
//fork 完成,子进程即将开始运行
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;
}
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;
...
/*
* One for us, one for whoever does the "release_task()" (usually
* parent)
*/
// 将进程描述符的使用计数器置为2
atomic_set(&tsk->usage, 2);
#ifdef CONFIG_BLK_DEV_IO_TRACE
tsk->btrace_seq = 0;
#endif
tsk->splice_pipe = NULL;
tsk->task_frag.page = NULL;
account_kernel_stack(ti, 1);
// 返回新申请的结点
return tsk;
free_ti:
free_thread_info(ti);
free_tsk:
free_task_struct(tsk);
return NULL;
}
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());
p->thread.io_bitmap_ptr = NULL;
tsk = current;
err = -ENOMEM;
// 如果父进程使用IO权限位图,那么子进程获得该位图的一个拷贝
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);
}
...
return err;
}
Linux通过复制父进程来创建一个新进程,通过调用do_ fork来实现并为每个新创建的进程动态地分配一个task_ struct结构。进程是处于执行期程序以及其相关资源的总称。在Linux系统中,对于进程和线程并没有明显的区分,线程是一种特殊的进程。Linux系统常用fork()创建子进程。fork()由父进程来进行调用并且由sys_clone系统调用实现,最后通过exit()退出执行。
Linux的进程创建由fork()与exec()来完成,前者拷贝当前进程创建子进程,后者负责读取可执行文件并将其载入地址空间开始运行。
fork只会被调用一次,却能够返回两次,它可能有三种不同的返回值:
在父进程中,fork返回新创建子进程的进程ID;
在子进程中,fork返回0;
如果出现错误,fork返回一个负值;
在进程描述符PID中,state描述了进程当前的状态,TASK_RUNNING标识该进程正在运行或等待运行,是否在运行取决于它是否取得了内核的控制权,这是进程在用户空间执行的唯一可能状态。
- 新进程是从哪里开始执行的?为什么从那里能顺利执行下去,执行起点与内核堆栈如何保证一致?
dup_task_struct为新进程分配了新的堆栈,copy_process调用sched_fork将新进程的state置为TASK_RUNNING,copy_thread中执行语句*childregs = *current_ pt_ regs()将父进程保存的寄存器上下文复制给子进程,保证了父子进程堆栈信息的一致,从而保证了执行起点与内核堆栈的一致。最后将ret_from_fork的地址设置为eip寄存器的值,新进程即从这里开始执行。