操作系统八股文--带你了解操作系统的56个问题

操作系统基础
一.什么是操作系统(操作系统的作用)
1.操作系统(Operating System,简称 OS)是用户与计算机硬件之间的接口。
2.操作系统本质上是一个运行在计算机上的软件程序 ,用于管理计算机硬件和软件资源。 举例:运行在你电脑上的所有应用程序都通过操作系统来调用系统内存以及磁盘等等硬件。
3.操作系统存在屏蔽了硬件层的复杂性,实现了对计算机资源的抽象。 操作系统就像是硬件使用的负责人,统筹着各种相关事项。
4.操作系统的内核(Kernel)是操作系统的核心部分,它负责系统的内存管理,硬件设备的管理,文件系统的管理以及应用程序的管理。 内核是连接应用程序和硬件的桥梁,决定着系统的性能和稳定性。
二.操作系统的分类
操作系统常规可分为批处理操作系统、分时操作系统、实时操作系统。

若一个操作系统兼顾批操作和分时的功能,则称该系统为通用操作系统。

常见的通用操作系统有:Windows、Linux、MacOS等。

操作系统的内核
1.内核:内核作为应⽤连接硬件设备的桥梁,应⽤程序只需关⼼与内核交互,不⽤关⼼硬件的细节。
(1)现代操作系统,内核⼀般会提供 4 个基本能⼒:
管理进程、线程,决定哪个进程、线程使⽤ CPU,也就是进程调度的能⼒;
管理内存,决定内存的分配和回收,也就是内存管理的能⼒;
管理硬件设备,为进程与硬件设备之间提供通信能⼒,也就是硬件通信能⼒;
提供系统调⽤,如果应⽤程序要运⾏更⾼权限运⾏的服务,那么就需要有系统调⽤,它是⽤户程序与操作系统之间的接⼝。
三.什么是内核态和用户态?
为了避免操作系统和关键数据被用户程序破坏,将处理器的执行状态分为内核态和用户态。

内核态是操作系统管理程序执行时所处的状态,能够执行包含特权指令在内的一切指令,能够访问系统内所有的存储空间。

用户态是用户程序执行时处理器所处的状态,不能执行特权指令,只能访问用户地址空间。

用户程序运行在用户态,操作系统内核运行在内核态。

四.如何实现内核态和用户态的切换?
处理器从用户态切换到内核态的方法有三种:系统调用、异常和外部中断。

系统调用是操作系统的最小功能单位,是操作系统提供的用户接口,系统调用本身是一种软中断。

异常,也叫做内中断,是由错误引起的,如文件损坏、缺页故障等。

外部中断,是通过两根信号线来通知处理器外设的状态变化,是硬中断。

