Linux进程状态与进程优先级
  PzTaj2xFbKXN 2023年11月13日 24 0

Linux进程概念与管理方式

Linux下的进程

冯·诺依曼计算机体系结构决定了,一个程序只有被加载到内存中才能被 CPU 执行。粗浅来说,一个可执行程序被加载到内存中,即成为一个进程,但这种说法是不完全正确的。在 Linux 中,一个进程包括两部分:可执行程序的代码、数据和与这个可执行程序相对应的 PCB,即进程 = 代码和数据 + PCB。PCB(process ctronl block) 即进程控制块,是一个可执行程序被加载到内存后被抽象的结果,操作系统通过管理各个进程 PCB 以管理不同的进程。在linux中,PCB本身是一个task_struct结构体,其中包含了进程的各种属性信息,包括进程编号、进程状态和进程优先级等。通过管理PCB对进程进行管理,体现了“先描述(数据抽象),后组织”的管理思想。下文中一旦提到对进程的管理,实际上是操作系统对进程对应PCB的管理。

进程编号(PID)是进程的唯一标识,用来区分进程。每个进程都有与众不同的标识符。在Linux中,系统提供了两个C语言接口:getpid()getppid(),前者返回当前进程的PID,后者返回当前进程的父进程的PID。

在Linux中,对进程的组织方式有很多种,最基本的组织方式是双链表。使用ps命令可以查看当前各个进程的PCB的部分内容。或者可以直接查看/proc目录文件。

Linux进程状态与进程优先级_进程优先级

通过 /proc 目录文件查看进程:

Linux进程状态与进程优先级_优先级调度_02

在 /proc 文件下,每个进程都以目录文件的形式被体现,目录文件的文件名往往是进程的PID,其中记录了进程的各种属性信息:

Linux进程状态与进程优先级_优先级调度_03

进程的创建与死亡

创建进程有两种方式:运行指令使用接口。前者是指令层面的,当在命令行输入某些命令时,本质上是运行了某些软件;对于后者,Linux提供了fork()函数接口用来创建子进程,属于代码层面创建进程的方法。

在子进程中,fork() 返回 0;在父进程中,如果创建进程成功,则 fork()  返回子进程的 PID,否则返回 -1。

#include <stdio.h>
#include <unistd.h>    
    
int main()    
{    
  pid_t id = fork(); //使用fork()创建子进程 
  if(id > 0)    
  {    
    while(1)    
    {
      printf("parent, pid:%d, ppid:%d\n", getpid(), getppid());
      sleep(1);
    }
  }
  else if(id == 0)
  {
    while(1)
    {
      printf("child, pid:%d, ppid:%d\n", getpid(), getppid());
      sleep(1);
    }
  }
  else {
    printf("error");
  }
  sleep(1);
  return 0;
}

针对父进程和子进程,fork() 的返回值有所不同,粗浅来看,这是为了区分父、子进程。同时,一个父进程可以有多个子进程,而一个子进程只会有一个父进程,将子进程的PID返回给父进程,可以将子进程进行区分。

fork() 的工作主要包含3步:创建进程填充进程的PCB值返回。对于父进程和子进程,二者共用的是父进程的代码指令,当fork()完成前两步工作时,子进程已经被创建,所以最后的一步 return 语句是被父、子进程共用的,即fork()分别在父、子进程中返回了两次。对于父进程的数据部分,子进程进行的是写时拷贝,即子进程不与父进程共用数据部分,也不在被创建时将父进程的数据进行拷贝,而是在需要修改父进程的数据时进行拷贝,进而一个变量可以在父、子进程中分别存在两份。这样可以达到保护父进程数据和节省内存空间的目的。

Linux进程状态与进程优先级_进程优先级_04

Linux进程状态

操作系统的进程状态

