Linux内核分析——进程的描述和创建

2019-07-14 11:52发布

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

一、进程的描述

1、struct task_struct(PCB)

task_struct内容非常庞大,大致分为:
  • 进程描述信息:
    PID、家族…
  • 进程控制、调度信息:
    当前状态、调度信息、计时信息…
  • 资源信息:
    使用的存储器空间、打开的文件…
  • 现场信息:
    当时CPU运行现场,以便下次切换时能够精准地继续运行。

2、在processor.h中查看task_struct的具体字段

(参考同学制图:http://www.cnblogs.com/hyq20135317/p/5337216.html
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

3、进程的状态

进程状态:进程描述符中state域描述了进程的当前状态。 TASK_RUNNING(可执行) TASK_INTERRUPTIBLE(正被阻塞) TASK_UNINTERRUPTIBLE(不可中断) _TASK_TRACED(被其他进程跟踪) _TASK_STOPPED(进程停止执行)
Linux进程状态之间的转换:
这里写图片描述

4、进程在内存中是怎样的

下图为个人理解,如有不当请指正!
【我画的图是一般的两个进程在内存中的情况,包括各自的堆栈空间、PCB板(保存了各个进程自己的sp、ip)、数据、以及切换时SAVE_ALL保存当时寄存器所有数值的情形。】
这里写图片描述
  • Linux分配机制:
    Linux通过Slab分配器分配task_struct结构。对于向下增长的栈来说,需要在栈底创建一个新的结构struct thread_info,而struct thread_info 中有一个指向该进程PCB的指针。
  • 对于X86这样寄存器较少的结构来说:
    通常分配8k的内核堆栈。通过计算偏移,间接找到task_struct结构。

二、进程的创建

fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建。 创建新进程是通过复制当前进程实现的。 do_fork主要是复制了父进程的task_struct,然后修改必要的信息,从而得到子进程的task_struct。归纳、总结后的关键过程如下(更多细节,尤其函数之间的调用层次见http://www.cnblogs.com/hyq20135317/p/5337216.html)。 1、 复制一个PCB板——task_struct: err = arch_dup_task_struct(tsk, orig); //直接指针赋值 2、给新进程分配一个新的内核堆栈: ti = alloc_thread_info_node(tsk, node); //分配一个struct thread_info类型的空间 tsk->stack = ti; //将thread_info(同时也就是栈底)的地址赋给task的stack变量 setup_thread_stack(tsk, orig); //这里只是复制thread_info,而非复制内核堆栈 3、修改复制过来的进程数据,比如pid、进程链表等。
4、子进程的启动: *childregs = *current_pt_regs(); //复制内核堆栈 childregs->ax = 0; //子进程的fork返回0的原因 p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶 p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址 刚fork出来的子进程接着从ret_from_fork开始执行,然后跳转到syscall_exit,从系统调用中返回。 5、小结
上面那张我画的图是一般的两个进程在内存中的情况,但是也可以帮助我们理解系统是怎样fork一个子进程并返回到用户态的。 如果这两个进程X、Y是父子进程关系的话:
  1. 假设进程X通过系统调用fork()创建了Y进程。
  2. 结合上面的分析,我们知道有PCB的复制和修改、为子进程开辟一块8k的内核堆栈等过程。
  3. 不同的是,我的图上“进程Y”的栈底应该没有数据,只有从父进程那里copy过来的SAVE_ALL(这是一个宏,实际上是把所有寄存器值打包变成的一个结构体),也就是*childregs = *current_pt_regs();
  4. 复制完SAVE_ALL后,将sp移到子进程栈顶:p->thread.sp = (unsigned long) childregs;
  5. p->thread.ip = (unsigned long) ret_from_fork;
    这一句的理解非常关键。刚fork出来的子进程从ret_from_fork开始执行,自动跳转到syscall_exit,从系统调用中返回。这样就指定了新进程的第一条指令地址。
【举例】上周我的blog中自选的系统调用服务恰好是fork,代码如下: #include #include int main () { pid_t fpid; fpid = fork(); if (fpid < 0) printf("error in fork!"); else if (fpid == 0) { printf("i am the child process, my process id is %d ",getpid()); } else { printf("i am the parent process, my process id is %d ",getpid()); } return 0; } 这里写图片描述
  • 对于父进程来说:
    通过系统调用fork()陷入内核。完成系统调用2号服务例程后,从iret返回,并恢复之前SAVE_ALL的寄存器值,从内核态回到用户态,继续执行用户态代码 printf("i am the parent process, my process id is %d ",getpid()),所以打印的是子进程pid值。
  • 对于子进程来说:
    由于是父进程创建的,一出生就存在于内核中了。用户代码fork()之前的它管不了,只能管fork()之后的代码。于是ret_from_fork–>syscall_exit,恢复父进程保存在SAVE_ALL的寄存器值,从内核态回到用户态,继续执行用户态代码printf("i am the child process, my process id is %d ",getpid())。由于ax寄存器值赋值为0,所以返回值为0,打印的是0。

    三、实验:gdb跟踪内核

    准备工作:
rm menu -rf
git clone http://github.com/mengning/menu.git # 更新Menu
cd menu
mv test_fork.c test.c # 把test.c覆盖掉
make rootfs
执行fork,可以看到父进程子进程都输出了信息。
进行gdb调试:
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
gdb
file linux-3.18.6/vmlinux
target remote:1234
// 设置断点
b sys_clone # 因为fork实际上是执行的clone
b do_fork
b dup_task_struct
b copy_process
b copy_thread
b ret_from_fork
c
n
……

四、总结

这次的学习过程不是一帆风顺的。虽然上周实验机智地使用了fork并预先学习了,然而这周看视频里的源代码还是晕。后来看书,并尝试着自己画图,突然就明白了许多(或许有不对的地方,请指正!!),又参考了同学的博客,细节上把握更细致了。收获很大。 参考资料:《Linux内核设计与实现》《Linux操作系统原理》
http://www.cnblogs.com/hyq20135317/p/5337216.html