Linux內(nèi)核教程(1) - 道路千萬條匈子,調(diào)試最重要
從信號量說起
大家可能都學(xué)過操作系統(tǒng)河胎,在操作系統(tǒng)課上,在進程同步互斥中虎敦,圖靈獎獲得者Dijkstra的信號量Semphone游岳。
Linux中當(dāng)然也提供了semphone的實現(xiàn)政敢,用做最普通的睡眠鎖。所謂睡眠鎖胚迫,意思是如果有一個任務(wù)試圖去獲取一個被占用的信號量時喷户,會被推到等待隊列中,然后讓其睡眠晌区。這樣CPU資源就可以用來處理別的事情摩骨,實現(xiàn)資源的合理利用。這與一直等待的自旋鎖形成鮮明的對比朗若。當(dāng)占有信號量的任務(wù)運行結(jié)束后恼五,會喚醒隊列里等待的任務(wù),這個信號量也會被喚醒的任務(wù)占有哭懈。
針對于P和V兩種原語的Linux實現(xiàn)是down和up兩個操作灾馒。還有支持被中斷的down_interruptible,可被殺的down_killable遣总,不等待的down_trylock睬罗,帶超時的down_timeout,考慮得非常周到旭斥。
不僅如此容达,信號量也是支持讀/寫信號量分離的。
一切看起來很美好垂券,不是么花盐?
我們看看semphone的作者在semphone.c的開頭是如何寫的:
2 /*
3 * Copyright (c) 2008 Intel Corporation
4 * Author: Matthew Wilcox <willy@linux.intel.com>
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 enforce
9 * rules which allow code to be debugged more easily.
10 */
對于懶得看英文的同學(xué),我簡單翻譯一下算芯,如果只是獲取一次的鎖熙揍,建議改用mutex.h,這樣會使調(diào)試更容易届囚。
在Linux kernel中析砸,為了方便調(diào)試,基本上每種機制都有自己的調(diào)試宏,以CONFIG_DEBUG_*開頭。下面是我隨便搜的幾個:
比如自旋鎖扰才,就有CONFIG_DEBUG_SPINLOCK,打開之后,會增加追蹤如下例:
3492 static inline void
3493 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 next
3497 * task (which is an invalid locking op but in the case
3498 * of the scheduler it's an obvious special-case), so we
3499 * 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_SPINLOCK
3504 /* this is a valid case when another task releases the spinlock */
3505 rq->lock.owner = next;
3506 #endif
3507 }
很不幸蜡坊,semaphone不支持自動調(diào)試宏据忘,連受限的也做不到窍仰。
所以,信號量最好的場景是特別復(fù)雜的場景,比如跨內(nèi)核空間和用戶空間的復(fù)雜交互類的常空,反正調(diào)試也不靠這個攘残。
而對于內(nèi)核代碼中正常的使用病曾,應(yīng)該使用semaphone的受限版本mutex。
mutex是通過怎樣的自律來獲取自由的呢:
- 任何時間寄疏,只能有一個任務(wù)持有mutex
- 因為只有一個是牢,所以加鎖者必須負責(zé)給mutex解鎖
- 因為要負責(zé)解鎖驳棱,所以持有mutex的進程不得退出
- mutex不得用于中斷處理程序罚渐,也包括下半部谈息。不了解中斷和下半部原理的我們后面會介紹
- mutex不能復(fù)制
- mutex不能手動初始化
- mutex只能初始化一次
加了這些自律之后凛剥,我們終于可以為mutex寫一些調(diào)試用的功能了侠仇。不像自旋鎖只是在處理上加了幾條語句,mutex專門設(shè)計了調(diào)試專用函數(shù)來做這些事情:
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版犁享。
從信號量的例子我們就可以看到可調(diào)試性在內(nèi)核中的重要性余素。
同樣,有很多在內(nèi)核開發(fā)中被重點強調(diào)的內(nèi)容炊昆,其重要原因也是因為難以調(diào)試桨吊,比如棧溢出。
因為不像用戶空間的應(yīng)用程序容易退出凤巨,內(nèi)核本身是一直長期運行的视乐,這就導(dǎo)致內(nèi)核不得不面對內(nèi)存嚴(yán)重碎片化的情況,想要分配連續(xù)頁的內(nèi)存會越來越困難敢茁。而且用作棧的內(nèi)存也沒有辦法換出到輔助存儲中去佑淀,所以盡管棧溢出調(diào)試?yán)щy,也只能分配4k大小的棧卷要。所以就要求開發(fā)者以自律享受自由渣聚,盡量避免在棧上分量大的對象。一旦棧溢出了僧叉,產(chǎn)生的結(jié)果是難以預(yù)測的奕枝。
所以,我們把內(nèi)核的可調(diào)試性當(dāng)作第一要務(wù)來強調(diào)瓶堕。后面我們會調(diào)用各種手段來對內(nèi)核進行調(diào)試隘道,包括打日志,調(diào)試文件系統(tǒng),perf和ftrace等工具谭梗,甚至SystemTap和eBPF這樣的自動生成內(nèi)核模塊的腳本工具等忘晤,以及通過模擬器進行調(diào)試等各種手段。
編譯內(nèi)核
講了調(diào)試的重要性之后激捏,我們身體力行设塔,首先討論如何編譯內(nèi)核,如何在模擬器上跑起內(nèi)核远舅。
目前Linux的兩個最主要的應(yīng)用場景:一是跑在電腦上闰蛔,主要場景是給自己的電腦更換內(nèi)核;另一個是跑在嵌入式設(shè)備上图柏,比如手機等序六。
下載內(nèi)核
內(nèi)核源代碼地址可以在kernel.org上下載,比如我寫此文時最新的穩(wěn)定版是5.10.1蚤吹,我們就可以下載這個包:
wget -c https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.1.tar.xz
如果要下載源碼樹的話例诀,可以去clone kernel主線的代碼庫:
git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
在運行Linux的電腦上,我們可以通過包管理系統(tǒng)獲取到當(dāng)前使用的系統(tǒng)內(nèi)核的源代碼裁着。
比如在Ubuntu上繁涂,可以通過apt install linux-source來安裝源代碼:
root@iZ8vb39159pi4fttv8aaoyZ:/boot# apt search linux-source
Sorting... Done
Full Text Search... Done
linux-source/focal-updates,focal-updates,focal-security,focal-security,now 5.4.0.58.61 all [installed]
Linux kernel source with Ubuntu patches
linux-source-5.4.0/focal-updates,focal-updates,focal-security,focal-security,now 5.4.0-58.64 all [installed,automatic]
Linux kernel source for version 5.4.0 with Ubuntu patches
源代碼會下載到/usr/src目錄下。
編譯Ubuntu 20.04的內(nèi)核
下載之后跨算,我們將其解壓之后爆土,就可以進行編譯了。
安裝gcc, flex, bison, bc之類的常規(guī)操作就不多說了诸蚕,有遇到問題的請在評論區(qū)里提問步势。
編譯之前需要對內(nèi)核進行一些配置,比如調(diào)試信息等背犯。這些配置會寫在一個config文件中坏瘩。
以Ubuntu 20.04系統(tǒng)為例,在/boot目錄下可以看到一些config開頭的文件漠魏,比如config-5.4.0-58-generic倔矾,這些就是我們使用的Ubuntu系統(tǒng)的config文件。
這些配置項柱锹,以CONFIG_* = y這樣的形式來表示這個配置被支持哪自。而如果不支持的話,則把這個配置項用“#”注釋掉禁熏,并將=y改為is not set以利于理解壤巷。
比如我們上面討論的鎖的調(diào)試信息,在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的源碼為例瞧毙,看看如何去編譯內(nèi)核胧华。
進入/usr/src/linux-source-5.4.0目錄寄症,我們會看到linux-source-5.4.0.tar.bz2,將其解壓矩动。
進入解壓后的目錄有巧,將/boot/config-5.4.0-58-generic文件復(fù)制過來。
然后執(zhí)行
make ./config-5.4.0-58-generic
成功后悲没,運行make menuconfig篮迎,在字符圖形界面下可以進行一些手動的配置:
配置好之后,保存到.config中檀训,最后執(zhí)行make -j4來進行編譯柑潦。j后面是編譯開啟的線程數(shù)。
編譯成功后峻凫,會看到類似于下面的輸出:
Setup is 16380 bytes (padded to 16384 bytes).
System is 8697 kB
CRC 1a8c27e4
Kernel: arch/x86/boot/bzImage is ready (#1)
編好的kernel在arch/x86/boot/bzImage。
如果我們是想在模擬器上運行內(nèi)核的話览露,沒有Ubuntu給我準(zhǔn)備config荧琼。這也不怕,我們可以用x86_64的默認config差牛,在其基礎(chǔ)上進行修改命锄。首先我們需要設(shè)置下ARCH變量為x86_64,這樣不用寫路徑偏化,make就知道去哪里找x86_64_defconfig
export ARCH=x86_64
make x86_64_defconfig
然后make menuconfig和make不變脐恩。
成功后輸出如下:
Kernel: arch/x86/boot/bzImage is ready (#2)
編譯ARM64內(nèi)核
x86_64的搞定了,換成別的架構(gòu)就是照方抓藥了侦讨。只不過需要裝交叉編譯的工具鏈驶冒。
在Ubuntu上,我們可以通過apt install gcc-aarch64-linux-gnu來安裝支持ARM64的工具鏈韵卤。
安裝好之后骗污,我們配置下CROSS_COMPILE環(huán)境變量:
export CROSS_COMPILE=aarch64-linux-gnu-
針對于arm64,只有一個defconfig沈条,設(shè)好ARCH之后就可以自動找到了:
export ARCH=arm64
ARCH后面的名字以arch下的子目錄名為準(zhǔn)需忿,目前kernel支持的架構(gòu)如下:
- 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
我們執(zhí)行make defconfig
make defconfig
然后運行make -j8之類就可以了。
init程序
但是這樣編出來的內(nèi)核蜡歹,真的只是一個內(nèi)核屋厘,沒有任何shell之類的可以用。內(nèi)核啟動的最后月而,是要啟動一個init程序的汗洒,當(dāng)然我們也可以手寫一個。但是為了能有個shell景鼠,我們選擇用busybox的init.
我們?nèi)usybox.net去下載源碼:
wget -c https://busybox.net/downloads/busybox-1.32.0.tar.bz2
剛才編譯內(nèi)核時已經(jīng)設(shè)置好ARCH和CROSS_COMPILE了仲翎,正好busybox也能用到痹扇。
busybox沒那么多defconfig,上來就make menuconfig就好溯香。
我們只需要一個busybox程序鲫构,所以選擇Build static binary。
退出保存之后玫坛,執(zhí)行make -j4去編譯结笨。
最后,執(zhí)行make install湿镀,會安裝到_install目錄下炕吸。
準(zhǔn)備好了之后,我們需要給busybox的init準(zhǔn)備一個配置文件勉痴,一般是/etc/init.d/inittab赫模。這時候別說inittab了,我們連目錄還沒建呢蒸矛。
第一步:在_install目錄下創(chuàng)建etc,dev,mnt和etc/init.d/目錄:
mkdir etc
mkdir dev
mkdir mnt
mkdir -p etc/init.d/
mkdir加-p參數(shù)的意思是如果父目錄沒有創(chuàng)建瀑罗,則創(chuàng)建之。
第二步:創(chuàng)建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-4
tty2::askfirst:-/bin/sh
tty3::askfirst:-/bin/sh
tty4::askfirst:-/bin/sh
# /sbin/getty invocations for selected ttys
tty4::respawn:/sbin/getty 38400 tty5
tty5::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之類的登陸界面,直接進入系統(tǒng)绑青,所以加一條直接調(diào)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目錄下創(chuàng)建rcS文件颅停,如下:
mkdir -p /proc
mkdir -p /tmp
mkdir -p /sys
mkdir -p /mnt
/bin/mount -a
mkdir -p /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
/proc是內(nèi)核向進程發(fā)送消息的機制。比如cat /proc/cpuinfo可以查看cpu運行信息掠拳,而cat /proc/meminfo是內(nèi)存信息癞揉。
/sys與/proc類似,也是內(nèi)核用于展示信息的虛擬文件系統(tǒng)溺欧,于2.5版引入喊熟,主要展示設(shè)備樹。
/tmp是臨時目錄
/mnt是掛載點
/dev/pts是通過ssh等遠程登陸時創(chuàng)建的控制臺設(shè)備文件
mdev是busybox提供的管理熱插拔的程序姐刁。
BusyBox v1.32.0 (2020-12-15 16:24:44 CST) multi-call binary.
Usage: mdev [-s] | [-df]
mdev -s is to be run during boot to scan /sys and populate /dev.
mdev -d[f]: 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賦給可執(zhí)行權(quán)限壁拉。
有同學(xué)問了谬俄,mount -a是掛載啥的?這是按照/etc/fstab來mount所有里面寫的文件系統(tǒng)的弃理,我們馬上就寫一個fstab溃论。
參照busybox-1.32.0/examples/bootfloppy/etc/init.d/rcS,mount -a調(diào)用fstab也是busybox的傳統(tǒng)操作痘昌。
第四步, 創(chuàng)建fstab文件
內(nèi)容如下:
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /dev tmpfs defaults 0 0
debugfs /sys/kernel/debug debugfs defaults 0 0
到此為止钥勋,欠busybox init的連環(huán)債算是還清了,下面我們就以此文件系統(tǒng)去編譯內(nèi)核辆苔。
在qemu中運行
有了busybox的init程序算灸,我們重新編譯下內(nèi)核,在menuconfig中將剛才的_install目錄設(shè)置進去驻啤,在General配置的Init RAM filesystem中:
將我們剛才準(zhǔn)備好的_install目錄的路徑設(shè)置進去就好菲驴。
配置好保存之后,make -j4開始編譯骑冗。
經(jīng)過一段歡快的編譯谢翎,生成Image:
...
LD vmlinux.o
MODPOST vmlinux.symvers
MODINFO modules.builtin.modinfo
GEN modules.builtin
LD .tmp_vmlinux.kallsyms1
KSYMS .tmp_vmlinux.kallsyms1.S
AS .tmp_vmlinux.kallsyms1.S
LD .tmp_vmlinux.kallsyms2
KSYMS .tmp_vmlinux.kallsyms2.S
AS .tmp_vmlinux.kallsyms2.S
LD vmlinux
SORTTAB vmlinux
SYSMAP System.map
MODPOST Module.symvers
OBJCOPY arch/arm64/boot/Image
GZIP arch/arm64/boot/Image.gz
下面我們調(diào)用qemu來運行這個內(nèi)核,qemu可以通過apt來安裝沐旨。我們模擬4核A72,16G內(nèi)存:
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 -a
Linux (none) 5.10.1 #4 SMP PREEMPT Wed Dec 16 18:19:47 CST 2020 aarch64 GNU/Linux
我們再看看cpuinfo:
/ # cat /proc/cpuinfo
processor : 0
BogoMIPS : 125.00
Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 1
BogoMIPS : 125.00
Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 2
BogoMIPS : 125.00
Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
processor : 3
BogoMIPS : 125.00
Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
CPU implementer : 0x41
CPU architecture: 8
CPU variant : 0x0
CPU part : 0xd08
CPU revision : 3
最后的秘技是如何退出qemu,按Ctrl-a x良风,就可以退出了谊迄,顯示:
/ # QEMU: Terminated
恭喜,一個可玩的內(nèi)核已經(jīng)可以工作啦烟央。
懂源碼是根本
Linus反對在內(nèi)核里加入調(diào)試器也不是沒有道理统诺,調(diào)試器只是手段,我們也不能舍本逐末疑俭,有了方便的調(diào)試手段就不去鉆研原理和源碼了粮呢。
我們希望在解剖kernel的時候能讓大家有更豐富的視角,但是最近我們的目標(biāo)還是理解內(nèi)核的邏輯和代碼钞艇。
請大家跟我一起沉下心來啄寡,我們一步一步開始探索之旅。