五.什么是系统调用呢? 能不能详细介绍一下。
我们运行的程序基本都是运行在用户态,如果我们调用操作系统提供的系统态级别的子功能咋办呢?那就需要系统调用了!也就是说在我们运行的用户程序中,凡是与系统态级别的资源有关的操作(如文件管理、进程控制、内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。
用户态(user mode) : 用户态运行的进程可以直接读取用户程序的数据。
系统态(kernel mode):可以简单的理解系统态运行的进程或程序几乎可以访问计算机的任何资源,不受限制。
系统调用大致分为如下几类
设备管理。完成设备的请求或释放,以及设备启动等功能。
文件管理。完成文件的读、写、创建及删除等功能。
进程控制。完成进程的创建、撤销、阻塞及唤醒等功能。
进程通信。完成进程之间的消息传递或信号传递等功能。
内存管理。完成内存的分配、回收以及获取作业占用内存区大小及地址等功能。
六.硬中断和软中断区别
软中断是执行中断指令产生的,而硬中断是由外设引发的。
硬中断的中断号是由中断控制器提供的,软中断的中断号由指令直接指出,无需使用中断控制器。
硬中断是可屏蔽的,软中断不可屏蔽。
硬中断处理程序要确保它能快速地完成任务,这样程序执行时才不会等待较长时间,称为上半部。
软中断处理硬中断未完成的工作,是一种推后执行的机制,属于下半部。
-----------------------------------------------------------------分割线--------------------------------------------------------------
进程和线程
七.进程和线程的区别
线程是进程划分成的更小的运行单位,进程中的一条执行流程
线程是调度的基本单位,⽽进程则是资源拥有的基本单位
进程拥有⼀个完整的资源平台,一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。当进程只有⼀个线程时,可以认为进程就等于线程。
线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。当进程中的⼀个线程崩溃时,会导致其所属进程的所有线程崩溃。
线程能减少并发执⾏的时间和空间开销,但不利于资源的管理和保护;而进程正相反。
八.进程相比线程能减少开销体现在

线程的创建时间⽐进程快,因为进程在创建的过程中,还需要资源管理信息,⽐如内存管理信息、⽂件管理信息,⽽线程在创建的过程中,不会涉及这些资源管理信息,⽽是共享它们;

线程的终⽌时间⽐进程快,因为线程释放的资源相⽐进程少很多;

同⼀个进程内的线程切换⽐进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同⼀个进程的线程都具有同⼀个⻚表,那么在切换的时候不需要切换⻚表。⽽对于进程之间的切换,切换的时候要把⻚表给切换掉,⽽⻚表的切换过程开销是⽐较⼤的;

由于同⼀进程的各线程间共享内存和⽂件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更⾼了;

九.进程线程和程序的区别
程序:为执行特定的任务,用某种语言编写的一组指令集合,指一段静态的代码
线程是进程当中的⼀条执⾏流程。同⼀个进程内多个线程之间可以共享代码段、数据段、打开的⽂件等资源,但每个线程各⾃都有⼀套独⾃的寄存器和栈,这样可以确保线程的控制流是相对独⽴的。
进程:编写的代码只是⼀个存储在硬盘的静态⽂件,通过编译后就会⽣成⼆进制可执⽂件件,当我们运⾏这个可执⽂件后,它会被装载到内存中,接着 CPU 会执⾏程序中的每⼀条指令,那么这个运⾏中的程序,就被称为「进程(Process)。
线程的优点:
⼀个进程中可以同时存在多个线程;
各个线程之间可以并发执⾏;
各个线程之间可以共享地址空间和⽂件等资源;
线程的缺点:
当进程中的⼀个线程崩溃时,会导致其所属进程的所有线程崩溃。
十.进程有哪几种状态
创建状态(new) :进程正在被创建,尚未到就绪状态。
就绪状态(ready) :进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
运行状态(running) :该时刻进程占⽤ CPU;(单核 CPU 下任意时刻只有一个进程处于运行状态)。
阻塞状态(waiting) :又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。
结束状态(terminated) :进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。

NULL -> 创建状态 :⼀个新进程被创建时的第⼀个状态;
创建状态 -> 就绪状态 :当进程被创建完成并初始化后,⼀切就绪准备运⾏时,变为就绪状态,这个
过程是很快的;
就绪态 -> 运⾏状态 :处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运⾏
该进程;
运⾏状态 -> 结束状态 :当进程已经运⾏完成或出错时,会被操作系统作结束状态处理;
运⾏状态 -> 就绪状态 :处于运⾏状态的进程在运⾏过程中,由于分配给它的运⾏时间⽚⽤完,操作
系统会把该进程变为就绪态,接着从就绪态选中另外⼀个进程运⾏;
运⾏状态 -> 阻塞状态 :当进程请求某个事件且必须等待时,例如请求 I/O 事件;
阻塞状态 -> 就绪状态 :当进程要等待的事件完成时,它从阻塞状态变到就绪状态;
十一.进程挂起
被阻塞状态的进程占⽤着物理内存就⼀种浪费物理内存的⾏为。所以,在虚拟内存管理的操作系统中,通常会把阻塞状态的进程的物理内存空间换出到硬盘,等需要再次运⾏的时候,再从硬盘换⼊到物理内存。
导致进程挂起的原因不只是因为进程所使⽤的内存空间不在物理内存,还包括如下情况:
通过 sleep 让进程间歇性挂起,其⼯作原理是设置⼀个定时器,到期后唤醒进程。
⽤户希望挂起⼀个程序的执⾏,⽐如在 Linux 中⽤ Ctrl+Z 挂起进程;
挂起状态可以分为两种:
阻塞挂起状态:进程在外存(硬盘)并等待某个事件的出现;
就绪挂起状态:进程在外存(硬盘),但只要进⼊内存,即刻⽴刻运⾏;

十二.并发和并行的区别
并发: 同⼀时间段,多个任务都在执⾏ (单位时间内不⼀定同时执⾏);
对于单核 CPU 时,可以让每个任务执⾏⼀⼩段时间,时间到就切换另外⼀个任务,从宏观⻆度看,⼀段时间内执⾏了多个任务,这被称为并发。这种并发并不能提高计算机的性能,只能提高效率(如降低某个进程的相应时间)。
并⾏: 单位时间内,多个任务同时执⾏。
对于多核 CPU 时,多个任务可以同时被不同核⼼的 CPU 同时执⾏,两者之间互不影响,单个周期内每个程序都运行了自己的指令,也就是运行了两条指令。这样说来并行的确提高了计算机的效率。所以现在的cpu都是往多核方面发展。
十三.PCB中都包含什么(PCB 是进程存在的唯⼀标识,这意味着⼀个进程的存在,必然会有⼀个 PCB,如果进程消失了,那么PCB 也会随之消失。)

PCB的组织形式有链表和索引两种。通常是通过链表的⽅式进⾏组织,把具有相同状态的进程链在⼀起,组成各种队列。⽐如:
将所有处于就绪状态的进程链在⼀起,称为就绪队列;把所有因等待某事件⽽处于等待状态的进程链在⼀起就组成各种阻塞队列;
另外,对于运⾏队列在单核 CPU 系统中则只有⼀个运⾏指针了,因为单核 CPU 在某个时间,只能运⾏⼀个程序。
索引⽅式的⼯作原理:将同⼀状态的进程组织在⼀个索引表中,索引表项指向相应的 PCB,不同状态对应不同的索引表。
十四.进程的控制
(1)创建进程
操作系统允许⼀个进程创建另⼀个进程,⽽且允许⼦进程继承⽗进程所拥有的资源,当⼦进程被终⽌时,其在⽗进程处继承的资源应当还给⽗进程。同时,终⽌⽗进程时同时也会终⽌其所有的⼦进程。
创建进程的过程
为新进程分配⼀个唯⼀的进程标识号,并申请⼀个空⽩的 PCB,PCB 是有限的,若申请失败则创建失败;
为进程分配资源,此处如果资源不⾜,进程就会进⼊等待状态,以等待资源;
初始化 PCB;
如果进程的调度队列能够接纳新进程,那就将进程插⼊到就绪队列,等待被调度运⾏;
(2)终止进程
进程可以有 3 种终⽌⽅式:正常结束、异常结束以及外界⼲预(信号 kill 掉)。
终⽌进程的过程如下:
查找需要终⽌的进程的 PCB;
如果处于执⾏状态,则⽴即终⽌该进程的执⾏,然后将 CPU 资源分配给其他进程;
如果其还有⼦进程,则应将其所有⼦进程终⽌;
将该进程所拥有的全部资源都归还给⽗进程或操作系统;
将其从 PCB 所在队列中删除;
(3)阻塞进程
当进程需要等待某⼀事件完成时,它可以调⽤阻塞语句把⾃⼰阻塞等待。⽽⼀旦被阻塞等待,它只能由另⼀个进程唤醒。
阻塞进程的过程如下:
找到将要被阻塞进程标识号对应的 PCB;
如果该进程为运⾏状态,则保护其现场,将其状态转为阻塞状态,停⽌运⾏;
将该 PCB 插⼊到阻塞队列中去;
(4)唤醒进程
进程由「运⾏」转变为「阻塞」状态是由于进程必须等待某⼀事件的完成,所以处于阻塞状态的进程是绝对不可能叫醒⾃⼰的。
如果某进程正在等待 I/O 事件,需由别的进程发消息给它,则只有当该进程所期待的事件出现时,才由发
现者进程⽤唤醒语句叫醒它。
唤醒进程的过程如下:
在该事件的阻塞队列中找到相应进程的 PCB;
将其从阻塞队列中移出,并置其状态为就绪状态;
把该 PCB 插⼊到就绪队列中,等待调度程序调度;
进程的阻塞和唤醒是⼀对功能相反的语句,如果某个进程调⽤了阻塞语句,则必有⼀个与之对应的唤醒语句
十五.上下文切换
1.CPU上下文:CPU 寄存器和程序计数器。CPU 寄存器是 CPU 内部⼀个容量⼩,但是速度极快的内存(缓存)。程序计数器则是⽤来存储 CPU 正在执⾏的指令位置、或者即将执⾏的下⼀条指令位置。所以说,CPU 寄存器和程序计数是 CPU 在运⾏任何任务前,所必须依赖的环境,这些环境就叫做 CPU上下⽂。
CPU上下文切换:CPU 上下⽂切换就是先把前⼀个任务的 CPU 上下⽂(CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下⽂到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运⾏新任务。
把 CPU 上下⽂切换分成:进程上下⽂切换、线程上下⽂切换和中断上下⽂切换。
2.进程的上下文切换:一个进程切换到另一个进程运行,进程是由内核管理和调度的,所以进程的切换只能发⽣在内核态。所以,进程的上下⽂切换不仅包含了虚拟内存、栈、全局变量等⽤户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。通常,会把交换的信息保存在进程的 PCB,当要运⾏另外⼀个进程的时候,我们需要从这个进程的 PCB取出上下⽂,然后恢复到 CPU 中,这使得这个进程可以继续执⾏。

3.线程的上下文切换
这还得看线程是不是属于同⼀个进程:
当两个线程不是属于同⼀个进程,则切换的过程就跟进程上下⽂切换⼀样;
当两个线程是属于同⼀个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据;
所以,线程的上下⽂切换相⽐进程,开销要⼩很多
十六.进程的基本操作
1.进程的创建:fork()。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用 fork 时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的 PID。fork函数是有趣的(也常常令人迷惑), 因为它只被调用一次,却会返回两次:一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork 返回子进程的 PID。在子进程中,fork 返回 0。因为子进程的 PID 总是为非零,返回值就提供一个明 确的方法来分辨程序是在父进程还是在子进程中执行。
2.回收子进程:当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收(reaped)。当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程。一个进程可以通过调用 waitpid 函数来等待它的子进程终止或者停止。
3.加载并运行程序:execve 函数在当前进程的上下文中加载并运行一个新程序。
4.进程终止 exit函数
十七.简述进程间通信方法
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。

不同进程间的通信本质:进程之间可以看到一份公共资源;而提供这份资源的形式或者提供者不同,造成了通信方式不同。

管道/匿名管道(Pipes) :只存在于内存中,用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。通信方式是单向的,生命周期随着进程创建而建立,随着进程终止而消失。
有名管道(Names Pipes) : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
消息队列(Message Queuing) :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显式地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺点。但消息队列不适合⽐较⼤数据的传输并且消息队列通信过程中,存在用户态和内核态之间的数据拷贝开销。
共享内存(Shared memory) :内存的共享机制就是两个进程拿出一块虚拟地址间,映射到相同的物理内存中。使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
信号量(Semaphores) :内存共享带来的新问题是如果多个进程同时修改同一个共享空间,很有可能就冲突了。为了防⽌多进程竞争共享资源,⽽造成的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被⼀个进程访问。正好,信号量就实现了这⼀保护机制。信号量其实是⼀个整型的计数器,主要⽤于实现进程间的互斥与同步,⽽不是⽤于缓存进程间通信的数据。
套接字(Sockets) :管道、消息队列、共享内存、信号量和信号都是在同⼀台主机上进⾏进程间通信,那要想跨⽹络与不同主机上的进程之间通信,就需要 Socket 通信了。Socket 通信不仅可以跨⽹络与不同主机的进程间通信,还可以在同主机上进程间通信。此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。
十八.线程间同步的方式
程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。操作系统一般有下面三种线程同步的方式:

互斥量(Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制。
信号量(Semaphore) :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
十九.什么是信号
一个信号就是一条小消息,它通知进程系统中发生了一个某种类型的事件。Linux 系统上支持的30 种不同类型的信号。每种信号类型都对应于某种系统事件。低层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。信号提供了一种机制,通知用户进程发生了这些异常。

发送信号:内核通过更新目的进程上下文中的某个状态,发送(递送)一个信号给目的进程。发送信号可以有如下两种原因:

内核检测到一个系统事件,比如除零错误或者子进程终止。

—个进程调用了kill 函数, 显式地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。

接收信号:当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理程序(signal handler)的用户层函数捕获这个信号。

二十.什么是死锁以及产生死锁的四个必要条件

死锁是多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于进程/线程被无限期地阻塞,因此程序不可能正常终止。
死锁只有同时满⾜互斥、持有并等待、不可剥夺、环路等待这四个条件的时候才会发⽣。
互斥:资源必须处于非共享模式,即一次只有一个进程可以使用。如果另一进程申请该资源,那么必须等待直到该资源被释放为止。多个线程不能使用统一资源
占有并等待:一个进程至少应该占有一个资源,并等待另一资源,而该资源被其他进程所占有。当线程 A 已经持有了资源 1,⼜想申请资源 2,⽽资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放⾃⼰已经持有的资源 1。
非抢占(不可剥夺):资源不能被抢占。只能在持有资源的进程完成任务后,该资源才会被释放。
循环等待:有一组等待进程 {P0, P1,…, Pn}, P0 等待的资源被 P1 占有,P1 等待的资源被 P2 占有,…,Pn-1 等待的资源被 Pn 占有,Pn 等待的资源被 P0 占有。
二十一.解决死锁的方法
解决死锁的方法可以从多个角度去分析,一般的情况下,有预防,避免,检测和解除四种。

预防 是采用某种策略,限制并发进程对资源的请求,从而使得死锁的必要条件在系统执行的任何时间上都不满足。
避免则是系统在分配资源时,根据资源的使用情况提前做出预测,从而避免死锁的发生
检测是指系统设有专门的机构,当死锁发生时,该机构能够检测死锁的发生,并精确地确定与死锁有关的进程和资源。
解除 是与检测相配套的一种措施,用于将进程从死锁状态下解脱出来。
二十二.死锁的预防

资源一次性分配:破坏请求条件
只要有一个资源得不到分配,不给这个进程分配其他的资源:破坏请求保持条件
可剥夺资源:当进程获得了部分资源但得不到其他资源,则释放已占用资源:破坏不可剥夺条件
资源有序分配法:进程按规定顺序请求资源,释放则相反:破坏环路等待条件
死锁四大必要条件上面都已经列出来了,很显然,只要破坏四个必要条件中的任何一个就能够预防死锁的发生。

破坏第一个条件 互斥条件:使得资源是可以同时访问的,这是种简单的方法,磁盘就可以用这种方法管理,但是我们要知道,有很多资源 往往是不能同时访问的 ,所以这种做法在大多数的场合是行不通的。

破坏第三个条件 非抢占 :也就是说可以采用 剥夺式调度算法,但剥夺式调度方法目前一般仅适用于 主存资源 和 处理器资源 的分配,并不适用于所以的资源,会导致 资源利用率下降。

所以一般比较实用的 预防死锁的方法,是通过考虑破坏第二个条件和第四个条件。

1、静态分配策略

静态分配策略可以破坏死锁产生的第二个条件(占有并等待)。所谓静态分配策略,就是指一个进程必须在执行前就申请到它所需要的全部资源,并且知道它所要的资源都得到满足之后才开始执行。进程要么占有所有的资源然后开始执行,要么不占有资源,不会出现占有一些资源等待一些资源的情况。

静态分配策略逻辑简单,实现也很容易,但这种策略 严重地降低了资源利用率,因为在每个进程所占有的资源中,有些资源是在比较靠后的执行时间里采用的,甚至有些资源是在额外的情况下才是用的,这样就可能造成了一个进程占有了一些 几乎不用的资源而使其他需要该资源的进程产生等待 的情况。

2、层次分配策略

层次分配策略破坏了产生死锁的第四个条件(循环等待)。在层次分配策略下,所有的资源被分成了多个层次,一个进程得到某一次的一个资源后,它只能再申请较高一层的资源;当一个进程要释放某层的一个资源时,必须先释放所占用的较高层的资源,按这种策略,是不可能出现循环等待链的,因为那样的话,就出现了已经申请了较高层的资源,反而去申请了较低层的资源,不符合层次分配策略,证明略。

二十三.死锁的避免
上面提到的 破坏 死锁产生的四个必要条件之一就可以成功 预防系统发生死锁 ,但是会导致 低效的进程运行 和 资源使用率 。而死锁的避免相反,它的角度是允许系统中同时存在四个必要条件 ,只要掌握并发进程中与每个进程有关的资源动态申请情况,做出 明智和合理的选择 ,仍然可以避免死锁,因为四大条件仅仅是产生死锁的必要条件。

我们将系统的状态分为 安全状态 和 不安全状态 ,每当在未申请者分配资源前先测试系统状态,若把系统资源分配给申请者会产生死锁,则拒绝分配,否则接受申请,并为它分配资源。

如果操作系统能够保证所有的进程在有限的时间内得到需要的全部资源,则称系统处于安全状态,否则说系统是不安全的。很显然,系统处于安全状态则不会发生死锁,系统若处于不安全状态则可能发生死锁。

那么如何保证系统保持在安全状态呢?通过算法,其中最具有代表性的 避免死锁算法 就是 Dijkstra 的银行家算法,银行家算法用一句话表达就是:当一个进程申请使用资源的时候,银行家算法 通过先 试探 分配给该进程资源,然后通过 安全性算法 判断分配后系统是否处于安全状态,若不安全则试探分配作废,让该进程继续等待,若能够进入到安全的状态,则就 真的分配资源给该进程。

银行家算法详情可见:《一句话+一张图说清楚——银行家算法》open in new window 。

操作系统教程树中讲述的银行家算法也比较清晰,可以一看.

死锁的避免(银行家算法)改善解决了 资源使用率低的问题 ,但是它要不断地检测每个进程对各类资源的占用和申请情况,以及做 安全性检查 ,需要花费较多的时间。

二十四.死锁的检测
对资源的分配加以限制可以 预防和避免 死锁的发生,但是都不利于各进程对系统资源的充分共享。解决死锁问题的另一条途径是 死锁检测和解除 (这里突然联想到了乐观锁和悲观锁,感觉死锁的检测和解除就像是 乐观锁 ,分配资源时不去提前管会不会发生死锁了,等到真的死锁出现了再来解决嘛,而 死锁的预防和避免 更像是悲观锁,总是觉得死锁会出现,所以在分配资源的时候就很谨慎)。

这种方法对资源的分配不加以任何限制,也不采取死锁避免措施,但系统 定时地运行一个 “死锁检测” 的程序,判断系统内是否出现死锁,如果检测到系统发生了死锁,再采取措施去解除它。

#进程-资源分配图
操作系统中的每一刻时刻的系统状态都可以用进程-资源分配图来表示,进程-资源分配图是描述进程和资源申请及分配关系的一种有向图,可用于检测系统是否处于死锁状态。

用一个方框表示每一个资源类,方框中的黑点表示该资源类中的各个资源,每个键进程用一个圆圈表示,用 有向边 来表示进程申请资源和资源被分配的情况。

图中 2-21 是进程-资源分配图的一个例子,其中共有三个资源类,每个进程的资源占有和申请情况已清楚地表示在图中。在这个例子中,由于存在 占有和等待资源的环路 ,导致一组进程永远处于等待资源的状态,发生了 死锁。

进程-资源分配图

进程-资源分配图中存在环路并不一定是发生了死锁。因为循环等待资源仅仅是死锁发生的必要条件,而不是充分条件。图 2-22 便是一个有环路而无死锁的例子。虽然进程 P1 和进程 P3 分别占用了一个资源 R1 和一个资源 R2,并且因为等待另一个资源 R2 和另一个资源 R1 形成了环路,但进程 P2 和进程 P4 分别占有了一个资源 R1 和一个资源 R2,它们申请的资源得到了满足,在有限的时间里会归还资源,于是进程 P1 或 P3 都能获得另一个所需的资源,环路自动解除,系统也就不存在死锁状态了。

利用工具排查死锁问题(p图解245)
排查你的 Java 程序是否死锁,则可以使⽤ jstack ⼯具,它是 jdk ⾃带的线程堆栈分析⼯具。
在 Linux 下,我们可以使⽤ pstack + gdb ⼯具来定位死锁问题
二十五.死锁检测步骤
知道了死锁检测的原理,我们可以利用下列步骤编写一个 死锁检测 程序,检测系统是否产生了死锁。

如果进程-资源分配图中无环路,则此时系统没有发生死锁
如果进程-资源分配图中有环路,且每个资源类仅有一个资源,则系统中已经发生了死锁。
如果进程-资源分配图中有环路,且涉及到的资源类有多个资源,此时系统未必会发生死锁。如果能在进程-资源分配图中找出一个 既不阻塞又非独立的进程 ,该进程能够在有限的时间内归还占有的资源,也就是把边给消除掉了,重复此过程,直到能在有限的时间内 消除所有的边 ,则不会发生死锁,否则会发生死锁。(消除边的过程类似于 拓扑排序)
二十六.死锁的解除
当死锁检测程序检测到存在死锁发生时,应设法让其解除,让系统从死锁状态中恢复过来,常用的解除死锁的方法有以下四种:

立即结束所有进程的执行,重新启动操作系统 :这种方法简单,但以前所在的工作全部作废,损失很大。
撤销涉及死锁的所有进程,解除死锁后继续运行 :这种方法能彻底打破死锁的循环等待条件,但将付出很大代价,例如有些进程可能已经计算了很长时间,由于被撤销而使产生的部分结果也被消除了,再重新执行时还要再次进行计算。
逐个撤销涉及死锁的进程,回收其资源直至死锁解除。
抢占资源 :从涉及死锁的一个或几个进程中抢占资源,把夺得的资源再分配给涉及死锁的进程直至死锁解除。
二十七.互斥锁与自旋锁
利用加锁来实现互斥和同步的一般模型为,加锁→临界区:访问公共变量→解锁
当判断lock已被加锁,则有两种处理:一是调用系统函数将当前线程阻塞,线程释放CPU给其他线程,二是使其一直处于循环判断的状态,即互斥和自旋。阻塞当前线程可以让出CPU资源去执行别的线程,通过减少CPU浪费来提高效率;但切换线程需要进行上下文切换,因此耗费时间较长。对于线程占用锁时间短的情况,自旋锁会更加高效。
对于单核和不可抢占内核,自旋锁会退化为空操作;对于单核和可抢占内核,自旋锁持有期间内内核的抢占将被禁止。
互斥锁所保护的临界区可包含可能引起阻塞的代码,如果自旋锁用于这种情况,则可能因进程切换而发生死锁
二十八.读写锁
如果只读取共享资源⽤「读锁」加锁,如果要修改共享资源则⽤「写锁」加锁。读写锁是用于能明确区分读操作和写操作的场景
读写锁的⼯作原理是:
当「写锁」没有被线程持有时,多个线程能够并发地持有读锁,这⼤⼤提⾼了共享资源的访问效率,因为「读锁」是⽤于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。
但是,⼀旦「写锁」被线程持有后,读线程的获取读锁的操作会被阻塞,⽽且其他写线程的获取写锁的操作也会被阻塞。
写锁是独占锁,因为任何时刻只能有⼀个线程持有写锁,类似互斥锁和⾃旋锁,⽽读锁是共享锁,因为读锁可以被多个线程同时持有。
读写锁在读多写少的场景,能发挥出来优势
读写锁分为读优先和写优先
读优先锁期望的是,读锁能被更多的线程持有,以便提⾼读线程的并发性,它的⼯作⽅式是:当读线程 A先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 仍然可以成功获取读锁,最后直到读线程 A 和 C 释放读锁后,写线程 B 才可以成功获取写锁。
写优先锁是优先服务写线程,其⼯作⽅式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 获取读锁时会失败,于是读线程 C 将被阻塞在获取读锁的操作,这样只要读线程 A 释放读锁后,写线程 B 就可以成功获取读锁。
读优先锁会造成写线程饥饿,写优先锁会造成读线程饥饿,所以有了公平读写锁
公平读写锁:⽤队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现「饥饿」的现象。
互斥锁和⾃旋锁都是最基本的锁,读写锁可以根据场景来选择这两种锁其中的⼀个进⾏实现。
二十九.乐观锁和悲观锁
互斥锁、自旋锁和读写锁都属于悲观锁
悲观锁做事⽐较悲观,它认为多线程同时修改共享资源的概率⽐较⾼,于是很容易出现冲突,所以访问共享资源前,先要上锁。
乐观锁(全程没有加锁,所以叫无锁编程)做事⽐较乐观,它假定冲突的概率很低,它的⼯作⽅式是:先修改完共享资源,再验证这段时间内有没有发⽣冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
在线文档用了乐观锁,它允许多个⽤户打开同⼀个⽂档进⾏编辑,编辑完提交之后才验证修改的内容是否有冲突。
什么叫发生冲突:,⽐如⽤户 A 先在浏览器编辑⽂档,之后⽤户 B 在浏览器也打开了相同的⽂档进⾏编辑,但是⽤户 B ⽐⽤户 A 先提交改动,这⼀过程⽤户 A 是不知道的,当 A 提交修改完的内容时,那么 A 和 B 之间并⾏修改的地⽅就会发⽣冲突。
服务端怎么验证是否冲突:
由于发⽣冲突的概率⽐较低,所以先让⽤户编辑⽂档,但是浏览器在下载⽂档时会记录下服务端返回的⽂档版本号;
当⽤户提交修改时,发给服务端的请求会带上原始⽂档版本号,服务器收到后将它与当前版本号进⾏⽐较,如果版本号⼀致则修改成功,否则提交失败。
常见的SVN和Git也采用了乐观锁
乐观锁虽然去除了加锁解锁的操作,但是⼀旦发⽣冲突,重试的成本⾮常⾼,所以只有在冲突概率⾮常低,且加锁成本⾮常⾼的场景时,才考虑使⽤乐观锁。
三十.进程调度算法
调度算法分为两类
⾮抢占式调度算法挑选⼀个进程,然后让该进程运⾏直到被阻塞,或者直到该进程退出,才会调⽤另外⼀个进程,也就是说不会理时钟中断这个事情。
抢占式调度算法挑选⼀个进程,然后让该进程只运⾏某段时间,如果在该时段结束时,该进程仍然在运⾏时,则会把它挂起,接着调度程序从就绪队列挑选另外⼀个进程。这种抢占式调度处理,需要在时间间隔的末端发⽣时钟中断,以便把 CPU 控制返回给调度程序进⾏调度,也就是常说的时间⽚机制。

(1)先来先服务调度算法(FCFS):非抢占式,每次从就绪队列选择最先进⼊队列的进程,然后⼀直运⾏,直到进程退出或被阻塞,才会继续从队列中选择第⼀个进程接着运⾏。
对⻓作业有利,适⽤于 CPU 繁忙型作业的系统,⽽不适⽤于 I/O 繁忙型作业的系统。
(2)最短作业优先调度算法(SJF):优先选择运行时间最短的进程运行,有助于提高系统的吞吐量。对长作业不利。
(3)高响应比优先调度算法(HRRN):主要是权衡了短作业和长作业,每次进⾏进程调度时,先计算「响应⽐优先级」,然后把「响应⽐优先级」最⾼的进程投⼊运⾏

如果两个进程的「等待时间」相同时,「要求的服务时间」越短,「响应⽐」就越⾼,这样短作业的进程容易被选中运⾏;
如果两个进程「要求的服务时间」相同时,「等待时间」越⻓,「响应⽐」就越⾼,这就兼顾到了⻓作业进程,因为进程的响应⽐可以随时间等待的增加⽽提⾼,当其等待时间⾜够⻓时,其响应⽐便可以升到很⾼,从⽽获得运⾏的机会;
(4)时间片轮转调度算法(RR):每个进程都被分配一个时间片,所有线程同等重要,如果时间⽚⽤完,进程还在运⾏,那么将会把此进程从 CPU 释放出来,并把 CPU 分配给另外⼀个进程,如果该进程在时间⽚结束前阻塞或结束,则 CPU ⽴即进⾏切换;
注意时间片的设置:如果时间⽚设得太短会导致过多的进程上下⽂切换,降低了 CPU 效率;如果设得太⻓⼜可能引起对短作业进程的响应时间变⻓。将⼀般来说,时间⽚设为 20ms~50ms 通常是⼀个⽐较合理的折中值。
(5)最高优先级调度算法(HPF):调度程序从就绪队列中选择最高优先级的进程进行运行
进程的优先级可以分为,静态优先级和动态优先级:
静态优先级:创建进程时候,就已经确定了优先级了,然后整个运⾏时间优先级都不会变化;
动态优先级:根据进程的动态变化调整优先级,⽐如如果进程运⾏时间增加,则降低其优先级,如果进程等待时间(就绪队列的等待时间)增加,则升⾼其优先级,也就是随着时间的推移增加等待进程的优先级。
该算法也有两种处理优先级⾼的⽅法,⾮抢占式和抢占式:
⾮抢占式:当就绪队列中出现优先级⾼的进程,运⾏完当前进程,再选择优先级⾼的进程。
抢占式:当就绪队列中出现优先级⾼的进程,当前进程挂起,调度优先级⾼的进程运⾏。
但是依然有缺点,可能会导致低优先级的进程永远不会运⾏。
(6)多级反馈队列调度算法(MFQ):「时间⽚轮转算法」和「最⾼优先级算法」的综合和发展。「多级」表示有多个队列,每个队列优先级从⾼到低,同时优先级越⾼时间⽚越短。
「反馈」表示如果有新的进程加⼊优先级⾼的队列时,⽴刻停⽌当前正在运⾏的进程,转⽽去运⾏优先级⾼的队列;
设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从⾼到低,同时优先级越⾼时间⽚越短;新的进程会被放⼊到第⼀级队列的末尾,按先来先服务的原则排队等待被调度,如果在第⼀级队列规定的时间⽚没运⾏完成,则将其转⼊到第⼆级队列的末尾,以此类推,直⾄完成;当较⾼优先级的队列为空,才调度较低优先级的队列中的进程运⾏。如果进程运⾏时,有新进程进⼊较⾼优先级的队列,则停⽌当前运⾏的进程并将其移⼊到原队列末尾,接着让较⾼优先级的进程运⾏;
可以发现,对于短作业可能可以在第⼀级队列很快被处理完。对于⻓作业,如果在第⼀级队列处理不完,可以移⼊下次队列等待被执⾏,虽然等待的时间变⻓了,但是运⾏时间也变更⻓了,所以该算法很好的兼顾了⻓短作业,同时有较好的响应时间。
三十一.进程调度的时机
当前运行的进程运行结束。

当前运行的进程由于某种原因阻塞。

执行完系统调用等系统程序后返回用户进程。

在使用抢占调度的系统中,具有更高优先级的进程就绪时。

分时系统中,分给当前进程的时间片用完。

三十二.不能进行进程调度的情况

在中断处理程序执行时。

在操作系统的内核程序临界区内。

其它需要完全屏蔽中断的原子操作过程中。

三十三.进程调度策略的基本设计目标

CPU 利⽤率:调度程序应确保 CPU 是始终匆忙的状态,这可提⾼ CPU 的利⽤率;
系统吞吐量:吞吐量表示的是单位时间内 CPU 完成进程的数量,⻓作业的进程会占⽤较⻓的 CPU 资源,因此会降低吞吐量,相反,短作业的进程会提升系统吞吐量;
周转时间:周转时间是进程运⾏和阻塞时间总和,⼀个进程的周转时间越⼩越好;
等待时间:这个等待时间不是阻塞状态的时间,⽽是进程处于就绪队列的时间,等待的时间越⻓,⽤户越不满意;
响应时间:⽤户提交请求到系统第⼀次产⽣响应所花费的时间,在交互式系统中,响应时间是衡量调度算法好坏的主要标准。
三十四.内存页面置换算法
地址映射过程中,若在页面中发现所要访问的页面不在内存中,则发生缺页中断 。

缺页中断 就是要访问的页不在主存,需要操作系统将其调入主存后再进行访问。 在这个时候,被内存映射的文件实际上成了一个分页交换文件。

当发生缺页中断时,如果当前内存中并没有空闲的页面,操作系统就必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。用来选择淘汰哪一页的规则叫做页面置换算法,我们可以把页面置换算法看成是淘汰页面的规则。

OPT 页面置换算法(最佳页面置换算法) :最佳(Optimal, OPT)置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存下的若千页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现。一般作为衡量其他置换算法的方法。最佳⻚⾯置换算法作⽤是为了衡量你的算法的效率,你的算法效率越接近该算法的效率,那么说明你的算法是⾼效的。
FIFO(First In First Out) 页面置换算法(先进先出页面置换算法) : 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。
LRU (Least Recently Used)页面置换算法(最近最久未使用页面置换算法) :LRU 算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。
最优置换算法是通过「未来」的使⽤情况来推测要淘汰的⻚⾯,⽽ LRU 则是通过「历史」的使⽤情况来推测要淘汰的⻚⾯。
LFU (Least Frequently Used)页面置换算法(最少使用页面置换算法) 当发⽣缺⻚中断时,选择「访问次数」最少的那个⻚⾯,并将其淘汰。的实现⽅式是,对每个⻚⾯设置⼀个「访问计数器」,每当⼀个⻚⾯被访问时,该⻚⾯的访问计数器就累加 1。在发⽣缺⻚中断时,淘汰计数器值最⼩的那个⻚⾯。
时钟页面置换算法(Lock):把所有的⻚⾯都保存在⼀个类似钟⾯的「环形链表」中,⼀个表针指向最⽼的⻚⾯。当发⽣缺⻚中断时,算法⾸先检查表针指向的⻚⾯:如果它的访问位位是 0 就淘汰该⻚⾯,并把新的⻚⾯插⼊这个位置,然后把表针前移⼀个位置;如果访问位是 1 就清除访问位,并把表针前移⼀个位置,重复这个过程直到找到了⼀个访问位为 0 的⻚⾯为⽌;
三十五.磁盘调度算法(为了提高磁盘的访问性能,一般通过优化磁盘的访问请求顺序)
1.先来先服务(FCFS):先到来的请求,先被服务
2.最短寻道时间优先(SSF):优先选择从当前磁头位置所需寻道时间最短的请求.因为是磁头在一小块区域来回移动所以会产生饥饿现象
四.扫描算法(SCAN):为了解决上面那个问题,磁头在⼀个⽅向上移动,访问所有未完成的请求,直到磁头到达该⽅向上的最后的磁道,才调换⽅向
循环扫描算法(CSCAN):只有磁头朝某个特定⽅向移动时,才处理磁道访问请求,⽽返回时直接快速移动⾄最靠边缘的磁道,也就是复位磁头,这个过程是很快的,并且返回中途不处理任何请求,该算法的特点,就是磁道只响应⼀个⽅向上的请求。
三十六.什么是孤儿进程?僵尸进程?
孤儿进程:父进程退出,子进程还在运行的这些子进程都是孤儿进程,孤儿进程将被init进程(1号进程)所收养,并由init进程对他们完成状态收集工作。
2。僵尸进程:进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait 获waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中的这些进程是僵尸进程。

三十七.进程和线程的基本API

------------------------------------------------------------------------------分割线--------------------------------------------------------------------------------------------------
操作系统内存管理基础

三十八.操作系统的内存管理主要是做什么?
操作系统的内存管理主要负责内存的分配与回收(malloc 函数:申请内存,free 函数:释放内存),另外地址转换也就是将逻辑地址转换成相应的物理地址等功能也是操作系统内存管理做的事情。
三十九.什么是虚拟内存(Virtual Memory)?
这个在我们平时使用电脑特别是 Windows 系统的时候太常见了。很多时候我们使用了很多占内存的软件,这些软件占用的内存可能已经远远超出了我们电脑本身具有的物理内存。为什么可以这样呢? 正是因为 虚拟内存 的存在,通过 虚拟内存 可以让程序可以拥有超过系统物理内存大小的可用内存空间。另外,虚拟内存为每个进程提供了一个一致的、私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉(每个进程拥有一片连续完整的内存空间)。这样会更加有效地管理内存并减少出错。

虚拟内存是计算机系统内存管理的一种技术,我们可以手动设置自己电脑的虚拟内存。不要单纯认为虚拟内存只是“使用硬盘空间来扩展内存“的技术。虚拟内存的重要意义是它定义了一个连续的虚拟地址空间,并且 把内存扩展到硬盘空间。

虚拟内存 使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如 RAM)的使用也更有效率。目前,大多数操作系统都使用了虚拟内存,如 Windows 家族的“虚拟内存”;Linux 的“交换空间”等。

四十.局部性原理

局部性原理是虚拟内存技术的基础,正是因为程序运行具有局部性原理,才可以只装入部分程序到内存就开始运行。

局部性原理表现在以下两个方面:

时间局部性 :如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问。产生时间局部性的典型原因,是由于在程序中存在着大量的循环操作。
空间局部性 :一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,这是因为指令通常是顺序存放、顺序执行的,数据也一般是以向量、数组、表等形式簇聚存储的。
时间局部性是通过将近来使用的指令和数据保存到高速缓存存储器中,并使用高速缓存的层次结构实现。空间局部性通常是使用较大的高速缓存,并将预取机制集成到高速缓存控制逻辑中实现。虚拟内存技术实际上就是建立了 “内存一外存”的两级存储器的结构,利用局部性原理实现髙速缓存。
虚拟内存(又叫虚拟存储器)结合局部性原理
基于局部性原理,在程序装入时,可以将程序的一部分装入内存,而将其他部分留在外存,就可以启动程序执行。由于外存往往比内存大很多,所以我们运行的软件的内存大小实际上是可以比计算机系统实际的内存大小大的。在程序执行过程中,当所访问的信息不在内存时,由操作系统将所需要的部分调入内存,然后继续执行程序。另一方面,操作系统将内存中暂时不使用的内容换到外存上,从而腾出空间存放将要调入内存的信息。这样,计算机好像为用户提供了一个比实际内存大得多的存储器——虚拟存储器。
四十一.常见的几种内存管理机制
简单分为连续分配管理方式和非连续分配管理方式这两种。连续分配管理方式是指为一个用户程序分配一个连续的内存空间,常见的如 块式管理 。同样地,非连续分配管理方式允许一个程序使用的内存分布在离散或者说不相邻的内存中,常见的如页式管理 和 段式管理。
块式管理 : 远古时代的计算机操作系统的内存管理方式。将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了。这些在每个块中未被利用的空间,我们称之为碎片。
2.段式管理 : 页式管理虽然提高了内存利用率,但是页式管理其中的页并无任何实际意义。 段式管理把主存分为一段段的,段是有实际意义的,每个段定义了一组逻辑信息,例如,有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。 段式管理通过段表对应逻辑地址和物理地址。

分段机制下的虚拟地址由两部分组成,段选择⼦和段内偏移量。
段选择⼦就保存在段寄存器⾥⾯。段选择⼦⾥⾯最重要的是段号,⽤作段表的索引。段表⾥⾯保存的是这个段的基地址、段的界限和特权等级等。
虚拟地址中的段内偏移量应该位于 0 和段界限之间,如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。
虚拟地址是通过段表与物理地址进⾏映射的,分段机制会把程序的虚拟地址分成 4 个段,每个段在段表中有⼀个项,在这⼀项找到段的基地址,再加上偏移量,于是就能找到物理内存中的地址。
分段机制的不足
第⼀个就是内存碎⽚的问题。
外部内存碎⽚,也就是产⽣了多个不连续的⼩物理内存,导致新的程序⽆法被装载;
解决方法:内存交换,在 Linux 系统⾥,也就是我们常看到的 Swap 空间,这块空间是从硬盘划分出来的,⽤于内存与硬盘的空间交换。
内部内存碎⽚,程序所有的内存都被装载到了物理内存,但是这个程序有部分的内存可能并不是很常使⽤,这也会导致内存的浪费;
第⼆个就是内存交换的效率低的问题。
对于多进程的系统来说,⽤分段的⽅式,内存碎⽚是很容易产⽣的,产⽣了内存碎⽚,那不得不重新Swap 内存区域,这个过程会产⽣性能瓶颈。因为硬盘的访问速度要⽐内存慢太多了,每⼀次内存交换,我们都需要把⼀⼤段连续的内存数据写到硬盘上。所以,如果内存交换的时候,交换的是⼀个占内存空间很⼤的程序,这样整个机器都会显得卡顿。
为了解决内存分段的内存碎⽚和内存交换效率低的问题,就出现了内存分⻚。
3.页式管理 :把主存分为大小相等且固定的一页一页的形式,页较小,相比于块式管理的划分粒度更小,提高了内存利用率,减少了碎片。页式管理通过页表对应逻辑地址和物理地址。
分页如何解决分段的内存碎片、内存交换效率低的问题
分页中内存空间都是预先划分好的,并且采用了分页,释放的内存都是以页为单位释放的,不会产生给进程使用的小内存从而解决了分段的内存碎片
如果内存空间不够,操作系统会把其他正在运⾏的进程中的「最近没被使⽤」的内存⻚⾯给释放掉,也就是暂时写在硬盘上,称为换出(Swap Out)。⼀旦需要的时候,再加载进来,称为换⼊(Swap In)。所以,⼀次性写⼊磁盘的也只有少数的⼀个⻚或者⼏个⻚,不会花太多时间,内存交换的效率就相对⽐较⾼。
分⻚的⽅式使得我们在加载程序的时候,不再需要⼀次性都把程序加载到物理内存中。我们完全可以在进⾏虚拟内存和物理内存的⻚之间的映射之后,并不真的把⻚加载到物理内存⾥,⽽是只有在程序运⾏中,需要⽤到对应虚拟内存⻚⾥⾯的指令和数据时,再加载到物理内存⾥⾯去。
简单来说:页是物理单位,段是逻辑单位。分页可以有效提高内存利用率,分段可以更好满足用户需求。
4.段页式管理:段页式管理机制结合了段式管理和页式管理的优点。简单来说段页式管理机制就是把主存先分成若干段,每个段又分成若干固定⼤⼩的页,也就是说 段页式管理机制 中段与段之间以及段的内部的都是离散的。
段⻚式地址变换的数据结构是每⼀个程序⼀张段表,每个段⼜建⽴⼀张⻚表,段表中的地址是⻚表的起始地址,⽽⻚表中的地址则为某⻚的物理⻚号

段⻚式地址变换中要得到物理地址须经过三次内存访问:
第⼀次访问段表,得到⻚表起始地址;
第⼆次访问⻚表,得到物理⻚号;
第三次将物理⻚号与⻚内位移组合,得到物理地址。
四十二.快表和多级页表
在分页内存管理中,很重要的两点是:

虚拟地址到物理地址的转换要快。 块表
解决虚拟地址空间大,页表也会很大的问题。 多级页表
快表(TLB)
为了提高虚拟地址到物理地址的转换速度,操作系统在 页表方案 基础之上引入了 快表 来加速虚拟地址到物理地址的转换。就是把最常访问的⼏个⻚表项存储到访问速度更快的硬件,我们可以把快表理解为一种特殊的高速缓冲存储器(Cache),其中的内容是页表的一部分或者全部内容,专⻔存放程序最常访问的⻚表项的 Cache,这个 Cache 就是 TLB(Translation Lookaside Buffer) ,通常称为⻚表缓存、转址旁路缓存、快表等。作为页表的 Cache,它的作用与页表相似,但是提高了访问速率。由于采用页表做地址转换,读写内存数据时 CPU 要访问两次主存。有了快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。TLB 的命中率其实是很⾼的,因为程序最常访问的⻚就那么⼏个。

使用快表之后的CPU寻址转换流程是这样的:

根据虚拟地址中的页号查快表;
如果该页在快表中,直接从快表中读取相应的物理地址;
如果该页不在快表中,就访问内存中的页表,再从页表中得到物理地址,同时将页表中的该映射表项添加到快表中;
当快表填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。
看完了之后你会发现快表和我们平时经常在我们开发的系统使用的缓存(比如 Redis)很像,的确是这样的,操作系统中的很多思想、很多经典的算法,你都可以在我们日常开发使用的各种工具或者框架中找到它们的影子。

在 CPU 芯⽚⾥⾯,封装了内存管理单元(Memory Management Unit)芯⽚,它⽤来完成地址转换和 TLB的访问与交互。

多级页表
引入多级页表的主要目的是为了避免把全部页表一直放在内存中占用过多空间,特别是那些根本就不需要的页表就不需要保留在内存中。

多级页表属于时间换空间的典型场景。

比如:在 32 位的环境下,虚拟地址空间共有 4GB,假设⼀个⻚的⼤⼩是 4KB(2^12),那么就需要⼤约 100 万(2^20) 个⻚,每个「⻚表项」需要 4 个字节⼤⼩来存储,那么整个 4GB 空间的映射就需要有 4MB的内存来存储⻚表。这 4MB ⼤⼩的⻚表,看起来也不是很⼤。但是要知道每个进程都是有⾃⼰的虚拟地址空间的,也就说都有⾃⼰的⻚表。那么, 100 个进程的话,就需要 400MB 的内存来存储⻚表,这是⾮常⼤的内存了,更别说 64 位的环境了。

把这个 100 多万个「⻚表项」的单级⻚表再分⻚,将⻚表(⼀级⻚表)分为 1024 个⻚表(⼆级⻚表),每个表(⼆级⻚表)中包含 1024 个「⻚表项」,形成⼆级分⻚。
分了⼆级表,映射 4GB 地址空间就需要 4KB(⼀级⻚表)+ 4MB(⼆级⻚表)的内存,这样占⽤空间不是更⼤了吗?
如果 4GB 的虚拟地址全部都映射到了物理内存上的话,⼆级分⻚占⽤空间确实是更⼤了,但是,我们往往不会为⼀个进程分配那么多内存。
但是根据局部性原理,如果使⽤了⼆级分⻚,⼀级⻚表就可以覆盖整个 4GB 虚拟地址空间,但如果某个⼀级⻚表的⻚表项没有被⽤到,也就不需要创建这个⻚表项对应的⼆级⻚表了,即可以在需要时才创建⼆级⻚表。
那么为什么不分级的⻚表就做不到这样节约内存呢?我们从⻚表的性质来看,保存在内存中的⻚表承担的职责是将虚拟地址翻译成物理地址。假如虚拟地址在⻚表中找不到对应的⻚表项,计算机系统就不能⼯作了。所以⻚表⼀定要覆盖全部虚拟地址空间,不分级的⻚表就需要有 100 多万个⻚表项来映射,⽽⼆级分⻚则只需要 1024 个⻚表项(此时⼀级⻚表覆盖到了全部虚拟地址空间,⼆级⻚表在需要时创建)。
四十三.分页机制和分段机制的共同点和区别
共同点 :
分页机制和分段机制都是为了提高内存利用率,减少内存碎片。
页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的。
区别 :
页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。
分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满足用户的需要。
十九.逻辑(虚拟)地址和物理地址
我们程序所使⽤的内存地址叫做虚拟内存地址(Virtual Memory Address)比如在 C 语言中,指针里面存储的数值就可以理解成为内存里的一个地址
实际存在硬件⾥⾯的空间地址叫物理内存地址(Physical Memory Address) 更具体一点来说就是内存地址寄存器中的地址。物理地址是内存单元真正的地址。
二十.CPU 寻址了解吗?为什么需要虚拟地址空间?
现代处理器使用的是一种称为 虚拟寻址(Virtual Addressing) 的寻址方式。使用虚拟寻址,CPU 需要将虚拟地址翻译成物理地址,这样才能访问到真实的物理内存。 实际上完成虚拟地址转换为物理地址转换的硬件是 CPU 中含有一个被称为 内存管理单元(Memory Management Unit, MMU) 的硬件。
先从没有虚拟地址空间的时候说起吧!单⽚机的 CPU 是直接操作内存的「物理地址」。但是这样有什么问题呢?

用户程序可以访问任意内存,寻址内存的每个字节,这样就很容易(有意或者无意)破坏操作系统,造成操作系统崩溃。
在这种情况下,要想在内存中同时运⾏两个程序是不可能的。如果第⼀个程序在 2000 的位置写⼊⼀个新的值,将会擦掉第⼆个程序存放在相同位置上的所有内容,所以同时运⾏两个程序是根本⾏不通的,这两个程序会⽴刻崩溃。
总结来说:如果直接把物理地址暴露出来的话会带来严重问题,比如可能对操作系统造成伤害以及给同时运行多个程序造成困难。

通过虚拟地址访问内存有以下优势:

程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区。
程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理器会将物理内存页(通常大小为 4 KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动。
不同进程使用的虚拟地址彼此隔离。一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存。
四十四.虚拟内存的技术实现

虚拟内存的实现需要建立在离散分配的内存管理方式的基础上。 虚拟内存的实现有以下三种方式:

请求分段存储管理 :建立在分段存储管理之上,增加了请求调段功能、分段置换功能。请求分段储存管理方式就如同请求分页储存管理方式一样,在作业开始运行之前,仅装入当前要执行的部分段即可运行;在执行过程中,可使用请求调入中断动态装入要访问但又不在内存的程序段;当内存空间已满,而又需要装入新的段时,根据置换功能适当调出某个段,以便腾出空间而装入新的段。
请求分页存储管理 :建立在分页管理之上,为了支持虚拟存储器功能而增加了请求调页功能和页面置换功能。请求分页是目前最常用的一种实现虚拟存储器的方法。请求分页存储管理系统中,在作业开始运行之前,仅装入当前要执行的部分段即可运行。假如在作业运行的过程中发现要访问的页面不在内存,则由处理器通知操作系统按照对应的页面置换算法将相应的页面调入到主存,同时操作系统也可以将暂时不用的页面置换到外存中。
请求段页式存储管理
这里多说一下?很多人容易搞混请求分页与分页存储管理,两者有何不同呢?

请求分页存储管理建立在分页管理之上。他们的根本区别是是否将程序全部所需的全部地址空间都装入主存,这也是请求分页存储管理可以提供虚拟内存的原因,我们在上面已经分析过了。

它们之间的根本区别在于是否将一作业的全部地址空间同时装入主存。请求分页存储管理不要求将作业全部地址空间同时装入主存。基于这一点,请求分页存储管理可以提供虚存,而分页存储管理却不能提供虚存。

不管是上面那种实现方式,我们一般都需要:

一定容量的内存和外存:在载入程序的时候,只需要将程序的一部分装入内存,而将其他部分留在外存,然后程序就可以执行了;
缺页中断:如果需执行的指令或访问的数据尚未在内存(称为缺页或缺段),则由处理器通知操作系统将相应的页面或段调入到内存,然后继续执行程序;
虚拟地址空间 :逻辑地址到物理地址的变换。
----------------------------------------------------------------------------------------------分割线------------------------------------------------------------------------------------------------------
文件系统
四十五.文件的存储

有连续空间存放方式和非连续空间存放方式,非连续空间存放方式又分为链表方式和索引方式

连续空间存放方式:⽂件存放在磁盘「连续的」物理空间中。⽂件的数据都是紧密相连,读写效率很⾼,因为⼀次磁盘寻道就可以读出整个⽂件。使⽤连续存放的⽅式有⼀个前提,必须先知道⼀个⽂件的⼤⼩,这样⽂件系统才会根据⽂件的⼤⼩在磁盘上找到⼀块连续的空间分配给⽂件。所以,⽂件头⾥需要指定「起始块的位置」和「⻓度」。缺点是有磁盘空间碎片和文件长度不易扩展
非连续空间存放方式分为链表式和索引式
链表的⽅式存放是离散的,不⽤连续的,于是就可以消除磁盘碎⽚,可⼤⼤提⾼磁盘空间的利⽤率,同时⽂件的⻓度可以动态扩展。根据实现的⽅式的不同,链表可分为「隐式链表」和「显式链接」两种形式。⽂件要以「隐式链表」的⽅式存放的话,实现的⽅式是⽂件头要包含「第⼀块」和「最后⼀块」的位置,并且每个数据块⾥⾯留出⼀个指针空间,⽤来存放下⼀个数据块的位置,
隐式链表的存放⽅式的缺点在于⽆法直接访问数据块,只能通过指针顺序访问⽂件,以及数据块指针消耗了⼀定的存储空间。隐式链接分配的稳定性较差,系统在运⾏过程中由于软件或者硬件错误导致链表中的指针丢失或损坏,会导致⽂件数据的丢失。
如果取出每个磁盘块的指针,把它放在内存的⼀个表中,就可以解决上述隐式链表的两个不⾜。那么,这种实现⽅式是「显式链接」,它指把⽤于链接⽂件各数据块的指针,显式地存放在内存的⼀张链接表中,该表在整个磁盘仅设置⼀张,每个表项中存放链接指针,指向下⼀个数据块号。
索引的实现是为每个⽂件创建⼀个「索引数据块」,⾥⾯存放的是指向⽂件数据块的指针列表。索引的⽅式优点在于:⽂件的创建、增⼤、缩⼩很⽅便;
不会有碎⽚的问题;⽀持顺序读写和随机读写;由于索引数据也是存放在磁盘块的,如果⽂件很⼩,明明只需⼀块就可以存放下,但还是需要额外分配⼀块来存放索引数据,所以缺陷之⼀就是存储索引带来的开销。
现在一般采用链表 + 索引的组合,这种组合称为「链式索引块」,它的实现⽅式是在索引数据块留出⼀个存放下⼀个索引数据块的指针

四十六.硬链接和软连接
1)硬链接
多个⽬录项中的「索引节点」指向⼀个⽂件,也就是指向同⼀个 inode,但是 inode 是不可能跨越⽂件系统的,每个⽂件系统都有各⾃的 inode 数据结构和列表,所以硬链接是不可⽤于跨⽂件系统的。由于多个⽬录项都是指向⼀个 inode,那么只有删除⽂件的所有硬链接以及源⽂件时,系统才会彻底删除该⽂件。
不足:1)不可以在不同文件系统的文件间建立链接;2)只有超级用户才可以为目录创建硬链接。
2)软链接
软链接又称符号链接,包含的是原文件inode的记录。没有任何文件系统的限制,任何用户可以创建指向目录的符号链接。删除软链接不影响被指向的文件,但若原文件被删除,则软链接将失效
不足:因为链接文件包含有原文件的路径信息,所以当原文件从一个目录下移到其他目录中,再访问链接文件,系统就找不到了,而硬链接就没有这个缺陷,你想怎么移就怎么移;还有它要系统分配额外的空间用于建立新的索引节点和保存原文件的路径。

