1.1理解分时技术
随着计算器处理能力的逐步提高,计算机处理多道程序成为了可能。
所谓分时技术,就是把处理器的运行时间分成很短的时间片,按时间片轮流把处理器给各程序使用。这样在时间线上表现为线性,但是在体感上感觉是一起执行,好像每个程序都在独占计算机有一样。主要应用于个人PC与服务器。
区别与实时系统,就是在没有获得程序结果之前,只执行本程序。这种一般应用于军事实时控制,要求计算机对于外来信息能以足够快的速度进行处理,并在对被控对象允许时间范围内作出快速响应。
分时技术 | 实时技术 | |
---|---|---|
支持多程序 | 是 | 否 |
速度 | 相对慢 | 相对快 |
2.1认识进程
计算机的资源(内存,CPU,总线等)是有限的,如何充分,有效的利用系统资源,进程的提出给出了答案。
进程是为了提高CPU的执行效率,减少等待带来的CPU空转,以及其他计算机软硬件资源的浪费而提出。进程是为了完成用户任务所需要的一次执行过程以及分配资源的基本单位。
2.1.1并发与并行
并发(concurrent):是指单核CPU的在宏观上多个程序或任务在同时运行,而在微观上这些程序交替执行,可以提高系统的资源利用率和吞吐量。
并行(parallel):多核 CPU 的每个核心都可以独立地执行一个任务,而且多个核心之间不会相互干扰。在不同核心上执行的多个任务,是真正地同时运行,这种状态就叫做并行。
2.1.2定义进程
并发执行的程序,在执行过程中分配和管理系统资源的基本单位
与程序的区别:
- 程序是静态概念,表示指令的集合,进程是动态概念,强调执行过程,动态创建,并被调度执行后消亡。
- 进程具有并发特征
- 进程是竞争计算机系统资源的基本单位,
- 不同的进程可以包含同一程序,只是程序对应的数据集不一样而已
2.1.3进程的描述
上图所示,进程的描述主要包括:进程控制块(PCB),程序段以及程序段对其进行操作的数据结构集。
2.1.4进程状态转换
一个进程在运行期间,不断地从一种状态转换到另一种状态,它可以多次处于就绪状态和执行状态,也可以多次处于阻塞状态。
-
就绪→执行
处于就绪状态的进程,当进程调度程序为之分配了处理机后,该进程便由就绪状态转变成执行状态。 -
执行→就绪
处于执行状态的进程在其执行过程中,因分配给它的一个时间片已用完或更高优先级的进程抢占而不得不让出处理机,于是进程从执行状态转变成就绪状态。 -
执行→阻塞
正在执行的进程因等待某种事件发生而无法继续执行时,便从执行状态变成阻塞状态。 -
阻塞→就绪
处于阻塞状态的进程,若其等待的事件已经发生,于是进程由阻塞状态转变为就绪状态。 -
运行→终止
程序执行完毕,撤销而终止
2.1.5进程通信
进程间通信(Inter-Process Communication,IPC)是指在不同进程之间传递数据或信号的机制。由于进程是操作系统中独立执行的单元,它们拥有各自独立的内存空间,因此不能直接访问彼此的内存。
2.1.5.1为什么要有进程间通信
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
2.1.5.2进程间通信的方式
单机系统中主要有如下几种方式:
- 主从式
主进程可以自由的使用从进程的资源与数据,从进程受主进程控制,主从关系固定 - 会话式
通信进程双方分别称为使用进程与服务进程,其中使用进程调度服务进程提供的服务(比如用户进程与磁盘进程) - 消息或邮箱机制
无连接,无论接收进程是否准备收消息,发送进程都将把消息发送到缓冲区或者邮箱。 - 共享内存
共享内存要求不移动数据,相互交换信息的进程通过对同一共享数据区的操作完成通信。
2.1.5.3进程间通信的方式举例
-
管道
通常指无名管道,是 UNIX 系统IPC最古老的形式。
特点:
<1> 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端,管道传输数据是单向的,如果想相互通信,需要建立两个管道
<2> 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
<3> 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。单个进程中的管道几乎没有任何用处。所以,通常调用 pipe 的进程接着调用 fork,这样就创建了父进程与子进程之间的 IPC 通道。速度慢,容量有限,只有父子进程能通讯
-
FIFO
FIFO,也称为命名管道,它是一种文件类型。
特点:
<1> FIFO可以在无关的进程之间交换数据,与无名管道不同。
<2> FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。FIFO的通信方式类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性。在数据读出时,FIFO管道中同时清除数据,并且“先进先出”。任何进程间都能通讯,但速度慢
-
消息队列
消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
特点:
<1> 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级
<2> 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除
<3> 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
-
共享内存
-
信号量
信号量(semaphore) :与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
特点:
<1> 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存
<2> 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作
<3> 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数
<4> 支持信号量组不能传递复杂消息,只能用来同步
-
socket
共享内存(Shared Memory):指两个或多个进程共享一个给定的存储区。
特点:
<1> 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
<2> 因为多个进程可以同时操作,所以需要进行同步。
<3> 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
<4> 能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存
3.1线程
3.1.1 认识线程
由前面的内容可知,创建一个进程要花费较大的系统开销以及占用较多的系统资源,但是现实场景,存在很多不确定,以及随机创建的需求(如:服务器用进程的办法来处理每个用户请求),用进程来管理用户请求的方法会极大限制服务器同时响应的用户数。
为了减少进程切换和创建带来的开销,提高执行效率和节约资源,引入了线程的概念。
- 线程是进程的一部分
- 线程是CPU调度的基本单位
- 一个没有线程的进程可以认为是单线程的
- 多线程,程序的执行过程不再是唯一线性的
3.1.2 线程控制块(TCB)
一个线程对应着一个TCB(Thread Control Block),叫做线程控制模块,控制着线程的运行和调度。
TCB组成:
1、threadID:线程的唯一标识。
2、status:线程的运行状态
3、register:线程关于CPU中寄存器的情况
4、PC程序计数器:线程执行的下一条指令的地址
5、优先级:线程在操作系统调度的时候的优先级
6、线程的专属存储区:线程单独的存储区域
7、用户栈:线程执行的用户方法栈,用来保存线程当前执行的用户方法的信息
8、内核栈:线程执行的内核方法栈,用来保存线程当前执行的内核方法信息。
3.1.3 线程类型
3.1.3.1 用户级线程(user-level thread)
管理过程由用户程序完成,操作系统只对进程进行管理
特点:
- 操作系统提供一个用户空间的线程库,功能:创建、调度和撤销线程功能,线程间通信、线程的执行以及存储线程上下文功能
- 调度算法和过程由用户指定,与操作系统内核无关。在用户线程系统中,操作系统内核的调度单位仍是进程
- 调度算法只进行线程上下文切换,不进行处理机切换,切内核不参与
3.1.3.1 内核级线程(kernel-level thread)
由操作系统内核进行管理,操作系统提供给应用程序响应的系统调用和应用程序接口(API),以使用户程序可以创建、执行和撤销线程
特点:
- 内核线程由操作系统内核统一管理,可以被调度到不同的处理机上并行执行
- 内核程序的上下文切换时间要大于用户级线程
操作 | 用户级线程 | 内核级线程 | 进程 |
---|---|---|---|
Null Fork | 34 | 948 | 11300 |
信号等待 | 37 | 441 | 1840 |
上面是VAX机的单处理机用UNIX系统测试的结果
3.1.3 线程的执行特性
3.1.3.1 线程各个状态说明
新建(New):当线程对象被创建但还没有调用start()方法时,线程处于新建状态。
就绪(Runnable):一旦线程调用了start()方法,它进入就绪状态。线程处于就绪状态时,表示它已经准备好被JVM调度执行,只是还没有得到CPU的时间片。
- 就绪状态只是说你自个儿运行,调度程序没有挑选到你,你就永远是就绪状态。
- 调用线程的start()方法,此线程进入就绪状态。
- 当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
- 当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
- 锁池里的线程拿到对象锁后,进入就绪状态。
运行(Running):当线程获得CPU时间片并开始执行时,它进入运行状态。
阻塞(Blocked):线程因为某些原因放弃CPU使用权,暂时停止运行,进入阻塞状态。比如线程在等待某个资源,或者调用了sleep()、wait()等方法。
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)之前时的状态。
等待(Waiting):线程进入等待状态,表示它正在等待其他线程做出一些特定动作(通常是通知或中断)。
调用sleep或是wait方法后线程处于WAITING状态,等待被唤醒。
超时等待(Timed Waiting):线程在等待指定的时间后会自动转为就绪状态。
调用sleep或是wait方法后线程处于TIMED_WAITING状态,等待被唤醒或时间超时自动唤醒。
终止(Terminated):线程执行完任务后或者因异常退出了run()方法,进入终止状态。
当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
3.1.3.1 线程状态之间的转换
NEW到RUNNABLE 状态
线程从NEW状态到RUNNABLE状态的转换通常发生在调用start()方法后。当线程对象被创建时,处于NEW状态,一旦调用start()方法,线程就会进入就绪(RUNNABLE)状态。在就绪状态下,表示线程已经准备好被JVM调度执行,只是还没有得到CPU的时间片。一旦获得CPU时间片,线程就会进入运行状态,开始执行其对应的run()方法中的代码。
RUNNABLE与BLOCKED 的状态转换
线程从RUNNABLE状态到BLOCKED状态的转换通常发生在线程等待获取某个锁资源时。当一个线程在运行过程中,需要访问一个被其他线程持有的锁时,它会进入BLOCKED状态。这可能是因为其他线程已经获取了该锁,并且当前线程需要等待其他线程释放锁才能继续执行。
具体来说,当一个线程在尝试获取一个锁时,如果锁已经被其他线程持有,那么当前线程将由RUNNABLE状态转换为BLOCKED状态。一旦其他线程释放了锁,当前线程将重新进入就绪状态,等待系统分配CPU资源,然后再次尝试获取锁并进入运行状态。
这种状态转换通常涉及多线程的并发操作,开发者需要合理地设计和管理锁资源,以避免线程因争夺锁而频繁进入BLOCKED状态,影响程序的性能和并发效率
RUNNABLE与WAITING 的状态转换
线程从RUNNABLE状态到WAITING状态的转换通常发生在调用了Object类的wait()方法或Thread类的join()方法时。
当一个线程调用了Object类的wait()方法后,该线程会释放占有的锁并进入WAITING状态,等待其他线程通过notify()或notifyAll()方法来唤醒它。在WAITING状态下,线程不会争夺CPU资源,只有当其他线程显式地唤醒它后,才有机会重新进入就绪状态。
另外,当一个线程调用了Thread类的join()方法后,它会等待被调用线程执行完毕。在调用线程的join()方法后,调用线程会进入WAITING状态,直到被调用线程执行完毕才会重新进入就绪状态。
需要注意的是,线程从WAITING状态到RUNNABLE状态的转换是由其他线程显式地唤醒或被调用线程执行完毕触发的,而不是自动发生的。这种状态转换多用于线程间的协作和同步操作,通过合理地使用wait()、notify()、join()等方法可以实现线程间的通信和协调。
RUNNABLE到TIMED TERMINATED 状态
线程从RUNNABLE状态到Timed Waiting状态的转换通常发生在调用了带有时间限制的等待方法,如Thread类的sleep(long millis)方法或Object类的wait(long timeout)方法。
当一个线程调用sleep(long millis)方法后,线程会进入Timed Waiting状态。在指定的时间段内,线程不会争夺CPU资源,而是处于休眠状态。一旦指定的时间过去,线程会重新进入就绪状态,等待系统分配CPU资源并继续执行。
另外,当一个线程调用了Object类的wait(long timeout)方法后,线程会释放占有的锁并进入Timed Waiting状态,等待其他线程通过notify()或notifyAll()方法来唤醒它,或者等待指定的时间段过去。如果指定的时间段内没有其他线程唤醒它,那么线程会自动重新进入就绪状态。
需要注意的是,线程从Timed Waiting状态到RUNNABLE状态的转换是由时间限制或其他线程的唤醒操作触发的。在Timed Waiting状态下,线程会等待一段时间或者被其他线程显式地唤醒后,才有机会重新进入就绪状态。
这种状态转换通常用于线程间的时间控制和同步操作,通过合理地使用sleep()、wait()等方法可以实现线程的定时等待和协调。
RUNNABLE到TERMINATED 状态
线程从RUNNABLE状态到TERMINATED状态的转换通常发生在线程执行完任务或由于异常退出时。
当一个线程处于RUNNABLE状态并且执行完了其run()方法中的代码,即任务完成时,线程会自动进入TERMINATED状态。在TERMINATED状态下,线程已经结束执行,不再具有任何运行状态。
另外,如果线程在执行过程中发生了未捕获的异常导致线程提前退出,也会使线程从RUNNABLE状态转换为TERMINATED状态。
需要注意的是,TERMINATED状态是最终状态,一旦线程进入该状态,就无法再回到其他状态。如果需要重新执行线程任务,必须创建一个新的线程对象并启动。
控制线程状态的转换是由JVM和操作系统来管理的,开发者可以通过合理的编码和使用线程控制方法来影响线程状态的转换,确保线程能够按照预期执行和完成任务。
3.1总结
- 线程的改变,只代表了CPU执行过程的改变,进程所拥有的资源不变
- 计算机软硬件的分配与线程无关,只共享它所属进程的资源
- 线程也有自己的线程控制块(TCB),信息远少于进程的PCB
- 进程是系统中所有资源分配的基本单位
- 进程拥有一个完整的地址空间
- 线程是进程的一部分,没有自己的地址空间,它和进程内的其他进程一起共享分配给该进程的所有资源