开启辅助访问 切换到窄版

打印 上一主题 下一主题

Linux内核教程(1)-道路千万条,调试最重要

[复制链接]
作者:6101111992 
版块:
嵌入式操作系统 linux 发布时间:2020-12-19 01:29:08
13580
楼主
跳转到指定楼层
| 只看该作者 回帖奖励 |倒序浏览 |阅读模式
大家可能都学过操作系统,在操作系统课上,在进程同步互斥中,图灵奖获得者Dijkstra的信号量Semphone。
Linux中当然也提供了semphone的实现,用做最普通的睡眠锁。所谓睡眠锁,意思是如果有一个任务试图去获取一个被占用的信号量时,会被推到等待队列中,然后让其睡眠。这样CPU资源就可以用来处理别的事情,实现资源的合理利用。这与一直等待的自旋锁形成鲜明的对比。当占有信号量的任务运行结束后,会唤醒队列里等待的任务,这个信号量也会被唤醒的任务占有。
针对于P和V两种原语的Linux实现是down和up两个操作。还有支持被中断的down_interruptible,可被杀的down_killable,不等待的down_trylock,带超时的down_timeout,考虑得非常周到。
不仅如此,信号量也是支持读/写信号量分离的。
一切看起来很美好,不是么?
我们看看semphone的作者在semphone.c的开头是如何写的:
2 /*3 * Copyright (c) 2008 Intel Corporation4 * Author: Matthew Wilcox 5 *6 * This file implements counting semaphores.7 * A counting semaphore may be acquired 'n' times before sleeping.8 * See mutex.c for single-acquisition sleeping locks which enforce9 * rules which allow code to be debugged more easily.10 */对于懒得看英文的同学,我简单翻译一下,如果只是获取一次的锁,建议改用mutex.h,这样会使调试更容易。
在Linux kernel中,为了方便调试,基本上每种机制都有自己的调试宏,以CONFIG_DEBUG_*开头。下面是我随便搜的几个:

