Linux的进程------进程的描述和进程的创建

时间:2021-09-06 17:34:08

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

一、基础知识
(1)操作系统的三个重要功能:进程管理、文件管理、内存管理
(2)为了管理进程,内核必须对每个进程进行清晰的描述,进程控制块PCB就完成了对进行描述的功能,进程控制块PCB——对应的task_struct(进程描述符)结构体,提供了内核需要了解的进程信息。
1.struct task_struct{……};里面的重要结构:
task_ struct在linux-3.18.6/include/linux/sched.h中定义
内容很多,例如:
进程的状态

volatile long state;    
/* -1 unrunnable, 0 runnable, >0 stopped */

进程状态转换图,注意两个task_running状态
Linux的进程------进程的描述和进程的创建
进程pid:唯一标识一个进程

        pid_t pid;
        pid_t tgid;

进程链表:

 struct list_head tasks;

用于链接进程,定义在include/linux/list.h。
进程链表的操作:
Linux的进程------进程的描述和进程的创建
对进程链表的操作实质上就是双向链表的操作,通过对进程链表节点的创建插入和删除,完成进程的创建调度和结束释放等操作。
进程的内核堆栈

void *stack;

Linux为每个进程分配一个8KB大小的内存区域,用于存放该进程两个不同的数据结构:
1.Thread_info(在thread _ union中定义)
2.进程的内核堆栈
(每个进程都有自己独立的4G地址空间)

union thread_union {
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

与内存管理相关的结构

struct mm_struct *mm,*active_mm;

此外还有很多,如文件系统和文件描述符,任务状态等等。
(3)解决问题:进程是如何被创建起来的?fork函数是怎么执行的?fork出来的子进程从哪里开始执行?
1.fork()的系统调用

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
    int pid;
    /* fork another process */
    pid = fork();
    if (pid < 0) 
    { 
        /* error occurred */
        fprintf(stderr,"Fork Failed!");
        exit(-1);
    } 
    else if (pid == 0) 
    {
        /* child process */
        printf("This is Child Process!\n");
    } 
    else 
    {  
        /* parent process */
        printf("This is Parent Process!\n");
        /* parent will wait for the child to complete*/
        wait(NULL);
        printf("Child Complete!\n");
    }
}

fork()调用一次返回两次,只不过返回的分别为父进程和子进程,也就是说,上面的代码有三种返回值,如果fork返回-1代表创建失败,结束。fork在子进程中会返回0,在父进程中会返回子进程pid(>0)。
2.创建一个新进程在内核中的执行过程
fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建;Linux通过复制父进程来创建一个新进程
复制父进程的PCB,要在某个位置可以修改父进程的PCB内容变为子进程自己的,应有位置分配一个新的内核堆栈,子进程从fork的系统调用返回到用户态时,可以知道返回的位置,所以还要在新的内核堆栈里保存上父进程的信息。
大致过程:
1)复制一个PCB——task_struct

err = arch_dup_task_struct(tsk, orig);

2)要给新进程分配一个新的内核堆栈

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

3)修改复制过来的进程数据,比如pid、进程链表等(在copy_process中)
4)从用户态的代码看fork();函数返回了两次,即在父子进程中各返回一次,子进程从系统调用中返回,那它在系统调用处理过程开始执行的位置以及
子进程的内核堆栈数据状态和task_struct中thread记录的sp和ip的一致性问题都需要设定。

*childregs = *current_pt_regs(); 
//复制内核堆栈
childregs->ax = 0; 
//为什么子进程的fork返回0,这里就是原因!
p->thread.sp = (unsigned long) childregs; 
//调度到子进程时的内核栈顶
p->thread.ip = (unsigned long) ret_from_fork;
//调度到子进程时的第一条指令地址

3.进入linux-3.18/kernel/fork.c查看do_fork系统调用,在do fork中调用了copy process:
copy_ process()中,开始时检测clone_ flag中的标志位,看看是否允许copy。然后就是创建两个结构体,task_ struck和thread_ info,用来保存子进程的信息,然后将父进程中的这两个地方的信息复制过来,存到刚刚创建的结构体中。然后更新一下系统中,关于进程数量等信息,更改一下子进程的clone_ flag信息。设置子进程运行的CPU,把子进程加入到运行队列。

p = copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace);

这个函数copy_ processs中调用了dup _ task_ struct 用于赋值当前PCB

    /*fpu相关操作*/  
    prepare_to_copy(orig);  
    /*通过alloc_task_struct()函数创建task_struct结构空间*/  
    tsk = alloc_task_struct(); 
    ......
   /*分配thread_info结构空间,可以看到order为1,也就是两个页面*/  
    ti = alloc_thread_info(tsk);  
    ......
    tsk->stack = ti;/*task的对应栈*/ 
    ......
   /*初始化thread info结构*/  
    setup_thread_stack(tsk, orig);  
    stackend = end_of_stack(tsk);/*返回的是栈结束的地址*/
    ...... 

到结束,基本完成了PCB的复制.
在copy_ process中

 p = dup_task_struct(current);

获得dup_task _struct返回的地址,用p指向当前进程的PCB,调用需要修改的内容进行修改,然后

retval = copy_thread(clone_flags, stack_start, stack_size, p);

copy_thread这个函数在process _32.c中定义。

        struct pt_regs *childregs;
        struct task_struct *tsk;
        int err;
        childregs = task_pt_regs(p);
        //相当于childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) - 1;
        //这儿是先转为struct pt_regs后再减1
        //这是在栈顶留出一个struct pt_regs的大小

        //保存父寄存器的值到自己存器中;
        //先将父进程的全部regs复制过来,然后再作调整
        *childregs = *regs;
        childregs->ax = 0;
        childregs->sp = sp;
        p->thread.sp = (unsigned long) childregs;
        //执行后p->thread.esp=(p-sizeof(pt_regs))​
        p->thread.sp0 = (unsigned long) (childregs+1);
        //执行后p->thread.esp0=(p),即将两页内存的最高端作为栈​顶

        p->thread.ip = (unsigned long) ret_from_fork;
        //下次调度时子进程执行的命令是ret_form_fork

childregs = task_ pt _ regs(p); 实际上是–> childregs = ((struct pt_ regs *) (THREAD_ SIZE + (unsigned long) p)) - 1;
也就是说childregs指向 “子进程的栈顶 减去 一个sizeof(struct pt_regs)的大小 的地方”
子进程堆栈空间示意图:
Linux的进程------进程的描述和进程的创建
5)子进程从哪里启动
在entry_ 32.S找到ret from work,标识了 p->thread.ip,当子进程获得CPU从内核态返回到用户态,在内核态中是从ret _ from _ work开始执行的。
ret_from _ work 会跳转到sys _ call _exit(这时的状态与调用前是相同的),从iret返回用户态,此时的内核空间变为子进程的内核空间。


二、gdb追踪进程创建的过程
添加系统menu的fork系统调用
Linux的进程------进程的描述和进程的创建
在sys_ clone(fork进行的系统调用)、do_fork、dup task struct、 copy _process ,copy _thread进行追踪查看,验证前面的过程,可以查看运行过程。
设置好断点后执行 fork,继续执行,程序停在do_ fork的位置,继续单步调式,可以看到相关的函数执行调用的过程。
Linux的进程------进程的描述和进程的创建


三、总结
进程管理是操作系统功能的重要组成部分,是理解操作系统的关键,对于fork这个系统调用(调用一次,返回两次)要记住子进程是如何被创建起来的,都拷贝了父进程的哪些信息,修改了哪些信息,子进程是如何找到返回位置的,它在内核中是如何执行起来的等等。