操作系统部分面试题
1. 简述进程切换的流程
如果想要从A进程切换到 B 进程,必定要先从用户态切换到内核态,因为这个切换的工作你不能让用户进程去实现,不然当 CPU 在用户进程手上的时候,他可以选择一直执行,不让出 CPU,这肯定是不允许的。所以操作系统需要先挂起正在占用 CPU 的 A 进程,才能切换到 B 进程。
由于从用户态切换到内核态的时候,CPU 是在用户进程手中,所以这个是通过硬中断来实现的。在从用户态切换到内核态之前需要保存用户进程的上下文,以便下一次执行时可以继续之前的工作。
这个上下文就是进程执行的环境,包括所有的寄存器变量,进程打开的文件、内存信息等。一个进程的上下文可以分为用户级上下文,寄存器上下文,系统级上下文。
用户级上下文存储的是用户进程的内存数据以及堆栈数据等;
寄存器上下文是一些通用寄存器;
系统级上下文是内核栈、PCB (进程控制块)等。
2. 进程在地址空间中会划分为哪些区域
- 代码段
- 数据段
- 未初始化的数据段
- 堆栈
3. 栈与堆有什么区别
- 堆是由用户来控制的,可以使用
malloc
这种命令来在堆中申请内存 - 栈是由操作系统控制的,在栈中存储的是这个进程的局部变量等
用 malloc
来申请一块内存,内存本身是在堆中开辟的,而指向这块内存的指针存储在栈中。
4. 操作系统为什么分内核态和用户态,这两者之间如何切换
因为在CPU的指令中,有一些是非常危险的,比如清理内存、设置时钟等,如果所有的程序都能使用,就可能造成系统的崩溃,所以,CPU 将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统使用。CPU 的特权级别有四级,从 Ring0 到 Ring3,正常使用时一般只有两级,即用户态的 Ring3 和内核态的 Ring0。Ring3 状态不能访问 Ring0 的地址空间,包括代码和数据。
用户态切换到内核态的三种方式
- 系统调用(系统调用是通过软中断实现的)
- 中断(硬)
- 异常
5. malloc
的实现机制
malloc
本质上是维护了一个内存空闲链表,每次我们调用 malloc
申请空间的时候,链表就会从头开始遍历,来寻找一个合适的空闲内存空间,然后把这个空间给分割开,一部分分配给用户,另一部分继续标注为空闲,而当没有足够大的空闲块时,malloc
就会通过系统调用来申请更多的内存块。而我们调用 free
来释放内存块的时候,该内存块就会回到链表中,并且相邻的内存块会被合并。
搜索空闲块的算法主要有首次适配、下一次适配、最佳适配。首次适配即第一次找到足够大的内存块就分配,但这样会产生很多的内存碎片,也因此第二次适配被提出来缓解这个问题。另一个极端则是最佳适配,即找到一块刚好大于我们所需内存大小的内存块,这种做法一方面耗时长,另一方面也会产生一些极小的内存碎片。
这两种思路可以看出是在性能和空间利用率上寻找一个平衡点,在工程中实际上有很多这种没有完美解决方案,只能寻找平衡的问题。
※6. 虚拟地址怎么映射到物理地址
虚拟地址的构成为页目录索引 (10位) +页表索引 (10位) +表内偏移 (12位)
以 win32 系统为例,页目录和页表都为 1024 个,页表大小为 4KB,一共是 4G 的虚拟内存空间
而从虚拟地址映射到物理地址实际上就是通过页目录和页表的索引找到内存页。
在页表项中有一位标志位,用来标识包含此数据的页是否在物理内存中,如果在的话,就直接做地址映射,否则,抛出缺页中断,操作系统会把次数据页调入内存。
7. socket 编程中怎么处理并发请求
对多线程的处理与单线程不同的位置在于各个不同的进程可能会访问相同的资源,如果是对资源进行修改的话,就需要用到锁。
※8. 简述 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上使用。
9. 简述进程通信的各种方法
进程间通信的方式通常分为管道、系统 IPC、套接字三种,其中管道有无名管道、命名管道,系统 IPC 有消息队列、信号、共享内存。
- 无名管道的本质是在内核缓冲区的环形队列,每次读取数据后缓冲区都会移动,并且无名管道只能在有亲缘关系的进程间使用。
- 命名管道则以文件的形式存在,由于有一个路径名,使用没有亲缘关系的进程间也可以使用命名管道。
- 消息队列是存放在内核中的消息链表,具有特定的格式,支持多种数据类型,且允许多个进程进行读写。
- 信号是软件层次上对中断机制的一种模拟,是一种异步通信方式,并且信号可以在用户空间进程和内核之间直接交互。
- 共享内存顾名思义就是两个进程对同一块内存进行读写,是最快的 IPC 形式,但不适合大量的数据传输。
- Socket 是对 TCP/IP 协议族的封装,不仅可以用于本机上的进程间通信,更多的被用于网络通信中。
10. 进程的互斥与同步
在操作系统中,进程是占有资源的最小单位,对于那种只能同时被一个进程持有的资源我们称为临界资源,对于临界资源的访问,必须是互斥的。
而进程之间访问临界资源时可以构成同步与互斥两种关系,同步即两个进程的资源访问必须是先后关系,比如经典的生产者消费者问题,读者写着问题。而互斥则是两种在进行资源抢到,比如购票问题。
通常在软件层面可以使用替换算法来实现,即每个进程持有一个标志,每次当使用资源时则将自己的标志与资源的标志互换,如果在互换的过程中发现自己获得的标志是正在使用的状态,则在此循环等待。这种方法的缺点在于每个进程都需要进行循环等待,比较低效。所以一般是通过硬件层面的信号量即PV操作来实现进程的临界资源管理。
11. 死锁的解决方法
死锁的产生是在这样一种环境中:比如我们有两个进程AB,他们都需要资源1和资源2,当进程A持有资源1,进线程B持有资源2的时候,他们都需要对方手上的进程,而一般操作系统又不允许抢占,这个时候就发生了死锁。
从这个例子中其实可以总结出死锁的几个必要条件:
- 1.一个资源只能被一个进程所占有,不能共享
- 2.一个线程请求资源失败时,它会等待而不是释放
- 3.一个线程在释放资源之前其他进程不能抢夺资源
- 4.循环等待
从死锁产生的原因未明可以设计一些方法去避免死锁的发生
- 1.静态分配资源,一开始就把一个进程所需的全部资源都分配给它,但这样会降低资源的使用效率
- 2.允许抢占,需要设置进程的不同优先级,高优先级的进程可以抢占低优先级的进程的资源
- 3.把资源进行编号,申请资源必须按照资源的编号顺序来申请
如果死锁已经发生了,就需要去解开死锁,其本质思想就是分配资源打破循环等待
1.可以运行抢占,从一个或多个进程中抢出资源来给其他进程
2.也可以终止一些进程,来达到释放资源的目的
12. 页面调度算法
- 先进先出调度算法
- 最近最少使用算法
- 最近最久未使用算法
- 时钟置换算法——为每一页设置访问位和修改位,将内存中所有页面通过连接指针接成循环队列,当页面被访问时访问位置 1,被修改则修改位置 1,每次淘汰时,从指针当前位置开始循环遍历,第一次寻找访问位和修改位都为0的页面,如果没有则将扫描过的节点访问位为 1 的置为 0,找到第一个访问位为 0 的将其淘汰。这个算法的原则就的在LRU的基础上偏向于淘汰未被修改的页面。
- 最佳置换算法——理想算法,找一个未来最长时间才会被访问的页面进行淘汰。