zoukankan      html  css  js  c++  java
  • 学习笔记6(必做)

    第3章 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死亡或被卡住,系统将停止工作,因为用户无法再次登录,系统内很快就会堆满腐烂的尸体。

    3.7 Unix/Linux中的进程

    3.7.1 进程来源

    当操作系统启动时,操作系统内核的启动代码会强行创建一个PID=0的初始进程,即通过分配PROC结构体(通常是proc[0])进行创建,初始化PROC内容,并让运行指向proc[0]。然后,系统执行初始进程P0。大多数操作系统都以这种方式开始运行第一个进程。P0继续初始化系统,包括系统硬件和内核数据结构。然后,它挂载一个根文件系统,使得系统可以使用文件。在未初始化系统之后,P0复刻出一个子进程P1,并把进程切换为以用户模式运行P1。

    3.7.2 INIT和守护进程

    当进程P1开始运行时,它将其执行映像更改为INIT程序。因此,P1通常被称为INIT进程,因为它的执行映像是init程序。P1开始复刻出许多子进程。P1的大部分子进程都是用来提供系统服务的。它们在后台运行,不与任何用户交互。这样的进程称为守护进程

    syslogd: log daemon process
    inetd:  Internet service daemon process
    httpd:  HTTP server daemon process
    etc.
    

    3.7.3 登录进程

    除了守护进程之外,P1还复刻了许多LOGIN进程,每个终端上一个,用于用户登录。
    每个LOGIN进程打开三个与自己的终端相关联的文件流。这三个文件流是用于标准输入的stdin、用于标准输出的stdout和用于标准错误消息的stderr。每个文件流都是指向进程堆区中FILE结构体的指针。每个FILE结构体记录一个文件描述符(数字),stdin的文件描述符是0,stdout是1,stderr是2。然后,每个LOGIN进程向stdout显示一个

    login:
    

    以等待用户登录。用户账户保存在/etc/passwd/etc/shadow文件中。每个用户账户在表单的/etc/passwd文件中都有一行对应的记录,

    name:x:gid:uid:desciption:home:program
    

    其中,name为用户登录名,x表示登陆时检查密码,gid为用户组ID,uid为用户ID,home为用户主目录,program为用户登录后执行的初始程序。其他用户信息保存在/etc/shadow文件中。shadow文件的每一行都包含加密的用户密码,后面是可选的过期限制信息,如过期日期和时间等。当用户尝试使用登录名和密码登录时,Linux将检查/etc/shadow文件,以验证用户的身份。

    3.7.4 sh进程

    当用户成功登录时,LOGIN进程会获取用户的gid和uid,从而成为用户的进程。它将目录更改为用户的主目录并执行列出的程序,通常是命令解释程序sh。现在,用户进程执行sh,因此用户进程通常称为sh进程。它提示用户执行命令。一些特殊命令,如cd(更改目录)、退出、注销等,由sh自己直接执行。其他大多数命令是各种bin目录(如/bin、/sbin、/usr/bin、/usr/local/bin等)中的可执行文件。对于每个(可执行文件)命令,sh会复刻一个子进程,并等待子进程终止。子进程将其执行映像更改为命令文件并执行命令程序。子进程在终止时会唤醒父进程sh,父进程会收集子进程终止状态、释放子进程PROC结构体并提示执行另一个命令等。除简单的命令之外,sh还支持I/O重定向和通过管道连接的多个命令。

    3.7.5 进程的执行模式

    在Unix/Linux中,进程以两种不同的模式执行,即内核模式用户模式,简称Kmode和Umode。在每种执行模式下,一个进程有一个执行映像,如图:

    图3.4

    在图3.4中,索引i表示这些是进程i的映像。通常,进程在Umode下的映像都不相同。但是,在Kmode下,它们的Kcode、Kdata和Kheap都相同,这些都是操作系统内核的内容,但是每个进程都有自己的Kstack。

    在进程的生命周期中,会在Kmode和Umode之间发生多次迁移。每个进程都在Kmode下产生并开始执行。事实上,它在Kmode下执行所有相关操作,包括终止。在Kmode模式下,通过将CPU的状态寄存器从K模式更改为U模式,可轻松切换到Umode。但是,一旦进入Umode,就不能随意更改CPU的状态了,原因很明显。Umode进程只能通过以下三种方式进入Kmode:
    (1)中断:中断是外部设备发送给CPU的信号,请求CPU的服务。当在Umode下执行时,CPU中断是启用的,因此它将响应任何中断。在中断发生时,CPU将进入Kmode来处理中断,这将导致进程进入Kmode。
    (2)陷阱:陷阱是错误条件,例如无效地址、非法指令、除以0等,这些错误条件被CPU识别为异常,使得CPU进入Kmode来处理错误。在Unix/Linux中,内核陷阱处理程序将陷阱原因转换为信号编号,并将信号传递给进程。对于大多数信号,进程的默认操作是终止。
    (3)系统调用:系统调用(简称syscall)是一种允许Umode进程进入Kmode以执行内核函数的机制。当某进程执行完内核函数后,它将期望结果和一个返回值返回到Umode,该值通常为0(表示成功)或-1(表示错误)。如果发生错误,外部全局变量errno(在errno.h中)会包含一个ERROR代码,用于标识错误。用户可使用库函数

    perror("error message");
    

    来打印某个错误消息,该消息后面跟着一个描述错误的字符串。
    每当一个进程进入Kmode时,它可能不会立即返回到Umode。在某些情况下,它可能根本不会返回到Umode。例如,_exit() syscall 和大多数陷阱会导致进程在内核中终止,这样它永远都不会返回到Umode。当某进程即将退出Kmode时,操作系统内核可能会切换进程以运行另一个具有更高优先级的进程。

    3.8 进程管理的系统调用

    在本节中,我们将讨论Linux中与进程管理相关的以下系统调用。

    fork()、wait()、exec()、exit()
    

    每个都是发出实际系统调用的库函数:

    int syscall(int a, int b, int c, int d);
    

    其中,第一个参数a表示系统调用号,b、c、d表示对应核函数的参数。在基于Intel x86的Linux中,系统调用是由汇编指令INT 0x80实现的,使得CPU进入Linux内核来执行由系统调用号a标识的核函数。

    3.8.1 fork()

    Usage: int pid = fork();
    

    fork()创建子进程并返回子进程的pid,如果fork()失败则返回-1。

    ———————————————————————————————————————————————————————————————— 转载麻烦附上本文链接和本声明,感谢! 博主<叶家星>博客园链接如下:https://www.cnblogs.com/yejiaxing-01/
  • 相关阅读:
    LINQ操作符一:Select
    DataGridView使用技巧十二:DataGridView Error图标表示的设定
    DataGridView使用技巧十一:DataGridView用户输入时,单元格输入值的设定
    DataGridView使用技巧十:单元格表示值的自定义
    DataGridView使用技巧九:DataGridView的右键菜单(ContextMenuStrip)
    清除DataGridView显示的数据
    SQL Server查询某个字段存在哪些表中
    DataGridView使用技巧八:设置单元格的ToolTip
    DataGridView使用技巧七:列顺序的调整、操作行头列头的标题
    csvkit---python一个牛逼到不行的csv处理库
  • 原文地址:https://www.cnblogs.com/yejiaxing-01/p/15422237.html
Copyright © 2011-2022 走看看