Linux内核-进程销毁

进程销毁

内核销毁流程

  1. 当用户程序调用exit()函数的时候,触发了系统调用,最后调用到kernel/exit.c中的sys_exit()函数,最后底层调用int do_exit(long code)该函数会释放进程的代码段和数据段占用的内存
  2. 关闭当前进程打开的所有文件,对当前的目录和inode节点进行同步
  3. 如果当前要销毁的进程有子进程,那么就让1号进程(init进程)作为新的父进程,子进程变成孤儿进程
  4. 如果当前进程是一个会话(一种进程间通信方式)头进程,则会终止会话中的所有进程
  5. 改变当前进程的运行状态,变成TASK_ZOMBIE僵死状态,并且向其父进程发送SIGCHLD信号

截止到上面都还没有销毁为该进程分配的task_struct结构体,这是因为这需要在该进程的父进程完成

  1. 父进程收到SIGCHLD信号时会终止处于僵死状态的子进程
  2. 父进程把子进程的运行时间累加到自己的进程变量中
  3. 把对应的子进程的进程描述结构体task_struct进行释放,置空任务数组对应项

具体源码

release

//// 释放指定进程(任务)。
void release(struct task_struct * p)
{
    int i;

    if (!p)
        return;
    for (i=1 ; i<NR_TASKS ; i++)
        if (task[i]==p) {
            task[i]=NULL;
            free_page((long)p);
            schedule();
            return;
        }
    panic("trying to release non-existent task");
}

这个函数释放指定进程,清空了任务描述表中的对应进程表项,并且释放对应的内存页(代码段,数据段,堆栈)

send_sig

// 向指定任务(*p)发送信号(sig),权限为 priv
static inline int send_sig(long sig,struct task_struct * p,int priv)
{
    if (!p || sig<1 || sig>32)
        return -EINVAL;
    if (priv || (current->euid==p->euid) || suser())
        p->signal |= (1<<(sig-1));
    else
        return -EPERM;
    return 0;
}

这个函数是用于向制定的进程发送信号

首先信号必须在合法范围之内,其次想要发送信号给该进程,需要满足三个条件中的任意一个:

  1. 有对应的权限
  2. 当前进程的所有者和待接收信号进程的所有者是同一个人
  3. 是超级用户

kill_session

这个函数是杀死所有与该进程相同对话的进程,从进程描述表的最后一个向前,并且不包括0号进程

// 终止会话
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

kill()函数的系统调用

/*
 * XXX need to check permissions needed to send signals to process
 * groups, etc. etc.  kill() permissions semantics are tricky!
 */
 //// kill()系统调用可用于向任何进程组或进程发送任何信号。 
 // 如果 pid 值>0,则信号被发送给 pid。 
 // 如果 pid=0,那么信号就会被发送给当前进程的进程组中的所有进程。 
 // 如果 pid=-1,则信号 sig 就会发送给除第一个进程外的所有进程。 
 // 如果 pid < -1,则信号 sig 将发送给进程组-pid 的所有进程。 
 // 如果信号 sig 为 0,则不发送信号,但仍会进行错误检查。如果成功则返回 0。
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) {
        if (*p && (*p)->pid == pid) 
            if (err=send_sig(sig,*p,0))
                retval = err;
    } else if (pid == -1) while (--p > &FIRST_TASK)
        if (err = send_sig(sig,*p,0))
            retval = err;
    else while (--p > &FIRST_TASK)
        if (*p && (*p)->pgrp == -pid)
            if (err = send_sig(sig,*p,0))
                retval = err;
    return retval;
}

tell_father

该函数用于通知父进程,给父进程发送SIGCHLD信号,注意这里的参数pid是父进程的pid而不是子进程的

// 通知父进程, 向父进程发送信号SIGCHILD
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));
            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);
}

do_exit

这个是exit()函数的最终系统调用,也是进程销毁的大头

释放内存

    // 释放当前进程代码段和数据段所占的内存页
    free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
    free_page_tables(get_base(current->ldt[2]),get_limit(0x17));

