Ant-Veil

Caspar Blog

Analyse Kernel Structures With Crash Utility

| Comments

关于 Kdump 和 Crash 的背景知识,不是本文的重点,可以参考下列文章:

这次碰到的问题是,系统当时死锁了,抓了个 vmcore 来研究,现在要看层层包含的某个结构体的一个 rw_semaphore 结构体变量的信息。当时这一层层的基本结构大体是这样的:

有两个链表,分别叫做: setsset_devs; 顾名思义,第一个 sets 是一堆 set 结构体组成的链表;第二个 set_devs 是一堆 set_dev 结构体组成的链表。两个结构体如下:

1
2
3
4
5
6
7
8
9
10
11
struct set {
    struct list_head *list;
    blah blah blah;
    struct list_head *set_devs;
};

struct set_dev {
    struct list_head *list;
    blah blah blah;
    struct rw_semaphore rw_sem;
};

最终任务是要找到 set_devs 这个列表里所有 set_devlock 这个信号量成员变量的信息。

首先,打开 vmcore (确保 kernel debuginfo 都在):

1
sudo crash /usr/lib/debug/lib/modules/3.10.0/vmlinux /var/crash/vmcore

如果要分析的结构体在内核模块中,还需要用 mod -s/-S 来加载内核模块的 debuginfo.

然后找到 sets 这个链表的入口地址。很幸运,模块里这个链表是全局的,直接找到 symbol 就可以用:

1
2
3
4
5
6
7
crash> p sets
sets = $1 = {
  next = 0xffffc90056696030,
  prev = 0xffffc90056696030
}
crash> p &sets // 顺手确认一下链表是否为空,如果 &sets == sets->next 就为空
$2 = (struct list_head *) 0xffffffffa04401f0 <sets>

我们就得到了一个不为空的链表的第一个元素(从上面也可以看到是唯一的一个)。由于内核里的链表只有指针没有元素内容,我们需要用类似 container_of 的机制来从成员反查链表元素的地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
crash> set -l set.list 0xffffc90056696030
struct set {
  <snip>
  list = {
    next = 0xffffffffa04401f0 <sets>,
    prev = 0xffffffffa04401f0 <sets>
  },
  <snip>
  set_devs = {
    next = 0xffffc90050f67000,
    prev = 0xffffc90056ee4000
  },    
}

这样我们又得到了一个链表的 LIST_HEAD 地址:

1
2
3
4
5
crash> list_head 0xffffc90050f67000
struct list_head {
  next = 0xffffc90065f2a000,
  prev = 0xffffc90056696da8
}

得到第一个元素地址后反查 set_dev 结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
crash> struct set_dev -l set_dev.list 0xffffc90065f2a000
struct set_dev {
  list = {
    next = 0xffffc900583dc000,
    prev = 0xffffc90050f67000
  },
  <snip>
  rw_sem = {
    count = 0,
    wait_lock = {
      raw_lock = {
        {
          head_tail = 0,
          tickets = {
            head = 0,
            tail = 0
          }
        }
      }
    },
    wait_list = {
      next = 0xffffc90065f2ab48,
      prev = 0xffffc90065f2ab48
    }
  },
}

找到 rw_sem 结构体信息,这正是我们想要的。然后依次往下找即可。btw, 我忘记链表是循环链表了,所以我傻乎乎地手工查了半个小时最后才发现自己重复了……

所以问题来了,crash 里面有快捷的方法可以挨个链表元素查找么……

Block Device Plugging

| Comments

关于 block 设备的 plug 和 unplug 机制,可以参考这篇内核文档。简单来说就是内核提供了一个 plug 机制,请求被放到一个空队列里之后,可以将队列处于 “plugged” 状态,此时队列并不真正向下层设备发射(issue)请求,而是攒够足够多的请求之后,将队列 “unplug” 之后才发射,在这期间,I/O 的调度算法将有机会对请求进行合并和排序。unplug 的时机既可以是一个手工的 blk_unplug() 操作,也可以由一个 unplug_timer 定时器超时触发。这整个过程听起来有点像在等机场大巴,到站停靠(plug),装载乘客(攒 request),到点发车(unplug_timeout)或者人满发车(blk_unplug)。

很容易想到,unplug_timer 这个定时器的启动会发生在 plug 阶段(见 block/blk-core.c 中的实现):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void blk_plug_device(struct request_queue *q)
{
        WARN_ON(!irqs_disabled());
        /*
         * don't plug a stopped queue, it must be paired with blk_start_queue()
         * which will restart the queueing
         */
        if (blk_queue_stopped(q))
                return;
        if (!queue_flag_test_and_set(QUEUE_FLAG_PLUGGED, q)) {
                mod_timer(&q->unplug_timer, jiffies + q->unplug_delay);
                trace_block_plug(q);
        }
}
EXPORT_SYMBOL(blk_plug_device);

而定时器超时函数见同一个文件下的实现:

1
2
3
4
5
6
void blk_unplug_timeout(unsigned long data)
{
        struct request_queue *q = (struct request_queue *)data;
        trace_block_unplug_timer(q);
        kblockd_schedule_work(q, &q->unplug_work);
}