进程状态描述了进程当前的状态。在操作系统学科中,进程状态分为大致三种:运行(run),阻塞(block)和挂起(loop),处于前两种状态的进程分别位于运行队列、阻塞队列中。换句话说,进程状态的本质其实是进程PCB处于哪个队列中。运行队列由CPU维护,每个CPU有一个运行队列,只要是在运行队列中的进程,无论是正在等待或是正在运行都处于运行状态,未处于运行状态的进程对于CPU来说都是空闲的。阻塞队列可能由各个硬件或某些软件维护,一个进程处于阻塞状态,本质上是在等待硬件/软件的某些资源或是等待某些条件成立,一旦资源到手或是条件成立,处于阻塞状态的进程就会被唤醒,进入运行队列。最常见的处于阻塞队列的进程,便是一个等待用户输入的进程。挂起是个比较少见的进程状态,常见于内存资源不足的情况。处于挂起状态的进程,它的内存资源(代码指令和数据)被从内存中移出(换出),以临时腾开内存空间,这个过程中进程的PCB依然保留在内存里。不常用或者不重要的进程最有可能被换出内存,当需要进程重新运行时,进程的代码和数据会被换入,并等待下一步调度。

Linux进程状态与进程优先级_Linux进程_05

Linux的进程状态

作为一款具体的操作系统,Linux定义了如下进程状态:

static const char* const task_state_array[] = {
  "R (running)", /* 0 */
  "S (sleeping)", /* 1 */
  "D (disk sleep)", /* 2 */
  "T (stopped)", /* 4 */
  "t (tracing stop)", /* 8 */
  "X (dead)", /* 16 */
  "Z (zombie)", /* 32 */
};

R状态即运行状态(runnning),如上文所说,处在运行队列中的进程,无论是正在被CPU执行或是在排队,都处于这个状态。

S状态即休眠状态(sleeping)。由于CPU的处理速度与硬件设备读取速度的差异,涉及IO的进程,往往大部分时间都处于休眠状态等待IO设备就绪,所以休眠状态也是最常观察到的进程状态。sleeping实际上是一种浅度休眠,处于浅度休眠状态下的进程可以被暂停或杀死。

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

int main()
{
  printf("I'm a process, pid: %d\n", getpid());
  int a = 0;
  scanf("%d", &a);
  return 0;
}

Linux进程状态与进程优先级_优先级调度_06

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

int main()
{
  while(1)
  {
    printf("I'm a process, pid:%d\n", getpid());
    sleep(1);
  }
  return 0;
}

Linux进程状态与进程优先级_进程优先级_07

D状态即深度休眠状态(disk sleep)。进程在等待硬件的资源时,往往会处在浅度休眠状态中,但在等待磁盘进行写入时,进程的状态为disk sleep,即深度休眠。处于深度休眠状态的进程不能被杀死,不响应任何请求,只能等待自己醒来,这是为了保证磁盘数据的安全,避免进程在接收磁盘的数据时被中断而导致数据丢失。深度休眠状态的时间一般很短。

T状态和t状态通称为暂停状态(stopped和tracing stop)。与休眠状态不同的是,T状态更偏向于对进程的控制,而不一定是等待硬件的某种资源,而处于休眠状态的进程一定是在等待某些资源。向进程发送 SIGSTOP 信号,则进程将处于T状态,对处在T状态的进程,向其发送 SIGCONT 信号以恢复其运行。当进程被跟踪时,它处在 t 状态,等待跟踪它的进程对它进行操作。对于进程本身来说,T状态和t状态很相似,都使进程暂停下来,而t状态不能被SIGCONT信号唤醒,只能通过调试进程对其进行操作,相当于在T状态的基础上加了一层保护。

Linux进程状态与进程优先级_优先级调度_08

X状态即死亡状态(dead)。X状态是一个返回状态,所以不能通过指令查看到这个状态。进程在退出时会被置为X状态,接下来这个进程会被彻底释放。X状态非常短暂,几乎不可能通过ps命令捕捉到。

Z状态即僵尸状态(zombie)。当一个子进程退出后,如果它的父进程没有查看和回收子进程的信息,则这个子进程就会进入Z状态。处于Z状态的进程,其PCB和部分资源都不能被释放,一个进程如果长期处于Z状态,就会造成内存泄漏问题。Z状态会持续到父进程退出,或者父进程通过waitwaitpid对僵尸进程进行状态收集,然后僵尸进程的资源便会被操作系统回收。

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

