进程控制块(PCB) —— task_struct

2019-07-14 12:57发布

我们把正在执行的程序称为进程,这是一种广义的定义。更具体一点来说,进程是由正文端 (text) 、用户数据段 (usr segment) 以及系统数据段 (system segment)共同组成的一个程序执行环境。

进程和程序

  • 程序
    程序是一个包含及其代码指令和数据的可执行文件,这个文件一般储存在磁盘上(储存设备)上,所以,程序是一个静态的实体。比如,我们用 C 语言写了一个C源程序,这个程序要经过 “预处理-编译-汇编-链接”这一过程才能生成可执行文件,这个可执行文件在 Windows 下一般为 xxx.exe,该可执行文件才是我们所说的程序。
  • 进程
    我们可以认为上述所说的程序是:你期望完成某项任务的方法和步骤,它只浮现在纸面上,等待去实现。而这个实现过程就是由进程来完成的,进程可以认为是运行中的程序。它除了包含程序中的所有数据之外,还包含一些额外数据。
    当程序被装入内存中并且获取到所需资源后就可运行了:在程序计数器 (PC) 和其它一些寄存器的控制下,机器指令被取至 cpu 运行。
    下图是其大概情况:
    这里写图片描述
进程在运行过程中,还需要一些系统资源。其中最重要的就是 cpu 资源了,除此之外还包括但不限于物理内存(以容纳进程本身和其有关数据)、打印机、键盘等等。
由上可见进程是一个动态的实体,它每时每刻都在发生着变化。那么如何管理和描述这个动态的进程呢?请看下面详情。

进程控制块

