面经总结3(操作系统)

Posted Zephyr丶J

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面经总结3(操作系统)相关的知识,希望对你有一定的参考价值。

面经总结3(操作系统)

进程、线程

1.进程:是并发执行的程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
2.线程:是进程的一个执行单元,是进程内部的调度实体。比进程更小的独立运行的基本单位。线程也被称为轻量级进程。
一个程序至少一个进程,一个进程至少一个线程。

进程是程序运行的实例。运行一个Java程序的实质就是启动一个java虚拟机进程。进程是程序向操作系统申请资源的基本单位。线程是进程中可独立执行的最小单位。一个进程可以包括多个线程,同一个进程中的所有线程共享该进程的资源。

线程安全和非线程安全: 一个类在单线程环境下能够正常运行,并且在多线程环境下,使用方不做特别处理也能运行正常,我们就称其实线程安全的。反之,一个类在单线程环境下运行正常,而在多线程环境下无法正常运行,这个类就是非线程安全的。

多线程编程的实质就是将任务的处理方式由串行改为并发。

进程和线程的区别:
1.从属关系:一个线程从属于一个进程,一个进程中可以有多个线程
2.资源分配:线程是CPU调度(程序执行)的最小单位,进程是系统资源调度(资源分配)的最小单位
3.死亡影响:一个线程挂掉,对应的进程挂掉;一个进程挂掉,不会影响其他进程
4.内存:一个进程拥有独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展段;但是每个线程也有自己的栈和寄存器组
5.资源占用:线程占用的资源比线程少很多,所以创建线程和切换线程的开销相对来说很小。进程的切换要刷新TLB(快表)并获取新的内存空间,然后切换硬件上下文和栈,线程切换只需要切换硬件上下文和内核栈
6.通信方式不同(后面介绍)

补充:线程上下文切换和进程上下文切换

上下文切换:上下文切换就是从当前执行任务切换到另一个任务执行的过程。但是,为了确保下次能从正确的位置继续执行,在切换之前,会保存上一个任务的状态。

进程是程序的动态表现。 一个程序进行起来后,会使用很多资源,比如使用寄存器,内存,文件等。每当切换进程时,必须要考虑保存当前进程的状态。状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开的文件描述符的集合,这个状态叫做上下文(Context)。可见,想要切换进程,保存的状态还不少。不仅如此,由于虚拟内存机制,进程切换时需要刷新TLB并获取新的地址空间。

线程存在于进程中,一个进程可以有一个或多个线程。线程是运行在进程上下文中的逻辑流。每个线程可以独立完成一项任务。同样线程有自己的上下文,包括唯一的整数线程ID, 栈、栈指针、程序计数器、通用目的寄存器和条件码。可以理解为线程上下文是进程上下文的子集。

由于保存线程的上下文明显比进程的上下文小,因此系统切换线程时,必然开销更小。

进程切换和线程切换的区别?
线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor’s Translation Lookaside Buffer (TLB))或者相当的神马东西会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。

协程

什么是协程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程是微线程,在子程序内部执行,可在子程序内部中断,转而执行别的子程序,在适当的时候再返回执行。

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方。在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。进程线程都是同步机制,而协程则是异步。协程不需要多线程的锁机制。

线程与协程的区别:
(1)协程执行效率极高。协程直接操作栈基本没有内核切换的开销,所以上下文的切换非常快,切换开销比线程更小。
(2)协程不需要多线程的锁机制,因为多个协程从属于一个线程,不存在同时写变量冲突,效率比线程高。
(3)一个线程可以有多个协程。

协程的优势:
(1)协程调用跟切换比线程效率高:协程执行效率极高。协程不需要多线程的锁机制,可以不加锁的访问全局变量,所以上下文的切换非常快。
(2)协程占用内存少:执行协程只需要极少的栈内存(大概是4~5KB),而默认情况下,线程栈的大小为1MB。
(3)切换开销更少:协程直接操作栈基本没有内核切换的开销,所以切换开销比线程少。

进程通信(IPC,InterProcess Communication):

是指在不同进程之间传播或交换信息。

1.匿名管道:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。

