ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

操作系统实验2实战:系统调用、进程控制与内核模块开发指南

操作系统实验2实战:系统调用、进程控制与内核模块开发指南 简介这份资源是西南科技大学计算机操作系统课程实验2的配套代码面向正在修读该课程、需要完成实验任务或复习操作系统核心机制的本科生。压缩包内仅含1个cpp源文件体积约1KB属于轻量级实验代码便于直接编译运行与阅读分析。实验内容围绕进程管理、内存管理、文件系统、设备管理、死锁预防与避免、线程与并发、系统调用等操作系统核心知识点展开通过编程实践加深对调度算法、页面置换策略、银行家算法等原理的理解。目前已有2061人学习下载说明该实验在课程学习中具有较高的参考需求。读者可借助这份代码对照实验要求梳理实现思路、验证算法逻辑并在此基础上完成实验报告中的结果分析与问题排查适合作为课程实验的起步参考与调试对照材料。1. 操作系统实验2到底在练什么从系统调用到内核模块的落地路径很多同学拿到“计算机操作系统实验2”这个题目时第一反应是去翻教材目录看看实验一做了什么实验二大概率是进程调度还是内存管理。但真正动手之后才发现实验2的跨度往往比想象中大——它可能要求你从用户态写一个系统调用封装也可能让你直接改内核源码编译模块。西南科技大学的操作系统课程实验通常配套汤小丹版教材实验2常见的落点是系统调用、进程控制、或内核模块编程这三类之一。这篇文章不针对某一份具体实验指导书而是把这三类任务里最通用的做法、参数和踩坑点讲透让你不管拿到哪种变体都能找到对应的操作路径。适合已经跑通实验一、对Linux基本命令不陌生、但一碰内核层就心里没底的同学。2. 先搞清楚实验2的三种常见形态系统调用、进程控制、内核模块2.1 系统调用型实验在用户态和内核态之间加一道门系统调用实验的核心目标是让你理解“用户程序请求内核服务”的完整链路。典型任务是新增一个系统调用比如返回当前进程的某些统计信息或者实现一个简单的字符设备读写接口。这类实验的难点不在写业务逻辑而在于把新系统调用挂进内核的调用表、重新编译内核、验证调用号。很多同学卡在编译完内核后启动失败或者调用号对不上导致返回-1。从选型角度看如果你用的是较新的内核比如5.x以上直接改sys_call_table的方式已经不太推荐因为该符号不再导出。更稳妥的做法是写一个内核模块通过kallsyms_lookup_name找到系统调用表地址再替换或者干脆用sysfs/procfs接口模拟系统调用的行为。但如果是课程实验明确要求“新增系统调用号”那就得按老路子走改arch/x86/entry/syscalls/syscall_64.tbl加一行自定义调用号再在kernel/sys.c里实现函数体。这里有个血泪经验调用号不要选已经被占用的。x86_64的调用号从0到几百都有分配你选一个靠后的比如548先查unistd_64.h确认没冲突。编译前记得make mrproper清干净否则残留的.config会让新调用号不生效。2.2 进程控制型实验fork、exec、wait的铁三角如果实验2落在进程控制上任务通常是让你用fork()创建子进程用exec()族函数加载新程序再用wait()或waitpid()回收。这类实验看起来简单但僵尸进程和孤儿进程的边界情况是老师最爱考的。比如父进程不调用wait()就退出子进程变成孤儿被init收养或者子进程先退出父进程没回收留下僵尸。我一般会让学生先写一个最小可复现的框架父进程fork两次分别测试正常回收、父进程先退出、子进程先退出三种场景用ps命令观察进程状态。参数上重点调waitpid()的optionsWNOHANG是非阻塞WUNTRACED也返回停止的子进程。如果你在实验里看到子进程状态是Z那就是僵尸说明父进程没wait。2.3 内核模块型实验从hello world到字符设备内核模块实验的入门任务是写一个hello_module加载时打印信息卸载时也打印。进阶任务是实现一个字符设备支持open、read、write、release。这类实验的坑集中在内核版本差异和并发控制上。比如register_chrdev在2.6以后推荐用alloc_chrdev_region加cdev_init老教材的代码直接抄会编译报错。参数方面module_init和module_exit是入口出口MODULE_LICENSE(GPL)必须加否则内核会报污染警告。字符设备的major号可以设0让内核动态分配也可以指定一个未使用的静态主设备号。我一般建议动态分配避免和已有设备冲突。读写函数里要注意copy_to_user和copy_from_user的返回值检查用户态指针不能直接解引用。3. 用QEMU加BusyBox搭一个可调试的实验环境3.1 为什么不用虚拟机而用QEMU很多同学在VMware或VirtualBox里装Ubuntu编译内核后重启一旦起不来就得重装。用QEMU加BusyBox的好处是启动快、快照方便、内核panic了直接看串口输出。你可以在宿主机上编译内核用-kernel参数直接加载bzImage根文件系统用BusyBox做的initramfs。整个环境不到100MB复制给别人也能跑。具体步骤先下载BusyBox源码make defconfig后make install把_install目录做成cpio归档。然后写一个init脚本挂载proc和sysfs最后启动一个shell。QEMU命令里加-nographic把串口重定向到终端加-s -S可以配合gdb调试内核。# 编译BusyBox并制作initramfs wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2 tar xf busybox-1.36.1.tar.bz2 cd busybox-1.36.1 make defconfig # 开启静态编译避免依赖宿主机的动态库 sed -i s/^# CONFIG_STATIC.*/CONFIG_STATICy/ .config make -j$(nproc) make install cd _install # 创建必要的目录 mkdir -p proc sys dev etc # 写init脚本 cat init EOF #!/bin/sh mount -t proc none /proc mount -t sysfs none /sys echo Welcome to the OS lab environment exec /bin/sh EOF chmod x init # 打包成cpio find . | cpio -o -H newc | gzip ../initramfs.cpio.gz这段脚本的关键点是CONFIG_STATICy静态编译后BusyBox不依赖宿主机的glibc放到QEMU里才能跑。init脚本里挂载proc和sysfs是必须的否则很多内核模块实验没法读取系统信息。打包用newc格式这是内核推荐的initramfs格式。3.2 QEMU启动命令与内核编译参数编译内核时make defconfig生成的配置够用但为了调试方便建议打开CONFIG_DEBUG_INFO和CONFIG_GDB_SCRIPTS。如果实验涉及系统调用还要确认CONFIG_FTRACE和CONFIG_KPROBES是开启的。编译命令用make -j$(nproc)生成的arch/x86/boot/bzImage就是QEMU要加载的内核。启动命令如下qemu-system-x86_64 \ -kernel linux-5.15/arch/x86/boot/bzImage \ -initrd initramfs.cpio.gz \ -append consolettyS0 nokaslr \ -nographic \ -s -SconsolettyS0把内核日志输出到串口nokaslr关闭地址随机化方便gdb下断点。-s在1234端口开gdb服务-S让QEMU启动时暂停等你连上gdb再继续。如果你不需要调试去掉-s -S即可。启动后你会看到BusyBox的shell这时候可以insmod你的内核模块或者运行用户态测试程序。提示如果QEMU启动后卡在Starting kernel ...大概率是initramfs打包有问题检查find . | cpio命令是否在_install目录下执行以及init脚本是否有可执行权限。4. 系统调用实验的完整实现从调用号分配到用户态验证4.1 修改系统调用表和实现函数体假设实验要求新增一个系统调用sys_myinfo返回当前进程的PID和父进程PID。第一步是在arch/x86/entry/syscalls/syscall_64.tbl里加一行548 common myinfo sys_myinfo548是自定义调用号选一个没被占用的。然后打开kernel/sys.c在文件末尾加函数实现// kernel/sys.c 末尾添加 SYSCALL_DEFINE2(myinfo, pid_t __user *, pid, pid_t __user *, ppid) { pid_t kpid task_tgid_vnr(current); pid_t kppid task_ppid_nr(current); if (copy_to_user(pid, kpid, sizeof(pid_t))) return -EFAULT; if (copy_to_user(ppid, kppid, sizeof(pid_t))) return -EFAULT; return 0; }SYSCALL_DEFINE2宏展开后会生成sys_myinfo函数参数个数是2。task_tgid_vnr(current)获取当前进程的PIDtask_ppid_nr(current)获取父进程PID。copy_to_user把内核数据拷回用户态返回值非0表示失败返回-EFAULT。这里必须用copy_to_user不能直接写用户态指针否则会触发内核oops。4.2 用户态测试程序与编译验证内核编译完成后写一个用户态程序调用syscall(548, pid, ppid)// test_myinfo.c #include stdio.h #include unistd.h #include sys/syscall.h int main(void) { pid_t pid -1, ppid -1; long ret syscall(548, pid, ppid); if (ret ! 0) { perror(syscall 548 failed); return 1; } printf(myinfo: pid%d, ppid%d\n, pid, ppid); printf(getpid%d, getppid%d\n, getpid(), getppid()); return 0; }编译用gcc -static -o test_myinfo test_myinfo.c静态编译后放进initramfs。运行结果应该和getpid()、getppid()一致。如果不一致检查task_tgid_vnr和task_ppid_nr的用法这两个函数在linux/sched.h里声明。参数说明syscall()的第一个参数是调用号后面依次是系统调用的参数。copy_to_user的第二个参数是内核缓冲区地址第三个是长度。如果用户态传了NULL指针copy_to_user会返回非0你的系统调用应该返回-EFAULT而不是崩溃。注意修改系统调用表后必须重新编译内核只编译模块是不生效的。编译前用make clean清一下kernel/sys.o确保改动被纳入。5. 避坑与排查实验2最容易翻车的五个地方5.1 内核编译通过但启动后找不到新系统调用现象syscall(548, ...)返回-1errno是ENOSYS。原因调用号没生效或者内核根本没加载你编译的版本。常见情况是QEMU启动时用了旧的bzImage或者syscall_64.tbl修改后没有重新生成unistd_64.h。解决确认make之后arch/x86/include/generated/uapi/asm/unistd_64.h里有__NR_myinfo 548。如果没有删掉arch/x86/include/generated目录重新编译。QEMU命令里检查-kernel路径指向新编译的bzImage。5.2 内核模块加载时报“invalid module format”现象insmod hello.ko提示invalid module format或version magic不匹配。原因模块编译时用的内核版本和QEMU里运行的内核版本不一致。比如你在宿主机上编译模块但QEMU里跑的是另一个版本的内核。解决模块必须在目标内核的源码树里编译。用make -C /path/to/linux M$PWD modules指定内核源码路径。编译前确认/lib/modules/$(uname -r)/build指向正确的源码树。如果是交叉环境用ARCH和CROSS_COMPILE变量。5.3 字符设备读写返回“Bad address”现象用户态read()或write()返回-1errno是EFAULT。原因内核模块里直接用了用户态指针没有通过copy_to_user/copy_from_user。解决所有从用户态传进来的指针都必须用copy_*_user函数访问。检查read和write的实现确保没有*(buf)这样的直接解引用。另外注意copy_to_user返回的是未拷贝的字节数返回0表示成功。5.4 fork之后子进程输出重复现象用printf打印信息fork之后父进程和子进程都输出而且顺序混乱。原因printf有缓冲区fork会复制缓冲区内容。如果fork之前有未flush的输出子进程会继承并重复输出。解决fork之前用fflush(stdout)清空缓冲区或者直接用write(1, ...)系统调用绕过缓冲区。更稳妥的做法是在fork之后分别用不同的输出函数避免依赖缓冲状态。5.5 QEMU启动后键盘无响应现象QEMU窗口里能看到内核日志但敲键盘没反应。原因-nographic模式下串口和标准输入输出绑定但如果没有正确配置consolettyS0shell可能跑在另一个tty上。解决内核命令行加consolettyS0init脚本里确保exec /bin/sh是在串口上。如果用的是-serial mon:stdio按Ctrl-A然后C可以切换到QEMU monitor再按Ctrl-A然后H看帮助。退出QEMU用Ctrl-A然后X。6. 用ftrace和gdb验证系统调用路径一个可复用的调试习惯实验做完不算完能证明你的系统调用确实被内核执行了才算。我一般会用ftrace抓一下调用栈。在QEMU的shell里# 挂载debugfs mount -t debugfs none /sys/kernel/debug # 设置ftrace过滤器只抓myinfo cd /sys/kernel/debug/tracing echo sys_myinfo set_ftrace_filter echo function current_tracer echo 1 tracing_on # 运行测试程序 ./test_myinfo echo 0 tracing_on cat tracetrace文件里会显示sys_myinfo被调用的记录包括调用者和时间戳。如果没抓到说明你的系统调用没进内核或者ftrace没配置对。这个方法比加printk更干净不用重新编译内核。另一个习惯是用gdb单步。QEMU启动时加-s -S宿主机上运行gdb vmlinux然后target remote :1234在sys_myinfo上下断点。vmlinux是编译内核时生成的带符号文件在源码根目录。gdb连上后按c继续QEMU里的测试程序一执行就会断在sys_myinfo。这时候可以bt看调用栈info registers看寄存器p current-pid看当前进程。这个流程走一遍你对“用户态到内核态”的理解会比看十遍教材都深。我自己的习惯是每次改完内核代码先make -j$(nproc)然后QEMU启动用ftrace确认函数被调用再用gdb确认参数传递正确。这套组合拳打下来实验2的验收基本不会翻车。希望帮到你。本文还有配套的精品资源点击获取
返回列表