本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:Linux内核源码1.0版本是操作系统发展史上的关键里程碑,为理解现代操作系统的底层机制提供了珍贵的学习资源。该版本已具备进程管理、内存管理、文件系统、网络协议栈、设备驱动和系统调用等核心功能,展现了早期Linux的模块化设计与系统架构。通过分析其源码结构与关键机制,开发者可深入掌握操作系统的工作原理,包括基于优先级的调度算法、页面管理、ext/vfat文件系统实现、TCP/IP协议支持及中断处理机制。本资料适用于操作系统初学者和内核爱好者,帮助建立对Linux内核演进路径的全面认知,夯实系统级编程与内核开发基础。

Linux 1.0 内核:从源码到现代操作系统的起点 🧱💻

还记得第一次打开 /proc/version 看到那个熟悉的“Linux version”时的激动吗?
你有没有想过,这一切的源头,竟然是一段只有十几万行 C 和汇编代码、运行在 386 上的“玩具内核”?

今天我们要回到 1994年3月 —— 那个没有 Docker、没有 Kubernetes、甚至连动态模块都还不完善的年代。林纳斯·托瓦兹发布了 Linux 1.0,一个被全世界开发者围观和协作的小项目,正式宣告自己不再是“仅供个人使用”的实验品。

但这不仅仅是一个版本号的变化,它标志着一种全新的软件开发范式的诞生: 开源协作、模块化设计、贴近硬件、持续迭代 。

我们不讲空话,直接钻进原始源码里,用 QEMU 启动一个真正的 Linux 1.0 实例,然后一层层剥开它的内核结构——从 bootsect.S 的第一条指令开始,到 init/main.c 中的 start_kernel() ,再到 fork() 、 schedule() 、 sys_open() …… 每一步都带着真实的代码片段、可运行的构建流程,以及那些藏在注释里的工程智慧。

准备好了吗?让我们一起穿越回三十年前,看看这个改变世界的“小东西”,到底是怎么工作的。🚀


🔤 林纳斯为什么要写一个操作系统?

故事得从一台 Intel 386 PC 说起。

1991 年,赫尔辛基大学的学生林纳斯正在学习 Minix(一个教学用的类 Unix 系统)。他发现这玩意儿挺好,但就是太封闭了——你想改点功能?不好意思,没源码。想支持新设备?等作者高兴吧。

于是他在 Usenet 上发了个帖子:

“我正在做一个(免费的)操作系统(只是爱好),不会搞得像 GNU 那么大那么专业……”

没人想到,这句话拉开了一个时代的序幕。

当时的背景是:Unix 商业化严重,AT&T、Sun、HP 各自为战;而 GNU 项目虽然已经做出了 gcc、gdb、emacs,却卡在 Hurd 内核上迟迟无法完成。整个自由操作系统生态,缺的就是那一块“拼图”。

Linux 正好补上了这块空白。

更关键的是,林纳斯选择了 GPL 协议发布代码。这意味着任何人都可以查看、修改、分发这份代码——只要你也将改动回馈社区。这种“开放即安全、透明即信任”的理念,彻底颠覆了传统闭源软件的开发模式。

三年后,经过无数次补丁合并与全球协作,Linux 1.0 终于稳定发布。它支持多任务、虚拟内存、TCP/IP、ext 文件系统…… 虽然功能简单,但它证明了一件事:

一群素未谋面的人,可以通过邮件列表和 FTP,共同打造一个生产级的操作系统。

这不仅是技术的成功,更是社会协作模型的胜利。


📁 源码长什么样?目录结构解析

下载一份原汁原味的 Linux 1.0 源码:

wget https://mirrors.edge.kernel.org/pub/linux/kernel/v1.0/linux-1.0.tar.gz
tar -xzf linux-1.0.tar.gz && cd linux-1.0

你会发现它的根目录非常干净:

arch/     drivers/  include/  kernel/   mm/       net/      fs/       init/     ipc/

每个目录职责分明,毫无冗余——这是早期 Linux 最令人惊叹的设计哲学之一: 高内聚、低耦合、清晰分层 。

核心子系统一览

