• 第六节 进程的描述和进程的创建——20135203齐岳


    第六节 进程的描述和进程的创建

    By 20135203齐岳

    本周的主要内容:

    1. 如何描述一个进程:进程描述符的数据结构;
    2. 如何创建一个进程:内核是如何执行的,以及新创建的进程从哪里开始执行;
    3. 使用gdb跟踪新进程的创建过程。

    进程的描述

    操作系统三大功能:

    • 进程管理(最核心最基础)
    • 内存管理
    • 文件系统

    进程描述符task_struct数据结构

    • task _ struct:为了管理进程,内核必须对每个进程进行清晰的描述,进程描述符提供了内核所需了解的进程信息。struct task_struct数据结构很庞大。

    • 进程的状态:Linux进程的状态(就绪态、运行态、阻塞态)与操作系统原理中的描述的进程状态有所不同,比如就绪状态和运行状态都是TASK_RUNNING,当一个进程处于TASK_RUNNING的时候是可运行的,至于有没有运行其中的区别就在于有没有获得CPU的使用权。

    • 进程的标示pid:用来标示进程

    • 进程描述符task_struct数据结构

        struct task_struct {
        volatile long state;	/* 进程的运行状态-1 unrunnable, 0 runnable, >0 stopped */
        void *stack;	/*指定了进程的内核堆栈*/
        atomic_t usage;
        unsigned int flags;	/* 每一个进程的标识符 */
        unsigned int ptrace;
      
        int on_rq;/运行队列和进程调度相关/
      
    • 进程调度的链表

      所有进程链表struct list_head tasks;

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

      内核的双向循环链表的实现方法:一个更简略的双向循环链表。

    • 进程的地址空间内存管理相关

        struct mm_struct *mm, *active_mm;
      
    • 进程标示(pid)

        pid_t pid;
        pid_t tgid;
      
    • 进程父子关系

      程序创建的进程具有父子关系,在编程时往往需要引用这样的父子关系。进程描述符中有几个域用来表示这样的关系

    	struct task_struct __rcu *real_parent; /* real parent process */
    	struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */
    
    	struct list_head children;	/* list of my children */
    	struct list_head sibling;	/* linkage in my parent's children list */
    	struct task_struct *group_leader;	/* threadgroup leader */
    
    • 调试

        struct list_head ptraced;
        struct list_head ptrace_entry;
      
    • 当前任务相关的CPU状态

        struct thread_struct thread;
      

      Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:Thread_info和进程的内核堆栈

    进程的创建

    进程的创建

    start _ kernel代码中的rest _ init创建两个内核线程,kernel _ init和kthreadd。kernel _ init将用户态进程init启动,是所有用户态进程的祖先,kthreadd是所有内核线程的祖先。

    在命令行下创建进程和启动内核的原理是大致相同的,复制一份0号进程描述符,然后根据进程需要将pid等数据结构修改,就完成了进程的创建。

    fork一个进程的用户态代码

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    int main(int argc, char * argv[])
    {
    	int pid;
    	/* fork another process */
    	pid = fork();//fork是在用户态用于创建一个子进程的系统调用
    	if (pid < 0) 
    	{ 
        	/* error occurred */
        	fprintf(stderr,"Fork Failed!");
        	exit(-1);
    	} 
    	else if (pid == 0) 
    	{
        	/* child process */
        	printf("This is Child Process!
    ");
    	} 
    	else 
    	{  
        	/* parent process  */
        	printf("This is Parent Process!
    ");
        	/* parent will wait for the child to complete*/
        	wait(NULL);
        	printf("Child Complete!
    ");
    	}
    }
    

    fork系统调用在父进程和子进程各返回一次,在子进程中返回的pid是0,父进程中的返回值子进程的pid,所以程序中的else if(pid==0)和else都会被执行,它的背后有两个进程,可以利用这一点在用户态创建子进程。

    创建一个新进程在内核中的执行过程

    系统调用再回顾:见第四节博客http://www.cnblogs.com/July0207/p/5277774.html

    fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建

    Linux通过复制父进程来创建一个新进程,大部分信息都是相同的,少数信息需要修改,否则就会发生混乱。例如:pid、进程链表、内核堆栈、进程执行上下文thread等,并设置好fork返回的下一条指令(esp和eip)。由此得出进程创建的一个大致框架:

    • 复制一个PCB——task_struct

        p = dup_task_struct(current);//复制进程的PCB
      
        int __weak arch_dup_task_struct(struct task_struct *dst,struct task_struct *src)
        {
        	*dst = *src;//通过赋值实现复制
        	return 0;
        }
      
    • 给新进程分配一个新的内核堆栈

        ti = alloc_thread_info_node(tsk, node);
        tsk->stack = ti;
        setup_thread_stack(tsk, orig); //这里只是复制thread_info,而非复制内核堆栈
      
    • 修改复制过来的进程数据,比如pid、进程链表等(见copy_process内部)。

        /*copy_thread in copy_process*/
        /*拷贝内核堆栈数据和指定新进程的第一条指令地址*/
        *childregs = *current_pt_regs(); //复制内核堆栈,只复制了SAVE_ALL相关的部分
        childregs->ax = 0; //子进程的fork返回0的原因
      
        p->thread.sp = (unsigned long) childregs; //调度到子进程时的内核栈顶
        p->thread.ip = (unsigned long) ret_from_fork; //调度到子进程时的第一条指令地址
      

    创建的新进程开始启动的位置

    int指令和SAVE_ALL压到内核栈的内容:系统调用压栈的相关数据结构,系统调用号,寄存器参数。

    当子进程获得CPU的控制权开始运行的时候,ret _ form _ fork可以将后面的堆栈出栈,从iret返回到用户态,从而切换到子进程的用户空间。

    实验——使用gdb跟踪创建新进程的过程

    1. 将menu目录删除,利用git命令克隆一个新的menu目录。

       rm menu -rf
       git clone https://github.com/mengning/menu.git
      
    2. 用test_fork.c将test.c覆盖,然后重新编译footfs

       mv test_fork.c test.c
       make rootfs
      

      可以看到fork函数的运行结果:

    1. gdb准备调试,加载符号表并设置端口。

       file linux-3.18.6/vmlinux
       target remote:1234
      
    2. 设置断点:

    1. 单步执行:

    (由于这里报错原因不明,没有办法进行单步调试,后续的实验在实验楼环境下完成。)

    (1)使用命令c继续执行,可看到执行到do_fork处

    (2)单步执行到copy_process

    (3)之后进入dup _ task _ struct

    (4)继续执行可到copy_thread

    (5)之后可跟踪到ret _ form _ fork,不能继续跟踪执行,结束调试。

    总结:通过使用gdb跟踪创建新进程的过程,可以看出创建一个新进程的过程如下:

    • 首先调用do_fork函数(函数功能是fork一个新的进程);
    • do _ fork中的copy _ process函数复制父进程的相关信息
    • dup _ task _ struct()复制进程的PCB
    • thread相关代码给新进程分配一个新的内核堆栈,copy _ thread复制内核堆栈,只复制SAVE _ ALL相关的部分
    • 设置sp调度到子进程时的内核栈顶,ip转到子进程时的第一条指令地址
    • 当子进程获得CPU的控制权开始运行的时候,ret _ form _ fork可以将后面的堆栈出栈,从iret返回到用户态,从而切换到子进程的用户空间,完成新进程的创建。

    参考资料

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

  • 相关阅读:
    玩转手工测试之客户端产品手工测试提效实践
    接口测试常用工具及测试方法(新手篇)
    我北漂 7 年,再也不打工了!
    测试人如何高效地设计自动化测试框架?
    你这样,测试人员不能活了。。。
    如何优雅地记录操作日志?
    C#String.IndexOf检索字符串中字符出现的次数
    C#基础之数组
    C#基础之is,as关键字
    C#委托与事件
  • 原文地址:https://www.cnblogs.com/July0207/p/5339058.html
Copyright © 2020-2023  润新知