探索 Linux v0.01 的内部结构

Linux 内核经常被认为是一个压倒性的大型开源软件。截至撰写本文时,最新版本是 v6.5-rc5,包含 36M 行代码。不用说,Linux 是许多贡献者几十年来辛勤工作的成果。

然而,Linux 的第一个版本 v0.01 非常小。它仅包含 10,239 行代码。除去注释和空行,只有 8,670 行。它足够小,

Linux command

易于理解,是了解类 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
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 进行了深度硬编码。这是 include/string.h 中 strcpy 的实现:

extern inline char * strcpy(char * dest,const char *src)
{
__asm__("cld
"
	"1:	lodsb
	"
	"stosb
	"
	"testb %%al,%%al
	"
	"jne 1b"
	::"S" (src),"D" (dest):"si","di","ax");
return dest;
}

它是用 i386 的字符串指令用汇编语言编写的。是的,它可以在当今的 Linux 中作为 strcpy 的优化实现找到,但它位于 include/string.h 中,而不是像 include/i386/string.h 这样的地方。而且,没有 #ifdef 来切换不同架构的实现。它只是针对 Intel 386 进行了硬编码。

此外,仅支持 PC/AT 设备:

您可能会注意到,它们并不像今天的 Linux 那样位于 drivers 目录中。它们被硬编码在核心子系统中。

“弗雷克斯”

我在某处读到 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 的指针。关键变量是 task_struct 中的 counter ( (*p)->counter )。调度程序选择具有最大 counter 值的任务并切换到它。如果所有可运行任务的计数器值为 0,它将按 counter = (counter >> 1) + priority 更新每个任务的 counter 值并重新启动循环。请注意, counter >> 1 是除以 2 的更快方法。

关键点是计数器更新。它还更新不可运行任务的计数器值。这意味着,如果一个任务长时间等待 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
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
r",NR_BUFFERS,
		NR_BUFFERS*BLOCK_SIZE);
	printf(" Ok.
r");
	if ((i=fork())<0)
		printf("Fork failed in initr
");
	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
",j,i);
	sync();
	_exit(0);	/* NOTE! _exit, not exit() */
}

它调用每个子系统的初始化函数。非常简单。但有一些有趣的事情:它调用内核的 main() 中的 fork(2) 。另外, init() 看起来像是用户空间中的普通实现,但它是硬编码在内核代码中的!

看起来好像是在内核空间中 fork(2)-ing,但实际上并非如此。诀窍在于 move_to_user_mode() :

#define move_to_user_mode() 
__asm__ ("movl %%esp,%%eax
	"  // EAX = current stack pointer
	"pushl $0x17
	"            // SS (user data seg)
	"pushl %%eax
	"            // ESP
	"pushfl
	"                 // EFLAGS
	"pushl $0x0f
	"            // CS (user code seg)
	"pushl $1f
	"              // EIP (return address)
	"iret
"                     // switch to user mode
	"1:	movl $0x17,%%eax
	"   // IRET returns to this address
	"movw %%ax,%%ds
	"         // Set DS to user data segment
	"movw %%ax,%%es
	"         // Set ES to user data segment
	"movw %%ax,%%fs
	"         // 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 很简约,但写得非常好。

展开阅读全文

页面更新:2024-05-05

标签:初始化   文件系统   内核   指令   计数器   内部结构   有趣   代码   功能   程序   用户

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top