
简介《华中科技大学计算机学院操作系统课程设计报告.doc》面向计算机专业学生及准备操作系统课程设计的学习者围绕Linux环境搭建、内核代码结构、系统调用添加、设备驱动开发、/proc文件系统分析等实验环节展开并记录文件拷贝程序与GTK图形界面并发进程展示的设计过程。压缩包内仅1个doc文件约1.22MB以完整实验报告形式组织包含环境配置、实现思路、关键代码段与运行结果说明便于对照课程要求梳理实验流程和排错要点。已有405人学习适合需要参考报告结构、补齐系统级编程实践或复盘Linux内核实验的读者使用。1. 这份操作系统课程设计报告值不值得拆四个实验把 Linux 内核入门串成一条线很多人学操作系统时能背进程调度却不敢碰一行内核代码。这份某高校计算机学院的操作系统课程设计报告把 Linux 编程环境、文件拷贝、GTK 三进程窗口、添加系统调用、字符设备驱动和 /proc 分析串成了一条可动手的线。互联网上流传的内核实验教程版本混杂2.6、3.0、5.x 的目录和入口表经常对不上这份报告把环境钉在 Ubuntu 11.10 加 3.0.0 内核反而让复现路径变窄了。它适合正在做课程设计、想补内核编译和模块加载经验、或者面试前需要把“系统调用到底怎么走”讲清楚的人。报告不是百科全书但里面的命令顺序、修改点和踩坑记录比只看概念更有落地价值。2. 把环境、文件拷贝和 GTK 三进程一次跑通Ubuntu 11.10 的依赖与编译参数2.1 为什么老版本环境反而是可复现的捷径这份报告的核心实验是往 3.0 内核里加系统调用而 3.0 的 32 位入口表还叫syscall_table_32.S系统调用号、unistd_32.h和sys.c的写法与后来的 4.x、5.x 差异很大。直接拿新内核教程套老报告最常见的结果是编译报“找不到 sys_open”或者入口表路径不存在。把环境固定在 Ubuntu 11.10 desktop i386、内核linux-headers-3.0.0-12-generic、源码linux-source-3.0.0虽然老但和报告里的每一步能对上。虚拟机软件用常见的桌面虚拟化工具即可关键是给内核编译留足磁盘和内存编译前打快照后面翻车能回滚。项目建议值用途发行版Ubuntu 11.10 desktop i386与 3.0 内核、32 位系统调用表匹配当前内核头文件linux-headers-3.0.0-12-generic编译外部模块时提供构建目录内核源码linux-source-3.0.0修改 sys.c、入口表和 Makefile编译工具gcc、g、gdb、make用户态程序和内核编译图形库libgtk2.0-devGTK 三进程窗口实验终端库libncurses5-devmake menuconfig依赖依赖工具aptitude处理 apt 依赖冲突提示老版本 apt 源可能已经不可用常见做法是挂载安装镜像做本地源或者在能连通的环境里先下载好 deb 包再拷进虚拟机。不要在这个环节反复重装系统先把源配通再往下走。工具链安装不用一次装太多先把编译和 GTK 依赖补齐。build-essential提供 gcc、make 等基础工具libgtk2.0-dev提供 GTK2.0 的头文件和pkg-config描述文件libncurses5-dev是后面make menuconfig的刚需。报告里提到用aptitude解决依赖这个经验在老 Ubuntu 上确实有用因为apt-get遇到依赖冲突时经常直接停住aptitude会给出降级或替换方案。# 基础编译工具先装这个再装图形库 sudo apt-get update sudo apt-get install build-essential # GTK2.0 开发包报告里只装 libgtk2.0-dev 就能编译 sudo apt-get install libgtk2.0-dev # 内核配置菜单依赖终端库 sudo apt-get install libncurses5-dev # 依赖冲突时用 aptitude 兜底 sudo apt-get install aptitude sudo aptitude install build-essential装完后别急着写代码先验证三件事gcc --version能输出版本pkg-config --modversion gtk-2.0能输出 2.x 的版本号make --version正常。pkg-config这一行很关键后面编译 GTK 程序要靠它自动带出-I和-l参数手写库路径容易漏掉依赖。root 登录的修改也要在这一步做掉否则后面改内核源码时频繁sudo会把节奏拖慢。报告里改的是/etc/lightdm/lightdm.conf在文件末尾加greeter-show-manual-logintrue然后重启。这个操作的本质是让登录界面显示手动输入用户名的入口从而允许 root 登录。改之前先备份原文件改完用sudo reboot重启。如果改错了导致进不了图形界面可以在恢复模式里把这一行删掉或者用快照回滚。# 给 root 设置密码后续编译安装内核会频繁用到 sudo passwd root # 备份登录管理器配置 sudo cp /etc/lightdm/lightdm.conf /etc/lightdm/lightdm.conf.bak # 在文件末尾追加手动登录选项 sudo sh -c echo greeter-show-manual-logintrue /etc/lightdm/lightdm.conf # 重启后选择“其他用户”输入 root 和刚设置的密码 sudo reboot内核源码用apt-get install linux-source-3.0.0安装装完一般在/usr/src下得到linux-source-3.0.0.tar.bz2。解压命令是tar -xjvf linux-source-3.0.0.tar.bz2 -C /usr/src解压后得到/usr/src/linux-source-3.0.0。这里有个容易忽略的点源码目录名要和后面 Makefile 里的版本后缀区分开源码目录只是工作目录编译出来的内核版本由 Makefile 的EXTRAVERSION决定。# 安装与当前环境接近的 3.0 内核源码包 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/src # 确认目录存在后面 cd 进去做 make mrproper ls -ld /usr/src/linux-source-3.0.02.2 文件拷贝argc、fopen 和 EOF 循环里的健壮性报告的第一个用户态实验是写一个 C 程序完成文件拷贝。核心逻辑不复杂用fopen打开源文件和目标文件循环fgetc读、fputc写直到EOF。真正值得看的是它对错误处理的要求参数个数必须是 3源文件必须能读目标文件必须能创建并写入。很多课程设计的文件拷贝只写 happy path一跑就过但遇到路径错误、权限不足或者目标磁盘满时直接崩掉后面调试内核实验时也会沿用这种坏习惯。#include stdio.h #include stdlib.h int main(int argc, char *argv[]) { FILE *fsource NULL; FILE *ftarget NULL; int c; /* 参数个数必须是 3程序名、源文件、目标文件 */ if (argc ! 3) { printf(Usage: %s source target\n, argv[0]); return 1; } /* 以二进制读方式打开源文件失败直接返回 */ fsource fopen(argv[1], rb); if (fsource NULL) { perror(open source); return 2; } /* 以二进制写方式创建目标文件失败要先关闭源文件 */ ftarget fopen(argv[2], wb); if (ftarget NULL) { perror(open target); fclose(fsource); return 3; } /* fgetc 返回 int不能声明成 char否则 EOF 判断会出错 */ while ((c fgetc(fsource)) ! EOF) { if (fputc(c, ftarget) EOF) { perror(write target); fclose(fsource); fclose(ftarget); return 4; } } fclose(fsource); fclose(ftarget); return 0; }这段代码的参数含义很直接argv[1]是源文件路径argv[2]是目标文件路径返回值用不同数字区分错误阶段方便脚本判断。rb和wb里的b在 Linux 上不改变换行行为但养成二进制模式习惯后拷贝图片、压缩包不会出玄学问题。fgetc的返回值必须用int接这是血泪经验用char接会把0xFF和EOF混淆大文件拷贝到一半停住或者多写一个字节查起来很费时间。编译和测试用三条命令就够。先准备一个源文件跑完cmp对比输出为空说明字节一致。如果cmp报差异先看是不是文本编辑器自动加了换行再看fputc的返回值有没有被忽略。# 编译开 -Wall 把隐式声明和未使用变量揪出来 gcc -Wall -o mycopy mycopy.c # 准备测试文件并拷贝 echo kernel demo source.txt ./mycopy source.txt target.txt # 字节级对比没有输出就是一致 cmp source.txt target.txt echo copy ok2.3 fork 出三个 GTK 窗口父子进程 PID 和事件循环第二个用户态实验是把进程并发和图形界面放在一起父进程 fork 出两个子进程三个进程各自弹一个 GTK 窗口窗口标题显示自己的 PID 和父进程 PID。这个实验的价值不在 GTK 控件多复杂而在于让你亲眼看到fork之后父子进程各自走各自的gtk_main三个窗口同时存在关掉一个不影响另外两个。报告里用fork的返回值区分父子进程父进程再 fork 第二个子进程结构是典型的二段式。#include gtk/gtk.h #include sys/types.h #include unistd.h #include stdio.h static void destroy_progress(GtkWidget *widget, gpointer data) { gtk_main_quit(); } static void show_window(int argc, char *argv[], const char *title) { GtkWidget *window; GtkWidget *vbox; GtkWidget *label; GtkWidget *button; char id_char[256]; 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); /* 窗口关闭时退出当前进程的 GTK 主循环 */ g_signal_connect(G_OBJECT(window), destroy, G_CALLBACK(destroy_progress), NULL); vbox gtk_vbox_new(FALSE, 10); gtk_container_add(GTK_CONTAINER(window), vbox); /* 显示当前进程 PID 和父进程 PID */ sprintf(id_char, %s, My PID: %d, title, getpid()); label gtk_label_new(id_char); gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 10); sprintf(id_char, Parent PID: %d, getppid()); label gtk_label_new(id_char); gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 10); 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_all(window); gtk_main(); _exit(0); } int main(int argc, char *argv[]) { pid_t pid1, pid2; pid1 fork(); if (pid1 0) { show_window(argc, argv, Child 1#); } else if (pid1 0) { pid2 fork(); if (pid2 0) { show_window(argc, argv, Child 2#); } else if (pid2 0) { show_window(argc, argv, Parent #); } } return 0; }这段代码的关键参数是fork的返回值0 表示子进程大于 0 表示父进程里拿到的子进程 PID小于 0 表示 fork 失败。三个进程各自调用gtk_init和gtk_main互不共享 GTK 主循环所以能同时显示三个窗口。g_signal_connect_swapped把按钮点击信号转到gtk_widget_destroy窗口销毁后触发destroy信号再调用gtk_main_quit退出当前进程。_exit(0)放在gtk_main后面避免子进程从show_window返回后继续执行父进程逻辑。编译命令里的反引号不能省pkg-config --cflags --libs gtk-2.0会展开成头文件路径和链接库。如果报gtk/gtk.h: No such file or directory先确认libgtk2.0-dev装了再确认pkg-config能查到 gtk-2.0。运行前检查$DISPLAY是否有值没有图形环境时窗口起不来但进程可能还在容易误判成代码问题。# 编译 GTK 程序反引号里的 pkg-config 负责带出头文件和库参数 gcc -Wall -o forkgtk forkgtk.c pkg-config --cflags --libs gtk-2.0 # 确认图形环境变量存在 echo $DISPLAY # 运行后应出现三个窗口标题分别显示 PID 和 PPID ./forkgtk关窗口时如果只关一个另外两个还在这是正常的因为三个进程各自有独立的事件循环。父进程窗口关闭后子进程会变成孤儿进程被 init 接管所以更稳的做法是在父进程里用waitpid回收两个子进程或者至少知道这个行为别在实验报告里写“关掉父窗口三个窗口全退”。这个实验真正练的是fork加事件循环的边界不是 GTK 布局。3. 添加系统调用从 sys.c 到 syscall_table_32.S 的完整链路3.1 系统调用链路int 0x80、入口表和 asmlinkage用户态程序调用syscall(347, src, dst)时CPU 从用户态切到内核态32 位 Linux 走的是int 0x80软中断。内核根据系统调用号 347 去入口表里找函数指针入口表就是syscall_table_32.S里的一串.long第 347 项指向sys_democopyfile然后跳到sys.c里的函数实现。函数前面的asmlinkage告诉编译器参数从栈上取而不是走寄存器这是系统调用约定的一部分。系统调用函数不能直接解引用用户态传进来的指针因为内核态和用户态的地址空间语义不同。报告里的写法是先fs get_fs()保存当前段设置再set_fs(get_ds())临时切换到内核数据段让sys_open、sys_read、sys_write能按用户态指针访问内存最后set_fs(fs)恢复。这个操作在 3.0 内核里常见但边界很敏感拷贝完成后必须恢复否则后续内核代码可能访问异常。修改文件修改点作用MakefileEXTRAVERSION .demo生成 3.0.69.demo 版本后缀区分新旧内核kernel/sys.c添加sys_democopyfile函数真正的文件拷贝实现arch/x86/include/asm/unistd_32.h添加__NR_democopyfile 347__NR_syscalls改成 348分配系统调用号arch/x86/kernel/syscall_table_32.S第 347 项加.long sys_democopyfile让入口表能找到函数注意报告里用个人代号做版本后缀复现时统一换成demo系统调用名也统一成sys_democopyfile。名字不一致是后面编译报“undefined reference”的常见原因改的时候四处一起改。3.2 内核源码四处改动Makefile、sys.c、unistd_32.h 和入口表先改 Makefile 的版本后缀。3.0 内核顶层 Makefile 里有EXTRAVERSION把它设成.demo编译出来的版本就是3.0.69.demo。这一步不影响功能但后面uname -r和/boot文件名都靠它区分别省略。# 顶层 Makefile 里找到这一行改成自己的后缀 EXTRAVERSION .demo然后是kernel/sys.c末尾加函数。下面这段是报告思路的整理版参数是两个用户态路径返回值 0 表示成功-1 到 -3 分别表示源文件打开失败、目标文件创建失败、读写失败。BUF_SIZE设 512 是报告里的值实际可以调到 4096但别超过内核栈能承受的范围。#include linux/fs.h #include linux/uaccess.h #include asm/segment.h asmlinkage int sys_democopyfile(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 */ fin sys_open(s_file, O_RDONLY, S_IRUSR); if (fin -1) { set_fs(fs); return -1; } /* 创建并截断目标文件失败返回 -2 */ fout sys_open(t_file, O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); if (fout -1) { sys_close(fin); set_fs(fs); return -2; } /* 循环读写读到 0 表示结束读到负值表示错误 */ while ((copy_count sys_read(fin, buf, BUF_SIZE)) 0) { if (sys_write(fout, buf, copy_count) ! copy_count) { sys_close(fin); sys_close(fout); set_fs(fs); return -3; } } if (copy_count 0) { sys_close(fin); sys_close(fout); set_fs(fs); return -3; } sys_close(fin); sys_close(fout); set_fs(fs); return 0; }这段函数的参数是用户态路径指针sys_open的第三个参数是权限位S_IRUSR表示用户可读S_IWUSR表示用户可写。sys_read返回实际读到的字节数0 表示文件结束负值表示错误sys_write返回值必须和要写的字节数相等否则就是写失败。set_fs的恢复放在每个返回路径上不能只在成功路径恢复否则错误返回后内核段设置会残留。接着改unistd_32.h。系统调用号 347 要加在文件末尾附近同时把__NR_syscalls从 347 改成 348。这个总数告诉内核入口表有多长如果只加号不改总数入口表最后一项可能被截掉调用时返回ENOSYS。/* arch/x86/include/asm/unistd_32.h 末尾 */ #define __NR_democopyfile 347 #define __NR_syscalls 348入口表在arch/x86/kernel/syscall_table_32.S注意扩展名是大写S大小写在不同版本里可能有差异。第 347 项加一行.long sys_democopyfile要和unistd_32.h里的号严格对齐。很多翻车现场就是号加了、表没加或者表加了但函数名拼错编译能过调用返回 -1。/* arch/x86/kernel/syscall_table_32.S 第 347 项 */ .long sys_democopyfile /* 347 */3.3 配置、编译、安装和 GRUB让新内核真正启动改完源码不要直接make先make mrproper清理旧配置和中间文件。这一步会删掉.config如果之前已经配过内核先备份。然后make menuconfig报告里只选了EXIT因为这次不裁剪模块只加系统调用。make dep在 3.0 上可能提示多余make clean一般没动作按报告顺序跑一遍即可。cd /usr/src/linux-source-3.0.0 # 清理旧配置和中间文件先备份 .config 再执行 cp .config .config.bak 2/dev/null || true make mrproper # 文本菜单配置直接选 EXIT 保存退出 make menuconfig # 建立依赖关系3.0 上可能提示多余 make dep # 清理配置残留 make clean # 并行编译内核镜像-j 后面的数字按 CPU 核数和内存调整 make bzImage -j4 # 编译内核模块 make modules -j4 # 安装模块和内核 sudo make modules_install sudo make install-j4不是固定值CPU 核多可以加大内存小就降到-j2否则链接阶段容易被 OOM 杀掉。编译完成后检查/boot下是否出现vmlinuz-3.0.69.demo和initrd.img-3.0.69.demo。如果没有 initrd用mkinitramfs生成版本号要和uname -r预期一致。# 生成与新内核匹配的 initrd sudo mkinitramfs 3.0.69.demo -o /boot/initrd.img-3.0.69.demo # 更新 GRUB 启动项 sudo update-grub2 # 把默认等待时间改长方便手动选择新旧内核 sudo sed -i s/^GRUB_TIMEOUT.*/GRUB_TIMEOUT100/ /etc/default/grub sudo update-grub2报告里直接改/boot/grub/grub.cfg的 timeout这个文件是update-grub2生成的下次更新会被覆盖。更稳的做法是改/etc/default/grub里的GRUB_TIMEOUT再跑update-grub2。重启后进 GRUB 菜单选3.0.69.demo进系统后用uname -r确认。如果还是旧版本说明默认启动项没切过去或者新内核安装失败回 GRUB 菜单手动选。3.4 用户态测试syscall(347) 和返回值检查测试程序用syscall函数直接传系统调用号不依赖 glibc 是否封装了新调用。第一个参数是 347后面跟源路径和目标路径。返回值 0 表示成功-1 表示源文件打不开-2 表示目标文件创建失败-3 表示读写失败。测试时先用小文本文件确认内容一致后再换二进制文件。#include stdio.h #include unistd.h #include sys/syscall.h #include errno.h #include string.h #define __NR_democopyfile 347 int main(int argc, char *argv[]) { long ret; if (argc ! 3) { printf(Usage: %s source target\n, argv[0]); return 1; } /* 直接走 347 号系统调用不经过 glibc 封装 */ ret syscall(__NR_democopyfile, argv[1], argv[2]); if (ret ! 0) { printf(syscall failed, ret%ld, errno%d (%s)\n, ret, errno, strerror(errno)); return 2; } printf(copy ok\n); return 0; }编译后运行./test_democopy source.txt target.txt再用cmp对比。如果返回 -1先看源文件路径是不是绝对路径、当前用户有没有读权限返回 -2 看目标目录是否可写返回 -3 看磁盘空间和dmesg里有没有内核报错。系统调用号 347 是报告里的分配换内核版本时要重新查unistd_32.h的最大号不能照搬。# 编译测试程序 gcc -Wall -o test_democopy test_democopy.c # 准备测试文件并调用新系统调用 echo syscall demo source.txt ./test_democopy source.txt target.txt # 字节对比 cmp source.txt target.txt echo syscall copy ok # 查看内核日志里有没有异常 dmesg | tail -204. 字符设备驱动与 /proc模块加载、设备节点和实时信息4.1 字符设备驱动骨架主次设备号、file_operations 和注册字符设备驱动要解决三件事向内核注册一个设备号定义用户态open/read/write/release对应的操作函数卸载时把资源释放掉。设备号由主设备号和次设备号组成主设备号标识驱动类型次设备号由驱动自己解释。报告里用模块方式加载不重新编译内核适合课程设计反复调试。#include linux/module.h #include linux/fs.h #include linux/cdev.h #include linux/uaccess.h #define DEMO_BUF_SIZE 256 static dev_t demo_devno; static struct cdev demo_cdev; static char demo_buf[DEMO_BUF_SIZE]; static int demo_len; static int demo_open(struct inode *inode, struct file *file) { printk(KERN_INFO demo_chrdev: open\n); return 0; } static ssize_t demo_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { int len demo_len; if (*ppos len) return 0; if (count len - *ppos) count len - *ppos; /* copy_to_user 返回未拷贝的字节数非 0 表示失败 */ if (copy_to_user(buf, demo_buf *ppos, count)) return -EFAULT; *ppos count; return count; } static ssize_t demo_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { if (count DEMO_BUF_SIZE) count DEMO_BUF_SIZE; /* copy_from_user 返回未拷贝的字节数非 0 表示失败 */ if (copy_from_user(demo_buf, buf, count)) return -EFAULT; demo_len count; return count; } static int demo_release(struct inode *inode, struct file *file) { printk(KERN_INFO demo_chrdev: release\n); return 0; } static struct file_operations demo_fops { .owner THIS_MODULE, .open demo_open, .read demo_read, .write demo_write, .release demo_release, }; static int __init demo_init(void) { int ret; /* 动态分配一个字符设备号主设备号由内核给出 */ ret alloc_chrdev_region(demo_devno, 0, 1, demo_chrdev); if (ret 0) { printk(KERN_ERR demo_chrdev: alloc failed\n); return ret; } cdev_init(demo_cdev, demo_fops); demo_cdev.owner THIS_MODULE; ret cdev_add(demo_cdev, demo_devno, 1); if (ret 0) { unregister_chrdev_region(demo_devno, 1); return ret; } printk(KERN_INFO demo_chrdev: major%d minor%d\n, MAJOR(demo_devno), MINOR(demo_devno p a hrefhttps://download.csdn.net/download/yhsbzl/86241095 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。