实际场景下,基本是使用软链接。

总结区别如下:

软链接文件与原文件拥有不同的inode号,即表明是两个不同的文件;硬链接文件和原文件共用一个inode号;
本质上硬链接文件和原文件是完全平等关系,而软链接则可理解为快捷方式
----------------------------------------------------------------------------------------------------------------------------分割线----------------------------------------------------------------------------------------------------------------
设备管理
四十七.键盘敲⼊A 字⺟时,操作系统期间发⽣了什么?
当⽤户输⼊了键盘字符,键盘控制器就会产⽣扫描码数据,并将其缓冲在键盘控制器的寄存器中,紧接键盘控制器通过总线给 CPU 发送中断请求。CPU 收到中断请求后,操作系统会保存被中断进程的 CPU 上下⽂,然后调⽤键盘的中断处理程序。键盘的中断处理程序是在键盘驱动程序初始化时注册的,那键盘中断处理函数的功能就是从键盘控制器的寄存器的缓冲区读取扫描码,再根据扫描码找到⽤户在键盘输⼊的字符,如果输⼊的字符是显示字符,那就会把扫描码翻译成对应显示字符的 ASCII 码,⽐如⽤户在键盘输⼊的是字⺟ A,是显示字符,于是就会把扫描码翻译成 A 字符的 ASCII 码。得到了显示字符的 ASCII 码后,就会把 ASCII 码放到「读缓冲区队列」,接下来就是要把显示字符显示屏幕了,显示设备的驱动程序会定时从「读缓冲区队列」读取数据放到「写缓冲区队列」,最后把「写缓冲区队列」的数据⼀个⼀个写⼊到显示设备的控制器的寄存器中的数据缓冲区,最后将这些数据显示在屏幕⾥。
显示出结果后,恢复被中断进程的上下⽂。
四十八.什么是中断,中断的处理流程和解决方案
中断,通知操作系统数据已经准备好了。我们⼀般会有⼀个硬件的中断控制器,当设备完成任务后触发中断到中断控制器,中断控制器就通知 CPU,⼀个中断产⽣了,CPU 需要停下当前⼿⾥的事情来处理中断。中断有两种,⼀种软中断,例如代码调⽤ INT 指令触发,⼀种是硬件中断,就是硬件通过中断控制器触发的。
中断处理程序的处理流程:

  1. 在 I/O 时,设备控制器如果已经准备好数据,则会通过中断控制器向 CPU 发送中断请求;
  2. 保护被中断进程的 CPU 上下⽂;
  3. 转⼊相应的设备中断处理函数;
  4. 进⾏中断处理;
  5. 恢复被中断进程的上下⽂;
    对于中断的解决方法是DMA: CPU 当要读取磁盘数据的时候,只需给 DMA 控制器发送指令,然后返回去做其他事情,当磁盘数据拷⻉到内存后,DMA 控制机器通过中断的⽅式,告诉 CPU 数据已经准备好了,可以从内存读数据了。仅仅在传送开始和结束时需要 CPU ⼲预。
    四十九.Linux 内存⽀持 5 种 I/O 调度算法
    第⼀种,没有调度算法,是的,你没听错,它不对⽂件系统和应⽤程序的 I/O 做任何处理,这种算法常⽤在虚拟机 I/O 中,此时磁盘 I/O 调度算法交由物理机系统负责。
    第⼆种,先⼊先出调度算法,这是最简单的 I/O 调度算法,先进⼊ I/O 调度队列的 I/O 请求先发⽣。
    第三种,完全公平调度算法,⼤部分系统都把这个算法作为默认的 I/O 调度器,它为每个进程维护了⼀个I/O 调度队列,并按照时间⽚来均匀分布每个进程的 I/O 请求。
    第四种,优先级调度算法,顾名思义,优先级⾼的 I/O 请求先发⽣, 它适⽤于运⾏⼤量进程的系统,像是桌⾯环境、多媒体应⽤等。
    第五种,最终期限调度算法,分别为读、写请求创建了不同的 I/O 队列,这样可以提⾼机械磁盘的吞吐量,并确保达到最终期限的请求被优先处理,适⽤于在 I/O 压⼒⽐较⼤的场景,⽐如数据库等
    -----------------------------------------------------------------------------------------------------------------分割线------------------------------------------------------------------------------------------------------------------------
    网络系统篇
    五十.Linux接受和发送网络包的流程
    当有⽹络包到达时,⽹卡发起硬件中断,于是会执⾏⽹卡硬件中断处理函数,中断处理函数处理完需要「暂时屏蔽中断」,然后唤醒「软中断」来轮询处理数据,直到没有新数据时才恢复中断,这样⼀次中断处理多个⽹络包,于是就可以降低⽹卡中断带来的性能开销。
    Linux接受网络包的流程

