Linux Kernel

perfbook 读书笔记:livelock

注:本文以及后面的文章中提到的 perfbook 均是指大牛 Paul E. McKenney 的书,《Is Parallel Programming Hard, And, If So, What Can You Do About It?》,书名太长,故简写之。

最近一直在读这本 perfbook,它算是少有的全面而专门介绍 locking 的书,值得每个系统程序员去读。在读的过程中发现有的地方它介绍的并不是很详细,所以就写几篇读书笔记也算是对它的小小补充。

书中 6.1.2 节讲到了 livelock,即所谓的“活锁”,相对于死锁(deadlock)而言。因为上过操作系统课的原因,我估计很多人对 deadlock 并不陌生,但是 livelock 相对不那么熟悉。livelock 无非是饥饿(starvation)的极端情况。饥饿是由竞争引起,所谓竞争(contention)是指尝试获取一个已经被锁住的锁,长时间竞争会导致这些得不到锁的进程饥饿,所以饥饿的极端情况就是多个进程一直在尝试获取某一(几)把锁,但一直都得不到满足。

书中给出的例子很简单,解决方法是给两个进程加上不同延迟,也同时增加了 overhead,所以这个例子并不是很好。我们看一个现实中的例子,Linux 内核中的一个 livelock 的 bug:

commit eaf5f9073533cde21c7121c136f1c3f072d9cf59
Author: Miklos Szeredi
Date: Tue Jan 10 18:22:25 2012 +0100

fix shrink_dcache_parent() livelock

发生 livelock 的地方是 shrink_dcache_parent(),看其定义:
[c]
void shrink_dcache_parent(struct dentry * parent)
{
LIST_HEAD(dispose);
int found;

    while ((found = select_parent(parent, &dispose)) != 0)
            shrink_dentry_list(&dispose);

}
[/c]

根据这个 commit 的描述,我们可以发现存在下面这种情况:

1. CPU0 上的进程调用 select_parent(P) 找到了 dentry C,并把它放入 dispose 链表中,返回1;

2. 与此同时,CPU1 上的另一个进程也调用了select_parent(P),获取了 dentry P 的 ->d_lock;

3. CPU0 上的进程继续调用 shrink_dentry_list(),获取了 dentry C 的 ->d_lock,然后又调用 try_prune_one_dentry(C),dentry_kill(C):

[c]

if (inode && !spin_trylock(&inode->i_lock)) {
relock:
spin_unlock(&dentry->d_lock);
cpu_relax();
return dentry; / try again with same dentry /
}
if (IS_ROOT(dentry))
parent = NULL;
else
parent = dentry->d_parent;
if (parent && !spin_trylock(&parent->d_lock)) {
if (inode)
spin_unlock(&inode->i_lock);
goto relock;
}
[/c]

因为另一个进程持有dentry P的->d_lock,所以 dentry_kill() 中 spin_trylock(&parent->d_lock) 失败,转而 spin_unlock(&dentry->d_lock) 并返回;

4. CPU1 上的进程此时仍在调用 select_parent(P),并找到了 dentry C,锁住了它的->d_lock,放入 dispose 链表并返回1(其实就是重复步骤(1));

5. CPU0 上的进程继续 shrink_dentry_list() 中的循环,发现 dispose 链表已经为空,退出循环并返回;

6. CPU1 的进程开始重复步骤(3);

7. CPU0 的进程开始重复步骤(2)。

Livelock 产生了!

这里问题的关键在于那个 dispose list,如果 dentry C被某个进程加入到 dispose list 中,另外的进程不应该再找到它,所以修复就是加一个标志位 DCACHE_SHRINK_LIST,来标示这个 dentry 有没有被加入。

关于 tmpfs

“tmpfs” 是 Linux 内核中另一个让人困惑的名字,它的实现是在 mm/shmem.c,”shmem” 猛一看和 “tmpfs” 根本不沾边,虽然我们知道 tmpfs 是基于内存的!我们可以通过看一下 tmpfs 都被用到哪些地方来了解它到底为什么叫这个名字。