2.有名管道:
匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO)。
有名管道不同于匿名管道之处在于它提供了一个路径名与之关联,以有名管道的文件形式存在于文件系统中,这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信,因此,通过有名管道,不相关的进程也能交换数据。值的注意的是,有名管道严格遵循先进先出(first in first out) ,对匿名管道及有名管道的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。有名管道的名字存在于文件系统中,内容存放在内存中。

3.共享内存:指两个或多个进程共享一个给定的存储区。共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。因为多个进程可以同时操作,所以需要进行同步。信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问

4.信号:信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。
如果该进程当前并未处于执行状态,则该信号就有内核保存起来,直到该进程恢复执行并传递给它为止。
如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。
信号是进程通信中唯一的异步通信方式
也可以简单理解为信号是某种形式上的软中断。一般信号来源可以是硬件、软件或者键盘输入

5.信号量:信号量主要用来解决进程和线程间并发执行时的同步问题,进程同步是并发进程为了完成共同任务采用某个条件来协调他们的活动,这是进程之间发生的一种直接制约关系。
对信号量的操作分为P操作(相当于wait操作)和V操作(相当于notify),P操作是将信号量的值减一,V操作是将信号量的值加一。当信号量的值小于等于0之后,再进行P操作时,当前进程或线程会被阻塞,直到另一个进程或线程执行了V操作将信号量的值增加到大于0之时。锁也是用的这种原理实现的。
信号量其实就是某种资源的数量,我们需要设定初始值,以及决定何时进行PV操作。

6.消息队列:
消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。
与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。
另外与管道不同的是,消息队列在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达
★消息队列特点总结:
(1)消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识.
(2)消息队列允许一个或多个进程向它写入与读取消息。
(3)管道和消息队列的通信数据都是先进先出的原则。
(4)消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取,比FIFO更有优势。
(5)消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺。
(6)目前主要有两种类型的消息队列:POSIX消息队列以及System V消息队列,系统V消息队列目前被大量使用。系统V消息队列是随内核持续的,只有在内核重起或者人工删除时,该消息队列才会被删除。

7.套接字socket:不同主机间的进程进行相互通信的端点,是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来。

线程通信方式

1、锁机制
互斥锁:提供了以排它方式阻止数据结构被并发修改的方法。
读写锁:允许多个线程同时读共享数据,而对写操作互斥。
条件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。

2、信号量机制:包括无名线程信号量与命名线程信号量

3、信号机制:类似于进程间的信号处理。

线程间通信的主要目的是用于线程同步,所以线程没有像进程通信中用于数据交换的通信机制。

进程调度的方法

1.先来先服务(FCFS)
2.短作业优先(SJF):包括非抢占式和抢占式,要求服务时间最短的进程先被服务
3.高响应比优先:响应比=(等待时间+要求服务时间)/ 要求服务时间
4.时间片轮转(RR)(抢占式的):按照各进程到达就绪队列的顺序,轮流让各进程分配一个时间片,如果没有运行完,将会被剥夺处理机,放入就绪队列末尾
5.优先级调度:每个进程都有优先级,然后按优先级高低调度(有抢占和非抢占)
6.多级反馈队列调度算法:设置多级就绪队列,各就绪队列从高级到低级,时间片从小到大。首先放在第一级队列,分配时间片,如果时间片结束没有执行完,放在第二级队列队尾。级别越高的队列越先执行

进程状态

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

2.就绪状态:当一个进程获得了除处理机以外的一切所需资源,一旦得到处理机即可运行,则称此进程处于就绪状态。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。

3.运行状态:当一个进程在处理机上运行时,则称该进程处于运行状态。处于此状态的进程的数目小于等于处理器的数目,对于单处理机系统,处于运行状态的进程只有一个。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。

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

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

有时候还有有七状态模型:
多了一个就绪挂起和阻塞挂起(进程被调入了硬盘中)
之所以出现这两个状态是因为进程优先级的出现,导致一些低优先级的进程可能等待很长时间,从而被对换至外存中。这样做:1.提高处理机效率 2.为运行进程提供足够的内存 3.在调试时,挂起被调试的进程

