尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Linux内核工程师笔试题深度解析:进程调度、内存管理与并发同步

Linux内核工程师笔试题深度解析:进程调度、内存管理与并发同步 滴滴出行2018校园招聘网申笔试-Linux内核工程师第三批这份题目虽然过去了好几年但直到今天我依然觉得它是国内校招里少有的、能真正筛选出“内核底子”的试卷之一。很多同学拿着它当普通笔试题刷说实话有点浪费。这东西的价值不在于对答案而在于它把Linux内核工程师这个岗位的考察逻辑摊开在了你面前——它考的不是你背了多少命令、会不会装系统而是你对操作系统底层机制的认知深度以及遇到问题时是否有“顺着内核源码追根因”的意识。我当时拿到这套题的时候第一感受是出题人很懂行。它不是那种堆砌概念的八股文而是每一个选项、每一个场景都在逼你回答“操作系统到底是怎么工作的”。这篇文章我就基于这套题结合我自己从校招到做内核相关开发的经历拆一拆它背后的考点、踩过的坑以及如果你现在正在准备类似岗位应该怎么有针对性地准备。1. 考题背后的能力模型Linux内核工程师到底在考什么1.1 从题目反推岗位要求的知识体系很多同学刷题喜欢按“题型”来选择题就选填空题就填这是应试思维。但内核岗位的笔试题尤其是滴滴这种大厂第三批放出来的卷子其核心目的是完成一次“能力侧写”。我从这套题反推它考察的知识面基本覆盖了以下几个维度。第一个维度是进程与调度。题目里大量涉及进程状态切换、上下文切换的开销、调度器的行为逻辑。这些不是让你背“进程有五种状态”而是让你理解一个进程在CPU上到底经历了什么从用户态陷入内核态、调度器切换task_struct、切换地址空间到恢复现场每一步都对应真实的硬件操作。第二个维度是内存管理。伙伴系统、slab分配器、页面回收、虚拟地址到物理地址的映射题目会把你放在“一个malloc的指针背后发生了什么”这个场景里。很多非内核方向的开发会觉得内存管理就是new/delete、malloc/free但内核卷子上考的是页表有几级、TLB miss之后硬件和软件各干什么、为什么说高端内存是历史遗留问题。第三个维度是并发与同步。自旋锁、信号量、读写锁、RCU这些同步原语的区别不是为了让你背定义而是为了让你在真实的驱动开发场景里选对工具。考题里经常出现“中断上下文能不能睡眠”这种问题本质就是考察你是否清楚不同上下文对内核 APIs 的硬性约束。第四个维度是文件系统与IO栈。VFS层、页缓存、块设备调度、direct IO和buffer IO的区别题目常常会包装成“为什么我的程序写文件那么慢”这种实际问题。第五个维度是内核模块与设备驱动。这部分的题目一般最贴近“岗位日常工作”比如module_init的调用顺序、ioctl怎么从用户态传导到内核态、并发访问设备时怎么加锁。所以我建议大家不要把这套题当成一次性的练习题而是当成一份知识地图。每错一道题就沿着题目牵出的那条线往下走去把相关的内核源码读一遍把相关的机制彻底搞懂这道题才真正发挥了价值。1.2 笔试和面试的考察差异为什么笔试更“硬核”很多人不理解为什么笔试题目出得那么底层、那么硬核真到了面试环节反而在聊项目和经历。我的理解是笔试承担的是一个“安全网”功能它要在最短时间内筛掉那些简历写得漂亮但对底层没有真实感知的人。面试还可以通过聊项目、聊思路来“圆回来”但笔试是机器阅卷或者统一评分做不了假。一道题你会就是会不会就是不会。尤其像内核这种领域工作场景里动辄要面对crash日志、死锁、内存越界这些问题的排查没有“差不多就行”的空间必须精确到机制层面。所以内核笔试的题目通常有两种“骗人”方式。第一种是表面看起来很基础但选项里埋了陷阱比如经典的“函数调用过程中栈指针由谁恢复”不懂汇编级调用约定的人会被绕进去。第二种是表面看起来很复杂实际考的是很朴素的概念比如给你一段有并发问题的驱动代码问你怎么改其实考的就是临界区保护和锁的选择。关于这套题我的建议是不要只对答案要把每道题背后的核心机制穷尽一遍。笔试过了只是入门面试和实际工作会把你笔试里“蒙对”的部分全数暴露出来。2. 核心考点逐个拆解那些年绕不开的内核基础知识2.1 进程调度与状态迁移一道经典题的展开这套题里进程调度相关的题目占比不低而且考得很细。比如进程从TASK_RUNNING被调度走之后进入什么状态、什么条件下会发生抢占、nice值和权重的关系是怎样的。这些知识点看起来很散实际上都在指向同一个问题——调度器如何决定哪个进程用CPU。从Linux 2.6.23开始内核引入了CFS完全公平调度器它替代了之前的O(1)调度器。CFS的核心思想不是给每个进程分配时间片而是用虚拟运行时间vruntime来保证公平。每个进程的vruntime按权重加权增长调度器每次都选择vruntime最小的进程来运行。那么问题来了进程的优先级体现在哪里答案是体现在vruntime的增长速度上优先级高的进程vruntime增长得慢所以更容易被选中。很多校招生背下了“CFS选择vruntime最小的进程”但题目一换个包装就懵了比如“为什么sleep的进程醒来后往往能立即获得CPU”。这就要理解sleep的进程在睡眠期间vruntime是不增长的所以它醒来时vruntime相对其他进程小很多自然会被调度器优先选中。这就是为什么你感觉一个被唤醒的进程“插队”了其实不是插队而是CFS的公平机制在起作用。上下文切换的代价也是常考点。一次完整的上下文切换包括保存当前进程的寄存器状态、切换内核栈、切换地址空间如果是不同进程的话还要切换页表基地址、加载新进程的寄存器状态。这里要注意同进程内的线程切换不需要切换地址空间这就是为什么线程比进程更轻量的核心原因之一。还有一道经典题是关于“进程和线程在内核里的表现形式”。很多人知道“Linux里线程就是轻量级进程”但不理解为什么。因为在内核看来线程和进程都是task_struct区别只在于是否共享地址空间、文件表、信号处理等资源。clone系统调用通过不同的flag组合来控制共享哪些资源这就是pthread_create在底层干的事情。2.2 内存管理不止是malloc和free内存管理这部分题目我觉得是整张卷子里最有区分度的。它不是考你“什么是虚拟内存”而是考你一个地址从进程空间到物理页框的完整旅程。当你在用户态调用malloc分配一块内存时表面上你得到了一个指针但实际上内核可能什么都没做。malloc是libc的机制它维护用户态的堆当堆不够时会通过brk或mmap系统调用向内核申请。而内核返回的虚拟地址区域只是一段vm_area_struct真正的物理页框要等到你访问这块内存时才通过缺页异常分配。这就是所谓的“请求分页”。笔试里经常出现“缺页异常和普通异常的区别”其实就是要你区分缺页异常是CPU在访问虚拟地址时发现页表项无效触发的一种“可修复”的异常内核在缺页异常处理函数里重新分配页框并建立映射然后重新执行导致异常的指令。而普通异常比如除零通常意味着程序有bug。如果说这部分题目里有一个最容易被忽略的知识点我觉得是TLB转译后备缓冲器。TLB是CPU内部缓存页表项的硬件它决定了虚拟地址到物理地址的转换速度。上下文切换时如果新进程的地址空间和旧进程不同TLB需要被刷新这也是为什么频繁切换进程会带来性能损失。很多性能调优场景里我们尽量减少进程切换本质上就是在减少TLB失效和cache污染。伙伴系统Buddy System和slab分配器也经常被考到。伙伴系统解决的是物理页框的分配问题它把内存按2的幂次分成不同大小的块分配和释放时进行合并与分裂。slab分配器则构建在伙伴系统之上专门解决内核对象频繁创建销毁的性能问题。比如task_struct、inode这些内核对象它们的分配和释放特别频繁slab通过维护对象缓存来避免每次都走底层的物理页分配。有一个经验之谈如果笔试里问“为什么需要slab而不是直接用伙伴系统”答案的核心是两点一是伙伴系统最小分配单位是页太粗二是内核对象的创建和销毁往往有成对的构造函数和析构函数缓存起来可以复用状态减少初始化开销。2.3 并发与同步中断上下文里的生死线并发相关的题是内核笔试里最容易“翻车”的部分因为它考的不仅是知识还有“并发意识”。很多非内核方向的程序员写代码是单线程思路但内核从启动那一刻起就处于一个多执行流并发共存的复杂世界里。一个经典问题自旋锁和信号量有什么区别各用在什么场景。答案的关键在于“能不能睡眠”。自旋锁在等待锁释放时是忙等待spin它不释放CPU所以它只能在不可睡眠的上下文里使用比如中断处理程序。信号量在获取不到锁的时候会让当前进程睡眠所以它只能在可以睡眠的上下文里使用。为什么中断处理程序里不能用信号量因为中断处理程序不是进程它没有task_struct不能被调度器挂起。如果一个中断处理程序尝试获取信号量而进入睡眠这个系统就彻底卡死了因为没有人能唤醒它。这是一个非常硬性的规则笔试里只要看到“中断上下文”加上“睡眠”基本就是挖了个坑。关于RCU读-拷贝-更新的问题也经常出现。RCU的特点是读端几乎无锁写端在更新数据时先拷贝一份副本修改完后再通过一个“发布”操作让读端看到新数据最后延迟回收旧数据。它适合读多写少的场景。我当年学习RCU的时候一直在想一个问题写端发布时读端会不会看到半个新数据半个旧数据答案是不会因为发布操作rcu_assign_pointer在Linux里是一个带内存屏障的指针赋值它保证了顺序性。另一个容易被考到的点是“并发粒度”。同样的数据结构你可以在全局加一把大锁也可以把锁细化到每个元素上。粒度越大越容易实现但并发度越低粒度越小性能越好但死锁风险越大。考题里往往会给你一段代码让你指出潜在的死锁条件这时候就要做“锁顺序分析”——如果两个执行路径以相反顺序获取两把锁就可能死锁。2.4 文件系统与IO路径写文件为什么慢文件系统相关的题目对校招生来说往往是比较陌生的领域因为平时写应用代码根本接触不到VFS这一层。但内核工程师不一样文件系统几乎就是日常工作的主战场之一。题目里出现频率最高的概念是页缓存page cache。你在用户态调用write写一个文件时数据不是直接写到磁盘的而是先写入页缓存然后由内核的pdflush线程或者周期性回写机制在稍后把脏页刷到磁盘。这就是为什么write系统调用返回成功不代表数据已经落盘了。想要保证落盘需要调用fsync或者使用O_SYNC标志。还有一个经典考点buffer IO和direct IO的区别。buffer IO就是走页缓存的IO它有缓存带来的性能优势但缺点是多了一次内存拷贝。direct IO绕过页缓存直接在用户态缓冲区和磁盘之间传输适合数据库这种需要自己管理缓存的场景。题目里如果问“为什么数据库喜欢用direct IO”核心答案就是数据库自己有更智能的缓存策略内核的页缓存对它来说反而是干扰。VFS层的概念也经常考。VFS是Linux文件系统的抽象层它定义了inode、dentry、super_block、file这些通用对象让上层的系统调用不关心底层的具体文件系统类型。只要底层的文件系统实现了VFS定义的操作函数集就能被内核统一管理。这就是为什么Linux能同时挂载ext4、xfs、tmpfs这些完全不同的文件系统。IO调度部分我建议校招生至少搞清楚noop、deadline和cfq三种调度器的基本逻辑。noop就是FIFO适合SSD这种不需要磁头寻道的设备deadline会给每个请求设置过期时间避免饥饿cfq按进程分组分配IO带宽。虽然现在越来越多的场景在用blk-mq和多队列但基础的IO调度知识依然是理解整个IO栈的起点。2.5 内核模块与驱动从module_init说起驱动开发是内核工程师的日常所以笔试题里一定会有内核模块相关的部分。module_init和module_exit是每个驱动的起点这部分题目一般不会太难但会考一些细节比如module_init在模块编译进内核和编译成独立模块时有什么区别。当模块编译进内核时module_init声明的函数会被链接到内核的initcall段在内核启动时按优先级调用当模块编译成.ko文件时module_init声明的函数会在insmod时被调用。这也是为什么驱动代码里经常看到__init宏这个宏表示该函数只在初始化阶段使用初始化完成后可以释放占用的内存。ioctl是设备驱动和应用层交互的主要方式之一。用户在用户态调用ioctl时系统调用会通过VFS层找到对应设备的file_operations然后调用驱动注册的unlocked_ioctl函数。需要注意的一点是从内核2.6.36开始原来的ioctl字段被移除了全部改用unlocked_ioctl这意味着驱动开发者必须自己处理并发问题。还有一个比较隐蔽但是很常考的细节内核空间和用户空间之间的数据传输。驱动里不能直接用用户态的指针因为用户态指针可能指向一个尚未被映射的页面直接访问会导致内核崩溃。正确做法是使用copy_to_user和copy_from_user或者get_user/put_user。这两个copy函数在访问用户态地址之前会做地址合法性检查并且处理缺页。写驱动的题几乎一定会考并发控制。设备驱动面向的是硬件硬件可以被多个进程同时访问如果没有并发保护就会出现数据错乱。常见的做法是自旋锁保护硬件寄存器操作或者用原子变量和完成量来协调中断上下文和进程上下文的通信。这块知识点建议结合一个具体的驱动例子来复习单纯背概念很容易在笔试时“对不上号”。3. 笔试答题策略时间分配与踩分技巧3.1 优先处理编程和简答题再把选择题当排查工具大部分笔试时间都是紧张的尤其是这种三批放出来的试卷题量往往不小。我的建议是先做编程题和简答题再做选择题。为什么这么排序因为简答题和编程题是“写清楚就给分”的题只要你思路对就能拿到大部分分数。而选择题是“选错一分没有”的题而且很容易被干扰项绕进去纠结一道题可能花费比一道简答题更多的时间。先做能拿分的再做需要纠结的这是最朴素的得分策略。在简答题里如果遇到不会的也尽量不要空着。内核方向的简答题就算不会做出完整的代码级回答也可以描述设计思路、分析可能的原因展示你的排查思路。出了校园之后你会发现内核开发里最重要的能力其实就是“面对未知问题时的排错思路”笔试时把它写出来是让阅卷人看到你潜力的一种方式。3.2 选择题里的“陷阱信号”识别内核笔试的选择题陷阱往往藏在用词里。我总结过几个高频陷阱信号分享出来给大家参考。第一类是“一定”“必须”“全部”这种绝对化表述。内核是一个极度讲究工程权衡的领域很少有什么绝对必须的事情。如果一个选项说“必须使用自旋锁”那它大概率是错的因为锁的选择永远取决于具体场景。第二类是“中断上下文可以睡眠”这类的伪正确表述。很多选项说的机制本身没问题但放在中断上下文的场景里就是错的。内核代码里有一个函数叫might_sleep它就是用来在debug模式下检测代码路径上是否有非法睡眠的这是内核调试中非常实用的工具。第三类是混淆了“虚拟地址”和“物理地址”的概念。比如题目说“内核函数直接访问物理地址”这显然不对因为内核开启MMU后访问的也是虚拟地址只是内核空间的虚拟地址和物理地址之间保持着线性偏移关系也就是直接映射段。选择题的另一个排查技巧是“代入场景法”。遇到不确定的题不要抽象地去想“这个知识点是什么”而是想“如果我在一台真实机器上执行这段代码会发生什么”。把抽象的知识具象成一个场景很多模糊的选项就会变得清晰起来。3.3 如果只能复习三件事锁、内存、调度如果准备时间不够我建议你把押注放在锁、内存、调度这三个主题上。这是内核笔试里占比最大、也是和工作最相关的内容。锁相关的题目核心要理清楚两种上下文进程上下文和中断上下文能用的同步原语清单以及各种锁之间的性能差异。内存相关的题目核心是理解页表映射的层级、请求分页的流程、以及slab和伙伴系统的关系。调度相关的题目核心是理解CFS的vruntime机制、抢占发生的时机、以及上下文切换的开销从哪里来。把这三个主题复习透了哪怕其他部分答得不理想也有很大概率通过笔试。因为这三个主题代表了你对内核核心机制的理解水平而这是内核工程师岗位最重要的基础能力。4. 从笔试到Offer技术面试的深度扩展准备4.1 笔试暴露的问题正是面试的追问方向以我的经验笔试的错题往往会在面试中被追问。面试官手上拿到的面试反馈表里会标出笔试中暴露的薄弱项然后针对这些薄弱项进行提问。这其实是你修补短板的一个机会。比如笔试里如果错了一道关于自旋锁的题面试官大概率会在面试里问你“什么情况适合用自旋锁”然后一步步深入问“在单核CPU上还会需要自旋锁吗”“持有自旋锁期间能不能发生调度”等等。每一个问题都在测试你是否真的理解了机制本身而不是背答案。所以笔试结束之后不要急着对答案然后抛之脑后要把错题整理出来对着内核源码试图把涉及到的机制推演一遍。我做这个动作的时候会画一些简单的流程草图比如“write系统调用从用户态到磁盘经历了哪些函数”画完一张图这部分知识基本就内化了。4.2 从系统调用切入建立全局视野内核开发面试里一个高频的考察方式是“描述一个系统调用的完整路径”。比如让你描述read系统调用从用户态发起一直到磁盘数据返回用户态的整个过程。这种问题的意义在于它能一次性考察你对进程切换、VFS层、页缓存、块设备层、设备驱动、中断处理的综合理解。回答这类问题时千万不要只背流程而是要把每个环节对应到具体的内核机制上。我在准备这类问题时选择了一个案例open系统调用。当你打开一个文件时内核要做的事包括通过系统调用号找到sys_open函数、根据路径名逐级查找dentry、在inode cache中查找inode、如果路径太深还要处理路径解析的挂载点切换、创建file结构体并关联file_operations、分配文件描述符、最终返回fd。这个过程涵盖了VFS绝大多数的核心对象搞懂一个open文件系统这块的骨架就有了。4.3 讲出“为什么”而不是只讲“是什么”面试和笔试最大的区别在于面试考察的是你“现场组织逻辑”的能力而不是你“回忆知识”的能力。一个常见的误区是面试官问一个问题候选人只给一个结论不给推理过程。比如问“为什么信号量可以睡眠而自旋锁不行”如果只回答“因为信号量是睡眠锁”等于没答。正确的思路是讲出“为什么”信号量的核心是一个计数器加一个等待队列获取不到锁时进程可以把自己挂到等待队列上并设置状态为TASK_UNINTERRUPTIBLE然后调用schedule让出CPU这就是睡眠。而自旋锁的核心是一个原子变量和忙等待循环它在等待期间不放弃CPU所以只能用于不能睡眠的上下文。我建议所有准备内核面试的同学在复习每个知识点时都对自己提一个问题“这个设计背后的约束条件是什么”只要能回答出这个问题你面试时的表达就不一样因为你是在讲逻辑而不是背书。5. 踩坑实录与典型问题排查5.1 校招笔试现场的时间管理教训我参加过的内核笔试里有两次教训印象比较深。第一次是在一场笔试里我花了大把时间在一道关于内存屏障的选择题上反复纠结结果后面有一道简答题没时间写了。那道简答题其实不难考的是kmalloc和vmalloc的区别属于送分题。后来我总结了一个规矩选择题如果思考超过两分钟还没有明确答案就先标记跳过把节省下来的时间留给后面的简答题和编程题。因为选择题的分数是固定的但简答题只要写出分析思路就算不完全正确也能拿到一部分分数整体收益更高。另一次教训是在一道驱动编程题上我一上来就尝试写完整的代码结果写到一半发现关中断的部分没处理对无奈只能涂掉重写。后来我养成了一个习惯任何编程题先花三到五分钟在白纸上写出关键流程和函数框架再动手写代码绝不直接开写。5.2 内核实验环境搭建与调试初体验准备内核笔试的过程中动手做实验的作用非常大。我强烈建议在本地搭一个虚拟机或者用QEMU来编译和调试内核不要只停留在看书和刷题。最简单的实验环境是用QEMU加载一个编译好的内核镜像和一个精简的根文件系统比如BusyBox。启动之后就可以在真实的Linux内核环境里验证各种机制比如写一个内核模块注册一个proc文件并尝试读写然后观察dmesg输出。这些操作虽然基础但会让你对模块加载、文件注册、内核日志有很好的直观感知。如果你想调试内核最推荐的方式是使用KGDB或者通过QEMU的GDB stub进行远程调试。打断点、看寄存器、单步执行这些操作比任何书本描述都更能让你理解内核到底在干什么。我当时第一次通过GDB看到sys_open被执行时的函数调用栈整个人的“内核观”都变了——原来操作系统不是一个黑盒而是每一行代码都可以被审视的普通程序。5.3 常见的内核问题排查问答集把一些笔试和实际工作中最高频的内核问题整理成一个速查表方便大家对照复习。问题核心排查思路系统响应突然变慢先看CPU占用率、平均负载、IO等待时间定位是CPU密集、IO密集还是内存换页进程卡死无法杀掉检查进程状态是否为D不可中断睡眠对应IO路径上是否有锁或长时间等待内核崩溃Oops查看Oops信息里的指令指针、调用栈和寄存器结合System.map或vmlinux反汇编定位内存持续增长不释放排查是否有内存泄漏在用户态用valgrind在内核态用kmemleak驱动加载报错先看dmesg日志确认是资源冲突、依赖模块缺失还是设备树配置问题5.4 内核学习路线从卷子到源码再到实践笔试考完之后如果确定要走内核方向我建议的学习路线是从这本书的经典开始建立理论框架然后逐步过渡到源码阅读。先读操作系统概念的教材建立进程、内存、文件系统的基本认知再读讲Linux内核设计的书籍了解Linux在具体实现上做了哪些工程折中最后才是真正对着源代码一行一行地读。源码阅读的路径上我的建议是“自顶向下按需深入”。先看系统调用入口的实现了解参数怎么传递、调用怎么路由然后顺着某个具体的系统调用往下读比如read、open、mmap读得多了你自然会发现它们最终都会归结到VFS层和下层的具体子系统。这时候再把锁机制和内存管理穿插着读整体的知识体系就逐渐成形了。还有两个实际的建议。第一给自己建一个“源码笔记”不用长但要把每个关键数据结构的字段、每段关键路径上的函数调用关系记录下来这个笔记在笔试和面试前复习时是效果最好的资料。第二多逛内核邮件列表LKML和看内核开发者提交的patch这是了解内核开发节奏和思考方式的有效途径虽然作为一个校招生可能看不懂所有内容但长期耳濡目染会极大提升你的“内核实感”。6. 一些我踩过之后才明白的事最后分享几个我在内核学习过程中踩过坑之后才真正想明白的事情。第一件是关于“背题”这件事。内核知识如果不理解底层机制是背不住的。就算笔试时蒙对了面试时也会被问穿。我从一开始也是靠背题应付直到有次面试被面试官连续追问了几个“为什么”之后直接懵了才彻底转变学习方式。从那以后任何一个知识点我都要求自己能够画出一条“为什么是这样”的逻辑链这才算是真正掌握了。第二件是关于动手的重要性。单看源码和单做题效果都有限。一定要在真实的内核环境里动手验证。比如你自己改一个调度参数观察进程行为的变化或者写一个简单的字符驱动用应用层程序去访问它。这些实验带来的理解深度远远超过读十篇分析文章。第三件是“内核实感”的培养需要时间。所谓实感就是你看到一个Linux系统现象时能在脑子里自然浮现出对应的内核机制。比如看到一个进程处于D状态你能第一时间联想到它可能卡在某段不响应信号的IO代码里看到一个程序频繁切换线程导致性能低下你能想到是上下文切换的cache污染在作祟。这种实感没有捷径只能靠长时间的内核源码阅读和实战积累但它是内核工程师最核心的竞争力。滴滴这套笔试虽然已经过去多年但它的出题思路和考察深度依然是Linux内核方向校招的金标准。如果你能把这套题里的每一个知识点都彻底吃透你的内核基础就已经超过了绝大多数校招候选人。关键不在于刷了多少题而在于你是否真正理解了题目背后那个一直在运转的操作系统。
返回列表