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流程分析 

  • 相关阅读:
    作业要求 20181009-9 每周例行报告
    20180925-1 每周例行报告
    作业要求20180925-4 单元测试,结对
    作业要求 20180925-3 效能分析
    作业要求 20180925-6 四则运算试题生成
    20180925-7 规格说明书-吉林市2日游
    20180925-5 代码规范,结对要求
    20170925-2 功能测试
    第二周例行报告
    作业要求 20180918-1 词频统计 卢帝同
  • 原文地址:https://www.cnblogs.com/theseventhson/p/15589310.html
Copyright © 2011-2022 走看看