Linux内核与驱动面试经典“小”问题集锦(1)
笔者混迹职场这么多年来换了不少工作。头些年做的是单片机工程师,后来转入了Linux。转入Linux后,一开始做BSP和驱动工程师(当然后来应用工程师的事也没少干),近几年来专做了Linux内核与系统工程师。由打转入Linux后,经过了大大小小无数次与其相关的面试、笔试,其中绝大多数都是与Linux驱动和内核相关的。在此,我把一些反复被问到过的、印象深刻的内核与驱动方面的相对“小”一些问题的问与答写这里,以供后来者了解和学习,如果能够帮助大家在今后的面试中提高通过的概率,个人觉得这也是一件“勿以善小而不为”(原本是想写“善莫大焉”来着,但个人觉得远远不够)的事情。
所谓“小”问题,并不意味着是简单问题、比较好回答的问题,更不代表是不重要的问题。而是指方面比较小,几句话就能够说清楚的问题、无需发表长篇大论。但不要小看问题“小”,往往这些题才是面试官需要透过其而看到你的基本功和功力的,对于这些问题如果能够回答出来、哪怕能够回答对一部分,面试官对于你的印象分也是能够大大提高的。
在此特别强调一点,对于以下各个问题,希望大家能够不光知其然、更要知其所以然。也就是说别光看问题和答案,要真正掌握其背后的原理。否则面试的时候,面试官一旦深入展开,就“露陷”了。
书归正传,开始列出各个问题与答案及解析。
注:下边问与答中带有“()”标记的是对于此题的分析,而非实际要回答出来的。
问题1
问:Linux内核中的锁都有哪些?各自的使用场景是什么?有什么区别和联系?
备注:这道题在百度、蔚来以及其它几家小公司面试时被问到过。
答:
(1)Linux内核中有许多不同类型的锁,这些锁的类型包括:自旋锁(spin lock)、信号量(semaphore)、互斥锁(mutex)、读写锁(rwlock)、顺序锁(seqlock)等。(这个等包括完成量、原子操作等。)
(这里,不要求你全说出来,只要能说出自旋锁、信号量、互斥锁就可以了。当然,英文也不要求你必须说出来,但你得知道,别后续面试官一说英文,你完全“懵逼”就行。)
(2)(实际上后两问基本上是一回事。要想回答这些锁机制各自的使用场景以及区别和联系,先得知道各个锁的作用)。
- 自旋锁(spin lock)
自旋锁是一种典型的对临界资源进行互斥访问的手段,其名称来源于它的工作方式。
为了获得一个自旋锁,在某CPU上运行的代码需要先执行一个原子操作,该操作测试并设置(Test-And-Set)某个内存变量。由于它是原子操作,因此在该操作完成前,其它执行单元不可能访问这个内存变量。如果测试结果表明已经空闲,则程序获得此自旋锁并继续执行;如果测试结果表明锁仍被占用,则程序将在一个小的循环内重复这个“测试并设置”操作,即进行所谓的“自旋”(说白了就是“原地打转”)。
当自旋锁的持有者通过重置该变量释放这个自旋锁后,某个等待的“测试并设置”操作向其调用者报告锁已释放。
(当然,不要求全说出来,说出核心意思即可。下同。)
- 信号量(semaphore)
信号量是操作系统中最典型的用于同步和互斥的手段,信号量的值可以是0、1或者n。信号量与操作系统中的经典概念PV原语(PV操作)对应。
由于新版本的Linux内核倾向于直接使用mutex作为互斥手段,因此信号量用作互斥不再被推荐使用。
- 互斥锁(mutex)
尽管信号量已经可以可以实现互斥的功能,但“正宗”的mutex在Linux内核中还是真实地存在着。
互斥锁是最基本的锁类型,也是最常用的锁,在Linux内核中使用较为广泛。它是一种二元锁,只能同时有一个执行单元持有该锁。当一个线程请求该锁时,如果锁已被占用,则该执行单元会被阻塞直到锁被释放。互斥锁可以保护临界资源,使得在某个时刻只有一个任务可以访问它。
互斥锁的实现使用了原子操作,因此它的性能比较高,但也容易出现死锁情况。
- 读写锁(rwlock)
自旋锁不关心锁定的临界区究竟在进行什么操作,不管是读还是写,它都一视同仁。即便是多个执行单元同时读取临界资源也会被锁住。实际上,对共享资源并发访问时,多个执行单元同时读取也是不会有问题的,这就产生了自旋锁的衍生锁读写自旋锁,简称读写锁。
读写锁是一种比自旋锁粒度更小的锁机制,它保留了“自旋”的概念,可允许读的并发,但在写操作方面,最多只能有一个写执行单元。当然,读和写也不能同时进行。
- 顺序锁(seqlock)
顺序锁是对读写锁的一种优化。若使用顺序锁,读执行单元不会被写执行单元阻塞。也就是说,读执行单元在写执行单元对被顺序锁保护的共享资源进行写操作时,仍然可以继续读,而不必等待写执行单元完成写操作;而写执行单元也不需要等待所有的读执行单元完成读操作后才去进行操作。
但是,写执行单元与写执行单元之间仍然是互斥的,即如果有写执行单元在进行写操作,其它写执行单元必须自旋在那里,直到写执行单元释放了顺序锁。
对于顺序锁而言,尽管读写之间不相互排斥,但如果读执行单元在读操作期间,写执行单元已经完成了写操作,那么读执行单元必须重新读取数据,以确保得到的数据是完整的。因此在这种情况下,读端可能反复读多次同样的区域才能读到有效的数据。
经过上述对于各个锁的介绍,接下来可以回答问题了。这里以自旋锁和互斥锁的比较为例来进行解答。而这两个锁的异同其实才是面试官所需要的答案。
自旋锁和互斥锁都是解决互斥问题的基本手段,面对特定的情况(场景),选择的依据是临界区的性质和系统的特点。
从严格意义上来说,互斥锁和自旋锁属于不同层次的互斥手段,前者的实现依赖于后者。在互斥锁本身的实现上,为了保证其结构存取的原子性,需要自旋锁来互斥。因此,自旋锁属于更底层的手段。
互斥锁(也称互斥体)是进程级的,用于多个进程之间对资源的互斥,虽然也是在内核中,但是该内核执行路径是以进程的身份、代表进程来争夺资源的。如果竞争失败,会产生进程上下文切换,当前进程进入睡眠状态,CPU将运行其它进程。鉴于进程上下文切换的开销也很大,因此,只有当进程占用资源时间较长时,使用互斥锁才是比较好的选择。
当所要保护的临界区访问时间比较短时,用自旋锁是非常方便的,因为它可以节省上下文切换时间。但是CPU得不到自旋锁会在那里空转,直到其它执行单元解锁为止,所以要求锁不能在临界区里长时间停留,否则会降低系统效率。
由此,可以总结出自旋锁和互斥锁选用的三项原则:
1)当锁不能被获取到时,使用互斥锁的开销是进程上下文切换时间,使用自旋锁的开销是等待获取自旋锁(由临界区执行时间决定)。若临界区比较短小,宜使用自旋锁;若临界区较长,应使用互斥锁。
2)互斥锁所保护的临界区可包含可能引起阻塞的代码;而自旋锁则绝对要避免包含可能引起阻塞的代码的临界区。因为阻塞意味着要进行进程的切换,如果进程被切换出去后,另一个进程企图获取此自旋锁,死锁就会发生。
3)互斥锁存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断(不可重入)情况下使用,则在互斥锁和自旋锁之间只能选择自旋锁。当然,如果一定要使用互斥锁,则只能通过mutex_trylock()方式进行,不能获取就立即返回以避免阻塞。
参考资料:
《Linux设备驱动开发详解 —— 基于最新的Linux 4.0内核》 宋宝华 编著,机械工业出版社