操作系统面试题总结

进程的常见状态?以及各种状态之间的转换条件?

创建状态:进程在创建时需要申请一个空白PCB,向其中填写控制和管理进程的信息,完成资源分配。如果创建工作无法完成,比如资源无法满足,就无法被调度运行,把此时进程所处状态称为创建状态

就绪状态:进程已经准备好,已分配到所需资源,只要分配到CPU就能够立即运行

执行状态:进程处于就绪状态被调度后,进程进入执行状态

阻塞状态:正在执行的进程由于某些事件(I/O请求,申请缓存区失败)而暂时无法运行,进程受到阻塞。在满足请求时进入就绪状态等待系统调用

终止状态:进程结束,或出现错误,或被系统终止,进入终止状态。无法再执行

进程的通信方式有哪些?

管道:管道是单向的(半双工的)、只能在一个方向上流动、有固定的读端和写端。写进程在管道的写端写入数据,读进程在管道的读端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。管道提供了简单的流控制机制。进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样地,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。

​ 注1:无名管道(PIPE)只能实现父子或者兄弟进程之间的通信。它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

​ 注2:命名管道(FIFA)可以在无关的进程之间交换数据,与无名管道不同。FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

信号量:信号量是一个计数器,不用于存储进程间通信数据。可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其它进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

消息队列:是一个在系统内核中用来保存消息的队列,它在系统内核中是以消息链表的形式出现的。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

共享内存:共享内存允许两个或多个进程访问同一个逻辑内存。这一段内存可以被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,可以被其他使用这个共享内存的进程,通过一个简单的内存读取读出,从而实现了进程间的通信。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。共享内存是最快的IPC方式,因为进程是直接对内存进行存取,它是针对其它进程间通信方式运行效率低而专门设计的。它往往与其它通信机制(如信号量)配合使用,来实现进程间的同步和通信。

套接字:套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。

信号:信号是一种比较复杂的通信方式,信号产生的条件:按键、硬件异常、进程调用kill函数将信号发送给另一个进程、用户调用kill命令将信号发送给其他进程,信号传递的消息比较少,主要用于通知接收进程某个时间已经发生。

通讯方式总结

管道:速度慢,容量有限,只有父子进程能通讯

FIFO:任何进程间都能通讯,但速度慢

消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题

信号量:不能传递复杂消息,只能用来同步

共享内存区:能够很容易控制容量,速度快,但要保持同步

进程与线程的区别和联系?

  • 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的基本独立单位。

  • 线程是进程的一个实体,是CPU调度的最小单位,它是比进程更小的能独立运行的基本单位。

进程和线程的关系