注意这里和上面release函数中的free_page((long)p);不同,前者释放的是进程的“家产”(用户空间的内存),后者释放的是进程的“户口本”(内核管理结构)。

  • free_page_tables(释放“家产”)
    这两行代码释放的是当前进程在用户态使用的所有物理内存页,包括:
  • current->ldt[1]代码段(CS 段)的基址。
  • current->ldt[2]数据段(DS/SS 段)的基址。
    这些空间存放着程序的可执行代码、全局变量、堆(Heap)和用户态栈(Stack)。free_page_tables 会遍历这两个段映射到的页目录和页表,将其中映射的物理页全部归还给物理内存管理器(mem_map),并清空页表项。
  • free_page((long)p)(释放“户口本”)
    这个函数释放的是进程控制块(task_struct)和内核栈所在的那一整块物理内存页(4KB)
    还记得 sched.c 里的 union task_union 吗?每个进程都独占一个 4KB 的物理页,低地址放 task_struct,高地址放内核栈。release 函数里的 free_page 释放的就是这一页内存。

将子进程变为孤儿进程

    for (i=0 ; i<NR_TASKS ; i++)
        if (task[i] && task[i]->father == current->pid) {
            task[i]->father = 1;
            if (task[i]->state == TASK_ZOMBIE)
                /* assumption task[1] is always init */
                (void) send_sig(SIGCHLD, task[1], 1);
        }

这里依然使用的task_struct的链表来处理,遍历这个数组,如果某个进程的父进程刚好等于当前退出的进程,那么就把1号进程(init进程)当作那个进程的父进程,这时候子进程就变成了孤儿进程

并且如果子进程中有僵死进程,那么就需要立即让init进程收尸

关闭文件

    // 关闭当前进程打开着的所有文件    
    for (i=0 ; i<NR_OPEN ; i++)
        if (current->filp[i])
            sys_close(i);

#define NR_OPEN 20可以看到最多打开20个文件,并且进程退出时会把每一个打开的文件关闭,调用的sys_close系统调用

    // 同步当前进程工作目录 pwd、根目录 root 以及运行程序的 i 节点并分别置空        
    iput(current->pwd);
    current->pwd=NULL;
    iput(current->root);
    current->root=NULL;
    iput(current->executable);
    current->executable=NULL;

这里的iput是释放一个 i 节点(回写入设备)。

关闭终端和协处理器

如果该进程使用了终端和协处理器,那么就要关闭

    if (current->leader && current->tty >= 0)
        tty_table[current->tty].pgrp = 0;
    if (last_task_used_math == current)
        last_task_used_math = NULL;

关闭会话

这里调用kill_session去关闭会话中的所有进程

    if (current->leader)
        kill_session();

置为僵死状态并告诉父进程

    current->state = TASK_ZOMBIE;
    current->exit_code = code;
    tell_father(current->father);
    schedule();
    return (-1);    /* just to suppress warnings */

在最后会重新进行进程调度

sys_waitpid

sys_waitpid是waitpid的系统调用1,他会挂起当前进程,直到 pid 指定的子进程退出(终止)或者收到要求终止

 // 该进程的信号,或者是需要调用一个信号句柄(信号处理程序)。如果 pid 所指的子进程早已 
 // 退出(已成所谓的僵死进程),则本调用将立刻返回。子进程使用的所有资源将释放。 
 // 如果 pid > 0, 表示等待进程号等于 pid 的子进程。 
 // 如果 pid = 0, 表示等待进程组号等于当前进程的任何子进程。 
 // 如果 pid < -1, 表示等待进程组号等于 pid 绝对值的任何子进程。 
 // [ 如果 pid = -1, 表示等待任何子进程。] 
 // 若 options = WUNTRACED,表示如果子进程是停止的,也马上返回。 
 // 若 options = WNOHANG,表示如果没有子进程退出或终止就马上返回。 
 // 如果 stat_addr 不为空,则就将状态信息保存到那里。
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;
}

整段代码的大致逻辑就是首先去遍历task_struct的链表,如果有符合对应pid的子进程,并且子进程处于退出或僵死状态,那么就根据options是否立即返回。

如果没有子进程处于退出或僵死状态那么就把当前进程设置为可中断睡眠状态,然后进行调度,直到它被信号唤醒,如果是SIGCHLD信号就退出,不是就重复流程

上一篇
下一篇