操作系统抽象
执行环境
执行环境是应用程序正确运行所需的服务与管理环境,用来完成应用程序在运行时的数据与资源管理、应用程序的生存期等方面的处理。它定义了应用程序有权访问的其他数据或资源,并决定了应用程序的行为限制范围。
随着软件需求的多样化和复杂化,执行环境的层次 也越来越多
- 对于没有操作系统,直接运行在裸机上的应用程序而言,其执行环境是 计算机的硬件
- 对于不需要直接访问硬件,有函数库的时期,应用程序的执行环境就变成了 函数库 -> 计算机硬件。而这时函数库的执行环境就是计算机的硬件
- 再进一步,操作系统取代了函数库来访问硬件,函数库通过访问操作系统的系统调用服务来进一步给应用程序提供丰富的功能和资源。这时,应用程序的执行环境就变成了 函数库 -> 操作系统内核 -> 计算机硬件
- 对于基于 java 语言的应用程序,多了一层 java 虚拟机,此时 java 应用程序的执行环境为:函数库 -> java 虚拟机 -> 操作系统内核 -> 计算机硬件
- 在云计算时代,在传统操作系统与计算机硬件之间多了一层 Hypervisor/VMM,此时应用程序的执行环境为: 函数库 -> java 虚拟机 -> 操作系统内核 -> Hypervisor/VMM -> 计算机硬件。
在 CPU 执行过程中,可以在不同层次的执行环境之间切换,这称为 执行环境切换。执行环境的切换主要是通过特定的 API 或 ABI 来完成的。这样不同执行环境的软件就能实现数据交换与交互操作,而且还保证了彼此之间有清晰的隔离。
对于应用程序的执行环境而言,应用程序只能看到执行环境提供给它的接口( API 或 ABI),这使得应用程序所得到的服务取决于执行环境提供给它的访问接口。
普通控制流
各种应用程序在执行环境中执行其功能,而具体如何执行,取决于 程序的控制流
在应用程序视角下,它只能接触到它所在的执行环境而不会跳到其他执行环境,所以应用程序执行基本上是以 普通控制流的形式完成整个运行的过程。
普通控制流——最简单的一种控制流(在没有异常或中断产生的前提下)是一个“平滑”的序列,其中每个要执行的指令地址在内存中都是相邻的。
异常控制流
应用程序在执行过程中,如果发出系统调用请求,或出现外设中断、CPU 异常等情况,处理器执行的前一条指令和后一条指令将会位于不同的执行环境。比如,前一条指令还在应用程序的代码段中,后一条指令就跑到操作系统的代码中去了,这就是一种控制流的“突变”,即控制流脱离了其所在的执行环境,并产生了 执行环境的切换。—— 异常控制流
控制流上下文(执行环境的状态)
控制流在执行完某指令时的物理资源内容,即 确保下一时刻能继续 正确 执行控制流指令的物理资源内容称为控制流的上下文。
- 物理资源:计算机硬件资源,如 CPU 的寄存器、可访问的物理内存等
- 虚拟资源:操作系统提供的资源,如文件,网络端口号,网络地址,信号等
对于异常控制流的上下文保存与恢复,主要通过 CPU 和操作系统(手动编写在栈上保存与恢复寄存器的指令)来协同完成;对于函数转移控制流的上下文保存与恢复,主要通过编译器(自动生成在栈上保存与恢复寄存器的指令)来帮助完成。
在操作系统中,需要处理三类 异常控制流:外设中断,陷入和异常
异常控制流:中断
外设 中断 由外部设备引起的外部 I/O 事件如时钟中断、控制台中断等。外设中断是异步产生的,与处理器的执行无关。产生中断后,操作系统需要进行中断处理来响应中断请求,这会破坏被打断前应用程序的控制流上下文,所以操作系统要保存与恢复被打断前应用程序的控制流上下文。
异常控制流:异常
异常是在处理器执行指令期间检测到不正常的或非法的内部事件(比如除零错、地址访问越界)。产生异常后,操作系统需要进行异常处理,这会破坏被打断前应用程序的控制流上下文,所以操作系统要保存与恢复被打断前应用程序的控制流上下文。
如果是应用程序产生的不可恢复的异常,操作系统有权直接终止该应用程序的执行。
异常控制流:陷入
陷入是程序中使用请求操作系统服务的系统调用而引发的有意事件。产生陷入后,操作系统需要执行系统调用服务来响应系统调用请求,这会破坏陷入前应用程序的控制流上下文,所以操作系统要保存与恢复陷入前应用程序的控制流上下文
进程
站在应用程序自身的角度,进程是 一个正在运行的程序实例。在计算机系统中,我们可以“同时”运行多个程序,这个 “同时”,其实是操作系统给用户造成的一个“幻觉”。
站在计算机系统和操作系统的角度来看,其实,进程是应用程序的一次执行过程。并且在这个执行过程中,由操作系统执行环境来管理程序执行过程中的进程上下文。
处理器是计算机的硬件资源。为了提高处理器的利用率,操作系统需要让处理器足够忙,即让不同的程序轮流占用处理器来运行。如果一个程序因某个事件而不能运行下去,就通过进程上下文切换把处理器占用权转交给另一个可运行程序。
一个进程是一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。
地址空间
地址空间是对物理内存的虚化和抽象,也称虚存。
它就是操作系统通过处理器中的内存管理单元硬件的支持而给应用程序和用户提供一个大的(可能超过计算机中的物理内存容量)、连续的(连续的地址空间编址)、私有的_(其他程序无法破坏的_)的储存空间。这需要操作系统将内存和外村(即持久存储,硬盘是一种典型的外村)结合起来管理,为用户提供一个容量比实际内存大得多得虚拟存储器,并且需要操作系统为应用程序分配内存空间,使用户存放在内存中的程序和数据彼此隔离、互不侵扰。
操作系统中的虚拟管理与处理器的 MMU 密切相关,在启动虚存机制后,软件通过 CPU 访问的每个虚拟地址都要通过 CPU 中的 MMU 转换为一个物理地址来进行访问。
文件
文件主要对持久存储的抽象,并进一步扩展为外设的抽象。
文件可以理解为存放在持久存储介质(比如硬盘、光盘、U 盘)上,方便应用程序和用户读写的数据。
以磁盘为代表的持久存储介质的数据访问单元是一个扇区或一个块,而在内存中的数据访问单元是一个字节或一个字。操作系统通过文件来屏蔽磁盘与内存差异,尽量以内存的读写方式来处理持久存储的数据。当处理器需要访问文件中的数据时,可通过操作系统把它们装入内存。
文件管理的任务是有效地支持 文件的存储、检索、和修改等操作。
操作系统的特征
- 虚拟化
- 并发性
- 异步性
- 共享性
- 持久性
虚拟化
内存虚拟化
在写应用程序时,我们不用考虑其程序的起始内存地址要放在计算机内存的某个位置,而是用字符串定义了各种变量和函数,直接在代码中便捷地使用这些符号就行了。
这是由于操作系统建立了一个 地址固定、空间巨大的虚拟内存给应用程序来运行,这是 内存虚拟化,其实是一种 空间虚拟化,可进一步细分为 内存地址虚拟化 和 内存大小虚拟化。
实际上,编译器和链接器也不知道程序每个符号对应的地址应该放在未来程序运行时的哪个物理内存中。所以,编译器的一个简单处理方法就是,设定一个固定地址作为起始地址开始存放代码,代码之后是数据,所有变量和函数的符号都在这个起始地址之后的某个固定偏移位置。假定程序每次运行都是位于一个不会变化的起始地址。这里的变量指的是全局变量,其地址在编译链接后会确定不变。但局部变量是放在堆栈中的,会随着堆栈大小的动态变化而变化。这里编译器产生的地址就是虚拟地址。
当程序要运行时,这个符号所对应的虚拟内存地址到计算机的物理内存地址的映射必须要解决。操作系统会把编译器和链接器生成的执行代码和数据放在空闲的物理内存中,并建立虚拟地址到物理地址的映射关系。由于物理内存中的空闲区域是动态变化的,这导致虚拟地址到物理地址的映射关系也是动态变化的,需要操作系统来维护好可变的映射关系,确保编译器 “固定起始地址”的假设成立。
只有操作系统维护好这个映射关系,才能让程序员只需写一些易于人理解的字符串符号来代表一个内存空间地址,且编译器只需确定一个固定地址作为程序的起始地址就可以不用考虑将来这个程序要在哪里运行的问题,从而实现了 内存地址虚拟化
应用程序在运行时不用考虑当前物理内存是否够用。如果应用程序需要一定的空间,但由于在某些情况下,物理内存的空闲空间可能不多了,这时操作系统通过把物理内存中最近没使用的空间 (不是空闲的,只是最近用得少)换出到硬盘上暂时缓存起来,这样空闲空间就大了,就可以满足应用程序的运行时内存需求了,从而实现了 内存大小虚拟化CPU 虚拟化
不同的应用程序可以在内存中并发运行,相同的应用程序也可有多个拷贝在内存中并发运行。而每个程序都认为 “自己独占了 CPU 在运行”,这是 “CPU 虚拟化”,也是一种 “时间虚拟化”。操作系统给了运行的应用程序一个幻象,即操作系统把时间分成小段,每个应用程序占用其中一小段时间片运行,用完这一时间片后,操作系统会切换到另一个应用程序,让它运行。由于时间片很短,操作系统的切换开销也很小,应用程序或使用应用程序的用户基本上是看不出来的,反而感觉多个程序各自在独立 并行执行,从而实现了 CPU 虚拟化。
- 并行( Parallel ) 是指两个或多个事件在同一时刻发生
- 并行 ( Concurrent )是指两个或多个事件在同一时间间隔内发生
对于基于单个 CPU的计算机而言,各个“同时”运行的程序其实是串行分时复用一个 CPU,任一时刻点上只有一个程序在 CPU 上运行。
并发性
操作系统为了能够让 CPU 充分地忙起来,并充分利用各种资源,就需要有多种不同的应用程序在执行。这些应用程序是分时执行的,并由操作系统来完成各个应用在运行时的任务切换。
并发性虽然能够有效改善系统资源的利用率,但也带来了对共享资源的争夺问题,即 同步互斥问题。还会带来执行时间的不确定性问题,即并发程序在执行中走走停停,断续推进,使得应用程序的完成时间是不确定的。并发性对操作系统的设计也带来了很多挑战,一不小心就会出现程序执行结果不确定,程序死锁等很难调试和重现的问题。
异步性
异步是指由于操作系统的调度和中断等,会不时地暂停或打断当前正在运行的程序,使得程序的整个运行过程走走停停。在应用程序运行的表现上,特别它的执行完成时间是不可预测的。但,只要应用程序的输入是一致的,那么它的输出结果应该是符合预期的。
共享性
共享是指多个应用并发运行时,宏观上体现出他们可同时访问同一个资源,即这个资源可被共享。
但在微观上,操作系统在硬件等的支持下要确保应用程序互斥访问这个共享的资源。比如,对于两个应用同时访问同一个内存单元的情况,从宏观的应用层面上看,二者都能正确地读取同一个内存单元的内容_;而在微观上,操作系统会调度应用程序的先后执行顺序,确保在任何一个时刻,只有一个应用去访问存储单元。_
持久性
操作系统提供了文件系统来从可持久保存的存储介质(磁盘,SSD 等,以后以硬盘代表)中取数据和代码到内存中,并可以把内存中的数据写回到硬盘上。硬盘在这里是外设,具有持久性,以文件系统的形式呈现给应用程序。
文件系统可以看成是操作系统对存储外设(如硬盘、SSD)的虚拟化。这种持久性的特征进一步带来了共享属性,即在文件系统中的文件可以被多个运行的程序所访问,从而给应用程序之间实现数据共享提供了方便。即使掉电,存储外设上的数据还不会丢失,可以在下一次机器加电后提供给运行程序使用。
进程和线程的区别?🤔
- 调度:进程是资源管理的基本单位,线程是程序执行的基本单位。
- 切换:线程上下文切换比进程上下文切换要快的多
- 拥有资源:进程是拥有资源的一个独立的单位,线程不拥有系统资源,但是可以访问隶属于进程的资源。
系统开销:创建或撤销进程时,系统都要为之分配或回收系统资源,如内存空间,I/O 设备等,OS 所付出的开销显著大于在创建或撤销线程时的开销,进程切换的开销也远大于线程切换的开销。
协程与线程的区别?
线程和进程都是同步机制,而协程是异步机制。
- 线程是抢占式,而协程是非抢占式的,需要用户释放使用权切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。
- 一个线程可以有多个协程,一个进程也可以有多个协程。
- 协程不被操作系统内核管理,而完全是由程序控制。线程是分割的 CPU 资源,协程是组织好的代码流程,线程是协程的资源。但协程不会直接使用线程,协程直接利用的是执行器关联任意线程或线程池。
-
并发和并行有什么区别?
并发是在一段时间内,多个任务都会被处理;但在某一个时刻,只有一个任务在执行。单核处理器可以做到并发。比如有两个进程 A 和 B,A 运行一个时间片之后,切换到 B,B 运行一个时间片之后又切换到 A。因为切换速度足够快,所以宏观上表现为在一段时间内能同时运行多个程序。这与操作系统的 CPU 虚拟化有关,操作系统给了应用程序一个幻象,操作系统把时间分成小段,每个应用程序占用其中一小段时间片运行,用完这一时间片之后,操作系统会切换到另一个应用程序,让它运行。由于时间片很短,操作系统的切换开销也很小,应用程序或使用应用程序的用户是看不出来的,反而感觉是多个应用程序在同一时间内 并行执行,从而实现了 CPU 虚拟化。
并行就是 同一时刻,有多个任务在执行。这需要多核处理器才能完成,在微观上就能同时执行多条指令,不同的程序被放到不同的处理器上运行,这个是物理上的多个进程同时进行。进程和线程的切换流程?
进程:
切换页表以使用新的地址空间,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了
- 切换内核栈和硬件上下文
对于 linux 来说,线程和进程的最大区别就在于地址空间
对于线程切换,第一步是不需要做的,只需要做第二步
每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程中切换时不涉及虚拟地址空间的转换。
为什么虚拟地址空间切换会比较耗时?
进程都有自己的虚拟地址空间,把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用 Cache 来缓存常用的地址映射,这样可以加速页表查找。这个 Cache 就是 TLB( Translation Lookaside Buffer ,TLB 本质上就是一个 Cache,是用来加速页表查找的)。
由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要切换,页表切换后 TLB 就失效了,Cache 失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换不会导致 TLB 失效,因为线程无需切换地址空间,因此我们通常说线程切换要比进程切换快,原因就在这里。
进程间通信方式有哪些?
- 管道:管道这种通信方式有两种限制,一是半双工的通信,数据只能单向流动;二是只能在具有亲缘关系的进程间使用,进程的亲缘关系通常是指父子进程关系。管道可以分为两类:
- 匿名管道:单向的,只能在有亲缘关系的进程间通信;
- 命名管道:以磁盘文件的方式存在,可以实现本机任意两个进程通信。
- 信号:信号是一种比较复杂的通信方式,信号可以在任何时候发给某一进程,而无需知道该进程的状态。比如 Linux 系统中常用信号:SIGKILL,用户终止进程执行信号,shell 下执行 kill -9 发送该信号;SIGTERM,结束进程信号,shell 下执行 kill 进程 pid 发送该信号。
- 信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种 锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
- 消息队列:消息队列是消息的链接表,包括 Posix 消息队列和 System V 消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
- 共享内存:共享内存就是映射一段能被其他进程所访问的内存。这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
- Socket:与其他通信机制不同的是,他可用于不同机器间的进程通信。
优缺点:
- 管道:速度慢,容量有限。
- Socket:任何进程间都能通,但速度慢。
- 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题;
- 信息量:不能传递复杂消息,只能用来同步
- 共享内存区:能够容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。
进程间同步的方式有哪些?
- 临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
- 优点:保证在某一时刻只有一个线程能访问数据的简便方法
- 缺点:虽然临界区同步速度快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。
- 互斥量:为协调共同对一个共享资源的单独访问而设计的,互斥量跟临界区很像,比临界区复杂,互斥对象只有一个,只有拥有互斥对象的线程才具有访问资源的权限。
- 优点:使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。
- 缺点:
- 互斥量是可以命名的,也就是说它可以跨进程使用,所以创建互斥量需要的资源更多,所以如果只为了在进程内部使用的话使用临界区会带来速度上的优势并能够减少资源占用量。
- 比如现在以为用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号量对象可以说是一种资源计数器。
- 信号量:为控制一个具有有限数量用户资源而设计。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。互斥量是信号量的一种特殊情况,当信号量的最大资源数 = 1 就是互斥量了。
- 优点:适用于对 Socket 程序中线程的同步
- 缺点:
- 信号量机制必须有公共内存,不能用于分布式操作系统,这是它最大的弱点。
- 信号量机制功能强大,但使用时对信号量的操作分散,而且难以控制,读写和维护都很困难,加重了编码负担。
- 核心操作 P-V 分散在个用户程序的代码中,不易控制和管理,一旦错误,后果严重,且不易发现和纠正。
事件:用来通知线程有一些事件已发生,从而启动后继任务的开始
临界区:当多个线程访问一个独占性共享资源时,可以使用临界区对象。拥有临界区的线程可以访问被保护起来的资源或代码段,其他线程若想访问,则被挂起,直到拥有临界区的线程放弃临界区为止,以此达到用原子方式操作共享内存的目的。
- 事件:事件机制,则允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。
- 互斥量:互斥对象和临界对象非常相似,只是其允许在进程间使用,而临界区只限制与同一进程的各个线程之间使用,但是更节省资源,更有效率。
- 信号量:当需要一个计数器来限制可以使用某共享资源的线程数目时,可以使用“信号量”对象。
区别
- 互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说互斥量可以跨越进程使用,但是创建互斥量需要的资源多,所以如果只是为了在进程内部使用的话,使用临界区会带来速度上的优势并能减少资源占用量。
互斥量,信号量,事件都可以被跨越进程使用来进行同步数据操作。
线程的分类?
从线程的运行空间来说,分为用户级线程和内核级线程
内核级线程:这类线程依赖于内核,又称为内核支持的线程或轻量级进程。无论是在用户程序中的线程还是系统进程中的线程,他们的创建、撤销、和切换都是由内核实现。比如英特尔 i5-8250U 是 4 核 8 线程,这里的线程就是内核级线程。
用户级线程:它进用于用户级中,这种线程是不依赖于操作系统核心的。应用进程利用线程库来完成其创建和管理,速度比较快,操作系统内核无法感知用户级线程的存在。
什么是临界区,如何解决冲突?
每个进程中访问临界资源的那段程序称为临界区,一次仅允许一个进程使用的资源称为临界资源。
解决冲突的办法:如果有若干进程要求进入空闲的临界区,一次仅允许一个进程进入,如已有