1. 操作系统基本特征
(1) 并发
- 并发是指宏观上在一段时间内能同时运行多个程序,而并行则指同一时刻能运行多个指令。
- 并行需要硬件支持,如多流水线、多核处理器或者分布式计算系统
- 操作系统通过引入进程和线程,使得程序能够并发运行。
(2) 共享
- 共享是指系统中的资源可以被多个并发进程共同使用。
- 有两种共享方式:互斥共享和同时共享。
- 互斥共享的资源称为临界资源,例如打印机等,在同一时刻只允许一个进程访问,需要用同步机制来实现互斥访问。
(3)虚拟
- 虚拟技术把一个物理实体转换为多个逻辑实体。
- 主要有两种虚拟技术:时(时间)分复用技术和空(空间)分复用技术。
- 多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占用处理器,每次只执行一个时间片并快速切换。
- 虚拟内存使用了空分复用技术,它将物理内存抽象为地址空间,每个进程都有各自的地址空间。地址空间的页被映射到物理内存,地址空间的页并不需要全部在物理内存中,当使用到一个没有在物理内存的页时,执行页面置换算法,将该页置换到内存中。
(4)异步
- 异步指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。
2. 操作系统基本功能
操作系统的基本功能:进程管理、内存管理、文件管理与设备管理。
(1)进程管理
- 进程控制、进程同步、进程通信、死锁处理、处理机调度等。
(2)内存管理
- 内存分配、地址映射、内存保护与共享、虚拟内存等。
(3)文件管理
- 文件存储空间的管理、目录管理、文件读写管理和保护等。
(4)设备管理
- 完成用户的 I/O 请求,方便用户使用各种设备,并提高设备的利用率。
- 主要包括缓冲管理、设备分配、设备处理、虛拟设备等。
3. 系统调用
介绍系统调用之前,我们先来了解一下用户态和系统态。根据进程访问资源的特点,操作系统需要两种CPU状态:
- 内核态:运行操作系统程序,cpu可以访问内存的所有数据,包括外围设备,例如硬盘,网卡,cpu也可以将自己从一个程序切换到另一个程序。
- 用户态:只能受限的访问内存,运行用户程序,且不允许访问外围设备,占用cpu的能力被剥夺,cpu资源可以被其他程序获取。
拓展:指令划分
- 特权指令:只能由操作系统使用、用户程序不能使用的指令。 举例:启动I/O 内存清零 修改程序状态字 设置时钟 允许/禁止终端 停机
- 非特权指令:用户程序可以使用的指令。 举例:控制转移 算数运算 取数指令 访管指令(使用户程序从用户态陷入内核态)
特权级别:
- 特权环:R0、R1、R2和R3
- R0相当于内核态,R3相当于用户态;
- 不同级别能够运行不同的指令集合;
系统调用:如果一个进程在用户态需要使用内核态的功能,就进行系统调用从而陷入内核,由操作系统代为完成。
也就是说在我们运行的用户程序中,凡是与系统态级别的资源有关的操作(如文件管理、进程控制、内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成。
系统调用和普通库函数调用非常相似,只是系统调用由操作系统核心提供,运行于系统态,而普通的函数调用由函数库或用户自己提供,运行于用户态。
4. 内核态与用户态,之间如何转换
为什么要有用户态和内核态?
- 由于需要限制不同的程序之间的访问能力, 防止他们获取别的程序的内存数据, 或者获取外围设备的数据, 并发送到网络, CPU划分出两个权限等级 -- 用户态和内核态。主要是安全考虑。
1)用户态切换到内核态的3种方式:
a. 系统调用
- 这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linux的int 80h中断。
b. 异常
- 当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
c. 外围设备的中断
- 当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
这3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。
内核态切换到用户态的途径——>设置程序状态字PSW,在PSW中有一个二进制位控制这两种模式。
内核态与用户态的区别:
- 内核态与用户态是操作系统的两种运行级别,当程序运行在3级特权级上时,就可以称之为运行在用户态;当程序运行在0级特权级上时,就可以称之为运行在内核态。
- 运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态。
- 处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理机是可被抢占的 ;而处于核心态执行中的进程,则能访问所有的内存空间和对象,且所占有的处理机是不允许被抢占的。
5. 大内核和微内核
1.大内核
- 大内核是将操作系统功能作为一个紧密结合的整体放到内核。
- 由于各模块共享信息,因此有很高的性能。
2.微内核
- 由于操作系统不断复杂,因此将一部分操作系统功能移出内核,从而降低内核的复杂性。移出的部分根据分层的原则划分成若干服务,相互独立。
- 在微内核结构下,操作系统被划分成小的、定义良好的模块,只有微内核这一个模块运行在内核态,其余模块运行在用户态。
- 因为需要频繁地在用户态和核心态之间进行切换,所以会有一定的性能损失。
进程与线程
1. 中断
- “中断” 是让【操作系统内核】夺回CPU使用权的唯一途径。
- 基本方式:CPU中断正在运行的程序,转到处理中断事件程序。
- 为什么在操作系统中设计了中断的概念?为了提高并发执行的效率。
- 中断有三种方式:外部中断、异常与陷入。
(1)外中断
- 由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。
(2)异常
- 由 CPU 执行指令的内部事件引起,如非法操作码、地址越界、算术溢出等。
(3)陷入
- 在用户程序中使用系统调用。
2. 进程与线程
进程是什么?
- 进程是资源分配的基本单位。
- 进程组成:PCB(进程描述信息、控制管理信息、资源分配信息等)、程序段(程序中代码)、数据段(运行过程中产生的各种数据)
- 进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。
PCB(Process Control Block)包含的信息:
(1)进程标识符信息。进程标识符用于惟一地标识一个进程。一个进程,通常有以下两个标识符:外部标识符,内部标识符。
(2)处理机状态信息。处理机状态信息主要是由处理机各种寄存器中的内容所组成。
(3)进程调度信息。在PCB中还存放了一些与进程调度和进程对换有关的信息,包括:进程状态、进程优先级、进程调度所需要的其他信息、事件。
(4)进程控制信息。进程控制信息包括:程序和数据的地址、进程同步和通信机制、资源清单、链接指针。
进程的组织形式:
- 链接方式:按照进程状态将PCB分为多个队列,操作系统持有指向各个队列的指针
- 索引方式:按照进程状态的不同,建立几张索引表,操作系统持有各个索引表的指针
进程的最大线程数
(1)32位windows下,一个进程空间4G,内核占2G,留给用户只有2G,一个线程默认栈是1M,所以一个进程最大开2048个线程。当然内存不会完全拿来做线程的栈,所以最大线程数实际值要小于2048,大概2000个。
(2)32位Linux下,一个进程空间4G,内核占1G,用户留3G,一个线程默认8M,所以
最多380个左右线程。
进程在操作系统中的主要状态
(1)就绪状态(ready):已经具备运行条件,因为其他线程正在运行而停止或者时间片已经用完,等待被调度(等待获取时间片)
(2)运行状态(running):占用cpu
(3)阻塞状态(waiting):等待资源/事件(如等待输入/输出而暂停运行)
ps:还有创建状态和结束状态。相互转换关系如下:
进程状态切换
注意:不能由阻塞态直接转换为运行态,也不能由就绪态直接抓换为阻塞态(因为进入阻塞态是进程主动请求的,必然需要进程在运行时才能发出这种请求);运行态到阻塞态是一种进程自身做出的主动行为;
线程是什么?
- 线程是cpu调度(程序执行)的基本单位。
- 一个进程中可以有多个线程,它们共享进程资源。例如:QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。
总结:两者的区别?
- 进程是资源分配的最小单位,线程是程序执行(cpu调度)的最小单位。
- 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,拥有系统资源;线程不拥有系统资源,但共享进程中的数据、地址空间。
- 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据;进程之间的通信需要以通信的方式(IPC(Inter-Process Communication))进行。
- 多进程程序更健壮,当进程中的一个线程奔溃时,会导致其所属进程的所有线程奔溃。但是,进程有自己独立的地址空间(进程之间互不干扰)。
- 线程相对进程能减少并发执行的时间和空间开销;
ps:协程,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换(上下文切换)那样消耗资源。
3. 什么是上下文切换?
- 当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
- 系统中的每个程序都是运行在某个进程的上下文中的。上下文是由程序正确运行所需的状态组成的,这个状态包括存放在存储器中的程序的代码和数据,他的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。
上下文切换的什么?
- 进程是由内核管理和调度的,所以进程的切换只能发生在内核态。
- 所以,进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
- 通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行。
上下文切换场景?
- 为了保证所有进程可以得到公平调度,CPU 时间被划分为一段段的时间片,这些时间片再被轮流分配给各个进程。这样,当某个进程的时间片耗尽了,就会被系统挂起,切换到其它正在等待 CPU 的进程运行;
- 进程在系统资源不足(比如内存不足)时,要等到资源满足后才可以运行,这个时候进程也会被挂起,并由系统调度其他进程运行;
- 当进程通过睡眠函数 sleep 这样的方法将自己主动挂起时,自然也会重新调度;
- 当有优先级更高的进程运行时,为了保证高优先级进程的运行,当前进程会被挂起,由高优先级进程来运行;
- 发生硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序;
进程切换与线程切换的区别?
- 进程切换就是上下文切换。虚拟内存是操作系统为每个进程提供的一种抽象,每个进程都有自己的虚拟地址空间。进程内的所有线程共享进程的虚拟地址空间。
- 进程切换和线程切换最主要的一个区别在于进程切换涉及虚拟地址空间的切换而线程不会,所以进程切换开销较大,耗时。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。
4. 进程控制(状态切换)
(1)进程的创建
- 为新进程分配一个唯一的进程标识号,并申请一个空白的 PCB,若申请失败则创建失败。
- 为新进程的程序和数据分配内存空间,若资源不足会进入阻塞态。
- 初始化 PCB,主要包括标志信息、处理机状态信息、以及设置进程优先级等。
- 若进程就绪队列未满,就将新进程插入就绪队列,等待被调度运行。
(2)进程的终止
进程终止包括:正常结束,表示进程已经完成并准备退出;异常结束,表示进程在运行时发生异常,程序无法继续运行,例如非法指令,IO 故障等;外界干预,指进程因为外界请求而终止,例如操作系统干预等。
- 根据被终止进程的标识符,检索 PCB,读出该进程的状态。
- 若被终止的进程处于执行状态,终止执行,将处理机资源分配给其他进程。
- 若进程还有子进程,将所有子进程终止。
- 将该进程的全部资源归还给父进程或操作系统,并将 PCB 从队列删除。
(3)进程的阻塞与唤醒
正在执行的进程由于等待的事件未发生,由系统执行阻塞原语,由运行态变为阻塞态。阻塞过程:
- 找到将要被阻塞进程的 PCB。
- 如果进程为运行态,保护现场并转为阻塞态,停止运行。
- 把 PCB 插入相应事件的等待队列,当被阻塞进程期待的事件发生时,由相关进程调用唤醒原语,将进程唤醒。
唤醒过程:
- 在该事件的等待队列中找到进程对应的 PCB。
- 将其从等待队列中移除,设置状态为就绪态。
- 将 PCB 插入就绪队列,等待调度程序调度。
(4)进程切换
进程切换是指CPU从一个进程的运行转到另一个进程上运行。进程切换过程:
- 保存处理机上下文,包括程序计数器和其他寄存器。
- 更新 PCB 信息,并把 PCB 移入相应的阻塞队列。
- 选择另一个进程执行并更新其 PCB。
- 更新内存管理的数据结构,恢复处理机上下文。
5.进程调度算法
进程调度算法评价指标:
image.png
说明:不同环境的调度算法目标不同,因此需要针对不同环境来讨论调度算法。
批处理系统
批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。
(1)先来先服务 first-come first-serverd(FCFS)
- 非抢占式的调度算法,按照请求的顺序进行调度。
- 有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。
(2)短作业优先 shortest job first(SJF)
- 非抢占式的调度算法,按估计运行时间最短的顺序进行调度。
- 长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。
(3)最短剩余时间优先 shortest remaining time next(SRTN)
- 最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。
- 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。
交互式系统
交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应。
(1)时间片轮转
- 将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。
时间片轮转算法的效率和时间片的大小有很大关系:
- 因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。
- 而如果时间片过长,那么实时性就不能得到保证。
(2)优先级调度
- 为每个进程分配一个优先级,按优先级进行调度。
- 为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。
(3)多级反馈队列
- 一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。
- 多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。
- 每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。
实时系统
- 实时系统要求一个请求在一个确定时间内得到响应。
- 分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。
6.进程通信与同步
进程同步与进程通信很容易混淆,它们的区别在于:
(1)进程同步:控制多个进程按一定顺序执行,一种目的;
(2)进程通信:进程间传输信息(包括进程同步所需要的信息,是一种手段)。
进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。
进程间的通信方式?
进程之间的通信方式有管道、FIFO(命名管道),消息队列、信号量、共享内存与套接字。
(1)管道
- 管道是通过调用 pipe 函数创建的,fd[0] 用于读,fd[1] 用于写。它具有以下限制:
- 只支持半双工通信(单向交替传输);
- 只能在父子进程或者兄弟进程中使用。
(2)FIFO(命名管道)
- 也称为命名管道,去除了管道只能在父子进程中使用的限制。
- FIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据。
(3)消息队列
- 消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难;
- 避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法;
- 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。
(4)信号量
- 它是一个计数器,用于为多个进程提供对共享数据对象的访问。
(5)共享内存
- 共享内存在系统内存中开辟一块内存区,分别映射到各个进程的虚拟地址空间中,任何一个进程操作了内存区都会反映到其他进程中。因为数据不需要在进程之间复制,所以这是最快的一种 IPC。需要使用信号量用来同步对共享存储的访问。
- 多个进程可以将同一个文件映射到它们的地址空间从而实现共享内存。当对共享内存的使用结束之后,这个映射关系将被删除。
(6)套接字(socket)远程调用
- 与其它通信机制不同的是,它可用于不同机器间的进程通信。
- socket编程是一个宽泛的说法, tcp,udp,http是我们经常用的一些网络协议。它的限制主要在与带宽,网络延时和连接数量的限制等。
总结:IPC进程通信
同一主机:
- pipe 管道,一个写入管道文件,一个读(单向)
- socket 套接字文件,进程间交换数据(双工工作方式)
- signal 信号
- shm shared memory,共享内存
- semaphore 信号量,一种计数器,分配资源
不同主机:
- socket ip 和端口号
- RPC 远程过程调用
- MQ 消息队列,如:Kafka , RabbitMQ,ActiveMQ
为什么要进程同步?
多进程虽然提高了系统资源利用率和吞吐量,但是进程的异步性(可能是多次执行完成)可能造成系统的混乱。
进程同步的任务就是对多个相关进程在执行顺序上进行协调,使并发执行的多个进程之间可以有效的共享资源和相互合作,保证程序执行的可再现性。
(1)临界区
- 临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。许多物理设备都属于临界资源,如输入机、打印机、磁带机等。
(2)同步与互斥
- 同步:多个进程因为合作产生的直接制约关系,使得进程有一定的先后执行关系。
- 互斥:多个进程在同一时刻只有一个进程能进入临界区。
(3)信号量
- 信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。
- P : 如果信号量大于 0 ,执行 -1 操作;如果信号量等于 0,进程睡眠,等待信号量大于 0;申请资源
- V :对信号量执行 +1 操作,唤醒睡眠的进程让其完成 P操作。释放资源
- P和 V操作需要被设计成原语,不可分割,通常的做法是在执行这些操作的时候屏蔽中断。
- 如果信号量的取值只能为 0 或者 1,那么就成为了互斥量(Mutex),0 表示临界区已经加锁,1 表示临界区解锁。
(4)管程
- 使用信号量机制实现的生产者消费者问题需要客户端代码做很多控制,而管程把控制的代码独立出来,不仅不容易出错,也使得客户端代码调用更容易。
- 管程有一个重要特性:在一个时刻只能有一个进程使用管程。进程在无法继续执行的时候不能一直占用管程,否则其它进程永远不能使用管程。
- 管程引入了条件变量以及相关的操作:wait()和 signal()来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞,把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。
7.线程的通信方式
线程通信主要通过共享内存和消息传递两种模型实现的,具体来说线程通信常用的方式有:
(1)Volatile 内存共享
volatile有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。volatile语义保证线程可见性有两个原则保证:
- 所有volatile修饰的变量一旦被某个线程更改,必须立即刷新到主内存
- 所有volatile修饰的变量在使用之前必须重新读取主内存的值
两个线程操作一个对象时,如果不加volitile关键字,就会使一个线程一直循环等待,volatile解决了一个线程空等待的问题。
(2)wait/notify(等待/通知) 机制
Object类提供了线程间通信的方法:wait()、notify()、notifyaAl(),
等待通知机制是基于wait和notify方法来实现的,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待直到被通知或者被唤醒。
为什么必须获取锁?
因为调用wait方法时(使线程处于等待状态),必须要先释放锁,如果没有持有锁将会抛出异常。notify唤醒一个线程,但是还会持有线程的锁。ps:sleep使线程休眠一段时间,会持有线程的锁。
(3)CountDownLatch 并发工具
但是如果对于高并发情况下大量数据,当执行到某个业务逻辑节点时,需要唤醒另外一个线程对当前节点数据处理,使用notify通知,并不解决线程间的实时通信问题(notify不释放线程的锁)
8. 守护、僵尸、孤儿进程的概念
(1)守护进程:运行在后台的一种特殊进程,独立于控制终端并周期性地执行某些任务。
(2)僵尸进程:一个进程 fork 子进程,子进程退出,而父进程没有wait/waitpid子进程,那么子进程的进程描述符仍保存在系统中,这样的进程称为僵尸进程。
(3)孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,这些子进程称为孤儿进程。(孤儿进程将由 init 进程(进程号1)收养并对它们完成状态收集工作)
谈一谈fork()函数
在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。
在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
子进程会继承父进程的哪些东西?
子进程从父进程继承了:
(1)用户号UIDs和用户组号GIDs(2)进程组号(3)当前工作目录(4)根目录(5)环境(6)打开文件的描述符(7)共享内存(8)堆栈等
子进程与父进程不同的:
(1)进程号PID(2)各自的父进程号(3)自己的文件描述符和目录流的拷贝(4)子进程不继承父进程的进程正文, 数据和其它锁定内存(5)不继承异步输入和输出(6)父进程设置的锁
死锁
什么是死锁?典型:哲学家进餐问题。
- 并发环境下,各进程竞争资源造成相互等待对方手中的资源,导致各个进程都阻塞,都无法向前推进。
1.死锁、饥饿与死循环的区别
- 死锁:一定是互相等待对方手里的资源导致的,至少两个或者以上进程循环等待才能死锁,死锁一定处于阻塞态。
- 饥饿:长期获取不到想要的资源,可能只有一个进程。既可以是阻塞态(长期得不到io设备)或者就绪态(长期得不到处理机)
- 死循环:与上两种不同,死循环主要是逻辑错误或死循环导致。上边是由操作系统分配资源不合理导致的。
1.死锁产生条件(同时满足四个)
(1)互斥:每个资源要么已经分配给了一个进程,要么就是可用的(打印机)。
(2)请求与保持:已经得到了某个资源的进程,可以再请求新的资源,此时请求被阻塞,原资源保持不放。
(3)不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式(主动)地释放。
(4)环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。
2.处理方法
主要有以下四种方法:
- 鸵鸟策略
- 死锁检测与死锁恢复
- 死锁预防
- 死锁避免
(1)鸵鸟策略
- 因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。大多数操作系统,包括 Unix,Linux 和 Windows,处理死锁问题的办法仅仅是忽略它。
(2)死锁检测与死锁恢复
死锁检测算法: 用于检测系统状态,以确定系统中是否发生了死锁。
- 每种类型一个资源的死锁检测
- 每种类型多个资源的死锁检测
步骤:
- 首先为每个进程和每个资源指定一个唯一的号码;
- 然后建立资源分配表和进程等待表。
死锁恢复:解除死锁的主要方法有:
- 资源剥夺。 挂起( 暂时放到外存上) 某些死锁进程, 并抢占它的资源, 将这些资源分配给其他的死锁进程。
- 撤销进程。 强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。 这种方式的优点是实现简单,但所付出的代价可能会很大。
- 进程回退。让一个或多个死锁进程回退到足以避免死锁的地步。这就要求系统要记录进程的历史信息,设置还原点。
(3)死锁预防
在程序运行之前预防发生死锁。
- 破坏互斥条件。例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。
- 破坏占有和等待条件。一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。
- 破坏不可抢占条件。
- 破坏环路等待。给资源统一编号,进程只能按编号顺序来请求资源。
(4)死锁避免
避免死锁并不是事先采取某种限制措施破坏死锁的必要条件,而是在资源动态分配过程中,防止系统进入不安全状态,以避免发生死锁,比如银行家算法、系统安全状态、安全性算法。
系统安全状态
- 不安全状态可能会导致死锁,如果一次分配不会导致系统进入不安全状态,则将资源分配给进程,否则就让进程等待。
- 安全状态是指系统能按照某种进程推进顺序为每个进程分配资源,直到满足每个进程对资源的需求。
银行家算法
- 把操作系统视为银行家,资源视为资金,进程向操作系统申请资源相当于用户向银行家贷款。操作系统按照规则为进程分配资源,当进程首次申请资源时,要测试系统现存资源能否满足其最大需求量,可以则按申请量分配,否则推迟分配。
- 当进程在执行中继续申请资源时,先测试该进程已占有的资源数与申请的资源数之和是否超过该进程对资源的最大需求量,如果超过则拒绝分配,否则再测试系统现存的资源能否满足该进程尚需的最大资源量,如果满足则按申请量分配,否则推迟分配。
内存管理
1. 内存管理的方式
操作系统中的内存管理有三种,段式,页式,段页式。
为什么需要三种管理方式?
由于【连续内存分配方式】会导致【内存利用率偏低】以及【内存碎片】的问题,因此需要对这些离散的内存进行管理。引出了三种内存管理方式。
分页存储管理:
(1)基本分页存储管理中不具备页面置换功能,因此需要整个程序的所有页面都装入内存之后才可以运行。
(2)需要一个页表来记录逻辑地址(页号)和实际存储地址(物理块号)之间的映射关系,以实现从页号到物理块号的映射。
(3)由于页表也是存储在内存中的,因此内存数据需要两次的内存访问(一次是从内存中访问页表,从中找到指定的物理块号,加上页内偏移得到实际物理地址;第二次就是根据第一次得到的物理地址访问内存取出数据)。
(4)为了减少两次访问内存导致的效率影响,分页管理中引入了快表。当要访问内存数据的时候,首先将页号在快表中查询,如果在快表中,直接读取相应的物理块号;如果没有找到,那么访问内存中的页表,从页表中得到物理地址,同时将页表中的该映射表项添加到快表中。
(5)在某些计算机中如果内存的逻辑地址很大,将会导致程序的页表项会很多,而页表在内存中是连续存放的,所以相应的就需要较大的连续内存空间。为了解决这个问题,可以采用两级页表或者多级页表的方法,其中外层页表一次性调入内存且连续存放,内层页表离散存放。相应的访问内存页表的时候需要一次地址变换,访问逻辑地址对应的物理地址的时候也需要一次地址变换,而且一共需要访问内存3次才可以读取一次数据。引入多级页表的主要目的是为了避免把全部页表一直放在内存中占用过多空间(时间换空间),特别是那些根本就不需要的页表就不需要保留在内存中。多级页表属于时间换空间的典型场景。
分段存储管理:
分页是为了提高内存利用率,而分段是为了满足程序员在编写代码的时候的一些逻辑需求(比如数据共享,数据保护,动态链接等)。
(1)分段内存管理当中,地址是二维的,一维是段号,一维是段内地址;
(2)其中每个段的长度是不一样的,而且每个段内部都是从0开始编址的。由于分段管理中,每个段内部是连续内存分配,但是段和段之间是离散分配的,因此也存在一个逻辑地址到物理地址的映射关系,相应的就是段表机制。段表中的每一个表项记录了该段在内存中的起始地址和该段的长度。段表可以放在内存中也可以放在寄存器中。
(3)访问内存的时候根据段号和段表项的长度计算当前访问段在段表中的位置,然后访问段表,得到该段的物理地址,根据该物理地址以及段内偏移量就可以得到需要访问的内存。由于也是两次内存访问,所以分段管理中同样引入了联想寄存器。
分段和分页的对比
(1)页是信息的物理单位,是出于系统内存利用率的角度提出的离散分配机制;段是信息的逻辑单位,每个段含有一组意义完整的信息,是出于用户角度提出的内存管理机制。
(2)页的大小是固定的,由系统决定;段的大小是不确定的,由用户决定。
(3)页地址空间是一维的,段地址空间是二维的。
(4)分段比分页更容易实现信息的共享和保护,分页系统的虚拟内存功能。
段页存储方式
段页存储方式,既拥有分段系统的共享和保护,又拥有分页系统的虚拟内存功能。
(1)先将用户程序分为若干个段,然后再把每个段分成若干个页,并且为每一个段赋予一个段名称。这样在段页式管理中,一个内存地址就由段号,段内页号以及页内地址三个部分组成。
(2)段页式内存访问:系统中设置了一个段表寄存器,存放段表的起始地址和段表的长度。地址变换时,根据给定的段号(还需要将段号和寄存器中的段表长度进行比较防止越界)以及寄存器中的段表起始地址,就可以得到该段对应的段表项,从段表项中得到该段对应的页表的起始地址,然后利用逻辑地址中的段内页号从页表中找到页表项,从该页表项中的物理块地址以及逻辑地址中的页内地址拼接出物理地址,最后用这个物理地址访问得到所需数据。由于访问一个数据需要三次内存访问,所以段页式管理中也引入了高速缓冲寄存器。
如何实现逻辑地址到物理地址的映射管理?
- 分页机制的思想是:通过映射,可以使连续的线性地址与物理地址相关联,逻辑上连续的线性地址对应的物理地址可以不连续!
- 分页的作用 :(1)将线性地址转换为物理地址 (2)用大小相同的页替换大小不同的段。
- 逻辑地址 = 页号 + 页内偏移量,取到页号之后,查询页表,得到块号,然后在内存中通过块号&页内偏移量得到最终的物理地址
2. 页表项中各个位的作用,什么是缺页中断?
页表项中各个位的作用:
- 中断位: 用于判断该页是不是在内存中,如果是0,表示该页面不在内存中,会引起一个缺页中断
- 保护位(存取控制位):用于指出该页允许什么类型的访问(只读或者读写),如果用一位来标识的话:1表示只读,0表示读写
- 修改位(脏位):用于页面的换出,如果某个页面被修改过(即为脏),在淘汰该页时,必须将其写回磁盘,反之,可以直接丢弃该页
- 访问位:不论是读还是写(get or set),系统都会设置该页的访问位,它的值用来帮助操作系统在发生缺页中断时选择要被淘汰的页,即用于页面置换
- 高速缓存禁止位(辅存地址位):对于那些映射到设备寄存器而不是常规内存的页面有用,假设操作系统正在循环等待某个I/O设备对其指令进行响应,保证硬件不断的从设备中读取数据而不是访问一个旧的高速缓存中的副本是非常重要的。即用于页面调入。
在请求分页系统中,可以通过【查询页表中的中断位】来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存时,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。
缺页本身是一种中断,与一般的中断一样,需要经过4个处理步骤:
- 保护CPU现场(保存断点)
- 分析中断原因
- 转入缺页中断处理程序进行处理(进入中断子程序)
- 恢复CPU现场,继续执行
3. 页面置换算法
就是说 当系统内存不足的时候,而我们想往内存中添加现在立刻要用的缓存时,需要从现在的内存中踢出一部分东西。那么选取哪一部分被踢出来呢?这个选择的部分 就叫页面置换算法。
我们为什么需要页面置换算法?因为在地址映射过程中,如果发现要访问的页面不在内存中,会产生缺页中断;当发生此现象的时候,如果操作系统内存中没有空余,则操作系统必须在内存里面选择一个页面将其移出内存。
(1)最佳置换算法(OPT):理想的置换算法。置换策略是将当前页面中在未来最长时间内不会被访问的页置换出去。操作系统无法提前预判页面访问序列(无法实现)。 因此, 最佳置换算法是无法实现的。
(2)先进先出置换算法(FIFO):每次淘汰最早调入的页面 。
(3)最近最久未使用算法(LRU):每次淘汰最久没有使用的页面。使用了一个时间标志。
(4)时钟算法clock(最近未使用算法NRU):页面设置一个访问位,并将页面链接为一个环形队列,页面被访问的时候访问位设置为1。页面置换的时候,如果当前指针所指页面访问为为0,那么置换,否则将其置为0,循环直到遇到一个访问为位0的页面
(5)改进型Clock算法:在Clock算法的基础上添加一个修改位,替换时根究访问位和修改位综合判断。优先替换访问为何修改位都是0的页面,其次是访问位为0修改位为1的页面。
(6)最少使用算法(LFU):设置寄存器记录页面被访问次数,每次置换的时候置换当前访问次数最少的。LFU和LRU是很类似的,支持硬件也是一样的,但是区分两者的关键在于一个以时间为标准,一个以次数为标准。
4. 虚拟内存(分页存储管理)
为了更好的管理内存,操作系统将【内存抽象成地址空间】(虚拟的存在,便于管理,实际是一种映射)。
(1)每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。
(2)通过虚拟内存可以让程序可以拥有超过系统物理内存大小的可用内存空间。
(3)虚拟内存为每个进程提供了一个一致的、私有的地址空间,它让每个进程产生了一种自己在独享主存的错觉(每个进程拥有一片连续完整的内存空间)。这样会更加有效地管理内存并减少出错。
5. 快表地址转化原理(交换空间)
为了解决虚拟地址到物理地址的转换速度,操作系统在页表方案基础之上引入了快表来加速虚拟地址到物理地址的转换。可以把快表理解为一种特殊的高速缓冲存储器(Cache),其中的内容是页表的一部分或者全部内容。作为页表的 Cache,它的作用与页表相似,但是提高了访问速率。由于采用页表做地址转换,读写内存数据时 CPU 要访问两次主存。有了快表,有时只要访问一次高速缓冲存储器,一次主存,这样可加速查找并提高指令执行速度。
使用快表之后的地址转换流程是这样的:
(1)根据虚拟地址中的页号查快表(快表中存放虚拟地址(逻辑地址)到物理地址的映射,这和页表相似);
(2)如果该页在快表中,直接从快表中读取相应的物理地址;
(3)如果该页不在快表中,就访问内存中的页表,再从页表中得到物理地址,同时将页表中的该映射表项添加到快表中;
(4)当快表填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。
6. 局部性原理
局部性原理表现在以下两个方面:
(1)时间局部性 :如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问。产生时间局部性的典型原因,是由于在程序中存在着大量的循环操作。
(2)空间局部性 :一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,这是因为指令通常是顺序存放、顺序执行的,数据也一般是以向量、数组、表等形式簇聚存储的。
局部性原理主要是实现高速缓存:
- 时间局部性是通过将近来使用的指令和数据保存到高速缓存存储器中,并使用高速缓存的层次结构实现。
- 空间局部性通常是使用较大的高速缓存,并将预取机制集成到高速缓存控制逻辑中实现。虚拟内存技术实际上就是建立了 “内存一外存”的两级存储器的结构,利用局部性原理实现髙速缓存。
7.内存泄露与溢出
- 内存溢出(OOM):指程序在申请内存时,没有足够的空间使用,即你要求分配的内存超出了系统所能给你的,系统不能满足需求,所以产生溢出。
- 内存泄露(ML):使用完系统分配的内存未归还,导致系统不能将它分配给其他程序了,一次内存泄露的危害还可以忽略。但是内存泄露堆积最终会导致内存溢出!
内存溢出的解决方案:
- 修改JVM启动参数,直接增加内存。
- 检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。
- 对代码进行走查和分析,找出可能发生内存溢出的位置。重点排查以下几点:
检查代码中是否有死循环或递归调用。
检查是否有大循环重复产生新对象实体。
检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。 - 使用内存查看工具动态查看内存使用情况。
文件系统
1. 磁盘结构
(1)盘面(Platter):一个磁盘有多个盘面;
(2)磁道(Track):盘面上的圆形带状区域,一个盘面可以有多个磁道;
(3)扇区(Track Sector):磁道上的一个弧段,一个磁道可以有多个扇区,它是最小的物理储存单位,目前主要有 512 bytes 与 4 K 两种大小;
(4)磁头(Head):与盘面非常接近,能够将盘面上的磁场转换为电信号(读),或者将电信号转换为盘面的磁场(写);
(5)制动手臂(Actuator arm):用于在磁道之间移动磁头;
(6)主轴(Spindle):使整个盘面转动。
2. 磁盘调度算法
读写一个磁盘块的时间的影响因素有:
(1)旋转时间(主轴转动盘面,使得磁头移动到适当的扇区上)
(2)寻道时间(制动手臂移动,使得磁头移动到适当的磁道上)
(3)实际的数据传输时间
其中,寻道时间最长,因此磁盘调度的主要目标是使磁盘的平均寻道时间最短。
先来先服务(FCFS, First Come First Served)
- 按照磁盘请求的顺序进行调度。
- 优点是公平和简单。缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。
最短寻道时间优先(SSTF, Shortest Seek Time First)
- 优先调度与当前磁头所在磁道距离最近的磁道。
- 虽然平均寻道时间比较低,但是不够公平。如果新到达的磁道请求总是比一个在等待的磁道请求近,那么在等待的磁道请求会一直等待下去,也就是出现饥饿现象。具体来说,两端的磁道请求更容易出现饥饿现象。
电梯算法(SCAN)
- 电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向。
- 电梯算法(扫描算法)和电梯的运行过程类似,总是按一个方向来进行磁盘调度,直到该方向上没有未完成的磁盘请求,然后改变方向。
- 因为考虑了移动方向,因此所有的磁盘请求都会被满足,解决了 SSTF 的饥饿问题。
Linux内核补充
1.linux终端按下Ctrl+C发生了什么
- ctrl-c: ( kill foreground process ) 发送 SIGINT 信号给前台进程组中的所有进程,强制终止程序的执行;
- ctrl-z: ( suspend foreground process ) 发送 SIGTSTP 信号给前台进程组中的所有进程,常用于挂起一个进程,而并非结束进程,用户可以使用fg/bg操作恢复
2.IO模型
(1)阻塞式 I/O
- 应用进程被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回。
- 应该注意到,在阻塞的过程中,其它应用进程还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其它应用进程还可以执行,所以不消耗 CPU 时间,这种模型的 CPU 利用率会比较高。
(2)非阻塞式 I/O
- 应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。
- 由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率比较低。ps:系统调用,一个进程在用户态需要使用内核态的功能。
(3)IO多路复用
- linux用select/poll函数实现IO复用模型,这两个函数也会使进程阻塞,但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。知道有数据可读或可写时,才真正调用IO操作函数。
(4)信号驱动IO
- linux用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO时间就绪,进程收到SIGIO信号,然后处理IO事件。相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高(通知进程可以开始io操作)。
(5)异步 I/O
- 应用进程执行 aio_read 系统调用会立即返回,应用进程可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号(通知进程io操作完成)。
异步 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操作;
- 异步I/O是指内核向应用程序通知的是完成事件,比如读取客户端的数据后才通知应用程序,由内核完成I/O操作。
3.select/poll/epoll
select/poll/epoll 都是 I/O 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll。
(1)select
- int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- select 允许应用程序监视一组文件描述符,等待一个或者多个描述符成为就绪状态,从而完成 I/O 操作。
- fd_set 使用数组实现,数组大小使用 FD_SETSIZE 定义,所以只能监听少于 FD_SETSIZE 数量的描述符。有三种类型的描述符类型:readset、writeset、exceptset,分别对应读、写、异常条件的描述符集合。
- select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理,通过轮询所有的文件描述符来检查是否有事件发生。
(2)poll
- int poll(struct pollfd *fds, unsigned int nfds, int timeout);
- poll 的功能与 select 类似,依然采用轮询遍历的方式检查是否有事件发生。
- 其和select不同的地方:采用链表的方式替换原有fd_set数据结构,而使其没有连接数的限制。
(3)epoll
- epoll是一种更加高效的IO多路复用的方式,它可以监视的文件描述符数量突破了1024的限制(十万),同时不需要通过轮询遍历的方式去检查文件描述符上是否有事件发生,因为epoll_wait返回的就是有事件发生的文件描述符。本质上是事件驱动。
具体是通过红黑树和就绪链表实现的,红黑树存储所有的文件描述符,就绪链表存储有事件发生的文件描述符;
(1)epoll_ctl可以对文件描述符结点进行增、删、改、查,并且告知内核注册回调函数(事件)。
(2)一旦文件描述符上有事件发生时,那么内核将该文件描述符节点插入到就绪链表里面
(3)这时候epoll_wait将会接收到消息,并且将数据拷贝到用户空间。
表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
epoll工作模式
- epoll 的描述符事件有两种触发模式:LT(level trigger)和 ET(edge trigger)。
- LT 模式
- 当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
- ET 模式
- 和 LT 模式不同的是,通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。
- 很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
EPOLLONESHOT
- 一个线程读取某个socket上的数据后开始处理数据,在处理过程中该socket上又有新数据可读,此时另一个线程被唤醒读取,此时出现两个线程处理同一个socket
- 我们期望的是一个socket连接在任一时刻都只被一个线程处理,通过epoll_ctl对该文件描述符注册epolloneshot事件,一个线程处理socket时,其他线程将无法处理,当该线程处理完后,需要通过epoll_ctl重置epolloneshot事件
4.select poll epoll区别
(1)消息传递方式(拷贝或者共享内存):
- select:内核需要将消息传递到用户空间,需要内核的拷贝动作;
- poll:同上;
- epoll:通过内核和用户空间共享一块内存来实现,性能较高;
(2)文件句柄剧增后带来的IO效率问题:
- select:因为每次调用都会对连接进行线性遍历,所以随着FD剧增后会造成遍历速度的“线性下降”的性能问题;
- poll:同上;
- epoll:由于epoll是根据每个FD上的callable函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll不会对性能产生线性下降的问题,如果所有socket都很活跃的情况下,可能会有性能问题;
(3)支持一个进程所能打开的最大连接数:
- select:单个进程所能打开的最大连接数,是由FD_SETSIZE宏定义的,其大小是32个整数大小(在32位的机器上,大小是3232,64位机器上FD_SETSIZE=3264),我们可以对其进行修改,然后重新编译内核,但是性能无法保证,需要做进一步测试;
- poll:本质上与select没什么区别,但是他没有最大连接数限制,他是基于链表来存储的;
- epoll:虽然连接数有上线,但是很大,1G内存的机器上可以打开10W左右的连接;
补充
1.使用信号量实现生产者-消费者问题
问题描述:使用一个缓冲区来保存物品,只有缓冲区没有满,生产者才可以放入物品;只有缓冲区不为空,消费者才可以拿走物品。
因为缓冲区属于临界资源,因此需要使用一个互斥量 mutex 来控制对缓冲区的互斥访问。
为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。
注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作, 发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,因为生产者对缓冲区加锁了,消费者就无法执行 up(empty) 操作,empty 永远都为 0,导致生产者永远等待下,不会释放锁,消费者因此也会永远等待下去。
2. 哲学家进餐问题
五个哲学家围着一张圆桌,每个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先拿起自己左右两边的两根筷子,并且一次只能拿起一根筷子。
下面是一种错误的解法,如果所有哲学家同时拿起左手边的筷子,那么所有哲学家都在等待其它哲学家吃完并释放自己手中的筷子,导致死锁。
为了防止死锁的发生,可以设置两个条件:
(1)必须同时拿起左右两根筷子;
(2)只有在两个邻居都没有进餐的情况下才允许进餐。
3.C语言编译的整个过程
(1)预处理阶段:处理以 # 开头的预处理命令;
(2)编译阶段:翻译成汇编文件;
(3)汇编阶段:将汇编文件翻译成可重定位目标文件;
(4)链接阶段:将可重定位目标文件和 printf.o 等单独预编译好的目标文件进行合并,得到最终的可执行目标文件。
ps: 目标文件
(1)可执行目标文件:可以直接在内存中执行;
(2)可重定位目标文件:可与其它可重定位目标文件在链接阶段合并,创建一个可执行目标文件;
(3)共享目标文件:这是一种特殊的可重定位目标文件,可以在运行时被动态加载进内存并链接;
ps: 静态链接和动态链接
- 静态链接
静态链接器以一组可重定位目标文件为输入,生成一个完全链接的可执行目标文件作为输出。链接器主要完成以下两个任务:
(1)符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来。
(2)重定位:链接器通过把每个符号定义与一个内存位置关联起来,然后修改所有对这些符号的引用,使得它们指向这个内存位置。。
- 动态链接
静态库有以下两个问题:
(1)当静态库更新时那么整个程序都要重新进行链接;
(2)对于 printf 这种标准函数库,如果每个程序都要有代码,这会极大浪费资源。
共享库是为了解决静态库的这两个问题而设计的,在 Linux 系统中通常用 .so 后缀来表示,Windows 系统上它们被称为 DLL。它具有以下特点:
(1)在给定的文件系统中一个库只有一个文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中;
(2)在内存中,一个共享库的 .text 节(已编译程序的机器代码)的一个副本可以被不同的正在运行的进程共享。
4.读者-写者问题
读者写者问题描述非常简单,有一个写者很多读者,多个读者可以同时读文件,但写者在写文件时不允许有读者在读文件,同样有读者在读文件时写者也不去能写文件。类似于生产者消费者问题的分析过程,首先来找找哪些是属于“等待”情况。
- 写者要等到没有读者时才能去写文件。
- 所有读者要等待写者完成写文件后才能去读文件。
“读者--写者问题”是保证一个Writer进程必须与其他进程互斥地访问共享对象的同步问题。要解决的问题:读、读共享; 写、写互斥; 写、读互斥
。
实现:
- 利用记录型信号量解决读者--写者问题
- 利用信号量集解决读者--写者问题,增加一个限制:最多只允许RN个读者同时读