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

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 <unistd.h>
#include <stdio.h>
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\n",getpid());
    }
    else {
        printf("i am the parent process, my process id is %d\n",getpid());
    }
    return 0;
}

这里写图片描述

  • 对于父进程来说:
    通过系统调用fork()陷入内核。完成系统调用2号服务例程后,从iret返回,并恢复之前SAVE_ALL的寄存器值,从内核态回到用户态,继续执行用户态代码 printf("i am the parent process, my process id is %d\n",getpid()),所以打印的是子进程pid值。
  • 对于子进程来说:
    由于是父进程创建的,一出生就存在于内核中了。用户代码fork()之前的它管不了,只能管fork()之后的代码。于是ret_from_fork–>syscall_exit,恢复父进程保存在SAVE_ALL的寄存器值,从内核态回到用户态,继续执行用户态代码printf("i am the child process, my process id is %d\n",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

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值