操作系统实验
在Apple Silicon的macOS平台跨平台编译x86_64平台的linux
准备docker linux环境,装一个fedora(w
)。
docker pull fedora:latest
docker run --privileged -itv ~/workspace:/root/workspace --name linux_kernel fedora:latest
安装必要工具
dnf install vim make wget xz bc flex bison diffutils cpio perl -y
安装必要的库
dnf install elfutils-libelf-devel openssl-devel -y
安装跨平台编译的工具链
dnf install binutils-x86_64-linux-gnu gcc-c++-x86_64-linux-gnu gcc-x86_64-linux-gnu -y
跨平台编译
export ARCH=x86_64
export CROSS_COMPILE=x86_64-linux-gnu-
make -j8
编译busybox
dnf install bzip2 -y
dnf install ncurses-devel glibc-static -y
make menuconfig
Settings --->
[*] Build static binary (no shared libs)
目前编译遇到找不到byteswap.h的问题,暂时没有解决。fatal error: byteswap.h: No such file or directory
export C_INCLUDE_PATH=/usr/include:$C_INCLUDE_PATH
#!/bin/busybox sh
bb=/bin/busybox
for cmd in $($bb --list); do
$bb ln -s $bb /bin/$cmd
done
mkdir -p /tmp
mkdir -p /proc
mkdir -p /sys
mkdir -p /dev
mount -t proc none /proc
mount -t sysfs none /sys
mknod /dev/tty c 4 1
mknod /dev/ttyS0 c 4 64
setsid /bin/sh < /dev/tty > /dev/tty 2>&1
编译最小的hello程序
#include
.global _start
_start:
movq $SYS_write, %rax
movq $1, %rdi # fd = 1; stdin
movq $string, %rsi
movq $(end-string), %rdx
syscall
movq $SYS_exit, %rax
movq $0, %rdi # status = 0
syscall
string:
.ascii "hello\n"
end:
打包根文件系统
mkdir initrd
cp hello initrd
cd initrd
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz
运行
qemu-system-x86_64 -kernel bzImage -initrd initramfs.cpio.gz -append "console=ttyS0 quiet rdinit=/hello" -serial mon:stdio
[ 1.265790] fail to initialize ptp_kvm
hello
[ 1.489397] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000000
[ 1.489811] CPU: 0 PID: 1 Comm: hello Not tainted 6.5.5 #2
[ 1.489907] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.16.2-0-gea1b7a073390-prebuilt.qemu.org 04/01/2014
[ 1.490154] Call Trace:
[ 1.490691]
[ 1.490865] dump_stack_lvl+0x36/0x50
[ 1.491122] panic+0x174/0x320
[ 1.491174] do_exit+0x959/0xac0
[ 1.491219] __x64_sys_exit+0x16/0x20
[ 1.491277] do_syscall_64+0x3f/0x90
[ 1.491325] entry_SYSCALL_64_after_hwframe+0x6e/0xd8
[ 1.491485] RIP: 0033:0x40102e
[ 1.491697] Code: 00 00 00 48 c7 c7 01 00 00 00 48 c7 c6 2e 10 40 00 48 c7 c2 06 00 00 00 0f 05 48 c7 c0 3c 00 00 00 48 c7 c7 00 00 00 00 0f 05 <68> 65 6c 6c 6f 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[ 1.491944] RSP: 002b:00007ffca8eb4a70 EFLAGS: 00000202 ORIG_RAX: 000000000000003c
[ 1.492051] RAX: ffffffffffffffda RBX: 0000000000000000 RCX: 000000000040102e
[ 1.492136] RDX: 0000000000000006 RSI: 000000000040102e RDI: 0000000000000000
[ 1.492239] RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000000
[ 1.492329] R10: 0000000000000000 R11: 0000000000000202 R12: 0000000000000000
[ 1.492424] R13: 0000000000000000 R14: 0000000000000000 R15: 0000000000000000
[ 1.492678]
[ 1.493120] Kernel Offset: 0x11400000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbfffffff)
[ 1.493397] ---[ end Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000000 ]---
关于根文件系统
如果initrd镜像文件只有如下两个文件打包
.
├── bin
│ └── busybox
└── init
1 directory, 2 files
其中init
内容为
#!/bin/busybox sh
bb=/bin/busybox
for cmd in $($bb --list); do
$bb ln -s $bb /bin/$cmd
done
sh
qemu-system-x86_64 -kernel bzImage -initrd init.cpio.gz -append "console=ttyS0 queit rdinit=/init" -serial mon:stdio
运行之后。可以看到,一没有任何挂载点;二内核会自动创建/root
和/dev
两个目录,/dev
目录下内核只创建了一个console
sh: can't access tty; job control turned off
/ # mount
mount: no /proc/mounts
/ # ls -l /
total 4
drwxr-xr-x 2 0 0 8000 Sep 25 00:15 bin
drwxr-xr-x 2 0 0 60 Sep 23 13:48 dev
-rwxr-xr-x 1 0 0 305 Sep 25 00:15 init
drwx------ 2 0 0 40 Sep 23 13:48 root
/ # ls -l /root
total 0
/ # ls -l /dev
total 0
crw------- 1 0 0 5, 1 Sep 25 00:16 console
/ #
做为对比
#!/bin/busybox sh
bb=/bin/busybox
for cmd in $($bb --list); do
$bb ln -s $bb /bin/$cmd
done
mkdir -p /proc
mkdir -p /sys
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
sh
sh: can't access tty; job control turned off
/ # mount
rootfs on / type rootfs (rw,size=47948k,nr_inodes=11987)
none on /proc type proc (rw,relatime)
none on /sys type sysfs (rw,relatime)
none on /dev type devtmpfs (rw,relatime,size=47948k,nr_inodes=11987,mode=755)
/ # ls -l /
total 4
drwxr-xr-x 2 0 0 8000 Sep 25 00:20 bin
drwxr-xr-x 7 0 0 2320 Sep 25 00:20 dev
-rwxr-xr-x 1 0 0 329 Sep 25 00:20 init
dr-xr-xr-x 99 0 0 0 Sep 25 00:20 proc
drwx------ 2 0 0 40 Sep 23 13:48 root
dr-xr-xr-x 12 0 0 0 Sep 25 00:20 sys
/ # ls -l /dev
total 0
crw-r--r-- 1 0 0 10, 235 Sep 25 00:20 autofs
drwxr-xr-x 2 0 0 80 Sep 25 00:20 bsg
crw------- 1 0 0 5, 1 Sep 25 00:20 console
drwxr-xr-x 3 0 0 60 Sep 25 00:20 cpu
crw------- 1 0 0 10, 62 Sep 25 00:20 cpu_dma_latency
crw-rw-rw- 1 0 0 1, 7 Sep 25 00:20 full
crw------- 1 0 0 10, 228 Sep 25 00:20 hpet
crw------- 1 0 0 10, 183 Sep 25 00:20 hwrng
drwxr-xr-x 2 0 0 100 Sep 25 00:20 input
crw-r--r-- 1 0 0 1, 11 Sep 25 00:20 kmsg
crw------- 1 0 0 10, 237 Sep 25 00:20 loop-control
brw------- 1 0 0 7, 0 Sep 25 00:20 loop0
...
brw------- 1 0 0 7, 7 Sep 25 00:20 loop7
drwxr-xr-x 2 0 0 60 Sep 25 00:20 mapper
crw------- 1 0 0 1, 1 Sep 25 00:20 mem
crw-rw-rw- 1 0 0 1, 3 Sep 25 00:20 null
crw------- 1 0 0 10, 144 Sep 25 00:20 nvram
crw------- 1 0 0 1, 4 Sep 25 00:20 port
crw-rw-rw- 1 0 0 5, 2 Sep 25 00:20 ptmx
crw-rw-rw- 1 0 0 1, 8 Sep 25 00:20 random
crw------- 1 0 0 10, 242 Sep 25 00:20 rfkill
crw------- 1 0 0 254, 0 Sep 25 00:20 rtc0
brw------- 1 0 0 8, 0 Sep 25 00:20 sda
brw------- 1 0 0 8, 1 Sep 25 00:20 sda1
crw------- 1 0 0 21, 0 Sep 25 00:20 sg0
crw------- 1 0 0 21, 1 Sep 25 00:20 sg1
crw------- 1 0 0 10, 231 Sep 25 00:20 snapshot
drwxr-xr-x 2 0 0 80 Sep 25 00:20 snd
brw------- 1 0 0 11, 0 Sep 25 00:20 sr0
crw-rw-rw- 1 0 0 5, 0 Sep 25 00:20 tty
crw------- 1 0 0 4, 0 Sep 25 00:20 tty0
...
crw------- 1 0 0 4, 9 Sep 25 00:20 tty9
crw------- 1 0 0 4, 64 Sep 25 00:20 ttyS0
...
crw------- 1 0 0 4, 67 Sep 25 00:20 ttyS3
crw-rw-rw- 1 0 0 1, 9 Sep 25 00:20 urandom
crw------- 1 0 0 250, 0 Sep 25 00:20 usbmon0
crw------- 1 0 0 7, 0 Sep 25 00:20 vcs
crw------- 1 0 0 7, 1 Sep 25 00:20 vcs1
crw------- 1 0 0 7, 128 Sep 25 00:20 vcsa
crw------- 1 0 0 7, 129 Sep 25 00:20 vcsa1
crw------- 1 0 0 7, 64 Sep 25 00:20 vcsu
crw------- 1 0 0 7, 65 Sep 25 00:20 vcsu1
crw------- 1 0 0 10, 63 Sep 25 00:20 vga_arbiter
crw-rw-rw- 1 0 0 1, 5 Sep 25 00:20 zero
这两次都提示sh: can't access tty; job control turned off
解决方法是
#!/bin/busybox sh
bb=/bin/busybox
for cmd in $($bb --list); do
$bb ln -s $bb /bin/$cmd
done
mkdir -p /proc
mkdir -p /sys
mount -t proc none /proc
mount -t sysfs none /sys
mknod /dev/tty c 4 1
setsid /bin/sh < /dev/tty > /dev/tty 2>&1
最初的/dev
是哪来的?
在验证的过程中产生了一个疑问:为什么/dev
目录在mount -t devtmpfs none /dev
前会被内核创建且已经有内容?是谁创建的?
验证方法是写一个列出根目录下所有文件的程序,把这个程序替换成init
。看看linux启动后列出的内容
#include
#include
#include
#include
void listdir(char *path, char *prefix) {
DIR *dir;
struct dirent *entry;
struct stat statbuf;
char filepath[256];
if ((dir = opendir(path)) == NULL) {
perror("opendir");
return;
}
chdir(path);
while ((entry = readdir(dir)) != NULL) {
lstat(entry->d_name, &statbuf);
if (S_ISDIR(statbuf.st_mode)) {
if (strcmp(".", entry->d_name) == 0 || strcmp("..", entry->d_name) == 0) {
continue;
}
sprintf(filepath, "%s/%s", prefix, entry->d_name);
printf("Directory: %s\n", filepath);
listdir(entry->d_name, filepath);
} else {
sprintf(filepath, "%s/%s", prefix, entry->d_name);
printf("File: %s\n", filepath);
}
}
chdir("..");
closedir(dir);
}
int main(int argc, char *argv[]) {
listdir("/", "/");
return 0;
}
用gcc将之静态编译成init
文件,替换掉initrd里的init
进程,运行发现,打出结果为
File: //init
Directory: //bin
File: //bin/busybox
Directory: //root
Directory: //dev
File: //dev/console
可以看到,就算intrd镜像文件里没有/root
、/dev
、/dev/console
这三个文件,但在init
进程运行时这三个文件就已经出现了。这说明是内核创建的。
最后经过定位基于linux-6.5.5,在其内核代码位置为linux-6.5.5/usr/default_cpio_list
的文件里有创建这三个文件的命令
# SPDX-License-Identifier: GPL-2.0-only
# This is a very simple, default initramfs
dir /dev 0755 0 0
nod /dev/console 0600 0 0 c 5 1
dir /root 0700 0 0
在里面随便加条命令创建个文件,编译后,在init
运行时会发现创建的文件会出现。
至此谁创建的这些文件捊清楚了,那为什么创建呢?原因不难猜测,是方便我们init
进程输入输出。
在 Linux 系统启动时,init
进程是第一个用户空间进程,它负责启动并管理系统中的其他进程。而在 init
进程启动之前,内核需要提供一个控制台(console)设备,以便在用户空间中输出日志和错误信息。
/dev/console
是一个特殊的设备文件,它可以被用作系统的控制台。在 Linux 系统启动时,内核会在内存中创建一个虚拟控制台,然后将其绑定到 /dev/console
设备文件上。这样,在用户空间中可以通过读取和写入 /dev/console
文件来访问控制台。
在创建 /dev/console 设备文件之前,内核无法将虚拟控制台绑定到该设备文件上。这意味着在用户空间中无法访问控制台,无法输出日志和错误信息。因此,在 Linux 系统启动时,必须在 init
进程启动之前创建 /dev/console
设备文件,以便访问控制台。
需要注意的是,/dev/console
设备文件只是一个设备文件,它并不是真正的物理设备。在实际的硬件上,控制台可能是一个串口、VGA 显示器或者其他设备。但是,在 Linux 系统中,/dev/console
设备文件是一个虚拟设备,它与内核中的虚拟控制台绑定在一起,以提供与实际控制台相同的功能。
因此,在 Linux 系统启动时,创建 /dev/console
设备文件是非常重要的,它为用户空间提供了访问控制台的接口,以便输出日志和错误信息。
那么,为什么内核不直接在启动init进程前,自动执行 mount -t devtmpfs none /dev
呢?
内核在启动 init 进程前不会自动执行 mount -t devtmpfs none /dev
的原因是,内核设计为尽可能地保持简单和通用。在启动过程中,内核的主要任务是加载 initramfs(或 initrd)并启动 init 进程。具体的文件系统挂载和其他初始化任务通常由用户空间的 init 程序处理,以便根据不同的系统配置和需求进行定制。
虽然内核确实在启动过程中为控制台设备(如 /dev/console)创建了一个设备节点,但这主要是为了确保在 init 进程启动之前有一个可用的控制台输入/输出设备。这是一个基本的需求,适用于几乎所有Linux系统。
然而,挂载 devtmpfs 文件系统并创建完整的设备节点属于更高级的初始化任务,可能需要根据特定的系统需求进行定制。例如,某些嵌入式系统可能不需要完整的设备节点,而只需要一些特定的设备。在这种情况下,init 程序可以根据需要创建所需的设备节点,而不是挂载整个 devtmpfs 文件系统。
此外,将这些任务留给用户空间的 init 程序可以让系统管理员和发行版维护者更容易地调整和定制系统的启动过程,而无需修改内核本身。
总之,内核在启动 init 进程前不会自动执行 mount -t devtmpfs none /dev
,因为这些任务通常由用户空间的 init 程序处理,以便根据不同的系统配置和需求进行定制。将这些任务留给 init 程序也使得系统的启动过程更容易进行调整和定制。
如果启动linux时指定根设备为/dev/sda1 那么从initrd指定的文件系统镜像里的根目录/到代表sda的根目录/的整个目录变动或者挂载流程是什么样的?
当在启动Linux时指定根设备为/dev/sda1,并使用initrd(Initial RAM Disk)时,从initrd指定的文件系统镜像里的根目录/到代表sda的根目录/的整个目录变动或者挂载流程如下:
在启动时,bootloader会加载内核和initrd(Initial RAM Disk)映像到内存中。
内核启动后,会将initrd映像解压缩并挂载到一个临时的根文件系统(通常是一个内存文件系统,如tmpfs)。
内核会在临时根文件系统中运行一个init程序,该程序负责加载必要的内核模块、检测硬件、设置网络等。
init程序会检测指定的根设备(/dev/sda1)并将其挂载到临时根文件系统的一个子目录下,例如/mnt/root。
init程序将会执行一个pivot_root操作,将临时根文件系统的/mnt/root目录替换为新的根目录/,并将原来的临时根文件系统挂载到新根目录的一个子目录下,例如/old-root。这样,/dev/sda1设备上的文件系统就成为了新的根文件系统。
init程序会执行一个chroot操作,将当前工作目录切换到新的根目录下,并运行新根文件系统中的init程序(例如/sbin/init或/systemd)。
新的init程序会继续完成系统启动过程,包括挂载其他文件系统、启动服务等。
在系统运行过程中,新的init程序可能会卸载原来的临时根文件系统,释放内存资源。
通过上述过程,内核会将initrd指定的文件系统镜像里的根目录/替换为/dev/sda1设备上的根目录/,从而完成系统启动。
init执行pivot_root后,文件系统的具体变动步骤是什么样的?
pivot_root
是一个系统调用,用于在 Linux 启动过程中将当前根文件系统切换到新的根文件系统。在 init 执行 pivot_root
之后,文件系统的具体变动步骤如下:
init
程序在临时根文件系统(例如 tmpfs)中运行,此时已经将目标根设备(如 /dev/sda1)挂载到临时根文件系统的一个子目录下,例如 /mnt/root。init
程序执行pivot_root
系统调用,将当前的根文件系统(临时根文件系统)切换到新的根文件系统(挂载在 /mnt/root 的根设备上的文件系统)。具体调用形式如下:
pivot_root("/mnt/root", "/mnt/root/old-root")
在
pivot_root
调用完成后,新的根文件系统(/dev/sda1)成为了当前的根目录(/)。原始的临时根文件系统现在被挂载到新根文件系统的一个子目录下(/old-root)。为了避免在新根文件系统中保留对临时根文件系统的引用,
init
程序会将所有其他挂载点(如 /proc、/sys、/dev 等)从临时根文件系统移动到新的根文件系统。例如,将 /old-root/proc 移动到 /proc,将 /old-root/sys 移动到 /sys,将 /old-root/dev 移动到 /dev 等。在完成所有挂载点的移动后,
init
程序会使用chroot
系统调用将当前工作目录切换到新的根文件系统。这样,所有后续操作都将在新的根文件系统中进行。最后,
init
程序可能会卸载原始的临时根文件系统(/old-root),以释放内存资源。这可以通过umount
命令或umount2
系统调用完成。
通过以上步骤,文件系统会在 init
执行 pivot_root
后发生变动,从临时根文件系统切换到目标根设备上的根文件系统。
在第3步后能访问/mnt/root/old-root/mnt/root这个目录吗?里面是什么内容?
在第3步执行 pivot_root
后,新的根文件系统(挂载在 /mnt/root
的根设备)成为了当前的根目录(/
),原始的临时根文件系统被挂载到新根文件系统的一个子目录下(/old-root
)。
此时,你可以访问 /mnt/root/old-root/mnt/root
这个目录。但是,这个目录实际上是一个环回引用,它指向新的根文件系统的根目录(/
)。换句话说,访问 /mnt/root/old-root/mnt/root
相当于访问 /
。
在这个目录下,你将看到新根文件系统的内容,包括各个子目录(如 /bin
, /sbin
, /etc
, /home
等)和文件。
能简述pivot_root在内核是如何实现的吗?
pivot_root
是一个 Linux 系统调用,其主要目的是在系统启动过程中将当前的根文件系统切换到新的根文件系统。pivot_root
的内核实现较为复杂,但以下是一个简化的概述:
首先,
pivot_root
系统调用接收两个参数:新的根目录(new_root)和旧的根目录在新根文件系统中的挂载点(put_old)。在内核中,pivot_root
实现通过sys_pivot_root
函数定义,该函数接收两个参数:const char __user *new_root
和const char __user *put_old
。sys_pivot_root
函数首先对传入的参数进行一些基本的验证,例如检查新旧根目录是否相同、是否位于同一文件系统等。如果验证通过,函数将继续执行。接下来,
sys_pivot_root
函数会查找新旧根目录对应的dentry
(目录项)和vfsmount
(虚拟文件系统挂载点)结构。这些结构包含了文件系统层次结构中目录和挂载点的详细信息。在找到新旧根目录的
dentry
和vfsmount
结构后,sys_pivot_root
函数会将当前进程的根目录(current->fs->root
)设置为新的根目录。这实际上将新的根文件系统设置为当前进程的根目录。接下来,
sys_pivot_root
函数会将旧的根文件系统挂载到新根文件系统中的指定挂载点(put_old
)。这是通过将旧根目录的vfsmount
结构链接到新根目录的vfsmount
结构来实现的。最后,
sys_pivot_root
函数会将所有其他进程的根目录和当前工作目录从旧的根文件系统切换到新的根文件系统。这是通过遍历进程列表并更新每个进程的fs_struct
结构中的root
和pwd
字段来实现的。
通过以上步骤,pivot_root
系统调用在内核中实现了根文件系统的切换。需要注意的是,这里的描述是一个简化的概述,实际的内核实现可能更加复杂,涉及到许多内核数据结构和函数调用。
总结流程
- 内核会首先注册一个
sysfs
文件系统,并用全局变量sysfs_mnt
保存其挂载点的挂载项。 - 内核会注册一个
rootfs
文件系统,并将init_task
的root
、pwd
都指向rootfs
的根目录/
- 加载initrd文件,并将所有文件创建在
rootfs
的/
中 - 如果initrd没有
/root
、/dev
、/dev/console
这三个文件,内核会默认创建(以6.5.5为例,其内核代码位置为linux-6.5.5/usr/default_cpio_list
,这个文件貌似是用来初始化initramfs
的?) - 执行
init
程序,init
程序挂载三个文件系统:mount -t proc none /proc
、mount -t sysfs none /sys
、mount -t devtmpfs none /dev
init
程序会检测指定的根设备(/dev/sda1
)并将其挂载到临时根文件系统的一个子目录下,例如/mnt/root
init
程序将会执行一个pivot_root
操作,将临时根文件系统的/mnt/root
目录替换为新的根目录/
,并将原来的临时根文件系统挂载到新根目录的一个子目录下,例如/old-root
。这样/dev/sda1
设备上的文件系统就成为了新的根文件系统init
程序会执行一个chroot
操作,将当前工作目录切换到新的根目录下,并运行新根文件系统中的init
程序(例如/sbin/init
或/systemd
)- 新的
init
程序会继续完成系统启动过程,包括挂载其他文件系统、启动服务等,在系统运行过程中,新的init
程序可能会卸载原来的临时根文件系统,释放内存资源