【Linux】内核的编译和加载

在这里插入图片描述

Linux内核是操作系统的核心,负责管理系统的硬件资源,并为用户空间的应用程序提供必要的服务。内核的编译和加载是操作系统开发和维护的重要环节。本文将详细介绍Linux内核的编译过程以及如何加载内核到系统中。

1. 引言

Linux内核的编译是一个复杂的过程,涉及到配置、预处理、编译、链接等多个步骤。加载内核则是启动操作系统的关键一步,它决定了系统的启动方式和初始状态。通过理解内核的编译和加载过程,我们可以更好地掌握Linux系统的工作原理。

2. Linux内核编译过程

Linux内核的编译过程通常包含以下几个主要步骤:

2.1 配置内核

配置内核是编译过程的第一步,它决定了哪些功能将被编译进内核,哪些功能将以模块的形式加载。配置内核可以通过以下几种方式:

2.1.1 使用menuconfigncurses工具

这些工具提供了图形化的配置界面,可以让用户选择启用或禁用特定的功能。

make menuconfig
2.1.2 手动编辑配置文件

如果熟悉内核配置项,也可以直接编辑.config文件。

nano .config
示例配置项

配置文件中的一些典型配置项如下:

CONFIG_SMP=y # 启用多处理器支持
CONFIG_PREEMPT=y # 启用抢占式调度
CONFIG_CMDLINE=y # 启用从引导加载程序传递的命令行参数
CONFIG_DEVTMPFS=y # 启用/dev文件系统的自动创建

2.2 预处理

预处理阶段主要包括头文件的处理、宏定义的展开、条件编译的判断等。内核使用预处理器(如GCC的预处理器)来处理源代码文件,生成经过预处理的源代码文件。

2.2.1 预处理命令

预处理命令包括#include#define#ifdef等。例如:

#define MAX_DEVICES 256

struct device {
    char name[MAX_DEVICES]; // 设备名称的最大长度
    int id;                 // 设备ID
};

2.3 编译

编译阶段是将预处理后的源代码文件转换成机器语言的过程。这个过程通常由编译器(如GCC)完成。Linux内核的编译过程非常复杂,因为它包含了大量的源代码文件和依赖关系。

2.3.1 编译命令

使用make命令进行编译,可以指定并行编译的数量来加快编译速度:

make -j$(nproc)
2.3.2 编译过程

编译过程涉及以下几个步骤:

  1. 编译内核源代码:将C/C++源代码编译成汇编代码。
  2. 汇编汇编代码:将汇编代码转换成目标文件(.o)。
  3. 处理汇编文件:对生成的目标文件进行处理,如添加调试信息等。
示例代码
// 文件:drivers/chardev.c

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>

static int major = 240; // 为设备分配主设备号

static dev_t dev_num = MKDEV(major, 0); // 构造设备号
static struct cdev c_dev;                // 字符设备结构体
static struct class *class;              // 设备类指针
static struct device *device;            // 设备指针

static int dev_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Device opened.\n");
    return 0;
}

static int dev_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Device closed.\n");
    return 0;
}

static ssize_t dev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    // 实现读逻辑
    return count;
}

static ssize_t dev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    // 实现写逻辑
    return count;
}

static const struct file_operations fops = {
    .owner          = THIS_MODULE,
    .read           = dev_read,
    .write          = dev_write,
    .open           = dev_open,
    .release        = dev_release,
};

static int __init dev_init(void)
{
    // 注册字符设备
    register_chrdev_region(MKDEV(major, 0), 1, "my_char_dev");

    // 初始化字符设备结构
    cdev_init(&c_dev, &fops);

    // 添加字符设备到设备类
    class = class_create(THIS_MODULE, "my_char_class");
    device = device_create(class, NULL, dev_num, NULL, "my_char_dev");

    // 注册字符设备
    cdev_add(&c_dev, dev_num, 1);

    return 0;
}

static void __exit dev_exit(void)
{
    // 删除字符设备
    cdev_del(&c_dev);

    // 移除设备
    device_destroy(class, dev_num);

    // 销毁设备类
    class_unregister(class);

    // 注销字符设备区域
    unregister_chrdev_region(dev_num, 1);
}

