Linux 内核经常被认为是一个压倒性的大型开源软件。截至撰写本文时,最新版本是 v6.5-rc5,包含 36M 行代码。不用说,Linux 是许多贡献者几十年来辛勤工作的成果。然而,Linux 的第一个版本 v0.01 非常小。它仅包含 10,239 行代码。除去注释和空行,只有 8,670 行。它足够小,易于理解,是了解类 UNIX 操作系统内核内部结构的良好起点。阅读 v0.01 对我来说真的很有趣。这就像参观山景城的计算机历史博物馆 - 终于我亲眼目睹了故事确实是真的!我写这篇文章是为了与您分享这段令人兴奋的经历。让我们深入了解一下吧!免责声明:显然我不是 Linux v0.01 的作者。如果您发现本文有任何错误,请告诉我!
系统调用是什么样的?
v0.01 有 66 个系统调用。以下是其中的列表:
access acct alarm break brk chdir chmod
chown chroot close creat dup dup2 execve
exit fcntl fork fstat ftime getegid geteuid
getgid getpgrp setsid getpid getppid
getuid gtty ioctl kill link lock lseek
mkdir mknod mount mpx nice open pause
phys pipe prof ptrace read rename rmdir
setgid setpgid setuid setup signal stat
stime stty sync time times ulimit umask
umount uname unlink ustat utime waitpid write
- 它支持读取、写入、创建和删除文件和目录。此外,还支持其他基本概念,例如
chmod(2)
(权限)、chown(2)
(所有者)和pipe(2)
(进程间通信)。 fork(2)
并execve(2)
在那里。仅a.out
支持可执行格式。- 套接字的概念没有实现。因此,没有网络支持。
- 诸如此类的一些功能
mount(2)
尚未实现。他们只是返回ENOSYS
:
int sys_mount()
{
return -ENOSYS;
}
针对 Intel 386 架构进行深度硬编码
Linus 与 MINIX 的作者 Andrew S. Tanenbaum 就操作系统的设计进行了一场非常著名的辩论:单片内核与微内核,哪个设计更好?Tanenbaum指出Linux 是(或曾经是)不可移植的,因为它针对 Intel 386 (i386) 进行了深度硬编码:MINIX 被设计为相当便携,并且已从 Intel 系列移植到 680x0(Atari、Amiga、Macintosh)、SPARC 和 NS32016。LINUX 与 80x86 的联系相当紧密。这不是要走的路。这确实是真的。Linux v0.01 针对 i386 进行了深度硬编码。strcpy
这是in的实现include/string.h
:
extern inline char * strcpy(char * dest,const char *src)
{
__asm__("cld\n"
"1:\tlodsb\n\t"
"stosb\n\t"
"testb %%al,%%al\n\t"
"jne 1b"
::"S" (src),"D" (dest):"si","di","ax");
return dest;
}
它是用 i386 的字符串指令用汇编语言编写的。是的,它可以作为当今 Linuxstrcpy
中的优化实现找到,但它不是在类似. 而且,无需切换不同架构的实现。它只是针对 Intel 386 进行了硬编码。include/string.h
include/i386/string.h
#ifdef
此外,仅支持 PC/AT 设备:
- CMOS:实时时钟 (
init/main.c
)。 - 可编程间隔定时器 (PIT):定时器 (
kernel/sched.c
)。 - ATA (PIO):硬盘 (
kernel/hd.c
)。 - VGA(文本模式):显示 (
kernel/console.c
)。 - 英特尔 8042:PS/2 键盘 (
kernel/keyboard.s
)。是的,它完全是用汇编编写的!
您可能已经注意到,它们并不drivers
像今天的 Linux 那样位于目录中。它们被硬编码在核心子系统中。
“弗雷克斯”
我在某处读到 Linus 最初将他的内核命名为“FREAX”。Linux v0.01中的Makefile仍然有以下注释:
# Makefile for the FREAX-kernel.
这确实是FREAX!
v0.01支持什么文件系统?
如今,Linux 支持多种文件系统,例如 ext4、Btrfs 和 XFS。v0.01呢?外部2?不,这里有一个提示include/linux/fs.h
:
#define SUPER_MAGIC 0x137F
答案是,正如GPT-4 猜对的那样,MINIX 文件系统!有趣的事实:ext(“扩展文件系统”)是 ext2/ext3/ext4 的前身,受到 MINIX 文件系统的启发。
“可能”没有任何理由改变调度程序
这是Linux v0.01的调度程序:
while (1) {
c = -1;
next = 0;
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
}
if (c) break;
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
}
switch_to(next);
i
并p
分别保存任务表中任务的索引(不是PID!)和指向的指针task_struct
。关键变量在( )counter
中。调度程序选择具有最大值的任务并切换到它。如果所有可运行任务的计数器值为 0,它将更新每个任务的值并重新启动循环。请注意,这是除以 2 的更快方法。task_struct
(*p)->counter
counter
counter
counter = (counter >> 1) + priority
counter >> 1
关键点是计数器更新。它还更新不可运行任务的计数器值。这意味着,如果一个任务长时间等待 I/O ,并且其优先级高于 2,则计数器值将单调增加 当计数器更新时增加直到某个上限(已编辑) 。这只是我的猜测,但我认为这是为了优先考虑很少可运行但对延迟敏感的任务,例如 shell,它在大部分时间里都会等待键盘输入。最后,switch_to(next)
是一个将 CPU 上下文切换到所选任务的宏。这里描述得很好。简而言之,它基于称为任务状态段 (TSS)的 x86 特定功能,该功能不再用于 x86-64 架构中的任务管理。顺便说一句,有一个关于调度程序的有趣评论:
* 'schedule()' is the scheduler function. This is GOOD CODE! There
* probably won't be any reason to change this, as it should work well
* in all circumstances (ie gives IO-bound processes good response etc).
是的,这确实是很好的代码。不幸的是(或者幸运的是),这个预言是错误的。Linux 成为最实用、最高性能的内核之一,多年来引入了许多调度改进和新算法,例如完全公平调度程序 (CFS)。
5 行内核恐慌
volatile void panic(const char * s)
{
printk("Kernel panic: %s\n\r",s);
for(;;);
}
让用户知道出了问题,并挂起系统。时期。
fork(2)
在内核空间?
内核初始化的主要部分可以在init/main.c
(有趣的事实:这个文件仍然存在于今天的 Linux 内核中并初始化内核)中找到:
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
time_init();
tty_init();
trap_init();
sched_init();
buffer_init();
hd_init();
sti();
move_to_user_mode();
if (!fork()) { /* we count on this going ok */
init();
}
/*
* NOTE!! For any other task 'pause()' would mean we have to get a
* signal to awaken, but task0 is the sole exception (see 'schedule()')
* as task 0 gets activated at every idle moment (when no other tasks
* can run). For task0 'pause()' just means we go check if some other
* task can run, and if not we return here.
*/
for(;;) pause();
}
void init(void)
{
int i,j;
setup();
if (!fork())
_exit(execve("/bin/update",NULL,NULL));
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);
printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,
NR_BUFFERS*BLOCK_SIZE);
printf(" Ok.\n\r");
if ((i=fork())<0)
printf("Fork failed in init\r\n");
else if (!i) {
close(0);close(1);close(2);
setsid();
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);
_exit(execve("/bin/sh",argv,envp));
}
j=wait(&i);
printf("child %d died with code %04x\n",j,i);
sync();
_exit(0); /* NOTE! _exit, not exit() */
}
它调用每个子系统的初始化函数。非常简单。但有一些有趣的事情:它调用fork(2)
内核的main()
. 另外,init()
看起来像是用户空间中的普通实现,但它是硬编码在内核代码中的!看起来好像是在内核空间中 fork(2)-ing,但实际上并非如此。诀窍在于move_to_user_mode()
:
#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \ // EAX = current stack pointer
"pushl $0x17\n\t" \ // SS (user data seg)
"pushl %%eax\n\t" \ // ESP
"pushfl\n\t" \ // EFLAGS
"pushl $0x0f\n\t" \ // CS (user code seg)
"pushl $1f\n\t" \ // EIP (return address)
"iret\n" \ // switch to user mode
"1:\tmovl $0x17,%%eax\n\t" \ // IRET returns to this address
"movw %%ax,%%ds\n\t" \ // Set DS to user data segment
"movw %%ax,%%es\n\t" \ // Set ES to user data segment
"movw %%ax,%%fs\n\t" \ // Set FS to user data segment
"movw %%ax,%%gs" \ // Set GS to user data segment
:::"ax") // No RET instruction here:
// continue executing following
// lines!
您不需要完全理解上面的汇编代码。它的作用是使用IRET 指令切换到用户模式,但使用当前堆栈指针继续执行内核代码中的以下几行!因此,下面if (!fork())
是在用户模式下执行的,fork(2)
实际上是一个系统调用。
Linus 没有 8MB RAM 的机器
* For those with more memory than 8 Mb - tough luck. I've
* not got it, why should you :-) The source is here. Change
* it. (Seriously - it shouldn't be too difficult. ...
如今,8GB RAM 的机器非常常见。此外,8GB 对于软件工程师来说根本不够用;)
很难用现代工具链进行编译
最后,我尝试使用现代工具链编译内核,但失败了。我认为 GCC(或 C 本身)具有良好的向后兼容性,但这还不够。即使使用较旧的标准也会-std=gnu90
导致编译错误,但修复起来并不容易。一个有趣的事实是 Linus 使用了他自己的 GCC,其功能名为-mstring-insns
:
# If you don't have '-mstring-insns' in your gcc (and nobody but me has :-)
# remove them from the CFLAGS defines.
我不确定它是什么,但它似乎是支持(或优化?)x86 字符串指令的功能。如果您设法使用现代工具链编译内核,请写一篇文章并发给我一个链接:D
自己读吧!
我希望您和我一样喜欢阅读 Linux v0.01 的源代码。如果您对 v0.01 感兴趣,请从 kernel.org 下载 v0.01 的 tarball。阅读代码并不难,特别是如果您之前读过xv6。Linux v0.01 很简约,但写得非常好。