这个函数是在 blk_queue_make_request 定义的时候设置的(见 block/blk-settings.c):

1
2
3
4
5
6
7
8
9
10
11
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
{
        <snip>
        q->unplug_thresh = 4;           /* hmm */
        q->unplug_delay = msecs_to_jiffies(3);  /* 3 milliseconds */
        if (q->unplug_delay == 0)
                q->unplug_delay = 1;
        q->unplug_timer.function = blk_unplug_timeout;
        q->unplug_timer.data = (unsigned long)q;
        <snip>
}

可以看到除了定义了超时函数,还定义了超时的时间,是 3ms. 这个 3ms 可以用来排查一系列奇怪的 I/O 性能问题。比如说在 iodepth 为 1 的情况下,发现 IOPS 是很固定的 333,那么多半就是因为请求队列没有及时 unplug 而导致定时器超时自动 unplug 了。通过简单计算得知,因为每个队列需要 3ms 传输,那一秒钟只能传送 333 个队列,即 IOPS == 333.

如果要主动 unplug,就需要调用 blk_unplug() 函数,或者 generic_unplug_device() 函数,从通用性来说,调用前者会比较好,看如下代码(block/blk-core.c):

1
2
3
4
5
6
7
8
9
10
void blk_unplug(struct request_queue *q)
{
        /*
         * devices don't necessarily have an ->unplug_fn defined
         */
        if (q->unplug_fn) {
                trace_block_unplug_io(q);
                q->unplug_fn(q);
        }
}

q->unplug_fn 一般不需要自行设置,如果没有定义,会使用默认的函数,即 generic_unplug_device()

看完了这几个函数,可以知道,plug 完之后及时 unplug 通常是避免 IO 延迟过高的良好手段。

Use Non-export Symbol

| Comments

最近在折腾 LIO,里面的模块好多,有核心模块(target_core_mod)、前端驱动模块(vhost-scsi, iscsi, etc)、后端驱动模块(target_core_iblock, target_core_file, etc)。我想要实现某种程度的模块“热升级”,具体来说,比如target_core_mod这个核心模块升级了之后,想要和旧模块共存,新的前端和后端驱动在接入的时候直接使用新模块。

