现代生活环境中,各式各样的电子设备包围着我们每个人。但凡稍微复杂一点的设备,比如一台电视机、路由器或智能手机,它很有可能运行的就是Linux操作系统,这种想法一直在我脑海出现。
让我更加困惑的是,在这些设备或服务器上运行的Linux操作系统的内核居然都是由同一个源码构建而成的,这些源码被放在了一个叫做kernel.org网站的仓库中。
如此多样的设备上运行的操作系统都是由相同的源代码组合而成的!当然,这种说法有点片面,实际上内核通常由特定Linux发行版的开发人员以及特定设备的开发人员扩展和修改的,但内核中却有很多通用的源代码。
我一直想靠自己从源码开始构建一个Linux操作系统,但过程实在是太复杂,而且越做越乱,究其原因是我有很多没有掌握的领域。终于在某一时刻,我积累了足够的知识,现在我可以实现我的梦想了。因此,在这篇文章中,我将展示如何在计算机上从源码编译和运行一个极简的Linux。
虽然它不会具备所有的功能,但它拥有最重要的东西——命令行界面。相信我,在真正的计算机上获得一个可以工作的Linux命令行界面将是一种不可思议的体验。
令人惊讶的是,获得Linux命令行所需的最小集合只有两个文件:Linux内核文件和根目录文件系统初始镜像文件。显然,这需要一个引导装载程序来加载这两个文件并启动内核的运行,必要时还可以向它传递初始根目录文件系统的镜像和其他参数。
1
极简Linux操作系统
为了能以某种方式使用此操作系统,你需要四个组件:
引导程序(Bootloader):是一个特殊的程序,它允许处理器去执行位于操作系统内核文件中的机器指令。
内核(Kernel):此程序的代码包含:
  • 用于处理器可以使用的各种物理I/O设备(设备驱动程序)的抽象;
  • 用于存储的数据结构的抽象(文件系统);
  • 程序指令(进程、线程)的时间分离抽象;
  • 其他抽象。
由于内核的存在,应用程序的开发人员通常不关心计算机上安装了哪种显卡、键盘或硬盘。他们只编写与I/O设备、进程、文件、套接字等工作的代码。
初始根目录文件系统(Initial root filesystem):此文件系统是必须的,以便内核能够在引导操作系统启动时加载所必须的文件。其中最重要的就是Linux内核模块,这些模块没有包含在内核文件中,而是用于进一步加载,以及在操作系统启动时创建第一个进程文件(init)。
操作系统内核接口的一组实用程序集:它允许我们使用在操作系统内核中发现的抽象。根据操作系统所运行的设备的复杂性和用途的不同,这个集合也有所不同。这些实用程序定义功能和用户接口。例如,路由器会有一个程序;手机会有另一个程序;个人电脑还会有一个程序。
2
加载Linux操作系统
在不同的架构和计算机上,Linux操作系统的引导都可能不同,但对于x86架构的计算机,大多数情况下它引导是这样的:
  1. 电脑开机。
  2. BIOS或UEFI在计算机上找到操作系统引导程序,并向其传输控制指令。
  3. 操作系统引导装载程序将Linux内核文件和初始文件系统镜像文件(initrd文件)加载到RAM中。
  4. 操作系统引导装载程序将控制传输到Linux操作系统内核。
  5. 操作系统内核执行初始化。
  6. 操作系统内核访问初始文件系统镜像中的文件(加载镜像)。
  7. 内核在初始文件系统中查找init文件,并基于该文件启动第一个用户进程。
  8. init进程装载一个已经持久化的文件系统,继续初始化操作系统,从Linux文件系统的根目录转移到已装载的文件系统,并启动初始化所需的其他进程。
3
Linux发行版
发行版是一个Linux内核以及安装在计算机或设备上的一组库、实用组件和程序。
目前,各种发行版在市场上流行。我们可以从DistroWatch[1]上查看这些清单。
现代的Linux发行版通常以ISO镜像的形式发布,并允许我们安装更新和额外的程序(包),但我们现在要做的是一个极简的发行版,因此对我们没什么意义。
关于如何从头开始构建Linux发行版的最完整的说明,可以参考这里[2]。构建Linux发行版是一个有趣的过程,可以让我们学习到很多新的东西,但它非常耗时,这意味着我们需要很大的意志力才能从头到尾完成。
我试图将创建分发套件的简化过程做到极致:我们不会挂载一个永久的文件系统,但作为一个init文件,我们将使用一个脚本文件,该脚本文件将执行最小初始化并启动sh (shell)。
4
操作系统引导加载
多年来,Linux已经可以移植到许多硬件平台上。然而,每个平台的Linux引导都是不同的。对于x86可能有如下不同:
  1. 它会被用来引导BIOS或者UEFI吗?
  2. BIOS或UEFI将在哪个存储设备上寻找引导程序(硬盘,闪存驱动器U盘,光驱,网络)?
  3. 如何标记硬盘或U盘(MBR或GPT)?
  4. 内核文件和带有初始根文件系统镜像的文件(称为initrd)位于什么媒介上,在哪种文件系统(FAT、NTFS、EXT、CDFS,等等)中?
5
初始根文件系统的结构
初始的根文件系统包含Linux后续操作所需的最小量级的目录和文件。在我们的例子中,这些是bin、dev、proc和sys目录。bin目录包含了使用Linux内核的实用工具。
6
实用程序包
极简版的Linux是一个内核和一组命令行实用程序包。内核和命令行实用程序由不同的程序员团队开发。
最常见的集合是:
  • GNU Core Utils;
  • BusyBox。
BusyBox结构简单,占用磁盘空间少,常用于嵌入式设备。为了简单起见,我们将使用这个。
7
创建Linux构建环境
这听起来很矛盾,Linux通常被编译成Linux。要做到这一点,我们需要一个Linux操作系统,其中包含允许我们构建Linux内核的程序和一组用于构建的实用程序包。
例如,在Ubuntu 22.10上,我们需要安装以下软件包:make、build-essential、bc、bison、flex、libssl-dev、libelf-dev、wget、cpio、fdisk、extlinux、dosfstool和qemu-system-x86。
注意,对于其他版本的Linux系统,包的集合可能不同。
8
Ubuntu 22.10上构建极简Linux
安装所需的程序包。
cd
 ~

$ mkdir -p simple-linux/build/sources

$ mkdir -p simple-linux/build/downloads

$ mkdir -p simple-linux/build/out

$ mkdir -p simple-linux/linux

$ sudo apt update

$ sudo apt install --yes make build-essential bc bison flex libssl-dev libelf-dev wget cpio fdisk extlinux dosfstools qemu-system-x86
从Linux Kernel上下载源码,再从Internet上下载BusyBox。
cd
 simple-linux/build

$ wget -P downloads  https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.79.tar.xz

$ wget -P downloads https://busybox.net/downloads/busybox-1.35.0.tar.bz2
将压缩包解压为源码。
$ tar -xvf downloads/linux-5.15.79.tar.xz -C sources

$ tar -xjvf downloads/busybox-1.35.0.tar.bz2 -C sources
为Linux内核构建BusyBox的二进制文件。这个过程需要一些时间,大约10分钟或更多,所以耐心等待就好。
cd
 sources/busybox-1.35.0

$ make defconfig

$ make LDFLAGS=-static

$ cp busybox ../../out/

cd
 ../linux-5.15.79

$ make defconfig

$ make -j8 || 
exit
$ cp arch/x86_64/boot/bzImage ~/simple-linux/linux/vmlinuz-5.15.79
创建init文件。
$ mkdir -p ~/simple-linux/build/initrd

cd
 ~/simple-linux/build/initrd

$ vi init
除了vim编辑器,还可以使用其他的文本编辑器,例如getit。
init文件:
#! /bin/sh
mount -t sysfs sysfs /sys

mount -t proc proc /proc

mount -t devtmpfs udev /dev

sysctl -w kernel.printk=
"2 4 1 7"
/bin/sh

poweroff -f
创建目录和文件结构。
$ chmod 777 init

$ mkdir -p bin dev proc sys

cd
 bin

$ cp ~/simple-linux/build/out/busybox ./

for
 prog 
in
 $(./busybox --list); 
do
 ln -s /bin/busybox 
$prog
done
将该文件目录在initrd文件中,这是我们的cpio归档文件。
cd
 ..

$ find . | cpio -o -H newc > ~/simple-linux/linux/initrd-busybox-1.35.0.img
从QEMU模拟器中启动镜像的制作。
cd
 ~/simple-linux/linux

$ qemu-system-x86_64 -kernel vmlinuz-5.15.79 -initrd initrd-busybox-1.35.0.img -nographic -append 
'console=ttyS0'
9
U创建一个引导镜像
如果我们想在真正的硬件上运行Linux,那么可能最简单的方法是创建一个镜像,放在引导驱动器上并启动它。在我看来,创建这样的镜像并从驱动器启动是最困难的过程,需要我们具备更高阶的知识。
创建镜像时,需要做以下几个决定:
  • 哪个会启动引导(BIOS或UEFI)?
  • 将使用哪个驱动器(光盘驱动器、U盘还是硬盘驱动器)?
  • 如何对驱动器(MBR、GPT)进行分区?
  • 使用哪个引导程序?
  • 将使用什么文件系统,Linux和引导加载程序文件将放在哪里?
我用了一个安装了MBR和EXTLINUX引导装载程序的U盘,文件放在了一个FAT32分区中。BIOS为我启动了引导过程(如果你的计算机有UEFI BIOS,则为选择Legacy boot选项)。
创建可引导U盘镜像的方法如下:
创建一个镜像文件。
$ dd if=/dev/zero of=boot-disk.img bs=1024K count=50
在镜像文件里创建一个启动分区。
echo"type=83,bootable" | sfdisk boot-disk.img
在boot-disk.img文件,给启动分区上设置回环设备。
$ losetup -D

$ LOOP_DEVICE=$(losetup -f)

$ losetup -o $(expr 512 \* 2048) 
${LOOP_DEVICE}
 boot-disk.img
给回环设备创建一个文件系统。
$ mkfs.vfat ${LOOP_DEVICE}
挂载回环设备。
$ mkdir -p /mnt/os

$ mount -t auto 
${LOOP_DEVICE}
 /mnt/os
在boot-disk.img文件里,把Linux内核文件和initrd文件拷贝到第一个分区。
$ cp vmlinuz-5.15.79 initrd-busybox-1.35.0.img /mnt/os
在boot-disk.img文件里,将引导程序EXTLINUX放进来。
$ mkdir -p /mnt/os/boot

$ extlinux --install /mnt/os/boot
为引导程序创建配置文件,这样我们可以指定到底需要加载什么。
echo"DEFAULT linux"
 >> /mnt/os/boot/syslinux.cfg
echo"  SAY Booting Simple Linux via SYSLINUX"
 >> /mnt/os/boot/syslinux.cfg
echo"  LABEL linux"
  >> /mnt/os/boot/syslinux.cfg
echo"  KERNEL /vmlinuz-5.15.79"
 >> /mnt/os/boot/syslinux.cfg
echo"  APPEND initrd=/initrd-busybox-1.35.0.img nomodeset"
 >> /mnt/os/boot/syslinux.cfg
卸载回环设备。
$ umount /mnt/os

$ losetup -D
在boot-disk.img文件中的硬盘开头部分,安装MBR引导加载器。
$ dd if=/usr/lib/syslinux/mbr/mbr.bin of=boot-disk.img bs=440 count=1 conv=notrunc
boot-disk.img文件将会包含U盘的引导镜像。
10
使用Docker构建Linux
上述方法包含许多命令和参数;敲这么多代码很容易出错。我们可以将命令组合成bash脚本,为了能够在Windows 10或11操作系统上构建Linux,建议使用Docker Desktop。
Docker的本质如下:
  1. 在Dockerfile中,描述程序或脚本的基础环境结构。
  2. 使用Docker实用工具,基于Dockerfile,以特定格式创建该环境的镜像。
  3. 使用相同的工具,可以启动基于镜像的程序或脚本实例,这些实例运行在隔离的环境中,在Docker术语中称为Docker容器。
  4. 创建好的镜像可以存储在存镜像仓库中并进行复用。由同一个镜像创建的Docker容器是可以在其他计算机上同样运行。
  5. Dockerfile易于阅读和学习,也易于发布。
下面是Dockerfile的内容:
FROM ubuntu:22.10

RUN apt update && apt install --yes make build-essential bc bison flex libssl-dev libelf-dev wget

RUN apt install --yes cpio fdisk extlinux dosfstools qemu-system-x86

RUN apt install --yes vim

ARG APP=/app

ARG LINUX_DIR=
$APP
/linux

ARG FILES_DIR=
$APP
/files

ARG SCRIPTS_DIR=
$APP
/scripts

ENV BUILD_DIR=
$APP
/build

ENV LINUX_DIR=
$LINUX_DIR
ENV FILES_DIR=
$FILES_DIR
ENV LINUX_VER=5.15.79

ENV BUSYBOX_VER=1.35.0

ENV BASH_ENV=
"$SCRIPTS_DIR/bash-env/env"
COPY ./scripts 
$APP
/scripts

COPY ./files 
$APP
/files

RUN mkdir -p 
$LINUX_DIR
RUN  ln -s 
$APP
/scripts/start-linux.sh /usr/bin/start &&\

     ln -s 
$APP
/scripts/build-linux.sh /usr/bin/build &&\

     ln -s 
$APP
/scripts/build-image.sh /usr/bin/image

WORKDIR 
$APP
/scripts

CMD build
让我们快速看一下Dockerfile的主要命令:
  • The FROM command是最重要的,它指定了我们的Linux构建镜像将基于哪个系统镜像。在本例,我们是Ubuntu 22.10。
  • The RUN command在创建的镜像中运行命令。也就是说,RUN之后的命令将像运行Ubuntu 22. 10一样在命令行中执行。执行该命令后,系统镜像将发生变化,因为这些命令更改了其中的文件系统。
  • The COPY command将文件从操作系统的文件系统复制到创建的镜像中。与RUN类似,它也会修改镜像中的文件系统。
  • The ARG and ENV commands比较令人困惑,不知道我是否会说清楚,ARG是在创建镜像时使用的变量,而ENV是在基于该镜像创建容器时使用的变量,同时此变量在容器中是可见的。
  • The WORKDIR command指定启动基于我们的镜像创建的容器时,在哪个目录下工作。
  • The CMD command命令指定了容器启动时默认执行的命令。
11
使用Docker运行和构建极简Linux
我们可以试验一下这个项目。在Windows上,最好在PowerShell中运行Docker。
创建一个Docker镜像。
$ git 
clone
 https://github.com/artyomsoft/simple-linux.git

cd
 simple-linux

$ docker build --build-arg APP -t simple-linux .
启动极简Linux。
$ mkdir linux

cd
 linux

$ docker run -v 
${pwd}
:/app/linux --rm -it simple-linux build
创建的Linux目录将包含构建Llinux内核文件和初始的根系统镜像文件。
为U盘创建一个引导镜像。
注意,在Docker中需要使用--privileged选项,因为镜像脚本使用回环设备。
$ docker run -v ${pwd}:/app/linux –-privileged --rm -it simple-linux image
如果使用Linux下的Docker Desktop,Docker将需要使用sudo运行,并且需要使用${pwd}而不是$(pwd)。
12
制作一个引导镜像U
为U盘(linux-5.15.79-busybox-1.35.0-disk.img)创建的镜像文件可以使用Win32DiskImager写入U盘。
需要注意的是,在写入时,U盘上现有的数据会丢失!
在将镜像写入U盘后,重新启动计算机并选择从USB-HDD启动,即可以从刚创建的U盘启动。如果遇到问题,最可能的情况是,在此之前,需要在BIOS中选择“Legacy Boot”并禁用“Secure Boot”。
13
在笔记本上运行Linux项目
安装Docker Desktop for Windows后,并在PowerShell中使用一个简单的命令来构建一个极简的Linux操作系统。
docker run -v ${pwd}:/app/linux --rm -it artyomsoft/simple-linux build
我们可以使用极简的Linux命令行,当退出时,我们可以在当前目录中看到一个Linux内核文件和一个initrd文件。
14
结论
在这篇文章中,我尽量提供详细的说明,告诉大家如何从源码获得一个可以工作的Linux系统。
对一些人来说,这篇文章可能看起来很简单,不值得关注。但是,为了不被细节吓到,我没有深入讨论诸如BIOS、UEFI、文件系统、bootloader、glibc库、详细的操作系统引导过程、各种规范、动态和静态链接、Linux内核模块……我只是给出了让大家能够接受和理解的最基本的理论,事实上,这能让大家更快速地理解这个主题,而不是像我之前那也一点一点地收集所有信息。
我们其实几乎不会在未来使用这样的操作系统,这篇文章也只是希望大家能将Linux的抽象知识转化为对Linux更深的理解和一些小技能。
这篇文章有用吗?你是否尝试了从源代码构建一个极简的Linux操作系统?欢迎大家在评论中分享你的经验!
相关链接:
  1. https://distrowatch.com/
  2. https://www.linuxfromscratch.org/
推荐阅读:

分布式实验室策划的《Kubernetes实战集训营》正式上线了。这门课程通过通过5天线上培训,40个小时直播,15个随堂练习,50天课后辅导,把Kubernetes的60多个重要知识点讲给你,并通过实战帮你掌握Kubernetes。培训重实战、重项目、更贴近工作,边学边练,2月25日正式开课。
继续阅读
阅读原文