zoukankan      html  css  js  c++  java
  • linux源码解读(一):进程的创建、调度和销毁

      不论是做正向开发,还是逆向破解,操作系统、编译原理、数据结构和算法、计算机组成原理、计算机网络、密码学等都是非常核心和关键的课程。为了便于理解操作系统原理,这里从linux 0.11开始解读重要和核心的代码!简单理解:操作系统=计算机组成原理+数据结构和算法

      用户从开机上电开始,cpu会先用bios读取初始化代码,这一系列的初始化流程请详见本人之前撰写的x86系列教程(https://www.cnblogs.com/theseventhson/p/13030374.html),这里不再赘述;先看一下代码的整体结构,从文件名就能很容易猜出来各个模块分别是干啥的:硬件启动、文件系统、头文件、整个初始化、内核、库函数、内存管理、工具等;

            

             linux代码大部分是C写的,所以入口也不免俗,也是用main开始的,具体就是init/main.c文件的main函数,主要干了这么几件事:设置根文件和驱动、设置内存、初始化idt表、打开中断、切换到用户模式;从这里可以看出:前面这些代码执行的时候是屏蔽了中断的,所以在这个阶段,用户移动鼠标、敲击键盘什么的都是没用的!这些准备工作都做完后,终于生成了“永世不灭”的0号进程:从linus的注释看,只要没有其他任务运行了就运行0号进程;pause函数也只是查看是否有其他可以运行的任务。如果有就跳转过去运行,如果没有继续在这里死循环

      注意:这里面有个buffer_memory_end字段,标识了内核缓冲区;一般情况下:cpu往块设备写数据,会先写入这里的缓存,缓存到一定数量后统一写入设备,能提升一些效率;比如本人之前用ida去trace x音时,log文件并不是实时更新的;要等trace借结束后才会统一写入磁盘的log文件!

    //main函数 linux引导成功后就从这里开始运行
    void main(void)        /* This really IS void, no error here. */
    {            /* The startup routine assumes (well, ...) this */
    /*
     * Interrupts are still disabled. Do necessary setups, then
     * enable them
     */
    //前面这里做的所有事情都是在对内存进行拷贝
         ROOT_DEV = ORIG_ROOT_DEV;//设置操作系统的根文件
         drive_info = DRIVE_INFO;//设置操作系统驱动参数
         //解析setup.s代码后获取系统内存参数
        memory_end = (1<<20) + (EXT_MEM_K<<10);
        //取整4k的内存大小
        memory_end &= 0xfffff000;
        if (memory_end > 16*1024*1024)//控制操作系统的最大内存为16M
            memory_end = 16*1024*1024;
        if (memory_end > 12*1024*1024) 
            buffer_memory_end = 4*1024*1024;//设置高速缓冲区的大小,跟块设备有关,跟设备交互的时候,充当缓冲区,写入到块设备中的数据先放在缓冲区里,只有执行sync时才真正写入;这也是为什么要区分块设备驱动和字符设备驱动;块设备写入需要缓冲区,字符设备不需要是直接写入的
        else if (memory_end > 6*1024*1024)
            buffer_memory_end = 2*1024*1024;
        else
            buffer_memory_end = 1*1024*1024;
        main_memory_start = buffer_memory_end;
    #ifdef RAMDISK
        main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
    #endif
    //内存控制器初始化
        mem_init(main_memory_start,memory_end);
        //异常函数初始化,主要是初始化idt表
        trap_init();
        //块设备驱动初始化
        blk_dev_init();
        //字符型设备出动初始化
        chr_dev_init();
        //控制台设备初始化
        tty_init();
        //加载定时器驱动
        time_init();
        //进程间调度初始化
        sched_init();
        //缓冲区初始化
        buffer_init(buffer_memory_end);
        //硬盘初始化
        hd_init();
        //软盘初始化
        floppy_init();
        sti();
        //从内核态切换到用户态,上面的初始化都是在内核态运行的
        //内核态无法被抢占,不能在进程间进行切换,运行不会被干扰
        move_to_user_mode();
        if (!fork()) {    //创建0号进程 fork函数就是用来创建进程的函数    /* we count on this going ok */
            //0号进程是所有进程的父进程
            init();
        }
    /*
     *   NOTE!!   For any other task 'pause()' would mean we have to get a
     * signal to awaken, but task 0 is the sole exception (see 'schedule()')
     * as task 0 gets activated at every idle moment (when no other tasks
     * can run). For task0 'pause()' just means we go check if some other
     * task can run, and if not we return here.
     */
    //0号进程永远不会结束,他会在没有其他进程调用的时候调用,只会执行for(;;) pause();
        for(;;) pause();
    }

       1、main中有个非常核心的函数:fork;linux中所有的进程创建都通过fork函数;这个函数本质上是个系统调用,实现代码在system_call.s中,如下:

    _sys_fork://fork的系统调用
        call _find_empty_process//调用这个函数
        testl %eax,%eax
        js 1f
        push %gs
        pushl %esi
        pushl %edi
        pushl %ebp
        pushl %eax
        call _copy_process//
        addl $20,%esp
    1:    ret

      整个fork的执行过程并不复杂:先是找空进程,再复制进程,这两个函数到底是怎么做的了?在kernel/fork.c中有他们的实现过程,先来看看find_empty_process: 这个版本的NR_TASKS=64,也就是说最多支持64个进程“同时”运行(直观感觉这算个漏洞啊,如果别有用心的人想办法短时间内恶意把进程数提升到64个,是不是就没法运行新的程序了?间接达到DOS的效果!整个代码很简单:直接遍历64个task_struct结构体,看看哪个结构体还是null的,说明这个结构体还未初始化,没被使用,直接返回这个task结构体在数组的index。这个index也被用来作为进程的编号,也就是pid

    int find_empty_process(void)
    {
        int i;
    
        repeat:
            if ((++last_pid)<0) last_pid=1;
            for(i=0 ; i<NR_TASKS ; i++)
                if (task[i] && task[i]->pid == last_pid) goto repeat;
        for(i=1 ; i<NR_TASKS ; i++)
            if (!task[i])//直到找到一个空的task结构体
                return i;
        return -EAGAIN;//达到64的最大值后,返回错误码
    }

      一旦找到空的进程(实际上是还没使用的task结构体,就是所谓的进程槽),继续执行copy_process,这里又拷贝了啥了?

    • 新建一个task结构体,并分配内存
    • 根据上一步得到的pid,把task结构体保存在这个index
    • 用传入的参数初始化task结构体(主要是context寄存器)
    • 设置子进程的ldt,并复制父进程的数据段(注意:不复制代码段,否则没必要生成子进程了)
    • 父进程打开过的文件,子进程继承该属性
    • 在gdt中设置该子进程的tss和ldt
    • 子进程设置成运行态

      总结:函数名称叫copy_process,实际上只是拷贝了父进程的数据段,继承了父进程打开文件的数量,其他都是“个性化”设置的,所以linus当初为啥要取名为copy_process了?这里很不解!

    // 对内存拷贝
    // 主要作用就是把代码段数据段等栈上的数据拷贝一份
    int copy_mem(int nr,struct task_struct * p)
    {
        unsigned long old_data_base,new_data_base,data_limit;
        unsigned long old_code_base,new_code_base,code_limit;
    
        code_limit=get_limit(0x0f);
        data_limit=get_limit(0x17);
        old_code_base = get_base(current->ldt[1]);
        old_data_base = get_base(current->ldt[2]);
        if (old_data_base != old_code_base)
            panic("We don't support separate I&D");
        if (data_limit < code_limit)
            panic("Bad data_limit");
        //数据段和代码段的base地址是一样的
        new_data_base = new_code_base = nr * 0x4000000;
        //设置新进程代码入口地址
        p->start_code = new_code_base;
        //设置新进程的idt
        set_base(p->ldt[1],new_code_base);
        set_base(p->ldt[2],new_data_base);
        //新进程直接简单粗暴地复制了父进程的数据,但是代码并未复用
        if (copy_page_tables(old_data_base,new_data_base,data_limit)) {
            free_page_tables(new_data_base,data_limit);
            return -ENOMEM;
        }
        return 0;
    }
    
    /*
     *  Ok, this is the main fork-routine. It copies the system process
     * information (task[nr]) and sets up the necessary registers. It
     * also copies the data segment in it's entirety.
     */
    // 所谓进程创建就是对0号进程或者当前进程的复制
    // 就是结构体的复制 把task[0]对应的task_struct 复制一份
    //除此之外还要对栈堆拷贝 当进程做创建的时候要复制原有的栈堆
    // nr就是刚刚找到的空槽的pid
    int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
            long ebx,long ecx,long edx,
            long fs,long es,long ds,
            long eip,long cs,long eflags,long esp,long ss)
    {
        struct task_struct *p;
        int i;
        struct file *f;
        //其实就是malloc分配内存
        p = (struct task_struct *) get_free_page();//在内存分配一个空白页,让指针指向它
        if (!p)
            return -EAGAIN;//如果分配失败就是返回错误
        task[nr] = p;//把这个指针放入进程的链表当中
        *p = *current;//把当前进程赋给p,也就是拷贝一份    /* NOTE! this doesn't copy the supervisor stack */
        //后面全是对这个结构体进行赋值相当于初始化赋值
        p->state = TASK_UNINTERRUPTIBLE;
        p->pid = last_pid;
        p->father = current->pid;
        p->counter = p->priority;
        p->signal = 0;
        p->alarm = 0;
        p->leader = 0;        /* process leadership doesn't inherit */
        p->utime = p->stime = 0;
        p->cutime = p->cstime = 0;
        p->start_time = jiffies;//当前的时间
        p->tss.back_link = 0;
        p->tss.esp0 = PAGE_SIZE + (long) p;
        p->tss.ss0 = 0x10;
        p->tss.eip = eip;
        p->tss.eflags = eflags;
        p->tss.eax = 0;//把寄存器的参数添加进来
        p->tss.ecx = ecx;
        p->tss.edx = edx;
        p->tss.ebx = ebx;
        p->tss.esp = esp;
        p->tss.ebp = ebp;
        p->tss.esi = esi;
        p->tss.edi = edi;
        p->tss.es = es & 0xffff;
        p->tss.cs = cs & 0xffff;
        p->tss.ss = ss & 0xffff;
        p->tss.ds = ds & 0xffff;
        p->tss.fs = fs & 0xffff;
        p->tss.gs = gs & 0xffff;
        p->tss.ldt = _LDT(nr);
        p->tss.trace_bitmap = 0x80000000;
        if (last_task_used_math == current)//如果使用了就设置协处理器
            __asm__("clts ; fnsave %0"::"m" (p->tss.i387));
        if (copy_mem(nr,p)) {//老进程向新进程代码段和数据段进行拷贝
            task[nr] = NULL;//如果失败了
            free_page((long) p);//就释放当前页
            return -EAGAIN;
        }
        for (i=0; i<NR_OPEN;i++)//
            if (f=p->filp[i])//父进程打开过文件
                f->f_count++;//就会打开文件的计数+1,说明会继承这个属性
        if (current->pwd)//跟上面一样
            current->pwd->i_count++;
        if (current->root)
            current->root->i_count++;
        if (current->executable)
            current->executable->i_count++;
        //设置GDT表的tss和ldt
        set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
        set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
        p->state = TASK_RUNNING;//把状态设定为运行状态    /* do this last, just in case */
        return last_pid;//返回新创建进程的id号
    }

       fork执行结束后,如果是0号进程(也就是父进程)成功创建,会继续执行init函数:这个函数内部的代码也很特别,不知道大家有没有注意到:里面有个while(1)循环,而且没有break打断,也就是说这里面的代码会一直执行,直到被中断/系统调用等方式打断;等中断/系统调用执行万后又会遇到这里继续执行!作者本意因该是父进程只负责创建子进程,创建好的子进程来执行/bin/sh,周而复始一直循环执行!所以问题又来了:为什么要不停的生成子进程去执行/bin/sh了?sh是用来接受用户输入并执行的程序,为了让用户随时随地可以输入指令,这里只好不停地生成进程执行/bin/sh了

    void init(void)
    {
        int pid,i;
        //设置了驱动信息
        setup((void *) &drive_info);
        //打开标准输入控制台 句柄为0, 方便进程交互
        (void) open("/dev/tty0",O_RDWR,0);
        (void) dup(0);//打开标准输入控制台 这里是复制句柄的意思
        (void) dup(0);//打开标准错误控制台
        printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,
            NR_BUFFERS*BLOCK_SIZE);
        printf("Free mem: %d bytes\n\r",memory_end-main_memory_start);
        if (!(pid=fork())) {//这里创建1号进程
            close(0);//关闭了0号进程的标准输入输出
            if (open("/etc/rc",O_RDONLY,0))//如果1号进程创建成功打开/etc/rc这里面保存的大部分是系统配置文件 开机的时候要什么提示信息全部写在这个里面
                _exit(1);
            execve("/bin/sh",argv_rc,envp_rc);//运行shell程序
            _exit(2);
        }
        if (pid>0)//如果这个不是0号进程
            while (pid != wait(&i))//就等待父进程退出,等待期间啥也不干
                /* nothing */;
        while (1) {
            if ((pid=fork())<0) {//再创建新进程:如果创建失败
                printf("Fork failed in init\r\n");
                continue;
            }
            //如果创建成功
            if (!pid) {//这段代码是在子进程执行的
                close(0);close(1);close(2);//关闭上面那几个输入输出错误的句柄
                setsid();//重新设置id
                (void) open("/dev/tty0",O_RDWR,0);
                (void) dup(0);
                (void) dup(0);//重新打开
                _exit(execve("/bin/sh",argv,envp));//这里不是上面的argv_rc和envp_rc了是因为怕上面那种创建失败,换了一种环境变量来创建,过程和上面是一样的其实
            }
            //这段代码是在父进程执行的:如果还在父进程,那么等待子进程结束退出,并重新开始循环
            while (1)
                if (pid == wait(&i))
                    break;
            printf("\n\rchild %d died with code %04x\n\r",pid,i);
            sync();
        }
        _exit(0);    /* NOTE! _exit, not exit() */
    }

      进程相关重要的结构体: task_struct:描述了task的方方面面,比如时间片、优先级、信号、pid、运行时间、ldt、tss等,每个进程都会生成一个task结构体来存储该进程的所有属性!

    //task即进程的意思,这个结构体把进程能用到的所有信息进行了封装
    struct task_struct {
    /* these are hardcoded - don't touch */
        long state;    //程序运行的状态/* -1 unrunnable, 0 runnable, >0 stopped */
        long counter; //时间片
        //counter的计算不是单纯的累加,需要下面这个优先级这个参数参与
        long priority;//优先级
        long signal;//信号
        struct sigaction sigaction[32];//信号位图
        long blocked;//阻塞状态    /* bitmap of masked signals */
    /* various fields */
        int exit_code;//退出码
        unsigned long start_code,end_code,end_data,brk,start_stack;
        long pid,father,pgrp,session,leader;
        unsigned short uid,euid,suid;
        unsigned short gid,egid,sgid;
        long alarm;//警告
        long utime,stime,cutime,cstime,start_time;//运行时间
        //utime是用户态运行时间 cutime是内核态运行时间
        unsigned short used_math;
    /* file system info */
        int tty;    //是否打开了控制台    /* -1 if no tty, so it must be signed */
        unsigned short umask;
        struct m_inode * pwd;
        struct m_inode * root;
        struct m_inode * executable;
        unsigned long close_on_exec;
        struct file * filp[NR_OPEN];//打开了多少个文件
    /* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
        struct desc_struct ldt[3];//ldt包括两个东西,一个是数据段(全局变量静态变量等),另一个是代码段,不过这里面存的都是指针
    /* tss for this task */
        struct tss_struct tss;//进程运行过程中CPU需要知道的进程状态标志(段属性、位属性等)
    };

      因为最多“同时”运行64个进程,为了快速遍历、查找目标进程,这里用一个数组来管理所有的进程,如下: 

    extern struct task_struct *task[NR_TASKS];//进程的“链表”数组

      只要拿到task数组,就等于得到了所有的task结构体,也就掌控了所有的进程;我们平时用的ps命令、用调试器查看所有进程疑似就是这样得到进程列表的!用数组管理task结构体属于早期方法,这种方法的缺点也很明显:由于数组定长,这里只能“同时”运行64个进程,所以后来windows改成了双向链表:进程和线程之间都是通过双向链表连接的,遍历也是通过双向链表,这样对于进程或线程的数量就没有限制了(只要内存足够大)

      2、进程创建好后就需要调度了,核心代码在sched.c的schedule函数中,如下:

    • 如果时间到点了,设置进程的sigalrm信号;如果进程处于可中断休眠状态,那么把该进程设置为可运行状态;
    • 接着找到时间片最大的进程;如果没找到,就根据优先级重新计算进程的时间片,公式很简单:counter = counter/2 + priority
    • 最后切换到目标进程执行
    // 时间片分配
    void schedule(void)
    {
        int i,next,c;
        struct task_struct ** p;//双重指针,指向task结构体数组
    
    /* check alarm, wake up any interruptible tasks that have got a signal */
    
        for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)//通过task结构体数组从后往前遍历每个task
            if (*p) {//在哪个jiffies发生警告?
                if ((*p)->alarm && (*p)->alarm < jiffies) {//alarm存在,并且已经到点
                        (*p)->signal |= (1<<(SIGALRM-1));//新增一个警告信号量
                        (*p)->alarm = 0;//警告清空
                    }
                    //如果该进程为可中断睡眠状态 则如果该进程有非屏蔽信号出现就将该进程的状态设置为running
                if (((*p)->signal //有singal
                        & ~(_BLOCKABLE & (*p)->blocked)) && //并且非阻塞
                            (*p)->state==TASK_INTERRUPTIBLE) //并且状态是可中断的
                    (*p)->state=TASK_RUNNING;
            }
    
    /* this is the scheduler proper: */
        // 以下思路,循环task列表 根据counter大小决定进程切换
        while (1) {
            c = -1;
            next = 0;
            i = NR_TASKS;
            p = &task[NR_TASKS];//从最后一个任务开始循环
            while (--i) {
                if (!*--p)//task结构体是空,继续循环
                    continue;
                //task结构体不为空,说明有进程
                if ((*p)->state == TASK_RUNNING && (*p)->counter > c)//找出c最大的task
                    c = (*p)->counter, next = i;
            }
            if (c) break;//如果c找到了,就终结循环;如果为0,说明所有进程的时间片都用光了
            //如果没有找到最大时间片的进程,就根据优先级进行时间片的重新分配
            for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
                if (*p)//这里很关键,在低版本内核中,是进行优先级时间片轮转分配,这里搞清楚了优先级和时间片的关系
                //counter = counter/2 + priority
                    (*p)->counter = ((*p)->counter >> 1) +
                            (*p)->priority;
        }
        //切换到下一个进程 这个功能使用宏定义完成的
        switch_to(next);
    }

       任务切换前面的代码容易理解,最后一个switch_to的代码如下:首先声明了一个_tmp的结构,这个结构里面包括两个long型,32位机里面long占32位,声明这个结构主要与ljmp这个长跳指令有关(任务跳转就是通过ljmp指令实现的),这个指令有两个参数,一个参数是段选择符,另一个是偏移地址,所以这个_tmp就是保存这两个参数。再比较任务n是不是当前任务,如果不是则跳转到标号1,否则交互ecx和current的内容,交换后的结果为ecx指向当前进程,current指向要切换过去的新进程;最后执行长跳,%0代表输出输入寄存器列表中使用的第一个寄存器,即"m"(*&__tmp.a),这个寄存器保存了*&__tmp.a,而_tmp.a存放的是32位偏移,_tmp.b存放的是新任务的tss段选择符,长跳到段选择符会造成任务切换

    /*
     *    switch_to(n) should switch tasks to task nr n, first
     * checking that n isn't the current task, in which case it does nothing.
     * This also clears the TS-flag if the task we switched to has used
     * tha math co-processor latest.
     */
    // 进程切换是用汇编宏定义实现的
    //1. 将需要切换的进程赋值给当前进程的指针
    //2. 将进程的上下文(TSS和当前堆栈中的信息)切换
    #define switch_to(n) {\
    struct {long a,b;} __tmp; \
    __asm__("cmpl %%ecx,_current\n\t" \
        "je 1f\n\t" \
        "movw %%dx,%1\n\t" \
        "xchgl %%ecx,_current\n\t" \
        "ljmp %0\n\t" \
        "cmpl %%ecx,_last_task_used_math\n\t" \
        "jne 1f\n\t" \
        "clts\n" \
        "1:" \
        ::"m" (*&__tmp.a),"m" (*&__tmp.b), \
        "d" (_TSS(n)),"c" ((long) task[n])); \
    }

       这里的切换是通过TSS实现的,这也是x86硬件提供的切换方式,图示如下:

             

       3、进程(为了便于辨识,这里叫A进程)通过schedule开始执行后,不太可能一直运行,中途可能需要等待,比如等待需要某些资源,这时就需要主动把cpu让出去,让其他task执行,避免自己“占着茅坑不拉屎”,这时就需要让进程sleep了,0.11版本的linux是这么干的: 最核心的就是把task结构体中的state改成TASK_UNINTERRUPTIBLE后重新调用schedule函数运行其他任务;由于本任务的状态已经不是TASK_RUNNING了,所以schedule函数不会跳转到当前task执行!注意:调用schedule函数后,如果有时间片高的进程B,会通过ljmp跳转到B进程执行,所以A进程的schedule此时时不返回的,导致下面的if(tmp)代码是不执行的!直到其他某个进程比如C进程调用wake_up函数,把A进程的状态改成runable,C进程再调用schedule函数,A进程才可能继续执行后续的if(tmp)代码

    // 把当前任务置为不可中断的等待状态,并让睡眠队列指针指向当前任务。
    // 只有明确的唤醒时才会返回。该函数提供了进程与中断处理程序之间的同步机制。函数参数P是等待
    // 任务队列头指针。指针是含有一个变量地址的变量。这里参数p使用了指针的指针形式'**p',这是因为
    // C函数参数只能传值,没有直接的方式让被调用函数改变调用该函数程序中变量的值。但是指针'*p'
    // 指向的目标(这里是任务结构)会改变,因此为了能修改调用该函数程序中原来就是指针的变量的值,
    // 就需要传递指针'*p'的指针,即'**p'.
    void sleep_on(struct task_struct **p)
    {
        struct task_struct *tmp;
    
        // 若指针无效,则退出。(指针所指向的对象可以是NULL,但指针本身不应该为0).另外,如果
        // 当前任务是任务0,则死机。因为任务0的运行不依赖自己的状态,所以内核代码把任务0置为
        // 睡眠状态毫无意义。
        if (!p)
            return;
        if (current == &(init_task.task))
            panic("task[0] trying to sleep");
        // 让tmp指向已经在等待队列上的任务(如果有的话),例如inode->i_wait.并且将睡眠队列头的
        // 等等指针指向当前任务。这样就把当前任务插入到了*p的等待队列中。然后将当前任务置为
        // 不可中断的等待状态,并执行重新调度。
        tmp = *p;
        *p = current;
        current->state = TASK_UNINTERRUPTIBLE;
        schedule();
        // 只有当这个等待任务被唤醒时,调度程序才又返回到这里,表示本进程已被明确的唤醒(就
        // 续态)。既然大家都在等待同样的资源,那么在资源可用时,就有必要唤醒所有等待该该资源
        // 的进程。该函数嵌套调用,也会嵌套唤醒所有等待该资源的进程。这里嵌套调用是指一个
        // 进程调用了sleep_on()后就会在该函数中被切换掉,控制权呗转移到其他进程中。此时若有
        // 进程也需要使用同一资源,那么也会使用同一个等待队列头指针作为参数调用sleep_on()函数,
        // 并且也会陷入该函数而不会返回。只有当内核某处代码以队列头指针作为参数wake_up了队列,
        // 那么当系统切换去执行头指针所指的进程A时,该进程才会继续执行下面的代码,把队列后一个
        // 进程B置位就绪状态(唤醒)。而当轮到B进程执行时,它也才可能继续执行下面的代码。若它
        // 后面还有等待的进程C,那它也会把C唤醒等。在这前面还应该添加一行:*p = tmp.
        if (tmp)                    // 若在其前还有存在的等待的任务,则也将其置为就绪状态(唤醒).
            tmp->state=0;
    }

      上述整个过程图示如下:

             

      唤醒进程的代码也很简单,如下:核心也是把task的state改成0,也就是runable!注意:这里把*p=NULL是为啥了? 唤醒进程后,进程终于可以从sleep_on函数中的schedule()下一行代码开始运行,此时会通过tmp->state=0把状态改成runable,所以如果这个进程下次再被sleep时,wake_up这里不需要再设置状态了!

    void wake_up(struct task_struct **p)
    {
        if (p && *p) {
            (**p).state=0;
            *p=NULL;
        }
    }

       4、进程的代码运行完毕,用户也拿到了想要的结果,进程就可以销毁了;销毁进程的入口在kernel/exit.c/do_exit()函数里,流程也不复杂:

    • 释放ldt占用的内存
    • 如果是某个进程的父进程,更子进程的新父进程为1号进程
    • 关闭文件
    • 关闭终端、清空协处理器
    • 给父进程发signal
    • 重新调度
    int do_exit(long code)
    {
        int i;
        //释放内存页
        free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
        free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
        //current->pid就是当前需要关闭的进程
        for (i=0 ; i<NR_TASKS ; i++)
            if (task[i] && task[i]->father == current->pid) {//如果当前进程是某个进程的父进程
                task[i]->father = 1;//就让1号进程作为新的父进程
                if (task[i]->state == TASK_ZOMBIE)//如果是僵死状态
                    /* assumption task[1] is always init */
                    (void) send_sig(SIGCHLD, task[1], 1);//给父进程发送SIGCHLD
            }
        for (i=0 ; i<NR_OPEN ; i++)//每个进程能打开的最大文件数NR_OPEN=20
            if (current->filp[i])
                sys_close(i);//关闭文件
        iput(current->pwd);
        current->pwd=NULL;
        iput(current->root);
        current->root=NULL;
        iput(current->executable);
        current->executable=NULL;
        if (current->leader && current->tty >= 0)
            tty_table[current->tty].pgrp = 0;//清空终端
        if (last_task_used_math == current)
            last_task_used_math = NULL;//清空协处理器
        if (current->leader)
            kill_session();//清空session
        current->state = TASK_ZOMBIE;//设为僵死状态
        current->exit_code = code;
        tell_father(current->father);
        schedule();
        return (-1);    /* just to suppress warnings */
    }

      在执行do_exit方法的时候,间接调用了一些重要的函数,如下:

      (1)release函数:释放task结构体本身占用的内存,并重新调度

    void release(struct task_struct * p)
    {
        int i;
    
        if (!p)
            return;
        for (i=1 ; i<NR_TASKS ; i++)//在task[]中进行遍历
            if (task[i]==p) {
                task[i]=NULL;
                free_page((long)p);//释放内存页
                schedule();//重新进行进程调度
                return;
            }
        panic("trying to release non-existent task");
    }

      (2)给指定的进程发送信号,本质就是通过task结构体给对方进程的signal字段增加一个值!为了确保安全,给对方发信号需要具备以下三个条件之一:

    • 权限不为0
    • euid确实是当前进程的
    • 系统超级用户
    static inline int send_sig(long sig,struct task_struct * p,int priv)
    {
        if (!p || sig<1 || sig>32)
            return -EINVAL;
        if (priv //要么权限不为0
            || (current->euid==p->euid) //要么euid相等(当前进程使用者)
                || suser()) //要么是超级用户才能给另一个进程发信号,这里可以确保安全,避免接收到恶意信号
            p->signal |= (1<<(sig-1));
        else
            return -EPERM;
        return 0;
    }

      (3)关闭进程间对话的session:居然也是给task结构体的signal字段增加一个值

    //关闭session
    static void kill_session(void)
    {
        struct task_struct **p = NR_TASKS + task;//指向最后一个task结构体
        while (--p > &FIRST_TASK) {//从最后一个开始扫描(不包括0进程)
            if (*p && (*p)->session == current->session)//确认确实是当前会话
                (*p)->signal |= 1<<(SIGHUP-1);
        }
    }

      (4)通知被销毁进程的父进程:通过遍历task结构体数组找到父进程的task结构体,然后增加signal字段的sigchld值!(本人调试x音的时候ida经常会收到这个消息,只要点击确认x音就直接退出)

    static void tell_father(int pid)
    {
        int i;
        if (pid)
            for (i=0;i<NR_TASKS;i++) {
                if (!task[i])
                    continue;
                if (task[i]->pid != pid)
                    continue;
                task[i]->signal |= (1<<(SIGCHLD-1));//找到父亲发送SIGCHLD信号
                return;
            }
    /* if we don't find any fathers, we just release ourselves */
    /* This is not really OK. Must change it to make father 1 */
        printk("BAD BAD - no father found\n\r");
        release(current);//释放子进程
    }

      总结:这里的tell_father、kill_session都是通过发送signal实现的;发送signal的方式也很简单:直接找到对方的task结构体,通过“或”逻辑运算增加信号量

       (5)还有一个“挂羊头、卖狗肉”的方法:sys_kill如下:名字叫sys_kill,实际上是在给目标task结构体发信号!linux的shell中kill命令就是用这个函数实现的!

    // 系统调用 向任何进程 发送任何信号(类比shell中的kill命令也是发送信号的意思)
    int sys_kill(int pid,int sig)
    {
        struct task_struct **p = NR_TASKS + task;//指向最后
        int err, retval = 0;
    
        if (!pid) while (--p > &FIRST_TASK) {
            if (*p && (*p)->pgrp == current->pid) //如果pid=0,就给当前进程所在的进程组发信号
                if (err=send_sig(sig,*p,1))
                    retval = err;
        } else if (pid>0) while (--p > &FIRST_TASK) {//pid>0给对应进程发送信号
            if (*p && (*p)->pid == pid) 
                if (err=send_sig(sig,*p,0))
                    retval = err;
        } else if (pid == -1) while (--p > &FIRST_TASK)//pid=-1给任何进程发送
            if (err = send_sig(sig,*p,0))
                retval = err;
        else while (--p > &FIRST_TASK)//pid<-1 给进程组号为-pid的进程组发送信息
            if (*p && (*p)->pgrp == -pid)
                if (err = send_sig(sig,*p,0))
                    retval = err;
        return retval;
    }

      5、父进程创建子进程时,有时候需要等待子进程执行完毕,需要调用sys_waitpid函数阻塞父进程自己,如下:如果子进程是僵死状态,就把子进程运行的时间叠加到父进程,然后释放子进程的task结构体!如果子进程还在运行,父进程通过schedule让出cpu,把自己阻塞;下次被唤醒后再次检查子进程是否给自己发送了SIGCHLD信号;如果没收到,重复检查的过程,直到子进程庄涛变为僵死后返回继续执行后续的代码

    int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options)
    {
        int flag, code;
        struct task_struct ** p;
    
        verify_area(stat_addr,4);//验证区域是否可以用
    repeat:
        flag=0;
        for(p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
            if (!*p || *p == current)
                continue;
            if ((*p)->father != current->pid)
                continue;
            if (pid>0) {
                if ((*p)->pid != pid)
                    continue;
            } else if (!pid) {
                if ((*p)->pgrp != current->pgrp)
                    continue;
            } else if (pid != -1) {
                if ((*p)->pgrp != -pid)
                    continue;
            }
            switch ((*p)->state) {
                case TASK_STOPPED://子进程是stop状态
                    if (!(options & WUNTRACED))
                        continue;
                    put_fs_long(0x7f,stat_addr);
                    return (*p)->pid;
                case TASK_ZOMBIE://子进程是僵死状态:把子进程消耗的时间叠加到父进程,并释放子进程的结构体
                    current->cutime += (*p)->utime;
                    current->cstime += (*p)->stime;
                    flag = (*p)->pid;
                    code = (*p)->exit_code;
                    release(*p);
                    put_fs_long(code,stat_addr);
                    return flag;
                default:
                    flag=1;//子进程还在运行,设置flag为1,好让下面的代码执行
                    continue;
            }
        }
        if (flag) {//说明子进程状态不是stop或zombie,父进程需要阻塞等待,最直接的办法就是让出cpu
            if (options & WNOHANG)
                return 0;
            current->state=TASK_INTERRUPTIBLE;//设置程可种段的
            schedule();//父进程阻塞,让出cpu
            //当父进程再次被唤醒后,检查一下是否收到了子进程结束的通知;如果没有,再次从repeate开始执行
            if (!(current->signal &= ~(1<<(SIGCHLD-1))))
                goto repeat;
            else
                return -EINTR;
        }
        return -ECHILD;
    }

       这么一圈代码解读下来,个人觉得的需要总结的一些要点:

    • 所谓进程,本质上就是task结构体;结构体包含了很多字段属性,用来描述进程的方方面面(借鉴了面向对象的思想)
    • 对于进程的各种操作,本质上就是修改task结构体的属性,通过这些属性的逻辑组合完成各种复杂的功能;这里有点像汽车:汽车本质上也是由螺丝钉、齿轮、轴承等基础零配件构成的,但这些零配件通过一定的逻辑关系结合,就实现了复杂的功能!
    • 找到task数组就等于找到所有进程的task结构体;

       为了便于记忆和理解,整理了一些要点:

            

       后续解读linux源码的时候,会发现大量的struct,每个struct又有很多字段构成,了解清楚每个字段的含义才能真正理解操作系统的各个细节,这里简单总结一下struct内部各个变量的类型:

    • 指针:也就是地址
    • 计数/计量的,比如size、length、index、count、height、amount、sequenceNo等
    • 有应用业务意义的值:id、name等
    • 标记位/控制位:flags等

    参考:

    1、源码下载:https://mirrors.edge.kernel.org/pub/linux/kernel/

                            https://github.com/karottc/linux-0.11
    2、https://www.bilibili.com/video/BV1tQ4y1d7mo?p=1  操作系统体系结构
    3、https://www.bilibili.com/video/BV1VJ41157wq?spm_id_from=333.999.0.0  linux操作系统-构建自己的内核
    4、https://blog.csdn.net/heiworld/article/details/25397155  对linux 0.11版本中switch_to()的理解
    5、https://www.rutk1t0r.org/2016/12/23/Linux%E5%86%85%E6%A0%B80-11%E5%AE%8C%E5%85%A8%E6%B3%A8%E9%87%8A-%E5%85%B3%E4%BA%8E%E4%BB%BB%E5%8A%A1%E7%9D%A1%E7%9C%A0%E5%92%8C%E5%94%A4%E9%86%92%E7%9A%84%E7%90%86%E8%A7%A3/  Linux内核0.11完全注释 关于任务睡眠和唤醒的理解
    6、https://blog.csdn.net/u012351051/article/details/79646843  linux-0.11/init/main.c流程分析 

  • 相关阅读:
    11. Container With Most Water
    9. Palindrome Number
    375. 猜数字大小 II leetcode java
    leetcode 72 编辑距离 JAVA
    73. 矩阵置零 leetcode JAVA
    快速排序 JAVA实现
    63. 不同路径 II leetcode JAVA
    重写(override)与重载(overload)
    62 不同路径 leetcode JAVA
    leetcode 56 合并区间 JAVA
  • 原文地址:https://www.cnblogs.com/theseventhson/p/15589310.html
Copyright © 2011-2022 走看看