死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

条件:
1.互斥条件:即某个资源在一段时间内只能由一个进程占有,不能同时被两个或两个以上的进程占有。这种独占资源如打印机等,必须在占有该资源的进程主动释放它之后,其它进程才能占有该资源。这是由资源本身的属性所决定的。如独木桥就是一种独占资源,两方的人不能同时过桥。
2.不可剥夺条件:进程所获得的资源在未使用完毕之前,资源申请者不能强行地从资源占有者手中夺取资源,而只能由该资源的占有者进程自行释放。如过独木桥的人不能强迫对方后退,也不能非法地将对方推下桥,必须是桥上的人自己过桥后空出桥面(即主动释放占有资源),对方的人才能过桥。
3.请求与保持:进程至少已经占有一个资源,但又申请新的资源;由于该资源已被另外进程占有,此时该进程阻塞;但是,它在等待新资源之时,仍继续占用已占有的资源。还以过独木桥为例,甲乙两人在桥上相遇。甲走过一段桥面(即占有了一些资源),还需要走其余的桥面(申请新的资源),但那部分桥面被乙占有(乙走过一段桥面)。甲过不去,前进不能,又不后退;乙也处于同样的状况。
4.循环等待:存在一个进程等待序列{P1,P2,…,Pn},其中P1等待P2所占有的某一资源,P2等待P3所占有的某一源,…,而Pn等待P1所占有的的某一资源,形成一个进程循环等待环。就像前面的过独木桥问题,甲等待乙占有的桥面,而乙又等待甲占有的桥面,从而彼此循环等待。

安全序列:如果系统按照这种序列分配资源,那么每个进程都能够顺利完成;只要能找出一个安全序列,系统就是安全状态
银行家算法:在进程申请资源前,先预判此次分配是否会使系统进入不安全状态;如果会进入不安全状态,就使该进程阻塞等待

死锁检测:资源分配图
死锁解除:资源剥夺,撤销进程,回退进程

用户态、内核态

内核态其实从本质上说就是内核,它是一种特殊的软件程序,控制计算机的硬件资源,例如协调CPU资源,分配内存资源,并且提供稳定的环境供应用程序运行。

用户态就是提供应用程序运行的空间,为了使应用程序访问到内核管理的资源例如CPU,内存,I/O,内核必须提供一组通用的访问接口,这些接口就叫系统调用。

系统调用是操作系统的最小功能单位。根据不同的应用场景,不同的Linux发行版本提供的系统调用数量也不尽相同,大致在240-350之间。这些系统调用组成了用户态跟内核态交互的基本接口。

从用户态到内核态切换可以通过三种方式:
1.系统调用:系统调用本身就是中断,但是是软件中断,跟硬中断不同。
2.异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,就会触发切换。例如:缺页异常。
3.外设中断:当外设完成用户的请求时,会向CPU发送中断信号。

自旋锁、乐观锁、悲观锁

悲观锁

悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁

乐观锁总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。

自旋锁

自旋锁的定义:当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock)。

自旋锁有以下特点:
1.用于临界区互斥
2.在任何时刻最多只能有一个执行单元获得锁
3.要求持有锁的处理器所占用的时间尽可能短
4.等待锁的线程进入忙循环

自旋锁存在的问题
1.如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
2.无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

