• 《Unix/Linux系统编程》第三章学习笔记


    第3章 Unix/Linux进程管理

    本章讨论了Unix/Linux中的进程管理,阐述了多任务处理原则,介绍了进程概念,说明了多任务处理、上下文切换和进程处理的各种原则和方法。

    3.1 多任务处理

    多任务处理指的是同时执行几个独立的任务。
    在单处理器(单CPU)系统中,一次只能执行一个任务。
    多任务处理是通过在不同任务之间多路复用CPU的执行时间来实现的,即将CPU执行操作从一个任务切换到另一个任务。
    不同任务之间的执行切换机制称为上下文切换,将一个任务的执行环境更改为另一个任务的执行环境。
    如果切换速度足够快,就会给人一种同时执行所有任务的错觉。这种逻辑并行性成为“并发”。
    在有多个CPU或处理器也可以通过同时执行不同的任务来实现多任务处理。多任务处理是所有操作系统的基础。
    总体来说,它是并行编程的基础。


    3.2 进程的概念

    操作系统是一个多任务处理系统。在操作系统中,任务也称为进程。
    在实际应用中,任务和进程这两个术语可以互换使用。在第2章中,我们把执行映像定义为包含执行代码、
    数据和堆栈的存储区。
    进程的正式定义:
    进程是对映像的执行。
    操作系统内核将一系列执行视为使用系统资源的单一实体。系统资源包括内存空间、I/O设备以及最重要的CPU时间。
    在操作系统内核中,每个进程用一个独特的数据结构表示,叫作进程控制块(PCB)或任务控制块(线程控制块)(TCB)等。
    在本书中,我们直接称它为PROC结构体。与包含某个人所有信息的个人纪录一样,PROC结构体包含某个进程的所有信息。在实际操作系统中,PROC结构体可能包含许多字段,而且数量可能很庞大。首先,我们来定义一个非常简单的PROC结构体来表示进程。

    typedef struct proc{
        struct proc *next;
        int *ksp;
        int pid;
        int ppid;
        int status;
        int priority;
        int kstack[1024];
    } PROC;
    

    在PROC结构体中,next是指向下一个PROC结构体的指针。ksp字段是保存的堆栈指针。
    当某进程放弃使用CPU时,它会将执行上下文保存在堆栈中,并将堆栈指针保存在PROC.ksp中,以便以后恢复。在PROC结构体的其他字段中,pid是标识一个进程的进程ID编号,ppid是父进程ID编号,status是进程的当前状态,priority是进程调度优先级,kstack是进程执行时的堆栈。
    操作系统内核通常会在其数据区中定义有限数量的PROC结构体,表示为:

    PROC proc[NPROC];       // NPROC a constant, e.g. 64
    

    用来表示系统中的进程。
    在一个单CPU系统中,一次只能执行一个进程。操作系统内核通常会使用正在运行的或当前的全局变量PROC指针,指向当前正在执行的PROC。在有多个CPU的多处理操作系统中,可在不同CPU上实时、并行执行多个进程。
    因此,在一个多处理器系统中正在运行的[NCPU]可能是一个指针数组,每个指针指向一个正在特定CPU上运行的进程。
    为简便起见,我们只考虑单CPU系统。


    3.3 多任务处理系统

    多任务处理系统,简称MT。


    3.4 进程同步

    一个操作系统包含许多并发进程,这些进程可以彼此交互。进程同步是指控制和协调进程交互以确保其正确执行所需的各项规则和机制。
    最简单的进程同步工具是休眠和唤醒操作。

    3.4.1 睡眠模式

    当某进程需要某些当前没有的东西时,例如申请独占一个存储区域、等待用户通过标准输入来输入字符等,它就会在某个事件值上进入休眠状态,该事件值表示休眠的原因。
    为实现休眠操作,我们可在PROC结构体中添加一个event字段,并实现ksleep(int event)函数,使得进程进入休眠状态。

    typedef struct proc{
        struct proc *next;
        int *ksp;
        int pid;
        int ppid;
        int status;
        int priority;
        int event;
        int exitCode;
        struct proc *child;
        struct proc *sibling;
        struct proc *parent;
        int kstack[1024];
    } PROC;
    

    ksleep()算法为:

    /****************** Algorithm of ksleep(int event) **********************/
    1. record event value in PROC.event:    running->event = event;
    2. change status to SLEEP;              running ->status = SLEEP;
    3. for ease of maintencance, enter caller into a PROC *sleepList
                                        enqueue(&sleepList, running);
    4.give up CPU;                          tswitch();
    

    由于休眠进程不在readyQueue中,所以它在被另一个进程唤醒之前不可运行。因此,在让自己进入休眠状态之后,进程调用tswitch()来放弃使用CPU。

    3.4.2 唤醒操作

    多个进程可能会进入休眠状态等待同一个事件,这是很自然的,因为这些进程可能都需要同一个资源,例如一台当前正处于繁忙状态的打印机。在这种情况下,所有这些进程都将休眠等待同一个事件值。当某个等待时间发生时,另一个执行实体(可能是某个进程或中断处理程序)将会调用kwakeup(event),唤醒正处于休眠状态等待该事件值的所有程序。如果没有任何程序休眠等待该程序,kwakeup()就不工作,即不执行任何操作。
    kwakeup()算法是:

    /****************** Algorithm of kwakeup(int event) **********************/
    // Assume SLEEPing procs are in a global sleepList
    for each PROC *p in a global sleepList
    for each PROC *p in sleepList do{
        if (p->event == event){         // if p is sleeping for the event
            delete p from sleepList;
            p->status = READY;          // make p READY to run again
            enqueue(&readyQueue, p);    // enter p into readyQueue
        }
    }
    

    注意,被唤醒的进程可能不会立即运行。它只是被放入readyQueue中,排队等待运行。
    当被唤醒的进程运行时,如果它在休眠之前正在试图获取资源,那么它必须尝试重新获取资源。这是因为该资源在它运行时可能不再可用。ksleep()和kwakeup()函数一般用于进程同步,但在特殊情况下也用于同步父进程和子进程。


    3.5 进程终止

    在操作系统中,进程可能终止或死亡,这是进程终止的通俗说法。如第2章所述,进程能以两种方式终止:

    • 正常终止:进程调用exit(value),发出_exit(value)系统调用来执行在操作系统内核中的kexit(value),这就是我们本节要讨论的情况。
    • 异常终止:进程因某个信号而异常终止。信号和信号处理将在第六章讨论。

    在这两种情况下,当进程终止时,最终都会在操作系统内核中调用kexit()。

    3.5.1 kexit()的算法
    /****************** Algorithm of kexit(int exitValue) **********************/
    1. Erase process user-mode context, e.g. close file descriptors,
       release resources, deallocate user-mode image memory, etc.
    2. Dispose of children processes, if any
    3. Record exitValue in PROC.exitCode for parent to get
    4. Become a ZOMBIE (but do not free the PROC)
    5. Wakeup parent and, if needed, also the INIT process P1
    
    3.5.2 进程家族树

    通常进程家族树通过一个PROC结构中的一对子进程和兄弟进程指针以二叉树的形式实现,如:

    PROC *child, *sibling, *parent
    

    其中,child指向进程的第一个子进程,sibling指向同一个父进程的其他子进程。为方便起见,每个PROC还使用一个parent指针指向其父进程。

    使用进程树,更容易找到进程的子进程。首先,跟随child指针到第一个子进程。然后,跟随sibling指针遍历兄弟进程。要想把所有子进程都送到P1中,只需要把子进程链表分出来,然后把它附加到P1的子进程链表中(还要修改它们的ppid和parent指针)。

    每个PROC都有一个退出代码(exitCode)字段,是进程终止时的进程退出值(exitValue)。在PROC.exitCode中记录exitValue之后,进程状态更改为ZOMBIE,但不释放PROC结构体。然后,进程调用kwakeup(event)来唤醒其父进程,其中事件必须是父进程和子进程使用的相同唯一值,例如父进程的PROC结构体地址或父进程的pid。如果它将任何孤儿进程送到P1中,也会唤醒P1。濒死进程的最后操作是进程最后一次调用tswitch()。在这之后,进程基本上死亡了,但还有一个空壳,以僵尸进程的形式存在,它通过等待操作被父进程埋葬(释放)。

    3.5.3 等待子进程终止

    在任何时候,进程都可以调用内核函数

    pid = kwait(int *status);
    

    等待僵尸子进程。如果成功,则返回的pid是僵尸子进程的pid,而status包含僵尸子进程的退出代码。此外,kwait()还会将僵尸子进程释放回freeList以便重用。
    kwait的算法是:

    /**************** Algorithm of kwait() *****************/
    int kwait(int *status)
    {
        if (caller has no child) return -1 for error;
        while(1) {              // caller has children
            search for a (any) ZOMBIE child;
            if (found a ZOMBIE child) {
                get ZOMBIE child pid;
                copy ZOMBIE child exitCode to *status;
                bury the ZOMBIE child (put its PROC back to freeList);
                return ZOMBIE child pid;
            }
            // ***** has children but none dead yet *****
            ksleep(running);    // sleep on its PROC address
        }
    }
    

    在kwait算法中,如果没有子进程,则会返回-1,表示错误。否则,它将搜索僵尸子进程。如果它找到僵尸子进程,就会收集僵尸子进程的pid和退出代码,将僵尸进程释放到freeList并返回僵尸子进程的pid。否则,它将在自己的PROC地址上休眠,等待子进程终止。由于每个PROC地址都是一个唯一值,所有子进程也都知道这个值,所以等待的父进程可以在自己的PROC地址上休眠,等待子进程稍后唤醒它。
    相应地,当进程终止时,它必须发出:

    kwakeup(running->parent);
    

    以唤醒父进程。若不用父进程地址,读者也可使用父进程pid进行验证。在kwait()算法中,进程唤醒后,当它再次执行while循环时,将会找到死亡的子进程。注意,每个kwait调用只处理一个僵尸子进程(如有)。如果某个进程有多个子进程,那么它可能需要多次调用kwait()来处理所有死亡的子进程。或者,某进程可以先终止,而不需要等待任何死亡子进程。当某进程死亡时,它所有的子进程都成了P1的子进程。在真实系统中,P1在无限循环中执行,多次等待死亡的子进程,包括接收的孤儿进程。因此,在类Unix系统中,INIT进程P1扮演着许多角色。

    • 它是除了P0之外所有进程的祖先。具体来说,它是所有用户进程的始祖,因为所有登录进程都是P1的子进程。
    • 它就像孤儿院的院长,所有孤儿都会送到它这里,并叫它爸爸。
    • 它又像是太平间管理员,因为它要不不停地寻找僵尸进程,以埋葬它们死亡的空壳。
      所以,在类Unix系统中,如果INIT进程P1死亡或被卡住,系统将停止工作,因为用户无法再次登录,系统内很快就会堆满腐烂的尸体。
  • 相关阅读:
    使用keepalived监控tomcat 达到双机热备
    nginx tomcat负载均衡 使用redis session共享
    Java线程安全和非线程安全
    Log4J日志配置详解
    使用java mail的网易smtp协议 发送邮件
    JavaScript-DOM(3)
    JavaScript-DOM(2)
    JavaScript-DOM(1)
    BOM简介
    JavaScript(数组、Date、正则)
  • 原文地址:https://www.cnblogs.com/qwer6653/p/16773560.html
Copyright © 2020-2023  润新知