进程、进程调度、进程状态、进程与程序、多任务

进程、进程调度、进程状态、进程与程序、多任务

多任务机制

  • 多任务处理是指用户可以在同一时间内运行多个应用程序,每个正在执行的应用程序被称为一个任务。Linux是一个支持多任务的操作系统,比起单任务系统它的功能增强了许多
  • 多任务操作系统使用某种调度策略支持多个任务并发执行。事实上(单核)处理器在某一时刻只能执行一个任务。每个任务创建时被分配时间片(几十到上百毫秒),任务执行(占用CPU)时,时间片递减。操作系统会在当前任务的时间片用完时调度执行其他任务。由于任务会频繁地切换执行,因此给用户多个任务同时运行的感觉。多任务操作系统通常有3个基本概念:任务、进程和线程
  • 任务指的是一个逻辑概念,指由一个软件完成的活动,或者是为实现某个目的而进行的一系列操作。通常一个任务是一个程序的一次运行,一个任务包含一个或多个完成独立功能的子任务,子任务是进程或线程。例如一个杀毒软件的一次运行是一个任务,目的是保护计算机系统不受各种病毒的侵害。这个任务包含多个独立功能的子任务,包括实时监控功能、定时查杀功能、防火墙功能等

进程与程序

  • 进程是指一个具有独立功能的程序在某个数据集合上的一次动态执行过程,它是操作系统进行资源分配和调度的基本单元。一次任务的运行可以激活多个进程,这些进程相互合作来完成该任务的一个最终目标

程序包含了一系列信息的文件,这些信息描述了程序在运行时如何创建一个进程,包括如下

  1. 二进制格式标识:每个程序文件都包含用于描述可执行文件格式的元信息
  2. 机器语言指令:对程序进行编码
  3. 程序入口地址:标识程序开始执行时的起始指令位置
  4. 数据:程序文件包含的变量初始值和程序使用的字面变量
  5. 符号表及重定位表:描述程序中函数和变量的位置及名称。这些表有多种用途,其中包含调试和运行时的符号解析
  6. 共享库和动态链接信息:程序文件所包含的一些字段,列出了程序运行时需要使用的共享库,以及加载共享库的动态链接器的路径名 7

进程是程序动态执行的过程,具有并发性、动态性、交互性和独立性

  • 并发性:指系统中多个进程可以同时并发执行,相互之间不受干扰
  • 动态性:指进程都有完整的生命周期,而且在进程中声明周期内,进程的状态是不断变化,而且进程具有动态的地址(包含代码、数据和进程控制块等)
  • 交互性:指进程是在执行过程中可能会与其他进程发生直接和间接的通信,如进程同步和进程互斥等,需要为此添加一定的进程处理机制
  • 独立性:指进程是一个相对完整的资源分配和调度的基本单位,各个进程的地址空间是相对独立的,因此需要引入一些通信机制起来实现进程之间的通信

进程和程序有本质区别

  1. 程序是一段静态的代码,是保存在非易失性存储器上的指令和数据的有序集合,没有任何执行的概念
  2. 进程是一个动态的概念,它是程序的一次执行过程,包括了动态创建、调度、执行和消亡的整个过程,它是程序执行和资源管理的最小单位。可以用一个程序来创建许多进程。或者反过来说,许多进程运行的可以是同一个程序

进程类型

  1. 交互式进程:交互式进程经常与用户进行交互,需要等待用户的输入(键盘和鼠标操作等)。当接收用户的输入之后,这类进程能够立刻响应。典型的交互式进程有Shell命令进程、文本编程器和图形应用程序运行等
  2. 批处理程序:批处理程序不必与用户进行交互,因此通常在后台运行。由于这类进程通常不必很快地响应,因此往往不会优先调度。典型的批处理进程是编译器的编译操作、数据库搜索引擎等
  3. 守护进程:守护进程一直在后台运行,和任何终端都不关联。通常系统启动时开始执行,系统关闭时才结束。很多系统进程(各种服务)都是以守护进程的形式存在的

进程的状态

内核将所有进程存放在双向循环链表(进程链表)中,链表的节点都是task_struct结构体,称为进程控制块的结构。该结构包含了与一个进程相关的所有信息,如进程的状态、进程的基本信息、进程标识符、内核相关信息、父进程相关信息、与进程相关的终端信息、当前工作目录、打开的文件信息、所接收的信号信息等