自旋锁的优点
1.自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
2.非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。(线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

自旋锁与互斥锁的区别
1.自旋锁与互斥锁都是为了实现保护资源共享的机制。
2.无论是自旋锁还是互斥锁,在任意时刻,都最多只能有一个保持者。
3.获取互斥锁的线程,如果锁已经被占用,则该线程将进入睡眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放。

CAS

CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作

高并发环境下,对同一个数据的并发读(两边都读出余额是100)与并发写(一个写回28,一个写回38)导致的数据一致性问题。

解决方案是在set写回的时候,加上初始状态的条件compare,只有初始状态不变时,才允许set写回成功,这是一种常见的降低读写锁冲突,保证数据一致性的方法。

IO多路复用和select、poll、epoll

IO多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;没有文件句柄就绪时会阻塞应用程序,交出cpu。多路是指网络连接,复用指的是同一个线程

IO多路复用有三种实现方式:select, poll, epoll

1.select:时间复杂度O(n),它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。

select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。

2.poll:时间复杂度O(n),poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的。

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

3.epoll:时间复杂度O(1),epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以说epoll实际上是事件驱动(每个事件关联上fd)的,此时对这些流的操作都是有意义的。

epoll操作过程需要三个接口,分别如下:
int epoll_create(int size)//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

int epoll_create(int size):创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event):函数是对指定描述符fd执行op操作。- epfd:是epoll_create()的返回值。- op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。- fd:是需要监听的fd(文件描述符)- epoll_event:是告诉内核需要监听什么事,

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout):等待epfd上的io事件,最多返回maxevents个事件。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

执行过程:
1.创建红黑树,调用epoll_create()创建一颗空的红黑树,用于存放FD及其感兴趣事件;
2.注册感兴趣事件,调用epoll_ctl()向红黑树中添加节点(FD及其感兴趣事件),时间复杂度O(logN),向内核的中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它添加到就绪队列中。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到就绪队列中了;
3.获取就绪事件,调用epoll_wait()返回就绪队列中的就绪事件,时间复杂度O(1);

三者区别

1.最大连接数

2.FD剧增后带来的效率问题

3.消息传递方式

poll和epoll的区别

poll将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有缺点:
1.大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
2.poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。其优点有:
1.没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。
2.效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
3.内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

select和epoll的区别

select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。

select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。
内核需要传递消息到用户空间,需要内存拷贝

相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

epoll能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。

效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

内存的分配和回收

有两大类管理方式:第一类是连续分配管理方式,第二类是非连续分配管理方式

连续分配方式又分为三种(通俗易懂):

对应动态分区分配的方式,有四种算法来对分区就行选择

连续分配的缺点和明显,会有空间的浪费,虽然能通过紧凑来处理,但是紧凑操作时间代价很高
所以出现了非连续分配方式,指的是为进程分配的是分散的空间,地址上不是连续的

非连续分配方式包括三种,基本分页存储管理,基本分段存储管理,段页式存储管理

基本分页存储管理的核心思想就是把内存分成一个个大小相等的小分区(页框),然后把进程按照分区大小分成小的部分(页面),把进程的这些小的部分就可以放到这些分区中;然后用一个页表来记录页面和内存块的对应关系(页表存放在内存中)(下图1)
那么页表又怎么找到呢,就是通过一个页表寄存器来存储的,用来存放页表的起始位置和页表长度
为了加速查询内存中的页表,出现了快表(TLB),相当于缓存,存放在高速存储器中(下图2)
页表可能过大,所以出现了多级页表(下图3)


基本分段存储管理
基本分段是按照程序自身逻辑把进程分为多个段,每个段大小可能不一定相同,
同样也有段表,不过段表中要存放段长和基址,因为段长度不一定相同
也可以加入快表,还有多级段表

段页式存储管理
先分段,再将段分成大小相同的页

虚拟内存

虚拟内存的出现是为了解决传统的内存管理方式中
1.一次性:作业必须一次性全部装入内存中才能运行,这样会造成大作业无法装入,在有大量作业时,并发能力很低
2.驻留性: 在一个时间段内,其实程序中只有一部分数据被用到了,大部分数据用不到,所以很多内存都被没有必要的数据占用了

局部性原理
1.时间局部性:指一段指令被访问,不久后很可能再次被访问
2.空间局部性:一旦访问了某个存储单元,不久后附近的存储单元也很可能被访问

基于局部性原理,可以将程序中用到的数据装入内存中,暂时用不到的放在外存中。当程序执行时,如果数据不在内存中,再从外存中调入。内存不够时,将暂时用不到的数据换出到外存。
这样,内存空间好像变的大了很多,这就是虚拟内存

虚拟内存的实现,基于三种内存管理方式,也就是非连续分配方式的改进
请求分页存储管理,请求分段存储管理,请求段页式存储管理
主要区别就是当程序执行时,如果访问点的信息不在内存中,由操作系统从外存中调入(缺页中断)。内存不够时,将暂时用不到的数据换出到外存(页面置换)。页表当然也有新增的项