你的桌面 Linux 系统中基本上都会挂载了 tmpfs:

% grep tmpfs /proc/mounts
devtmpfs /dev devtmpfs rw,seclabel,nosuid,relatime,size=1958956k,nr_inodes=489739,mode=755 0 0
tmpfs /dev/shm tmpfs rw,seclabel,nosuid,nodev,relatime 0 0
tmpfs /run tmpfs rw,seclabel,nosuid,nodev,relatime,mode=755 0 0
tmpfs /sys/fs/cgroup tmpfs rw,seclabel,nosuid,nodev,noexec,relatime,mode=755 0 0
tmpfs /media tmpfs rw,rootcontext=system_u:object_r:mnt_t:s0,seclabel,nosuid,nodev,noexec,relatime,mode=755 0 0

正如这篇文章中提到的,/dev/shm 是 POSIX IPC 用到的,用来实现进程间通信。除了 sem_open(3) 的中 oflag 参数,基本上看不出来和文件有什么相关。

除此之外,另一个用到它的地方是 anonymous shared mapping!

mmap_region():

        if (file) {
        //...
        } else if (vm_flags & VM_SHARED) {
                error = shmem_zero_setup(vma);
                if (error)
                        goto free_vma;
        }

shmem_zero_setup() 在内核通过 kern_mount() 挂载的(用户不可见的) tmpfs 的根目录中创建了一个”dev/zero”的文件,注意,这里可以重复创建哦,因为内核跳过了类似 may_create() 的检查,而且这个文件本身很特殊,它一开始就是 unlinked 的。所以内核实际上是通过 tmpfs 中的一个文件来实现了匿名的共享映射!到此,你可以看出,tmpfs 这个名字其实名副其实了。

另外,tmpfs 本身可以随意挂载,通过 mount -t tmpfs,你可以在上面进行任意文件操作。所以,内核通过 tmpfs 一套代码把下面三个东西给统一起来了:1. 匿名共享映射;2. POSIX IPC;3. tmpfs 文件操作。Mel Gorman 这样解释到:

This is a very clean interface that is conceptually easy to understand but it does not help anonymous pages as there is no file backing. To keep this nice interface, Linux creates an artifical file-backing for anonymous pages using a RAM-based filesystem where each VMA is backed by a “file” in this filesystem. Every inode in the filesystem is placed on a linked list called shmem_inodes so that they may always be easily located. This allows the same file-based interface to be used without treating anonymous pages as a special case.

之所以用 tmpfs 而不是同样基于内存的 ramfs 是因为:1) tmpfs 文件是可以 swap 的;2) tmpfs 是有大小限制的,不会允许用户无限制使用内存从而导致 OOM。(注:只有当 CONFIG_SHMEM=n 时,tmpfs 才会调用 ramfs 代码。)

Fedora 上那些内核包

Fedora 上和内核相关的包很多,运行下面的命令看一下:


~% repoquery -q ‘kernel*’
kernel-0:2.6.35.11-83.fc14.x86_64
kernel-debug-0:2.6.35.11-83.fc14.x86_64
kernel-debug-debuginfo-0:2.6.35.6-45.fc14.x86_64
kernel-debug-devel-0:2.6.35.11-83.fc14.x86_64
kernel-debuginfo-0:2.6.35.6-45.fc14.x86_64
kernel-debuginfo-common-x86_64-0:2.6.35.6-45.fc14.x86_64
kernel-devel-0:2.6.35.11-83.fc14.x86_64
kernel-doc-0:2.6.35.11-83.fc14.noarch
kernel-headers-0:2.6.35.11-83.fc14.x86_64



我们一一看一下这些包的内容和用途。

kernel 这个包不言而喻了,必备的,里面包含了vmlinuz,initramfs,以及所有内核模块,也就是/lib/modules/uname -r下面的东东。