进程状态

  1. 运行态(TASK_RUNNING):进程当前正在运行,或者正在运行队列中等待调度
  2. 可中断的睡眠态(TASK_INTERRUPTIBLE):进程处于阻塞(睡眠)状态。正在等待某些事情发生或能够占用某些资源。处在这种状态下的进程被信号中断。接收信号或被显示唤醒呼叫之后,进程将转变为运行(TASK_RUNNING)状态
  3. 不可中断的睡眠态(TASK_UNINTERRUPTIBLE):此进程状态类似可中断的睡眠,只是它不会处理信号,把信号传递到这种状态下的进程不能改变其状态。只有在它所等待的时间发生时,进程才显示地被唤醒呼叫
  4. 停止态(TASK_STOPPED):进程的执行被暂停。当进程接收SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号,就会进入暂停状态
  5. 僵尸态(TASK_ZOMBIE):子进程运行结束,父进程未退出,并且未使用wait()函数族(如使用waitpid()函数)等系统调用来回收子进程的资源。处在该进程下的子进程已经放弃了几乎所有的内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等供其父进程收集
  6. 消亡态:进程退出,不占用任何资源,更不会被调度,该状态不可见

进程标识符

Linux内核通过唯一的进程标识符(进程身份证号)PID(Process ID)来标识每个进程。 Linux中获得当前进程的进程号(PID)和父进程号(PPID)的系统调用函数分别为getpid()和getppid()

进程组与会话组

Linux系统中,进程是以组的形式(进程之间的层次关系)进程管理的,如进程组和会话组,进程组是一组相关进程的集合,会话组是一组相关进程组或进程的集合

进程的优先级

Linux和大多数其他UNIX实现一样,调度进程使用CPU默认模型是循环时间共享算法。在这种模型下,每个进程轮流使用CPU一段时间,这段时间被称为时间片。循环时间共享算法满足了交互式多任务系统满足两个重要需求

  1. 公平性:每个进程都有机会用到CPU
  2. 响应性:一个进程在使用CPU之前无须等待太长时间

在循环时间共享算法中,进程无法直接控制何时使用CPU以及使用CPU的时间。在默认情况下,每个进程轮流使用CPU直至时间片被用光或自己自动放弃CPU(如进程睡眠)。如果所有进程都试图尽可能地使用CPU,那么它们使用CPU的时间差不多是相等的

进程的特性nice值允许进程间接地影响内核的调度算法。每个进程都有一个nice值,其取值的范围为-20(高优先级)~19(低优先级),默认值为0。在传统的UNIX实现中,只有特权进程才能赋给自己(或其他进程)一个负(高)优先级。非特权进程只能降低自己优先级,即赋一个大于默认值0的nice值。

nice值是一个权重因素,它导致内核调度器倾向于调度拥有更高优先级的进程。给一个进程赋一个低优先级,并不会导致它完全无法用到CPU,但会导致它使用CPU的时间变少。nice值对进程调度的影响程序则根据Linux内核版本的不同而不同

 

进程的调度策略

在系统中有多个进程同时执行,单个CPU下,实际上任意时刻只能有一个进程处于执行状态,而其他进程则处于非执行状态。下面两种调度策略

  1. SCHED_RR策略

在SCHED_RR策略中,优先级相同的进程以循环时间分享的方式进行。进程每次使用CPU的时间为一个固定长度的时间片。一旦被调度执行之后,使用SCHED_RR策略的进程满足下列条件中的一个会放弃CPU的控制,否则会保持对CPU的控制

a. 时间片结束
b. 资源放弃CPU,如执行阻塞式的系统调用
c. 进程终止
d. 被一个优先级更高的系统调用

前两种情况中,进程放弃CPU之后,将会放置在与其优先级级别对应的队列的队尾。在最后一种情况中,当优先级更高的进程执行结束之后,被抢占的进程会继续执行知道其时间片的剩余部分被消耗完(被抢占的进程仍然位于其优先级级别对应的队列的队头)

  1. SCHED_FIFO策略

SCHED_FIFO策略(先入先出)与SCHED_RR策略类似。它们之间最主要的差别在于SCHED_FIFO策略中不存在时间片,如果一个SCHED_FIFO进程获得了CPU的控制权之后,它就会一直执行直到下面某个条件满足

a. 自愿放弃CPU
b. 进程终止
c. 被一个优先级更高的进程抢占

第一种情况中,进程会被放置在与其优先级级别对应的队列的队尾。在最后一种情况中,当高优先级进程执行结束后,被抢占的进程会继续执行

上述两种被抢占的原因可能有以下几种

  1. 之前被阻塞的高优先级进程解除阻塞了
  2. 另一个进程的优先级被提到了一个比当前进程的优先级高的级别
  3. 当前运行的进程优先级被降低到低于其他可运行的进程的优先级

 

进程编程

子进程被创建是通过对父进程进行复制得来的,即子进程是父进程的复制品,进程都有属于自己的虚拟地址空间,用以保存进程的各种信息。因此,子进程是对父进程的复制,实质上讲,是复制了父进程的整个地址空间,其中包含了进程的上下文、代码段、进程堆栈、内存信息、文件描述符、符号处理函数等;而子进程所独有的只有它的进程号、资源使用、计时器等

