<Linux内核学习>系统的进程管理

环境:Linux 0.11 / Linux 3.4.2

参考书籍:Linux内核完全剖析基于0.11内核-赵炯

一、系统进程的运转方式

Linux系统能够运转依靠的是系统时间(jiffies)。

在CPU的内部有一个RTC(对于PC为CMOS),CPU在上电的时候会调用kernel_mktime()函数计算出从1970年1月1日0时开始到当前开机时间所过的秒数并保存在全局变量startup_time中,并且后面会被jiffies所使用

/*
 *  linux/kernel/mktime.c
 *
 *  (C) 1991  Linus Torvalds
 */

// 系统滴答,系统上电之后,会自动算出从1970年1月1日到当前开机点所过的秒数
#include <time.h>

/*
 * This isn't the library routine, it is only used in the kernel.
 * as such, we don't care about years<1970 etc, but assume everything
 * is ok. Similarly, TZ etc is happily ignored. We just do everything
 * as easily as possible. Let's find something public for the library
 * routines (although I think minix times is public).
 */
/*
 * PS. I hate whoever though up the year 1970 - couldn't they have gotten
 * a leap-year instead? I also hate Gregorius, pope or no. I'm grumpy.
 */
#define MINUTE 60
#define HOUR (60*MINUTE)
#define DAY (24*HOUR)
#define YEAR (365*DAY)

/* interestingly, we assume leap-years */
static int month[12] = {
    0,
    DAY*(31),
    DAY*(31+29),
    DAY*(31+29+31),
    DAY*(31+29+31+30),
    DAY*(31+29+31+30+31),
    DAY*(31+29+31+30+31+30),
    DAY*(31+29+31+30+31+30+31),
    DAY*(31+29+31+30+31+30+31+31),
    DAY*(31+29+31+30+31+30+31+31+30),
    DAY*(31+29+31+30+31+30+31+31+30+31),
    DAY*(31+29+31+30+31+30+31+31+30+31+30)
};
//这个函数用来算出据1970年所有的秒数
long kernel_mktime(struct tm * tm)//这个tm是开机的时候从CMOS读出来的
{
    long res;
    int year;

    year = tm->tm_year - 70;
/* magic offsets (y+1) needed to get leapyears right.*/
    res = YEAR*year + DAY*((year+1)/4);
    res += month[tm->tm_mon];
/* and (y+2) here. If it wasn't a leap-year, we have to adjust */
    if (tm->tm_mon>1 && ((year+2)%4))
        res -= DAY;
    res += DAY*(tm->tm_mday-1);
    res += HOUR*tm->tm_hour;
    res += MINUTE*tm->tm_min;
    res += tm->tm_sec;
    return res;
}

什么是JIFFIES

JIFFIES是一个系统的时钟滴答,对于Linux 0.11内核,一个系统时钟滴答为10ms。JIFFIES的产生是来源于一个10ms的定时器中断定义在(_timer_interrupt:/kernal/system_call.s文件中)

.align 2
//JIFFIES 是一个系统的时钟滴答 一个系统滴答是10ms,每隔10ms会引发一个定时器中断
//就是这个
_timer_interrupt:
    push %ds        # save ds,es and put kernel data space
    push %es        # into them. %fs is used by _system_call
    push %fs
    pushl %edx        # we save %eax,%ecx,%edx as gcc doesn't
    pushl %ecx        # save those across function calls. %ebx
    pushl %ebx        # is saved as we use that in ret_sys_call
    pushl %eax
    movl $0x10,%eax
    mov %ax,%ds
    mov %ax,%es
    movl $0x17,%eax
    mov %ax,%fs
    incl _jiffies //自加自身
    movb $0x20,%al        # EOI to interrupt controller #1
    outb %al,$0x20
    movl CS(%esp),%eax
    andl $3,%eax        # %eax is CPL (0 or 3, 0=supervisor)
    pushl %eax//以上是在中断时对现场进行保存
    call _do_timer        # 'do_timer(long CPL)' does everything from
    addl $4,%esp        # task switching to accounting ...
    jmp ret_from_sys_call

