【游戏客户端面试题干货】-- 2021年度最新游戏客户端面试干货(操作系统篇)
大家好,我是Lampard~~
经过一番艰苦奋战之后,我终于是进入了心仪的公司。
今天给大家分享一下我在之前精心准备的一套面试知识。
今天和大家分享的是操作系统相关的面试题
额其实这篇博客在项目开发的过程中,用到的情况并不多(也可能因为我还是新人,接触不多)。然鹅各种游戏大厂都会很注重面试者的基础,在博主所见识过的大厂面试中,它们主要是着重面试者以下几种技能:1.语言:c++,c#,lua,js,python等,2.引擎:内存管理,渲染机制,事件分发机制,计时器调度机制,设计模式等,3.基础:虽然客户端和服务端会有不同的侧重,但是只要你是科班出身,它们就要考察你的基础,比如说计算机网络,操作系统,数据库,计算机组成原理等,4.算法:前三者如果说大家可以靠过目不忘的本领把知识点都背下来,那么算法的测试就是见真章的时候了,做出来不是终点,它们会一直深挖一直深挖,让你一直优化(头皮发麻)。5.项目经验:若各位是校招的话,前面4点会占了80,90%的面试比重,但是若是社招的同学,项目经验就是最重要的,起码占50%的比重,面试官会考察你做过什么系统,看你的能力到哪里。对于客户端来说如果你有计算机图形学,接过战斗系统,接过sdk,负责过打包patch业务,有为项目写过脚本完善工具链,那么就会有加分点。所以多准备准备是没有错滴。最后祝大家疯狂收割offer,通过自己的努力摆脱生活的苟且,走向诗和远方~
一. 线程和进程
基本概念:
进程:进程是对运行时程序的一个封装,是系统进行资源调度和分配的基本单位(在操作系统层面的并发)。
线程:线程是进程的子任务,是cpu调度和分派的基本单位(在进程层面的并发)。
为什么要设计进程和线程呢?有一位解释的很好
一开始大家想要同一时间执行那么三五个程序,大家能一块跑一跑。特别是UI什么的,别一上计算量比较大的玩意就跟死机一样。于是就有了并发,从程序员的角度可以看成是多个独立的逻辑流。内部是单cpu时间分片,能快速的切换逻辑流,看起来像是大家一块跑的就行。
但是一块跑就有问题了。我计算到一半,刚把多次方程解到最后一步,你突然插进来,我的中间状态咋办,我用来储存的内存被你覆盖了咋办?所以跑在一个cpu里面的并发都需要处理上下文切换的问题。进程就是这样抽象出来个一个概念,搭配虚拟内存、进程表之类的东西,用来管理独立的程序运行、切换。
后来一电脑上有了好几个cpu,好咧,大家都别闲着,一人跑一进程。就是所谓的并行。
因为程序的使用涉及大量的计算机资源配置,把这活随意的交给用户程序,非常容易让整个系统分分钟被搞跪。所以核心的操作需要陷入内核(kernel),切换到操作系统,让老大帮你来做。
有的时候碰着I/O访问,阻塞了后面所有的计算。空着也是空着,老大就直接把CPU切换到其他进程,让人家先用着。当然除了I\O阻塞,还有时钟阻塞等等。一开始大家都这样弄,后来发现不成,太慢了。为啥呀,一切换进程得反复进入内核,置换掉一大堆状态。进程数一高,大部分系统资源就被进程切换给吃掉了。后来搞出线程的概念,大致意思就是,这个地方阻塞了,但我还有其他地方的逻辑流可以计算,这些逻辑流是共享一个地址空间的,不用特别麻烦的切换页表、刷新TLB,只要把寄存器刷新一遍就行,能比切换进程开销少点。
简单来说就是,进程存在的意义是为了在cpu进行并发操作时,保护上下文,保护进程数据间不相互影响而设计出来的概念。有了进程我们就可以在单个cpu进行多进程分片,也可以多个cpu并发处理进程。而线程,就是在一个进程内其实也有多个逻辑,当一个逻辑需要io的时候,会白占cpu,占着茅坑不拉屎。而进程间切换的代价也比较大,为了提高效率,在就设计出可线程的概念,当进程中其中一个线程需要IO的时候,另一个线程来占用cpu以此提高效率。
进程和线程的区别,我们可以用以下的五点进行概括:
1.一个线程只能属于一个进程,而一个进程可以有多个线程,线程依赖于进程而存在。
2.一个进程在执行的过程中有独立的内存空间,而线程之间共享进程的内存(线程有自己的寄存器组)。
3.进程是资源分配的最小单位,而线程是cpu调度的最小单位
4.进程之间切换开销大,而线程切换只需要保存一些寄存器数据
5.进程之间相互独立不会造成影响,而线程之间会造成死锁
二. 线程和进程之间的通讯方式
实际上只有进程间需要通信,同一进程的线程共享地址空间,没有通信的必要,但要做好同步/互斥,保护共享的全局变量。
而进程间通信无论是信号,管道pipe还是共享内存都是由操作系统保证的,是系统调用.
一、进程间的通信方式
- 管道( pipe ):
管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。 - 有名管道 (namedpipe) :
有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。 - 信号量(semophore ) :
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 - 消息队列( messagequeue ) :
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 - 信号 (sinal ) :
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。 - 共享内存(shared memory ) :
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。 - 套接字(socket ) :
套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信。
二、线程间的通信方式
- 锁机制:包括互斥锁、条件变量、读写锁
互斥锁提供了以排他方式防止数据结构被并发修改的方法。
读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。 - 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
- 信号机制(Signal):类似进程间的信号处理
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。
三. 死锁
死锁是不同的进程在争夺资源而造成相互等待的现象。
要造成死锁,必须满足以下的四种条件。
1.互斥条件
进程对所分配的资源不允许其他进程访问
2.请求和保持条件
在等待资源的过程中不会释放资源
3.不可剥夺
其他进程不可以强制剥夺该资源
4.环路等待
必然存在一条进程资源的环形链
图为环形链:
解决死锁
上文提及,造成死锁必须满足四个条件。那么我们就可以破坏其中任意一个条件来解决死锁。
1.互斥条件
当超时未释放时允许访问
2.请求和保持条件
一次性分配资源不需要等待
3.不可剥夺
超时判定死锁时可以强制剥夺
4.环路等待
终止某一个进程,破坏该链
四. 并发和并行
并发:是指同一个cpu执行多个进程,进程之间交叉执行着指令
并行:指的是多cpu机器,分别单独的运行着不同的程序
五. 说一说进程状态的转换图
进程的状态分别有:创建,就绪,执行,终止,等待阻塞
正常的流程是:创建 -> 就绪(准备好)-> 执行 -> 终止
若在执行的过程中,执行量过大,不能够在操作系统分配的时间片内完成,那么就回到就绪态等待下一次执行
若在执行的过程中,需要等待资源,那么就会进入等待阻塞状态,等到资源齐备之后,就会被唤醒回到就绪态再次等待执行。
六. 虚拟内存和物理内存
为什么需要虚拟内存
操作系统有虚拟内存与物理内存的概念。在很久以前,还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是有限的,这取决于CPU的地址线条数。
比如在32位平台下,寻址的范围是2^32也就是4G。并且这是固定的,如果没有虚拟内存,且每次开启一个进程都给4G的物理内存,就可能会出现很多问题:因为我的物理内存时有限的,当有多个进程要执行的时候,都要给4G内存,很显然你内存小一点,这很快就分配完了,于是没有得到分配资源的进程就只能等待。当一个进程执行完了以后,再将等待的进程装入内存。这种频繁的装入内存的操作是很没效率的
于是针对上面会出现的各种问题,虚拟内存就出来了。
这个虚拟内存你可以认为,每个进程都认为自己拥有4G的空间,这只是每个进程认为的,但是实际上,在虚拟内存对应的物理内存上,可能只对应的一点点的物理内存,实际用了多少内存,就会对应多少物理内存。进程得到的这4G虚拟内存是一个连续的地址空间(这也只是进程认为),而实际上,它通常是被分隔成多个物理内存碎片,还有一部分存储在外部磁盘存储器上,在需要时进行数据交换。
进程开始要访问一个地址,它可能会经历下面的过程
- 次我要访问地址空间上的某一个地址,都需要把地址翻译为实际物理内存地址
- 所有进程共享这整一块物理内存,每个进程只把自己目前需要的虚拟地址空间映射到物理内存上
- 进程需要知道哪些地址空间上的数据在物理内存上,哪些不在(可能这部分存储在磁盘上),还有在物理内存上的哪里,这就需要通过页表来记录
- 页表的每一个表项分两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址(如果在的话)
- 当进程访问某个虚拟地址的时候,就会先去看页表,如果发现对应的数据不在物理内存上,就会发生缺页异常
- 缺页异常的处理过程,操作系统立即阻塞该进程,并将硬盘里对应的页换入内存,然后使该进程就绪,如果内存已经满了,没有空地方了,那就找一个页覆盖,至于具体覆盖的哪个页,就需要看操作系统的页面置换算法是怎么设计的了。
上文提及,若能够在页表中找到虚拟内存对应的物理内存,那么就会访问到该物理内存的内容。但是当在页表中查询发现对应的数据不在物理内存上(在磁盘中),就会发生缺页异常。
缺页中断的处理步骤:
1.保护cpu现场
2.分析原因
3.执行缺页中断恢复
4.恢复现场
执行缺页中断恢复同样分两种情况
首先是当前的物理内存还有空余,那么就会将硬盘里对应的页换入内存。若当前内存已经满了,那么此时可以有两种执行方案:1.替换最先进入的页,2.替换最少使用的页(常用)
好,今天的分享就到这里,祝各位功力渐长平步青云,谢谢大家~~