一、操作系统的功能
①计算机系统可以粗分为四个组件:硬件(中央处理器(CPU)、内存(memory)、输入输出设备(I/O)),操作系统,应用程序,用户
②操作系统是一个多道程序执行,保证cpu不会空闲
多道程序设计技术是在计算机主存中同时存放几道相互独立的程序,它们在操作系统控制之下,相互穿插的运行。
多道程序运行的特征:
1、多道:计算机主存中同时存放几道相互独立的程序。
2、宏观上并行:同时进入系统的几道程序都处于运行过程中,即它们都开始运行,但都未运行完毕。
3、微观上串行:从微观上看,主存中的多道程序轮流或分时地占有处理机,交替运行。
③用户模式和内核模式:为了保证操作系统能正常运作,必须区分操作系统代码和用户代码的执行,所以计算机硬件通过一个模式位来表示当前模式:内核模式(0)和用户模式(1)
一般用户程序是运行在用户态,而对于一些外设的调用一般采用的是内核态。但当有需要的时候就会引起用户态到内核态的切换。一般有以下几种情况会引起用户态到内核态的切换:系统调用,异常,外围设备的中断。
对于在java中的synchronized如果升级为重量级锁时,多个线程争用一个线程会引起线程阻塞,这就会导致线程从用户态切换到内核态。而线程切换是很消耗cpu时间的。因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。参考链接
④内存存储器是cpu唯一一个能直接访问的存储器。 所以当我们要对硬盘等外设的存储信息进行访问时,首先要将这些信息加载到内存中,才能进行访问和处理。而为了提高效率,在内存和cpu(硬件寄存器)之间添加了高速缓存。在运行时cpu首先会检查高速缓存中是否存在所需资源。当没有时才会访问内存。
硬盘---->内存---->高速缓存--->硬件寄存器
----------------------------------------------------->
数据复制
<------------------------------------------------------
写回
二、进程管理
进程的概念:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础.(应注意的是一个程序本身不是一个进程,程序是一个静态体,而进程是一个动态体,只要在运行中的程序才能算是进程。)
线程的概念:线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
①进程的状态和线程的状态
进程状态
创建状态:进程在创建时需要申请一个空白PCB,向其中填写控制和管理进程的信息,完成资源分配。如果创建工作无法完成,比如资源无法满足,就无法被调度运行,把此时进程所处状态称为创建状态
就绪状态:进程已经准备好,已分配到所需资源,只要分配到CPU就能够立即运行
执行状态:进程处于就绪状态被调度后,进程进入执行状态
阻塞状态:正在执行的进程由于某些事件(I/O请求,申请缓存区失败)而暂时无法运行,进程受到阻塞。在满足请求时进入就绪状态等待系统调用
终止状态:进程结束,或出现错误,或被系统终止,进入终止状态。无法再执行
线程状态
详细信息参考本篇文章
②进程调度
前面提到多道程序设计的目标是,无论何时都有进程运行,从而最大化CPU利用率。
所以这里就要涉及到CPU对线程的调度了。分时系统的目的是在进程之间快速切换CPU,以便用户在程序运行时能与其交互。为了满足这些目标,进程调度器选择一个可用进程到CPU上面执行。如果只要但CPU而又多个进程。则剩下的进程就需要等待CPU空闲才能重新调度。而为了管理进程被CPU调度就需要用到调度队列。调度队列一般采用的是链队列。
最初一个进程被加入到就绪队列,它在就绪队列中等待,知道被选中或者被分派。当该进程分配到CPU并执行时,可能发生以下事件:
1)进程可能发出I/O请求,并被放到I/O队列。
2)进程可能创建一个子进程,并等待其终止
3)进程可能由于中断而被强制释放CPU,并被放到就绪队列。
由上可以看出在进程整个生命周期中,会在各种调度队列之间迁移。通常,大多数进城可分为:I/O密集型(执行I/O比执行计算需要花费更多时间),CPU密集型(很少产生I/O)
③进程间通信和线程间的通信方式
进程间的通信(IPC)
1)共享内存系统[
采用共享内存的进程间通信,需要通信进程建立共享内存区域。共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
例如生产者-消费者问题,采用共享内存的方法,为了允许生产者进程和消费者进程并发执行,应用一个可用的缓冲区。而这个缓冲区可用(信号量来控制),这个缓冲区驻留在生产者进程和消费者进程的共享内存区域内。
2)消息传递系统:(分布式进程)
允许进程不必通过共享地址空间来实现通信和同步。消息传递工具提供至少两种操作:
send(message) receive(message)
而消息传递有直接通信和间接通信两种方式
<1>直接通信:
对称性通信,既通信的每个进程必须明确指定通信的接受者和发送者,通俗点说就是只能两个人一对一进行沟通。
非对称通信:发送者必须指定接受者,但是接受者可以接受到对个人的消息,具体要接受哪个,我可以自己修改。通俗点讲就是每个人心中有一个女神,但是女神可以选择跟谁在一起。
<2>间接通信(通过邮箱或者端口来发送和接受信息)
每个进程可以建立多个邮箱,而每个邮箱都有唯一的一个标识符。只有当发送者的邮箱标识符和接受者的邮箱标识符一致的时候才能进行通信。这样可以保证一个进程和多个进程进行通信。
对于发送者(客户端)和接受者(服务端)的通信会有阻塞/非阻塞和同步/异步的问题,而这两者如何区别呢:
同步/异步主要针对客户端:
同步:就是当客户端发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是说必须一件一件的事情去做,等一件做完了才能去做下一件。
异步:就是当客户端发出一个功能调用时,调用者不用等接收方发出响应。实际处理这个调用的部件在完成后,会通过状态,通知和回调来通知调用者。客户端可以接着去做 后面的事情。
虽然主要是针对客户端,但是服务器端不是完全没有关系的,同步/异步必须配合服务器端才能实现。同步/异步是由客户端自己控制,但是服务器端是否阻塞/非阻塞,客户端完全不需要关心。
阻塞/非阻塞主要是针对服务器端:
阻塞:阻塞调用是指服务器端被调用者调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞:指在不能立即得到结果之前,该调用不会阻塞当前线程。
3)套接字(分布式进程,低效)
通过网络通信的每对进程需要使用一对套接字。即每个进程各有一个,每个套接字由一个IP地址和一个端口号组成。服务端通过监听指定端口来等待客户请求。
java中提供了三种不同类型的套接字,但主要是面向连接的套接字(TCP)和面向无连接的套接字(UDP)。
4)远程过程调用(RPC):(分布式进程,高效(RMI(基于JAVA语言)、Hessian(HTTP协议传输)、Dubbo(序列化传输,TCP连接,底层用netty(netty底层是NIO)))注册中心用的zookeeper)
5)管道
管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
普通管道:生产者只能从一端写,消费者只能从一段读
命名管道:通信可以是双向的。
线程间的通信
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。
参考JDK并发包
Linux系统中的线程间通信方式主要以下几种:
-
锁机制:包括互斥锁、条件变量、读写锁和自旋锁。
互斥锁确保同一时间只能有一个线程访问共享资源。当锁被占用时试图对其加锁的线程都进入阻塞状态(释放CPU资源使其由运行状态进入等待状态)。当锁释放时哪个等待线程能获得该锁取决于内核的调度。
读写锁当以写模式加锁而处于写状态时任何试图加锁的线程(不论是读或写)都阻塞,当以读状态模式加锁而处于读状态时“读”线程不阻塞,“写”线程阻塞。读模式共享,写模式互斥。
条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
自旋锁上锁受阻时线程不阻塞而是在循环中轮询查看能否获得该锁,没有线程的切换因而没有切换开销,不过对CPU的霸占会导致CPU资源的浪费。 所以自旋锁适用于并行结构(多个处理器)或者适用于锁被持有时间短而不希望在线程切换产生开销的情况。
信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
信号机制(Signal):类似进程间的信号处理
线程
关于线程的大多内容和跟进程的区别在前面已经提到很多了。
每个线程是CPU使用的一个基本单元,它包括线程ID、程序计数器、寄存器组和堆栈。它与同一个进程的其他线程共享代码段、数据段和其他操作系统资源。
①多线程模型
有两种不同的方法来提供线程支持:用户层的用户线程和内核层的内核线程。用户线程位于内核线程之上。而这两者存在着多种联系方式:多对一模型、一对一模型、多对多模型。关于这方面的文章看这里线程的三种实现方式
进程同步
①临界区问题:进程能同步首先要满足临界区的问题。当一个进程在临界区内执行时,其他进程不允许在他们的临界区内同时执行。它满足三个条件:互斥,进步和无线等待。(这个临界区跟线程的临界区差不多类似)
②同步方法:
硬件同步(test_and_set,compare_and_swap(java中并发非常重要的一个知识))
软件同步(peterson解决方案)
互斥锁(mutex)
信号量(semaphore):会出现哲学家就餐问题导致死锁
管程:(解决了信号量错误使用带来了很多问题)在java中就是objectMonitor(synchronized),在java的每个对象都有一个单独的锁。通过monitorenter和monitorexit来控制进入加锁的代码块具体可以看synchronized是什么
而且有必要强调是管程是一种思想。
③死锁:参考文章
CPU调度
在支持线程的操作系统,操作系统实际调度的是内核级线程而非进程。
①调度的准则:从以下几种来考虑一个调度算法的效率
CPU使用率
吞吐量
周转时间
等待时间
响应时间
②调度算法:(CPU调度处理的问题是:从就绪队列中选择一个以便为其分配CPU)
<1>先到先服务调度(FCFS)
先请求CPU的进程首先分配到CPU,但是缺点是平均等待时间很长。比如先到的进程需要运行的时间很长,而后面需要运行的进程时间很短,这样就会导致后面的长时间等待,所得到的的平均等待时间很长。
<2>最短作业优先(SJF)
通常用上次的执行时间来预测作为下次要执行的时间。有非抢占式的和抢占式的。非抢占式的就是当一个进程在执行时,这个时候来了一个优先级比较低的他会等待当前进程执行完毕,而抢占式则不会等待,而是直接抢过来。
<3> 优先级调度(PS)
优先级高的优先执行。但是会导致进程饿死。解决方法就是老化,将长时间没有执行的进程老死算了。
<4>轮转调度(RR)
采用的是一种循环的FCFS,但是给每个进程分配一定量的时间片,当时间片执行完了换另外一个进程执行。但是要对这个时间片的设置要拿捏好,过大就变成FCFS,过小会导致大量的上下文的切换。
<5>其他调度算法
多级队列调度:进程分为前台组和后台组。前台使用FCFS,后台使用RR。
多级反馈队列调度
内存管理
①内存
内存由一个很大的字节数组来组成,每个字节都有各自的地址。cpu根据程序计数器的值从内存中提取指令。这些指令可能会引起对特定内存地址的额外加载与存储。
<1>进程地址
在内存中,每个进程都有自己的基地址----界限地址。一般情况下程序是作为二进制文件存放在磁盘等外存上面,为了执行,程序应被调入内存,并放在进程中(在这中间会检查高速缓存是否存在),在磁盘上等待调到内存以便执行的进程形成了输入队列。这种调度涉及到调度算法。
硬盘---->内存---->高速缓存—>硬件寄存器
----------------------------------------------------->
数据复制
<------------------------------------------------------
写回
<2> 内存分配
内存分配应容纳操作系统和用户各种进程(对比我们自己的电脑安装w7),内存通常分为两个区域,操作系统分在低内存或者高内存
1)分区:将内存分为固定大小的区,每个分区只包含一个进程,在操作系统中有一个表用来记录内存哪个区可用和已用。但是这种分配方式会存在碎片问题,因为进程所需的空间并不一致。
2)分段:逻辑地址空间是由一组段组成,每个段包含(段号,偏移)
3)分页:将物理内存分为固定大小的块,称为帧或者页帧。而将逻辑内存也分为同样大小的块,称为页或者页面。当需要执行一个进程时,它的页从文件系统或者备份存储等源处,加载到内存的可用帧。
<3>页表结构
前面提到的内存分配的逻辑地到物理地址的映射都要用到一个页表保存地址。那么这个页表有哪些组织形式呢?主要有:分层分页,哈希页表,倒置页表。
②虚拟内存
前面说的内存管理策略,所有这些策略的目标都是将多个进程保存在内存中,以便允许多道程序,但是我们的内存是有限的,而且是弥足珍贵的。所以就有了虚拟内存技术允许进程不必完成处于内存。它使程序不必再受内存的大小所影响。
<1>请求调页:(实现请求调页必须解决两个问题:页面置换算法和帧分配)
在前面一章的策略中我们需要将整个进程加载到内存中,但是有时候我们并不需要整个进程全部加载,我们只需要需要运行的部分加载,这就需要用到请求调页技术。页面只有在程序执行期间被请求时才被加载。不然都不会加载到内存中。
(1)页面置换
当内存分配达到饱和,发现空闲帧列表上面没有空闲帧,这时可以终止进程,但是这并不是最佳,因为我们使用请求调页就是为了来改善CPU的,这时候我们可以考虑另外一种方案:页面置换。我们可以通过置换出一个进程释放它的帧来给另外一个进程使用。
常见页面置换算法
基本页面置换步骤
1、找到所需页面的磁盘位置(虚拟内存)
2、找到一个空闲帧:
a、如果有,就使用它
b、如果没有,就使用页面置换算法来牺牲一个帧
c、将牺牲帧内容写到磁盘上,修改对应的页表和帧表
3、将所需页面读入空闲帧,修改页表和帧表
4、从发生缺页错误位置,继续用户进程。
如果没有空闲帧,那么就查找当前不在使用的一个帧,并释放它。
1)FIFO页面置换算法
这种算法为每个页面记录一个调到内存的时间,当必须置换页面时选择一个最旧的页面进行置换,当然实际可使用一个队列,每次置换的是队头的元素即可。调入页面添加到队尾。但是缺页(调用时发现页表上面没有就是缺页)错误率比较高,还会出现Belady异常。
2)最优页面置换算法
置换最长不会使用的帧,就是要预先知道未来哪个帧不会用到,这个虽然能最大化的降低缺页错误率但是很难实现,因为我们无法知道未来的串是怎么样的。
3)LRU页面置换算法
最近最少使用算法,如果说最优页面置换算法是预测未来的,那么LRU则是根据以前的出现来判断的。可以采用一个双向链表或者计数器来实现这个最近最少使用。
局部置换和全局置换
局部置换是一个进程不会去争夺另一个进程的帧,而全局置换会去争取
(2)帧分配
平均分配和比例分配,
<2>系统抖动
当一个进程的调页时间多于它的执行时间,则表明这个进程在抖动。