页面置换算法

OPT最佳置换算法(难以实现);FIFO先入先出置换算法;
LRU最近最久未使用置换算法(最接近最佳置换)
CLOCK时钟置换算法

物理地址、逻辑地址、线性地址(虚拟地址)

为了能够很好地理清楚这三个地址之间的关系,先想想为什么要有这些地址?

1.首先物理地址不用多说,就是在存储器中是以字节为单位存储信息,为了正确存放和获取信息,每一个字节单元给以一个唯一的存储器地址,这个地址称为物理地址,又称为绝对地址。
地址从0开始编号,顺序地每次加1,因此存储器的物理地址空间是呈线性增长的。它是用二进制数来表示的,是无符号整数,书写格式为十六进制数。它是出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果。用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。

2.逻辑地址(相对地址):物理地址都是二进制数,如果要对某一个地址进行操作,必须得知道这个单元的地址,这肯定不是正常人能办到的。另外,在写程序的时候,我们并不需要去直接对一个物理地址进行操作,我们是给出一个相对地址,然后由操作系统来帮我们找到存储单元来存储数据的。而我们写的这个相对地址,就是逻辑地址,逻辑地址也被称为相对地址

逻辑地址是指在计算机体系结构中是指应用程序角度看到的内存单元(memory cell)、存储单元(storage element)、网络主机(network host)的地址。 逻辑地址往往不同于物理地址(physical address),通过地址翻译器(address translator)或映射函数可以把逻辑地址转化为物理地址。
在有地址变换功能的计算机中,访问指令给出的地址 (操作数) 叫逻辑地址,也叫相对地址。要经过寻址方式的计算或变换才得到内存储器中的物理地址。把用户程序中使用的地址称为相对地址即逻辑地址。逻辑地址由两个16位的地址分量构成,一个为段基值,另一个为偏移量。两个分量均为无符号数编码。

3.线性地址(虚拟地址)
如果知道了虚拟内存的话, 可以看到在虚拟内存的实现方式中,有请求段页式存储管理
看了很多关于线性地址和虚拟地址的解释,我的理解就是它们指的是同一个东西,就是中间层,即逻辑地址和物理地址之间转换的一个途径,可以理解为页表寄存器一样的东西

简单理解:在内存的段页式管理方式中,逻辑地址经过分段单元转换为线性地址,线性地址经过分页单元映射为物理地址。如果没有第二步的分页,逻辑地址就直接转换成物理地址

有这么一段话:
随着寻址空间的进一步扩大、虚拟内存技术的引入,操作系统引入了分页机制。引入分页机制后,逻辑地址经过段机制转换得到的地址仅是中间地址,还需要通过页机制转换,才能得到实际的物理地址。逻辑地址 -->(分段机制) 线性地址 -->(分页机制) 物理地址
(又认真研究了一下,是这么解释的,分段是刚开始出现的机制,而且这个分段我感觉和内存分配机制里面的基本分段存储管理也不太一样,它的出现是了解决直接操作物理内存产生的问题,这时候转换过程就是逻辑地址直接转换为物理地址;后来引入了虚拟内存以后,又出现了分页,这时候,要将逻辑地址转换为物理地址,就是先经过分段转换为线性地址,再分页转换为物理地址。而且线性地址好像是为了兼容一个处理器才有这么一个概念,所以应该不必过于在意,应该也不会问)

僵尸进程和孤儿进程

我们知道在unix / linux中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。 当一个 进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。

深拷贝和浅拷贝

浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间;深拷贝不断对指针进行拷贝,而且对指针指向的内容进行拷贝,经深拷贝后的指针是指向两个不同的地址空间。

浅拷贝
对一个已知对象进行拷贝时,编译系统会自动调用一次构造函数(拷贝构造函数),如果用户未定义拷贝构造函数,则会调用默认拷贝构造函数,调用一次构造函数,调用两次析构函数,两个对象的指针成员所指内存相同,但是程序结束时该内存被释放了两次,会造成内存泄漏问题。

