记录一下面试以来遇到的操作系统相关问题及解答

内存管理

简述进程切换的流程

涉及公司:阿里云实习生

如果想要从A进程切换到B进程,必定要先从用户态切换到内核态,因为这个切换的工作你不能让用户进程去实现,不然当CPU在用户进程手上的时候,他可以选择一直执行,不让出CPU,这肯定是不允许的。所以操作系统需要先挂起正在占用CPU的A进程,才能切换到B进程。

由于从用户态切换到内核态的时候,CPU是在用户进程手中,所以这个是通过硬中断来实现的。在从用户态切换到内核态之前需要保存用户进程的上下文,以便下一次执行时可以继续之前的工作。

这个上下文就是进程执行的环境,包括所有的寄存器变量,进程打开的文件、内存信息等。一个进程的上下文可以分为用户级上下文,寄存器上下文,系统级上下文。用户级上下文存储的是用户进程的内存数据以及堆栈数据等;寄存器上下文是一些通用寄存器;系统级上下文是内核栈、PCB(进程控制块)等。

进程在地址空间中会划分为哪些区域

涉及公司:阿里云实习生

这个问题在我之前的工作中其实还是有所涉及的,我来简单讲一下把文件加载到内存中的一个过程,以Window平台为例吧,PE文件我比较熟,在PE文件中,有一个叫节的概念,节是PE文件中存放代码和数据的基本单元,用以存储不同类型的数据,比如data节、code节等,一个节的所有原始数据必须加载到连续的内存空间里,这也就造成了在虚拟地址空间中的区块划分。

在虚拟地址空间中会按照节划分为代码段、数据段、未初始化的数据段以及堆栈这些区块。

栈与堆有什么区别

涉及公司:阿里云实习生、拼多多实习生

我们常说堆栈堆栈,其实堆栈是两个不同的概念,最直观的理解,堆是由用户来控制的,我们可以使用malloc这种命令来在堆中申请内存,而栈是由操作系统控制的,在栈中存储的是这个进程的局部变量等,比如我们用malloc来申请一块内存,内存本身是在堆中开辟的,而指向这块内存的指针存储在栈中。

操作系统为什么分内核态和用户态,这两者之间如何切换

涉及公司:拼多多实习生

因为在CPU的指令中,有一些是非常危险的,比如清理内存、设置时钟等,如果所有的程序都能使用,就可能造成系统的崩溃,所以,CPU将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统使用。CPU的特权级别有四级,从Ring0到Ring3,正常使用时一般只有两级,即用户态的Ring3和内核态的Ring0。Ring3状态不能访问Ring0的地址空间,包括代码和数据。

用户态切换到内核态的三种方式

  • 系统调用(系统调用是通过软中断实现的)
  • 中断(硬)
  • 异常

malloc的实现机制

涉及公司:阿里云实习生

malloc本质上是维护了一个内存空闲链表,每次我们调用malloc申请空间的时候,链表就会从头开始遍历,来寻找一个合适的空闲内存空间,然后把这个空间给分割开,一部分分配给用户,另一部分继续标注为空闲,而当没有足够大的空闲块时,malloc就会通过系统调用来申请更多的内存块。而我们调用free来释放内存块的时候,该内存块就会回到链表中,并且相邻的内存块会被合并。

搜索空闲块的算法主要有首次适配、下一次适配、最佳适配,首次适配即第一次找到足够大的内存块就分配,但这样会产生很多的内存碎片,也因此第二次适配被提出来缓解这个问题。另一个极端则是最佳适配,即找到一块刚好大于我们所需内存大小的内存块,这种做法一方面耗时长,另一方面也会产生一些极小的内存碎片。
这两种思路可以看出是在性能和空间利用率上寻找一个平衡点,在工程中实际上有很多这种没有完美解决方案,只能寻找平衡的问题。

虚拟地址怎么映射到物理地址

涉及公司:阿里云实习生、腾讯实习生

虚拟地址的构成为页目录索引(10位)+页表索引(10位)+表内偏移(12位)

以win32系统为例,页目录和页表都为1024个,页表大小为4KB,一共是4G的虚拟内存空间

而从虚拟地址映射到物理地址实际上就是通过页目录和页表的索引找到内存页。

