实验六分析Linux内核创建一个新进程的过程
王康 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
1,进程的描述
操作系统三大功能:进程管理(核心),内存管理,文件系统
1,进程控制块PCB——task_struct
也叫进程描述符,为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息。
struct task_struct数据结构很庞大:可以看到state进程状态,stack内核堆栈,
flag进程标识符,CONFIG_SMP条件编译(多处理器用到)
和进程队列调度相关的
进程的内存地址空间:
下图是抽象图:task进程链表管理,tty控制台,fs文件系统,files打开的文件描述符,mm内存管理描述,signal进程间通信信号
Linux进程的状态与操作系统原理中的描述的进程状态似乎有所不同,比如就绪状态和运行状态都是TASK_RUNNING,为什么呢?
fork创建好新进程变成就绪态,调度器选择了进程后变成运行态。但都是task running,状态相同。因为进程是taskrunning时是可运行的,有无在运行看进程是否在cpu实际执行,根据这个来划分就绪和运行态。
正在运行进程调用do exit会变成taskzombie僵尸进程由系统处理掉;
正在运行进程等待资源会进入阻塞态,可用时被唤醒进入就绪态。
进程状态量:
进程的标示pid
所有进程链表struct list_head tasks;
程序创建的进程具有父子关系,在编程时往往需要引用这样的父子关系。进程描述符中有几个域用来表示这样的关系(都是通过双向链表来连接起来的)
pids[] pid的哈希表 和 线程组
CPU相关状态,执行进程上下文切换:可以看到thread_struct有sp,ip,与之前mykernel定义类似
和文件描述符以及信号处理相关的:
Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:Thread_info和进程的内核堆栈
进程处于内核态时使用,不同于用户态堆栈,即PCB中指定了内核栈,那为什么PCB中没有用户态堆栈?用户态堆栈是怎么设定的?
内核控制路径所用的堆栈很少,因此对栈和Thread_info来说,8KB足够了
struct thread_struct thread; //CPU-specific state of this task
2,进程的创建
之前进程创建最后rest init创建了两个kernel thread内核线程:1,kernel init最终去启动用户态进程init;2,启动了k_threadd是内核线程的demo
使用shell时,0号进程在linux写死,1号进程从当前进程复制一份进程描述符PCB,根据需要修改pid等内容,再加载init可执行程序。
进程是如何创建起来的?可能是复制后修改了一份成为了子进程,那子进程是从哪来启动的呢?先来分析shell命令行是如何创建一个子进程:
fork()用于在用户态创建一个子进程的系统调用。
底下两个判断都会被执行,fork以后子父进程pid返回值不一样:子进程返回0;父进程返回子进程pid。从fork这里就变成了2个进程执行。
2.1系统调用回顾
int 0x80中断指令,由于陷入内核的所以是机器自动保存和转换堆栈。
iret和int 0x80对应
总控程序是entry32s:
fork也是一个系统调用,如下:
父进程fork是如下执行和传统系统调用没什么区别,子进程copy了父进程所有的内存信息,做适当修改,子进程作为独立子进程也会被调度,子进程运行时会从哪里运行呢?
如果是从用户态空间来看是fork下一句;
但是fork在子进程当中也是有返回0的,所以fork出的子进程在内核执行返回,即子进程在内核处理程序里边在什么代码地方开始执行呢?
2.2,创建进程过程
可以看到fork调用过程和普通系统调用没什么差别,但进程方面不一样:
复制父进程PCB,修改复制出来的PCB,分配一个新的内核堆栈(因为内核堆栈其中一部分数据是fork返回到用户态,所以内核堆栈启动一部分也要从父进程拷贝),还有thread根据拷贝内核堆栈状况设置好esp eip的位置。
3种系统调用内核处理函数:
CONFIG_MMU内核编译,调用的都是do_fork;clone有不同的参数重载(用户态调用的sys_clone vfork都是使用do_fork)
do_fork:
进入copy_process:
dup_task_struct赋值PCB
dup_task_struct:
arch_dup_task_struct来复制,orig是当前进程
arch_dup_task_struct:直接把src值复制给dest
dup_task_struct:
还有alloc了一个thread_info。内核堆栈是由thread_info和堆栈合到一起的union。
做了一个实际分配内核空间的效果。
把返回的ti赋给了task
alloca_thread_info_node:
kmem_pages创建了一定大小的页,一部分存放Threadinfo一部分存放堆栈
setup_thread_stack也把thread_info信息复制了过来
回到fork.c:
这里p指向了子进程的pcb
之后均是进程的修改代码初始化
process32.c copy_thread:
从子进程pid stack内核堆栈位置找到栈空间,找到栈底在系统调用时压栈的内容比如SAVE_ALL内容地址。找到地址后赋值sp。
current_pt_regs是当前进程即父进程的堆栈拷贝给childregs,包括返回值ax = 0 赋值,栈顶数据赋值以及thread_ip内容
2.3,子进程是从哪里开始启动的?
ret_from_fork是从进程得到调用cpu时候这里开始执行
而复制内核堆栈时候只复制内核堆栈一部分如SAVE_ALL部分
pt_regs是系统调用压栈内容和系统调用号等等
再来看entry32s:
内核堆栈只有之前那些内容,在这里会jmp到sys_call_exit。
即到sys_call_exit时与sys_call之前堆栈状态是一样的,就可以继续往下执行RESTOREALL,IRET。
所以子进程获得控制权时候就会ret_from_fork把后边堆栈出栈IRET返回用户态,这时候用户态就是子进程的空间了
2.4使用gdb跟踪创建新进程的过程:
test_fork.c覆盖掉test.c
与之前方法一样,只是多设置几个断点:
do_fork执行到copy_process
又进入dup_task_struct
把内核堆栈压的寄存器复制到子进程来
最后跳转到syscall_exit
3,实验
与上文一致,选取2张:
4,总结
从代码角度来分析整个流程:
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; //总的pid数量
/* * 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;
}
// 复制进程描述符,返回创建的task_struct的指针
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
/* * 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);
// 取出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);
/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event_pid(trace, pid);
// 如果设置了 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;
}
1 通过copy_process来复制进程描述符,返回新创建的子进程的task_struct的指针(即PCB指针).
2 将新创建的子进程放入调度器的队列中,让其有机会获得CPU,并且要确保子进程要先于父进程运行,
这里为什么要确保子进程先于父进程运行呢?,答案是在Linux系统中,有一个叫做copy_on_write技术(写时拷贝技术),该技术的作用是创建新进程时可以减少系统开销,具体该技术的细节请各位Google之,这里子进程先于父进程运行可以保证写时拷贝技术发挥其作用
3 这里有一个重点的地方需要说明,在使用get_pid系统调用时,返回的并不是进程的pid,而是线程的tgid,tgid指的是一个线程组当时领头的进程的pid
4 在do_fork中,copy_process函数是比较重要的,其作用是创建进程描述符以及子进程所需要的其他所有数据结构为子进程准备运行环境,下面将深入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 (!p)
goto fork_out;
···
retval = -EAGAIN;
// 检查该用户的进程数是否超过限制
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;
}
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. */
retval = -EAGAIN;
// 检查进程数量是否超过 max_threads,后者取决于内存的大小
if (nr_threads >= max_threads)
goto bad_fork_cleanup_count;
if (!try_module_get(task_thread_info(p)->exec_domain->module))
goto bad_fork_cleanup_count;
delayacct_tsk_init(p); /* Must remain after dup_task_struct() */
p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);
// 表明子进程还没有调用exec系统调用
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);
// 初始化定时器
p->utime = p->stime = p->gtime = 0;
p->utimescaled = p->stimescaled = 0;#ifndef CONFIG_VIRT_CPU_ACCOUNTING_NATIVE
p->prev_cputime.utime = p->prev_cputime.stime = 0;#endif#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN
seqlock_init(&p->vtime_seqlock);
p->vtime_snap = 0;
p->vtime_snap_whence = VTIME_SLEEPING;#endif
...
#ifdef CONFIG_DEBUG_MUTEXES
p->blocked_on = NULL; /* not blocked yet */#endif#ifdef CONFIG_BCACHE
p->sequential_io = 0;
p->sequential_io_avg = 0;#endif
/* Perform scheduler related setup. Assign this task to a CPU. */
// 完成对新进程调度程序数据结构的初始化,并把新进程的状态设置为TASK_RUNNING
// 同时将thread_info中得preempt_count置为1,禁止内核抢占
retval = sched_fork(clone_flags, p);
if (retval)
goto bad_fork_cleanup_policy;
retval = perf_event_init_task(p);
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_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;
}
...
// 清除子进程thread_info结构的 TIF_SYSCALL_TRACE,防止 ret_from_fork将系统调用消息通知给调试进程
clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE);#ifdef TIF_SYSCALL_EMU
clear_tsk_thread_flag(p, TIF_SYSCALL_EMU);#endif
clear_all_latency_tracing(p);
/* 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 (likely(p->pid)) {
ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);
init_task_pid(p, PIDTYPE_PID, pid);
if (thread_group_leader(p)) {
...
// 将pid加入散列表
attach_pid(p, PIDTYPE_PGID);
attach_pid(p, PIDTYPE_SID);
__this_cpu_inc(process_counts);
} else {
...
}
// 将pid加入PIDTYPE_PID这个散列表
attach_pid(p, PIDTYPE_PID);
// 递增 nr_threads的值
nr_threads++;
}
total_forks++;
spin_unlock(¤t->sighand->siglock);
syscall_tracepoint_update(p);
write_unlock_irq(&tasklist_lock);
...
// 返回被创建的task结构体指针
return p;
...
}
· copy_process的参数与do_fork的参数类型一模一样,参数也是几乎相同,除了struct pid那里是空
· dup_task_struct这个函数,会分配一个新的task_struct给子进程,但是这个task_struct是未初始化的,下面将具体说说这个dup_task_struct:
· copy_process中会进行各种各样的初始化和信息检查,比如初始化自旋锁,初始化堆栈信息等等,同时会把新创建的子进程运行状态置为TASK_RUNNING(这里是就绪态)
· copy_process中,会通过copy_thread来初始化子进程的内核栈,下面也会进行具体说明
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;
}
这其中有个比较重要的结构struct thread_info,但是在内部分配时,其实是一个union(联合体),这个union包括了一个内核堆栈,其结构如下图
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;
}
1 这里的过程基本就是给新的进程的各种运行时状态进行初始化,比如寄存器信息(通过父进程的寄存器信息来初始化,但是eip会是个例外,eip将会取决于最后子进程将会从哪里开始执行),栈会被置空未初始化状态
2 在代码中,有两段这样的代码p->thread.ip = (unsigned long) ret_from_kernel_thread; p->thread.ip = (unsigned long) ret_from_fork;,这里表面了在fork完成之后,新进程将会在哪里开始执行,如果新的创建的新的线程是内核线程,那么将会从ret_from_kernel_thread开始执行,但是如果是普通的用户态线程,则将会从p->thread.ip = (unsigned long) ret_from_fork开始执行.
新进程在copy_process中的具体创建过程执行如下:
1. dup_task_struct中为其分配了新的堆栈。
2. 调用了sched_fork,将其置为TASK_RUNNING。
3. copy_thread中将父进程的寄存器上下文复制给子进程,保证了父子进程的堆栈信息是一致的。
4. 将ret_from_fork的地址设置为eip寄存器的值。
最终子进程从ret_from_fork开始执行。
整个过程可大致表示为下图: