首页 > 技术文章 > 2021-2022-1-diocs-Unix/Linux系统编程第六周学习笔记

20191218tangqiheng 2021-10-24 15:47 原文

2021-2022-1-diocs-Unix/Linux进程管理(第六周学习笔记)

思维导图


本章讨论了Unix/inux 中的进程管理;阐述了多任务处理原则;介绍了进程概念;并以一个编程示例来说明多任务处理、上下文切换和进程处理的各种原则和方法。 多任务处理系统支持动态进程创建、进程终止,以及通过休眠与唤醒实现进程同步、进程关系,以及以二叉树的形式实现进程家族树,从而允许父进程等待子进程终止;提供了一个具体示例来阐释进程管理函数在操作系统内核中是如何工作的;然后,解释了Unix/Linux中各进程的来源,包括系统启动期间的初始进程、INIT进程、守护进程、登录进程以及可供用户执行命令的sh进程;接着,对进程的执行模式进行了讲解,以及如何通过中断、异常和系统调用从用户模式转换到内核模式;再接着,描述了用于进程管理的 Unix/Linux 系统调用,包括fork、wait、exec 和 exit;阐明了父进程与子进程之间的关系,包括进程终止和父进程等待操作之间关系的详细描述;解释了如何通过INIT进程处理孤儿进程,包括当前Linux 中的subreaper 进程,并通过示例演示了subreaper进程;接着,详细介绍了如何通过 exec 更改进程执行映像,包括execve 系统调用、命令行参数和环境变量;解释了I/O重定向和管道的原则及方法,并通过示例展示了管道编程的方法∶读者可借助本章的编程项目整合进程管理的各种概念和方法,实现用于执行命令的 sh 模拟器。sh模拟器的功能与标准sh完全相同。它支持简单命令、具有1/O重定向的命令和通过管道连接的多个命令的执行。

基本概念

1. 多任务处理
一般来说,多任务处理指的是同时进行几项独立活动的能力。比如,我们经常看到有人一边开车一边打电话。从某种意义上说,这些人正在进行多任务处理,尽管这样非常不好。在计算机技术中.多任务处理指的是同时执行几个独立的任务。在单处理器(单CPU)系统中,一次只能执行一个任务。多任务处理是通过在不同任务之间多路复用CPU的执行时间来实现的,即将CPU执行操作从一个任务切换到另一个任务。不同任务之间的执行切换机制称为上下文切换,将—个任务的执行环境更改为另个任务的执行环境。如果切换速度足够快,就会给人一种同时执行所有任务的错觉。这种逻辑并行性称为"并发"。在有多个CPU或处理器内核的多处理器系统中。可在不同CPU上实时、并行执行多项任务。此外,每个处理器也可以通过同时执行不同的任务来实现多任务处理。多任务处理是所有操作系统的基础。总体上说,它也是并行编程的基础。
2. 进程的概念
操作系统是一个多任务处理系统。在操作系统中,任务也称为进程。在实际应用中,任务和进程这两个术语可以互换使用。在第2章中,我们把执行映像定义为包含执行代码、数据和堆栈的存储区。进程的正式定义∶进程是对映像的执行
操作系统内核将一系列执行视为使用系统资源的单一实体。系统资源包括内存空间、I/O设备以及最重要的 CPU时间。在操作系统内核中,每个进程用一个独特的数据结构表示,叫作进程控制块(PCB)或任务控制块(TCB)等。在本书中,我们直接称它为PROC 结构体。与包含某个人所有信息的个人记录一样,PROC结构体包含某个进程的所有信息。在实际操作系统中,PROC结构体可能包含许多字段,而且数量可能很庞大。首先,我们来定义一个非常简单的PROC结构体来表示进程。

typedef etruct proc(
  struct proc *next;       // next proc pointer 
  int *ksp;                // saved sp: at byte offset 4
  int pid;                 // process ID
  int ppid;                // parent proces pid 
  int statuS;              // PROC status=FREE|READY, etc.
  int priority;            // scheduling priority 
  int kstack[1024];        // process execution stack
)PROC;

在PROC结构体中,next是指向下一个PROC结构体的指针,用于在各种动态数据结构(如链表和队列)中维护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上运行的进程。

多任务处理系统

多任务处理(MT)系统,说明多任务处理、上下文切换和进程处理原则。下面的程序实现了一个模拟操作系统内核模式各项操作的多任务环境,由以下几个部分组成。
1. type.h 文件
type.h 文件定义了系统常数和表示进程的简单 PROC结构体。

/*********** type.h file ************/
#define NPROC    9               // number of PROCs
#define SSIZE 1024               // gtack size = 4KB

// PROC status 
#define FREE     0
#define READY    1
#define SLEEP    2
#define ZOMBIE   3

typedef struct proc{
  struct proc *next;             // next proc pointer
  int *ksp;                      // saved stack pointer
  int pid;                       // pid = 0 to NPROC-1
  int ppid;                      // parent pid
  int status;                    // PROC status
  int priority;                  // scheduling priority
  int kstack[SSIE]               // process stack
}PROC;

2. ts.s文件
ts.s是汇编代码,在32位GCC汇编代码中可实现进程上下文切换。

#------------- ts.s file file -----------------
       .globl running, scheduler, tswitch
tswitch:
SAVE:  pushl %eax
       pushl %ebx
       pushl %ecx
       pushl %edx
       pushl %ebp
       pushl %esi
       pushl %edi
       pushfl
       movl running, %ebx  # ebx -> PROC
       movl %esp, 4(%ebx)   # PORC.save_sp = esp
FIND:  call scheduler
RESUME:movl running, %ebx  # ebx -> PROC
       movl 4(%ebx), %esp  #esp = PROC.saved_sp
       popf1
       popl %edi
       popl %esi
       popl %ebp
       popl %edx
       popl %ecx
       popl %ebx
       popl %eax
       ret
# stack contents = |retpc|eax|ebx|ecx|edx|ebp|esi|edi|eflag|
#                    -1   -2  -3  -4  -5  -6  -7  -8   -9

3. queue.c文件
queue.c文件可实现队列和链表操作函数。enqueue()函数按优先级将PROC输入队列中。在优先级队列中,具有相同优先级的进程按先进先出(FIFO)的顺序排序。dequeue()函数可返回从队列或链表中删除的第一个元素。printList()函数可打印链表元素。

/******************************* queue.c file *******************************/
int enqueue(PROC **queue,PROC *p)
{
    PROC *q = *queue;
    if(q == 0 || p->priority> q->priority){
        *queue = p;
        p->next = q;
    }
    else{
        while(g->next && p->priority <= q->next->priority)
            q = q->next;
        p->next = q->next;
        q->next = p;
    }
}
PROC *dequeue (PROC **queue)
{
    PROC *p = *queue;
    if (p)
        *queue =(*queue)->next;
    return p;
}
int printList(char *name,PROC *p)
{
    printf("%s = ",name);
    while(p){
        printf("[8d %d]->",p->pid,p->priority);
        p = p->next;
    }
    printf("NULL\n");
}

4. t.c文件
t.c文件定义MT系统数据结构、系统初始化代码和进程管理函数。

进程同步

一个操作系统包含许多并发进程,这些进程可以彼此交互。进程同步是指控制和协调进程交互以确保其正确执行所需的各项规则和机制。最简单的进程同步工具是休眠和唤醒操作。
1. 睡眠模式
当某进程需要某些当前没有的东西时,它就会在某个事件值上进入休眠状态,该事件值表示休眠的原因。为实现休眠操作,我们可在PROC结构体中添加一个event字段,并实现 ksleep(int event)函数,使进程进入休眠状态。
2. 唤醒操作
多个进程可能会进入休眠状态等待同一个事件。在这种情况下,所有这些进程都将休眠等待同一个事件值。当某个等待时间发生时,另一个执行实体(可能是某个进程或中断处理程序)将会调用kwakeup(event),唤醒正处于休眠状态等待该事件值的所有程序。如果没有任何程序休眠等待该程序,kwakeup()就不工作,即不执行任何操作。

进程终止

在以下种情况下,当进程终止时,最终都会在操作系统内核中调用 kexit() 。
1. 正常终止
进程调用 exit(value), 发出exit(value)系统调用来执行在操作系统内核中的kexit(value)
2. 异常终止
进程因某个信号而异常终止。

Unix/Linux中的进程

  • INIT进程P1

    ①它是除了P0之外所有进程的祖先,所有登录进程都是它的子进程。
    ②它管理所有没有父进程的进程。(他就像孤儿院的院长,所有孤儿都叫他爸爸)。
    ③它不停寻找僵尸进程,并终止他们(埋葬它们死亡的空壳)。

操作系统启动时,内核会强行创建PID=0的初始进程,然后系统执行P0。系统挂载文件,然后初始化完成后,复刻出子进程P1。
P1运行时,执行映像更改为INIT程序,复刻出更多子进程,用于提供系统服务,这样的进程成为守护进程。

  • 进程的执行模式
  1. 中断:中断是外部设备发送给 CPU的信号,请求CPU服务。
  2. 陷阱:陷阱是错误条件,例如无效地址、非法指令、除以0等、这些错误条件被CPU识别为异常,使得CPU进入Kmode来处理错误。
  3. 系统调用:系统调用(简称syscall)是一种允许Umode 进程进入Kmode 以执行内核函数的机制。如果发生错误,外部全局变量 errno(在errno.h中)会包含一个ERROR代码,用于标识错误。用户可使用库函数perror( "error message");
  • OpenEuler下fork()的实践
    结合教材和网上资料实现代码

    运行结果

I/O重定向

在Unix系统中,每个进程都有STDINSTDOUTSTDERR这3种标准I/O,它们是程序最通用的输入输出方式。C语言可以通过scanf("%s", str);从终端输入字符串,通过printf("%s\n", str);向终端输出字符串。
理解I/O重定向的原理需要从Linux内核为进程所维护的关键数据结构入手。对Linux进程来讲,每个打开的文件都是通过文件描述符(FD)来标识的,内核为每个进程维护了一个文件描述符表,这个表以FD为索引,再进一步指向文件的详细信息。在进程创建时,内核为进程默认创建了0、1、2三个特殊的FD,这就是STDIN、STDOUT和STDERR,如下图所示意:

所谓的I/O重定向也就是让已创建的FD指向其他文件
下面是对STDOUT重定向到testfile.txt前后内核文件描述符表变化的示意图

重定向前:

重定向后:

在I/O重定向的过程中,不变的是FD 0/1/2代表STDIN/STDOUT/STDERR,变化的是文件描述符表中FD 0/1/2对应的具体文件,应用程序只关心前者。本质上这和接口的原理是相通的,通过一个间接层把功能的使用者和提供者解耦。

文件重定向常用方法

  COMMAND_OUTPUT >
     # 重定向stdout到一个文件.
     # 如果没有这个文件就创建, 否则就覆盖.

     ls -lR > dir-tree.list
     # 创建一个包含目录树列表的文件.

  : > filename
     # > 会把文件"filename"截断为0长度.
     # 如果文件不存在, 那么就创建一个0长度的文件(与'touch'的效果相同).
     # : 是一个占位符, 不产生任何输出.

  > filename    
     # > 会把文件"filename"截断为0长度.
     # 如果文件不存在, 那么就创建一个0长度的文件(与'touch'的效果相同).
     # (与上边的": >"效果相同, 但是在某些shell下可能不能工作.)

  COMMAND_OUTPUT >>
     # 重定向stdout到一个文件.
     # 如果文件不存在, 那么就创建它, 如果存在, 那么就追加到文件后边.


     # 单行重定向命令(只会影响它们所在的行):
     # ---------------------------------------------------------------

  1>filename
     # 重定向stdout到文件"filename".
  1>>filename
     # 重定向并追加stdout到文件"filename".
  2>filename
     # 重定向stderr到文件"filename".
  2>>filename
     # 重定向并追加stderr到文件"filename".
  &>filename
     # 将stdout和stderr都重定向到文件"filename".

     =====================================================================
     # 重定向stdout, 一次一行.
     LOGFILE=script.log

     echo "This statement is sent to the log file, \"$LOGFILE\"." GFILE
     echo "This statement is appended to \"$LOGFILE\"." 1>>$LOGFILE
     echo "This statement is also appended to \"$LOGFILE\"." OGFILE
     echo "This statement is echoed to stdout, and will not appear $LOGFILE\"."
     # 每行过后, 这些重定向命令会自动"reset".



     # 重定向stderr, 一次一行.
     ERRORFILE=script.errors

     bad_command1 2>$ERRORFILE       #  错误消息发到$ERRORFILE中.
     bad_command2 2>>$ERRORFILE      #  错误消息添加到$ERRORFILE中.
     bad_command3                    #  错误消息echo到stderr,
                                     #+ 并且不出现在$ERRORFILE中.
     # 每行过后, 这些重定向命令也会自动"reset".
     =====================================================================



  2>&1
     # 重定向stderr到stdout.
     # 得到的错误消息与stdout一样, 发送到一个地方.

  i>&j
     # 重定向文件描述符i 到 j.
     # 指向i文件的所有输出都发送到j中去.

  >&j
     # 默认的, 重定向文件描述符1(stdout)到 j.
     # 所有传递到stdout的输出都送到j中去.

  0< FILENAME
   < FILENAME
     # 从文件中接受输入.
     # 与">"是成对命令, 并且通常都是结合使用.
     #
     # grep search-word <filename


  [j]<>filename
     # 为了读写"filename", 把文件"filename"打开, 并且分配文件描述符"j"
     # 如果文件"filename"不存在, 那么就创建它.
     # 如果文件描述符"j"没指定, 那默认是fd 0, stdin.
     #
     # 这种应用通常是为了写到一个文件中指定的地方.
     echo 1234567890 > File    # 写字符串到"File".
     exec 3<> File             # 打开"File"并且给它分配fd 3.
     read -n 4 <&3             # 只读4个字符.
     echo -n . >&3             # 写一个小数点.
     exec 3>&-                 # 关闭fd 3.
     cat File                  # ==> 1234.67890
     # 随机存储.



  |
     # 管道.
     # 通用目的的处理和命令链工具.
     # 与">"很相似, 但是实际上更通用.
     # 对于想将命令, 脚本, 文件和程序串连起来的时候很有用.
     cat *.txt | sort | uniq > result-file
     # 对所有的.txt文件的输出进行排序, 并且删除重复行,
     # 最后将结果保存到"result-file"中.


OpenEuler下管道实践
尝试通过代码实现简单的管道操作

运行结果

本周实践代码已上传到我的码云仓库

推荐阅读