int main()
{
  pid_t id = fork();
  if(id > 0)
  {
    int cnt = 100;
    while(cnt--)
    {
      printf("parent, pid:%d, ppid:%d\n", getpid(), getppid());
      sleep(1);
    }
  }
  else if(id == 0)
  {
    int cnt = 10;
    while(cnt--)
    {
      printf("child, pid:%d, ppid:%d\n", getpid(), getppid());
      sleep(1);
    }
  }
  else {
    printf("error");
  }
  sleep(1);
  return 0;
}

Linux进程状态与进程优先级_优先级调度_09

除了上述Linux定义的进程状态外,还有一种进程叫做孤儿进程(orphan process)。当一个父进程先于其子进程退出后,子进程的父进程会变为1号进程(操作系统),父进程是1的进程称为孤儿进程。父进程退出后,将子进程托管给操作系统,便于操作系统对子进程的信息和资源进行回收。孤儿进程运行在操作系统后台。

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

int main()
{
  pid_t id = fork();
  if(id > 0)
  {
    int cnt = 10;
    while(cnt--)
    {
      printf("parent, pid:%d, ppid:%d\n", getpid(), getppid());
      sleep(1);
    }
  }
  else if(id == 0)
  {
    int cnt = 100;
    while(cnt--)
    {
      printf("child, pid:%d, ppid:%d\n", getpid(), getppid());
      sleep(1);
    }
  }
  else {
    printf("error");
  }
  sleep(1);
  return 0;
}

Linux进程状态与进程优先级_优先级调度_10

Linux进程优先级

由于CPU的资源有限,所以各个进程之间互为竞争关系,为了保证各进程之间的良性竞争,操作系统需要根据进程的优先权进行进程调度。进程的优先权(priority)指的是CPU资源分配的先后顺序。优先权高的进程有优先执行权利,通过优先权对进程进行调度,有利于提高系统的整体性能。如果进程长时间得不到CPU资源,代码不得以执行,就会造成进程饥饿问题。

优先级是进程优先权的一个量度,使用ps -l [PID]命令可以查看进程的优先级信息:

Linux进程状态与进程优先级_Linux进程_11

PRI即进程的优先级,PRI越小,进程的优先级越高;NI值即nice值,是PRI的修正数据,加入nice值后,有PRI(new) = PRI(old) + NI,当nice值为负时,PRI减小,进程的优先级提高,当nice值为正时,PRI增大,进程的优先级下降。

用户如何调整进程的优先级

用户可以通过nice或者renice命令通过修改进程的nice值间接修改进程的优先级。一种修改方式为,在top进程管理器界面输入r,并按提示输入要修改的进程PID、nice值即可对进程的优先级进行修改。当通过这种方式进行优先级修改时,无论先前的PRI为何值,PRI(old)都以80计。

Linux进程状态与进程优先级_优先级调度_12

虽然Linux允许用户修改进程优先级的操作,但是不建议轻易进行,随意地对进程优先级进行修改,可能会妨碍其他进程工作,影响系统性能。为了防止用户过多地对进程优先级进行操作和干扰,提高进程的优先级时往往会需要root权限,且规定NI的范围为[-20, 19],也就是说进程的PRI值的范围为[60, 99]

Linux 2.6内核对进程的优先级调度概览

大体上来说,一个运行队列的数据结构应具有下面的关键结构:

struct runqueue
{
  /*…………*/
  char bitmap[5];
  task_struct** run;
  task_struct** wait;
  task_struct* running[140];
  task_struct* waiting[140];
  /*…………*/
}

上面代码中的running是一个PCB数组,维护的是运行队列中的进程的PCB,数组大小为140,其中下标为[0, 99]的进程为其它进程,一般为操作系统的进程,下标为[100, 139]的进程为用户的进程,run指针是维护running的一个指针,它的作用在下文可以看到。bitmap是一个含有40个比特位的位图,每个比特位分别记录了PRI值为[60, 99]的进程队列是否有进程,用 1 或 0 进行标识,位图的存在可以快速找到有进程存在的那一栏优先级。各种优先级的进程的调度和新进程插入时的情景大致如下:

Linux进程状态与进程优先级_Linux进程_13

散列表维护各个优先级的进程的运行。除此之外,为了高效拿到有进程运行的优先级队列,使用位图对各个优先级队列中的进程有无做标记。散列表和位图结合,可以实现维护优先级队列中进程的时间复杂度近似为O(1)

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月13日 0

暂无评论