目录 功能说明
arch/ 架构相关代码,比如 i386 的启动、中断、页表初始化
init/ 系统入口,包含 main.c 和 start_kernel()
mm/ 内存管理,实现分页、swap、页面分配器
fs/ 文件系统抽象层(VFS) + ext/minix/msdos 支持
drivers/ 块设备、字符设备、网络设备驱动集合
net/ TCP/IP 协议栈基础,socket 接口实现
ipc/ 进程间通信机制:信号量、消息队列、共享内存

这些名字至今仍存在于现代内核中,说明什么?

👉 架构的生命力,来自于最初的合理设计。

哪怕后来加了 cgroups、eBPF、kvm、bpftrace…… 整体骨架依然是当年那个样子。


⚙️ 第一行代码在哪?从 bootsect.S 开始

真正意义上的第一行执行代码,位于:

arch/i386/boot/bootsect.S

这是一个 512 字节的引导扇区程序,由 BIOS 加载到物理地址 0x7C00 并跳转执行。

来看看这段经典的 NASM 汇编:

mov ax, #BOOTSEG
mov ds, ax
mov ax, #INITSEG
mov es, ax
mov cx, #256
sub si, si
sub di, di
rep movw                ; 将自身复制到 0x90000
jmpi    go, INITSEG     ; 跳转到 INITSEG:go

go:
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, #0xFF00

别小看这几行代码,它们干了几件大事:

  1. 设置段寄存器 :确保数据访问正确;
  2. 内存迁移 :把引导代码从 0x7C00 复制到高地址 0x90000 ,避免后续加载覆盖;
  3. 建立堆栈 :设置 ss:sp ,为 C 语言执行准备环境;
  4. 远跳转 :进入新的代码段继续执行。

这就是传说中的“自举”过程:操作系统如何在没有任何运行时支持的情况下,自主接管机器控制权。

接下来是 setup.S ,负责探测内存大小、显卡模式,并最终跳入保护模式下的 head.s 。

// arch/i386/kernel/head.s
startup_32:
    movl $0x10,%eax
    mov %ax,%ds
    mov %ax,%es
    mov %ax,%fs
    mov %ax,%gs
    call main

看到没?连函数调用都很朴素。这里设置了段选择子,然后直接 call main —— 是的,内核也有 main() 函数!


🏗️ 编译系统是怎么工作的?Makefile 的艺术

Linux 1.0 还没有 Kbuild 或 Kconfig GUI 工具,但它已经有了一套完整的配置与构建体系。

运行:

make config

你会看到一连串文本问答:

Configure with NFS (y/n): n
Math emulation (y/n): n
SysV IPC (y/n): y
POSIX timers (y/n): n

这些选项写入 .config 文件:

CONFIG_SYSVIPC=y
CONFIG_NFS_FS=n

顶层 Makefile 解析这些宏,决定是否编译对应模块:

ifdef CONFIG_SYSVIPC
  SUB_DIRS += ipc
endif

更精彩的是镜像生成流程:

Image: boot/bootsect boot/setup tools/build
    tools/build boot/bootsect boot/setup Image > Image

vmlinuz: piggy.o Image
    $(LD) $(LDFLAGS) -Ttext 0x100000 -e stext vmlinux.o >> System.map

其中 tools/build 是一个小巧的 C 程序,负责将三部分拼接成可引导镜像:

偏移地址 内容
0x0000 bootsect(512B 引导扇区)
0x0200 setup(实模式初始化代码)
0x2000 压缩后的内核正文(zImage)

最终产物 arch/i386/boot/zImage 可以直接丢给 QEMU 启动!

qemu-system-i386 -kernel arch/i386/boot/zImage \
                 -hda rootfs.img \
                 -append "root=/dev/hda"

如果一切顺利,你会看到:

Linux version 1.0.0 (root@localhost) ...
Calibrating delay loop... ok - 33.33 BogoMIPS
Starting init: /sbin/init

🎉 成功了!你的虚拟机跑起了 1994 年的操作系统。


🔁 系统是如何启动的?start_kernel 全解析

所有初始化逻辑始于 init/main.c 中的 start_kernel() 函数。

void __init start_kernel(void)
{
    printk("Linux version " UTS_RELEASE);
    trap_init();          
    init_IRQ();           
    sched_init();         
    time_init();          
    console_init();       
    kmem_cache_init();    
    sti();                
    calibrate_delay();    
    mem_init();           
    buffer_init();        
    check_bugs();
    smp_init();           
    rest_init();          
}