module_init(dev_init);
module_exit(dev_exit);
MODULE_LICENSE("GPL");

2.4 链接

链接阶段是将编译后的各个目标文件合并成一个可执行文件的过程。对于Linux内核而言,这个过程将生成最终的内核映像文件(通常是vmlinuz)。链接阶段还包括生成符号表、重定位等操作。

2.4.1 链接命令

使用make命令进行链接:

make bzImage
2.4.2 生成最终映像

最终的内核映像通常会被压缩,并加上引导加载程序所需要的头部信息。生成的最终映像文件可以是vmlinuzzImage等形式。

cp arch/x86/boot/bzImage /boot/vmlinuz

2.5 创建模块

除了核心内核之外,还有许多功能是以模块的形式存在的,这些模块可以在系统运行时动态加载。创建模块的过程包括编译模块源代码,并生成模块文件(通常扩展名为.ko)。

2.5.1 模块编译命令
make modules
2.5.2 安装模块

将编译好的模块安装到系统中:

make modules_install

2.6 生成模块依赖关系

生成模块依赖关系,确保模块在加载时可以找到所需的其他模块。

make modules_prepare

3. Linux内核加载过程

加载内核是启动操作系统的关键一步,它由引导加载程序(Bootloader)完成。引导加载程序负责加载内核到内存,并将控制权传递给内核。以下是加载内核的主要步骤:

3.1 加载引导加载程序

计算机启动时,BIOS/UEFI会加载引导加载程序到内存,并执行引导加载程序。常用的引导加载程序有GRUB、LILO等。

3.1.1 GRUB示例

使用GRUB加载内核:

grub> kernel /boot/vmlinuz root=/dev/sda1 ro
grub> initrd /boot/initrd.img
grub> boot

3.2 加载内核映像

引导加载程序读取并加载内核映像到内存中。内核映像通常位于存储设备的某个分区或扇区中。

3.2.1 内核映像的结构

内核映像通常包含以下部分:

  • 压缩的内核映像:使用gzipbzip2压缩的内核映像。
  • 引导加载程序的头部信息:包含了引导加载程序所需的引导参数。

3.3 初始化内核

加载内核映像后,引导加载程序会跳转到内核的入口点,开始执行内核代码。内核初始化过程包括设置内存管理、初始化设备驱动、加载模块等。

3.3.1 内核初始化过程

内核初始化过程包括:

  1. 设置内存页表:初始化内存管理。
  2. 初始化硬件设备:初始化CPU、内存控制器等。
  3. 初始化中断向量表:设置中断处理机制。
  4. 初始化系统调用表:设置系统调用表,以便用户空间程序调用。
  5. 初始化进程管理:设置进程调度器,初始化进程管理数据结构。
  6. 初始化文件系统:挂载根文件系统,初始化文件系统管理数据结构。
  7. 初始化网络堆栈:初始化网络协议栈,设置网络设备。
示例代码
#include <linux/kernel.h>
#include <linux/init.h>
#include <asm/system.h>
#include <asm/processor.h>
#include <asm/io.h>
#include <asm/setup.h>
#include <asm/irq.h>

void __init early_printk(const char *fmt, ...)
{
    static char *console_output = NULL;
    static int console_output_baud = 0;
    static int console_output_line = 0;
    char *p;
    va_list args;

    va_start(args, fmt);
    p = vprintf(fmt, args);
    va_end(args);

    if (console_output) {
        put_port(console_output, p);
        console_output_line++;
        if (console_output_line >= 25)
            console_output_line = 0;
    }
}

asmlinkage void __init start_kernel(void)
{
    extern void __init trap_setup(void);
    extern void __init mem_init(void);
    extern void __init mem_setup(void);
    extern void __init setup_arch(char **cmdline);
    extern void __init secondary_cpu_boot(void);

    // 初始化架构相关
    setup_arch(&command_line);

    // 设置内存管理
    mem_setup();

    // 设置内存页表
    mem_init();

    // 设置中断向量表
    trap_setup();

    // 初始化硬件设备
    early_irq_setup();

    // 初始化系统调用表
    setup_syscalls();

    // 初始化进程管理
    init_idle_boot_cpu();

    // 初始化文件系统
    initrd_load();

    // 初始化网络堆栈
    init_network_namespace();

    // 启动其他CPU
    secondary_cpu_boot();
}

