Linux内核分析:分析Linux内核创建一个新进程的过程

张家骥 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-100002900

1.阅读理解task_struct数据结构

http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#1235
进程描述符(PCB)的数据结构就是task_struct类型,它包括了一个进程相关的所有信息。
这里写图片描述

state字段

用于描述进程当前的状态,由一组标志组成,每个标志描述一种进程可能的状态,在当前的Linux版本中,这些状态的是互斥的。

可运行状态(TASK_RUNNING)

进程要么在CPU执行,要么准备执行。

可中断的等待状态(TASK_INTERRUPTIBLE)

进程被挂起(睡眠), 知道某个条件为真。产生一个硬件中断,释放进程正在等待的系统资源,或传递一个信号,将进程唤醒。(回到TASK_RUNNING)

不可中断的等待状态(TASK_UNINTERRUPTIBLE)

进程必须等待,知道一个不可被中断的事件发生。

暂停状态(TASK_STOPPED)

进程的执行被暂停。当进程接收到SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU信号后,进入暂停状态。

跟踪状态(TASK_TRACED)

进程的状态已由debugger程序暂停。当一个进程被另一个进程监控时(例如debugger执行ptrace()系统调用监控一个测试程序),任何信号都可以把这个进程置于TASK_TRACED状态。

僵死状态(EXIT_ZOMBIE)

进程的执行被终止,但是,父进程还没有发布wait4()或waitpid()系统调用来返回有关死亡进程的信息。发布wait()类系统调用前,内核不能丢弃包含在死亡描述符中的数据,因为父进程可能还需要它。

僵死撤销状态(EXIT_DEAD)

最终状态:由于父进程刚发出wait4()或waitpid()系统调用,进而进程由系统删除。

thread_info字段

Linux把两个不同的数据结构紧凑的存放在一个单独的为进程分配的存储区域内:一个是进程内核堆栈,另一个是thread_info,叫做线程描述符。这块存储区域的大小通常是8192字节,内核让这8K空间占据连续的两个页框并让第一个页框的起始地址是8192(2的13次方)的倍数。因为内核控制路径使用很少的栈,所以8KB足够了。
这里写图片描述
thread_info结构是52个字节长。C语言使用下列的联合结构方便的表示一个进程的线程描述符和内核栈:

union thread_union{
    struct thread_info thread_info;
    unsigned long stack[2048];
}

task字段

所有的进程的描述符组成了一个双向链表。task字段的类型为list_head,这个类型的prev和next字段分别指向前面和后面的task_struct元素。

struct list_head{
    struct list_head *next,*prev;
};

进程链表的头是init_task描述符,它是所谓的0进程的进程描述符。init_task的tasks.prev字段指向链表中最后插入的进程描述符的tasks字段。

2.分析fork函数对应的内核处理过程sys_clone,理解创建一个新进程如何创建和修改task_struct数据结构

fork();//系统调用,用于在用户态创建一个子进程。创建进程时通过复制当前进程实现的。
设想创建一个新进程要做哪些事:
1.复制一个PCB——task_struct。
2.给新进程分配一个新的内核栈。
3.修改pid,进程链表等。
在fork中:
复制一个PCB——task_struct:

err=arch_dup_struct(tsk,org);

给新进程分配一个新的内核栈:

ti = alloc_thread_info_node(tsk, node);
tsk->stack = ti;
setup_thread_stack(tsk, orig);//这里只是复制thread_info,而非复制内核堆栈

修改pid,进程链表等:
fork()中会调用do_fork(),do_fork()会调用copy_process(),在copy_process()中会做这些。

copy_process{
    ......
    //出错处理
    ......
    p=dup_task_struct(current);//复制PCB,p指向子进程的PCB

    ......
    //修改子进程的工作
    ......

    retval= copy_thread(...,...,...,p);
}
//copy_process()中调用的copy_thread()部分代码
int copy_thread(unsigned long clone_flags, unsigned long usp,unsigned long arg, struct task_struct *p)
{
    struct pt_regs *childregs = task_pt_regs(p);
    struct task_struct *tsk;
     ......
    p->thread.sp = (unsigned long) childregs;
     //赋sp=XXX,此时还在父进程的执行上下文中。
     ......
     *childregs = *current_pt_regs();
     //将父进程的内核堆栈数据拷贝过来。
     ......
     childregs->ax = 0;
     //这就是子进程的fork()返回值为0的原因。
     p->thread.pc = (unsigned long) ret_from_fork;
     //指定新进程的第一条指令地址。
    return 0;
}

pt_regs的内容:
这里写图片描述
创建的新进程从ret_form_fork开始执行,在entry_32.S中可以找到ENTRY(ret_form_fork),此时内核栈中只有pt_regs中的那些内容。
这里写图片描述
从图中可以看到,它会跳转到 syscall_exit
然后

RESTORE_ALL//与我们mykernel实验中类似。
iret//就回到子进程的用户态空间。因为之前ip保存了fork()的下一条指令地址,此时刚好返回到那里。

3.使用gdb跟踪分析一个fork系统调用内核处理函数sys_clone ,验证您对Linux系统创建一个新进程的理解

(结合实验截图、绘制堆栈状态执行流程图)

3.1实验过程

(1)更新带fork命令的menu系统。
这里写图片描述
(2)测试新的menu系统fork命令。
这里写图片描述
(3)启动gdb,调试menu,跟前几次实验方法一样,具体命令参看前几次作业。
这里写图片描述
(4)设置观察sys_clone过程所需要的断点。
这里写图片描述
(5)在menu界面中输入fork命令。
这里写图片描述
(6)此时,在gdb窗口中,可以看到,停在了断点:sys_clone处
这里写图片描述
(7)按c,继续执行,发现接下来停在了do_fork处,接着是copy_process
这里写图片描述
(8)接下来就来到了copy_thread,这时关键部分,按n单步执行。
这里写图片描述
上图中可以看到:
*childregs = *current_pt_regs(); //复制内核堆栈
这里写图片描述
上图中可以看到:
childregs->ax = 0; //为什么子进程的fork返回0,这里就是原因。
这里写图片描述
上图中可以看到:
调用完copy_thread后,又回到copy_process中。
(9)继续执行,接下来停到了ret_from_fork处
这里写图片描述
(10)gdb无法跟踪后,按c,回到menu界面,看到fork命令执行完毕。
这里写图片描述

3.2 堆栈状态执行流程图

这里写图片描述
堆栈变化的最后有一句:

addl $4,esp

它相当会弹出之前保存在栈中的系统调用号(已经没有用了),对应开头的

pushl %eax

最后一条指令:

iret

会弹出eip,cs,eflags,esp,ss。

4.自己对“Linux系统创建一个新进程”的理解

在start_kernel中,创建了0号进程,0号进程创建了1号进程,1号进程是所有用户态进程的祖先,0号进程是所有内核进程的祖先。之后其他进程都通过clone(),fork(),vfork()创建,是0号或1号的子孙进程。创建新进程通过复制父进程实现,先复制task_struct,再为其分配一个内核栈,然后修改需要改动的地方,比如进程pid,进程链表等等。

创建好之后,会跳转到ret_from_fork,在ret_from_fork中会jmp到syscall_exit,此时父进程的内核栈中保存了父进程在执行fork()前的上下文。子进程的内核栈中也从父进程那里拷贝得到了自己的上下文。所以它们现在只有被调度到,都可以正常的执行。

接下来可能发生进程调度,如果子进程得到CPU,就可以正常执行,父进程得到CPU也可以正常执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值