比如自旋锁,就有CONFIG_DEBUG_SPINLOCK,打开之后,会增加追踪如下例:
3492 static inline void3493 prepare_lock_switch(struct rq *rq, struct task_struct *next, struct rq_flags *rf)3494 {3495 /*3496 * Since the runqueue lock will be released by the next3497 * task (which is an invalid locking op but in the case3498 * of the scheduler it's an obvious special-case), so we3499 * do an early lockdep release here:3500 */3501 rq_unpin_lock(rq, rf);3502 spin_release(&rq->lock.dep_map, _THIS_IP_);3503 #ifdef CONFIG_DEBUG_SPINLOCK3504 /* this is a valid case when another task releases the spinlock */3505 rq->lock.owner = next;3506 #endif3507 }很不幸,semaphone不支持自动调试宏,连受限的也做不到。
所以,信号量最好的场景是特别复杂的场景,比如跨内核空间和用户空间的复杂交互类的,反正调试也不靠这个。
而对于内核代码中正常的使用,应该使用semaphone的受限版本mutex。
mutex是通过怎样的自律来获取自由的呢:

  • 任何时间,只能有一个任务持有mutex
  • 因为只有一个,所以加锁者必须负责给mutex解锁
  • 因为要负责解锁,所以持有mutex的进程不得退出
  • mutex不得用于中断处理程序,也包括下半部。不了解中断和下半部原理的我们后面会介绍
  • mutex不能复制
  • mutex不能手动初始化
  • mutex只能初始化一次
加了这些自律之后,我们终于可以为mutex写一些调试用的功能了。不像自旋锁只是在处理上加了几条语句,mutex专门设计了调试专用函数来做这些事情:
17 extern void debug_mutex_lock_common(struct mutex *lock,18 struct mutex_waiter *waiter);19 extern void debug_mutex_wake_waiter(struct mutex *lock,20 struct mutex_waiter *waiter);21 extern void debug_mutex_free_waiter(struct mutex_waiter *waiter);22 extern void debug_mutex_add_waiter(struct mutex *lock,23 struct mutex_waiter *waiter,24 struct task_struct *task);25 extern void mutex_remove_waiter(struct mutex *lock, struct mutex_waiter *waiter,26 struct task_struct *task);27 extern void debug_mutex_unlock(struct mutex *lock);28 extern void debug_mutex_init(struct mutex *lock, const char *name,29 struct lock_class_key *key);注:本文中的代码取自kernel 5.9.10版。
从信号量的例子我们就可以看到可调试性在内核中的重要性。
同样,有很多在内核开发中被重点强调的内容,其重要原因也是因为难以调试,比如栈溢出。
因为不像用户空间的应用程序容易退出,内核本身是一直长期运行的,这就导致内核不得不面对内存严重碎片化的情况,想要分配连续页的内存会越来越困难。而且用作栈的内存也没有办法换出到辅助存储中去,所以尽管栈溢出调试困难,也只能分配4k大小的栈。所以就要求开发者以自律享受自由,尽量避免在栈上分量大的对象。一旦栈溢出了,产生的结果是难以预测的。
所以,我们把内核的可调试性当作第一要务来强调。后面我们会调用各种手段来对内核进行调试,包括打日志,调试文件系统,perf和ftrace等工具,甚至SystemTap和eBPF这样的自动生成内核模块的脚本工具等,以及通过模拟器进行调试等各种手段。
讲了调试的重要性之后,我们身体力行,首先讨论如何编译内核,如何在模拟器上跑起内核。
目前Linux的两个最主要的应用场景:一是跑在电脑上,主要场景是给自己的电脑更换内核;另一个是跑在嵌入式设备上,比如手机等。
内核源代码地址可以在kernel.org上下载,比如我写此文时最新的稳定版是5.10.1,我们就可以下载这个包:
wget -c http://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.1.tar.xz如果要下载源码树的话,可以去clone kernel主线的代码库:
在运行Linux的电脑上,我们可以通过包管理系统获取到当前使用的系统内核的源代码。
比如在Ubuntu上,可以通过apt install linux-source来安装源代码:
root@iZ8vb39159pi4fttv8aaoyZ:/boot# apt search linux-sourceSorting... DoneFull Text Search... Donelinux-source/focal-updates,focal-updates,focal-security,focal-security,now 5.4.0.58.61 all Linux kernel source with Ubuntu patcheslinux-source-5.4.0/focal-updates,focal-updates,focal-security,focal-security,now 5.4.0-58.64 all Linux kernel source for version 5.4.0 with Ubuntu patches源代码会下载到/usr/src目录下。
下载之后,我们将其解压之后,就可以进行编译了。
安装gcc, flex, bison, bc之类的常规操作就不多说了,有遇到问题的请在评论区里提问。
编译之前需要对内核进行一些配置,比如调试信息等。这些配置会写在一个config文件中。
以Ubuntu 20.04系统为例,在/boot目录下可以看到一些config开头的文件,比如config-5.4.0-58-generic,这些就是我们使用的Ubuntu系统的config文件。
这些配置项,以CONFIG_* = y这样的形式来表示这个配置被支持。而如果不支持的话,则把这个配置项用“#”注释掉,并将=y改为is not set以利于理解。
比如我们上面讨论的锁的调试信息,在config文件中的描述如下:
## Lock Debugging (spinlocks, mutexes, etc...)#CONFIG_LOCK_DEBUGGING_SUPPORT=y# CONFIG_PROVE_LOCKING is not set# CONFIG_LOCK_STAT is not set# CONFIG_DEBUG_RT_MUTEXES is not set# CONFIG_DEBUG_SPINLOCK is not set# CONFIG_DEBUG_MUTEXES is not set# CONFIG_DEBUG_WW_MUTEX_SLOWPATH is not set# CONFIG_DEBUG_RWSEMS is not set# CONFIG_DEBUG_LOCK_ALLOC is not set# CONFIG_DEBUG_ATOMIC_SLEEP is not set# CONFIG_DEBUG_LOCKING_API_SELFTESTS is not set# CONFIG_LOCK_TORTURE_TEST is not set# CONFIG_WW_MUTEX_SELFTEST is not set# end of Lock Debugging (spinlocks, mutexes, etc...)我们以Ubuntu 20.04的源码为例,看看如何去编译内核。
进入/usr/src/linux-source-5.4.0目录,我们会看到linux-source-5.4.0.tar.bz2,将其解压。
进入解压后的目录,将/boot/config-5.4.0-58-generic文件复制过来。
然后执行
make ./config-5.4.0-58-generic成功后,运行make menuconfig,在字符图形界面下可以进行一些手动的配置:

配置好之后,保存到.config中,最后执行make -j4来进行编译。j后面是编译开启的线程数。
编译成功后,会看到类似于下面的输出:
Setup is 16380 bytes (padded to 16384 bytes).System is 8697 kBCRC 1a8c27e4Kernel: arch/x86/boot/bzImage is ready (#1)编好的kernel在arch/x86/boot/bzImage。
如果我们是想在模拟器上运行内核的话,没有Ubuntu给我准备config。这也不怕,我们可以用x86_64的默认config,在其基础上进行修改。首先我们需要设置下ARCH变量为x86_64,这样不用写路径,make就知道去哪里找x86_64_defconfig
export ARCH=x86_64make x86_64_defconfig然后make menuconfig和make不变。
x86_64的搞定了,换成别的架构就是照方抓药了。只不过需要装交叉编译的工具链。
在Ubuntu上,我们可以通过apt install gcc-aarch64-linux-gnu来安装支持ARM64的工具链。
安装好之后,我们配置下CROSS_COMPILE环境变量:
export CROSS_COMPILE=aarch64-linux-gnu-针对于arm64,只有一个defconfig,设好ARCH之后就可以自动找到了:
export ARCH=arm64ARCH后面的名字以arch下的子目录名为准,目前kernel支持的架构如下:

  • arc
  • arm64
  • csky
  • hexagon
  • m68k
  • mips
  • nios2
  • parisc
  • riscv
  • sh
  • um
  • x86_64
  • alpha
  • arm
  • c6x
  • h8300
  • ia64
  • microblaze
  • nds32
  • openrisc
  • powerpc
  • s390
  • sparc
  • x86
  • xtensa
我们执行make defconfig
make defconfig然后运行make -j8之类就可以了。
但是这样编出来的内核,真的只是一个内核,没有任何shell之类的可以用。内核启动的最后,是要启动一个init程序的,当然我们也可以手写一个。但是为了能有个shell,我们选择用busybox的init.
我们去busybox.net去下载源码:
wget -c http://busybox.net/downloads/busybox-1.32.0.tar.bz2刚才编译内核时已经设置好ARCH和CROSS_COMPILE了,正好busybox也能用到。
busybox没那么多defconfig,上来就make menuconfig就好。

我们只需要一个busybox程序,所以选择Build static binary。
退出保存之后,执行make -j4去编译。
最后,执行make install,会安装到_install目录下。
准备好了之后,我们需要给busybox的init准备一个配置文件,一般是/etc/init.d/inittab。这时候别说inittab了,我们连目录还没建呢。
第一步:在_install目录下创建etc,dev,mnt和etc/init.d/目录:
mkdir etcmkdir devmkdir mntmkdir -p etc/init.d/mkdir加-p参数的意思是如果父目录没有创建,则创建之。
第二步:创建inittab文件。
这个我们哪会写,看busybox给我们的例子:
::sysinit:/etc/init.d/rcS# /bin/sh invocations on selected ttys## Note below that we prefix the shell commands with a "-" to indicate to the# shell that it is supposed to be a login shell. Normally this is handled by# login, but since we are bypassing login in this case, BusyBox lets you do# this yourself...## Start an "askfirst" shell on the console (whatever that may be)::askfirst:-/bin/sh# Start an "askfirst" shell on /dev/tty2-4tty2::askfirst:-/bin/shtty3::askfirst:-/bin/shtty4::askfirst:-/bin/sh# /sbin/getty invocations for selected ttystty4::respawn:/sbin/getty 38400 tty5tty5::respawn:/sbin/getty 38400 tty6# Stuff to do when restarting the init process::restart:/sbin/init# Stuff to do before rebooting::ctrlaltdel:/sbin/reboot::shutdown:/bin/umount -a -r::shutdown:/sbin/swapoff -a我们把注释删一删,tty也用不了这么多,精简一下:
::sysinit:/etc/init.d/rcS::askfirst:-/bin/sh::restart:/sbin/init::ctrlaltdel:/sbin/reboot::shutdown:/bin/umount -a -r::shutdown:/sbin/swapoff -a另外,我们不想用::respawn:-/sbin/getty或者login之类的登陆界面,直接进入系统,所以加一条直接调shell: ::respawn:-/bin/sh。
这个参考自busybox的examples/bootfloppy/etc下面的inittab
最后写出来如下:
::sysinit:/etc/init.d/rcS::askfirst:-/bin/sh::respawn:-/bin/sh::restart:/sbin/init::ctrlaltdel:/sbin/reboot::shutdown:/bin/umount -a -r::shutdown:/sbin/swapoff -a写完之后,我们欠Busybox一个启动脚本/etc/init.d/rcS。
第三步:在etc/init.d目录下创建rcS文件,如下:
mkdir -p /procmkdir -p /tmpmkdir -p /sysmkdir -p /mnt/bin/mount -amkdir -p /dev/ptsmount -t devpts devpts /dev/ptsecho /sbin/mdev > /proc/sys/kernel/hotplugmdev -s/proc是内核向进程发送消息的机制。比如cat /proc/cpuinfo可以查看cpu运行信息,而cat /proc/meminfo是内存信息。
/sys与/proc类似,也是内核用于展示信息的虚拟文件系统,于2.5版引入,主要展示设备树。
/tmp是临时目录
/mnt是挂载点
/dev/pts是通过ssh等远程登陆时创建的控制台设备文件
mdev是busybox提供的管理热插拔的程序。
BusyBox v1.32.0 (2020-12-15 16:24:44 CST) multi-call binary.Usage: mdev | mdev -s is to be run during boot to scan /sys and populate /dev.mdev -d: daemon, listen on netlink.-f: stay in foreground.Bare mdev is a kernel hotplug helper. To activate it:echo /sbin/mdev >/proc/sys/kernel/hotplug如上面说明所示,-s用于启动时扫描,激活命令我们也照抄。
rcS写好了之后需要通过chmod +x rcS赋给可执行权限。
有同学问了,mount -a是挂载啥的?这是按照/etc/fstab来mount所有里面写的文件系统的,我们马上就写一个fstab。
参照busybox-1.32.0/examples/bootfloppy/etc/init.d/rcS,mount -a调用fstab也是busybox的传统操作。
第四步, 创建fstab文件
内容如下:
proc /proc proc defaults 0 0tmpfs /tmp tmpfs defaults 0 0sysfs /sys sysfs defaults 0 0tmpfs /dev tmpfs defaults 0 0debugfs /sys/kernel/debug debugfs defaults 0 0到此为止,欠busybox init的连环债算是还清了,下面我们就以此文件系统去编译内核。
有了busybox的init程序,我们重新编译下内核,在menuconfig中将刚才的_install目录设置进去,在General配置的Init RAM filesystem中:


将我们刚才准备好的_install目录的路径设置进去就好。
配置好保存之后,make -j4开始编译。
经过一段欢快的编译,生成Image:
...LD vmlinux.oMODPOST vmlinux.symversMODINFO modules.builtin.modinfoGEN modules.builtinLD .tmp_vmlinux.kallsyms1KSYMS .tmp_vmlinux.kallsyms1.SAS .tmp_vmlinux.kallsyms1.SLD .tmp_vmlinux.kallsyms2KSYMS .tmp_vmlinux.kallsyms2.SAS .tmp_vmlinux.kallsyms2.SLD vmlinuxSORTTAB vmlinuxSYSMAP System.mapMODPOST Module.symversOBJCOPY arch/arm64/boot/ImageGZIP arch/arm64/boot/Image.gz下面我们调用qemu来运行这个内核,qemu可以通过apt来安装。我们模拟4核A72,16G内存:
qemu-system-aarch64 -machine virt -cpu cortex-a72 -machine type=virt -nographic -m 16384 -smp 4 -kernel arch/arm64/boot/Image --append "rdinit=/linuxrc console=ttyAMA0"然后我们就可以登陆进我们的aarch64的Linux啦,我们可以uname看看,是不是我们编的5.10.1:
/ # uname -aLinux (none) 5.10.1 #4 SMP PREEMPT Wed Dec 16 18:19:47 CST 2020 aarch64 GNU/Linux我们再看看cpuinfo:
/ # cat /proc/cpuinfoprocessor : 0BogoMIPS : 125.00Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuidCPU implementer : 0x41CPU architecture: 8CPU variant : 0x0CPU part : 0xd08CPU revision : 3processor : 1BogoMIPS : 125.00Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuidCPU implementer : 0x41CPU architecture: 8CPU variant : 0x0CPU part : 0xd08CPU revision : 3processor : 2BogoMIPS : 125.00Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuidCPU implementer : 0x41CPU architecture: 8CPU variant : 0x0CPU part : 0xd08CPU revision : 3processor : 3BogoMIPS : 125.00Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuidCPU implementer : 0x41CPU architecture: 8CPU variant : 0x0CPU part : 0xd08CPU revision : 3最后的秘技是如何退出qemu,按Ctrl-a x,就可以退出了,显示:
/ # QEMU: TerminatedLinus反对在内核里加入调试器也不是没有道理,调试器只是手段,我们也不能舍本逐末,有了方便的调试手段就不去钻研原理和源码了。
我们希望在解剖kernel的时候能让大家有更丰富的视角,但是最近我们的目标还是理解内核的逻辑和代码。
请大家跟我一起沉下心来,我们一步一步开始探索之旅。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表