_timer_interrupt中断程序中首先

①每10毫秒将变量_jiffies自增1。

②发送中断结束指令给8259A中断控制器。

③把当前的特权级作为参数传给c函数do_timer()

④do_timer()调用返回后再去检测并处理信号。

do_timer定义在/kernal/sched.c中

void do_timer(long cpl)
{
    extern int beepcount;
    extern void sysbeepstop(void);

    if (beepcount)
        if (!--beepcount)
            sysbeepstop();

    if (cpl)//cpl表示当前被中断的进程是用户态还是内核态
        current->utime++;//给用户程序运行时间+1
    else
        current->stime++;//内核程序运行时间+1

    if (next_timer) {// next_timer 是连接jiffies变量的所有定时器的事件链表
    // 可以这样想象,jiffies是一个时间轴,然后这个时间轴上每个绳结上绑了一个事件,运行到该绳结就触发对应的事件
        next_timer->jiffies--;
        while (next_timer && next_timer->jiffies <= 0) {
            void (*fn)(void);
            
            fn = next_timer->fn;
            next_timer->fn = NULL;
            next_timer = next_timer->next;
            (fn)();
        }
    }
    if (current_DOR & 0xf0)//取高四位
        do_floppy_timer();
    if ((--current->counter)>0) return;
    current->counter=0;//counter进程的时间片为0,task_struct[]是进程的向量表 
    //counter在哪里用? 进程的调度就是task_struct[]中检索,找时间片最大的进程对象来运行 直到时间片为0退出 之后再进行新一轮调用
    //counter在哪里被设置? 当task_struct[]所有进程的counter都为0,就进行新一轮的时间片分配
    if (!cpl) return;
    schedule();//这个就是进行时间片分配
}

先分析关键代码:

① 在do_timer()程序中首先进行判断当前的特权级,如果cpl = 0代表运行在内核态,将内核运行时间+1,否则运行在用户态并将用户运行时间+1。

扫描二维码关注公众号,回复: 14730094 查看本文章

② 判断当前timer_list链表上的next_timer头指针是否为NULL,如果存在就将当前的定时器的值减1,如果定时器的定时值减为0,则会将执行当前定时器处理程序,并把处理程序指针置位NULL,然后去掉定时器。

③ 判断当前进程的时间片是否用完,没用完直接返回。如果时间片用完了,并且当前处于内核态直接返回,如果处于用户态则直接进行调度schedule()。

二、进程的控制

Linux是通过分时技术实现多个进程同时运行的系统,分时系统对于每个进程划分一个时间片,使每个进程在时间片内运行当时间片用完以后就会发生进程切换,由于每个时间片的时间很短,所以在宏观上看起来是所有进程都在同时运行。

在0.11内核中,系统进程是由一个指针数组struct task_struct * task[NR_TASKS]保存,其中NR_TASKS = 64代表系统最多允许存在的进程数目。从指针数组的定义中可以看出,数组内容是指向task_struct内容的指针。

任务数据结构

在Linux中每一项任务都是由一个结构体task_struct表示的,task_struct的定义在include/linux/sched.h中。

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需要知道的进程状态标志(段属性、位属性等)
};

在Linux中每一个进程相对于系统的关系如下图所示:

进程的运行状态

在Linux中进程有5种状态:

① 运行状态(TASK_RUNNING):当进程正在被执行,或者处于就绪队列中,此时进程的state被设置为TASK_RUNNING。