于是想当然地认为只要把编译出来的模块改个名字就好了。然而事情并没那么简单,中间碰到两个问题,首先就是本文要讲到的,导出符号(EXPORT_SYMBOL)重复的问题。其实问题很好理解,我改了个名字之后的模块(比如:target_core_mod_new.ko,在代码中还是会导出相同的符号,所以解决方法就是在新的模块中不导出这些符号,删掉所有的EXPORT_SYMBOL()宏。

可是问题又来了,我的后端驱动模块(比如:target_core_iblock.ko)需要依赖新的模块的函数,在无法导出符号的情况下,我没有办法使用后端驱动模块。稍微搜了一下,找到kallsyms_lookup_name()函数(定义),这个函数传入一个类似 module:symbol 形式的参数,用于读取/proc/kallsyms中的符号表,找到对应的符号,然后返回符号的地址(即函数的入口)。

于是我拿了这个函数开开心心地去改代码了。以target_complete_cmd()这个函数为例,我在后端驱动模块的代码需要调用这个函数的地方:

Linux Kernel 208.5d Panic Issue

| Comments

最近线上碰到的问题,虽说早就有解决方案了,但是陆陆续续还是碰到很多没有升级内核的机器挂掉。记录一下以下内容,仅供参考。

现象是机器运行到一定天数(刚开始反馈集中在 208.5 天左右)就会主动挂掉,报告文章末尾的 CallTrace,所幸社区几年前就有了解决方案:

Upstream Patch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
commit 4cecf6d401a01d054afc1e5f605bcbfe553cb9b9
Author: Salman Qazi <sqazi@google.com>
Date:   Tue Nov 15 14:12:06 2011 -0800

    sched, x86: Avoid unnecessary overflow in sched_clock

    (Added the missing signed-off-by line)

    In hundreds of days, the __cycles_2_ns calculation in sched_clock
    has an overflow.  cyc * per_cpu(cyc2ns, cpu) exceeds 64 bits, causing
    the final value to become zero.  We can solve this without losing
    any precision.

    We can decompose TSC into quotient and remainder of division by the
    scale factor, and then use this to convert TSC into nanoseconds.

    Signed-off-by: Salman Qazi <sqazi@google.com>
    Acked-by: John Stultz <johnstul@us.ibm.com>
    Reviewed-by: Paul Turner <pjt@google.com>
    Cc: stable@kernel.org
    Signed-off-by: Peter Zijlstra <a.p.zijlstra@chello.nl>
    Link: http://lkml.kernel.org/r/20111115221121.7262.88871.stgit@dungbeetle.mtv.corp.google.com
    Signed-off-by: Ingo Molnar <mingo@elte.hu>

Blog 恢复

| Comments

Blog 已经迁移到了 GitHub,准备恢复更新,主要更新一些内核相关的内容(瞬间变得高大上了有没有~lol)。

且看看我这会儿能坚持多久吧。

对了,坚持用 FeedBurner 订阅的用户,跳出来冒个泡呗…… 估计已经没有多少了吧?

Fix Razer Orochi Re-connect Issue

| Comments

Issue

雷蛇八歧大蛇 2013 版鼠标,短时间内不用自动休眠,系统中蓝牙连接断(预期行为),鼠标从休眠恢复后系统中的蓝牙连接却无法自动重连,只能将鼠标置为配对模式,通过系统中的蓝牙工具手工连接。

Environment

1
2
3
4
5
Razer Orochi 蓝牙鼠标
Gentoo + KDE
kernel 3.10 ~ 3.16
bluez 4.101 ~ 5.23
bluedevil 1.3.2 ~ 2.0_rc1

Solution

Gentoo KDE 下折腾 OpenConnect

| Comments

最近呢,推上某奸商推出了 AnyConnect 套餐,这对广大翻墙群众来说绝对是个利好消息啊,可以自动配路由的东东。我毫不犹豫去把自己的绝版廉价套餐换成了 100 元的套餐,然后开始折腾 AnyConnect 配置。

iOS 上配置十分简单,找到 Cisco AnyConnect 这个 App 就搞定。Linux 下要把它配置得很舒服,着实花了一番功夫。

公司 VPN 用的也是 AnyConnect,我就继续用着公司的客户端(Cisco AnyConnect Secure Mobility Client), 直到今天早上我修复了笔记本上的无线网,机器有了两个 IP 为止。有了两个 IP 的 AnyConnect service 居然 segfault 了,看了一下 debug 信息,我觉得对这种闭源工具我还是别折腾了。直接换开源方案 OpenConnect.

Portage 里搜了一下,openconnect 有两个包,networkmanager-openconnect 有 libkeyring-gnome 依赖,我现在是个有洁癖的 KDE 党,果断不能装啊,于是用第二个, openconnect 命令行版…… 不行,不能这么罗里八嗦,反正最后折腾结果如下:

WizNote: 终于找到比较干净的 Qt 的笔记软件

| Comments

Linux 上记笔记,向来很纠结。以前用过一段时间 Zim,后来变成了 KDE 党,只好果断抛弃了。找到 BasKet,功能还是很强大的,可是格式一团糟。无法以 Plain Text 存储文字,拷贝来拷贝去的时候经常格式混乱。也尝试在 Web 端存东西,结果还是发现不习惯,而且离线无法访问。

然后今天工作的时候有同事提到客户报告鄙厂产品的问题,碰到了故障(当然在内核组大牛们的努力下该问题已经解决了),我就好奇去看了一下报告者信息,然后就发现了这个笔记软件:Wiz. 看起来还挺 Geek 的,有手机端有 Linux 端有 Mac 端。于是去 gentoo-zh 的 Overlay 找了一圈(是的,5 年过去了我还在 Gentoo 的不归路上慢慢走着),找到了

看起来样子还不错,还比较清爽,也可以格式化为 Plain Text。功能上比 BasKet 少一些,但是用着还是挺舒服的。

最不能忍受的问题是没有任务栏图标,于是接着搜。

找到了一年前某人的一个没有 Merge 的 Pull Request,然后稍微修改了一下补丁,打到源代码里,发现可以用了。

修改后的 ebuild 请猛戳

最后,有更靠谱的笔记软件请推荐。

MCE 的一些零散记录

| Comments

以前也测过 MCE 相关的东西,但是惭愧,一直不清不楚。这两天稍微整理了一下相关知识,感觉见识还是很浅,记些零碎的东西在这,权当博客复活第一篇。如有理解错误,欢迎指出。

CPU 检测到硬件错误时,内核会报 Machine-check,根据硬件错误是否可以修正(CE, Correctable Error; UCE/UE, Un-correctable Error),内核做出不同反应。CE 的话内核只把相关信息写到一个字符设备 /dev/mcelog 中,UE 的话是会在记录相关信息之余,还会做出不同处理,比如中断当前遇错的应用程序,或者 Panic. [TODO: 这块相关的内核代码还得再看几遍]

/dev/mcelog 中的信息可以用用户空间工具 mcelog 来读取。mcelog 比较有意思的一个参数是 --dmi,可以尝试解析 ADDR 字段以获取诸如内存出厂信息,DIMM 位置等有用的信息,可是很遗憾在实际环境中这些 DIMM 信息基本上都是错的。 (mcelog 的 man page 说了,不要怪 Linux,得怪那稀奇古怪的主板厂商……)

man page 还有一个地方提到:

The kernel prefers old messages over new. If the log buffer overflows only old ones will be kept.

Resolved: 如何更新 Git 补丁

| Comments

使用 git 制作补丁时,经常发现补丁需要修改。如果只是最后一次 commit 需要修改,那就好办,用下面的方法就可以搞定:

1
2
3
4
git reset HEAD^
# edit edit edit
git commit -a -s -c ORIG_HEAD
git format-patch --subject-prefix="PATCH v2"

但是如果是一系列补丁中的中间几个补丁需要修改,该怎么办呢?

笨办法已经被删掉>.<