Linux发送包流程

左边是接收右边是发送

五十一.I/O的多路复用
I/O 的多路复⽤,可以只在⼀个进程⾥处理多个⽂件的 I/O,Linux 下有
三种提供 I/O 多路复⽤的 API,分别是: select、poll、epoll。
五十二.简述 select
select是一种多路复用技术。其收到所有输入的文件描述符,返回哪些文件有新数据。将已连接的 Socket 都放到⼀个⽂件描述符集合,然后调⽤ select 函数将⽂件描述符集合拷⻉到内核⾥,让内核来检查是否有⽹络事件产⽣,检查的⽅式很粗暴,就是通过遍历⽂件描述符集合的⽅式,当检查到有事件产⽣后,将此 Socket 标记为可读或可写, 接着再把整个⽂件描述符集合拷⻉回⽤户态⾥,然后⽤户态还需要再通过遍历的⽅法找到可读或可写的 Socket,然后再对其处理。

其可以设置为阻塞或者非阻塞状态,底层采用1024位bitmap做实现,因此有文件描述符上限数。

五十三.简述poll
poll是一种多路复用技术。其收到所有输入的文件描述符,返回哪些文件有新数据。其通过链表代替了之前select的数据结构,使得其没有上限限制。但是 poll 和 select 并没有太⼤的本质区别,都是使⽤「线性结构」存储进程关注的 Socket 集合,因此都需要遍历⽂件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),⽽且也需要在⽤户态与内核态之间拷⻉⽂件描述符集合,这种⽅式随着并发数上来,性能的损耗会呈指数级增⻓