在页表项中有一位标志位,用来标识包含此数据的页是否在物理内存中,如果在的话,就直接做地址映射,否则,抛出缺页中断,操作系统会把次数据页调入内存。

socket编程中怎么处理并发请求

涉及公司:阿里云实习生、腾讯实习生

对多线程的处理与单线程不同的位置在于各个不同的进程可能会访问相同的资源,如果是对资源进行修改的话,就需要用到锁

简述IO多路复用

涉及公司:阿里云实习生、腾讯实习生

Linux的IO访问通常是先将数据拷贝到操作系统的内核缓冲区,然后再从内核缓冲区拷贝到应用程序的地址空间。在这两个阶段中,有不同的IO方式,主要分为阻塞IO、非阻塞IO、异步IO以及IO多路复用。

阻塞IO即当数据还未准备好,也就是数据还在操作系统的内核缓存区时,用户进程就会一直阻塞,等待数据从操作系统内核缓冲区拷贝到应用程序的地址空间。阻塞IO在这两个阶段都是阻塞的。

非阻塞IO则是如果数据还没准备好,操作系统会给应用程序返回一个error,并不阻塞应用程序,而一般应用程序会持续询问内核数据是否准备好,所以从另一个角度来说也是阻塞的。

而异步IO才是真正的不阻塞,当用户程序发起read后,操作系统会立即进行回复,这样用户程序就可以去做其他事情,当数据被拷贝到用户程序的地中空间后,操作系统会给用户程序发一个信号,而用户程序可以采用回调函数的方式对这个信号进行响应。

IO多路复用则是允许一个程序同时等待多个文件描述符,当任意一个文件描述符就绪,select函数就会返回,当然IO多路复用在本质上还术语阻塞IO,只不过可以同时进行多个IO操作。

Linux的IO多路复用机制中有select、poll、epoll三种,
select和poll的时间复杂度都是O(n),因为他们都是在对IO列表进行轮询,不同点在于select能监视的文件描述符有上限,一般为1024,当然这个是在Linux内核中进行的宏定义,是可以修改的,而poll是基于链表来存储的,所以没有这个上限。
而epoll是基于事件驱动的,所以不需要轮询,epoll会把事件和每一个IO流对应起来。并且epoll是通过一块共享内存来实现内核空间和用户空间的通信的,比起select和poll的大量数据拷贝效率更高。
不过select的优点在于兼容不同的操作系统,而poll和epoll都只能在linux上使用。

简述进程通信的各种方法

涉及公司:腾讯实习生

进程间通信的方式通常分为管道、系统IPC、套接字三种,其中管道有无名管道、命名管道,系统IPC有消息队列、信号、共享内存

  • 无名管道的本质是在内核缓冲区的环形队列,每次读取数据后缓冲区都会移动,并且无名管道只能在有亲缘关系的进程间使用
  • 命名管道则以文件的形式存在,由于有一个路径名,使用没有亲缘关系的进程间也可以使用命名管道
  • 消息队列是存放在内核中的消息链表,具有特定的格式,支持多种数据类型,且允许多个进程进行读写
  • 信号是软件层次上对中断机制的一种模拟,是一种异步通信方式,并且信号可以在用户空间进程和内核之间直接交互
  • 共享内存顾名思义就是两个进行对同一块内存进行读写,是最快的IPC形式,但不适合大量的数据传输
  • Socket是对TCP/IP协议族的封装,不仅可以用于本机上的进程间通信,更多的被用于网络通信中

进程线程管理

进程的互斥与同步

在操作系统中,进程是占有资源的最小单位,对于那种只能同时被一个进程持有的资源我们称为临界资源,对于临界资源的访问,必须是互斥的。(对于;临界资源的访问过程分为:进入区、临界区、退出区、剩余区)

而进程之间访问临界资源时可以构成同步与互斥两种关系,同步即两个进程的资源访问必须是先后关系,比如经典的生产者消费者问题,读者写着问题。而互斥则是两种在进行资源抢到,比如购票问题。

通常在软件层面可以使用替换算法来实现,即每个进程持有一个标志,每次当使用资源时则将自己的标志与资源的标志互换,如果在互换的过程中发现自己获得的标志是正在使用的状态,则在此循环等待。这种方法的缺点在于每个进程都需要进行循环等待,比较低效。所以一般是通过硬件层面的信号量即PV操作来实现进程的临界资源管理。