深拷贝
在对含有指针成员的对象进行拷贝时,必须要自己定义拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间,即进行深拷贝,这样就避免了内存泄漏的发生,调用一次构造函数,一次自定义拷贝构造函数,两次析构函数。两个对象的指针成员所指内容不同。

硬链接和软链接

软链接相当于快捷方式,实现跳转,指向的是file(一个文件名)。硬链接相当于创建了另一个指针,指向文件,而删除原file,软链接失效;硬链接因为是inode的另一个指针,所以还是可以访问文件

软链接相当于建立了一个新的快捷方式文件,该文件有自己的名称和inode以及物理存储的文件数据,文件数据里记录着如何跳转的设置数据,访问该快捷文件会被重新定向到原始文件,删除原始文件,软链文件失效;
硬链接相当于为当前文件名对应的文件再建立了一个文件别名,别名对应的inode以及物理数据都是一样的,一旦建立,我们甚至根本无法区分谁是原始文件的原始名称,删除文件的其中一个名称,文件不会丢失,除非把所有的名称都删除。

注:inode是文件系统中存储文件元信息的区域,中文叫节点索引,每个节点索引包含了文件的创建者,大小,日期等等。可以通过ls -i file 命令查看inode的值。

软连接和硬链接的区别
(1) 软链接可以为文件和目录(哪怕是不存在的)创建链接;硬链接只能为文件创建链接。
(2) 软链接可以跨文件系统;硬链接必须是同一个文件系统
(3) 硬链接因为只是文件的一个别名,所以不重复占用内存;软链接因为只是一个访问文件的快捷方式文件,文件内只包含快捷指向信息,所以占用很小的内存。
(4) 软链接的文件权限和源文件可以不一样;硬链接文件权限肯定是一样的,因为他们本来就是一个文件的不同名称而已。

二者使用场景
​ 一般比较重要的文件我们担心文件被误删除且传统复制备份方式占用double数量的空间会造成浪费,可以使用硬链做备份来解决;软链接一般被用来设置可执行文件的快捷方式的路径。

软链接文件的源文件必须写成绝对路径,而不能写成相对路径(硬链接没有这样的要求);否则软链接文件会报错。

硬中断和软中断

硬中断
​ 由与系统相连的外设(比如网卡、硬盘)自动产生的。主要是用来通知操作系统外设状态的变化。比如当网卡收到数据包的时候,就会发出一个中断。我们通常所说的中断指的是硬中断(hardirq)。

软中断
​ 为了满足实时系统的要求,中断处理应该是越快越好。Linux为了实现这个特点,当中断发生的时候,硬中断处理那些短时间就可以完成的工作,而将那些处理事件比较长的工作,放到中断之后来完成,也就是软中断(softirq)来完成。

中断嵌套
​ Linux下硬中断是可以嵌套的,但是没有优先级的概念,也就是说任何一个新的中断都可以打断正在执行的中断,但同种中断除外。软中断不能嵌套,但相同类型的软中断可以在不同CPU上并行执行。

软中断与硬中断之间的区别
(1)硬中断是由外部事件引起的因此具有随机性和突发性;软中断是执行中断指令产生的,无外面事件中断请求信号,因此软中断的发生不是随机的而是由程序安排好的;
(2)硬中断的中断号是由中断控制器提供的;软中断的中断号是由指令直接给出的,无需使用中断控制器。
(3)硬中断的中断响应周期,CPU需要发中断回合信号;软中断的中断响应周期,CPU不需要发中断回合信号。
(4)硬中断是可屏蔽的;软中断是不可屏蔽的。

以上是关于面经总结3(操作系统)的主要内容,如果未能解决你的问题,请参考以下文章

面向校招操作系统面经总结

面试必备超长JVM面经总结

头条抖音后端技术3面,从零开始系统化学Java,一线互联网公司面经总结

(面经总结)冲刺大厂之面经总结

9.13面经

游戏开发面经我在阿里HRG面这关跪掉了,游戏客户端开发岗,总结一下(阿里 | 游戏 | 凉面面经)