注意顺序!这不是随便排的,而是严格的依赖链:

  • 必须先设好 IDT(中断描述符表),才能开启中断(sti);
  • 必须初始化调度器,才有进程上下文;
  • 必须准备好缓冲区缓存,文件系统才能挂载。

我们可以画出初始化调用链:

graph TD
    A[start_kernel] --> B[trap_init]
    A --> C[sched_init]
    A --> D[buffer_init]
    A --> E[hd_init]
    A --> F[net_init]
    B --> G[IDT设置]
    C --> H[task[0]创建]
    D --> I[缓冲区哈希表初始化]
    E --> J[IDE控制器注册]
    F --> K[socket协议族注册]

每一个箭头背后,都是对硬件状态的精确掌控。

比如 sched_init() 创建了第一个进程 task[0] ,也就是 idle 进程:

task[0].state = TASK_UNINTERRUPTIBLE;
task[0].policy = SCHED_OTHER;
current = &task[0];

而 rest_init() 则 fork 出 init 进程:

if (!fork()) {
    execve("/sbin/init", argv_init, envp_init);
}

从此,用户空间开始运转。


👥 进程是怎么创建的?深入 fork() 实现

fork() 是 Unix 系统的灵魂。在 Linux 1.0 中,它的核心实现在 kernel/fork.c 的 copy_process() 函数中。

int copy_process(int nr, long ebx, long ecx, ..., long ss)
{
    struct task_struct *p;

    p = (struct task_struct*) get_free_page();
    if (!p) return -EAGAIN;

    *p = *current;  // 浅拷贝父进程结构体

    p->pid = last_pid++;
    p->counter = 0;
    p->policy &= ~PF_PTRACED;

    copy_mem(nr, p);           // 复制页表

    p->regs.eax = 0;           // 子进程返回 0

    task[nr] = p;
    p->state = TASK_RUNNING;

    return last_pid;
}

重点来了:

  • *p = *current :结构体赋值,继承大部分属性;
  • copy_mem() :不是复制物理内存,而是复制页表项,并标记为只读——这就是 写时复制(Copy-on-Write) 的雏形!💥
  • p->regs.eax = 0 :关键所在!子进程 fork() 返回 0,父进程返回子 PID;
  • 插入 task[] 数组,状态改为 TASK_RUNNING ,等待调度。

这种“延迟复制+按需分配”的策略,极大提升了性能,也成为后来所有类 Unix 系统的标准做法。


🕹️ 调度器怎么工作?时间片轮转算法揭秘

Linux 1.0 使用的是最简单的 循环调度器(Round-Robin Scheduler) ,实现在 kernel/sched.c 的 schedule() 函数中。

void schedule(void)
{
    int c;
    struct task_struct *p, *next;

    while (1) {
        c = -1;
        next = NULL;

        for_each_task(p) {
            if (p->state == TASK_RUNNING && p->counter > c)
                c = p->counter, next = p;
        }

        if (c) break;

        for_each_task(p)
            p->counter = (p->counter >> 1) + p->priority;
    }

    if (current != next)
        switch_to(next);
}

逻辑很清晰:

  1. 找出 counter 最大的就绪进程;
  2. 如果全都耗尽,则重置: counter = (counter / 2) + priority ;
  3. 切换上下文。

这是一个典型的“多级反馈队列”简化版。虽然没有抢占式调度(完全依赖主动调用 schedule ),但在单 CPU 时代足够用了。

而且你看那句 (counter >> 1) + priority —— 它让低优先级进程不会饿死,体现了设计者对公平性的考量。

graph TD
    A[开始调度] --> B{是否存在 counter > 0 的 RUNNING 进程?}
    B -- 是 --> C[选取最大 counter 的进程]
    B -- 否 --> D[重载时间片: counter = (counter/2)+priority]
    D --> B
    C --> E{是否需要切换?}
    E -- 是 --> F[执行 switch_to()]
    F --> H[更新 current 指针]
    E -- 否 --> G[继续当前进程]

💾 内存管理:分页、页表、swap 如何协同

Linux 1.0 在 i386 上实现了基本的分页机制。

分页初始化

void setup_paging(void)
{
    pgd_t *pg_dir = (pgd_t*) 0x1000;
    pte_t *pg_table = (pte_t*) 0x2000;

    for (i = 0; i < PTRS_PER_PAGE; i++) {
        pg_dir[i] = __pgd(((unsigned long) pg_table) +
                          PAGE_PRESENT | PAGE_RW | PAGE_USER);
    }

    for (i = 0; i < 256; i++) {
        pg_table[i] = __pte(i << 12 | PAGE_PRESENT | PAGE_RW);
    }

    __asm__ __volatile__(
        "movl %0,%%cr3\n\t"
        "movl %%cr0,%%eax\n\t"
        "orl $0x80000000,%%eax\n\t"
        "movl %%eax,%%cr0"
        : : "r" (pg_dir) : "eax"
    );
}
  • 设置 CR3 指向页目录;
  • 开启 CR0.PG 位,激活 MMU;
  • 前 1MB 物理内存做线性映射。

页面分配器

物理页由 mem_map 数组管理:

struct page {
    unsigned long flags;
    atomic_t count;
};

unsigned long get_free_page(void)
{
    for (int i = 0; i < max_mapnr; i++) {
        if (!atomic_read(&mem_map[i].count)) {
            atomic_inc(&mem_map[i].count);
            memset((void*)(i<<12), 0, PAGE_SIZE);
            return i << 12;
        }
    }
    return 0;
}

虽然还不是真正的伙伴系统(直到 2.2 才引入),但已有引用计数、清零语义、按页粒度分配等特性,为未来扩展打下基础。

Swap 与缺页异常

当内存不足时,页面会被 swap 到磁盘。

缺页处理函数:

void do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
    unsigned long address = read_cr2();

    if (is_valid_page(address)) {
        alloc_page_and_map(address);
    } else if (is_swap_area(address)) {
        swap_in(address);
    } else {
        send_sig(SIGSEGV, current, 1);
    }
}

整个流程闭环完整:访问非法地址 → 触发 page fault → 分配或换入 → 继续执行。


🗂️ 文件系统:VFS 抽象与 ext 实现

Linux 1.0 支持多种文件系统:minix、ext、msdos。

其核心是 VFS(虚拟文件系统)层,提供统一接口:

asmlinkage int sys_open(const char *filename, int flags, int mode)
{
    char *tmp = getname(filename);
    struct file *f = filp_open(tmp, flags, mode);
    putname(tmp);

    if (IS_ERR(f))
        return PTR_ERR(f);

    return f->f_dentry->d_inode->i_ino;
}

注意:这里返回的是 inode 编号作为 fd!😱

这是因为在早期版本中,fd 表尚未完全独立,直接用 inode 号索引。虽然后来改掉了,但能看出当时设计的“够用就行”风格。

VFS 抽象的关键结构是 inode 和 dentry :

  • inode :代表文件元数据(权限、大小、块指针等);
  • dentry :目录项缓存,加速路径查找;
  • file_operations :操作函数表,不同 FS 实现不同方法。

例如 ext 文件系统有自己的 ext_file_ops ,而 msdos 有 msdos_file_ops ,通过指针调用实现多态。


🌐 网络协议栈:TCP/IP 的初步支持

尽管功能有限,Linux 1.0 已经内置了 TCP/IP 协议栈。

net/socket.c 提供 socket API:

int sock_write(struct socket *sock, struct socket *usock,
               struct sockaddr *addr, int addr_len)
{
    struct sock *sk = sock->sk;
    return sk->prot->write(sk, usock, addr, addr_len);
}

每种协议(TCP/UDP)都有自己的 struct proto 实现。

初始化在 net_init() 中完成:

void net_init(void)
{
    sock_init();
    packet_init();
    ip_init();
    tcp_init();
    udp_init();
}

虽然还没有 netfilter、iptables、AF_NETLINK…… 但基本通信能力已经有了。

你可以想象,在 1994 年,一台跑着 Linux 1.0 的机器,已经能 ping 通另一台主机了——这在当时是多么震撼的事情!


🧰 系统调用是如何工作的?int 0x80 全解析

用户态程序通过软中断 int 0x80 进入内核。

