实验六:分析 Linux 内核创建一个新进程的过程
一、相关知识
a.操作系统内核三大功能是进程管理,内存管理,文件系统,最核心的是进程管理。
b.Linux 进程的状态和操作系统原理的描述进程状态有所不同,比如就绪状态和运行状态都是TASK_RUNNING。(这个表示它是可运行的,但是实际上有没有在运行取决于它是否占有 CPU)。
c.fork 被调用一次,能够返回两次。在父进程中返回新创建子进程的 pid;在子进程中返回 0。
d.调用 fork 之后,数据、堆、栈有两份,代码仍然为一份(这个代码段成为两个进程的共享代码段)。当父子进程有一个想要修改数据或者堆栈时,两个进程真正分裂。
二、内核代码分析
SYSCALL_DEFINE0(fork)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, 0, 0, NULL, NULL);
#else
return -EINVAL;
#endif
}
SYSCALL_DEFINE0(vfork)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
0, NULL, NULL);
}
通过上面的代码可以看出 fork、vfork 和 clone 三个系统调用都可以创建一个新进程,而且都是通过 do_fork 来创建进程,只不过传递的参数不同。
(1)do_fork
do_fork()主要完成了调用 copy_process() 复制父进程信息、获得pid、调用 wake_up_new_task 将子进程加入调度器队列,为之分配 CPU、通过 clone_flags 标志做一些辅助工作。其中 copy_process()是创建一个进程内容的主要的代码。
(2)copy_process
copy_process 主要完成了调用 dup_task_struct 复制当前的 task_struct、信息检查、初始化、把进程状态设置为 TASK_RUNNING、复制所有进程信息、调用 copy_thread 初始化子进程内核栈、设置子进程pid。
(3)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;
tsk = alloc_task_struct_node(node); //为子进程创建进程描述符
...
ti = alloc_thread_info_node(tsk, node); //实际上是创建了两个页,一部分用来存放 thread_info,一部分就是内核堆栈
...
err = arch_dup_task_struct(tsk, orig); //复制父进程的task_struct信息
...
tsk->stack = ti; // 将栈底的值赋给新结点的stack
setup_thread_stack(tsk, orig);//对子进程的thread_info结构进行初始化(复制父进程的thread_info 结构,然后将 task 指针指向子进程的进程描述符)
...
return tsk; // 返回新创建的进程描述符指针
...
}
(4)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; //如果创建的是内核线程,则从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();//复制内核堆栈(复制父进程的寄存器信息,即系统调用SAVE_ALL压栈的那一部分内容)
childregs->ax = 0; //子进程的eax置为0,所以fork的子进程返回值为0
...
p->thread.ip = (unsigned long) ret_from_fork;//ip指向 ret_from_fork,子进程从此处开始执行
task_user_gs(p) = get_user_gs(current_pt_regs());
...
return err;
dup_task_struct 只是为子进程创建一个内核栈,copy_thread 才真正完成赋值。
三、实验
1、用户态创建进程的实验
2.通过实验跟踪分析进程创建的过程
在MenuOS中增加命令fork:
MenuOS的运行的结果:
设置几个断点: