Windyland 内核崩坏

编译内核若只如初见

编译内核是一件很有趣的事情。通过编译内核,你可以发现修改源代码、然后达到某一个小功能或者完成一个彩蛋是挺有意义的。

搜索网络上的攻略或者经验帖子,通常以复杂自居。下面介绍两种可以很简单地完成编译的方法。

第一种是通过Ubuntu的dpkg-buildpackage方式编译,编译的最后结果是deb 包。deb 包可以通过dpkg包管理器安装,这种方式对已有的内核源代码可以加以很方便的定制,然后重新封装,编译结果也可以用于广泛的部署或者测试。

Ubuntu 下用于编译的内核,按照 Ubuntu dpkg 方式需要准备: 1) 包含debian 包配置的源代码 2) 准备编译环境 3) 编译代码

虽然Linux 内核的配置和编译软件的方式复杂, 但是所幸 dpkg 对这些都加以标准化区分。以下命令会下载当前内核的源文件加以重新编译:

# Step 1, Obtaining the source for current Ubuntu kernel
apt-get source linux-image-$(uname -r)

# Step 2, Preparing Build Environment
sudo apt-get build-dep linux-image-$(uname -r)

chmod a+x debian/rules
chmod a+x debian/scripts/*
chmod a+x debian/scripts/misc/*
fakeroot debian/rules clean
fakeroot debian/rules editconfigs

# Step 3, Building the kernel
fakeroot debian/rules clean
# quicker build:
fakeroot debian/rules binary-headers binary-generic binary-perarch
# if you need linux-tools or lowlatency kernel, run instead:
fakeroot debian/rules binary

# Step4, Installing the kernel packages (optional)
dpkg -i linux*-image*.deb

第二种我们描述如何用Linux 内核初始方式编译,需要一定的配置,编译的最后结果是 bzImage。bzImage 可以通过复制部署到其他机器上, 也可以用于QEMU调试。

在 Red Hat 发行的系统比如 Fedora、RHEL 或者 CentOS,可以执行一下命令安装内核编译需要的库和工具:

sudo yum install gcc make git ctags ncurses-devel openssl-devel
git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git
cd linux-stable

以上命令会抓取稳定版本的内核源代码到本地。就Linux 开发而言,分为两种,一种是开发版本,用于功能的开发; 其次是稳定版本,用于生产环境的使用,主要用于修补和维护。

如果希望是后者,可以使用 git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git 代替。

第二步是配置内核,一般的配置内核可以从使用默认配置开始。

下面命令都可以生成默认配置,默认配置 make defconfig 或者 x86_64的默认配置 make x86_64_defconfig, 后文推荐使用的KVM Guest 的默认配置 make kvmconfig

最后,这些配置被保存在源代码目录所在的 .config 文件中,

所有的配置都以 CONFIG_XXX_YYY=y/n/m 的条目保存。 比如 CONFIG_PREEMPT_RT=y 表示这个内核开启了 PREEMPT_RT 功能。 此外这个配置也可以保持内核模块开启与否的配置, 更具体的配置可以使用 tui 界面展示并修改, make menuconfig

此时可以使用make 进行编译:

make -j$(nproc)

编译结果保存在

arch/x86/boot/bzImage

以上命令可以编译你的第一个内核。

下一次我们会介绍如何使用QEMU 和JTAG 启动这个内核,并进行运行时的内核调试。

套接字模型速记

Linux 的网络栈支持套接字模型,又称 Berkeley sockets, 最早出现在1983年的4.2 BSD操作系统。 历史多年, 套接字接口经历了IPX、IPv4、IPv6、Unix Socket还有Netlink的支持带来的变化, 还有Internet的出现带来的万维网,NAPI的Patch的整合带来的异步网络编程变迁, 但其核心技巧还是没有发生改变。其接口简单是:

  1. socket - 创建特定类型的套接字文件表述符。
  2. bind - 将给定的本地网络地址绑定到套接字
  3. listen - 监听远程地址的连接请求
  4. accept - 接受远程地址的连接请求
  5. connect - 向远程地址发起连接请求
  6. getsockopt and setsockopt

在Unix 哲学概念影响下,每一个套接字资源最初被表述为文件表述符。 程序只需要获取远程域名、IP地址和端口号,那么程序就可以无需获取 本机的具体网络配置的情况比如网卡MAC地址或者路由表配置与远程服务器建立连接传递信息。 内核代其生成包含正确的Ethernet Header、IP Header还是其他类型的物理网络,就整个过程而言, 对程序是透明的。

调试技巧 - GDB

有些时候阅读代码并不能解决问题, 调试技巧 or Debugging Skills, 就成为一种必不可少的技能。 但是和编程技巧一样, 极难被系统化或者体系化, 大概取决于具体的编程语言和项目本身。

下面是一些个人积累和流传的内核调试经验,一定程度上也受用于其他静态编程语言。

GDB

可能很多人听说过gdb 也能没有,但是学习CS 的同学基本都接触过 调试器,比如dos 的 debug.exe 或者Visual C++自带的调试器等等。和这些调试器类似,GDB 支持设置断点 (breakpoint), 打印变量 (print), 逐步执行 (step), 和打印代码内容 (dis/list), 更多的指令可以查看列表

最简单的调试是对本机系统或者已有的coredump文件的调试。这种方式不能控制程序的执行,也无法修改内存, 因为这时候程序内存只是静态文件的映射。但是这种方式由于不需要 动态环境,可能某种意义上也是最常见的方式。

如果当前内核支持 CONFIG_PROC_KCORE 选项, 也就是 procfs 的 /proc/kcore 文件可以只读的访问当前内核。如果使用 Ubuntu 系统可以使用下面命令:

sudo gdb -q /boot/vmlinuz-$(uname -r) /proc/kcore

然后在 gdb 中执行, 从内核内存中获取到log buffer:

lx-dmesg

其二是 coredump file, 一般core文件生成在 系统根目录下或者 /var/core

gdb /boot/vmlinuz-$(uname -r) /var/core/core file

以上技巧建议在安装调试信息以后再做尝试。

KGDB

GDB 也支持第二种模式的调试, 又称kgdb,也就是 gdb 作为客户端动态接管了目标内核、或者用户态程序的执行。 这种模式通常需要硬件或者GDB的特权许可。gdb 可以作为一个很有扩展性的调试工具, 可以在目标程序的特定函数位置执行 breakpoint/watch 操作,中断当前程序的执行, 或者可以根据调试信息逐行执行 step, 打印当前函数调用栈 backtrace,了解调用者和被调用函数的关系。 当然最主要的还是可以打印和修改程序的内存内容,函数的临时变量(info local) 打印CPU寄存器(info registers) 。

如果配合QEMU 使用可能会变得异常简单,

$ qemu -kernel bzImage -append "root=/dev/sda console=tty0 nokaslr" -m 2G -hda wheezy.img -s -S
$ gdb bzImage
(gdb) target remote localhost:1234

nokaslr 表示禁用内存地址随机分布

-s 表示默认启动 gdb server 在1234 端口

-S 表示cpu 不会启动直到GDB的continue 指令

如何掌握这种技巧在于动手, 下一次我们会讨论如何编译内核,以及下一种调试方式, netconsole和内核日志。

BTW gdb 又名 GNU Debugger,最早出自GNU 创始人 Richard Stallman 之手, 可以认为这是他的GNU Emacs 之后的一大力作,具体可以看同一作者写的Debugging with GDB: The GNU Source-level Debugger.

AF XDP One TCP zero copy

Linux 内核网络栈从不缺乏丰富的功能,比如OpenVPN 和 IPSec,足够满足一般日常需要。但是对于高速网络来说,内核上各项限制已经造成了性能瓶颈的即成事实。其中之一和应用业务联系最密切的就是TCP。 TCP 协议不同于 Ethernet/VLAN/IP/VXLAN,是内核遇到的最靠近底层但是需要维护状态的协议。除了其链接状态, congesion 行为的研究一直是各SGI的前列,我且援引 netconf2018上的一个讨论, 有人提到几点可以在现在 Linux 内核基础上更进一步优化, 第一点就是TCP zero copy。

  1. TCP zero copy receive
  2. SO_SNDBUF
  3. ACK compression
  4. PSH flag set on every TSO packet

Zero-copy 一直是硬件交换机和操作系统在高吞吐量性能优化的核心,这个优化会对降低CPU占用起决定性作用。如何避免内存的复制,在配置较差的嵌入式环境有非常大的意义。同样在虚拟化环境中降低对VM和其他业务的影响可以降低对硬件的开销,也非常重要。而TCP 作为 网络栈上较为复杂的模块, 其从用户端程序到最后网卡驱动,可以通过相关的两个commit 了解其涉及模块和复杂程度。两个commit 相对简单:

在接受方,TCP zero copy receive 更像是作为套接字协议接口上为tcp 协议的开启的优化。使用时通过 用户程序调用 getsockopt 时候 给定的可用的内存地址,然后套接字模块将其映射至网卡驱动可直接使用的MMU内存。这个优化在Linux 开发中不乏先例, 例如 pcap 套接字的 zero copy receive的也是利用自定义套接字的mmap完成。

 int tcp_mmap(struct file *file, struct socket *sock,
             struct vm_area_struct *vma)
 {
-       unsigned long size = vma->vm_end - vma->vm_start;
-       unsigned int nr_pages = size >> PAGE_SHIFT;
-       struct page **pages_array = NULL;
-       u32 seq, len, offset, nr = 0;
-       struct sock *sk = sock->sk;
-       const skb_frag_t *frags;
+       if (vma->vm_flags & (VM_WRITE | VM_EXEC))
+               return -EPERM;
+       vma->vm_flags &= ~(VM_MAYWRITE | VM_MAYEXEC);
+
+       /* Instruct vm_insert_page() to not down_read(mmap_sem) */
+       vma->vm_flags |= VM_MIXEDMAP;
+
+       vma->vm_ops = &tcp_vm_ops;
+       return 0;
+}
+EXPORT_SYMBOL(tcp_mmap);