系统调用表

extern fn_ptr sys_call_table[];

这是一个静态数组,索引即调用号:

号 函数 功能
0 sys_setup 初始化
1 sys_exit 退出
2 sys_fork 创建进程
3 sys_read 读文件
… … …

中断入口

system_call:
    pushl %eax
    SAVE_ALL
    sti
    call *sys_call_table(,%eax,4)
    movl %eax,%edx
    cli
    RESTORE_ALL
    movl %edx,%eax
    iret
  • %eax 存系统调用号;
  • %ebx , %ecx , %edx 传参数;
  • 查表调用对应函数;
  • 结果写回 %eax , iret 返回用户态。

整个过程高效简洁,几乎没有额外开销。

graph TD
    A[用户调用 write()] --> B[设置 eax=4, ebx=fd...]
    B --> C[int 0x80]
    C --> D[进入 system_call]
    D --> E[保存现场]
    E --> F[查表调用 sys_write]
    F --> G[执行文件写入]
    G --> H[返回结果]
    H --> I[iret 回用户态]

🧩 模块化设计思想:虽无 .ko,已有模块之魂

很多人说 Linux 1.0 不支持模块,所以不算模块化。

错!🚫

虽然确实没有 .ko 文件和 insmod ,但它的 编译期模块化 思想已经成型:

  • 通过 .config 控制哪些代码参与编译;
  • 驱动通过 register_blkdev() 注册,不影响核心;
  • 子系统之间通过函数指针表解耦;
  • Makefile 按目录递归编译,支持裁剪。

这种“静态链接 + 条件编译”的方式,在资源受限环境下反而更可靠。

直到 Linux 2.1 才引入动态模块,而今天我们习以为常的 lsmod 、 modprobe ,正是从这一点点积累而来。


✨ 它教会了我们什么?对现代系统的启示

回头看,Linux 1.0 的意义远不止“第一个稳定版”。

它告诉我们:

✅ 复杂系统可以从简单开始
不要追求一开始就完美,先让它跑起来。

✅ 模块化不是靠工具,而是思维方式
即使没有动态加载,也能做到职责分离。

✅ 可移植性来自良好的抽象
arch/ 目录的设计,让 Linux 很快支持 MIPS、Alpha、SPARC……

✅ 社区比代码更重要
林纳斯一个人写不出 Linux,是全球开发者一起把它变成现实。

今天的云原生、容器、微服务、eBPF…… 哪一个不是站在这个巨人肩膀上?

当你在 Kubernetes 里部署 Pod 时,底层的 cgroup、namespace、vfs、tcp stack…… 全都能追溯到 Linux 1.0 的某个角落。


🏁 结语:致敬那个“只是爱好”的开始

三十多年过去了,Linux 已经成为世界上最重要的软件之一。

它驱动着手机、服务器、超算、路由器、智能电视,甚至火星探测器。

但我们不应忘记,这一切始于一个大学生的“业余项目”。

下次当你敲下 uname -a 的时候,不妨多看一眼输出中的版本号。也许就在某一行,藏着一段属于 bootsect.S 的记忆。

“It’s not about being the best. It’s about being part of something that works.”
—— Linus Torvalds

感谢那个勇敢按下“发布”按钮的年轻人。🌍❤️


🔍 动手建议 :试试用 QEMU 跑一遍 Linux 1.0 吧!你可以从 kernel.org 下载源码,搭配一个 Minix 格式的 rootfs,亲自体验从编译到启动的全过程。你会发现,那些看似神秘的内核机制,其实都藏在几万行代码之中。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:Linux内核源码1.0版本是操作系统发展史上的关键里程碑,为理解现代操作系统的底层机制提供了珍贵的学习资源。该版本已具备进程管理、内存管理、文件系统、网络协议栈、设备驱动和系统调用等核心功能,展现了早期Linux的模块化设计与系统架构。通过分析其源码结构与关键机制,开发者可深入掌握操作系统的工作原理,包括基于优先级的调度算法、页面管理、ext/vfat文件系统实现、TCP/IP协议支持及中断处理机制。本资料适用于操作系统初学者和内核爱好者,帮助建立对Linux内核演进路径的全面认知,夯实系统级编程与内核开发基础。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