kernel-devel 这个包是给开发内核驱动,写内核模块的人用的,里面包含了内核源代码中的头文件,Kconfig文件,Makefile等等。

kernel-headers 这个包是给glibc 等用户空间的库、程序使用的,它提供内核导出到用户空间的所有头文件,主要在/usr/include/linux/中,这样以来用户空间就可以和内核使用相同的常量,结构体定义等等。

kernel-doc 这个包提供内核文档,即源代码 Documentation/目录下的文档,不过里面包含了make htmldocs 和 make mandocs 之后的文档。

kernel-debug 和 kernel 包类似,但编译时使用的是带debug选项的config文件。

kernel-debug-devel 同理,和 kernel-devel 包类似。

kernel-debuginfo,给SystemTap在 kernel 上使用的。 kernel-debug-debuginfo 同理,供 SystemTap在 kernel-debug 上使用。 而 kernel-debuginfo-common 包为它们提供一些共同的源文件。注意,kernel-debuginfo 包提供vmlinux! 像 crash 这样的工具也需要它。

这里似乎少了一个 kernel-firmware,原来在 Fedora 上它已经改名为 linux-firmware 了,但是在RHEL上面没改,里面提供内核源代码目录 firmware/ 里面的固件。

当然了,因为 perf 也在内核源代码树中,所以 perf 这个包也是从同一个spec文件生成的。

LINUX_REBOOT_MAGIC

include/linux/reboot.h 中有五个魔数:

define LINUX_REBOOT_MAGIC1 0xfee1dead

define LINUX_REBOOT_MAGIC2 672274793

define LINUX_REBOOT_MAGIC2A 85072278

define LINUX_REBOOT_MAGIC2B 369367448

define LINUX_REBOOT_MAGIC2C 537993216

第一个的意思很了然,是feel dead的hex表示,类似于0xdeadbeef。

而后面四个就有意思了,今天在公司的邮件列表上看到一个讨论说这些其实是Linus和他女儿的生日。猛一看看不懂,这么一看就了然了:

% awk ‘/MAGIC2/ {printf “%Xn”,$3}’ include/linux/reboot.h
28121969
5121996
16041998
20112000

kdump 介绍

今天在公司内部讲了一下自己工作中做的kdump,都是开源的东西,没什么机密,所以拿出来和大家分享一下。Slides可以在这里下载

准备这个slides没花费我多长时间,画里面那三张图倒是花费了我好几个小时。没有功劳也又苦劳啊。T_T

看完之后起码你能明白为啥我说这个工作是又脏又累了,整天修一些蛋疼的bug。每一个自己做initrd的人上辈子都是折翼天使!而做kdump的initrd的人连上上一辈子都是折翼天使!

以后我可能还会讲其它和Linux内核相关的一些有趣的东西,讲完之后会陆续在这里和大家分享。

CVE-2009-0029 和 CVE-2010-3301

从我出去旅行到现在这段时间内,Linux内核似乎是漏洞频频啊,光我看到的安全漏洞就已经若干个了,有机会真想八一八这些安全漏洞。

CVE-2010-3301是其中一个。这个漏洞的成因是,在64位的内核上执行32位的系统调用时,作为传递系统调用号的%rax高32位未被清零处理,而且在进行比较的时候直接使用的%eax,导致高32位被忽略:

        cmpl $(IA32_NR_syscalls-1),%eax
        ja ia32_badsys
ia32_do_call:
        IA32_ARG_FIXUP
        call *ia32_sys_call_table(,%rax,8)

这样以来,通过静心构造的%rax就可以跳转到它想要的位置去!在这个exploit中,它就利用ptrace()来跟踪系统调用,并把计算好的想要跳转地址的偏移传递到%rax中,然后执行事先放置好的代码来提升权限!

修复方法很简单,要么把%rax的高位清零,要么比较的时候使用%rax。修复这个问题的commit是:

http://git.kernel.org/linus/36d001c70d8a0144ac1d038f6876c484849a74de
http://git.kernel.org/linus/eefdca043e8391dcd719711716492063030b55ac

和这个问题类似的问题之前也曾出现过,CVE-2009-0029,问题更严重,涉及很多的系统调用。不同的是,这个涉及64位的内核和64位的用户空间,来自用户空间的传递系统调用参数的寄存器的高32位同样没被清零,而带32位参数(比如int)的系统调用就会有问题,内核代码只会检查对它有意义的低32位,高32位就被忽略而直接传递到后面去了,这就会带来问题了。

问题的解决方法也很简单,就是要把这些寄存器高位清零。说起来简单,做起来难。要是和上面一样直接用汇编处理的话,参数的类型的信息就丢失了,因为你汇编里分不清它到底是32位还是64位;而如果用C处理的话,有那么多系统调用,一个一个处理?那不符合Linus的作风!他是怎么做的呢?用宏!而且用强制转化,把所有的32位参数声明为long,然后再强制转化成实际的类型,比如int。去看看SC_CASTx()和SC_LONGx()的定义就知道了:

[c]

define __SC_CAST1(t1, a1) (t1) a1

define __SC_LONG1(t1, a1) long a1

define __SYSCALL_DEFINEx(x, name, …)

    asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__));           
    static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__));       
    asmlinkage long SyS##name(__SC_LONG##x(__VA_ARGS__))            
    {                                                               
            __SC_TEST##x(__VA_ARGS__);                              
            return (long) SYSC##name(__SC_CAST##x(__VA_ARGS__));    
    }                                                               
    SYSCALL_ALIAS(sys##name, SyS##name);                            
    static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__))

[/c]

可见Linus大神把宏用到了何等出神入化的地步。:-) 这也是为什么你在内核中看到系统调用都是用SYSCALL_DEFINEx()来定义了。

ELF Extended Numbering

ELF格式天生就有个不小的缺陷—— segment 个数最多 65535 个,这是由e_phnum来决定的,其类型是16位的无符号整数。对于一些程序来说,65535是不够的,如果它使用了很多mmap(2)的话。

要解决这个问题并不容易,e_phnum是用了多年早已定型了的东西,不能随意修改,只能在别的地方想办法去补救,于是下面这个方案就出来乐。这个方案在Solaris上已经使用了长时间了,但 Linux 上一直还没有。其原理是:e_phnum保持不动, 如果 segment 的个数大于等于0xffff时,e_phnum设为0xffff ,同时下标为0的section header中的sh_info被设为真实的 segment 数。而 sh_info 是32位无符号类型,所以它最多能支持4294967295个。使用这个 sh_info 是合理的,因为下标为0的section header平时是没用的,其类型是 SHT_NULL。随之相应改变的还有 sh_size 和 sh_link,它们分别被设为 e_shnum和 e_shstrndx(见该文档P215)。

Daisuke HATAYAMA 提交了Linux上的补丁,主要分为三部分:内核,gdb,和binutils。内核部分补丁不少,但真正起作用的补丁只有一个,其它的都是refactor,见下:

8d9032bbe4671dc481261ccd4e161cd96e54b118 elf coredump: add extended numbering support
93eb211e6c9ff6054fcf9c5b9e344d8d9ad29175 elf coredump: make offset calculation process and writing process explicit
1fcccbac89f5bbc5e41aa72086960059fce372da elf coredump: replace ELF_CORE_EXTRA_* macros by functions
088e7af73a962fcc8883b7a6392544d8342553d6 coredump: move dump_write() and dump_seek() into a header file
05f47fda9fc5b17bfab189e9d54228025befc996 coredump: unify dump_seek()

