读《计算机的心智》,另一个角度看操作系统
关于线程
线程模式: 一个进程至少包含一个线程,也可以包含多个线程。
线程提高多核CPU利用率: 让不同的线程运行在不同的处理器上,提高进程的执行速度。
线程管理: 线程控制表维护线程的各种信息。同一进程内的线程共享一块内存空间。
线程共享与独享资源划分
共享 | 独享 |
---|---|
地址空间,全局变量,打开的文件,子进程,信号… | 程序计数器,寄存器,栈,状态字 |
线程的三种实现方式
内核级线程实现方式(1:1线程模型):
内核线程建立和销毁都是由操作系统负责、通过系统调用完成的。线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只有一个到内核级线程的编程接口.内核为进程及其内部的每个线程维护上下文信息(线程表),调度也是在内核基于线程架构的基础上完成。
一对一线程映射:内核线程驻留在内核空间,它们是内核对象。有了内核线程,每个用户线程被映射或绑定到一个内核线程。用户线程在其生命期内都会绑定到该内核线程。一旦用户线程终止,两个线程都将离开系统。
特点:当某个线程希望创建一个新线程或撤销一个已有线程时,它进行一个系统调用
优势:
- 多处理器系统中,内核能够并行执行同一进程内的多个线程
- 如果进程中的一个线程被阻塞,能够切换同一进程内的其他线程继续执行(用户级线程的一个缺点)
- 所有能够阻塞线程的调用都以系统调用的形式实现,代价很小
- 信号是发给进程而不是线程的,线程可以“注册”它们感兴趣的信号
缺陷:
- 效率低,每次线程切换都需要陷如内核
- 占用系统内核资源多,操作系统需要维护线程表
用户级线程实现方式(1:n线程模型):
有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在. 应用程序可以通过使用线程库设计成多线程程序.
用户级线程仅存在于用户空间中,此类线程的创建、撤销、线程之间的同步与通信功能,都无须利用系统调用来实现。用户进程利用线程库来控制用户线程。由于线程在进程内切换的规则远比进程调度和切换的规则简单,不需要用户态/核心态切换,所以切换速度快。多见于一些历史悠久的操作系统。
用户级线程对于操作系统是不可见的,因此无法被调度到处理器内核。任意给定时刻每个进程只能够有一个线程在运行,而且只有一个处理器内核会被分配给该进程。
库调度器从进程的多个线程中选择一个线程,然后该线程和该进程允许的一个内核线程关联起来。内核线程将被操作系统调度器指派到处理器内核。用户级线程是一种”多对一”的线程映射。
特点:内核对线程包一无所知。从内核角度考虑,就是按正常的方式管理,即单线程进程(存在运行时系统)
优点:
- 可以在不支持线程的操作系统中实现。
- 创建和销毁线程、线程切换代价等线程管理的代价比内核线程少得多, 因为保存线程状态的过程和调用程序都只是本地过程
- 线程能够利用的表空间和堆栈空间比内核级线程多
- 不需要内陷,不需要上下文切换,也不需要对内存高速缓存进行刷新,使得线程调用非常快捷
缺陷:
- 线程发生I/O或页面故障引起的阻塞时,会阻塞整个进程从而阻塞所有线程
- 一个单独的进程内部,没有时钟中断,所以不可能用轮转调度的方式调度线程
- 资源调度按照进程进行,多个处理机下,同一个进程中的线程只能在同一个处理机下分时复用(无法利用计算机多核)
混合型线程(n:m线程模型):
用户态的执行系统负责进程内部的线程在非阻塞时的切换;内核态的操作系统负责阻塞线程的切换,同时实现内核态和用户态线程管理。(用户态线程被多路复用到内核态线程上)
Java线程模型
Java线程的实现
Java中最常用的JVM是Oracle/Sun研发的HotSpot VM。在这个JVM的较新版本所支持的所有平台上,它都是使用内核级线程模型。
这种方式实现的线程,是直接由操作系统内核支持的——由内核完成线程切换,内核通过操纵调度器(Thread Scheduler)实现线程调度,并将线程任务反映到各个处理器上。内核线程是内核的一个分身。程序一般不直接使用该内核线程,而是使用其高级接口,即轻量级进程(LWP),也即线程。
线程调度(Java使用的线程调度方式是抢占式调度)
程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式线程调度(Cooperative Threads-Scheduling)和抢占式线程调度(Preemptive Threads-Scheduling)。
如果使用协同式调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。协同式多线程的最大好处是实现简单,而且由于线程要把自己的事情干完后才会进行线程切换,切换操作对线程自己是可知的,所以没有什么线程同步的问题。缺陷是会出现阻塞问题。
如果使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(在Java中,Thread.yield()可以让出执行时间,但是要获取执行时间的话,线程本身是没有什么办法的)。在这种实现线程调度的方式下,线程的执行时间是系统可控的,也不会有一个线程导致整个进程阻塞的问题。
Java的线程优先级
Java语言一共设置了10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在两个线程同时处于Ready状态时,优先级越高的线程越容易被系统选择执行。Java的线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统,操作系统的 优先级不一定和Java的会一一对应(Linux 1~99,Window 7种),导致Java线程优先级不一定可靠。
Java线程状态转换
- 新建(New):创建后尚未启动的线程处于这种状态。
- 运行(Runable):Runable包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间。
- 无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。以下方法会让线程陷入无限期的等待状态:
没有设置Timeout参数的Object.wait()方法。
没有设置Timeout参数的Thread.join()方法。
LockSupport.park()方法。
- 限期等待(Timed Waiting):处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤 醒。以下方法会让线程进入限期等待状态:
Thread.sleep()方法。
设置了Timeout参数的Object.wait()方法。
设置了Timeout参数的Thread.join()方法。
LockSupport.parkNanos()方法。
LockSupport.parkUntil()方法。
5.阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是:“阻塞状态”在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
6.结束(Terminated):已终止线程的线程状态,线程已经结束执行。
线程同步
线程同步的目的:不管多线程之间的执行如何穿插,运行结果都是正确的。(保证多线程执行下结果的确定性)
多个线程争相执行同一段代码或访问同一资源的现象称为竞争,造成竞争的代码段或者资源称为临界区,通过协调限制任何时刻都只有一个线程在临界区里,称为互斥
线程同步方案
锁
锁的特征:
- 锁的初始状态是打开的
- 进入临界区必须获得锁
- 出临界区必须打开锁
- 如果别人持有锁,必须等待
缺陷:循环等待
Wait-Notify
sleep原语:进入睡眠,释放CPU wakeup原语:发送信号给指定进程
伪代码:
define N 100
int count = 0;
void producer(void){
while(true){
if(count==N) sleep();
//生产商品
count = count + 1;
if(count == 1)wakeup(consumer);
}
}
void consumer(void){
while(true){
if(count==0) sleep();
//获取商品
count = count - 1;
if(count == N -1 ) wakeup(producer);
}
}
缺陷:
- 如果消费者先获取商品,获取不到sleep,但是在sleep语句之前CPU切换,生产者生产商品,发送wakeup给消费者(此时消费者还没进入sleep),当生产者生产完成,进入sleep之后,CPU切换为消费者,此时消费者继续进程Sleep,导致死锁。
2.count变量没有保护,会发生竞争
信号量-semphore
信号量是一个计数器,其取值为当前累积的信号数量,支持两个操作,加法操作up,减法操作down
-
up(一组原子操作):
- 将信号量的值加1,唤醒一个在该信号量上面等待的线程
- 程序继续执行
-
down(一组原子操作):
- 判断信号量的取值是否大于等于1
- true 将信号量的值减去1,继续执行
- false 在该信号量上等待
如果将信号量的取值限制为0和1,等效于锁。因为二元信号量是原子的,等效于锁+Wait-Notify
例子(进程B执行完再执行进程A):
- 进程A 进程B
- down() doSomeThing
- doSomeThing up()
伪代码:
define N 100
typedef int semphore;//定义信号量类型
semphore mutex = 1;//互斥信号量
semphore empty = N;//缓存区计数信号量,空位数量
semphore full = 0;//缓存区计数信号量,商品数量
void producer(void){
while(true){
down(empty);
down(mutex);
//生产商品
up(mutex);
up(full);
}
}
void consumer(void){
while(true){
down(full);
down(mutex);
//获取商品
up(mutex);
up(empty);
}
}
缺陷:程序效率低下
管程monitor(监视器)
管程是一个程序语言级别的构造,它的正确运行由编译器保证。(Java Synchronized)
把需要同步的代码使用管程的构造框起来,即将需要保护的代码置于begin monitor 和 end monitor之间。编译器在识别到monitor包裹的代码块时,在翻译成低级代码时就会将需要的操作系统原语添加上去, 使两个线程不能同时活跃在同一个管程内。
管程的同步机制:锁(互斥) | 条件变量(控制执行的顺序)
缺陷:依赖编译器,只能在单台计算机上生效
锁的实现
线程上下文切换(切换线程)方式
中断启用和禁止来实现锁(繁忙等待)
- 线程自愿放弃CPU(yield之类的操作系统调用)
- 线程被强制放弃CPU而失去控制权(中断实现)
操作系统主要通过周期性时钟中断来获得CPU控制权。要想让一组操作变成原子操作,可以通过禁止中断,并且不自动调用yield之类让出CPU,防止进行切换,将一组操作变为原子操作。
unlock实现
unlock(){
disable interrupts;//启用中断
value = FREE;//value设置为FREE
enable interrupts;//禁止中断
}
测试与设置指令来实现锁(繁忙等待)
测试与设置(test&set)指令 “读——修改——写”
- 设置操作:将x写入指定内存单元
- 读取操作:返回指定内存单元原来的值(写入x之前的值)
lock/unlock实现
//value原始值=0
lock(){
while(test_and_set(value) == 1){
}
}
unlock(){
value = 0
}