五十四.简述epoll
通过两个⽅⾯解决了 select/poll 的问题。
epoll 在内核⾥使⽤「红⿊树」来关注进程所有待检测的 Socket,红⿊树是个⾼效的数据结构,增删查⼀般时间复杂度是 O(logn),通过对这棵⿊红树的管理,不需要像 select/poll 在每次操作时都传⼊整个 Socket 集合,减少了内核和⽤户空间⼤量的数据拷⻉和内存分配。
epoll 使⽤事件驱动的机制,内核⾥维护了⼀个「链表」来记录就绪事件,只将有事件发⽣的 Socket集合传递给应⽤程序,不需要像 select/poll 那样轮询扫描整个集合(包含有和⽆事件的 Socket ),⼤⼤提⾼了检测的效率。
⽽且,epoll ⽀持边缘触发和⽔平触发的⽅式,⽽ select/poll 只⽀持⽔平触发,⼀般⽽⾔,边缘触发的⽅式会⽐⽔平触发的效率⾼。
⽔平触发的意思是只要满⾜事件的条件,⽐如内核中有数据需要读,就⼀直不断地把这个事件传递给⽤户;⽽边缘触发的意思是只有第⼀次满⾜条件的时候才触发,之后就不会再传递同样的事件了。
多路复⽤ API 返回的事件并不⼀定可读写的,如果使⽤阻塞 I/O, 那么在调⽤
read/write 时则会发⽣程序阻塞,因此最好搭配⾮阻塞 I/O,以便应对极少数的特殊情况。
五十五.多线程模型
基于最原始的阻塞⽹络 I/O, 如果服务器要⽀持多个客户端,其中⽐较传统的⽅式,就是使⽤多进程模型,也就是为每个客户端分配⼀个进程来处理请求。服务器的主进程负责监听客户的连接,⼀旦与客户端连接完成,accept() 函数就会返回⼀个「已连接Socket」,这时就通过 fork() 函数创建⼀个⼦进程,实际上就把⽗进程所有相关的东⻄都复制⼀份。这两个进程刚复制完的时候,⼏乎⼀摸⼀样。不过,会根据返回值来区分是⽗进程还是⼦进程,如果返回值是 0,则是⼦进程;如果返回值是其他的整数,就是⽗进程。正因为⼦进程会复制⽗进程的⽂件描述符,于是就可以直接使⽤「已连接 Socket 」和客户端通信了,可以发现,⼦进程不需要关⼼「监听 Socket」,只需要关⼼「已连接 Socket」;⽗进程则相反,将客户服务交给⼦进程来处理,因此⽗进程不需要关⼼「已连接 Socket」,只需要关⼼「监听 Socket」。
当「⼦进程」退出时,实际上内核⾥还会保留该进程的⼀些信息,也是会占⽤内存的,如果不做好“回收”⼯作,就会变成僵⼫进程,随着僵⼫进程越多,会慢慢耗尽我们的系统资源。
那么有两种⽅式可以在⼦进程退出后回收资源,分别是调⽤ wait() 和 waitpid() 函数。
这种⽤多个进程来应付多个客户端的⽅式,在应对 100 个客户端还是可⾏的,但是当客户端数量⾼达⼀万时,肯定扛不住的,因为每产⽣⼀个进程,必会占据⼀定的系统资源,⽽且进程间上下⽂切换的“包袱”是很重的,性能会⼤打折扣。