② 可中断睡眠状态(TASK_INTERRUPTIBLE:当系统产生了一个中断或者释放了当前进程所需要的的资源或者收到了信号,都可以唤醒处于可中断睡眠状态的进程。

③ 不可中断睡眠状态(TASK_UNINTERRUPTIBLE:处于该状态的进程,不会因为信号而被唤醒,只能通过wake_up()函数进行唤醒。

④ 僵尸状态(TASK_ZOMBIE):进程已经死亡,但是父进程没有回收资源。

⑤ 暂停状态(TASK_STOPPED:当进程收到SIGSTOP,SIGTSTP等信号就会处于该状态,并且只能通过信号SIGCONT唤醒。

三、进程初始化和创建

0号进程的创建(idle进程)

Linux内核在被引导成功后首先从/init/main.c文件开始执行,在main函数中完成了各模块的初始化:

//main函数 linux引导成功后就从这里开始运行
void main(void)        /* This really IS void, no error here. */
{           
    /********
    xxxxxx
    *******/
//内存控制器初始化
    mem_init(main_memory_start,memory_end);
    //异常函数初始化
    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()) {    //创建1号进程 fork函数就是用来创建进程的函数    /* we count on this going ok */
        //1号进程是所有进程的父进程
        init();
    }
/*
 *   NOTE!!   For any other task 'pause()' would mean we have to get a
 * signal to awaken, but task0 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();
}

其中包括进程的初始初始化sched_init();和1号进程init的创建。

其中sched_init()代码如下:

void sched_init(void)
{
    int i;
    struct desc_struct * p;

    if (sizeof(struct sigaction) != 16)
        panic("Struct sigaction MUST be 16 bytes");
    //gdt是全局描述符(系统级别)和前面所说的ldt(局部描述符)对应
    //内核的代码段
    //内核的数据段
    //进程0...n的数据
    set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
    set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
    p = gdt+2+FIRST_TSS_ENTRY;
    for(i=1;i<NR_TASKS;i++) {//0-64进程进行遍历
        task[i] = NULL;
        p->a=p->b=0;
        p++;
        p->a=p->b=0;
        p++;
    }//作用是清空task链表
/* Clear NT, so that we won't have troubles with that later on */
    __asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
    ltr(0);
    lldt(0);
    //以下都是设置一些小的寄存器组
    outb_p(0x36,0x43);        /* binary, mode 3, LSB/MSB, ch 0 */
    outb_p(LATCH & 0xff , 0x40);    /* LSB */
    outb(LATCH >> 8 , 0x40);    /* MSB */
    set_intr_gate(0x20,&timer_interrupt);
    outb(inb_p(0x21)&~0x01,0x21);
    //设置系统中断
    set_system_gate(0x80,&system_call);
}

在函数sched_init()关于进程主要是完成了:

①0号进程在全局描述符表GDT中的位置。

②清空task数组。

③复位NT表示,好像NT标志被置位的时候如果在中断执行iret指令会导致进程的切换。

④将进程0的TSS段选择符和局部描述符LDT选择符加载到对应的寄存器(只有进程0是根据此方式进行加载,以后的进程切换都是根据task_struct的tss段进行加载

⑤设置时钟中断,开启时钟中断,设置系统调用中断。

在执行完sched_init()以后,0号进程也被称为idle进程永远的运行在 for(;;) pause();中。

init函数

在main函数的末尾使用fork()函数创建了1号进程,并且调用Init()函数,init()函数具体实现如下:

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())) {//这里创建2号进程
        close(0);//关闭了1号进程的标准输入输出
        //关闭标准输入的原因是为了把/etc/rc文件的文件描述符定位到标准输入
        if (open("/etc/rc",O_RDONLY,0))//如果2号进程创建成功打开/etc/rc这里面保存的大部分是系统配置文件 开机的时候要什么提示信息全部写在这个里面
            _exit(1);
        execve("/bin/sh",argv_rc,envp_rc);//运行shell程序
        _exit(2);
    }
    if (pid>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));
        }
        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() */
}

在init()函数中完成了以下工作:

①通过系统调用setup()安装根文件系统。

②打开标准输入、输出和错误控制台,打印显示了系统信息。

③调用fork创建了一个新进程(进程2),在新进程中调用shell程序完成/etc/rc文件的配置。进程1在进程2调用shell程序的过程中会等待其结束,在等待结束后,进程1会进入一个无线的循环:在该循环中进程1会再次生成一个新进程,以登录shell的方式再次执行/bin/sh,以创建用户的环境,并且继续等待子进程的结束。此时用户就可以操作Liunx命令行环境了,直到用户在命令行输入exit或logout命令,进程1会打印shell退出的信息,并重复循环下去。