在 Linux 中每个进程由一个 task_struct 结构体来描述,该结构体也被称为进程控制块(PCB)。它被定义于 include/linux/sched.hLinux源码github地址)。 task_struct 容纳了一个进程的所有信息,它是系统对进程进行控制的唯一手段,也是最有效的手段。 每当系统创建一个进程,就会给该进程动态的分配一个 task_struct 结构体对象。一个系统内所允许的最大进程个数一般由机器硬件 (物理内存) 决定。在一台 IA32 体系结构中,内存为 512M 的机器上所允许的最大进程数是 32k 。 总之包含进程所有信息的 task_struct 内容是比较庞大复杂的,我们将其部分内容罗列如下: struct task_struct{ ... // 进程标识符 ... // 上下文信息 ... // 进程状态 ... // 进程优先级 ... // 进程通信有关信息 ... // 时间和定时器有关信息 ... // 文件系统信息 ... // 虚拟内存信息 ... // 其它 ... };
  • 进程标识符
操作系统中有很多进程,不管对于用户还是对于内核,如何用一种简单的方式以区分不同进程呢?这就引入了进程标识符 (PID:process identifier),每个进程都拥有一个唯一的进程标识符,内核以此来区分不同进程,同时,用户也可以通过此标识符来给具体进程发号施令。Linux 中我们可以通过以下几种方式获取 PID:
  • shell中:通过ps aux列出所有进程详细信息,在其中我们可以看到进程的 PID, 也可以通过 ps aux | grep '进程名' 查看指定进程信息,除此之外还有 top 命令也可以查看进程信息。
查看所有进程信息:
这里写图片描述
查看指定进程信息:
这里写图片描述
  • C程序中相关系统调用函数
#include .h> pid_t getpid(); //调用进程的进程id pid_t getppid(); //调用进程的父进程id uid_t getuid(); //调用进程的实际用户的id uid_t geteuid(); //调用进程的有效用户id gid_t getgid(); //调用进程的实际组id gid_t getegid(); //调用进程的有效组id 这里写图片描述
  • 也可以通过文件 /proc/ 文件来查看进程信息。
这里写图片描述 除此之外的其它 id:
这里写图片描述
  • 上下文信息
    上下文信息一般和处理器密切相关。进程作为一个程序执行环境的综合,当处理器调度执行某个程序时,需要将相关指令和数据加载到对应的寄存器和堆栈中,当进程暂停或者等时,必须将其对应的寄存器和堆栈信息暂存起来,以便稍后重新调度该进程时,将其回复到暂停之前的状态,那么这一部分信息就是进程的上下文信息。
  • 进程状态
    进程在执行时,会根据环境改变其状态,进程状态是进程调度的依据。在 Linux 中进程更主要有这些状态:
/* Used in tsk->state: */ #define TASK_RUNNING 0x0000 //可运行 #define TASK_INTERRUPTIBLE 0x0001 //可中断的等待 #define TASK_UNINTERRUPTIBLE 0x0002 //不可中断的等待 #define __TASK_STOPPED 0x0004 //暂停 /* Used in tsk->exit_state: 退出状态*/ #define EXIT_DEAD 0x0010 //死亡 #define EXIT_ZOMBIE 0x0020 //僵死 #define TASK_STATE_MAX 0x1000 //进程最大个数:8k
  • 可运行状态:处于该状态的进程,要么正在运行,要么准备运行(在等待cpu资源)。系统通过一个运行队列 (run_queue) 来管理处于此状态的进程。
  • 等待状态:处于该状态的进程在等待某个事件或某个资源(磁盘、打印机),这些进程位于系统中的等待队列中(wait_queue),对于等待不同资源设置有不同等待队列,比如,需要打印机的进程被置于打印机的等待队列中,需要磁盘的进程被置于磁盘等待队列中。可中断的等待可以被信号唤醒,如果被唤醒,该进程就被加入到运行队列中,等待被调度,不可中断的等待是由于没有所需的硬件而等待,需要的磁盘资源暂时被其它进程占用,这一类进程不可以被信号唤醒,直到它获取到需要的硬件资源。
  • 暂停状态:此时进程停止运行等待接收某种处理通常进程接收到 SIGSTOP、SIGTSTP、SIGTTIN 或 SIGTTOU信号后就处于这种状态。正在调试的进程就处于这种状态,如下图所示:
    这里写图片描述
  • 僵死状态:当子进程退出,而父进程没有退出,也没有读取(wait()) 子进程退出状态时,此时子进程就进入僵尸状态 (僵尸进程详情)。
  • 进程优先级
    调度程序依靠这一部分信心决定进程的执行顺序,并结合进程的状态信息保证系统运转的公平和高效。
Linux通过以下几种方式查看进程优先级:
  • top: 动态列出系统的整体运行情况;
  • ps -l:采用详细格式显示进程情况。
下面是用top 查看的详情:
这里写图片描述 如上图,有两列和进程优先级有关:
  • PR (priority):进程优先级,越小代表优先级越高;
  • NI (nice):优先级修正参数。
如何设置优先级?
在此之前我们先创建一个小脚本文件,它件循环执行累减操作,以达到占用更多cpu资源的目的。然后我们再以此脚本文件为例,来修改其优先级,观察其达到的效果。 该脚本文件如下:
文件名: count.sh
文件内容: #!/bin/bash x=100000000 echo $(date) while[ $x -gt 0 ]; do x=$((x-1)); done echo $(date) 有了上面的脚本文件,我们就可以依次通过下图命令行指令来执行该脚本文件(用 top 查看进程情况):
这里写图片描述
显然此时该进程优先级为默认的20,而且该进程占用了极高的cpu资源。 修改优先级的第一种方法是:在程序开始执行前,通过如下命令修改: nice +n +5 -p fileName//在原来优先级基础上+5 详情如下图所示:
这里写图片描述
可以看到当降低 (PR越大,优先级越小) 进程优先级后,其占用 cpu 资源降低了很多。 修改优先的第二中方法:在程序执行时修改,其命令如下: renice +5 -p pid//需要知道进程id 详情如下图:
这里写图片描述
  • 进程通信有关信息
    为了可以使进程相互协作完成任务,不同进程间必须进行通信,即交流数据。Linux 支持多种不同的通信机制。支持电信的 Unix 通信机制(IPC) ,比如管道、信号,也支持 System V 通信机制:共享内存、信号量和消息队列。
  • 时间和定时器
    一个进程从创建到终止叫做该进程的生存期。一个进程在其生存期内使用 cpu 的时间系统都要进程记录,以便进程统计、“计费”等操作。“时间”对于操作系统是及其重要的,比如在进程调度的时间片轮转中,操作系统要根据当前进程在 cpu 上执行时间的长短以确定是否要将 cpu 分配给其它进程,这个关乎到系统对进程调度是否公平和高效。
    这里写图片描述
  • 文件系统信息
    进程可以打开和关闭文件,文件属于系统资源,Linux 内核需要记录进程对文件的使用情况。task_struct 进程控制块中有两个结构体用于记录文件相关信息。其中 fs_struct 中描述了两个 VFS节点,分别是 root 和 pwd,一个指向根目录,一个指向当前目录。还有 files_struct 记录进程打开的文件描述符。
    这里写图片描述
    在文件系统中,每个 VFS 索引节点唯一描述一个文件或目录,同时该节点也是向更低
    层的文件系统提供的统一的接口。
  • 虚拟内存信息
    除了内核线程(kernel thread),每个进程都拥有自己的地址空间(也叫虚拟空间),
    用 mm_struct 来描述。另外 Linux2.4 还引入了另外一个域 active_mm,这是为内核线程而引
    入。因为内核线程没有自己的地址空间,为了让内核线程与普通进程具有统一的上下文切换
    方式,当内核线程进行上下文切换时,让切换进来的线程的 active_mm 指向刚被调度出去
    的进程的 active_mm(如果进程的 mm 域不为空,则其 active_mm 域与 mm 域相同)。
    这里写图片描述
我们对进程的 task_struct 结构进行了归类讨论,还有一些域没有涉及到。task_struct 结构是进程实体的核心,Linux 内核通过该结构来控制进程:首先通过其中的调度信息决定该进程是否运行;当该进程运行时,根据其中保存的处理机状态信息来恢复进程运行现场,然后根据虚拟内存信息,找到程序的正文和数据;通过其中的通信信息和其他进程实现同步、通信等合作。几乎所有的操作都要依赖该结构,所以,task_struct 结构是一个进程存在的唯一标志。

task_struct 在内存中的存放

  • 进程内核栈
    每个进程都有自己的内核栈。当进程从用户态进入内核态时,CPU 就自动地设置
    该进程的内核栈,也就是说,CPU 从任务状态段中装入内核栈指针 esp。
    这里写图片描述
  • 当前进程
    当一个进程在某个 CPU 上正在执行时,内核如何获得指向它的 task_struct 的指针?current 指针指向当前 cpu 上运行的进程。
    这里写图片描述
    除此之外还有当前进程状态相关信息:
    这里写图片描述

进程的组织方式

  • 哈希表
    哈希表是一种查找效率极高的数据结构,Linux 中以哈希表来组织进程。
    这里写图片描述
下面代码判断进程是否闲置(这个我也不太清楚)。
这里写图片描述 上述我们讨论进程控制块的部分内容,有关该结构的内容实在是太多了(有大约1600行代码),有兴趣的朋友可以自行研究。

参考资料

【作者:果冻 http://blog.csdn.net/jelly_9