OpenEuler 内核编程入门与系统调用机制

举报
pluto1447 发表于 2025/12/23 20:27:03 2025/12/23
【摘要】 本实验深入探索了 OpenEuler 内核开发基础与 LKM(可加载内核模块)机制。通过对比用户态(Ring 3)与内核态(Ring 0)在权限等级、内存栈空间及稳定性风险上的本质差异,确立了严谨的内核编程意识。 实践环节成功实现了基础内核模块的编写、编译与动态加载。

OpenEuler 内核编程入门与系统调用机制

一、 实验目的

  1. 掌握内核开发基础:学会编写、编译并加载 Linux 内核模块(LKM),这是后续进行进程调度与内存管理实验的必备技能。
  2. 透视系统调用机制:跳过 C 库函数封装,使用汇编指令直接触发系统调用,理解用户态(Ring 3)与内核态(Ring 0)的边界及交互方式。
  3. 理解内核通信接口:通过 /proc 文件系统实现用户态与内核态的数据交换,建立“一切皆文件”的内核交互观。
  4. 体验 OpenEuler 特性:初步使用 A-Tune 工具,了解操作系统对性能的自动化感知能力。

二、 实验环境

  1. 基础平台:OpenEuler 22.03/24.03 虚拟机。
  2. 开发工具:VS Code (Remote-SSH 连接), gcc, make。
  • 安装命令 :
sudo dnf install kernel-devel-$(uname -r) gcc make
  1. 开发套件:GCC, Make, Kernel-Devel 包

三、 实验内容与步骤

在编写第一行 .ko 代码之前,请务必阅读本章节。Linux 内核开发环境与普通应用程序开发环境(C/C++)有着天壤之别。如果你带着用户态的编程习惯进入内核,可能会导致系统崩溃。

1. 内核概念知识科普

1.1.什么是内核

如果把操作系统内核(Kernel)比作是一个精密运转的巨大工厂,用户态程序则是工厂外面的顾客。顾客只能通过窗口(系统调用)喊话:“给我打印一行字!”或者“给我读个文件!”。如果出现死循环、内存越界等,内核就会将他Kill,工厂不受影响。

而一旦进入内核内部,规则就会发生改变。内核编程没有stdio.h,printf等C语言标准库,需要用到内核内部专用的函数(如printk),并且内核操作具有一定的风险,一旦不慎,可能会导致系统崩溃

1.2.OpenEuler内核概念科普

openEuler (欧拉) 内核 是 openEuler 操作系统的核心组件。它基于 Linux 内核社区的长期支持(LTS)版本(如 Linux 4.19, 5.10, 6.6 等)进行开发,并针对服务器、云计算、边缘计算和嵌入式等多种场景进行了深度的优化和增强。

1.2.1 宏观架构:宏内核与模块化

openEuler 是典型的 宏内核 (Monolithic Kernel) 操作系统。

  • 这意味着什么? 文件系统、设备驱动、进程调度、网络协议栈等所有核心组件,都编译在一个巨大的二进制文件(vmlinuz)中,运行在同一个内存地址空间。
  • 带来的问题:如果每次修改驱动都要重新编译整个内核并重启,效率太低。
  • 解决方案 (LKM)可加载内核模块 (Loadable Kernel Module)。它允许你在系统运行时,像插入 USB 设备一样,动态地将一段代码“插入”到内核中运行,用完后再“拔出”。本实验就是教你如何制造这种“插件”。

1.2.2. 权限等级:Ring 0 vs Ring 3

CPU 硬件层面提供了不同的特权级(Rings),Linux 主要使用两级:

  • Ring 3 (用户态)
    • 内容:Shell, Vim, Python, 你的 C 作业。
    • 限制:受限模式。不能直接访问硬件,访问内存受限。一旦出错(如空指针),内核会捕获并杀死该进程(SegFault),系统不受影响。
  • Ring 0 (内核态)
    • 内容:内核代码、驱动程序、你即将编写的模块。
    • 权限:你可以执行任何 CPU 指令(包括关机、停止 CPU),访问任何内存地址。
    • 风险:内核代码一旦发生非法内存访问,没有“容错率”。结果就是 Kernel Panic (内核恐慌),也就是俗称的死机/蓝屏,必须断电重启。

1.2.3 极小的栈空间 (Stack Overflow)

