进程的状态

一个进程有三种状态

  1. 运行态(running state)
  2. 就绪态(ready state)
  3. 阻塞态(Blocked state)

一个新创建的进程最开始会进入就绪态,当系统选择该进程执行的时候,就进入了运行态;如果这个时候有其它进程打断,或者有更优先的进程需要被执行的时候,这个进程就又回到了就绪态;如果这个进程需要到某些资源,而这个资源当前还没有准备好,进程就会转入阻塞态;当这个资源准备好了,进程就会被转入就绪态等待被执行。

states

其它状态

某些操作系统甚至提供更多状态。

比如说:

  • 当一个新的进程被创建的时候,它的状态是新状态(new state)。
  • 当进程被终止,它的状态会变成僵死态(Terminated state)。

分时系统

分时系统是上个世纪六七十年代一个伟大的发明。它通过虚拟化的方式,给每个进程分配了一个虚拟处理器(virtual CPUs)。这么做有几个好处

  1. 通过隔离使得计算机支持多用户。
  2. 可以模拟其它型号的处理器来兼容程序。

行程控制块

操作系统利用一个叫行程控制块(Process Control Block, PCB)数据结构来管理所有运行在其环境中的进程。一个用户登陆之后,通常执行的第一个程序是GUI或者是Shell。不同操作系统的行程控制块大致相同,下面这个表格是一个常见的行程控制块。

信息 解释
进程状态 (process_state) 就绪/运行/阻塞
进程ID(process_id) 进程的唯一标识
内存 (memory) 该进程被分配的内存上限,虚拟地址、物理地址等信息
调度情况 (scheduling_information) 该进程的优先级信息
已打开的文件(open_files) 记录了该进程打开的文件
寄存器状态(registers ) 进程上一次在CPU寄存器中的信息,这些信息会在进程运行的时候使用,并且在进程阻塞或者暂停的时候刷新保存
父进程 一个指向父进程的指针
子进程 一个指针指向:一个记录所有子进程的链表的第一个块

行程控制块的管理

有两种行程控制块的管理方式

  1. 用数组直接存行程控制块,这么做的优点是不需要进行动态内存管理,有空位就把控制块放进去就好了;缺点是如果数组不装满的话就浪费空间。

pcb

  1. 用数组装住一系列指针,这些指针会指向相应的行程控制块。这么做可以节省空间,但需要进行动态内存管理。

pointer

注:所谓动态内存管理,意思是需要改变内存的储存结构来添加或删除某个进程块

我做了一个表格,方便复习

review

子进程在PCB中的储存

相对于直接用链表来记录子进程,Linux设计了一种更合适的链表来做这件事情。下面的图中解释了这种不直接用链表的方式。

linked-list

在上面这幅图中,子进程1,2,3是用链表的方式来记录的。当父进程0创建子进程3的时候,它要在链表的末尾添加一个指针来指向子进程3。

no-linked-list

如果我们给所有PCB添加一个信息来指向同辈,链表就显得多余了。当我们给0添加一个子进程3的时候,只需要将它和前面的同辈进程相连就好了。

PCB的管理

操作系统用几个进程表(Process List)管理了所有的进程。等待列表(waiting list)被用于记录那些处在阻塞态中的进程。也就是PCB块中process_stateblocked的进程。(用来记录进程状态信息的元素类型并非string, 通常是用int或者char来记录)

就绪列表(Ready List)被用于记录那些处在就绪态中的进程。process_stateready。就绪列表按照一定的规则给所有的进程进行排序,我后面会把这部分笔记整理出来。就目前来看,我们只要只到这个列表会一直更新,以此来保证所有的进程都能够被照顾到,同时还不影响它们的优先级关系。

进程的终止

当一个进程终止的时候,不同的操作系统有不同的方式来处理它的子进程。有的操作系统会将这些进程的父进程改为初始进程(该进程是所有其它进程的父进程);有的系统会将这个进程的子进程一并删除。

资源控制块

