恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
百度核心系统工程师笔试考点解析:操作系统、C++与分布式系统
首页
资讯中心
/
百度核心系统工程师笔试考点解析:操作系统、C++与分布式系统
百度核心系统工程师笔试考点解析:操作系统、C++与分布式系统
发布时间:2026/8/31 11:33:44
1. 拿到这批题的第一眼百度核心系统工程师到底在考什么1.1 为什么会有“第二批”这种叫法很多同学第一次看到“百度2019校招核心系统工程师笔试题第二批”这个标题时第一反应是这跟普通开发岗笔试题有什么区别为什么还要分“第二批”先说“第二批”的来历。百度校招笔试历来不是一次性统考完而是分批次进行——不同批次对应不同的投递时段、不同的事业群组比如搜索公司、AI平台、基础架构部等。2024年再回看2019年的这批题你会发现它在校招笔试题里非常有代表性既有操作系统、网络、存储这类系统基础又有C/算法这类硬功底还带着明显的“分布式系统”倾向。核心系统工程师这个岗位在百度内部通常对应的是基础设施组、存储组、中间件组这类与底层交互较多的团队所以笔试题的风格会明显偏向系统底层而不是纯业务开发。1.2 与普通后端开发岗相比这批题“重”在哪如果拿百度当年的普通后端开发岗题目做对比最直观的感受是普通开发岗会把不少分值放在数据库写SQL、框架用法、项目场景设计上而核心系统工程师岗几乎不考具体框架考的是你对于一个请求在整条链路中每一层如何工作的理解。举个例子普通岗可能问“Redis的过期策略有哪些”系统工程师岗可能问“Redis的过期键删除策略在内存碎片较多时为什么可能导致延迟抖动如何从内核内存分配层面缓解”。这批题里暴露出来的核心能力诉求可以概括成三句话看得懂操作系统行为知道进程线程、内存页表、IO调度是怎么回事。有分布式系统意识知道单机性能和多机协作之间隔着哪些问题。有扎实的编程基本功尤其是C/C在复杂场景下的工程能力。1.3 题型分布与难度曲线根据我对2019年前后百度校招笔试的观察核心系统工程师试卷大体上是这样的结构题型典型考点占比单选题操作系统概念、网络协议字段、数据结构复杂度约20%多选题边界条件辨析、多线程同步、Linux命令行为约15%填空题页表寻址计算、TCP首部、文件系统inode计算约15%简答/编程题手写算法、并发程序设计、死锁分析约30%场景设计题高并发计数器、消息队列模型、缓存系统设计约20%难度曲线并不是从简单到难线性递增而是“单选里藏着陷阱、简答题里藏着深坑”。很多基础扎实的同学在前面客观题答得不错结果栽在最后一道场景设计题上原因不是不会写代码而是不会把前面那些孤立的知识点串联成一个系统方案。下面我把这批题里最值得深挖的几个考点逐一拆开来讲每个考点都会结合我自己的答题经验和后来做面试官时看到的考生表现来说尽量还原“当时应该怎么想、怎么写、为什么这么写”。2. 进程与线程百度这类底层岗几乎必考的操作系统基础2.1 从“线程进程区别”看怎样答题才能拿全分几乎所有系统工程师笔试都会出现进程与线程的对比题2019年这批也不例外。但如果你以为把教科书上的“进程是资源分配的基本单位线程是CPU调度的基本单位”写上去就能拿分那就低估了出题人的意图。我印象里那道题的大致问法是在多线程模型下为什么进程崩溃不会直接带崩其他进程而同进程内的一个线程崩溃却可能导致整个进程退出要求从内存空间、信号处理、线程共享资源等角度回答。这道题考察的深度远超表面。它真正想看到的答案层次是第一层进程拥有独立的地址空间线程共享进程的地址空间。所以不同进程之间天然隔离一个进程的非法内存访问不会污染另一个进程。第二层线程共享同一个地址空间和全局资源一个线程访问了非法地址触发的段错误信号会发给整个进程进程默认动作就是退出所以其他线程也跟着没了。第三层更深一层是如果你想让进程在某个线程崩溃后不至于整体退出可以怎么做——用信号处理函数捕获SIGSEGV、在线程里用独立的异常边界、或者干脆用多进程模型替代多线程模型。这一层能写出来说明你真的理解信号与线程的关系而不只是背概念。这个“三层回答法”是我后来在面试别人时最希望看到的模式。笔试答题其实是限时场景下的沟通你展示的层次越多越能证明你不是背了面经而是真的思考过。2.2 线程池、协程与上下文切换的隐藏连线这批题里还有一道容易被忽略的题目表面上考线程池参数实际上考的是上下文切换成本。题目大概是有一个任务队列每个任务执行时间约200微秒系统是8核CPU你会怎么设置线程池大小为什么很多人的第一反应是“8核就设8个线程”甚至有人直接写“线程池大小 CPU核心数 1”这是因为看过某些性能调优文章。但实际上这个公式的前提是任务主要是CPU密集型且没有阻塞。当任务本身执行时间很短、切来切去频繁时线程过多会导致上下文切换本身成为瓶颈。这个问题的正确推理路径应该是任务的特点是“纯计算”还是“IO混合”。题目没有明说所以你要分情况讨论。如果是CPU密集型8核机器上设置8个左右的工作线程通常足够多设只会增加切换开销。如果任务是内联IO操作则线程数要适当放大经典公式线程数 CPU核心数 × (1 等待时间/计算时间)。任务单次执行200微秒这属于“微任务”频繁切换的代价不容忽视所以相对于无脑设大线程数更应该考虑“批量拉取任务”“无锁队列”“线程绑核”等手段。这道题后面其实还暗含协程的考点。一提到“高并发情况下线程创建太多导致栈内存和调度开销大”出题人很自然把话题引向协程。协程与线程最大的区别在于协作式调度和用户态切换但协程并不能简单替代线程——如果任务里有真正的阻塞IO协程仍然需要底层线程来承载。所以正确的理解是线程是系统资源维度协程是代码控制流维度两者可以配合不是替代关系。2.3 死锁分析与经典哲学家就餐问题2019年这批题里死锁相关题目几乎没缺席。有一道题问的是哲学家就餐问题中如果所有人先拿左边的叉子再拿右边的叉子为什么一定会死锁怎样设计可以避免这道题的“死锁结论”很多人能说出来但答得好的关键是把死锁四条件套进这个场景里互斥条件叉子同时只能被一个人拿。持有并等待每个人拿着一只叉子等另一只。不可剥夺别人不能从你手里抢走叉子。循环等待拿左边叉子的动作形成了一个环。解决方案有很多但笔试里最优答案是“资源编号排序法”也就是给每把叉子编号要求所有人先拿编号小的再拿编号大的。这样就不存在循环等待了——因为所有人都在往同一个方向取叉子。我还记得有一个考生在这个题下面写了一句“也可以只让奇数哲学家先拿左边偶数哲学家先拿右边”这在理论上是对的本质也是破坏循环等待。能写出两种不同解法并能对比优劣这类题基本就满分了。3. 内存与并发页表、Cache一致性、原子操作3.1 虚拟内存与页表寻址一道计算题背后的完整推演百度这批题里有一道很经典的计算题某64位系统页面大小4KB虚拟地址48位物理地址52位采用多级页表已知一级页表每项8字节请问需要几级页表各级页表的偏移量分别是多少位这种题在平时刷题时看起来很“套路”但其实很考验你是否真的理解页表的本质。我当时做题的时候习惯用这样的推演顺序第一步页内偏移占12位因为4KB等于2的12次方。第二步虚拟地址剩余位数是48 - 12 36位这部分全用来索引页表。第三步每级页表的索引位数为 log2(8/8) 0不对这里要注意页表项大小是8字节一个页面4KB所以每页能放 4KB / 8B 512 项也就是需要9位来索引一级页表。第四步36位分成若干9位得到 4 级页表。减去最低一级的页内偏移就是页目录索引层数。这道题的真正陷阱在于页表项大小直接决定每个页面能映射多少页如果页表项是16字节而不是8字节那么每页只能放256项需要8位索引层数可能就是5级。很多人背住了“4级页表”这个答案换个参数就懵了这就是没有理解推导过程。3.2 从“多核一致性”看面试官为什么爱问MESI我记得这批题里有一道关于多核缓存一致性的选择题问的是MESI协议中当一个核心对某个缓存行执行写操作时如果该行状态是Shared共享会先做什么操作。答案是先发送Invalidate失效请求把其他核心持有的该行副本置为Invalid然后再执行写入。这个知识点本身不难但面试官在复试里往往会把这道题延展成一个实际场景两个线程同时对全局变量做 i为什么最终结果可能小于200万即使我们使用了 volatile因为 volatile 只能保证“每次读写都从内存读取”不能保证“读-改-写这个复合操作的原子性”。两个线程同时读取 i100各自加1然后各自写回101最终结果就是101而不是102。这就是典型的“丢失更新”。从MESI协议的角度看问题出在“读取-修改-写回”之间存在窗口期核心A读取i时拿到Shared状态核心B也读取i拿到Shared状态A写时要把B置为Invalid但A整个读取、修改、写回的过程不是原子的B在这期间可能也完成了同样的操作。想解决这个问题就是引入硬件级别的原子指令比如 x86 的 LOCK 前缀或者C11的 std::atomic。很多没见过底层实现的同学只知道“用synchronized或者atomic就好”但如果能把这个“为什么必须用”讲清楚在复试环节明显更占优势。3.3 原子操作、内存序与无锁编程的入门门槛批卷过程中有一道编程题让我印象挺深请用CAS实现一个无锁的计数器支持 increment 和 get 方法要求线程安全。这道题写的答案五花八门有人用 std::atomic 一写了之有人用 spinlock 实现还有人写了一个带 ABA 问题的版本。最基本的CAS版本是这样#include atomic class AtomicCounter { private: std::atomicint64_t value{0}; public: void increment() { int64_t old value.load(); while (!value.compare_exchange_weak(old, old 1)) { // 如果比较交换失败old会被更新为当前值重试即可 } } int64_t get() const { return value.load(); } };关键要回答出两点第一为什么用 compare_exchange_weak 而不是 compare_exchange_strong因为在高并发下CAS失败是常态weak版本允许少数时候“伪失败”这正好可以通过循环重试来解决性能更好。第二为什么这里内存序用默认的 seq_cst顺序一致性就能满足要求因为计数器只要求最终一致没有更复杂的依赖关系选择更松的 relaxed 也可以但会牺牲一点可读性。到了更深的考察面试官会追问ABA问题如果线程A读到值1然后线程B把它改成2又改回1A的CAS比较时发现还是1就认为没有被人动过实际上中间发生过变化。解决方法是引入版本号或使用 tagged pointer。这道题能答到ABA问题说明你对无锁编程不是停留在调用API层面。4. 网络与并发模型从TCP到epoll全是高频题4.1 一道TCP粘包/拆包题背后的真实场景网络部分几乎是核心系统工程师笔试的必考模块2019年这批题里有两道关于TCP的题值得单独拿出来说。第一道题问的是TCP是字节流协议发送方连续发送了两次“hello”和“world”接收方可能一次性收到“helloworld”这就是粘包现象怎么解决这道题考察的其实不是TCP本身而是应用层协议设计。TCP不保证消息边界粘包与否是应用层要处理的。常见的三种方案固定长度每条消息都是等长字节接收方按长度切分。简单高效但浪费空间。分隔符每条消息末尾加特殊字符如HTTP的\r\n、Redis的\r\n遇到分隔符就认为一条消息结束。适合文本协议。长度字段包头里存消息长度接收方先读长度再读对应字节数。这是最常用的二进制协议方案。答题时的加分点是能说清楚为什么不能靠“接收方睡一会再读”来解决粘包因为网络延迟不确定应用层不应该依赖sleep来猜测对端何时发完。实际上现实中更复杂的情况是“半包”——一条消息被拆成两次到达或者两条消息合并后前一条的后半截和后一条的前半截同时到达。设计协议时不仅要定消息格式还要在接收缓冲区里维护一个“待处理字节流”的状态机。这个状态机的设计就是后面场景题“实现一个简单的协议解析器”的雏形。4.2 select、poll、epoll各自适用边界另一道网络高频题是请比较select、poll、epoll的工作原理以及它们分别适合什么场景。很多同学能背出“select有1024个fd限制”“epoll用红黑树事件驱动”这类结论但关键得分点在“为什么”。我来梳理一个便于理解的推导过程。select和poll本质上是“轮询”每次调用都把fd集合拷贝到内核内核遍历一遍看哪些fd有事件再拷贝回用户态。所以它的时间复杂度是O(n)而且每次调用的拷贝开销很大。epoll的做法是通过epoll_ctl注册新的fd内核用红黑树管理这些fd当某个fd就绪时通过回调机制把它放到就绪链表里用户态调用epoll_wait时只把就绪事件拷贝出来。这个过程不需要每次全量遍历fd时间复杂度接近O(1)。那epoll是否永远比select好笔试里要答出反例或边界。如果连接数很少比方说只有几十个epoll的复杂度和回调机制优势并不明显select的简单直接反而更合适。而且select的fd数量限制在单线程模型下也不一定是瓶颈因为你可以用多线程分别管理不同的fd集合。另外一个常被忽略的对比点是“水平触发LT”和“边缘触发ET”。epoll默认是LTET模式下必须一次性把数据读完否则之后可能不再通知。ET的优点是减少系统调用次数、效率高但对编程要求高容易漏读导致死等。百度这批题里有一道选择题就是问ET模式下应不应该用while循环读到EAGAIN——正确答案是需要因为只有循环读到底才能避免漏数据。4.3 HTTP状态码与连接管理里那些细节百度这批题的网络部分除了TCP和epoll还有一道关于HTTP的状态码题。题目问以下哪个状态码表示“请求已成功处理但没有返回任何内容”正确答案是204 No Content。这道题本身不难但值得展开的是它的应用场景比如客户端提交一个表单、服务器处理完不需要返回页面时用204可以避免不必要的流量又比如配合PUT方法做资源更新时204被广泛用作“更新成功但没有响应体”的语义。与204紧密相关的还有301、302、307、308这几个重定向状态码的区分状态码含义特点301永久重定向浏览器会缓存下次直接访问新地址302临时重定向不会缓存每次都要原地址访问307临时重定向保持请求方法不变308永久重定向保持请求方法不变这道题的陷阱在于101是协议切换比如从HTTP升级到WebSocket时使用很多人会选成“重定向”或者在204和304之间犹豫。304是“缓存未修改”返回的是304 Not Modified而不是204。两个状态码都“没有响应体”但语义完全不同204是服务器有意返回空内容304是服务器告诉客户端“可以用缓存”。对系统工程师来说理解HTTP连接管理也很重要尤其是Keep-Alive和队头阻塞的关系。很多人以为Keep-Alive只是减少连接建立的开销其实它还和浏览器并发连接数、HTTP/2的多路复用密切相关。考场上如果时间允许可以把HTTP/1.1的队头阻塞和TCP的队头阻塞区分开来讲能体现出你对网络分层模型的整体理解。5. 文件系统与存储fsync、页缓存和IO模型5.1 “把数据写进文件”这件事并没有想象中可靠有一道简答题是调用write()把数据写入文件后断电重启数据就一定在磁盘上吗如果不是怎样做才比较可靠这道题考的是Linux页缓存page cache机制。write()返回成功通常只代表数据从用户态复制到了内核态的页缓存里并不代表已经被刷到磁盘物理介质上。如果这时候断电页缓存里的数据可能丢失。可靠的方式是调用fsync()它会把文件的脏页dirty pages刷到磁盘同时更新元数据。但这里有个细节值得注意fsync是否刷metadata取决于文件系统实现和文件状态。有些情况下数据虽然刷了但文件长度信息还没更新断电后文件可能是“旧长度乱数据”。所以如果真的追求数据安全应该完整写入后立即fsync。扩展考点是单个文件fsync和整个目录fsync的区别。为新建的文件调用fsync之后如果文件所在目录的元数据比如目录项还没有落盘断电后仍然可能找不到这个文件。所以很多数据库在创建新文件后会额外对目录做一次fsync。这个细小知识点很多人不知道但在百度这样的底层岗位笔试里如果写出来非常加分。5.2 同步IO、异步IO与存储组件的落地选择文件系统相关的另一道题是让考生比较阻塞IO、非阻塞IO、IO多路复用、异步IO这几种模型的区别并且说明在成熟的存储组件里通常会怎么选。我给出一个直观的类比你去餐厅吃饭如果坐在座位上一直等服务生上菜阻塞IO等待期间什么也做不了如果你一会儿问一次“好了没”非阻塞轮询性能好一点但很浪费如果你在大厅吃着饭IO多路复用听到叫号再去拿菜事件通知如果你留了地址让餐厅做好了直接送上门异步IO这才是最理想的。放到存储系统里数据库和分布式存储最常用的是IO多路复用加小规模线程池因为异步IO比如Linux的io_uring或者早年的AIO编程复杂度高、在各文件系统上的行为差异大。但注意io_uring在2019年还没那么普及当时更多讨论的是libaio和用户态协议栈。这道题想考察的实际是你在设计一个存储中间件时能不能根据场景选择正确的IO模型。不需要把io_uring吹得天花乱坠而是要注意“模型的选择本质上是延迟、吞吐、CPU开销、编程复杂度的权衡”。5.3 一个隐藏考点文件描述符泄漏简答题里有一道观察题一个服务运行一段时间后出现“too many open files”错误可能的原因有哪些怎么排查这类题看似是运维题其实是系统工程师的基本功考点包括每个进程打开文件数受限于RLIMIT_NOFILE默认可能是1024或更大。典型原因是未关闭fd比如循环里打开文件只读了一部分就continue连接被异常中断后fd没有关闭。排查工具是lsof -p 看fd数量持续增长的方向结合strace跟踪系统调用判断是哪个函数引起的。还有一个容易忽略的坑日志文件句柄轮转logrotate之后如果没有重新打开新文件旧文件的fd会一直占着可能出现明明文件很小但fd数没降的情况。我当时做题时写的是“使用lsof输出重定向到文件再每隔几秒用wc -l统计fd数量观察哪个路径在增长”这比单纯说“用lsof”更有实操感阅卷人能看到你确实是这样排查问题的。6. 算法与C功底笔试筛人最狠的部分6.1 一道手写快排之外的边界考察编程题里有一道很基础的题“实现一个函数找出数组中出现次数超过一半的数字。要求时间复杂度O(n)空间复杂度O(1)。”很多人的解题思路是先排序再取中位数或者用哈希表计数但都不满足“空间O(1)”。正确答案是Boyer-Moore投票算法维护一个候选值和计数遍历数组时遇到相同元素计数1不同元素计数-1计数为0时更换候选值。这道题代码很短但真正的考察点在于对“存在性判断”的敏感度。投票算法最后的候选值不一定是出现次数超过一半的元素所以必须再遍历一次确认它的出现次数确实超过一半。很多人在笔试里忘记了这一步导致虽然算法路径正确但代码不完整。这一个细节决定了能拿多少分因为阅卷时这个确认步骤是重点采分点。类似的边界考察还包括数组长度是奇数还是偶数、元素可能是0吗、候选值是整个数组的第一个元素时会不会有问题。这些东西不是“钻牛角尖”而是做系统工程师必须具备的严谨性。6.2 C高频考察点从内存布局到移动语义在C部分2019年这批题几乎有一半的客观题都集中在三个方向内存布局、智能指针、移动语义。我整理一下最常见的考察方式。内存布局方面题目会问一个空类在C中占多少字节答案是1字节因为必须让该类型的对象有独立地址。而如果类里有虚函数会有一个虚表指针vptr在64位系统里占8字节。这个题本身不难但是连续问三四个“继承虚函数成员变量”的组合题时很容易错。智能指针方面核心考点是unique_ptr为什么能取代裸指针以及shared_ptr的引用计数本身是否线程安全。后者是个大坑shared_ptr的引用计数是原子操作的多线程安全但同一个shared_ptr对象被多个线程同时读写它指向的资源不一定是安全的。移动语义方面常考的是std::move到底做了什么。它本质是static_cast到右值引用让编译器可以调用移动构造函数而不是对资源做一份“深拷贝”。一个常见的笔试陷阱是std::move对const对象无效因为const T优先匹配拷贝构造而不是移动构造。6.3 场景设计题高并发计数器的完整方案这批题的最后一道我印象里是一道综合设计题请设计一个高并发环境下的计数器支持增加、减少和读取当前值要求尽可能高的性能并且允许数据在多个实例之间同步。这个题完全可以当系统设计题来答。从单机角度可以用std::atomic实现无锁计数如果需要更高吞吐可以参考CPU的per-CPU计数思路每个线程先加到本地变量定期合并到全局计数。这个思想在Linux内核里的per-CPU变量、以及很多统计组件里都能看到。从多机角度则要引入分布式计数方案一个中心节点保存计数所有实例通过RPC访问简单但中心节点是瓶颈而且要考虑重试导致的重复计数问题。用Redis的INCR/DECR命令Redis单线程模型天然适合这种简单操作但需要处理Redis故障、主从切换后的计数可靠性问题。更高级的做法是“分片计数”把计数值分成多个槽shard每个实例只更新自己的槽读的时候把所有槽汇总。这样写扩展性更好但读成本上升。答题时应选择其中一种方案作为主线同时指出它的缺陷和备选方案。这种“先确定边界再选方案再指出权衡”的答题框架是面试官最爱看到的他们希望候选人不是只背了一个答案而是真的会做系统设计。7. 复盘与备战路线如果我想拿这类Offer该怎么准备7.1 备考时间线刷题、背概念、写实现的比例我接触过不少后来进了大厂基础架构部门的学弟学妹他们的共同特点不是“刷题多”而是“视角对”。核心系统工程师笔试不像算法岗那样把LeetCode刷到300道就能稳过它需要的是操作系统、网络、C、数据结构、系统设计五条线并行。我的建议比例是这样的刷题占40%以LeetCode中等于或大于中等难度的题为主尤其是数组、链表、栈、队列、堆、二分、DFS/BFS、动态规划这些高频题型。概念梳理占30%操作系统、网络、数据库三大块以“能给别人讲明白”作为标准而不是“自己看懂了”。写实现占20%别只背理论动手写线程池、写一个事件循环、写一个简单的内存池写的过程中你会发现理论和实践之间差着一大截。系统设计占10%从Redis、Kafka、Nginx这些开源组件的设计思路入手看它们的持久化、并发模型、数据分片方案。7.2 最容易翻车的三个思维误区第一个误区是觉得操作系统原理“太理论不考”。恰恰相反百度这类公司的系统工程师岗位笔试里操作系统的分值占比可能是最高的。页表、调度、锁、内存、文件系统这些问题在笔试里一个都跑不掉而且经常以组合拳的形式出现。第二个误区是认为“会写代码就行语言特性无所谓”。对核心系统工程师来说C是基本功如果连虚函数多态、智能指针、内存对齐、堆栈布局都说不清楚几乎不可能通过笔试。这不是因为百度必须用C而是这些语言特性直接反映你对底层资源管理的理解深度。第三个误区是忽略了“题目给的高频热词”。当年这批题反复出现的关键词是进程调度、页表、缓存一致性、TCP状态、epoll、fsync、原子操作。如果你准备时能把这些热词串成一棵树——操作系统这棵树的根是进程干是内存枝是文件系统叶是网络——那你的知识体系就是成片的而不是一个个孤立的点。7.3 为什么“会做”和“懂原理”在阅卷人眼里差距很大我从自己面试候选人的体验来说笔试答题时最容易拉开差距的其实不是“答案正确率”而是“思考痕迹”。同样一道题有人直接写一个答案有人会在答案前面写两三句“为什么这么考虑、有什么边界条件、还有没有其他备选方案”。后者哪怕最终结论没那么完美也会让阅卷人觉得“这人像是做系统的”。举一个真实例子一道关于原子操作的题目候选人A直接写“用atomic_int”候选人B写的是“用std::atomic并在increment里使用compare_exchange_weak循环重试顺便解释为什么不能只用load和store保证原子性”。B的答案明显更打动人因为他展示的不止是“知道有这个工具”而是“清楚工具背后的原理和边界”。所以在做这套题的时候不妨把自己想象成一个面试官看到这个题目我最希望看到什么样的答案是模糊的结论还是清晰的推导过程是堆砌术语还是拿术语解释现象把这个问题想透了答每道题的水平都会高一个档次。最后分享一点个人体会。复盘2019年这批题最大的价值不是“背住答案”而是借这个机会把整个计算机系统的知识栈重新梳理一遍。你会发现很多平时写业务代码时用不到的东西比如页表、缓存行、fsync、epoll的LT和ET恰恰是支撑高并发、高可靠服务的基础。系统工程师这个岗位的核心竞争力也正在于此别人只看到了接口的调用你能看到接口背后整个内核与硬件的协作过程。这种“往下钻一层”的思维方式远比多背几道题更值得花时间去培养。