OpenEuler 内核编程入门与系统调用机制
OpenEuler 内核编程入门与系统调用机制
一、 实验目的
- 掌握内核开发基础:学会编写、编译并加载 Linux 内核模块(LKM),这是后续进行进程调度与内存管理实验的必备技能。
- 透视系统调用机制:跳过 C 库函数封装,使用汇编指令直接触发系统调用,理解用户态(Ring 3)与内核态(Ring 0)的边界及交互方式。
- 理解内核通信接口:通过 /proc 文件系统实现用户态与内核态的数据交换,建立“一切皆文件”的内核交互观。
- 体验 OpenEuler 特性:初步使用 A-Tune 工具,了解操作系统对性能的自动化感知能力。
二、 实验环境
- 基础平台:OpenEuler 22.03/24.03 虚拟机。
- 开发工具:VS Code (Remote-SSH 连接), gcc, make。
- 安装命令 :
sudo dnf install kernel-devel-$(uname -r) gcc make
- 开发套件: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)。
在内核态,这种做法坚决不行!
- 内核栈大小:非常小,通常只有 8KB 或 16KB(视架构而定)。
- 后果:如果你在函数里定义巨大的局部数组,或者递归调用层级太深,会瞬间击穿栈底,覆盖掉重要的内核数据,导致系统立即崩溃。
- 建议:大块内存必须使用
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

可以看到,我们顺利的完成了内核模块的参数传递
- 点赞
- 收藏
- 关注作者
评论(0)