注:这一部分的内容除了在书中出现,没有在别处找到参考,慎重。

操作系统除了行程控制块外,还有一个资源控制块(Resources Control Block)。跟行程控制块一样,不同的操作系统长得大致像下面这个表格一样。

| 信息 | 解释 |
|———- |————————– |
| 资源介绍 | 解释了该资源的属性和用途 |
| 状态 | 当前该资源的使用情况 |
| 等待列表 | 正在等待该资源的进程 |

当一个进程需要使用某个资源的时候,会查看这个资源的使用情况,也就是状态。如果这个资源目前被占用了,系统会把该进程从运行态转成阻塞态,然后在等待列表中添加一个指针指向该进程。当另一个进程完成了该资源的使用后,等待列表中的进程会转成就绪态并且等待操作系统的调度。

线程

前提知识:一种抽象的理解方式

我们上面讲到PCB中有一个叫寄存器状态的值,它会记录进程的程序计数器(Process Counter)以及堆栈指针(Stack Pointer)等保留寄存器的信息。当我们说执行某个进程的时候,我们要在存储设备里面加载这个程序的代码(二进制,也能翻译成汇编)。PC和SP是一个指针。PC指向代码的第一行,而SP则在堆底。随着进程的执行,PC会在代码之间挪动,某些操作(例如调用某些系统服务)需要用到SP,另一些操作需要用到数据。

segments

正题:线程是什么?

一个进程可以将自己某部分的模块单独,并行地运行,这被称之为线程(Threads)。你可以想象一个进程被分开执行。在进程访问资源的时候,有的资源不是必要资源,可以一边等待资源一边执行其它模块。我们可以创建一个线程一直等待该资源。

1
2
3
4
5
6
7
8
9
10
resource;
resource_availability == False;
while(resource_availability != True){
resource_availability = get_resource_state();
if(resource_availability){
resource = getresource();
}
}
result1 = some_calculation();
result2 = some_other_calculation(resource);

上面的伪代码展示了对资源的请求。如果我们的资源一直没有被释放,整个进程就会一直在阻塞态。然而result1并不需要resource来完成。通过创建一个线程,该进程会一边在长久的while循环里等待这个资源,一边获取result1的计算。

1
2
3
4
5
6
7
8
9
10
resource;
resource_availability == False;
while(resource_availability != True){
resource_availability = get_resource_state();
if(resource_availability){
resource = getresource();
result2 = some_other_calculation(resource);
}
}
result1 = some_calculation();

因为调度的原因,我们引入线程这个概念之后也要为其准备一个线程控制块(Threads Control Block,TCB)。当一个线程被创建的时候,PC和SP会被复制到TCB中。所以其实线程和进程一样都有自己的状态(就绪,运行,阻塞)。在进程处理some_calculation();的同时,该线程会在这个while循环中一直转,直到获得资源为止。

根据前提知识里面讲到模型,现在因为多了一个线程的PC和SP,我们可以想象有两个PC和SP在同时作业。

segments_thread

用户级线程和内核级线程

线程分为两种类型,一种是用户级线程(User-level Threads, ULTs),另一种是内核级线程(Kernel-level Threads, KLTs)。可能翻译的误差,这两种线程又叫普通线程和内核线程。普通线程由程序生成,系统内核不会知道这个线程的存在;而内核线程不可以被进程直接访问,需要像创建进程一样用内核调用来获取。

相对来说,普通线程的优点有

  1. 比内核线程更方便管理
  2. 相比内核线程,能创建更多线程
  3. 程序移植到别的平台上不需要太多的更改

当然,缺点也有

  1. 因为系统不知道普通线程的存在,所以如果某一个线程阻塞了,整个进程就进入了阻塞态。
  2. 不能很好的利用多CPU环境,因为底层把它当成了一个单一线程的进程。

更多现代的操作系统会结合两者的优缺点来提升性能。将普通线程映射到内核线程中,这样一来就算是某个普通线程阻塞了,其它线程也能从内核线程池里面找到接口来使用。