尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

西南科技大学操作系统实验2:系统调用与内核模块实战指南

西南科技大学操作系统实验2:系统调用与内核模块实战指南 简介这份资源是西南科技大学计算机操作系统课程实验2的配套代码面向正在修读该课程或自学操作系统核心机制的本科生与考研复习者用于通过编程实践理解进程调度、内存管理、文件系统等抽象概念。压缩包内共1个文件为cpp源码体积约1KB属于轻量级实验代码便于直接编译运行与对照调试。目前已有2061人学习下载说明其在课程实验场景中具有一定参考价值。代码围绕实验2的具体任务展开读者可借助它梳理进程创建与调度、同步互斥、页面置换或系统调用等知识点的实现思路并对照自己的实验方案查漏补缺。对于需要提交实验报告或准备操作系统笔试的学生而言这份源码可作为理解算法流程、验证输出结果的辅助材料帮助把课堂理论落到可运行的代码层面。1. 西南科技大学操作系统实验2从系统调用到内核模块的落地路线如果你正在搜“计算机操作系统实验2 西南科技大学”大概率不是想听一遍进程调度概念而是手里已经有一个实验任务书需要把代码跑起来、把现象解释清楚、把报告写扎实。操作系统实验最尴尬的地方在于理论课讲调度算法、内存管理、文件系统实验课却常常只给一个空壳框架剩下的靠你自己补。西南科技大学这门课的实验2通常落在“系统调用/内核模块/进程通信”这条线上要求你在 Linux 环境下改内核或写用户态程序观察内核行为。它适合已经学过进程、内存、文件基本概念但还没真正进过内核态的同学。这篇笔记按“环境搭好 → 最小可跑 → 参数调对 → 踩坑排查”的顺序走你照着做能复现也能看懂每一步为什么这么做。2. 实验环境搭建把内核编译和模块加载跑通操作系统实验和普通 C 语言实验最大的区别是你的代码运行在特权级出错不是段错误而是内核 panic 或者机器直接卡死。所以环境搭建不是走形式它决定了你后面调试时有没有后悔药。这一章把编译内核、加载模块、准备快照三件事讲清楚。2.1 选内核版本与装依赖为什么我建议锁 5.15 LTS很多同学一上来就用发行版自带内核结果发现头文件路径对不上、make modules_prepare报错。常见做法是单独下载一份 LTS 内核源码和当前运行内核解耦。我一般选 5.15因为它的模块 API 稳定网上资料多编译时间也可控。先装依赖Ubuntu/Debian 系sudo apt update sudo apt install -y build-essential libncurses-dev bison flex \ libssl-dev libelf-dev bc dwarves zstd这几行不是随便列的libncurses-dev给make menuconfig用bison flex是内核构建的语法分析工具libssl-dev和libelf-dev处理签名与 ELFdwarves提供pahole生成 BTF 信息。少一个都可能在编译中途报错。下载并解压内核源码wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.15.tar.xz tar -xf linux-5.15.tar.xz cd linux-5.15参数说明版本号5.15是主版本如果你实验指导书指定了别的版本以指导书为准。解压后目录名就是linux-5.15后面所有命令都在这个目录里执行。2.2 配置与编译defconfig 起步别一上来就 menuconfig新手最容易翻车的地方是直接make menuconfig然后乱关选项编出来的内核起不来。稳妥路线是用defconfig打底只改必要的几项。make defconfig # 打开模块卸载与调试符号方便实验观察 scripts/config --enable CONFIG_MODULE_UNLOAD scripts/config --enable CONFIG_DEBUG_INFO scripts/config --disable CONFIG_DEBUG_INFO_BTF make -j$(nproc)逻辑说明defconfig生成一份通用配置scripts/config是内核自带的配置修改脚本比手点菜单可靠。关掉CONFIG_DEBUG_INFO_BTF是因为部分环境pahole版本不匹配会中断编译实验阶段不需要 BTF。-j$(nproc)用满 CPU 核数编译时间从半小时降到几分钟。编译完成后装模块和内核sudo make modules_install sudo make install sudo update-grub注意make install会把新内核装进/bootupdate-grub更新启动项。重启时在 GRUB 菜单选新内核进入。这一步之前强烈建议给虚拟机打一个快照内核起不来是常态有快照才有后悔药。2.3 验证环境写一个最小内核模块环境是否可用用一个 hello 模块验证最快。// hello.c #include linux/init.h #include linux/module.h #include linux/kernel.h static int __init hello_init(void) { printk(KERN_INFO hello: module loaded\n); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO hello: module unloaded\n); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(os-lab); MODULE_DESCRIPTION(minimal module for os experiment 2);配套 Makefileobj-m hello.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逻辑说明obj-m表示编译成可加载模块KDIR指向当前运行内核的构建目录M$(PWD)告诉内核构建系统去外部目录找源码。编译后sudo insmod hello.ko用dmesg | tail看输出出现hello: module loaded就说明环境通了。printk的KERN_INFO是日志级别决定消息进哪个缓冲区实验报告里解释日志级别是个加分点。3. 系统调用实验从用户态进内核态的最小闭环实验2如果涉及系统调用核心是理解“用户态通过软中断陷入内核内核根据系统调用号分发”。这一章给一个可复现的自定义系统调用流程并解释每一步的参数含义。3.1 系统调用号与 syscall 表改哪里、为什么改Linux 的系统调用通过sys_call_table分发x86_64 下系统调用号定义在arch/x86/entry/syscalls/syscall_64.tbl。新增一个系统调用要动三处表项、函数声明、函数实现。在syscall_64.tbl末尾找一个未使用的号比如 548548 64 my_oslab_syscall sys_my_oslab_syscall逻辑说明第一列是系统调用号第二列64表示 64 位 ABI第三列是用户态调用的名字第四列是内核实现函数名。号不能和已有冲突否则会覆盖原系统调用这是典型的翻车点。在include/linux/syscalls.h加声明asmlinkage long sys_my_oslab_syscall(int pid);asmlinkage告诉编译器参数从栈上取这是系统调用约定的要求漏了会导致参数错乱。3.2 实现系统调用用 pid 查进程信息在kernel/sys.c末尾加实现SYSCALL_DEFINE1(my_oslab_syscall, int, pid) { struct task_struct *task; struct pid *pid_struct; pid_struct find_get_pid(pid); if (!pid_struct) return -ESRCH; task get_pid_task(pid_struct, PIDTYPE_PID); if (!task) { put_pid(pid_struct); return -ESRCH; } printk(KERN_INFO oslab: pid%d comm%s state%ld\n, pid, task-comm, task-state); put_task_struct(task); put_pid(pid_struct); return 0; }逻辑说明SYSCALL_DEFINE1是内核提供的宏数字 1 表示一个参数后面跟参数类型和名字。find_get_pid通过 pid 号找到struct pidget_pid_task拿到对应的task_struct。task-comm是进程名task-state是进程状态。关键在引用计数find_get_pid和get_pid_task都会增加引用用完必须put_pid和put_task_struct否则内存泄漏跑久了系统会变慢。这是内核编程和用户态编程最大的思维差异。重新编译安装内核重启后写用户态测试// test_syscall.c #include unistd.h #include sys/syscall.h #include stdio.h #define __NR_my_oslab_syscall 548 int main(int argc, char *argv[]) { int pid atoi(argv[1]); long ret syscall(__NR_my_oslab_syscall, pid); printf(syscall returned %ld\n, ret); return 0; }编译gcc test_syscall.c -o test_syscall运行./test_syscall 1再用dmesg | tail看内核输出。参数说明__NR_my_oslab_syscall必须和表里的号一致syscall()是 glibc 提供的通用入口第一个参数是调用号后面是实际参数。3.3 用 strace 验证调用路径光看 dmesg 不够要证明用户态确实进了内核。用strace跟踪strace -e tracemy_oslab_syscall ./test_syscall 1如果 strace 显示my_oslab_syscall(1) 0说明调用链完整。注意strace 依赖系统调用名表新加的系统调用可能需要更新 strace 的映射看不到名字时用-e traceall看调用号 548 也行。这一步是实验报告里“验证方法”的硬证据比只贴代码有说服力。4. 进程与内存观察把内核数据结构打印出来很多操作系统实验2会要求观察进程调度或内存分配。与其空谈理论不如写一个模块把当前进程链表和内存信息打印出来用真实数据说话。4.1 遍历进程链表for_each_process 的正确用法#include linux/sched.h #include linux/sched/signal.h static void dump_processes(void) { struct task_struct *task; int count 0; for_each_process(task) { printk(KERN_INFO oslab: pid%d ppid%d comm%s\n, task-pid, task-real_parent-pid, task-comm); count; if (count 20) break; } printk(KERN_INFO oslab: total shown%d\n, count); }逻辑说明for_each_process是内核提供的宏遍历所有进程的task_struct链表。real_parent指向父进程pid是进程号。加count限制是因为进程多的时候 printk 会刷屏甚至拖慢系统。这个宏内部用了 RCU 或锁保护直接遍历是安全的但不要在遍历中睡眠。4.2 读取内存信息用 si_meminfo 拿全局数据#include linux/mm.h #include linux/swap.h static void dump_memory(void) { struct sysinfo si; si_meminfo(si); printk(KERN_INFO oslab: totalram%lu free%lu\n, si.totalram (PAGE_SHIFT - 10), si.freeram (PAGE_SHIFT - 10)); }逻辑说明si_meminfo填充struct sysinfototalram和freeram单位是页左移PAGE_SHIFT - 10转成 KB。PAGE_SHIFT在 x86 上通常是 12即 4KB 页。参数换算容易错实验报告里写清楚单位换算过程是加分项。4.3 模块参数让实验可配置而不是写死把上面的功能做成模块用模块参数控制行为static int show_proc 1; static int show_mem 1; module_param(show_proc, int, 0644); module_param(show_mem, int, 0644); static int __init oslab_init(void) { if (show_proc) dump_processes(); if (show_mem) dump_memory(); return 0; }逻辑说明module_param定义模块参数第三个参数0644是 sysfs 权限。加载时可以sudo insmod oslab.ko show_proc0只打印内存。这样一套代码能覆盖多个实验小问不用反复改代码重编。参数类型支持 int、charp、bool 等实验里用 int 做开关最方便。5. 避坑与排查内核实验翻车的五条血泪经验内核实验的坑和用户态完全不是一个量级用户态崩了顶多进程退出内核态崩了可能整机无响应。下面五条是我踩过或者看别人踩过的按“现象 → 原因 → 解决”写。5.1 insmod 报 Invalid module format现象insmod: ERROR: could not insert module hello.ko: Invalid module format。原因模块编译时用的内核版本和当前运行内核不一致或者vermagic不匹配。常见于你编译了新内核但没重启还在旧内核上加载新模块。解决uname -r确认当前内核modinfo hello.ko | grep vermagic看模块的版本串两者必须一致。不一致就重启进对应内核或者用/lib/modules/$(uname -r)/build重新编译。5.2 加载后 dmesg 没有输出现象insmod成功但dmesg | tail看不到 printk。原因printk 日志级别低于控制台级别消息进了缓冲区但没显示或者你 tail 的行数不够被其他日志淹了。解决dmesg -w实时看或者dmesg | grep oslab过滤。确认printk带了KERN_INFO或更高级别。控制台级别可以用cat /proc/sys/kernel/printk查看第一个数字是当前级别。5.3 卸载模块时报 Device or resource busy现象rmmod oslab报Module oslab is in use。原因模块引用计数不为零通常是模块里创建了 proc 文件、字符设备没清理或者有进程还持有模块提供的资源。解决检查lsmod | grep oslab的第三列引用计数。在__exit函数里确保释放所有注册的资源remove_proc_entry、unregister_chrdev一个都不能漏。实在退不出重启是最快的办法但报告里要写清楚原因。5.4 内核 panic 后看不到任何日志现象加载模块瞬间机器卡死或重启重启后 dmesg 里没有崩溃前的信息。原因panic 发生时日志还没落盘或者串口/控制台没配置。虚拟机里尤其常见。解决给虚拟机加串口输出GRUB 里加consolettyS0用-serial file:serial.log启动 QEMU 或 VMware 的串口文件。这样 panic 信息会写进文件。另外可以开kdump但配置复杂实验阶段串口够用。5.5 系统调用号冲突导致原有功能异常现象加完自定义系统调用后某个常用命令行为异常比如ls报奇怪的错误。原因系统调用号和已有号冲突覆盖了原实现。syscall_64.tbl里号是唯一的抄别人的号最容易出这事。解决grep -r 548 arch/x86/entry/syscalls/确认号没被占用选一个明显靠后的号。改完重新编译内核别只重编模块系统调用表是编进内核镜像的。6. 进阶技巧用 ftrace 和 bpftrace 验证内核行为实验做完不等于做透。真正让报告有深度的是“我怎么知道内核真的按我想的执行了”。这一章给两个验证手段都是内核自带的不需要额外装重型工具。6.1 ftrace 跟踪系统调用看调用次数和耗时ftrace 挂在/sys/kernel/debug/tracing先确认 debugfs 挂载mount | grep debugfs || sudo mount -t debugfs none /sys/kernel/debug跟踪自定义系统调用cd /sys/kernel/debug/tracing echo function current_tracer echo sys_my_oslab_syscall set_ftrace_filter echo 1 tracing_on ./test_syscall 1 echo 0 tracing_on cat trace | tail -20逻辑说明current_tracer设为function表示跟踪函数调用set_ftrace_filter只保留关心的函数避免日志爆炸。tracing_on是开关。输出里能看到调用栈和耗时报告里贴一段 trace 比空口说“调用了”强得多。参数上set_ftrace_filter支持通配符比如sys_*oslab*。6.2 bpftrace 一行命令统计进程状态分布bpftrace 适合做聚合统计比如统计当前所有进程的状态分布sudo bpftrace -e BEGIN { [comm] count(); }更贴近实验的写法统计各状态进程数sudo bpftrace -e kprobe:__x64_sys_my_oslab_syscall { [pid] count(); }逻辑说明kprobe挂在内核函数入口[pid]是按 pid 聚合的 mapcount()计数。这条命令能验证你的系统调用被哪些进程调用了多少次。bpftrace 需要内核支持 BPF5.15 默认支持。如果报权限错误检查kernel.unprivileged_bpf_disabled和是否用了 sudo。6.3 把验证结果写进报告一张表说清楚实验报告里最容易被扣分的是“只贴代码不贴证据”。我一般会整理一张验证表验证项命令预期输出实际输出模块加载dmesg | grep oslabmodule loaded一致系统调用返回./test_syscall 1returned 0一致调用次数bpftrace kprobecount 0一致进程遍历dmesg | grep pid列出进程一致这张表把“我做了”变成“我验证了”是实验2从及格到优秀的分水岭。参数和命令都来自前面章节不需要额外工具。最后说个习惯每次改内核代码前先git diff或备份原文件编译前打快照加载模块前想好怎么卸载。内核实验没有后悔药但有快照和日志。希望帮到你。本文还有配套的精品资源点击获取
返回列表