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

posted @ 2021-11-24 21:48  第七子007  阅读(7699)  评论(1编辑  收藏  举报