死锁的解决方法

涉及公司:阿里云实习生

死锁的产生是在这样一种环境中:比如我们有两个进程AB,他们都需要资源1和资源2,当进程A持有资源1,进线程B持有资源2的时候,他们都需要对方手上的进程,而一般操作系统又不允许抢占,这个时候就发生了死锁。

从这个例子中其实可以总结出死锁的几个必要条件:

  • 1.一个资源只能被一个进程所占有,不能共享
  • 2.一个线程请求资源失败时,它会等待而不是释放
  • 3.一个线程在释放资源之前其他进程不能抢夺资源
  • 4.循环等待

从死锁产生的原因未明可以设计一些方法去避免死锁的发生

  • 1.静态分配资源,一开始就把一个进程所需的全部资源都分配给它,但这样会降低资源的使用效率
  • 2.允许抢占,需要设置进程的不同优先级,高优先级的进程可以抢占低优先级的进程的资源
  • 3.把资源进行编号,申请资源必须按照资源的编号顺序来申请

如果死锁已经发生了,就需要去解开死锁,其本质思想就是分配资源打破循环等待

  • 1.可以运行抢占,从一个或多个进程中抢出资源来给其他进程
  • 2.也可以终止一些进程,来达到释放资源的目的

进程调度算法

  • 先来先服务调度算法
    • 对长作业比较有利,但对短作业不利
  • 时间片轮转调度法
    • 每个进程只能运行一个时间片
    • 时间片的大小对系统性能的影响很大,时间片过大就和先来先服务算法一样,时间片过小会导致进行切换开销大
  • 短作业优先调度算法
    • 对长作业不利,不能保证紧迫性作业(进程)被及时处理
  • 最短剩余时间优先
    • 允许抢占,总是选择预期剩余时间最短的进程
  • 高响应比优先调度算法
    • R=(w+s)/s (R为响应比,w为等待处理的时间,s为预计的服务时间),选择R最大的进行执行
  • 优先级调度算法
    • 进程优先级可以分为静态优先级和动态优先级
  • 多级反馈队列调度算法
    • 分为多个队列,每个队列中按时间片轮转调度算法来进行进程调度,每一级的队列时间片大小也不一样,如果进行在第一个队列的时间片内没有完成,就会进入第二个队列,以此类推,只有当第一个队列为空才执行第二个队列的进行
    • 短作业有限且长作业不会太长时间不被处理

磁盘调度算法

  • 先来先服务算法(FCFS)
    • 根据进程请求访问磁盘的先后次序进行调度
    • 优点是公平、简单
    • 缺点是吞吐量低,寻道时间长
  • 最短寻道时间优先算法(SSTF)
    • 访问与当前磁头所在的磁道距离最近的磁道
    • 优点是可以得到比较好的吞吐量
    • 缺点是对内外边缘磁道的请求将会被无限延迟
  • 扫描算法(SCAN)电梯调度算法
    • 优先考虑磁头当前的移动方向,再考虑欲访问的磁道与当前磁道的距离
    • 优点是避免了饥饿现象的出现
    • 缺点是两侧磁道被访问的频率仍低于中间磁道
  • 循环扫描算法(CSCAN)
    • 在SCAN算法的基础上,磁头只单向移动,当磁头移到最外的被访问磁道时,磁头立即返回到最里的欲访磁道
    • 优点是访问请求均匀分布

页面调度算法

  • 先进先出调度算法(FIFO,First In First Out)
  • 最近最少使用算法(LFU, Least Frequently Used)
  • 最近最久未使用算法(LRU,Least Recently Used)
  • 时钟置换算法——为每一页设置访问位和修改位,将内存中所有页面通过连接指针接成循环队列,当页面被访问时访问位置1,被修改则修改位置1,每次淘汰时,从指针当前位置开始循环遍历,第一次寻找访问位和修改位都为0的页面,如果没有则将扫描过的节点访问位为1的置为0,找到第一个访问位为0的将其淘汰。这个算法的原则就的在LRU的基础上偏向于淘汰未被修改的页面。
  • 最佳置换算法——理想算法,找一个未来最长时间才会被访问的页面进行淘汰。