上述的改动将这块虚拟内存的地址修改为TCP 协议栈接受的地址。

最后,这两个优化在 Linux 4.18 已经合并进主线。Happy Hacking。

邮件 - 现代圆桌会议

现在随着科技发展,多个团队交叉完成任务变得更加普遍和常见。参于会议的各方可能来自不同方向、不同背景、不同国家甚至不同的公司和时区,沟通技巧的挑战也不在单纯限制在原先的面对面,或者端到端的即时电话会议。然而沟通难度的增加丝毫不能减少老板们对工作成果的迫切热情。所以如何写好、回复电子邮件减轻沟通带来的压力,或者巧妙利用邮件也是建立人际关系的途径。秉承着开卷有益的精神,我来写一点自己的经验体会。

现代圆桌会议

说到圆桌会议,与会各方都是在时下亟需定夺的事项。简而言之,邮件的内容最为重要。邮件发出往往事关沟通的各方,是为了解决某个悬而未决的问题、为了达成某种共识一边继续展开后续工作、或者是展开讨论来满足自己或者其他人的信息需要。且不论警言妙句、还是巧妙的提问,通常最简单明了的表述最容易达到目的。

圆桌会议的与会者和现在英国议会类似,与会者无非当时的参议员,辩论能力强者占优势无可厚非。邮件的载体,措辞和修辞手法、结构,经过一定时间的积淀,通常都会了超过简单实用的文法。这时候邮件的载体会使邮件更加有信息量,显得当事人显得富有涵养。譬如韩愈贾岛的推敲之争,只是深厚的积淀的文化人才有可能的趣事。

最后,邮件的对象。为了达到发信者的目的,对邮件各方的甄选其实是最困难的。如果有明确的目标,比如客户公司,尽可能发信前了解其公司运行业务背景,以度量需求和实际情况上的差距,让沟通更为有效,更容易达成稳固共识。如果只是有大致的行动方向,还需要寻找稳固的合作伙伴,更需要讲究技巧的在相互理解和抵触的各项事宜上将当前最重要的难点和愿景及时抛出,谨慎地抓住对方的意愿,将这个共识推展为一个可以前进的行动计划。