gdb 的补丁如下:
78999a9bcf9d87f72cd67a782e1e859a6a09d9de  * common.h (PN_XNUM): Define.
cce546478afe296ebbb69f208b708a8b3fe5f7e5 * elfcode.h (elf_swap_ehdr_out): Handle e_phnum > 0xffff.

binutils 的补丁:
[http://sourceware.org/ml/binutils/2010-01/msg00393.html](http://sourceware.org/ml/binutils/2010-01/msg00393.html)

man-page 补丁:
[http://permalink.gmane.org/gmane.linux.man/1302](http://permalink.gmane.org/gmane.linux.man/1302)

自动生成内核config

配置内核的人都清楚要选择内核的配置项是一件比较麻烦的事:要是多选了吧,内核体积可能变大,编译时间会变长;要是选少了吧,恐怕系统连启动都启动不了。所以一般我们的做法是拿过系统提供的 config 来直接make oldconfig。

但这么做并不是很好,因为发行版通常为了照顾不同配置的机器而选中了很多的模块,这样一来对本机器没有用的东西也会被编译进来,从而导致编译时间变长,这还是次要,关键是模块数量会增多,内核体积也有可能变大。

理想的情况是我们根据本系统运行时加载的模块来决定到底选中哪些模块。问题就来了,我们怎么才能知道自己的系统需要哪些模块不需要哪些模块呢?

为了解决这个问题,Steven Rostedt写了一个脚本,叫作streamline_config.pl,来解决这个问题。而且他已经把此脚本提交到内核,见scripts/kconfig/streamline_config.pl。这样一来,我们就可以通过 make localmodconfig 或 make localyesconfig 来生成我们真正需要的最小的config了。不同的是,后者会把系统正在使用模块编译进内核,而前者不会,它只会保留系统使用的模块而且继续以模块的形式存在。

这个脚本不复杂,原理也很简单,通过 lsmod 找出正在使用的驱动, 然后通过解析所有的Makefile中的obj-$(CONFIGXXX) += xxx.o模式来找到驱动对应的CONFIG*,不过还要解决依赖性的问题。

bonding和bridge对netpoll的支持

最近我在做 netpoll 的东东,具体说是让 bridge 和 bonding 支持 netpoll,这样我们就可以通过 bridge 和 bonding 设备来使用 netconsole 和 netdump 了(注:netdump 已经被遗弃)。三个补丁见这里:

http://lkml.org/lkml/2010/4/5/13

http://lkml.org/lkml/2010/4/5/14

http://lkml.org/lkml/2010/4/5/15

其实很简单,无非就是实现几个回调函数,传递几个关键的指针。最纠结的地方是 netpoll 的那几个成员变量,一步错了导致后面全错,不过说到底,还是对 net 部分的代码不熟悉。

最气人的是,不少时候明明是一个成员忘了赋值,结果导致 lockdep 警告,有时甚至连警告都没有直接死掉,非常莫名其妙!所以我就说啊,你遇到 oops,soft lockup 不要紧张,你最起码是已经得到很多错误信息了,真要是一条错误信息都没有,那才紧张呢!你问我这时候怎么办?没什么好办法,只能尝试缩小可疑代码的范围,然后凭直觉去猜问题应该在哪里!比如我这个,我可以通过使用或不使用 netconsole 来判断问题是出在我的补丁上还是原有代码中(不要太迷信原有代码,它们也会有 bug 的),如果是出在我的补丁中,那到底是哪一块,哪个函数。如果没任何信息,系统死掉很可能是因为什么?当然是加锁了!重新检查锁到底有没有用对。如果还不行就老老实实地加 printk 吧!

还有就是,不要迷恋现象,现象并不一定能正确表明问题所在,它只是问题带来的症状,仅此而已!换句话说,可能是前面的一个小问题引发了后面大的症状,这并不意味着问题就是后面的一个大问题,也不意味着问题就真如症状直接表明的那样。静下心来仔细分析症状的每一个细节,不要轻易下结论说问题应该在哪里。

赤果果的教训啊!