• 《Linux内核设计与实现》读书笔记 第十八章 调试


    一、内核调试概述

    1. 需要面对的

    • 一个确定的bug
    • 一个藏匿bug的内核版本
    • 相关的内核代码的知识和运气

    2. 艰难的调试工作

    • 重现bug很困难:大部分bug通常都不是行为可靠而且定义明确的。
    • 确定bug最初出现的内核版本很困难:内核版本更新快,难以确定bug从哪个版本开始出现的。

    3. 内核中的bug

    • 产生原因众多
    • 表象变化多

    从隐藏在源代码中的错误到展现在目击者面前的bug,往往都是经历一系列连锁反应的事件才可能触发。
    
    • 内核调试事实上与其它大型的软件项目没有什么太大的不同。
    • 内核有一些独特的问题需要考虑:例如定时限制和竞争条件等,都是允许多个线程在内核中同时运行产生的结果。

    二、通过打印来调试

    printk():内核的格式化打印函数
    

    1. 健壮性

    • 弹性极佳的函数:任何时候、任何地方都能调用它
      • 可以在中断上下文和进程上下文中被调用
      • 可以在任何持有锁时被调用
      • 可以在多处理器上同时被调用
    • 除非再启动过程中的初期就要在终端上输出

    2. 日志等级

    • printk()printf()在使用上最主要的区别就是前者可以指定一个日志级别。内核通过这个级别来判断是否在终端上打印消息。

    • 内核把级别比某个特定值低的所有消息显示在终端上。

    • printk的输出日志级别如下:

    等级描述
    KERN_ EMERG一个紧急情况
    KERN_ ALERT一个需要立即被注意到的错误
    KERN_ CRIT一个临界情况
    KERN_ ERR一个错误
    KERN_ WARNING一个警告
    KERN_ NOTICE一个普通的, 不过也有可能需要注意的情况
    KERN_ INFO一条非正式的消息
    KERN_ DEBUG一条调试信息--一般是冗余信息
    • 如果没有指定一个记录等级,函数会选用默认的DEFAULT_MASSAGE_LOGLEVEL

    • 内核会把这些记录等级转化,n指等级,从0-7,对应表中从上到下,数字越小越重要。

        0   KERN_EMERG  最重要
        7   KERN_DEBUG  最不重要
      
    • 调试信息, 有两种赋予记录等级的方法:

      • 保持终端的默认记录等级不变,给所有调试信息KERN_CRIT或更低的等级。
      • 给所有调试信息KERN_DEBUG等级,调整终端的默认记录等级。

    3. 记录缓冲区

    • 内核消息保存在一个LOG_BUF_LEN大小的环形队列中,读写都是按照环形队列方式操作的。

    • 大小是可以在编译时通过CONFIG_LOG_BUF_SHIFT进行调整。

    • 在单处理器的系统上默认值是16kb,即内核在同一时间只能保存16kb的内核消息,再多的话新消息就会覆盖老消息。

    • 优点:

      • 同步问题易解决
      • 记录维护容易
    • 缺点:可能会丢失消息

    4. syslogd和klogd

    这是两个用户空间的守护进程,klogd从记录缓冲区中获取内核消息,再通过syslogd守护进程将他们保存在系统日志文件中。
    
    • klogd

      • 既可以从/proc/kmsg文件中,也可以通过syslog()系统调用读取这些消息。
      • 默认是/proc方式。
      • 两种情况klogd都会阻塞,知道有新的内核消息可供读出,唤醒之后默认处理是将消息传给syslogd。
      • 启动时可以通过-c标志来改变终端的记录等级
    • syslogd

      • 将它接收到的所有消息添加到一个文件中,默认是/var/log/messages。

    三、oops

    1. 概述

    oops是内核告知用户有不幸发生的最常用的方式。
    
    • 内核很难自我修复,也不能将自己杀死,只能发布oops,过程:

        - 向终端上输出错误消息
        - 输出寄存器中保存的信息
        - 输出可供跟踪的回溯线索
      
    • 通常发送完oops之后,内核会处于一种不稳定状态。

    • oops发生的时机:

      • 中断上下文:内核无法继续,会陷入混乱,导致系统死机
      • 在idle进程或init进程(0号进程和1号进程):内核无法继续,会陷入混乱,导致系统死机
      • 在其他进程运行时,内核会杀死该进程并尝试着继续执行
    • oops发生的可能原因:

      • 内存访问越界
      • 非法的指令

    oops中包含的重要信息:寄存器上下文和回溯线索
    
    • 回溯线索:显示了导致错误发生的函数调用链。
    • 寄存器上下文信息也很有用,比如帮助冲进引发问题的现场

    2. ksymoops

    • 调用ksymoops命令:将回溯线索中的地址需要转化成有意义的符号名称
    • 并且还必须提供编译内核时产生的System.map。如果用的是模块,还需要调用一些模块信息:kysmoop saved_oops.txt

    3. kallsyms

    • 现在的版本中不需要使用kysmoops这个工具,因为可能会发生很多问题,新版本中引入了kallsyms,可以通过定义CONFIG_KALLSYMS配置选项启用。
    • 从函数的地址到符号名称的映射必须永久地驻留在内核地址上。

    四、内核调试相关工具

    1. 内核调试配置选项

    位于内核配置编辑器的内核开发菜单项中,依赖于CONFIG_DEBUG_KERNEL。
    
    • slab layer debugging slab层调试选项
    • high-memory debugging 高端内存调试选项
    • I/O mapping debugging I/O映射调试选项
    • spin-lock debugging 自旋锁调试选项
    • stack-overflow debugging 栈溢出检查选项
    • sleep-inside-spinlock checking 自旋锁内睡眠选项

    原子操作:指那些能够不分隔执行的东西;在执行时不能中断否则就是完不成的代码。
    
    • 正在使用一个自旋锁或禁止抢占的代码——原子操作
    • 使用锁时睡眠是引发死锁的元凶。
    • 内核提供了一个原子操作计数器

    2. 引发bug并打印信息

    (1)BUG()和BUG_ON()

    • 被调用时会引发oops,导致栈的回溯和错误信息的打印。
      可以把这些调用当做断言使用,想要断言某种情况不该发生:

        if (bad_thing)
            BUG();
        或
        BUG_ON(bad_thing);
      

    (2)BUILD_BUG_ON()

    • 与BUG_ON()作用相同,仅在编译时调用。

    (3)panic()

    • 可以引发更严重的错误,不但会打印错误信息,还会挂起整个系统。

    (4)dump_stack()

    • 只在终端上打印寄存器上下文和函数的跟踪线索。

    3. 神奇的系统请求键

    • 这个功能可以通过定义CONFIG_MAGIC_SYSRQ配置选项来启用。SysRq(系统请求)键在大多数键盘上都是标准键。

    • 该功能被启用时,无论内核出于什么状态,都可以通过特殊的组合键和内核进行通信。

    • 除了配置选项以外,还要通过一个sysctl用来标记该特性的开或关,启动命令如下:

        echo 1 > /proc/sys/kernel/sysrq
      
    • Sysrq的几个命令:

    • 重新启动濒临死亡的系统:

        SysRq-s:将“脏”缓冲区跟硬盘交换分区同步
        SysRq-u:卸载所有的文件系统
        SysRq-b:重启设备
      

    4. 内核调试器的传奇

    (1)gdb

    • 可以使用标准的GNU调试器对正在运行的内核进行查看。针对内核启动调试器的方法与针对进程的方法大致相同:

        gdb vmlinux /proc/kcore
      
        - vmlinx:未经压缩的内核映像,区别于zImage或bImage,它存放于源代码树的根目录上。
        - /proc/kcore:作为一个参数选项,是作为core文件来用的,通过它能够访问到内核驻留的高端内存。只有超级用户才能读取此文件的数据。
      
    • 可以使用gdb的所有命令来获取信息。例如:

      • 打印一个变量的值:p global_variable
      • 反汇编一个函数:disassemble function
      • -g参数还可以提供更多的信息。
    • 局限性:

      • 没有办法修改内核数据
      • 不能单步执行内核代码

    (2)kgdb

    • 一个补丁 ,可以让我们在远程主机上通过串口利用gdb的所有功能对内核进行调试。

    • 需要两台计算机:仪态运行带有kgdb补丁的内核,第二胎通过串行线使用gdb对第一台进行调试。

    • 通过kgdb,gdb的所有功能都能使用:

        - 读取和修改变量值
        - 设置断点
        - 设置关注变量
        - 单步执行
      

    五、探测系统

    1. 使用uid作为选择条件

    • 一般情况下,加入特性时,只要保留原有的算法而把新算法加入到其他位置上,基本就能保证安全。

    • 可以把用户id(UID)作为选择条件来实现这种功能,通过某种选择条件,安排到底执行哪种算法:

        if (current-> uid !=7777) {
            /* 老算法…… */
        } else {
            /* 新算法…… */
        }
      
        - 除了uid=7777的用户以外,其他所有的用户都是用的老算法,所以这个7777用户可以专门用来测试新算法。
      

    2. 使用条件变量

    • 如果代码与进程无关,或者希望有一个针对所有情况都能使用的机制来控制某个特性,可以使用条件变量。
    • 只需要创建一个全局变量作为一个条件选择开关:如果该变量为0,就使用某一个分支上的代码;否则,选择另外一个分支。
    • 操控方式:某种接口,或者调试器。

    3. 使用统计量

    • 这种方法常用于使用者需要掌握某个特定事件的发生规律的时候。

    • 方法是创建统计量,并提供某种机制访问其统计结果。

    • 例如:

      • 定义全局变量
      • 输出1:在/proc目录中创建一个文件
      • 输出2:新建一个系统调用
      • 输出3:通过调试器直接访问(最直接)
    • 注:不是SMP安全的,更好的方式是用原子操作。

    SMP(Symmetric Multi-Processing):对称多处理结构
    - 指在一个计算机上汇集了一组处理器(多CPU),各CPU之间共享内存子系统以及总线结构。
    - 在这种技术的支持下,一个服务器系统可以同时运行多个处理器,并共享内存和其他的主机资源。
    

    4. 重复频率限制

    • 当系统的调试信息过多的时候,有两种方式可以防止这类问题发生:

        重复频率限制
        发生次数限制
      

    (1)重复频率限制

    • 就是限制调试信息,最多几秒打印一次,可以根据自己的需要调节频率。
    • 例如printk()函数的调节频率,可以用printk_ratelimit()函数限制。

    (2)发生次数限制

    • 这种方法是要调试信息至多输出几次,超过次数限制后就不能再输出。
    • 这种方法可以用来确认在特定情况下某段代码的确被执行了。

    用到的变量都应该是静态的,并限制在函数局部范围以内。

    • 注:不是SMP安全或抢占安全的,更好的方式是用原子操作。

    六、二分查找

    确定bug引入内核源代码的时间,即首次出现某bug的内核版本号
    

    1. 二分查找法

    • 需要:

      • 一个可靠的可复制的错误
      • 一个确保没有问题的内核
      • 一个肯定有问题的内核
    • 工作:

      • 在有问题内核和良好的内核之间使用二分法,重复筛选,将问题局限在两个相继发行的版本之间。

    2. 使用git进行二分搜索

    • 告诉git要进行二分搜索:

        git bisect start
      
    • 提供已知出现问题的最早内核版本:

        git bisect bad <revision>
      
    • 若当前版本就是引发bug的最初版本,则使用:

        git bisect bad
      
    • 最新的可正常运行的内核版本:

        git bisect good <revision>
      
    • 之后,git就会利用二分搜索法在Linux源码树中,自动检测正常的版本内核和有bug的内核版本之间那个版本有隐患,然后再编译、运行以及测试正被检测的版本。

    • 如果这个版本正常:

        git bisect good
      
    • 如果这个版本运行有异常:

        git bisect bad
      
    • 对于每一个命令,git将在一个版本的基础上反复二分搜索源码树,并且返回所查的下一个内核版本。反复执行直到不能再进行二分搜索为止,最终git会打印出有问题的版本号。

    七、总结:关于内核调试

    1. 困难的内核调试

    • bug难重现、难确定,可能与特定的运行环境有关。
    • 调试有一定风险,更需要对内核代码的深入理解。

    2. 正确的方法和工具

    3. 大量的实践

    • 对于内核调试,应该去实实在在地阅读内核代码, 去修正一个个bug, 通过大量的实践才能真正理解内核。

    参考资料:《Linux内核设计与实现》(原书第三版)

  • 相关阅读:
    Http的请求协议请求行介绍
    Http概述
    服务器返回的14种常见HTTP状态码
    Tomcat发布项目
    Tomca的启动与关闭
    TomCat概述
    PrepareStatement
    JDBC的工具类
    JDBC的异常处理方式
    ResultSet
  • 原文地址:https://www.cnblogs.com/hyq20135317/p/5325598.html
Copyright © 2020-2023  润新知