在用户态写程序,定义一个 char buf[1024*1024] (1MB) 的局部变量通常没问题,因为用户栈很大(通常 8MB)。
在内核态,这种做法坚决不行!

  • 内核栈大小:非常小,通常只有 8KB16KB(视架构而定)。
  • 后果:如果你在函数里定义巨大的局部数组,或者递归调用层级太深,会瞬间击穿栈底,覆盖掉重要的内核数据,导致系统立即崩溃。
  • 建议:大块内存必须使用 kmalloc 动态分配,不要使用巨大的局部变量。

1.2.4. 并发与重入 (Concurrency)

你的用户态作业通常是单线程的,但在内核里,并发是常态

  • 你的模块函数可能同时被多个 CPU 执行。
  • 你的代码正在执行时,可能会被中断(Interrupt)打断。
  • 结论:在后续的高级内核开发中,必须时刻注意全局变量的保护(使用自旋锁 Spinlock 或 互斥体 Mutex),防止数据竞争。虽然本实验暂不涉及锁,但要有这个意识。

核心速查表 (Cheat Sheet)

场景 用户态 (User Space) 内核态 (Kernel Space)
头文件 #include <stdio.h> #include <linux/kernel.h>
入口 main() module_init(...)
打印 printf(...) printk(...)
日志查看 终端直接显示 使用命令 dmesg 查看
内存分配 malloc() / free() kmalloc() / kfree()
栈空间 很大 (MB级) 极小 (8KB - 16KB)
浮点数 随意使用 (double, float) 严禁使用 (需特殊手段,一般禁用)
出错后果 进程退出 (Coredump) 系统死机 (System Halt)

2.编写第一个内核模块

为了避免每次修改内核功能都重新编译整个操作系统,OpenEuler 提供了 LKM (Loadable Kernel Module) 机制。后续实验中,我们观测 PCB、页表等都需要用到它。

2.1.编写内核模块程序hello_module.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

// 模块加载时的入口函数
static int __init hello_init(void) {
    printk(KERN_INFO "OpenEuler Lab: Hello, Kernel World!\n");
    return 0;
}

// 模块卸载时的出口函数
static void __exit hello_exit(void) {
    printk(KERN_INFO "OpenEuler Lab: Goodbye, Kernel World!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Student Name");
MODULE_DESCRIPTION("A simple introductory module");

2.2.编写makefile程序

obj-m += hello_module.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

2.3.编译运行

make
# 加载模块 (进入内核态)
sudo insmod hello_module.ko
# 查看内核日志 (验证代码是否在 Ring 0 执行)
sudo dmesg | tail  

# 卸载模块
sudo rmmod hello_module  
sudo dmesg | tail 

3.内核模块交互

接下来,我们将使用模块参数来实现简单的交互,将参数传递给内核模块,这在内核驱动开发中非常常用。

3.1.编写模块程序param_test.c

我们要写一个模块,加载时可以接受一个整数(比如 PID)和一个字符串。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

// 定义变量
static int target_pid = 0;
static char *who_am_i = "Student";

// 注册参数
// module_param(变量名, 类型, 权限位)
module_param(target_pid, int, 0644);
module_param(who_am_i, charp, 0644);
MODULE_PARM_DESC(target_pid, "An integer PID to verify");

static int __init param_init(void) {
    printk(KERN_INFO "Lab: Module loaded.\n");
    printk(KERN_INFO "Lab: Input PID = %d\n", target_pid);
    printk(KERN_INFO "Lab: Who are you? -> %s\n", who_am_i);
    return 0;
}

static void __exit param_exit(void) {
    printk(KERN_INFO "Lab: Module unloaded.\n");
}

module_init(param_init);
module_exit(param_exit);
MODULE_LICENSE("GPL");

3.2.编写Makefile文件

# 核心配置:将 param_test.o 编译为模块
obj-m += param_test.o

# 指向当前运行系统的内核源码构建目录
KDIR := /lib/modules/$(shell uname -r)/build
# 获取当前目录路径
PWD := $(shell pwd)

all:
	make -C $(KDIR) M=$(PWD) modules

clean:
	make -C $(KDIR) M=$(PWD) clean

3.2.编译并运行

make
# 传递参数加载
sudo insmod param_test.ko target_pid=1024 who_am_i="OpenEuler"
# 查看日志
sudo dmesg | tail

可以看到,我们顺利的完成了内核模块的参数传递

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。