This post was written in Chinese, ReadOriginal Post.
Since this post has not been translate to English, Please use the tool provided to translate.

进程的状态

一个进程有三种状态

  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环境,因为底层把它当成了一个单一线程的进程。

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