写驱动的工程师,谁没被这样一句话扎过心:“你这驱动不是能跑吗?怎么我这边一上产线就重启?”
这句话我听了不下十次。每次听到,我都知道对方没说出来的后半句:这驱动在开发板上明明好好的,怎么到了实际产品上就今天崩一次、明天卡一次、后天莫名其妙看门狗复位。很多人把这归结为“运气不好”,但以我做嵌入式驱动开发这些年的经验来看,驱动能跑,说明你的功能逻辑是对的;驱动会崩,说明你对系统整体的理解还差一环。能跑是及格线,不崩才是交付线,中间差的就是量产级工程化那套东西。
这个专栏,就是专门聊这块的。我准备围绕嵌入式驱动开发,从并发、中断、资源管理、调试方法到工程纪律,把“能跑”和“会崩”之间那条鸿沟一点点填平。这一篇是开篇,不铺太多具体代码,先把最核心的认知框架讲清楚:驱动为什么会在你意想不到的地方崩,以及我们该怎么从“让功能通过”切换到“让失败可控”。
1. 为什么你的驱动在实验室里岁月静好,一上产线就开始抽风
1.1 “能跑”和“会崩”之间隔着一座冰山
我见过很多年轻的驱动工程师,判断自己代码写得好不好,标准就一条:编译通过,板子跑起来没死机。这个标准放在学习阶段没问题,但放在量产项目里,基本等同于看冰山露在水面上的那一小角。水面之下的体积,才是决定这个驱动能不能扛住真实环境的部分。
“能跑”是什么概念?是在正确的时间点,把正确的值写进正确的寄存器,然后读到预期的硬件响应。比如你初始化了I2C控制器,往某个设备寄存器里写了一个配置,读回来校验一致,功能就通了。这是驱动的基本功,也是大多数教科书和开发文档里覆盖的内容。
但“会崩”是另一码事。崩溃往往不是因为功能逻辑错了,而是因为这个驱动跟系统里的其他部分打架了。你写的驱动在跑,别的中断也在跑;你在驱动里用了全局变量,同一个变量可能在中断上下文和进程上下文中同时被操作;你申请了DMA缓冲区,但没考虑cache一致性;你注册了一个中断处理函数,但没想清楚这个中断触发时系统正处于什么状态。这些问题在实验室小规模验证时很难暴露,因为实验室环境太干净了。
一上产线,真实环境就完全不同。你面对的不再是一块安安静静放在防静电台上、电源纹波极其漂亮的开发板,而是一块塞在金属外壳里、旁边就是马达和开关电源、还有一堆线束在相互耦合的实际主板。现场环境充满了不确定性:电压浪涌、地弹、电磁干扰、相邻芯片的引脚串扰、上电时序和断电时序的不稳定。任何一个环节都可能让你的驱动走进一条从来没想过要处理的分支。
1.2 环境变了,你的驱动就必须跟着变
举一个特别常见的例子:按键检测。开发板的按键,上拉电阻是精密电阻,供电稳定,按下弹起波形干净,你去读GPIO电平,读到什么就是什么。可到了产线上,按键引脚旁边可能走了一根大电流的电机线,电机一转,引脚电平开始抖动。你的驱动如果只是简单地在中断里读一下电平,然后置一个标志位,那这个标志位就可能在半秒钟之内被错误翻转好几次。按键动作没错,但它反映到系统里,就变成了一次没有预料到的“幽灵触发”。
同样的道理也适用于通信类驱动。SPI、I2C、UART面对的不再是干净信号的时候,错误处理路径就变得极其重要。很多驱动只在“传输正常”这条路径上做了充分开发,一旦总线出现NACK、超时、CRC错误,驱动就只会卡死在那里,或者直接返回一个错误码后就再也没人管这件事。这种驱动在实验室里测一百遍都测不出问题,因为实验室里的从设备可能接的是普思仪器,线路短,干扰小。一旦到了真实产品里,错误总是会来的,区别只是早来还是晚来。等它来了,你的驱动是否准备好了退路,这就是量产级和非量产级的分水岭。
1.3 崩溃也分三六九等,别把账都算在硬件头上
驱动崩溃,从表现上大概能分成三类:
第一类是硬崩。内核panic、系统直接死机、串口打印出一大堆调用栈。这类问题其实最好查,因为崩溃现场是明确的,有栈信息,有寄存器状态,你用addr2line解析一下符号,很快就能定位到具体函数。
第二类是软崩。系统没完全死,但某个功能彻底失灵了,比如触摸屏突然没反应,网络模块疯狂报错,按键开始乱跳。这类问题隐蔽得多,因为它可能不产生任何panic信息,只是在系统日志里留下一些零零碎碎的异常记录,甚至什么都不留。很多工程师遇到这种问题,第一反应是“硬件坏了”,然后开始换板子、换物料,折腾一圈发现还是老样子。
第三类就是最折磨人的偶发崩。可能跑几十个小时都不出问题,也可能一开机就死,完全没有规律可循。这类问题九成以上跟并发访问和时序窗口有关,因为并发问题本来就不具备确定性触发条件,它取决于中断的到达时机、调度器的切换时机、DMA完成中断和CPU访问寄存器之间的微妙竞争。
我在后面的章节里会反复强调:看到偶发崩溃,先想并发;看到间歇性功能失灵,先查资源状态;看到必定复现的崩溃,再去查硬件逻辑和寄存器配置。把问题归类,排查效率至少翻一倍。
2. 驱动崩溃的根源,藏在这四个被大家默认“没问题”的地方
2.1 并发问题:你的驱动默认是裸奔的
先问一个问题:你在中断处理函数里改了一个全局变量,然后在进程上下文(比如read函数或者ioctl函数)里读这个变量,你觉得安全吗?
多数新手觉得安全,理由很直接:C语言里读一个变量不是原子操作吗?
不是。在ARM、MIPS这类主流嵌入式处理器上,对一个32位变量的读和写,如果地址没有做特殊对齐,或者编译器把它优化成了多条指令,那整个操作就不是原子的。就算你运气好,读写分别是单条指令,还有另一个问题:CPU的乱序执行和对cache的处理,可能导致读到的值并不是你想象中那个“最新”的值。更麻烦的是并发场景不仅仅是中断和进程之间的竞争,现在的嵌入式平台大量使用SMP多核,两个核同时在跑,你的驱动不知道自己在哪个核上被哪个上下文调用,这是并发失控的核心原因。
很多人写驱动的时候,脑子里根本没有“并发模型”这个概念。他以为自己的代码是顺序执行的:先申请资源,再初始化硬件,再注册中断,再提供服务,最后释放资源。但在真实内核里,你的驱动可能被多个线程同时打开,每个线程都在调用你的read函数;你可能在中断处理函数执行到一半的时候,被另一个更高优先级的中断打断;如果你用了自旋锁,又碰上了SMP平台,两个核上的代码就会同时进入同一个临界区。
所以我在代码评审里最关注的就是一件事:这个全局变量或者共享数据,到底有哪几个执行流会访问它?访问的顺序是不是确定?如果不确定,就必须用原子变量、自旋锁、互斥锁、读写锁或者禁用中断这类手段来隔离。这一步不做,你的驱动在实验室里怎么测都是好的,但一到多线程压力测试或者真实业务场景下,它就崩给你看。
2.2 中断上下文:这里不是让你随便逛街的地方
中断处理函数(ISR)是驱动工程师踩坑最多的地方。原因很简单:很多人写ISR的时候,还带着写普通C函数的习惯,觉得我在这里调一个函数、申请一段内存、打印一条日志,不是理所当然的事吗?
在Linux里还真不是理所当然的。中断上下文是一个受限上下文,它不允许睡眠,不允许调用可能导致睡眠的任何函数。你在ISR里调用kmalloc(),如果传入的是GFP_KERNEL标志,那是有可能睡眠的,这基本就是在制造一个偶发死机炸弹。正确做法是传入GFP_ATOMIC,告诉内核这是一次原子分配,不能睡。如果你在ISR里调用某个获取信号量的函数,那就更危险了,系统可能直接死锁或者调度器崩溃。
同样地,在ISR里做耗时操作也是一大忌。中断处理函数的执行期间,同级别或低级别的中断都会被屏蔽,系统响应能力急剧下降。如果在ISR里做了一次大数据量拷贝,或者做了一次慢速I2C读写,你等于把所有其他设备的中断都关了好几十微秒。这在功能测试阶段看不太出来,但放到真实产品里,其他模块就会出现超时、丢数据、响应卡顿。那种“明明每个模块单独测都没问题,放一起就互相拖累”的现象,很多就是ISR写得过于臃肿导致的。
标准的处理方式是把ISR拆成两段:上半部只做最关键、必须是原子的事,比如读取硬件状态、清中断标志、提交workqueue或者tasklet;下半部(workqueue或者tasklet)里再做那些耗时的、需要睡眠但实际上没那么紧急的活。这样既保证了中断的实时性,又给了驱动足够的空间去处理业务逻辑。
2.3 资源生命周期:申请和释放之间的那一道缝
驱动的本质是什么?是对硬件资源的封装和管理。外设需要中断线、GPIO、时钟、电源域、DMA通道、内存缓冲区,这些全是资源。你驱动写的每一步,本质上都是在问内核要资源,然后用完再还回去。
问题恰恰出在这个“要”和“还”的过程里。
最常见的是顺序不对。比如你先申请了中断,然后才去初始化硬件;结果硬件初始化到一半失败了,你直接返回错误,忘了释放之前申请的中断。下一次驱动重载时,中断号已经被占着,你再申请就申请不到了。一旦发生这种情况,这个驱动就只能卸载、加载、失败、再卸载、再失败,最后整个系统变得不可用。
更隐蔽的问题是释放的时机。你申请了DMA缓冲区,然后在驱动卸载函数里去释放它,但你是不是确认过所有正在使用这个缓冲区的请求都已经完成了?如果有一个异步请求还没结束,DMA引擎还在往这个缓冲区里写数据,你把缓冲区释放了,内核把这段内存分配给其他模块,然后DMA引擎走过来把这堆新数据覆盖了——这种bug查起来极其痛苦,因为崩溃现场根本不在你的驱动里,而是在别的模块里。
所以,量产级驱动设计资源生命周期的时候,一定要画清楚谁申请、谁使用、谁释放、释放前是否需要等待、等待的超时时间是多少。资源管理不是写几行代码的事,它是一套完整的流程。你的驱动要能保证在任何失败的路径下,资源都能回到一个干净的状态,而不是泄漏成无底洞。
2.4 硬件时序的不确定性:寄存器不是你以为的那个值
驱动工程师还有一个常见认知偏差:寄存器读到什么,硬件就是什么状态。真的不一定。
我曾经在调试一个SPI从设备的时候,遇到一个特别诡异的现象:读状态寄存器,bit0显示设备忙,但按照数据手册,这个设备初始化完成后bit0应该是空闲。我拿示波器去抓,确认SPI读写时序没有错误,后来才发现,这个从设备在上电后需要一段较长的内部初始化时间,期间状态寄存器的值是无效的,你的读操作返回的是一个随机值。数据手册上写的是“上电后等待500ms再访问状态寄存器”,我漏看了这行小字,结果就在那里瞎排查了一整天。
这类问题本质上就是硬件时序和软件预期不匹配。硬件有它自己的一套时序逻辑:上电要稳定、时钟要稳定、复位释放后要等一段时间、某些寄存器写入后要再等几个时钟周期才能生效,等等。你的驱动必须把这种时序约束固化下来,不能靠赌,更不能在压测时临时加延时。一定要在初始化流程里明确写上等待时间、检查条件以及超时后的回退策略,才叫真正的工程化处理。
3. 一个真实的崩案例:从“能跑的按键驱动”到“被打回归单”
说了这么多理论,还是讲一个我实际经历过的案例。这个故事特别适合作为开篇案例,因为按键驱动是每一个驱动工程师都写过的东西,但里面的坑几乎覆盖了上面所有方面。
3.1 案发现场:按键扫描驱动为什么偶尔死机
当时我们做的是一个嵌入式Linux网关产品,上面有一个物理按键,用来触发恢复出厂设置。功能很简单:用户长按3秒,系统格式化配置分区并重启。硬件工程师把按键接在一个GPIO上,按键按下拉低电平,触发下降沿中断,驱动负责检测按键时长。
我最初的设计是按常理来做的:
static int key_pressed; static irqreturn_t key_isr(int irq, void *dev_id) { if (gpio_get_value(KEY_GPIO) == 0) { key_pressed = 1; } else { key_pressed = 0; } return IRQ_HANDLED; } static ssize_t key_read(struct file *file, char __user *buf, size_t count, loff_t *offset) { char val = key_pressed ? '1' : '0'; return copy_to_user(buf, &val, 1) ? -EFAULT : 1; }在开发板上,这个驱动工作得非常好。按键按下去,用户态程序立刻读到1,松开读到0,没有任何异常。我当时心想,这个模块简单到不能再简单了,还能出什么问题。
结果到了整机联调阶段,问题来了。测试人员反馈:连续快速按这个按键10次左右,系统就会重启,而且不是每次都能复现,大概三分之一的概率。一开始大家都不信是驱动的问题,因为一个按键驱动能干什么呢?
3.2 整条排查链路:从现象反推嫌疑点
面对这种偶发问题,我第一条原则就是:先别猜,先复现。测试人员给出了一个相对稳定的复现条件:快速连按。那我就先照着做,果然在按到第7次左右的时候,系统真的panic了。串口打出来的调用栈指向了某个跟tasklet相关的函数,这让我很意外,因为我的驱动里根本没有用过tasklet。
接下来我把目光放到了中断上。于是启用了内核的ftrace,把中断相关的函数调用记录下来。结果发现,我的按键中断处理函数,在中断上下文里调用了gpio_get_value(),而这颗GPIO所在的控制器的寄存器访问,底层实现里包含了一次自旋锁操作和一次寄存器读。看起来没什么问题,但结合调用栈里的tasklet崩溃点,问题就清楚了。
原来这颗GPIO控制器的访问锁,和另一个中断驱动的访问锁是同一个锁。我的按键中断在运行的时候,持有了这个锁;此时另一个外设的中断触发,它的ISR也要拿同一把锁,于是它开始自旋等待。我的按键ISR呢?它在读完寄存器之后,又顺手做了一堆其他操作,耗时特别长。两边就这么互相等着,触发了死锁条件,看门狗介入,系统重启。
你得明白,这里的问题本质不是“锁”写错了,而是我在中断上下文里做了太多事情,还把中断服务设计成一次读就完事。实际上快速连按的时候,一次中断还没处理完,下一次中断已经来了,ISR被反复调用,锁的持有时间被无限拉长,整个中断系统的响应被拖垮了。
3.3 修复思路:把ISR做短,把共享变量做硬
这次崩溃之后,我重新设计了按键驱动。核心改动有两条。
第一,ISR不再负责“读取并判断”,只负责记录“发生了一次中断”这个事件,然后通过官方推荐的机制,把后续工作交给一个专用的内核线程或者工作队列去处理。
static irqreturn_t key_isr(int irq, void *dev_id) { // 屏蔽后续中断,防止抖动风暴 disable_irq_nosync(irq); schedule_work(&key_work); return IRQ_HANDLED; }第二,凡是需要在不同上下文之间传递的标记变量,全部改用原子操作或者由锁保护。这里因为只是一个按键状态,我直接用atomic_t就够了,不再用普通global变量。
static atomic_t key_release_requested = ATOMIC_INIT(0); static void key_work_handler(struct work_struct *work) { int value; // 读GPIO,做消抖判断,更新状态 ... atomic_set(&key_release_requested, 1); enable_irq(irq); }改完之后,我又做了两个补充:一是增加了软件消抖机制,按键按下后在10ms和50ms两个时间点各读一次GPIO,保证不是干扰脉冲;二是把按键中断触发模式从单纯的下降沿,改成了下降沿加超时确认,确保长按检测的准确性。这样改完,快速连按半小时,一次崩溃都没有出现,回归单关闭,这个模块才真正算交付了。
3.4 这个案例的普适意义在哪里
这个案例确实很小,但它把我想在这个专栏里讲的核心问题全都串起来了。你在一个看起来“绝对简单”的驱动里,就会碰到中断上下文限制、共享变量同步、锁的粒度、硬件消抖、并发重入这些量产级问题。按键驱动都这样,那些SPI、I2C、DMA、网络驱动,复杂度只会指数级上升。
所以我在接受新项目的时候,最怕听到的一句话就是“这个驱动很简单的”。说这话的人,往往只看到了功能路径,没看到这个功能路径在系统里会跟多少其他模块交织在一起。一旦交织,所有你以为的“简单”,都会变成偶发崩溃的温床。
4. 量产级驱动,是拿这六条防线堆出来的
聊完了案例,我想把那些真正能让驱动从“会崩”变成“不崩”的做法整理一下。我自己在带团队做评审的时候,基本上就是拿这六条防线去逐条对照代码的,缺一条就要求补齐再合入。
4.1 防线一:状态机和超时必须是显式的
一个量产级驱动,绝对不能只是一个“调用就执行、不调用就闲着”的函数集合。它必须有一套清晰的状态机:未初始化、初始化中、就绪、忙、错误、正在恢复,每种状态都有明确的迁移条件。
我特别要强调“错误状态”和“恢复路径”。很多驱动出问题,是因为它没有定义错误状态。总线通信失败后,它不知道自己该做什么,只能寄希望于下一次调用是成功的。这种做法非常危险,因为下一次调用可能还是失败,而底层硬件已经陷入了一个不确定的状态,后续所有操作都会错上加错。
正确的做法是:给每个需要等待硬件响应的操作设定超时时间,一旦超时,进入错误处理流程,尝试复位硬件或者重新初始化,并向上层返回明确的错误码和状态信息。所有状态迁移都应当有日志记录,这样系统出问题时,你才能顺着日志回溯,看它到底在哪一步走岔了路。
4.2 防线二:错误路径要像主路径一样被认真对待
我写代码习惯是“先写错误路径,再写功能路径”。也就是说,先把资源分配失败、通信超时、硬件复位失败、参数非法这些情况都处理掉,然后再写正常流程。因为正常流程谁都能写好,错误路径才真正决定一个驱动的下限。
举个例子。你在一个驱动里使用DMA传输,传输完成中断和传输错误中断是两个不同的处理函数。大多数人仔细写了完成中断,错误中断里只打印一句话就结束了。但在量产环境下,DMA错误中断常常意味着硬件状态机已经乱了,你需要在错误中断里做的不是打印,而是停止当前的DMA描述符链、复位DMAC控制器、重新初始化描述符、把传输请求重新排队。这样才能保证上层应用不需要重启系统就能恢复正常。没有这层处理,DMA一旦出错,整个子系统就只能跟着死掉。
4.3 防线三:依赖管理要变成一个清单
驱动不是孤岛,它依赖时钟、电源域、GPIO复用、中断控制器、DMA通道,甚至依赖另一个驱动的初始化顺序。量产级项目里,这些依赖关系必须变成一份显式的清单。
Linux里的设备树其实就是一个很好的载体。你在设备树里描述这个设备需要哪个时钟、哪个中断、哪个电源域,内核在probe的时候会按照依赖关系自动排序、自动使能,这样至少能从源头上避免“驱动A先初始化了,驱动B的时钟还没开导致B挂了”这种低水平问题。
但设备树不是万能的。它只描述了静态依赖,动态依赖仍然需要你在驱动里保持好顺序。比如你的外设需要先reset然后才能访问寄存器,那这个reset的时序就必须写在初始化函数里,并且要防止初始化失败后残留的状态影响下一次重试。这些内容,每一条都应该在代码评审的时候拿出来逐项确认。
4.4 防线四:日志和trace要能回溯战斗现场
做驱动调试,最怕的就是“现场被破坏”。系统崩了,串口上只有最后三行日志,其他信息全部丢失,你根本没法判断崩溃前发生了什么。所以要保证驱动从第一天起就有足够的可观测性。
我习惯的做法是:驱动里所有关键入口、所有状态迁移、所有错误路径,都必须留有可开关的日志。注意是“可开关”,平时默认关闭或者只在错误时输出,调试的时候才全量打开,避免日志风暴拖慢系统。内核的dynamic debug功能就是干这个的,大家一定要学会用它,不要再用printk写死一大堆然后逐步注释掉。
另外,ftrace和perf这类工具也是驱动工程师的标配。你排查偶发问题的时候,靠printk去逐步缩小范围是效率很低的做法,直接用ftrace记录中断发生时间和ISR耗时曲线,很快就能看到问题是不是出在中断响应延迟上。我那个按键驱动的案例,就是靠ftrace才看到了锁冲突的真相。
4.5 防线五:回归测试和老化测试必须覆盖到驱动
多数驱动工程师只做功能测试:板子起来,功能正常,就以为可以交付了。但对于要量产的驱动,功能测试远远不够。
你需要做至少三类测试。第一类是并发压力测试:多个进程同时打开设备节点、同时发ioctl、同时读写,看驱动在并发下会不会死锁或崩溃。第二类是异常注入测试:模拟总线错误、模拟硬件拔出、模拟中断风暴、模拟内存分配失败,看驱动能不能从错误路径中恢复。第三类是长时间老化测试:让设备在正常负载下跑七天以上,关注有没有内存泄漏、句柄泄漏、中断累计计数异常这类“慢病”。
这些测试听起来麻烦,但它们能帮你在大批量出货之前发现九成以上的量产级问题。与其等客户投诉后派工程师飞到现场去抓现象,不如在实验室里把这些可能性全部压榨出来。
4.6 防线六:构建、版本、内核源码三者对齐
最后一条看起来和驱动代码无关,但踩过坑的都知道它有多重要。很多驱动崩的“谜案”,最后查到根源竟然是“烧到板子上的内核版本和编译驱动的内核版本不一致”。驱动的二进制接口、内核头文件结构体定义、API签名,都会随着内核版本变化而变化。你用A版本内核编出来的驱动模块,烧到B版本内核的板子上,行为不可预期是必然的。
所以量产项目里必须严格管理:同一份驱动源码、同一个内核源码、同一套编译器版本、同一个设备树文件,四者打包锁定,发布的时候给它们打上同一个版本号。任何一条变了,都要重新走一遍完整的回归验证流程。这看起来是流程问题,但它直接决定了你的驱动在用户手里的稳定性。
5. 怎么从“会崩”走到“心里有底”:一套驱动抗疫的思维模型
5.1 从复现到定位:先缩小,再假设,再验证
驱动崩了以后,最常见的错误做法是“猜”。看代码,觉得某一行可疑,就改掉,然后再测。运气好,碰巧解决了;运气不好,改了一整天,问题照旧。
我自己的排查模型永远是三步走。第一步,把复现条件固定住。无论问题是偶发还是必现,先尽量找到触发条件:是并发量大时崩,还是特定操作序列后崩,还是高温环境下崩。复现条件越清晰,排查范围就越小。第二步,用工具和日志去缩小范围,而不是去猜代码。内核的oops栈、trace日志、设备状态寄存器、历史日志记录,这些都是线索。把崩溃现场完整保留下来,再开始分析。第三步才是针对性地阅读代码和修改假设,每一次修改都要能解释“为什么这个问题会在那一步触发”,而不是“先换一种写法看看”。
5.2 驱动工程师的悲观主义:提前假设硬件一定会出错
最后我想说一个心态上的转变。很多人写驱动,默认硬件是可靠的:寄存器写进去就是那个值,中断来了就一定是有效事件,总线传输就一定能成功。这种乐观主义在实验室里很愉快,但在量产现场就是灾难之源。
量产级驱动工程师,应当默认硬件可能出错:信号会抖,寄存器会被干扰,中断会重入,DMA会超时,从设备会不响应,电源会瞬间跌落。你的每一个逻辑分支,都要问一句:如果这里出了问题,系统该怎么办?这种悲观主义不是消极,而是用确定性的工程预案,去对冲真实世界的随机性。
我见过的最优秀的驱动工程师,不是那些能把datasheet背得滚瓜烂熟的人,而是那些能在脑海里模拟出整个系统在各种故障场景下的表现、并提前为每个故障准备好出口的人。驱动做到这个程度,才谈得上“心里有底”。
5.3 后面这个专栏,我会怎么陪你走下去
这一篇是开篇,我重点讲了“为什么能跑的驱动会崩”以及“量产级要考虑哪些维度”。后面的文章,我会一篇一篇地把这里提到的各个维度落到具体的代码和调试手法上去。计划里包括:
- 中断下半部机制的选择:tasklet、workqueue、threaded IRQ,各自适用的场景和踩坑经历。
- 并发与同步工具的对比:自旋锁、互斥锁、原子操作、RCU,在驱动里怎么选才不踩坑。
- 设备树与驱动匹配逻辑的完整实战:从零写一个设备树节点到驱动正确probe、正确release。
- DMA与Cache一致性:为什么你的 DMA 缓冲数据偶尔是脏的,如何用DMA API把它做对。
- 调试方法论专题:ftrace、kprobe、kgdb、串口日志分级,构建一套高效排查问题的工具箱。
写这个专栏,我给自己定的要求是每篇都必须有真实项目里的坑和验证过的解法。我一个人踩过的坑有限,所以也很欢迎大家在评论区分享自己的“翻车”经历,一起把这个话题做深做透。我看完每一条,都会认真去回应。