子进程虽然复制了父进程的地址空间,但子进程成功创建之后,子进程所访问的虚拟地址空间一定是属于自己的,而非与父进程共享此空间。其本质是父子进程映射到不同物理地址空间

因为子进程是对父进程的精准复制,所以父子进程的程序代码是一样的。因此需要某一种方式来区分父子进程。否则,父子进程执行的代码是一致的,创建子进程没有任何意义

如何区分父子进程

通过fork()函数返回值来判定的。子进程通过对父进程的精准复制产生,fork()函数如果执行失败,父进程得到返回值为-1;如果执行成功,在子进程中得到一个值为0.在父进程中得到一个子进程ID(一定大于0),需要注意的是,fork()函数不是父进程中得到两个返回值,而是父子进程分别得到fork()函数返回的一个值

#include <stdio.h>
#include <unistd.h>

int main(int argc, const char *argv[]) 
{
    pid_t pid;
    pid = fork();
    if (pid < 0) {
        perror("fork error");
        return -1;
    } else if (pid == 0) {
        // 子进程执行代码区
        printf("The clild process")
    } else {
        // 父进程执行代码区
        printf("The parent process");
    }
    return 0
}


## The parent process
## The clild process

代码解读

  1. 程序在执行fork()函数时,开始其创建子进程,子进程开始对父进程进行复制
  2. 在变量pid接收fork()函数的返回值之前,子进程创建结束
  3. 由于子进程几乎复制了父进程的使用地址空间(包括栈区),因此父子进程都有局部变量pid,子进程的pid接收的返回值为0,而在父进程的pid接收的值是子进程的ID
  4. 由于父子进程的变量pid接收的值不同,因此,根据代码的分支判断,可以看出父子进程虽然代码相同,但执行的内容却不同
  5. 父进程执行的代码是判断pid大于0的部分,以及fork()函数、fork()函数之前所有的执行代码,称为父进程的执行代码区。子进程执行的代码是判断pid等于0的部分,称为子进程的执行代码区。注意,子进程不执行fork()函数以及fork()函数以上所有的执行代码,否则子进程会被无限创建

 

Linux复制技术

写时复制技术是一种可以推迟甚至避免复制数据的技术。内核此时并不是复制整个进程空间。而是让父进程与子进程共享同一副本。即使用相同的物理内存空间,子进程的程序文本段、数据段、堆栈区都指向父进程的物理内存空间。也就是说,二者虚拟内存空间不同,但其对应的物理内存空间是同一个,并且这些分段的页被标记为只读。

当父进程中有更改相应段的行为发生时,再为子进程相应的段分配物理内存空间。这种技术使得地址空间中的复制被推迟到实际发生写入的时候

 

孤儿进程与僵尸进程

孤儿进程

父进程退出,子进程不退出,此时init进程成为其父进程。这时,此子进程就是一个孤儿进程。这里需要说明,进程在退出时,通常由该进程的父进程对其资源进程回收及释放资源。如果该进程的父进程提前退出,那么此时该进程将失去了“父亲”,则成为了“孤儿”。因此系统默认init进程成为孤儿进程的“父亲”,以便在以后对其资源进行回收

// 子进程代码执行区域
if (pid ==0) {
    printf("The child process");
    while(1);
}

僵尸进程

进程的僵尸态与死亡态很接近。唯一不同的是,死亡进程,即进程退出,释放所有资源;而僵尸进程,即进程退出但没有释放资源。因此在实际的编程中,应尽量关注一点,避免产生僵尸态的进程,因为僵尸进程不执行任何任务,但却占用系统资源。如果僵尸进程太多,就会导致系统浪费资源

// 父进程代码执行区域
if (pid < 0) {
    
} else if (pid == 0) {
    
} else {
    printf("The parent process");
    while(1);
}

如果父进程不退出子进程退出,此时父进程不会主动回收子进程的资源,子进程成为僵尸进程

$ ps axj
子进程的状态(STAT)为Z+ ,Z表示僵尸态

  • 产生僵尸进程往往是因为父进程没有对子进程的资源进行回收处理。因此为了避免这样子的结果出现,程序需要在子进程选择退出时,由父进程进行资源回收处理

守护进程

Linux守护进程为后台服务进程(独立于控制终端)。该进程通常周星期地执行某种任务或等待处理某些发生的事件。其生命周期较长,通常在系统启动时开始执行,在系统关闭时终止。Linux很多系统服务都是通过守护进程实现的

如何将一个进程变成一个守护进程,流程如下
  1. 创建子进程(子进程不退出,父进程退出):父进程先于子进程退出,造成子进程成为孤儿进程。此时子进程的父进程变成init进程
  2. 在子进程中创建新会话
  3. 改变当前的工作目录
  4. 重设文件权限掩码
  5. 关闭文件描述符
posted @ 2021-01-29 23:12  将来-小志  阅读(424)  评论(0编辑  收藏  举报