简介:这份华中科技大学计算机学院操作系统课程设计报告,面向计算机专业学生及操作系统自学者,提供一份完整的课程设计参考范例。报告围绕Linux系统展开,涵盖编程环境搭建、添加系统调用、设备驱动程序开发、/proc文件系统分析、文件拷贝程序与GTK+图形界面并发进程展示等核心实验内容,并涉及Ubuntu下依赖管理与内核编译流程,适合作为系统级编程入门与课设复盘的实操材料。资源包内含1个doc文档,压缩包约1.22MB,结构完整、条理清晰,便于按实验模块对照学习。目前已有404人学习下载。读者可从中获取各实验的设计思路、关键代码片段与执行结果分析,理解进程管理、内存管理、文件系统等内核机制,并借鉴报告的组织框架与排错经验,为自身课程设计或系统开发实践提供参考。
1. 从一份操作系统课设报告说起:为什么“改内核”比“写应用”更能拉开差距
很多人做操作系统课程设计,第一反应是写个生产者-消费者、模拟个页面置换算法就交差。这类题目不是不好,但它绕开了操作系统最硬核的部分——内核本身。真正能让你在答辩现场被追问二十分钟还站得住的,是那种“我改过内核、我加过系统调用、我写过驱动”的方案。这份课程设计报告围绕 Linux 编程环境、系统调用添加、设备驱动模块、/proc 文件系统分析四条主线展开,环境是 Ubuntu 11.10 加 Linux 3.0.0 内核,工具链是 gcc、make、GTK+。它适合两类人:一是正在做操作系统课设、需要一份能直接照着走的完整流程的同学;二是想理解“用户态到内核态到底怎么打通”的开发者。下面我按需求、设计、实现、验收的顺序,把这份方案拆开讲透。
2. 环境搭建与编程基础:从零把 Linux 内核实验环境跑起来
2.1 虚拟机与内核源码的版本对齐
这套方案的第一步不是写代码,而是把环境搭对。很多同学在这一步翻车,不是因为命令敲错,而是版本没对齐。报告里用的是 VMware 虚拟机装 Ubuntu 11.10(32 位),内核头文件是 linux-headers-3.0.0-12-generic,源码包通过apt-get install linux-source-3.0.0获取。这里有一个血泪经验:源码版本必须和当前运行内核版本尽量接近,否则后面编译出来的模块加载会报 “version magic” 不匹配。
# 查看当前内核版本,确认与源码包一致 uname -r # 下载对应版本的内核源码 sudo apt-get install linux-source-3.0.0 # 解压到 /usr/src 目录 sudo tar -xjvf /usr/src/linux-source-3.0.0.tar.bz2 -C /usr/srcuname -r输出的是当前运行内核的完整版本号,linux-source-3.0.0是源码包名,解压后会在/usr/src/linux-source-3.0.0下得到完整内核树。注意:如果你用的是 64 位系统,添加系统调用时的头文件路径和调用号表位置会不同,报告里明确建议换 32 位环境,这是踩过坑之后的结论。
2.2 root 登录与开发工具链安装
Ubuntu 默认不允许 root 直接登录,但内核实验大量操作需要 root 权限。报告里给了一个直接有效的办法:修改 lightdm 配置文件,开启手动登录。
# 先以普通用户登录,设置 root 密码 sudo passwd root # 编辑 lightdm 配置 sudo vi /etc/lightdm/lightdm.conf # 在文件末尾加入 greeter-show-manual-login=true # 重启后即可用 root 登录 sudo rebootgreeter-show-manual-login=true这一行的作用是让登录界面出现手动输入用户名的入口。重启后输入 root 和刚设置的密码即可。接下来装工具链:
# 安装基本编译工具 sudo apt-get install build-essential # 如果依赖冲突,用 aptitude 自动解决 sudo apt-get install aptitude sudo aptitude install build-essential # 安装 GTK 开发库,用于图形界面实验 sudo apt-get install libgtk2.0-devbuild-essential包含 gcc、g++、make、gdb 等;aptitude比 apt-get 更擅长处理依赖关系,遇到装不上的包先换它试试;libgtk2.0-dev是 GTK+ 2.0 的开发包,报告里特别提到“只要装这一个包就能搞定 GTK 环境”,不需要额外配置文件。这一步做完,编译带图形界面的 C 程序就不会报找不到头文件的错了。
3. 文件拷贝与多进程图形显示:把用户态编程基本功打扎实
3.1 带健壮性检查的文件拷贝程序
第一个实验任务是用 C 语言写一个文件拷贝程序。看起来简单,但报告里强调的不是“能拷贝就行”,而是健壮性。核心逻辑是按字符读写:
#include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { FILE *fsource, *ftarget; int c; // 参数个数必须为3:程序名、源文件、目标文件 if (argc != 3) { printf("Error in argc!\n"); return 0; } // 以只读二进制方式打开源文件 if ((fsource = fopen(argv[1], "rb")) == NULL) { printf("Error in open source file!\n"); return 0; } // 以写入二进制方式创建目标文件 if ((ftarget = fopen(argv[2], "wb")) == NULL) { printf("Error in open target file!\n"); return 0; } // 逐字符读取并写入 while ((c = fgetc(fsource)) != EOF) { fputc(c, ftarget); } fclose(fsource); fclose(ftarget); return 0; }argc != 3保证用户输入了源文件和目标文件两个参数;fopen的"rb"和"wb"分别表示二进制只读和二进制写入,避免文本模式下换行符被转换;fgetc返回EOF时结束循环。编译命令是gcc -o mycopy mycopy.c,运行./mycopy source.txt target.txt。这个程序虽然小,但它是后面系统调用版本文件拷贝的用户态对照——你先知道用户态怎么做,才能理解内核态为什么要用sys_open、sys_read、sys_write。
3.2 fork 加 GTK 实现三窗口并发显示
第二个任务更有意思:用fork()创建两个子进程,加上父进程,三个进程各自弹出一个 GTK 窗口,窗口标题显示自己的 PID 和父进程 PID。这背后考的是进程并发和图形库的结合。
#include <gtk/gtk.h> #include <unistd.h> #include <stdio.h> void show(int argc, char *argv[], char *title) { GtkWidget *window, *vbox, *label, *button; char id_char[128]; gtk_init(&argc, &argv); window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); gtk_window_set_title(GTK_WINDOW(window), title); gtk_container_set_border_width(GTK_CONTAINER(window), 20); vbox = gtk_vbox_new(FALSE, 10); gtk_container_add(GTK_CONTAINER(window), vbox); // 显示当前进程PID sprintf(id_char, "%s ,My ID:%d", title, getpid()); label = gtk_label_new(id_char); gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 10); gtk_widget_show(label); // 显示父进程PID sprintf(id_char, "父进程 ID:%d", getppid()); label = gtk_label_new(id_char); gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 10); gtk_widget_show(label); button = gtk_button_new_with_label("close"); g_signal_connect_swapped(G_OBJECT(button), "clicked", G_CALLBACK(gtk_widget_destroy), window); gtk_box_pack_start(GTK_BOX(vbox), button, FALSE, FALSE, 10); gtk_widget_show(button); gtk_widget_show_all(window); gtk_main(); } int main(int argc, char *argv[]) { pid_t pid_1, pid_2; if ((pid_1 = fork()) == 0) { printf("Child 1# is running......\n"); show(argc, argv, "This is Child 1#"); } else { if ((pid_2 = fork()) == 0) { printf("Child 2# is running......\n"); show(argc, argv, "This is Child 2#"); } else { printf("Parent # is running......\n"); show(argc, argv, "Parent #"); } } return 0; }fork()返回值等于 0 表示子进程,大于 0 表示父进程中返回的子进程 PID。getpid()取当前进程号,getppid()取父进程号。gtk_init初始化 GTK 并解析命令行参数,gtk_main进入事件循环。编译命令要带 pkg-config:
gcc -o forkgtk forkgtk.c `pkg-config --cflags --libs gtk+-2.0`反引号里的pkg-config会自动补全 GTK 的头文件路径和链接库。运行后会看到三个窗口同时出现,每个窗口显示不同的 PID,关掉任意一个窗口不影响其他两个——这就是多进程并发的直观体现。
4. 添加系统调用:从用户态到内核态打通一条完整链路
4.1 内核源码修改的三个关键位置
这是整个课设里最耗时的部分,也是最能体现“你真的改过内核”的环节。报告里明确说“投入了比较多的时间”,因为不同内核版本的添加方法有差异,网上资料甚至互相矛盾。下面按 Linux 3.0.0 的流程走。
第一步,修改内核版本后缀,方便区分自己编译的内核:
# 编辑内核源码根目录的 Makefile sudo vi /usr/src/linux-source-3.0.0/Makefile # 找到 EXTRAVERSION 行,加上自己的标识 EXTRAVERSION = .69.zcyEXTRAVERSION会附加到内核版本号后面,编译出来的内核就是3.0.69.zcy,在启动菜单里一眼就能认出来。
第二步,在kernel/sys.c末尾添加系统调用实现:
asmlinkage int sys_zcycopyfile(const char *s_file, const char *t_file) { const int BUF_SIZE = 512; int fin, fout; char buf[BUF_SIZE]; int copy_count; mm_segment_t fs; fs = get_fs(); set_fs(get_ds()); // 允许内核态访问用户态地址空间 // 打开源文件,失败返回-1 if ((fin = sys_open(s_file, O_RDONLY, S_IRUSR)) == -1) { printk("Error in open source file!\n"); return -1; } // 创建并打开目标文件,失败返回-2 if ((fout = sys_open(t_file, O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR)) == -1) { printk("Error in open target file!\n"); return -2; } // 循环读写,失败返回-3 while ((copy_count = sys_read(fin, buf, BUF_SIZE))) { if (copy_count == -1 || sys_write(fout, buf, copy_count) == -1) { printk("Error in copy file!\n"); return -3; } } set_fs(fs); // 恢复段寄存器 return 0; }asmlinkage告诉编译器参数从栈上取,这是系统调用的约定。get_fs()和set_fs()用来临时扩大内核态可访问的地址范围,因为sys_open等函数需要读取用户态传进来的文件路径字符串。sys_open、sys_read、sys_write是内核内部已经实现好的系统调用函数,直接调用即可。返回值设计成 0 成功、负数失败,方便用户态判断。
第三步,注册系统调用号。修改arch/x86/include/asm/unistd_32.h:
#define __NR_sys_zcycopyfile 347 #define NR_syscalls 348__NR_sys_zcycopyfile是分配给新系统调用的编号,347 是当前最后一个调用号的下一个。NR_syscalls要同步加 1,否则内核不知道多了一个调用。
第四步,在arch/x86/kernel/syscall_table_32.s中加入入口:
.long sys_zcycopyfile /* 347 */这张表是系统调用号到函数地址的映射。用户程序执行int 0x80软中断后,内核根据调用号在这张表里找到函数入口,跳转到sys.c里的实现。
4.2 编译安装内核与测试验证
源码改完后,编译安装流程如下:
cd /usr/src/linux-source-3.0.0 # 清理残留文件 sudo make mrproper # 安装 ncurses 库,menuconfig 需要 sudo apt-get install libncurses5-dev # 配置内核,直接选 EXIT 用默认配置 sudo make menuconfig # 建立依赖关系 sudo make dep # 编译内核镜像,-j9 表示用 9 个线程并行编译 sudo make bzImage -j9 # 编译内核模块 sudo make modules -j9 # 安装模块和内核 sudo make modules_install sudo make installmake mrproper清除之前编译留下的.o文件和配置,保证干净构建。-j9的数值取决于 CPU 核心数,报告里说“能很大提高效率”,但不要超过 CPU 支持的最大线程数。编译完成后,把新内核复制到 boot 目录并生成 initrd:
sudo cp arch/i386/boot/bzImage /boot/vmlinuz-3.0.69.zcy sudo mkinitramfs 3.0.69.zcy -o /boot/initrd.img-3.0.69-zcy sudo update-grub2mkinitramfs生成初始化内存盘,update-grub2自动更新启动菜单。报告里还建议把/boot/grub/grub.cfg里的timeout改成 100 秒,给自己留足选择系统的时间。重启后选择新内核进入,用uname -r确认版本号变成3.0.69.zcy。
测试程序zcopy.c通过syscall(347, src, dst)调用新系统调用:
#include <stdio.h> #include <unistd.h> #include <sys/syscall.h> int main(int argc, char *argv[]) { if (argc != 3) { printf("Usage: %s <source> <target>\n", argv[0]); return 1; } // 347 是注册的系统调用号 int ret = syscall(347, argv[1], argv[2]); if (ret == 0) { printf("Copy success!\n"); } else { printf("Copy failed, error code: %d\n", ret); } return 0; }syscall()是 glibc 提供的通用系统调用入口,第一个参数是调用号,后面是传给内核的参数。编译运行后如果目标文件内容和源文件一致,说明整条链路打通了。
5. 字符设备驱动与 /proc 文件系统:内核模块的加载与信息暴露
5.1 字符设备驱动的注册与文件操作接口
第三个实验是写一个字符设备驱动模块。Linux 把设备抽象成文件,驱动就是一组操作函数的集合。报告里把驱动分成三部分:注册设备、定义功能函数、卸载设备。
#include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/uaccess.h> #define DEVICE_NAME "zcydev" static int major; static char msg[256] = "Hello from kernel module!\n"; static struct cdev zcy_cdev; // open 函数:设备被打开时调用 static int zcy_open(struct inode *inode, struct file *file) { printk(KERN_INFO "zcydev opened\n"); return 0; } // read 函数:用户态读取设备时调用 static ssize_t zcy_read(struct file *file, char __user *buf, size_t len, loff_t *offset) { int n = strlen(msg); if (*offset >= n) return 0; if (len > n - *offset) len = n - *offset; // copy_to_user 把内核数据复制到用户空间 if (copy_to_user(buf, msg + *offset, len)) return -EFAULT; *offset += len; return len; } static struct file_operations zcy_fops = { .owner = THIS_MODULE, .open = zcy_open, .read = zcy_read, }; // 模块加载函数 static int __init zcy_init(void) { dev_t dev; // 动态申请主设备号 alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME); major = MAJOR(dev); cdev_init(&zcy_cdev, &zcy_fops); cdev_add(&zcy_cdev, dev, 1); printk(KERN_INFO "zcydev registered, major=%d\n", major); return 0; } // 模块卸载函数 static void __exit zcy_exit(void) { dev_t dev = MKDEV(major, 0); cdev_del(&zcy_cdev); unregister_chrdev_region(dev, 1); printk(KERN_INFO "zcydev unregistered\n"); } module_init(zcy_init); module_exit(zcy_exit); MODULE_LICENSE("GPL");alloc_chrdev_region让内核自动分配一个可用的主设备号,避免和已有设备冲突。cdev_init和cdev_add把file_operations结构注册到内核。copy_to_user是必须用的,不能直接memcpy,因为用户空间和内核空间地址不互通。module_init和module_exit分别指定加载和卸载时执行的函数。编译用 Makefile:
obj-m += zcydev.o KDIR := /lib/modules/$(shell uname -r)/build all: make -C $(KDIR) M=$(PWD) modules clean: make -C $(KDIR) M=$(PWD) cleanobj-m表示编译成可加载模块,KDIR指向当前内核的构建目录。make之后生成zcydev.ko,用insmod zcydev.ko加载,dmesg看内核日志确认主设备号,然后mknod /dev/zcydev c <major> 0创建设备节点,用户程序open和read这个设备文件就能拿到内核里的字符串。
5.2 /proc 文件系统的信息读取与分析
第四个实验是理解和分析/proc文件系统。/proc是内核提供的虚拟文件系统,不占磁盘空间,读它就是在读内核数据结构。报告里要求通过/proc获取系统实时状态。
# 查看 CPU 信息 cat /proc/cpuinfo # 查看内存使用 cat /proc/meminfo # 查看当前进程列表 ls /proc | grep -E '^[0-9]+$' # 查看指定进程的状态 cat /proc/1/status/proc/cpuinfo里的processor字段表示逻辑 CPU 编号,model name是型号。/proc/meminfo的MemTotal和MemAvailable分别表示总内存和可用内存。每个数字目录对应一个进程 PID,里面的status文件包含进程名、状态、父进程 PID 等信息。如果要在自己的驱动里也暴露一个/proc入口,可以用proc_create和seq_file接口,但报告里只要求“理解和分析”,所以重点是能读懂这些文件的内容,知道它们对应内核里的哪些结构。
6. 避坑与常见问题:那些让课设卡住一整天的细节
6.1 内核编译报错“version magic 不匹配”
现象:模块insmod时报 “disagrees about version of symbol module_layout” 或 “version magic ... should be ...”。原因:编译模块用的内核源码版本和当前运行内核版本不一致,或者EXTRAVERSION改了但没重新编译安装内核。解决:确认uname -r和/lib/modules/$(uname -r)/build指向同一版本,如果改了EXTRAVERSION,必须完整走一遍编译安装流程并重启进入新内核。
6.2 系统调用返回 -1 但不知道哪一步失败
现象:测试程序调用syscall(347, ...)返回 -1,但不知道是打开源文件失败还是创建目标文件失败。原因:内核里的printk输出到了内核日志,用户态看不到。解决:用dmesg | tail查看内核日志,或者在用户态根据返回值区分——报告里设计了 -1、-2、-3 三个错误码,分别对应源文件打开失败、目标文件创建失败、读写失败。
6.3 GTK 程序编译报“找不到 gtk/gtk.h”
现象:gcc -o forkgtk forkgtk.c报头文件不存在。原因:没有加pkg-config参数,编译器不知道 GTK 头文件在哪。解决:编译命令写成gcc -o forkgtk forkgtk.c \pkg-config --cflags --libs gtk+-2.0`,反引号不能省。如果还报错,确认libgtk2.0-dev` 是否安装成功。
6.4 虚拟机里编译内核时间过长或卡死
现象:make bzImage跑了很久没反应,或者虚拟机直接卡住。原因:虚拟机分配的内存和 CPU 核心数不够,并行编译-j数值设得太大导致资源耗尽。解决:给虚拟机至少 2GB 内存和 2 个 CPU 核心,-j数值不要超过核心数。如果卡死,强制重启后先make clean再重新编译。
6.5 新内核启动后找不到旧系统入口
现象:重启后 grub 菜单里只有新内核,进不去原来的系统。原因:update-grub2更新了启动项,旧内核入口被折叠到 “Previous Linux versions” 子菜单里。解决:在 grub 菜单里选 “Previous Linux versions” 展开就能看到旧内核。报告里也提到这一点,并且建议把timeout改大,给自己留足选择时间。
7. 验收与答辩:怎么证明你的内核真的改了
验收不是把报告交上去就完事。答辩现场导师最常问的三个问题是:你怎么证明系统调用真的加进去了?驱动模块真的加载了吗?/proc 里的数据你怎么解读?下面是我带项目时总结的一套验收流程,按这个走,基本不会被问倒。
第一步,现场演示uname -r输出3.0.69.zcy,证明你编译的新内核确实启动了。第二步,运行zcopy测试程序,拷贝一个文本文件,然后用diff对比源文件和目标文件,输出为空表示内容完全一致。第三步,dmesg | grep zcy查看内核日志里有没有驱动注册和系统调用的打印信息。第四步,lsmod | grep zcydev确认模块已加载,cat /dev/zcydev读出内核里的字符串。第五步,打开/proc/cpuinfo和/proc/meminfo,指着具体字段解释它对应内核里的哪个数据结构。
| 验收项 | 验证命令 | 预期结果 |
|---|---|---|
| 新内核启动 | uname -r | 输出 3.0.69.zcy |
| 系统调用可用 | ./zcopy src.txt dst.txt && diff src.txt dst.txt | 无输出,文件一致 |
| 驱动模块加载 | lsmod | grep zcydev | 显示模块信息 |
| 设备读写 | cat /dev/zcydev | 输出内核字符串 |
| 内核日志 | dmesg | grep zcy | 有注册和调用记录 |
| /proc 分析 | cat /proc/meminfo | 能解释 MemTotal 等字段 |
答辩时还有一个技巧:提前把关键截图和命令输出整理成一页纸,导师问什么直接指给他看,比现场敲命令稳得多。我一般还会准备一个“如果编译失败怎么回退”的说明——grub 里选旧内核启动,删掉/boot下新内核文件,update-grub2恢复。这套后悔药备着,现场就不慌。
从那以后我每次做内核相关的实验,都强制走一遍“改版本号、编译、重启、验证、记录”的完整闭环,绝不跳过任何一步。希望这份拆解能帮到你,把课设从“能跑就行”做到“经得起追问”。
本文还有配套的精品资源,点击获取