核心系统工程师笔试复盘:从Linux到系统设计的高频考点

核心系统工程师笔试复盘:从Linux到系统设计的高频考点 做核心系统工程师的笔试最忌讳的就是抱着“刷题软件背答案”的心态上考场。尤其是百度这类大厂的校招笔试题表面上是在考知识点实际是在摸底你有没有独立排障的底子能不能在线上环境出问题时抓住关键线索。2019年第二批核心系统工程师的笔试我印象很深整体题型和现在的校招笔试题一脉相承基础选择题、Linux/OS实操题、算法编程题、系统设计场景题混着来量大、覆盖广、有些坑专门留给粗心的人。这篇文章就是把当年那批题重新梳理一遍结合我后来工作里真实的踩坑经历讲清楚每道题背后的考点和答题套路给你一份可以直接照着复习的复盘笔记。不管你是正在准备校招的应届生还是工作两年想回炉基础的在职开发这篇文章都适用。我会按试卷的模块顺序走从操作系统和Linux开始然后是算法题再聊到网络与分布式系统最后是那种让人头疼的场景设计题。每个模块都会给“题目长什么样、为什么这么出、应该怎么答”三个维度争取让你做完这套复盘之后再碰到同类题不会慌。1. 核心系统工程师笔试的考察逻辑与试卷结构1.1 笔试到底在筛什么样的人很多同学一看到“核心系统工程师”这个名字会以为这是招运维或者硬件岗。实际上大厂的核心系统工程师覆盖面很广典型的业务包括服务器内核优化、CDN系统、存储系统、负载均衡、容器编排甚至部分基础中间件的研发。这批人的共同特点是离底层近、离业务也不远线上出问题时必须能扛事。所以笔试环节的筛选逻辑非常清晰不考偏门、不考背诵而是聚焦在计算机基础是否牢靠以及能不能把基础迁移到真实场景中解决问题。举个例子面试官不会直接问你“进程和线程的区别”而是会给你一段代码问你在多线程下为什么计数器会丢失更新这背后的考点就是原子性、临界区、内存序。同理算法题不会出那种“五分钟默写快排”的入门题而是会给你一个带有约束条件的场景考察你在资源受限时有没有优化意识。笔试就是第一道门槛它的核心目标不是筛出“最聪明的人”而是筛掉“基础不牢的人”。所以你在做题时不需要追求每道题都写出完美答案但一定要让阅卷的人感受到你的思路是成体系的知道哪个模块对应哪类问题而不是想到哪儿写到哪儿。1.2 第二批试卷的整体结构2019年第二批试卷的题型结构很有代表性大致可以分为四个模块我这里按实际做题时的时间投入从高到低排序Linux与操作系统基础、算法与数据结构、网络与分布式系统基础、系统设计与场景分析。整套卷子120分钟题量相当饱满我印象中选择题35道填空和简答8道左右剩下是两道编程题和一道系统设计题。选择题部分务必控制在40分钟内完成不然后面编程题会非常紧张。选择题的考查范围基本就是操作系统、计算机网络、Linux常用命令、数据库基础和一点安全常识。填空题喜欢出“查看某个端口被哪个进程占用”或者“某个进程CPU飙升列出3个排查命令”这种实操向的问题。编程题通常是一道数据结构题加一道系统设计相关的小题比如手写一个带容量的缓存或者模拟一个任务队列。最后那道系统设计题通常是拉分题给一个场景让你设计一个能支撑一定并发量的服务架构。这里有一个很多人忽略的细节笔试题里“简答题”和“系统设计题”的差距并不在于难度而在于答题的颗粒度。简答题要求你把关键点列出比如“如何排查丢包”你列tcpdump、netstat、网卡统计就够了但系统设计题必须给出模块拆分、数据流方向、异常处理方案才能拿高分。后面我会针对这个差异单独讲。2. 操作系统与Linux基础题解2.1 进程、内存与信号经典考点操作系统部分的高频考点其实非常集中无非是进程状态转换、虚拟内存、共享内存、信号量、死锁条件、内存分配算法。但2019年这批题的难处在于它会拿很基础的概念套进真实场景里让你判断结果。我当时遇到一道题给了一段典型的C代码父进程创建一个子进程子进程修改了自己内存空间里的一个全局变量问父进程里这个变量的值是否会变化。很多人一看就觉得是同一个进程空间直接答会变。这就是理解不到位了。考点在于fork之后的写时复制机制。子进程刚被创建时父子进程共享同一份物理内存页只要有一方尝试写入内核就会触发缺页中断把那个页复制一份再映射。所以子进程改全局变量只会改到自己复制出来的那一页上父进程持有的仍然是原始页面两边互不影响。这类题看起来考的是内存实际上考的是内核的分页机制和缺页处理流程。再举一个信号相关的案例。有一道简答题问“有哪些信号会让进程退出哪些可以被捕获”。这个知道的人不少但经常有人漏掉SIGKILL和SIGSTOP这两个不可捕获的信号。后面还追问了一句“如果业务进程被误kill除了重启还有没有更好的保护手段”这个问题就要引到systemd的Restarton-failure配置或者守护进程自动拉起机制上。如果只答“重启”说明你还没有在生产环境待过。顺带补充一个我后来工作里经常出现的排查场景某个服务进程内存不断上涨但业务量并没有明显增长。你首先要做的不是去猜泄漏代码而是用cat /proc/{pid}/status看VMRSS和VmSize的差值再通过pmap -x {pid}定位哪一段虚拟内存区域异常通常问题出在goroutine或者线程栈没有释放、日志buffer堆积这两类原因上。类似的排查路径如果写在简答题里会是比较亮眼的加分项。2.2 Linux命令与IO模型高频细节Linux命令在笔试里的考查从来不是死记硬背而是给一个现象让你用命令去定位。我记得卷子里有一道填空题进程状态为D无法用kill杀掉怎么确认是哪个操作导致的。这里需要写出ps -eo pid,stat,wchan或者cat /proc/{pid}/stack。D状态是uninterruptible sleep通常是内核在做磁盘IO比如NFS卡住或者磁盘硬件故障所以进程挂在等待IO上普通kill信号无法打断。如果不知道D状态这题基本拿不到分。IO模型也是高频部分。选择题里考了sync、fsync、fdatasync三者的区别。简单说sync是调度所有脏页写回但不等写回完成就返回fsync是等指定文件的所有数据块写盘完成才返回fdatasync只刷文件数据不刷元数据除非元数据影响数据读取。生产环境里MySQL刷binlog和Linux文件系统刷盘都用类似fsync这种强一致性的手段因为宁可性能损耗也不能忍受掉电丢数据。另一个容易踩坑的是free命令的输出。面试官常问“buffer和cache的区别是什么”大部分人能说出buffer用于块设备写缓存、cache用于文件页缓存但2019年这道题加了限定条件问“为什么free命令里cache那一栏特别大是不是内存不够了”。这是典型的生产场景题cache大反而是好事说明空闲内存被拿来缓存文件页了系统会在内存压力增大时自动回收这部分缓存所以不用太紧张。如果看到cache持续增长且无法回收才要怀疑内存碎片问题或者页缓存脏页比例过高。这套题里还出现了nohup和的区别表面上问的是后台运行方式实际上延伸到了进程和终端的会话关系。只是让进程在子shell里后台运行但它仍然和当前终端绑定终端关闭时进程会收到HUP信号nohup是让进程忽略HUP信号但如果没有它还是会占用当前终端。两者配合使用才是真正的“关闭终端后继续运行”。3. 数据结构与算法编程题3.1 最小栈不只是栈更是状态同步算法题我印象最深的是两道。第一道是“设计一个支持push、pop、top操作并且能在O(1)时间内获取最小元素的栈”。这道题在很多资料里都出现过但因为笔试题环境里没有IDE提示靠记事本手写代码很容易在两个细节上翻车。第一个细节是辅助栈的同步策略第二个细节是重复元素怎么处理。最简单且不容易出错的方案是双栈法主栈正常存数据辅助栈的栈顶永远保存当前主栈中的最小值。每次push时先判断辅助栈是否为空如果为空或者新元素小于等于辅助栈栈顶就往辅助栈里也push一个pop时如果主栈弹出的元素等于辅助栈栈顶辅助栈也弹出一个。特别注意条件里要用“小于等于”不要用“小于”否则连续压入多个相同最小值时pop一次后辅助栈顶就会丢失最小值。也有一种更省空间的解法是只用单个栈栈里保存元素值和当前最小值的差值。比如压入元素x时如果x比当前最小值min小就把x-min压栈然后更新min x出栈时如果栈顶元素小于0说明当前栈顶对应的真实值就是新的min需要回退。这种解法对数学推导要求高笔试时间紧张时不推荐因为边界处理容易出错。我当年写的就是双栈法代码干净又容易验证是笔试环境下的稳妥解。class MinStack: def __init__(self): self.stack [] self.min_stack [] def push(self, val: int) - None: self.stack.append(val) if not self.min_stack or val self.min_stack[-1]: self.min_stack.append(val) def pop(self) - None: if self.stack.pop() self.min_stack[-1]: self.min_stack.pop() def top(self) - int: return self.stack[-1] def getMin(self) - int: return self.min_stack[-1]写完代码后我建议在最后补一句复杂度说明时间O(1)每次操作空间最坏O(n)。很多阅卷人不会只盯着代码对错他们更在意你有没有分析和优化的意识。一句话就能体现出你和只会背题的人的区别。3.2 手写LRU Cache核心系统出镜率最高的题另一道编程题是LRU缓存题目要求“设计一个LRU缓存支持get和put容量有限get和put的平均时间复杂度要到O(1)”。这道题在核心系统岗位笔试里出现频率极高因为它跟缓存中间件、本地缓存、操作系统页面置换都挂钩。如果能顺手写出“为什么数组实现LRU不行”的原因直接加分。实现上O(1)的数据结构组合就是哈希表加双向链表。哈希表负责通过key快速定位节点双向链表负责维护访问顺序。每次get把命中的节点移到链表头部每次put如果key已存在就更新值并移到头部如果不存在则插入头部若容量满就把链表尾部的节点淘汰掉。需要注意的坑是链表的边界插入头部时要把head的后继节点处理好删除尾部时要把tail的前驱节点处理好否则很容易出现空指针。下面给一个C的简化版本方便理解双向链表的操作逻辑。笔试现场时间紧可以不把模板化做得太漂亮但节点结构体和两个核心函数的逻辑一定要完整。#include list #include unordered_map using namespace std; class LRUCache { public: LRUCache(int capacity) : cap(capacity) {} int get(int key) { auto it pos.find(key); if (it pos.end()) return -1; cache.splice(cache.begin(), cache, it-second); return it-second-second; } void put(int key, int value) { auto it pos.find(key); if (it ! pos.end()) { it-second-second value; cache.splice(cache.begin(), cache, it-second); return; } if (cache.size() cap) { int oldKey cache.back().first; cache.pop_back(); pos.erase(oldKey); } cache.emplace_front(key, value); pos[key] cache.begin(); } private: int cap; listpairint, int cache; unordered_mapint, listpairint, int::iterator pos; };这道题做完之后不要直接交卷可以顺手展开一句如果把淘汰策略从LRU改成LFU就需要再加一个频率计数桶复杂度会上升到O(1)但实现复杂度大很多。这种扩展写在题目旁边的注释里阅卷人能看到你的知识面。3.3 复杂度推导和边界条件不能丢算法题除了写代码还有一部分是纯计算题比如给一段递归代码让算时间复杂度。这套卷子里有一道比较典型的递归T(n) 2T(n/2) O(n)大部分人能看出是归并排序的复杂度O(nlogn)但题目给它加了一个场景问如果每次递归都多一层“取中位数”的开销复杂度会不会变化。这就是在考察master theorem的掌握程度。对于T(n) 2T(n/2) O(n)用主定理Case 2a 2b 2log_2(2) 1所以复杂度是O(nlogn)加一个O(n)取中位数的开销主定理第三项仍然是O(n)不改变最终结果。这道题的陷阱在于有人会忽略常数开销和大O表示法的定义误以为复杂度变成了O(nlog^2n)。边界条件的处理同样重要。笔试题里有一道二分查找的变种要求在旋转有序数组里找目标值这题很多刷过LeetCode的人都知道怎么解但手写时容易不写low high的退出条件或者在判断左半段有序时对等号处理不当。比如数组里有重复元素时nums[mid] nums[low]的情况不能简单判断哪边有序只能把low 1缩小范围。如果不提这个特殊情况代码只能跑通最常规的用例阅卷人一眼就看出来你只是在背模板。4. 网络与分布式系统基础题4.1 TCP握手与状态流转的送分题和送命题网络基础题里TCP三次握手和四次挥手几乎是必考但2019年这套题的送命题是后面跟着的TIME_WAIT连环问主动关闭连接的那端为什么要保持TIME_WAIT 2MSL如果服务端出现大量TIME_WAIT该怎么办这两个问题如果没有线上经验确实容易答偏。第一个问题的正解是两点一是为了保证最后一次ACK报文能到达对端如果对端没收到ACK会重发FIN主动关闭端还能在TIME_WAIT期间响应二是为了让旧连接产生的延迟数据包在网络中过期消失避免它们干扰相同四元组的新连接。所以TIME_WAIT不是bug是TCP可靠性的保护机制。第二个问题更贴近实际。一个高并发短连接服务端可能出现几万个TIME_WAIT连接占用大量端口和内存。常见的优化手段是开启net.ipv4.tcp_tw_reuse仅用于客户端和net.ipv4.tcp_timestamps让内核复用处于TIME_WAIT状态的连接或者调整端口范围、开启tcp_max_tw_buckets限制数量。但这里要注意tcp_tw_recycle在NAT环境下会引发严重问题不建议开很多老博客教人无脑开这个参数实际是坑。笔试时如果能把“为什么recycle可能造成NAT用户连接失败”讲出来这一题基本就稳了。另外试卷里还问到了SYN Flood的基础防护思路。从TCP握手看服务端收到SYN后分配连接控制块并进入SYN_RECV状态如果攻击者不发ACK服务端资源就会被耗尽。应对方案无非是SYN Cookie不分配资源通过Cookie校验ACK的合法性和调整syn_backlog、限制单IP连接频率。这题本身不难但需要你脑子里形成“握手过程与半连接队列”的映射不能只背状态转换图。4.2 HTTP/HTTPS与负载均衡的常见场景核心系统岗位不会只考TCPHTTP也算基础。题目里问到了HTTPS握手过程中证书验证和密钥协商的顺序。这个熟悉网络的同学都能写出来客户端先发ClientHello服务端回ServerHello和证书客户端验证证书链和有效期后生成预主密钥用服务端公钥加密发给服务端双方再通过预主密钥生成会话密钥。笔试时容易漏掉“前向保密”的概念如果提到ECDHE密钥交换算法下的临时密钥阅卷人会认为你理解得比较深。负载均衡部分有道简答题Nginx做四层负载均衡和七层负载均衡有什么区别各自适合什么场景。四层工作在传输层按IP和端口转发性能高但无法感知HTTP请求内容七层工作在应用层可以按URL、Header、Cookie做路由适合微服务网关、灰度发布、限流之类的场景。笔试现场如果只是把定义抄一遍得分有限最好补一句“生产环境通常是四层入口加七层内部的组合架构”既体现理解又落到实际。这道题后面还带了一个小问如果后端某台机器出现慢请求负载均衡策略该怎么调整。核心考点是“连接超时、读取超时、重试策略”这三板斧以及健康检查的主动探测和被动熔断区别。我在面试别人时经常看到有人答“直接下线这台机器”这是结果不是方案面试官想听到的是通过活跃连接数、错误率、P99延迟来动态调整权重这才是系统工程师的思维。4.3 一致性哈希与分布式基础概念分布式部分的题量不大但比较拉分。有一道选择题考的是哈希取模和一致性哈希的对比问“如果后端节点从5台扩容到6台迁移比例大概是多少”。这道题需要算出哈希取模有大量key会重新映射几乎需要迁移80%以上的缓存一致性哈希则只有部分key会迁移大概1/6左右。这就是为什么缓存集群用一致性哈希而不是简单的取模取余。一致性哈希本身也有热点问题所以在工业界的标准做法是引入虚拟节点。每个物理节点创建几十上百个虚拟节点分布在哈希环上即使实际节点数量很少也能让数据分布变得均匀。笔试题里如果只答“一致性哈希把节点映射到环上”是不够的必须补充虚拟节点的作用否则节点少时依然会有严重的倾斜问题。这类题目虽然占比不大但能刷掉一批只会背概念的人。我建议复习时不要只看一致性哈希的原理最好把“如何自己模拟一致性哈希环”的代码思路过一遍包括哈希函数的选择、虚拟节点的生成规则、查找key时顺时针找节点的实现。笔试如果正好出成编程题你就能直接套上这个框架。5. 系统设计与场景分析题5.1 设计一个支撑高并发的短链接服务第二批次最后一道系统设计题给了一个非常经典但很适合做文章的场景设计一个短链接服务要求能够支撑亿级日访问量并且短链接不能轻易被猜解。看到这道题不要一上来就聊数据库表结构一定要先拆解“短链接系统到底有哪些核心链路”分为生成短码和跳转两个方向。生成短码的常规思路有两个方向。一个是发号器模式用数据库自增id或者分布式ID生成器如Snowflake然后通过进制转换62进制得到短码。优点是短码唯一且可逆但容易被顺序枚举所以还要配合随机数或者混淆算法。另一个是完全随机生成短码然后查重优点是难以枚举但需要做唯一性保障并发高时冲突率会上升。笔试如果时间够建议两种方案都写然后说明取舍。短链接跳转链路的高并发设计才是拉分点。写清楚浏览器请求短链后反向代理层根据短码做哈希路由命中本地缓存或者Redis缓存里存真实长链接然后返回302重定向。如果缓存未命中才回源查询数据库同时回填缓存。这里要提一下布隆过滤器的使用用来快速过滤“完全不存在的短码”避免无效请求直接打到数据库。最后还要说清楚URL过期策略和恶意请求的封禁方案。这个题最大的考点不是技术有多新而是你在做设计时有没有围绕“缓存、降级、限流”这三个关键词。如果只写一个服务加数据库哪怕表设计再漂亮得分也有限。要体现出系统工程师的视角比如读写比例大约100:1需要考虑缓存预热和缓存穿透短码生成是写场景而跳转是读场景读场景可以水平扩容写场景需要考虑单点。把这些都想清楚了这道题才是真正答透了。5.2 线上故障排查场景题场景分析题里还有一类更接地气的直接抛一个线上故障现象让写出排查步骤。印象比较深的一道是某服务CPU使用率从20%突然涨到90%以上请求耗时明显上升如何定位问题。这题表面上是考命令实际上考的是分析路径是否清晰。我的答题思路是三步走。第一步先通过top -H -p {pid}找出消耗CPU最高的线程ID再用printf %x\n {tid}转成十六进制第二步用jstack {pid}Java环境或者gdbattachC/C环境把堆栈中的线程号跟前面算出来的十六进制线程ID对应上定位到具体代码行第三步结合perf top或者pstack看内核态还是用户态的消耗如果是内核态需要再查系统调用和上下文切换情况比如vmstat 1看cs列。这套组合拳在笔试里写出来哪怕没有实际环境阅卷人也会认为你有线上处理经验。这个题考完还追问了一句如果定位到代码里出现了死循环但这是核心链路不能直接回滚你会怎么临时缓解。参考答案是先把CPU占用高的线程通过kill -STOP {pid}暂停一段时间然后评估流量在负载均衡层摘掉一部分流量再根据具体情况决定是发补丁还是重启。当然这只是临时方案但能透过这道题看出你有没有灰度发布、快速回滚的全局意识。我在做这类题时有一个原则永远不要只给“根本原因”一定要同时给出“临时缓解”和“长期根治”两个层面的方案。大厂笔试特别注重的就是这种系统性的处置思维因为真正的核心系统故障第一时间不是讨论为什么要挂而是怎么先止损。6. 常见失误与备考心得6.1 笔试中最容易丢分的几个坑这套卷子做完我最想吐槽的是几类特别可惜的丢分情况。第一类是不审题比如题目要求“写出至少两种方案并对比”有人只写一种或者写了方案没做对比直接丢掉一半分。第二类是代码只写了核心逻辑但没写输入输出和边界条件阅卷人没法判断你是不是真的能跑通。第三类是概念题答案太短只写名词解释不写场景和背景比如问“为什么需要一致性哈希”光说“减少迁移”和“为了负载均衡”是拿不到高分的得把旧方案的不足、新方案的淘汰时机都说出来。还有一个被很多人忽略的隐形丢分点卷面逻辑混乱。笔试系统里通常是文本框代码和文字混在一起如果你不主动加编号、分段、标注关键结论阅卷人很可能没耐心细看你的过程。像简答题里我建议用“一、二、三”或“1. 2. 3.”的分点结构每个点先用一句话下结论再用一两句话展开。这样阅卷速度快也显得你有条理。最后提醒一下不要把笔试题里的简答题当成小学问答题写个两行就交差。核心系统岗位的简答题本质上是让你跟面试官“笔试里的对话”你的每一个判断都应该体现“我知道原理也知道实际操作怎么做”。所以哪怕题目只是问“如何查看端口占用”最好也补一句“如果发现端口被占用怎么确认是哪个进程、是否需要处理”。6.2 我自己的复习顺序和刷题建议既然你已经准备投核心系统工程师岗我的建议是不要只刷笔试真题而是按“操作系统 - Linux - 网络 - 数据结构 - 分布式理论 - 系统设计”这个顺序复习。操作系统和Linux是基础中的基础这部分不扎实后面所有场景题都会飘。数据结构和算法每天保持一两道手写题优先高频题比如LRU、最小栈、TopK、两个线程交替打印、死锁的模拟实现。系统设计不需要刷很多每周精练一道经典场景题比如短链接、秒杀系统、分布式限流核心是熟悉答题节奏。备考时间如果只有两周我的安排是前五天死磕操作系统和Linux命令每天两小时左右把《Linux内核设计与实现》和《深入理解计算机系统》里关于进程、内存、文件系统的章节快速过一遍动手敲ps、top、vmstat、strace、perf这些命令。中间五天刷算法和网络算法以LeetCode的Top100为基础网络以TCP状态迁移和HTTP协议为主。最后三天集中做系统设计题和整套模拟卷练习题感。还有一个很管用的笨办法就是把每道错题都当成一个“线上故障”来对待。比如你不会“僵尸进程产生原理”那就把它从头到尾模拟一遍写个C程序fork子进程但不wait让子进程变成僵尸再用ps -ef观察状态最后用kill或者改造父进程来处理。这套实验做完你对僵尸进程的理解会比背十遍书深刻得多。我个人这几年带校招生发现笔试能考高分的人往往是那种平时真的会在自己的电脑上折腾环境、编译内核模块、写脚本做监控的人。知识本身是死的但你把知识放到场景里活学活用这个能力才是校招笔试真正要筛的东西。我建议你从今天开始每学一个概念就问自己一句如果线上环境遇到这个问题我第一个命令是什么第二个命令是什么什么时候能下结论。养成这个习惯笔试和面试都会顺很多。