(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。

(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。

(3)处理机分给线程,即真正在处理机上运行的是线程。

(4)线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。

进程与线程的区别

(1)进程有自己的独立地址空间,线程没有

(2)进程是资源分配的最小单位,线程是CPU调度的最小单位

(3)进程和线程通信方式不同(线程之间的通信比较方便。同一进程下的线程共享数据(比如全局变量,静态变量),通过这些数据来通信不仅快捷而且方便,当然如何处理好这些访问的同步与互斥正是编写多线程程序的难点。而进程之间的通信只能通过进程通信的方式进行。

(4)进程上下文切换开销大,线程开销小

(5)一个进程挂掉了不会影响其他进程,而线程挂掉了会影响其他线程

(6)对进程进程操作一般开销都比较大,对线程开销就小了

进程与程序的区别

1.进程是动态的,程序是静态的。

2.进程有一定的生命周期,而程序是指令的集合,无“动态”的含义,没有建立进程的程序不能作为1个独立单位得到操作系统的认可。

3.一个程序可以对应多个进程,但1个进程只能对应1个程序。

上下文切换

对于单核单线程CPU而言,在某一时刻只能执行一条CPU指令。上下文切换(Context Switch)是一种将CPU资源从一个进程分配给另一个进程的机制。从用户角度看,计算机能够并行运行多个进程,这恰恰是操作系统通过快速上下文切换造成的结果。在切换的过程中,操作系统需要先存储当前进程的状态(包括内存空间的指针,当前执行完的指令等等),再读入下一个进程的状态,然后执行此进程。

为什么进程上下文切换比线程上下文切换代价高?

进程切换分两步:

1.切换页目录以使用新的地址空间

2.切换内核栈和硬件上下文

对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。

切换的性能消耗:

  • 线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。

  • 另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor's Translation Lookaside Buffer (TLB))或者相当的神马东西会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。

进程调度算法有哪些?

进程调度的任务

1.保存处理机的现场信息

首先要保存当前进程的处理机的现场信息,如程序计数器、多个通用寄存器中的内容等。

2.按某种算法选取进程

按照某种算法从就绪队列中选取一个进程,将其状态改为运行状态,并准备把处理机分给他。

3.把处理器分配给进程

由分派程序把处理器分配给该进程,此时需要将选中进程的进程控制块内有关的处理现场的信息装入处理器相应的各个寄存器中,把处理器的控制权交给该进程,让它从上次的断点处恢复运行。

进程的调度算法

  • 先来先服务调度算法

调度的顺序就是任务到达就绪队列的顺序。

公平、简单(FIFO队列)、非抢占、不适合交互式。

未考虑任务特性,平均等待时间可以缩短。

  • 最短作业优先调度算法

最短的作业(CPU区间长度最小)最先调度。

可以保证最小的平均等待时间。

  • 优先权调度算法

每个任务关联一个优先权,调度优先权最高的任务。

注意:优先权太低的任务一直就绪,得不到运行,出现“饥饿”现象。

  • 时间片轮转调度算法

设置一个时间片,按时间片来轮转调度(“轮叫”算法)

优点: 定时有响应,等待时间较短;缺点: 上下文切换次数较多;

时间片太大,响应时间太长;吞吐量变小,周转时间变长;当时间片过长时,退化为FCFS。

  • 多队列调度算法

按照一定的规则建立多个进程队列

不同的队列有固定的优先级(高优先级有抢占权)

不同的队列可以给不同的时间片和采用不同的调度方法

存在问题1:没法区分I/O bound和CPU bound;

存在问题2:也存在一定程度的“饥饿”现象;

  • 多级反馈队列调度

在级队列的基础上,长期得不到运行的队列优先级会被提高。

可以根据“享用”CPU时间多少来移动队列,阻止“饥饿”。

最通用的调度算法,多数OS都使用该方法或其变形,如UNIX、Windows等。

死锁的条件?如何处理死锁问题?

定义:如果一组进程中的每一个进程都在等待仅由该组进程中的其他进程才能引发的事件,那么该组进程就是死锁的。

或者在两个或多个并发进程中,如果每个进程持有某种资源而又都等待别的进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗地讲,就是两个或多个进程被无限期地阻塞、相互等待的一种状态。

产生死锁的必要条件:

  • 互斥条件:进程对所分配的资源进行排他性使用,一次只能有一个进程使用一个资源。如果有其他进程请求该资源,请求智能等待。

  • 占有并等待: 进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已经被其他进程占有,请求进程会被阻塞,但对自己的资源不放。

  • 不可抢占: 进程已经获得的资源在未使用之前不能被剥夺,只能在进程使用完时由自己释放。

  • 循环等待:在一个封闭进程链中,每个进程占有下一个进程所等待的资源。

如何处理死锁问题:

  • 预防死锁:通过设置某些限制条件,破坏产生死锁的4个必要条件中的一个或者多个;实现起来比较简单,但是如果限制过于严格会降低系统资源利用率以及吞吐量

  • 避免死锁:不是事先采取各种限制措施,在资源的动态分配中,防止系统进入不安全状态(可能产生死锁的状态)

  • 检测死锁:允许系统运行过程中产生死锁,在死锁发生之后,采用一定的算法进行检测,并确定与死锁相关的资源和进程,采取相关方法清除检测到的死锁。实现难度大。

  • 解除死锁:与检测死锁配合,将系统从死锁中解脱出来(撤销进程或者剥夺资源)。对检测到的和死锁相关的进程以及资源,通过撤销或者挂起的方式,释放一些资源并将其分配给处于阻塞状态的进程,使其转变为就绪态。实现难度大。

  • 忽略死锁:windows,Linux个人版都不做死锁处理,直接忽略,大不了重启就好了,小概率事件,代价可以接受。

一个程序的运行过程(四个过程)

  1. 预处理:条件编译,头文件包含,宏替换的处理,编译参数加 -E,生成.i文件。

  2. 编译:将预处理后的文件转换成汇编语言,编译参数加 -S,生成.s文件

  3. 汇编:汇编变为目标代码(机器代码),编译参数加 -c ,生成.o的文件

  4. 链接:连接目标代码,生成可执行程序,不需要加参数

临界资源

  • 在操作系统中,进程是占有资源的最小单位(线程可以访问其所在进程内的所有资源,但线程本身并不占有资源或仅仅占有一点必须资源)。但对于某些资源来说,其在同一时间只能被一个进程所占用。这些一次只能被一个进程所占用的资源就是所谓的临界资源。典型的临界资源比如物理上的打印机,或是存在硬盘或内存中被多个进程所共享的一些变量和数据等(如果这类资源不被看成临界资源加以保护,那么很有可能造成丢数据的问题)。

  • 对于临界资源的访问,必须是互斥进行。也就是当临界资源被占用时,另一个申请临界资源的进程会被阻塞,直到其所申请的临界资源被释放。而进程内访问临界资源的代码被成为临界区。

内存池、进程池、线程池

​ 首先介绍一个概念“池化技术 ”。池化技术就是:提前保存大量的资源,以备不时之需以及重复使用。池化技术应用广泛,如内存池,线程池,连接池等等。内存池相关的内容,建议看看Apache、Nginx等开源web服务器的内存池实现。
​ 由于在实际应用当做,分配内存、创建进程、线程都会设计到一些系统调用,系统调用需要导致程序从用户态切换到内核态,是非常耗时的操作。因此,当程序中需要频繁的进行内存申请释放,进程、线程创建销毁等操作时,通常会使用内存池、进程池、线程池技术来提升程序的性能。

  • 线程池:线程池的原理很简单,类似于操作系统中的缓冲区的概念,它的流程如下:先启动若干数量的线程,并让这些线程都处于睡眠状态,当需要一个开辟一个线程去做具体的工作时,就会唤醒线程池中的某一个睡眠线程,让它去做具体工作,当工作完成后,线程又处于睡眠状态,而不是将线程销毁。

  • 进程池:与线程池同理。

  • 内存池:内存池是指程序预先从操作系统申请一块足够大内存,此后,当程序中需要申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取;同理,当程序释放内存的时候,并不真正将内存返回给操作系统,而是返回内存池。当程序退出(或者特定时间)时,内存池才将之前申请的内存真正释放。

中断与系统调用

所谓的中断就是在计算机执行程序的过程中,由于出现了某些特殊事情,使得CPU暂停对程序的执行,转而去执行处理这一事件的程序。等这些特殊事情处理完之后再回去执行之前的程序。

中断的分类三类:

内部异常中断:由计算机硬件异常或故障引起的中断;

软中断:由程序中执行了引起中断的指令而造成的中断(这也是和我们将要说明的系统调用相关的中断);

外部中断:由外部设备请求引起的中断,称为外部中断;

​ 简单来说,对中断的理解就是对一些特殊事情的处理。与中断紧密相连的一个概念就是中断处理程序了。当中断发生的时候,系统需要去对中断进行处理,对这些中断的处理是由操作系统内核中的特定函数进行的,这些处理中断的特定的函数就是我们所说的中断处理程序了。

​ 另一个与中断紧密相连的概念就是中断的优先级。中断的优先级说明的是当一个中断正在被处理的时候,处理器能接受的中断的级别。中断的优先级也表明了中断需要被处理的紧急程度。每个中断都有一个对应的优先级,当处理器在处理某一中断的时候,只有比这个中断优先级高的中断可以被处理器接受并且被处理。优先级比这个当前正在被处理的中断优先级要低的中断将会被忽略。

典型的中断优先级如下所示:

机器错误 > 时钟 > 磁盘 > 网络设备 > 终端 > 软件中断

用户态和内核态

​ 在讲系统调用之前,先说下进程的执行在系统上的两个级别:用户级和核心级,也称为用户态和系统态(user mode and kernel mode)。

​ 用户空间就是用户进程所在的内存区域,相对的,系统空间就是操作系统占据的内存区域。用户进程和系统进程的所有数据都在内存中。处于用户态的程序只能访问用户空间,而处于内核态的程序可以访问用户空间和内核空间。

​ 当一个进程在执行用户自己的代码时处于用户运行态(用户态),此时特权级最低,为3级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态。Ring3状态不能访问Ring0的地址空间,包括代码和数据;当一个进程因为系统调用陷入内核代码中执行时处于内核运行态(内核态),此时特权级最高,为0级。执行的内核代码会使用当前进程的内核栈,每个进程都有自己的内核栈。

​ 用户运行一个程序,该程序创建的进程开始时运行自己的代码,处于用户态。如果要执行文件操作、网络数据发送等操作必须通过write、send等系统调用,这些系统调用会调用内核的代码。进程会切换到Ring0,然后进入3G-4G中的内核地址空间去执行内核代码来完成相应的操作。内核态的进程执行完后又会切换到Ring3,回到用户态。这样,用户态的程序就不能随意操作内核地址空间,具有一定的安全保护作用。这说的保护模式是指通过内存页表操作等机制,保证进程间的地址空间不会互相冲突,一个进程的操作不会修改另一个进程地址空间中的数据。

用户态切换到内核态的方式如下:

  • 系统调用:程序的执行一般是在用户态下执行的,但当程序需要使用操作系统提供的服务时,比如说打开某一设备、创建文件、读写文件(这些均属于系统调用)等,就需要向操作系统发出调用服务的请求,这就是系统调用。如写数据write,创建进程fork,vfork等都是系统调用。

  • 异常:当CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。

  • 外围设备的中断:当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。

用户态和核心态(内核态)之间的区别是什么呢?

权限不一样:

用户态的进程能存取它们自己的指令和数据,但不能存取内核指令和数据(或其他进程的指令和数据)。

内核态下的进程能够存取内核和用户地址某些机器指令是特权指令,在用户态下执行特权指令会引起错误。在系统中内核并不是作为一个与用户进程平行的进程的集合。

进程的结构

从结构上将,每个进程都是由程序、数据、进程控制块(PCB)组成。

进程控制块(PCB):为了参与并发执行的每个程序(含数据)都能够独立运行,在操作系统中必须配置一个必须的数据结构。

在Linux中 task_struct 结构体即是PCB。PCB是进程的唯一标识,PCB由链表实现(为了动态插入和删除)。

进程创建时,为该进程生成一个PCB;进程终止时,回收PCB。

PCB包含信息:

  1. 进程状态(state)

  2. 进程标识信息(uid、gid)

  3. 定时器(time)

  4. 用户可见寄存器、控制状态寄存器、栈指针等(tss)

每个进程都有一个非负的唯一进程ID(PID)。虽然是唯一的,但是PID可以重用,当一个进程终止后,其他进程就可以使用它的PID了。

PID为0的进程为调度进程,该进程是内核的一部分,也称为系统进程;PID为1的进程为init进程,它是一个普通的用户进程,但是以超级用户特权运行;PID为2的进程是页守护进程,负责支持虚拟存储系统的分页操作。

task_struct 结构体的内容分类(进程的描述信息)下面简单的介绍几类:

要进行(运行)的程序(程序是死的),放在硬盘中。当运行起来时候就会被加载到内存中。

站在操作系统的角度:进程是PCB(进程控制块)task struct,在进程执行时,都可以被表征为一下元素:

标识符:与进程相关的唯一标识符,用来区别正在执行的进程和其他进程。

状态:描述进程的状态,因为进程有挂起,阻塞,运行等好几个状态,所以都有个标识符来记录进程的执行状态。

优先级:如果有好几个进程正在执行,就涉及到进程被执行的先后顺序的问题,这和进程优先级这个标识符有关。

程序计数器:程序中即将被执行的下一条指令的地址。

内存指针:程序代码和进程相关数据的指针。

上下文数据:进程执行时处理器的寄存器中的数据。

I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表等。

记账信息:包括处理器的时间总和,记账号等等。

操作系统对进程的控制就是通过对上面的这些元素的控制来控制操作系统的,但这些信息都不是单独存放的,而是存放在PCB(进程控制块)数据结构 task_struct 中,这个数据结构是一种结构体,由操作系统创建和管理,并通过进程控制块来对进程进行控制和调度。

进程的创建与终止的过程

进程的创建过程

  1. 申请空白的PCB,为新进程申请获得唯一的数字标识符,从PCB集合中去除空白PCB。

  2. 为新进程分配其运行所需资源,包括各种物理/逻辑资源,如内存、文件、I/O设备和CPU时间。这些资源从操作系统或从其父进程获得。

  3. 初始化进程控制块的PCB。包括:标识信息、处理机状态信息、处理机控制信息。

  4. 如果进程就绪队列能够接纳新进程,便将新进程插入到就绪队列。

进程的终止过程

  1. 根据被终止的进程的标识符,从PCB集合中检索出该进程的PCB,从中读出该进程的状态。

  2. 若被终止进程正处于执行状态,应立即终止该进程的执行,并置其调度标志为真,表示该进程被终止后应重新调度。

  3. 若该进程还有子孙进程,还应该将其多有子孙进程都予以终止,防止他们成为不可控制进程。

  4. 将被终止的进程所拥有的全部资源归还给父进程或系统。

  5. 将被终止进程PCB从所在队列(或链表)中移出,等待其他程序来搜集信息。

附加:

新进程的创建,首先在内存中为新进程创建一个task_struct结构,然后将父进程的task_struct内容复制其中,再修改部分数据。分配新的内核堆栈、新的PID、再将task_struct 这个node添加到链表中。所谓创建,实际上是“复制”。

子进程刚开始,内核并没有为它分配物理内存,而是以只读的方式共享父进程内存,只有当子进程写时,才复制。即“读时共享,写时拷贝”。fork都是由do_fork实现的,do_fork的简化流程如下图:

img

进程创建fork函数与vfork函数的理解

fork函数

fork函数时调用一次,返回两次。在父进程和子进程中各调用一次。子进程中返回值为0,父进程中返回值为子进程的PID。程序员可以根据返回值的不同让父进程和子进程执行不同的代码。

一个形象的过程:

img

一般来说,fork之后父、子进程执行顺序是不确定的,这取决于内核调度算法。进程之间实现同步需要进行进程通信。

什么时候使用fork呢?

一个父进程希望子进程同时执行不同的代码段,这在网络服务器中常见——父进程等待客户端的服务请求,当请求到达时,父进程调用fork,使子进程处理此请求。如果要一个进程要执行一个不同的程序,一般fork之后立即调用exec

vfork函数

vfork与fork对比:

相同:

返回值相同

不同:

​ fork创建子进程,把父进程数据空间、堆和栈复制一份;vfork创建子进程,与父进程内存数据共享。实际上vfork只完成了一件事:复制内部的内核数据结构。

​ vfork先保证子进程先执行,当子进程调用exit()或者exec后,父进程才往下执行。fork( )的父子进程的执行次序不确定

​ 当需要改变共享数据段中变量的值,则拷贝父进程。

为什么需要vfork?

​ 因为用vfork时,一般都是紧接着调用exec,所以不会访问父进程数据空间,也就不需要在把数据复制上花费时间了,因此vfork就是“为了exec而生”的。

附加(关于execve):

​ 可执行文件装入内核的linux_binprm结构体。进程调用exec载入二进制映像,替换当前进程的映像,新的程序从main函数开始执行。因为调用exec并不创建新进程,只是替换了当前进程的代码区、数据区、堆和栈。execve是内核的系统调用,其他的几个execl、 execv、execle、execve、execlp等只是库函数,他们最终都要调用该系统调用,如下图所示:

img

execve的实现由do_execve完成,简化的实现过程如下图:

img

​ 在早期的Unix系统中,创建进程比较原始。当调用fork时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容逐页的复制到子进程的地址空间中。但从内核角度来说,逐页的复制方式是十分耗时的。现代的Unix系统采取了更多的优化,例如Linux,采用了写时复制的方法,而不是对父进程空间进程整体复制。

写时复制

​ 写时复制是一种采取了惰性优化方法来避免复制时的系统开销。它的前提很简单:如果有多个进程要读取它们自己的那部门资源的副本,那么复制是不必要的。每个进程只要保存一个指向这个资源的指针就可以了。只要没有进程要去修改自己的“副本”,就存在着这样的幻觉:每个进程好像独占那个资源。从而就避免了复制带来的负担。如果一个进程要修改自己的那份资源“副本”,那么就会复制那份资源,并把复制的那份提供给进程。不过其中的复制对进程来说是透明的。这个进程就可以修改复制后的资源了,同时其他的进程仍然共享那份没有修改过的资源。所以这就是名称的由来:在写入时进行复制。

​ 写时复制的主要好处在于:如果进程从来就不需要修改资源,则不需要进行复制。惰性算法的好处就在于它们尽量推迟代价高昂的操作,直到必要的时刻才会去执行。

​ 写时复制在内核中的实现非常简单。与内核页相关的数据结构可以被标记为只读和写时复制。如果有进程试图修改一个页,就会产生一个缺页中断。内核处理缺页中断的方式就是对该页进行一次透明复制。这时会清除页面的COW属性,表示着它不再被共享。

​ 现代的计算机系统结构中都在内存管理单元(MMU)提供了硬件级别的写时复制支持,所以实现是很容易的。

​ 在调用fork( )时,写时复制是有很大优势的。因为大量的fork之后都会跟着执行exec,那么复制整个父进程地址空间中的内容到子进程的地址空间完全是在浪费时间:如果子进程立刻执行一个新的二进制可执行文件的映像,它先前的地址空间就会被交换出去。写时复制可以对这种情况进行优化。

一个线程崩溃为什么会影响整个进程?

​ 线程有自己的栈空间,但是没有单独的堆,也没有单独的地址空间。只有进程有自己的地址空间,而这个空间中经过合法申请的部分叫做进程空间。进程空间之外的地址都是非法地址。当一个线程向非法地址读取或者写入,无法确认这个操作是否会影响同一进程中的其它线程,所以只能是整个进程一起崩溃。

  1. 进程(主线程)创建了多个线程,多个子线程均拥有自己独立的栈空间(存储函数参数、局部变量等),但是多个子线程和主线程共享堆、全局变量等非栈内存。

  2. 如果子线程的崩溃是由于自己的一亩三分地引起的,那就不会对主线程和其他子线程产生影响,但是如果子线程的崩溃是因为对共享区域造成了破坏,那么大家就一起崩溃了。

  3. 举个栗子:主线程是一节车厢的乘务员,诸多乘客(也就是子线程)就是经过乘务员(主线程)检票确定可以进入车厢的,也就是主线程创建了诸多子线程,每个子线程有自己独立的区域(座位啊啥的),但是诸多乘客和乘务员共享走廊啊卫生间啊等等,如果其中一名乘客座位坏了,摔了(可以认为奔溃了),那么其他乘客和乘务员都不受影响,但是如果乘客将卫生间给破坏了,他也无法使用卫生间(崩溃了),其他乘客和乘务员也不能用卫生间,好吧,那么大家一起憋着吧(崩溃了)。

​ 总体来说,线程没有独立的地址空间,如果崩溃,会发信号,如果没有错误处理的handler,OS一般直接杀死进程。就算是有handler了处理,一般也会导致程序崩溃,因为很有可能其他线程或者进程的数据被破坏了。

导致线程崩溃的可能原因

  1. 读取未赋值的变量
    ****这个错误一般是由于程序猿粗心大意,没有给变量初始化和赋值,导致该变量的值为脏值。

  2. 函数栈溢出
    函数栈溢出一般是由两种情况引起的:定义了一个体积太大的局部变量或者参数,参数和局部变量一般都是存储在栈中的,但是栈所占的内存空间很小,在32位下只占有8M的空间,因此如果没有使用malloc和new来在堆上创建内存空间的话,栈溢出就会很容易发生。

函数的嵌套调用的层次太深了,就像无穷递归和无穷的循环调用一样。

  1. 数组访问越界
    这个是程序崩溃最常见的问题,这个就只需要程序猿仔细小心点了。

  2. 指针的目标对象不可用
    指针所指向的对象要正常使用,它就必须是一个合法的,有效的,可以访问的对象,像当指针为空指针(NULL)或者是野指针时,你再使用它程序就会立马崩溃。而出现空指针或者野指针一般都是下面这几种操作导致的:

    ​ 指针未赋值

    ​ free/delete释放了该对象

    ​ 不恰当的指针强制类型转换

  3. 参数错误
    这个就是由于程序猿的猿性导致的了,我们一般为了保证参数合法,都会设置断言来保证只有参数合法的情况下程序才可以正常运行,因此这类错误一般都是由于触发了断言导致的。

实时操作系统与分时操作系统

什么是实时操作系统?

​ 实时操作系统(RTOS)是指当外界事件或数据产生时,能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统作出快速响应,并控制所有实时任务协调一致运行的操作系统。其特点是及时响应和高可靠性。实时系统又分为硬实时系统和软实时系统,硬实时系统要求在规定的时间内必须完成操作,这是在操作系统设计时保证的;软实时则只要按照任务的优先级,尽可能快地完成操作即可。

什么是分时操作系统?

​ 使一台计算机同时为几个、几十个甚至几百个用户服务的一种操作系统。把计算机与许多终端用户连接起来,分时操作系统将系统处理机时间与内存空间按一定的时间间隔,轮流地切换给各终端用户的程序使用(时间片的概念)。由于时间间隔很短,每个用户的感觉就像他独占计算机一样。

实时操作系统需要满足哪些特征?

多任务:由于真实世界的事件的异步性,能够运行许多并发进程或任务是很重要的。多任务提供了一个较好的对真实世界的匹配,因为它允许对应于许多外部事件的多线程执行。系统内核分配CPU给这些任务来获得并发性。

抢占调度:真实世界的事件具有继承的优先级,在分配CPU的时候要注意到这些优先级。基于优先级的抢占调度,任务都被指定了优先级,在能够执行的任务(没有被挂起或正在等待资源)中,优先级最高的任务被分配CPU资源。换句话说,当一个高优先级的任务变为可执行态,它会立即抢占当前正在运行的较低优先级的任务。

任务间的通讯与同步:在一个实时系统中,可能有许多任务作为一个应用的一部分执行。系统必须提供这些任务间的快速且功能强大的通信机制。内核也要提供为了有效地共享不可抢占的资源或临界区所需的同步机制。

任务与中断之间的通信:****尽管真实世界的事件通常作为中断方式到来,但为了提供有效的排队、优先化和减少中断延时,我们通常希望在任务级处理相应的工作。所以需要在任务级和中断级之间存在通信。

分时操作系统需要满足哪些特征?

  • 交互性:用户与系统进行人机对话。

  • 多路性:多用户同时在各自终端上使用同一CPU。

  • 独立性:用户可彼此独立操作,互不干扰,互不混淆。

  • 及时性:用户在短时间内可得到系统的及时回答。

实时操作系统主要应用领域

​ 主要应用于过程控制、数据采集、通信、多媒体信息处理等对时间敏感的场合。例如:机器人的运动控制、无人驾驶等。

分时操作系统主要应用领域

​ 现在流行的PC,服务器都是采用这种运行模式,即把CPU的运行分成若干时间片分别处理不同的运算请求。

实时调度算法

  • 最早截止时间优先算法EDF

EDF(Earliest DeadLine First) 这个算法的含义就是,谁完成任务的截止时间早,谁就先执行。好比我们在排队买票的时候,经常出现有人说,不好意思,我赶时间让我先。

img

EDF在非抢占式调度方式的应用

首先任务 1 先到达并且执行任务

在任务 1 执行的过程中,任务 2,3 到达。也就是任务 1 执行完时,有任务 2,3 等待被执行

比较任务 2,3 的截止时间,也就是最上面的开始截止时间,可得到,任务 3 先执行

同样的分析方式,可以得出任务 4 先于任务 2 执行

所以执行顺序是 1,3,4,2

EDF在抢占式调度方式的应用

有两个周期性任务,任务A的周期时间为20ms,每个周期的处理时间为10ms;任务B的周期时间为50ms,每个周期的处理时间为25ms。

img

在 t = 0 时,A1 和 B1 同时到达。此时比较二者的截止时间(最后期限),可以看出 A1 的截止时间早,所以 A1 先运行。

A1 执行 10ms 后因为 A2 还没有到,所以不存在比较,直接切换 B1 执行。

当 B1 执行了 10ms 后,A2 到达,比较二者的截止时间,A2 先于 B1。所以A2 抢占处理机,B1 中断。

此后的调度方式按照上面的分析即可完成

EDF 在抢占式和非抢占式的区别就是,抢占式就是,当新任务 A 来到的时 候,如果 A 的截止时间比正在执行的任务 B 的截止时间提前,那么就中断 B ,抢夺处理机控制权,转而运行 A。

  • 最低松弛度优先算法LLF

LLF(Least Laxity First)这个算法的思想就是,根据任务的紧急程度判断。任务越紧急,松弛度就越低,优先级就越高。

松弛度 = 必须完成的时间 - 本身运行时间 - 当前时间

先举一个下例子。比如此时时间轴为 0ms,任务 A 和 B 同时到达,需要确定先执行哪个任务。

任务 A 需要在 200ms 这个时刻(或之前)完成,而 A 本身要运行 100ms。所以 A 的松弛度就是 200 - 100 - 0 = 100ms。

此时还有一个任务 B ,必须在 400ms 时刻(或者之前)完成,本身需要运行 150ms,所以松弛度为 400 - 150 - 0 = 250

所以,松弛度低(任务越紧急)先执行。

线程共享资源和独占资源问题

​ 一个进程中的所有线程共享该进程的地址空间,但它们有各自独立的(/私有的)栈(stack),Windows线程的缺省堆栈大小为1M。堆(heap)的分配与栈有所不同,一般是一个进程有一个运行时堆,这个堆为本进程中所有线程共享,windows进程还有所谓进程默认堆,用户也可以创建自己的堆。
用操作系统术语,线程切换的时候实际上切换的是一个可以称之为线程控制块的结构(TCB),里面保存所有将来用于恢复线程环境必须的信息,包括所有必须保存的寄存器集,线程的状态等。

:是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。

:是个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是thread safe的。操作系统在切换线程的时候会自动的切换栈,就是切换SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。

线程共享和独享资源如下图:

img

同步和异步

  • 同步

同步的定义:是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么,这个进程将会一直等待下去,直到收到返回信息才继续执行下去。

特点:

同步是阻塞模式;

同步是按顺序执行,执行完一个再执行下一个,需要等待,协调运行;

  • 异步

是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。

特点:

异步是非阻塞模式,无需等待;

异步是彼此独立,在等待某事件的过程中,继续做自己的事,不需要等待这一事件完成后再工作。线程是异步实现的一个方式。

同步与异步的优缺点:

同步可以避免出现死锁,读脏数据的发生。一般共享某一资源的时候,如果每个人都有修改权限,同时修改一个文件,有可能使一个读取另一个人已经删除了内容,就会出错,同步就不会出错。但,同步需要等待资源访问结束,浪费时间,效率低。

异步可以提高效率,但安全性较低。

阻塞与非阻塞

阻塞:当某个任务在执行过程中,发出一个请求操作,但是由于该请求操作需要的条件不满足,那么就会一直在那等待,直至条件满足;

非阻塞:当某个任务在执行过程中,发出一个请求操作,如果该请求操作需要的条件不满足,会立即返回一个标志信息告知条件不满足,不会一直在那等待。

两者区别

阻塞和非阻塞的区别关键在于当发出请求一个操作时,如果条件不满足,是会一直等待还是返回一个标志信息。

举个例子

​ 假如我要读取一个文件中的内容,如果此时文件中没有内容可读,对于同步来说就是会一直在那等待,直至文件中有内容可读;而对于非阻塞来说,就会直接返回一个标志信息告知文件中暂时无内容可读。

​ 同步和异步的着重点在于多个任务的执行过程中,一个任务的执行是否会导致整个流程的暂时等待;

​ 而阻塞和非阻塞的着重点在于发出一个请求操作时,如果进行操作的条件不满足是否会返会一个标志信息告知条件不满足。

同步IO和异步IO

同步IO:即如果一个线程请求进行IO操作,在IO操作完成之前,该线程会被阻塞。

异步IO:如果一个线程请求进行IO操作,IO操作不会导致请求线程被阻塞。

事实上,同步IO和异步IO模型是针对用户线程和内核的交互来说的:

同步IO:当用户发出IO请求操作之后,如果数据没有就绪,需要通过用户线程或者内核不断地去轮询数据是否就绪,当数据就绪时,再将数据从内核拷贝到用户线程。

异步IO:只有IO请求操作的发出是由用户线程来进行的,IO操作的两个阶段都是由内核自动完成,然后发送通知告知用户线程IO操作已经完成。也就是说在异步IO中,不会对用户线程产生任何阻塞。

区别

同步IO和异步IO的关键区别反映在数据拷贝阶段是由用户线程完成还是内核完成。所以说异步IO必须要有操作系统的底层支持。

线程安全

​ 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。或者说一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。

​ 线程安全问题都是由全局变量静态变量引起的。

​ 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

判断一个函数是否线程安全不是一件很容易的事情。但是读者可以通过下面几条确定一个函数是线程不安全的。

  • 函数中访问全局变量和堆。

  • 函数中分配,重新分配释放全局资源。

  • 函数中通过句柄和指针的不直接访问。

  • 函数中使用了其他线程不安全的函数或者变量。

因此在编写线程安全函数时,要注意两点:

  1. 减少对临界资源的依赖,尽量避免访问全局变量,静态变量或其它共享资源,如果必须要使用共享资源,所有使用到的地方必须要进行互斥锁 (Mutex) 保护。

  2. 线程安全的函数所调用到的函数也应该是线程安全的,如果所调用的函数不是线程安全的,那么这些函数也必须被互斥锁 (Mutex) 保护。

可重入概念

所谓可重入,即:当一个函数在被一个线程调用时,可以允许被其他线程再调用。显而易见,如果一个函数是可重入的,那么它肯定是线程安全的。但反之未然,一个函数是线程安全的,却未必是可重入的。程序开发人员应该尽量编写可重入的函数。

一个函数想要成为可重入的函数,必须满足下列要求

  • 不能使用静态或者全局的非常量数据

  • 不能够返回地址给静态或者全局的非常量数据

  • 函数使用的数据由调用者提供

  • 不能够依赖于单一资源的锁

  • 不能够调用非可重入的函数

动态连接与静态连接

静态连接库

​ 在实际开发中,不可能将所有代码放在一个源文件中,所以会出现多个源文件,而且多个源文件之间不是独立的,而会存在多种依赖关系,如一个源文件可能要调用另一个源文件中定义的函数,但是每个源文件都是独立编译的,即每个.c文件会形成一个.o文件,为了满足前面说的依赖关系,则需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。这个链接的过程就是静态链接。

​ 链接器在链接静态链接库的时候是以目标文件为单位的。比如我们引用了静态库中的printf()函数,那么链接器就会把库中包含printf()函数的那个目标文件链接进来,如果很多函数都放在一个目标文件中,很可能很多没用的函数都被一起链接进了输出结果中。由于运行库有成百上千个函数,数量非常庞大,每个函数独立地放在一个目标文件中可以尽量减少空间的浪费,那些没有被用到的目标文件就不要链接到最终的输出文件中。

​ 静态链接的缺点很明显,一是浪费空间,因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,如多个程序中都调用了printf()函数,则这多个程序中都含有printf.o,所以同一个目标文件都在内存存在多个副本;另一方面就是更新比较困难,因为每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。

img

动态连接库

​ 静态库很方便,但是如果我们只是想用库中的某一个函数,却仍然得把所有的内容都链接进去。一个更现代的方法则是使用动态库,避免了在文件中静态库的大量重复,另外一方面解决了更新困难的问题。
​ 动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
​ 假设现在有两个程序program1.o和program2.o,这两者共用同一个库lib.o,假设首先运行程序program1,系统首先加载program1.o,当系统发现program1.o中用到了lib.o,即program1.o依赖于lib.o,那么系统接着加载lib.o,如果program1.o和lib.o还依赖于其他目标文件,则依次全部加载到内存中。当program2运行时,同样的加载program2.o,然后发现program2.o依赖于lib.o,但是此时lib.o已经存在于内存中,这个时候就不再进行重新加载,而是将内存中已经存在的lib.o映射到program2的虚拟地址空间中,从而进行链接(这个链接过程和静态链接类似)形成可执行程序。

​ 动态链接的优点显而易见,就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;另一个优点是,更新也比较方便,更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。但是动态链接也是有缺点的,因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。

​ 据估算,动态链接和静态链接相比,性能损失大约在5%以下。经过实践证明,这点性能损失用来换区程序在空间上的节省和程序构建和升级时的灵活性是值得的。

img

两者区别

  • 使用静态库的时候,静态链接库要参与编译,在生成执行文件之前的链接过程中,要将静态链接库的全部指令直接链接入可执行文件中。而动态库提供了一种方法,使进程可以调用不属于其可执行代码的函数。函数的可执行代码位于一个.dll文件中,该dll包含一个或多个已被编译,链接并与使用它们的进程分开储存的函数。

  • 静态库中不能再包含其他动态库或静态库,而在动态库中还可以再包含其他动态或者静态库。

  • 静态库在编译的时候,就将库函数装在到程序中去了,而动态库函数必须在运行的时候才被装载,所以使用静态库速度快一些。

虚拟存储器

问题:

随着计算机系统软件和应用软件的功能不断增强,某些程序却需要很大的内存才能运行,但是计算机本身的物理内存容量比较小。而且在多用户多任务系统中,多用户或多个任务共享全部主存,要求同时执行多道程序。这些同时运行的程序到底占用实际内存中的哪一部分,在编制程序时是无法确定的,必须等到程序运行时才动态分配。

解决问题:

在程序运行时,则分配给每个程序一定的运行空间,由地址转换部件(硬件或软件)将编程时的地址转换成实际内存的物理地址。如果分配的内存不够,则只调入当前正在运行的或将要运行的程序块(或数据块),其余部分暂时驻留在辅存中。一个大作业在执行时,其一部分地址空间在主存,另一部分在辅存,当所访问的信息不在主存时,则由操作系统而不是程序员来安排I/O指令,把信息从辅存调入主存。从效果上来看,好像为用户提供了一个存储容量比实际主存大得多的存储器,用户无需考虑所编程序在主存中是否放得下或放在什么位置等问题。称这种存储器为虚拟存储器.

虚拟存储器只是一个容量非常大的存储器的逻辑模型,不是任何实际的物理存储器。它借助于磁盘等辅助存储器来扩大主存容量,使之为更大或更多的程序所使用。虚拟存储器指的是主存-外存层次,它以透明的方式为用户提供了一个比实际主存空间大得多的程序地址空间。

实地址和虚地址

用户编制程序时使用的地址(虚拟地址由编译程序生成)称为虚地址逻辑地址,其对应的存储空间称为虚存空间或逻辑地址空间;而计算机物理内存的访问地址则称为实地址物理地址,其对应的存储空间称为物理存储空间主存空间。程序进行虚地址到实地址转换的过程称为程序的再定位。

特点

(1)每个程序的虚地址空间可以远大于实地址空间,也可以远小于实地址空间。

(2)有了虚拟机制后,应用程序就可以透明地使用整个虚存空间。

(3)每个程序就可以拥有一个虚拟的存储器,它具有辅存的容量和接近主存的访问速度。

cache与虚存的异同

相同点:

​ 两者都是为了提高存储系统的性能价格比,即使存储系统的性能接近高速存储器。

​ 都是基于程序局部性原理。一个程序运行时,只会用到程序和数据的一小部分,仅把这部分放到比较快速的存储器中即可,其他大部分放在速度低、价格便宜、容量大的存储器中,这样可以以较低的价格实现较高速的运算。

不同点:

​ 在虚拟存储器中未命中的性能损失要远大于Cache系统中未命中的损失。因为主存和Cache的速度相差5~10倍,而外存和主存的速度相差上千倍。

​ cache主要解决主存与CPU的速度差异问题,而虚存主要解决存储容量问题。

​ CPU与cache和主存之间均有直接访问通路,cache不命中时可直接访问主存。而虚存所依赖的辅存与CPU之间不存在直接的数据通路,当主存不命中时只能通过调页解决,CPU最终还是要访问主存。

​ cache的管理完全由硬件完成,对系统程序员和应用程序员均透明。而虚存管理由软件(操作系统)和硬件共同完成,虚存对实现存储管理的程序员是不透明的(段式和段页式管理对应用程序员”半透明“)。

页式虚拟存储器

页式虚拟存储器的地址映射过程

img

其他:

(1)为了节省页表本身占用的主存空间,一些系统把页表存储在虚存中,因而页表本身也要进行分页。当一个进程运行时,其页表中一部分在主存中,另一部分则在辅存中保存。

(2)另一些系统采用二级页表结构。每个进程有一个页目录表,其中的每个表项指向一个页表。因此,若页目录表的长度(表项数)是m,每个页表的最大长度(表项数)为n,则一个进程最多可以有mXn个页。

(3)在页表长度较大的系统中,还可以采用反向页表实现物理页号到逻辑页号的反向映射。页表中对应每一个物理页号有一个表项,表项的内容包含该物理页所对应的逻辑页号。访存时,通过逻辑页号在反向页表中逐一查找。如果找到匹配的页,则用表项中的物理页取代逻辑页号;如果没有匹配表项,则说明该页不在主存中。这种方式的优点时页表所占空间大大缩小,但代价是需要对反向页表进行检索,查表得时间很长。有些系统通过散列(哈希)表加以改进。

转换后援缓冲器(TLB)

问题

​ 由于页表通常在主存中,因而即使逻辑页已经在主存中,也至少要访问两次物理存储器才能实现一次访存,这将使虚拟存储器的存取时间加倍。

解决问题

为了避免对主存访问次数的增多,可以对页表本身实行二级缓存,把页表中的最活跃的部分存放在高速存储器中。这个专用于页表缓存的高速存储器部件通常称为转换后援缓冲器(TLB),又称为快表。而保存在主存中的完整页表则称为慢表。快表的作用是加快地址变换。

img

内页表和外页表

内页表:虚地址到主存物理地址的变换表

外页表:虚地址与辅存地址之间的变换表,常常放在辅存中。需要时可调入主存。当主存不命中时,由存储管理部件向CPU发出”缺页中断“,进行调页操作。

虚拟存储器、TLB和Cache的协同操作

在最好额情况下,虚拟地址由TLB进行转换,然后被送到Cache,找到正确的数据并取回处理器。在最坏的情况下,一次访问会在存储器层次结构的三个组成部分都产生缺失:TLB、页表和Cache。

页式虚拟存储器的优缺点

优点:页面的起点和终点地址是固定的,方便编造页表,新页调入主存也很容易掌握,比段式空间浪费小,易于管理,不存在外碎片。

缺点:页长与程序的逻辑大小不相关,处理、保护和共享都没有段式方便。

段式虚拟存储器

段:按照程序的自然分界划分的长度可以动态改变的区域。通常,程序员把子程序、操作数和常数等不同类型的数据划分到不同的段中,并且每个程序可以有多个相同类型的段。

img

段表的每一个表项对应一个段。每个表项至少包含下面三个字段:

(1)有效位:该段是否已经调入实存

(2)段起位:在该段已经调入实存的情况下,该段在实存中的首地址

(3)段长:记录该段的实际长度。(目的是为了保证访问某段的地址空间时,段内地址不会超出该段长度导致地址越界而破坏其他段)

段表本身也是段,一般驻留在主存中。

特点:分段通常对程序员是可见的,因而分段为组织程序和数据提供了方便。段的长度是不固定的。

段式虚拟存储器优缺点

优点:

(1)段的逻辑独立性使其易于编译、管理、修改和保护,也便于多道程序共享。

(2)段长可以根据需要动态改变,允许自由调度,以便有效利用主存空间。

缺点:

(1)主存空间分配比较麻烦。

(2)容易在段间留下许多外碎片,造成存储空间利用率降低。

(3)由于段长不一定是2的整数次幂,因而不能简单地像分页方式那样用虚地址和实地址的最低若干二进制位作为段内偏移量,并与段号进行直接拼接,必须用加法操作通过段地址与段内偏移量的求和运算求得物理地址。因此,段式存储管理比页式存储管理方式需要更多的硬件支持。

段页式虚拟存储器

含义:段式虚拟存储器和页式虚拟存储器的结合。

第一步:实存被等分成页。

第二步,先段后页:每个程序则先按逻辑结构分段,每段再按照实存的页大小分页,程序按页进行调入和调出操作,但可按段进行编程、保护和共享。

每道程序均通过一个段表和多个页表进行两级再定位。段表中的每一个表项对应一个段,每个表项有一个指针指向该段的页表。页表则指明该段各页再主存中的位置,以及是否已装入、是否已修改等状态信息。

段页式的优缺点

优点:结合了段式和页式的优点。

缺点:在由虚地址向主存地址的映射过程中需要多次查表,需要花费时间,因而实现复杂度较高。

虚存的替换算法与cache的替换算法区别

(1)cache的替换全部靠硬件实现,而虚拟存储器的替换算法是有操作系统的支持。

(2)虚存缺页对系统性能的影响比cache未命中要大得多。(因为调页需要访问辅存,并且要进行任务切换)

(3)虚存页面替换的选择余地很大,属于一个进程的页面都可替换。

对于将被替换出去的页面,假如该页面调入主存后没有被修改,就不必进行处理。否则,则就把该页重新写入外存,以保证外存中数据的正确性。为此,在页表的每一行应设置修改位。

页面置换算法

​ 在进程运行过程中,若其访问的页面不在内存,而需把它们调入内存,但内存已经无空闲时,为了保证该进程能正常运行,系统必须从内存中调出一页程序或数据送到磁盘的对换区中。但应该将那个调出,需要根据一定的置换算法来确定。通常,把选择换出页面的算法成为页面置换算法。

最优页面置换算法(OPT)

最理想的状态下,我们给页面做个标记,挑选一个最远才会被再次用到的页面调出。

最佳置换算法可以用来评价其他算法。假定系统为某进程分配了三个物理块,并考虑有以下页面号引用串:

7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1

进程运行时,先将7, 0, 1三个页面依次装入内存。进程要访问页面2时,产生缺页中断,根据最佳置换算法,选择第18次访问才需调入的页面7予以淘汰。然后,访问页面0时,因为已在内存中所以不必产生缺页中断。访问页面3时又会根据最佳置换算法将页面1淘汰……依此类推,如图所示。从图中可以看出釆用最佳置换算法时的情况。

可以看到,发生缺页中断的次数为9,页面置换的次数为6。

img

先进先出置换算法(FIFO)

是最简单的页面置换算法。这种算法的思想和队列是一样的,该算法总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面予淘汰。其理由是:最早调入主存的页面不再被使用的可能性最大。 实现:把一个进程已调入内存的页面按先后次序链接成一个队列,并且设置一个指针总是指向最老的页面。缺点:对于有些经常被访问的页面如含有全局变量、常用函数、例程等的页面,不能保证这些不被淘汰。

img

这里仍用上面的实例,釆用FIFO算法进行页面置换。进程访问页面2时,把最早进入内存的页面7换出。然后访问页面3时,再把2, 0, 1中最先进入内存的页换出。由图可以看出,利用FIFO算法时进行了 12次页面置换,比最佳置换算法正好多一倍。

最近最少使用页面置换算法LRU

最近最少使用页面置换算法(Least Recently Used)根据页面调入内存后的使用情况做出决策。LRU置换算法是选择最近最久未使用的页面进行淘汰。

为每个在内存中的页面配置一个移位寄存器。定时信号将每隔一段时间将寄存器右移一位。最小数值的寄存器对应页面就是最久未使用页面。

利用一个特殊的栈保存当前使用的各个页面的页面号。每当进程访问某页面时,便将该页面的页面号从栈中移出,将它压入栈顶。因此,栈顶永远是最新被访问的页面号,栈底是最近最久未被访问的页面号。

再对上面的实例釆用LRU算法进行页面置换,如图所示。进程第一次对页面2访问时,将最近最久未被访问的页面7置换出去。然后访问页面3时,将最近最久未使用的页面1换出。

img

实际上,LRU算法根据各页以前的情况,是“向前看”的,而最佳置换算法则根据各页以后的使用情况,是“向后看”的。

LRU性能较好,但需要寄存器和栈的硬件支持。LRU是堆栈类的算法。

内部碎片与外部碎片

在内存管理中,内部碎片是已经被分配出去的的内存空间大于请求所需的内存空间。

外部碎片是指还没有分配出去,但是由于大小太小而无法分配给申请空间的新进程的内存空间空闲块。

固定分区存在内部碎片,可变式分区分配会存在外部碎片;

页式虚拟存储系统存在内部碎片;段式虚拟存储系统,存在外部碎片

为了有效的利用内存,使内存产生更少的碎片,要对内存分页,内存以页为单位来使用,最后一页往往装不满,于是形成了内部碎片。

为了共享要分段,在段的换入换出时形成外部碎片,比如5K的段换出后,有一个4k的段进来放到原来5k的地方,于是形成1k的外部碎片。

系统调用和库函数的区别

系统调用(System call)是程序向系统内核请求服务的方式。可以包括硬件相关的服务(例如,访问硬盘等),或者创建新进程,调度其他进程等。系统调用是程序和操作系统之间的重要接口。

库函数:把一些常用的函数编写完放到一个文件里,编写应用程序时调用,这是由第三方提供的,发生在用户地址空间。

在移植性方面,不同操作系统的系统调用一般是不同的,移植性差;而在所有的ANSI C编译器版本中,C库函数是相同的。

在调用开销方面,系统调用需要在用户空间和内核环境间切换,开销较大;而库函数调用属于“过程调用”,开销较小。

守护进程、僵尸进程、孤儿进程

  • 守护进程:运行在后台的一种特殊进程,独立于*控制终端*并周期性地执行某些任务。

  • 僵尸进程:一个进程 fork 子进程,子进程退出,而父进程没有wait/waitpid子进程,那么子进程的进程描述符仍保存在系统中,这样的进程称为僵尸进程。

  • 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,这些子进程称为孤儿进程。(孤儿进程将由 init 进程收养并对它们完成状态收集工作)

中断处理的全过程

第一部分为准备部分,其基本功能是保护现场,对于非向量中断方式则需要确定中断源,最后开放中断,允许更高级的中断请求打断低级的中断服务程序。

第二部分为处理部分,即真正执行具体的为某个中断源服务的中断服务程序。

第三部分为结尾部分,首先要关中断,以防止在恢复现场过程中被新的中断请求打断,接着恢复现场,然后开放中断,以便返回原来的程序后可响应其他的中断请求。中断服务程序的最后一条指令一定是中断返回指令。

操作系统中的缺页中断

参考回答:malloc()和mmap()等内存分配函数,在分配时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常。

缺页中断:在请求分页系统中,可以通过查询页表中的状态位来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存时,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。

缺页本身是一种中断,与一般的中断一样,需要经过4个处理步骤

1、保护CPU现场

2、分析中断原因

3、转入缺页中断处理程序进行处理

4、恢复CPU现场,继续执行

但是缺页中断是由于所要访问的页面不存在于内存时,由硬件所产生的一种特殊的中断,因此,与一般的中断存在区别:

1、在指令执行期间产生和处理缺页中断信号

2、一条指令在执行期间,可能产生多次缺页中断

3、缺页中断返回时,执行产生中断的一条指令,而一般的中断返回时,执行下一条指令。

Linux虚拟内存空间

回答:为了防止不同进程同一时刻在物理内存中运行而对物理内存的争夺和践踏,采用了虚拟内存。

虚拟内存技术使得不同进程在运行过程中,它所看到的是自己独自占有了当前系统的4G内存。所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。 事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程运行过程中,要动态分配内存,比如malloc时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。

请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换。

虚拟内存的好处

1.扩大地址空间;

2.内存保护:每个进程运行在各自的虚拟内存地址空间,互相不能干扰对方。虚存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。

3.公平内存分配。采用了虚存之后,每个进程都相当于有同样大小的虚存空间。

4.当进程通信时,可采用虚存共享的方式实现。

5.当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存

6.虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。在内存中可以保留多个进程,系统并发度提高

7.在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片

虚拟内存的代价

1.虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存

2.虚拟地址到物理地址的转换,增加了指令的执行时间。

3.页面的换入换出需要磁盘I/O,这是很耗时的

4.如果一页中只有一部分数据,会浪费内存。

5.虚拟内存需要MMU的支持。(MMU是Memory Management Unit的缩写,中文名是*内存管理单元*,它是中央处理器(CPU)中用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权,多用户多进程操作系统。)

操作系统中的程序的内存结构

img

一个程序本质上都是由BSS段、data段、text段三个组成的。可以看到一个可执行程序在存储(没有调入内存)时分为代码段、数据区和未初始化数据区三部分。

bss段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量,对于未初始化的全局变量和静态变量,程序运行main之前时会统一清零。即未初始化的全局变量编译器会初始化为0

data段:存储程序中已初始化的全局变量和静态变量

text段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。

可执行程序在运行时又多出两个区域:栈区和堆区。

栈区:由编译器自动释放,存放函数的参数值、局部变量等。每当一个函数被调用时,该函数的返回类型和一些调用的信息被存放到栈中。然后这个被调用的函数再为他的自动变量和临时变量在栈上分配空间。每调用一个函数一个新的栈就会被使用。栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。

堆区:用于动态分配内存,位于BSS和栈中间的地址区域。由程序员申请分配和释放。堆是从低地址位向高地址位增长,采用链式存储结构。频繁的malloc/free造成内存空间的不连续,产生碎片。当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。

Linux系统怎么修改最大句柄数?

linux默认最大文件句柄数是1024个,在linux服务器文件并发量比较大的情况下,系统会报"too many open files"的错误。故在linux服务器高并发调优时,往往需要预先调优Linux参数,修改Linux最大文件句柄数。

有两种方法:

v ulimit -n <可以同时打开的文件数>,将当前进程的最大句柄数修改为指定的参数(注:该方法只针对当前进程有效,重新打开一个shell或者重新开启一个进程,参数还是之前的值)

首先用ulimit -a查询Linux相关的参数。

其中 open files 就是最大文件句柄数,默认是1024个。

修改Linux最大文件句柄数: ulimit -n 2048, 将最大句柄数修改为 2048个。

v 对所有进程都有效的方法,修改Linux系统参数

vi /etc/security/limits.conf 添加

soft  nofile  65536

hard  nofile  65536

将最大句柄数改为65536

修改以后保存,注销当前用户,重新登录,修改后的参数就生效了

并发和并行

并发(concurrency):指宏观上看起来两个程序在同时运行,比如说在单核cpu上的多任务。但是从微观上看两个程序的指令是交织着运行的,你的指令之间穿插着我的指令,我的指令之间穿插着你的,在单个周期内只运行了一个指令。这种并发并不能提高计算机的性能,只能提高效率。

并行(parallelism):指严格物理意义上的同时运行,比如多核cpu,两个程序分别运行在两个核上,两者之间互不影响,单个周期内每个程序都运行了自己的指令,也就是运行了两条指令。这样说来并行的确提高了计算机的效率。所以现在的cpu都是往多核方面发展。

页表寻址

页式内存管理,内存分成固定长度的一个个页片。操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表,页表的内容就是该进程的虚拟地址到物理地址的一个映射。页表中的每一项都记录了这个页的基地址。通过页表,由逻辑地址的高位部分先找到逻辑地址对应的页基地址,再由页基地址偏移一定长度就得到最后的物理地址,偏移的长度由逻辑地址的低位部分决定。一般情况下,这个过程都可以由硬件完成,所以效率还是比较高的。页式内存管理的优点就是比较灵活,内存管理以较小的页为单位,方便内存换入换出和扩充地址空间。

为什么有了进程还要线程?什么时候用进程?什么时候用线程?

第一:因为进程拥有独立的堆栈空间和数据段,所以每当启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这对于多进程来说十分“奢侈”,系统开销比较大,而线程不一样,线程拥有独立的堆栈空间,但是共享数据段,它们彼此之间使用相同的地址空间,共享大部分数据,比进程更节俭,开销比较小,切换速度也比进程快,效率高,但是正由于进程之间独立的特点,使得进程安全性比较高,也因为进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。一个线程死掉就等于整个进程死掉。

第二:体现在通信机制上面,正因为进程之间互不干扰,相互独立,进程的通信机制相对很复杂,譬如管道,信号,消息队列,共享内存,套接字等通信机制,而线程由于共享数据段所以通信机制很方便。属于同一个进程的所有线程共享该进程的所有资源,包括文件描述符。

第三:线程又称为轻量级进程,进程有进程控制块,线程有线程控制块。线程必定也只能属于一个进程,而进程可以拥有多个线程而且至少拥有一个线程。

第四:体现在程序结构上,举一个简明易懂的列子:当我们使用进程的时候,我们不自主的使用if else嵌套来判断pid,使得程序结构繁琐,但是当我们使用线程的时候,基本上可以甩掉它,当然程序内部执行功能单元需要使用的时候还是要使用,所以线程对程序结构的改善有很大帮助。

进程与线程的选择

1、需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程代价是很大的。

2、线程的切换速度快,所以在需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应。

3、因为对CPU系统的效率使用上线程更占优,所以可能要发展到多机分布的用进程,多核分布用线程。

4、并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求;

5、需要更稳定安全时,适合选择进程;需要速度时,选择线程更好。

单核机器上写多线程程序,是否需要考虑加锁,为什么?

在单核机器上写多线程程序,仍然需要线程锁。因为线程锁通常用来实现线程的同步和通信。在单核机器上的多线程程序,仍然存在线程同步的问题。因为在抢占式操作系统中,通常为每个线程分配一个时间片,当某个线程时间片耗尽时,操作系统会将其挂起,然后运行另一个线程。如果这两个线程共享某些数据,不使用线程锁的前提下,可能会导致共享数据修改引起冲突。

线程需要保存哪些上下文?SP、PC、EAX这些寄存器的作用?

线程在切换的过程中需要保存当前线程ID、线程状态、堆栈、寄存器状态等信息。其中寄存器主要包括SP PC EAX等寄存器,其主要功能如下:

SP:堆栈指针,指向当前栈的栈顶地址

PC:程序计数器,存储下一条将要执行的指令

EAX:累加寄存器,用于加法乘法的缺省寄存器

Linux线程间的同步方式,并说出具体的系统调用?

  • 信号量

信号量是一种特殊的变量,可用于线程同步。它只取自然数值,相当于一个计数器并且只支持两种操作:

P(SV):如果信号量SV大于0,将它减1;如果SV值为0,则挂起该线程。

V(SV):如果有其他进程因为等待SV而挂起,则唤醒,然后将SV+1;否则直接将SV+1。

信号量强调的是线程(或进程)间的同步:信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在sem_wait的时候,就阻塞在那里)。当信号量为单值信号量时,也可以完成一个资源的互斥访问。信号量测重于访问者对资源的有序访问,在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

系统调用

sem_open 创建打开有名信号量

sem_close 关闭信号量,进程终止时,会调用它

sem_unlink 删除信号量,立即删除信号量名字,当其他进程都关闭它时,销毁它

sem_wait 以原子操作的方式将信号量-1。如果信号量值为0,则sem_wait将被阻塞,直到这个信号量具有非0值。

sem_post 以原子操作将信号量值+1。当信号量大于0时,其他正在调用sem_wait等待信号量的线程将被唤醒。

  • 互斥锁

互斥量又称互斥锁,主要用于线程互斥,不能保证按序访问,可以和条件锁一起实现同步。当进入临界区时,需要获得互斥锁并且加锁;当离开临界区时,需要对互斥锁解锁,以唤醒其他等待该互斥锁的线程。

系统调用

pthread_mutex_init 初始化互斥锁

pthread_mutex_destroy 销毁互斥锁

pthread_mutex_lock 以原子操作的方式给一个互斥锁加锁,如果目标互斥锁已经被上锁,调用将阻塞,直到该互斥锁的占有者将其解锁

pthread_mutex_trylock 如果互斥量没被锁住, trylock函数将把互斥量加锁, 并获得对共享资源的访问权限; 如果互斥量被锁住了, trylock函数将不会阻塞等待而直接返回EBUSY, 表示共享资源处于忙状态

pthread_mutex_unlock 以一个原子操作的方式给一个互斥锁解锁

  • 条件变量

条件变量又称条件锁,用于在线程之间同步共享数据的值。条件变量提供一种线程间通信机制,当某个共享数据达到某个值时,唤醒等待这个共享数据的一个/多个线程。即,当某个共享变量等于某个值时,调用 signal/broadcast。此时操作共享变量时需要加锁。

条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。条件本身是由互斥量保护的。

线程在改变条件状态前必须首先锁住互斥量,其它线程在获得互斥量之前不会察觉到这种改变,因此必须锁定互斥量以后才能计算条件。条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。

系统调用

pthread_cond_init 初始化条件变量

pthread_cond_destroy 销毁条件变量

pthread_cond_signal 唤醒一个等待目标条件变量的线程。哪个线程被唤醒取决于调度策略和优先级。

pthread_cond_wait 等待目标条件变量。需要一个加锁的互斥锁确保操作的原子性。该函数中在进入wait状态前首先进行解锁,然后接收到信号后会再加锁,保证该线程对共享资源正确访问。返回时, 互斥量再次被锁住

  • 读写锁

    读写锁可以由三种状态:读模式下加锁状态、写模式下加锁状态、不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。读共享,写独占。写锁优先。

    在读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是如果线程希望以写模式对此锁进行加锁,它必须阻塞直到所有的线程释放读锁。虽然读写锁的实现各不相同,但当读写锁处于读模式锁住状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。

    读写锁非常适合于对数据结构读的次数远大于写的情况。当读写锁在写模式下时,它所保护的数据结构就可以被安全地修改,因为当前只有一个线程可以在写模式下拥有这个锁。当读写锁在读状态下时,只要线程获取了读模式下的读写锁,该锁所保护的数据结构可以被多个获得读模式锁的线程读取。

系统调用

pthread_rwlock_init 初始化读写锁

pthread_rwlock_destroy 销毁读写锁

pthread_rwlock_rdlock 获取读写锁的读锁(阻塞)

pthread_rwlock_wrlock 获取读写锁的写锁操作(阻塞)

pthread_rwlock_unlock 释放读写锁

操作系统中的结构体对其,字节对齐

1、原因:

平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

2、规则

数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。

结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。

结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。

3、定义结构体对齐

可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是指定的“对齐系数”。

4、举例

#pragma pack(2)

struct AA {
	int a;    //长度4 > 2 按2对齐;偏移量为0;存放位置区间[0,3]
	char b;  //长度1 < 2 按1对齐;偏移量为4;存放位置区间[4]
	short c;   //长度2 = 2 按2对齐;偏移量要提升到2的倍数6;存放位置区间[6,7]
	char d;  //长度1 < 2 按1对齐;偏移量为7;存放位置区间[8];共10个字节
};

软链接和硬链接区别

为了解决文件共享问题,Linux引入了软链接和硬链接。除了为Linux解决文件共享使用,还带来了隐藏文件路径、增加权限安全及节省存储等好处。若1个inode号对应多个文件名,则为硬链接,即硬链接就是同一个文件使用了不同的别名,使用ln创建。若文件用户数据块中存放的内容是另一个文件的路径名指向,则该文件是软连接。软连接是一个普通文件,有自己独立的inode,但是其数据块内容比较特殊。

什么是大端小端?如何判断大小端?

大端是指低字节存储在高地址;小端存储是指低字节存储在低地址。我们可以根据联合体来判断该系统是大端还是小端。因为联合体变量总是从低地址存储。

int fun()
{
  union test
  {
		int i;
		char c;
  };
  test t;
  t.i=1;
  return (t.c==1);
}

如果是大端,t.c为0x00,则t.c!=1,返回0

如果是小端,t.c为0x01,则t.c==1,返回1

静态变量什么时候初始化?

静态变量存储在虚拟地址空间的数据段和bss段,C语言中其在代码执行之前初始化,属于编译期初始化。而C++中由于引入对象,对象生成必须调用构造函数,因此C++规定全局或局部静态对象当且仅当对象首次用到时进行构造。

Windows的消息机制?

当用户有操作(鼠标,键盘等)时,系统会将这些时间转化为消息。每个打开的进程系统都为其维护了一个消息队列,系统会将这些消息放到进程的消息队列中,而应用程序会循环从消息队列中取出来消息,完成对应的操作。

你在哪些场景用到的锁?

生产者消费者问题利用互斥锁和条件变量可以很容易解决,条件变量这里起到了替代信号量的作用

内存溢出和内存泄露

  • 内存溢出

指程序申请内存时,没有足够的内存供申请者使用。内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误

内存溢出原因:

内存中加载的数据量过于庞大,如一次从数据库取出过多数据

集合类中有对对象的引用,使用完后未清空,使得不能回收

代码中存在死循环或循环产生过多重复的对象实体

使用的第三方软件中的BUG

启动参数内存值设定的过小

  • 内存泄漏

内存泄漏是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的分类:

堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak。

系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。

没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。

常用的线程模型有哪些?

Future 模型

该模型通常在使用的时候需要结合Callable接口配合使用。

Future是把结果放在将来获取,当前主线程并不急于获取处理结果。允许子线程先进行处理一段时间,处理结束之后就把结果保存下来,当主线程需要使用的时候再向子线程索取。

Callable是类似于Runnable的接口,其中call方法类似于run方法,所不同的是run方法不能抛出受检异常没有返回值,而call方法则可以抛出受检异常并可设置返回值。两者的方法体都是线程执行体。

fork & join模型

该模型包含递归思想和回溯思想,递归用来拆分任务,回溯用合并结果。可以用来处理一些可以进行拆分的大任务。其主要是把一个大任务逐级拆分为多个子任务,然后分别在子线程中执行,当每个子线程执行结束之后逐级回溯,返回结果进行汇总合并,最终得出想要的结果。

这里模拟一个摘苹果的场景:有100棵苹果树,每棵苹果树有10个苹果,现在要把他们摘下来。为了节约时间,规定每个线程最多只能摘10棵苹树以便于节约时间。各个线程摘完之后汇总计算总苹果树。

actor 模型

actor模型属于一种基于消息传递机制并行任务处理思想,它以消息的形式来进行线程间数据传输,避免了全局变量的使用,进而避免了数据同步错误的隐患。actor在接受到消息之后可以自己进行处理,也可以继续传递(分发)给其它actor进行处理。在使用actor模型的时候需要使用第三方Akka提供的框架。

生产者消费者模型

生产者消费者模型都比较熟悉,其核心是使用一个缓存来保存任务。开启一个/多个线程来生产任务,然后再开启一个/多个来从缓存中取出任务进行处理。这样的好处是任务的生成和处理分隔开,生产者不需要处理任务,只负责向生成任务然后保存到缓存。而消费者只需要从缓存中取出任务进行处理。使用的时候可以根据任务的生成情况和处理情况开启不同的线程来处理。比如,生成的任务速度较快,那么就可以灵活的多开启几个消费者线程进行处理,这样就可以避免任务的处理响应缓慢的问题。

master-worker 模型

master-worker模型类似于任务分发策略,开启一个master线程接收任务,然后在master中根据任务的具体情况进行分发给其它worker子线程,然后由子线程处理任务。如需返回结果,则worker处理结束之后把处理结果返回给master。

说一下微内核与宏内核

宏内核

除了最基本的进程、线程管理、内存管理外,将文件系统,驱动,网络协议等等都集成在内核里面,例如linux内核。

优点:效率高。

缺点:稳定性差,开发过程中的bug经常会导致整个系统挂掉。

微内核

内核中只有最基本的调度、内存管理。驱动、文件系统等都是用户态的守护进程去实现的。

优点:稳定,驱动等的错误只会导致相应进程死掉,不会导致整个系统都崩溃

缺点:效率低。典型代表QNX,QNX的文件系统是跑在用户态的进程,称为resmgr的东西,是订阅发布机制,文件系统的错误只会导致这个守护进程挂掉。不过数据吞吐量就比较不乐观了。

介绍一下5种IO模型

阻塞IO:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的去检查这个函数有没有返回,必须等这个函数返回才能进行下一步动作。
非阻塞IO:非阻塞等待,每隔一段时间就去检测IO事件是否就绪。没有就绪就可以做其他事。
信号驱动IO:linux用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO时间就绪,进程收到SIGIO信号。然后处理IO事件。
IO多路复用:linux用select/poll函数实现IO复用模型,这两个函数也会使进程阻塞,但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检测。知道有数据可读或可写时,才真正调用IO操作函数。
异步IO:linux中,可以调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。

操作系统为什么要分内核态和用户态

为了安全性。在cpu的一些指令中,有的指令如果用错,将会导致整个系统崩溃。分了内核态和用户态后,当用户需要操作这些指令时候,内核为其提供了API,可以通过系统调用陷入内核,让内核去执行这些操作。

异步编程的事件循环

事件循环就是不停循环等待时间的发生,然后将这个事件的所有处理器,以及他们订阅这个事件的时间顺序依次依次执行。当这个事件的所有处理器都被执行完毕之后,事件循环就会开始继续等待下一个事件的触发,不断往复。当同时并发地处理多个请求时,以上的概念也是正确的,可以这样理解:在单个的线程中,事件处理器是一个一个按顺序执行的。即如果某个事件绑定了两个处理器,那么第二个处理器会在第一个处理器执行完毕后,才开始执行。在这个事件的所有处理器都执行完毕之前,事件循环不会去检查是否有新的事件触发。在单个线程中,一切都是有顺序地一个一个地执行的。

怎么实现线程池

设置一个生产者消费者队列,作为临界资源

初始化n个线程,并让其运行起来,加锁去队列取任务运行

当任务队列为空的时候,所有线程阻塞

当生产者队列来了一个任务后,先对队列加锁,把任务挂在到队列上,然后使用条件变量去通知阻塞中的一个线程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值