多对一模型。将多个用户级线程映射到一个内核级线程上。该模型下,线程在用户空间进行管理,效率较高。缺点就是一个线程阻塞,整个进程内的所有线程都会阻塞。几乎没有系统继续使用这个模型。

一对一模型。将内核线程与用户线程一一对应。优点是一个线程阻塞时,不会影响到其它线程的执行。该模型具有更好的并发性。缺点是内核线程数量一般有上限,会限制用户线程的数量。更多的内核线程数目也给线程切换带来额外的负担。linux和Windows操作系统家族都是使用一对一模型。

多对多模型。将多个用户级线程映射到多个内核级线程上。结合了多对一模型和一对一模型的特点。
五十六.socket模型
(1)针对TCP协议通信的socket编程模型
服务端和客户端初始化 socket ,得到⽂件描述符;
服务端调⽤ bind ,将绑定在 IP 地址和端⼝;绑定端⼝的⽬的:当内核收到 TCP 报⽂,通过 TCP 头⾥⾯的端⼝号,来找到我们的应⽤程序,然后把数据传递给我们。
服务端调⽤ listen ,进⾏监听;
服务端调⽤ accept ,等待客户端连接;
客户端调⽤ connect ,向服务器端的地址和端⼝发起连接请求;
服务端 accept 返回⽤于传输的 socket 的⽂件描述符;
客户端调⽤ write 写⼊数据;服务端调⽤ read 读取数据;
客户端断开连接时,会调⽤ close ,那么服务端 read 读取数据的时候,就会读取到了 EOF ,待处理完数据后,服务端调⽤ close ,表示连接关闭。
需要注意的是,服务端调⽤ accept 时,连接成功了会返回⼀个已完成连接的 socket,后续⽤来传输数据。所以,监听的 socket 和真正⽤来传送数据的 socket,是「两个」 socket,⼀个叫作监听 socket,⼀个叫作已完成连接 socket。
在 TCP 连接的过程中,服务器的内核实际上为每个 Socket 维护了两个队列:
⼀个是还没完全建⽴连接的队列,称为 TCP 半连接队列,这个队列都是没有完成三次握⼿的连接,此时服务端处于 syn_rcvd 的状态;
⼀个是⼀件建⽴连接的队列,称为 TCP 全连接队列,这个队列都是完成了三次握⼿的连接,此时服务端处于 established 状态;
在 TCP 三次握⼿过程中,当服务器收到客户端的 SYN 包后,内核会把该连接存储到半连接队列,然后再向客户端发送 SYN+ACK 包,接着客户端会返回 ACK,服务端收到第三次握⼿的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其增加到全连接队列 ,等待进程调⽤ accept() 函数时把连接取出来。
当 TCP 全连接队列不为空后,服务端的 accept() 函数,就会从内核中的 TCP 全连接队列⾥拿出⼀个已经完成连接的 Socket 返回应⽤程序,后续数据传输都⽤这个 Socket。