3.4 启动初始进程

内核初始化完成后,会启动初始进程initinit进程的PID为1,它是所有用户空间进程的父进程。init进程会读取/etc/inittab文件,根据配置启动相应的守护进程和服务。

3.4.1 init进程示例
static int start_init(void)
{
    struct file *filp;
    struct dentry *dentry;
    struct inode *inode;
    struct task_struct *task;

    // 创建初始进程
    task = alloc_task_struct();
    task->state = TASK_RUNNING;
    task->pid = 1;
    task->comm = "init";

    // 打开并执行"/sbin/init"
    filp = filp_open("/sbin/init", O_RDONLY | O_EXEC, 0755);
    if (IS_ERR(filp))
        return PTR_ERR(filp);

    // 创建进程并执行
    task->thread = kthread_create(execve, filp, "init");
    if (IS_ERR(task->thread))
        return PTR_ERR(task->thread);

    // 启动进程
    wake_up_process(task->thread, TASK_UNINTERRUPTIBLE, 0);

    return 0;
}

3.5 系统初始化

init进程启动后,会继续执行一系列初始化脚本和配置文件,完成系统的初始化工作,包括启动网络服务、挂载文件系统、启动用户界面等。

3.5.1 inittab文件示例
::system:/sbin/init
::respawn:/sbin/getty 38400 tty1
::respawn:/sbin/getty 38400 tty2
::respawn:/sbin/getty 38400 tty3
::respawn:/sbin/getty 38400 tty4
::respawn:/sbin/getty 38400 tty5
::respawn:/sbin/getty 38400 tty6

3.6 系统初始化脚本

系统初始化脚本通常位于/etc/rc.d/rc.local/etc/init.d目录下,这些脚本会在init进程启动后被执行。

示例初始化脚本
#!/bin/sh

# 检查是否启用网络
if [ "$NETWORKING" = "yes" ]; then
    /etc/init.d/networking start
fi

# 检查是否启用SSH
if [ "$SSH" = "yes" ]; then
    /etc/init.d/ssh start
fi

# 如果启用了显示管理器,则启动显示管理器
if [ "$DISPLAY_MANAGER" = "yes" ]; then
    /etc/init.d/gdm start
fi

# 执行用户定义的脚本
for script in /etc/rc.local.d/*.sh; do
    if [ -x "$script" ]; then
        . "$script"
    fi
done

# 进入多用户模式
exec /sbin/init -- rc 3

4. Linux内核模块管理

Linux内核模块是可动态加载和卸载的内核组件,允许内核在运行时扩展其功能。模块化设计使得Linux内核具有很高的灵活性。

4.1 模块编译

模块编译通常使用make modules命令来完成。编译完成后,模块文件会保存在lib/modules/目录下。

make modules

4.2 模块加载

模块可以使用insmodmodprobe等命令加载到内核中。加载模块后,内核会根据模块提供的功能扩展其能力。

insmod /path/to/module.ko
modprobe module_name

4.3 模块卸载

模块可以使用rmmod命令从内核中卸载。

rmmod module_name

4.4 模块初始化和清理

模块需要实现module_initmodule_exit函数,用于模块的初始化和卸载。

示例代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

// 模块初始化函数
static int __init mod_init(void)
{
    printk(KERN_INFO "Module loaded.\n");
    return 0;
}

// 模块退出函数
static void __exit mod_exit(void)
{
    printk(KERN_INFO "Module unloaded.\n");
}

// 初始化模块入口
module_init(mod_init);

// 卸载模块入口
module_exit(mod_exit);

// 指定模块许可
MODULE_LICENSE("GPL");

5. 小结

Linux内核的编译和加载是操作系统启动的关键步骤。通过理解内核的编译过程和加载机制,我们可以更好地掌握Linux系统的工作原理,并在开发和维护Linux系统时更加得心应手。希望本文能够为读者提供一个全面了解Linux内核编译和加载的视角,并为深入学习Linux内核打下坚实的基础。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值