fork.c文件

fork()使用来创建子进程,它的本质是一种系统调用。在Linux系统中,所有的进程都是进程1的子进程。从kernal/system_call.s文件中可以看到fork系统调用具体的执行过程:

.align 2
_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

在汇编代码中主要发现有两个C函数,分别为①find_empty_process(寻找空位)和②copy_process(进程复制)

其中find_empty_process定义如下:

//大概意思就是一直循环重复找,直到找到一个空的位置
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])
            return i;
    return -EAGAIN;//达到64的最大值后,返回错误码
}

它的作用就是获得一个未使用过的进程号,然后在task指针数组中寻找一个空位存放新进程。

copy_process定义如下,具体的含义在代码以标注。

/*
 *  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.
 */
// 所谓进程创建就是对1号进程或者当前进程的复制
// 就是结构体的复制 把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++;
    //设置新进程的两个段
    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号
}

四、进程的调度

sched.c文件

在sched.c文件中存放着与调度相关的基本函数schedule,sleep_on,wake_up等

schedule()函数

void schedule(void)
{
    int i,next,c;
    struct task_struct ** p;

/* check alarm, wake up any interruptible tasks that have got a signal */

    for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
        if (*p) {//alarm是用来设置警告,比如jiffies有1000个可能其中一些需要警告那么就用alarm来实现
            if ((*p)->alarm && (*p)->alarm < jiffies) {
                    (*p)->signal |= (1<<(SIGALRM-1));
                    (*p)->alarm = 0;
                }
                //~(_BLOCKABLE & (*p)->blocked  
                //&&(*p)->state==TASK_INTERRUPTIBLE
                //用来排除非阻塞信号
                //如果该进程为可中断睡眠状态 则如果该进程有非屏蔽信号出现就将该进程的状态设置为running
            if (((*p)->signal & ~(_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)
                continue;//进程为空就继续循环
            if ((*p)->state == TASK_RUNNING && (*p)->counter > c)//找出c最大的task
                c = (*p)->counter, next = i;
        }
        if (c) break;//如果c找到了,就终结循环,说明找到了
        //进行时间片的重新分配
        for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
            if (*p)//这里很关键,在低版本内核中,是进行优先级时间片轮转分配,这里搞清楚了优先级和时间片的关系
            //counter = counter/2 + priority
                (*p)->counter = ((*p)->counter >> 1) +
                        (*p)->priority;
    }
    //切换到下一个进程 这个功能使用宏定义完成的
    switch_to(next);
}

在第一个for循环中程序从task数组最后一个任务开始循环检索,检测alarm信号(进程的报警值)和唤醒任何已得到信号的可中断任务。

从while(1)程序开始就是0.11内核中采用的优先级时间片轮转调度算法。调度算法具体代码流程如下:

① 循环task列表判断进程是否处于就绪态,并找出时间片counter值最大的进程。

② 如果找到了处于就绪态的时间片最大的进程c,则跳出循环执行进程切换switch_to(next);

③ 如果所有进程时间片都用完,或者所有的进程处于阻塞态,则进行新一轮的时间片分配。//时间片counter = counter/2 + priority

sleep_on()函数

// 当某个进程想访问CPU资源,但是CPU资源被占用访问不到,就会休眠
void sleep_on(struct task_struct **p)
{
    struct task_struct *tmp;

    if (!p)//如果传进来的是空的 就返回
        return;
    if (current == &(init_task.task))//当前进程是0号 
        panic("task[0] trying to sleep");//就打印并且返回
    tmp = *p;
    *p = current;//这两步相当于 给休眠链表添加了一个新node
    // 其实核心就是把state置为TASK_UNINTERRUPTIBLE
    current->state = TASK_UNINTERRUPTIBLE;
    schedule();
    if (tmp)
        tmp->state=0;
}

sleep_on函数的本质就是将当前进程的task->state设置为TASK_UNINTERRUPTIBLE。

wake_up()函数

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

wake_up函数的本质就是将进程的state设置为0,TASK_RUNNING状态

五、进程的销毁

进程的销毁流程:

① 进程通过调用exit()函数实现进程的销毁,exit的本质是一个是系统调用sys_exit执行的一个do_exit函数。

② do_exit函数首先会释放代码段/数据和堆栈段占用的内存。

③ 如果当前要销毁的进程有子进程,就让1号进程(init)进行接收,要接受的子进程处于僵尸状态,则发送SIGCHLD信号给1号进程。

④ 关闭当前进程打开的所有文件,并对当前目录和i节点进行同步。

⑤ 如果当前进程是会话头进程则关闭会话内所有进程。如果当前进程是会话头进程并拥有控制终端,则释放终端。

⑥ 改变当前进程的状态位TASK_ZOMBIE,向父进程发送SIGCHLD信号。

exit.c文件

do_exit()函数

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 */
}
// 定义系统调用
int sys_exit(int error_code)
{
    return do_exit((error_code&0xff)<<8);
}