(2)针对UDP协议通信的socket编程模型
UDP 是没有连接的,所以不需要三次握⼿,也就不需要像 TCP 调⽤ listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端⼝号,因此也需要 bind。
对于 UDP 来说,不需要要维护连接,那么也就没有所谓的发送⽅和接收⽅,甚⾄都不存在客户端和服务端的概念,只要有⼀个 socket 多台机器就可以任意通信,因此每⼀个 UDP 的 socket 都需要 bind。另外,每次通信时,调⽤ sendto 和 recvfrom,都要传⼊⽬标主机的 IP 地址和端⼝。
(3)针对本地进程间通信的socket编程模型
本地 socket 被⽤于在同⼀台主机上进程间通信的场景:
本地 socket 的编程接⼝和 IPv4 、IPv6 套接字编程接⼝是⼀致的,可以⽀持「字节流」和「数据报」两种协议;
本地 socket 的实现效率⼤⼤⾼于 IPv4 和 IPv6 的字节流、数据报 socket 实现;
对于本地字节流 socket,其 socket 类型是 AF_LOCAL 和 SOCK_STREAM。
对于本地数据报 socket,其 socket 类型是 AF_LOCAL 和 SOCK_DGRAM。
本地字节流 socket 和 本地数据报 socket 在 bind 的时候,不像 TCP 和 UDP 要绑定 IP 地址和端⼝,⽽是绑定⼀个本地⽂件,这也就是它们之间的最⼤区别。

你可能感兴趣的:(java)