release()函数

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");
}

完成清空进程指针数组表中对应的表项,释放对应的内存。

send_sig()函数

static inline int send_sig(long sig,struct task_struct * p,int priv)
{
    //若信号不在范围内,则直接退出
    if (!p || sig<1 || sig>32)
        return -EINVAL;
    //若果强制标志位priv为1或当前进程的用户标识符就是指定进程的用户标识符或当前进程是root用户,则发送信号
    if (priv || (current->euid==p->euid) || suser())
        p->signal |= (1<<(sig-1));
    else
        return -EPERM;
    return 0;
}

给指定的进程p发送信号

kill_session()函数

static void kill_session(void)
{
    struct task_struct **p = NR_TASKS + task;
    
    while (--p > &FIRST_TASK) {//从最后一个开始扫描(不包括0进程)
        if (*p && (*p)->session == current->session)
            (*p)->signal |= 1<<(SIGHUP-1);
    }
}

终止所有的会话组

sys_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) //如果等于进程组号
            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 给进程组发送信息
        if (*p && (*p)->pgrp == -pid)
            if (err = send_sig(sig,*p,0))
                retval = err;
    return retval;
}

sys_kill()函数是一个系统调用函数,根据参数pid的值不同执行对应的调用:

① pid == 0,给当前进程所在进程组的所有进程发送信号sig

② pid > 0,给进程号为pid的进程发送sig信号。

③ pid == -1,给1号(init进程)外所有进程发送信号sig。

④ pid < -1,给所有进程组为-pid的所有进程发送信号sig。

sys_waitpid()函数

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:
                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;
                continue;
        }
    }
    if (flag) {
        if (options & WNOHANG)
            return 0;
        current->state=TASK_INTERRUPTIBLE;
        schedule();
        if (!(current->signal &= ~(1<<(SIGCHLD-1))))
            goto repeat;
        else
            return -EINTR;
    }
    return -ECHILD;
}

系统调用函数waitpid()。挂起当前进程,直到pid指定的子进程退出(终止)或者收到要求终止该进程的信号,或者是需要调用一个信号句柄(信号处理程序)。如果pid所指的子进程早已退出(已成所谓的僵死进程),则本调用将立刻返回。子进程使用的所有资源将释放。

①如果pid > 0,表示等待进程号等于pid的子进程。

②如果pid = 0,表示等待进程组号等于当前进程组号的任何子进程。

③如果pid < -1,表示等待进程组号等于pid绝对值的任何子进程。

④如果pid = -1,表示等待任何子进程。

如果处于TASK_ZOMBIE的子进程,父进程会将它用户态和内核态运行的时间累积到父进程之中,释放子进程并返回子进程的退出码和pid.

猜你喜欢

转载自blog.csdn.net/qq_42174306/article/details/128660596