首页 / 资讯中心 / 文章详情

Linux内核编译与驱动开发实战:系统调用、字符设备与/proc完整链路

Linux内核编译与驱动开发实战:系统调用、字符设备与/proc完整链路 ★ FEATURED ARTICLE
简介这份华中科技大学计算机学院操作系统课程设计报告面向计算机专业学生及操作系统自学者提供一份完整的课程设计参考范例。报告围绕Linux系统展开涵盖Linux编程环境搭建、内核代码结构分析、添加系统调用、设备驱动程序开发、/proc文件系统解析等核心实验模块并包含文件拷贝程序与GTK图形界面并发进程展示等实践内容帮助读者理解操作系统底层原理与系统级编程方法。资源包共1个doc文件约1.22MB内容为34页实验报告结构完整、步骤详实包含环境配置、设计过程与实现代码。目前已有404人学习下载适合需要完成操作系统课程设计、准备系统开发相关面试或希望深入掌握Linux内核机制的学习者参考借鉴。1. 从一份课设报告说起Linux 内核编译与驱动开发到底能跑通什么很多人第一次接触操作系统课设都是从一个压缩包开始的。里面是一份 34 页的实验报告四个实验文件拷贝、添加系统调用、写字符设备驱动、分析 /proc。看起来像学生作业但真动手跑一遍就会发现它把 Linux 从用户态到内核态的完整链路都串起来了——用户程序怎么通过系统调用进内核、内核模块怎么注册设备、/proc 怎么暴露运行时状态。这套东西放到今天依然不过时因为内核编译、系统调用号分配、字符设备注册这些机制在主流内核版本里骨架没变变的只是细节。适合谁适合想搞嵌入式、驱动、内核模块但一直停留在“看文章”阶段的人。这份报告的价值不在报告本身而在于它记录了一条能跑通的路径以及路径上那些没人告诉你的坑。2. 环境搭建为什么版本选错后面全白干2.1 内核版本与源码包的匹配逻辑课设报告里用的是 Ubuntu 11.10 加 linux-headers-3.0.0-12-generic这个组合不是随便选的。添加系统调用必须改内核源码再重新编译而你编译时用的源码版本必须和当前运行的内核版本尽量一致。差一个小版本号编译出来的模块加载时就可能报version magic不匹配。报告里特意强调“下载和当前实验环境最为接近的系统版本”这是血泪经验。常见做法是先用uname -r看当前内核版本再去装对应的linux-source包。比如uname -r # 输出类似 3.0.0-12-generic apt-get install linux-source-3.0.0装完后源码包在/usr/src下解压cd /usr/src tar -xjvf linux-source-3.0.0.tar.bz2解压出来是linux-source-3.0.0目录。注意linux-source和linux-headers是两回事headers 只包含编译外部模块需要的头文件source 才是完整内核源码改系统调用表必须用 source。提示如果apt-get因为依赖问题装不上报告里提到的aptitude确实好用它会自动分析依赖并给出方案。先apt-get install aptitude再用aptitude install linux-source-3.0.0。2.2 root 登录与开发工具链的安装Ubuntu 默认不允许 root 直接登录图形界面但内核编译和模块安装大量需要 root 权限。报告里改/etc/lightdm/lightdm.conf加greeter-show-manual-logintrue的方法在当时的 LightDM 版本下有效。现在更常见的做法是直接用sudo -i切到 root shell或者sudo su不必改登录管理器配置。工具链方面build-essential是基础但 GTK 图形库那部分需要额外装apt-get install build-essential apt-get install libgtk2.0-dev报告里提到“只要安装 libgtk2.0-dev 就能搞定 GTK 环境”这个结论在当时成立因为libgtk2.0-dev会自动拉取libglib2.0-dev等依赖。编译 GTK 程序时用pkg-config取编译和链接参数gcc -o forkgtk forkgtk.c pkg-config --cflags --libs gtk-2.0pkg-config会输出一堆-I和-l参数省得手动写路径。如果报找不到gtk-2.0说明libgtk2.0-dev没装好用pkg-config --modversion gtk-2.0验证。2.3 文件拷贝程序的健壮性设计报告里第一个实验是写文件拷贝核心逻辑就是fgetc循环加fputc。但真正值得看的是它的错误处理检查argc是否为 3、源文件fopen是否成功、目标文件fopen是否成功。这三步缺一个程序在异常输入下就会段错误或者静默失败。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); }参数说明argv[1]是源文件路径argv[2]是目标文件路径rb和wb的b表示二进制模式在 Linux 下其实和文本模式没区别但跨平台时保留这个习惯。这个程序虽然简单但它是后面系统调用版文件拷贝的用户态对照——内核里不能用fopen只能用sys_open、sys_read、sys_write。3. 添加系统调用从改源码到编译安装的完整链路3.1 系统调用号分配与入口表填写添加系统调用的本质是在系统调用表里加一项在unistd_32.h里分配一个号在内核源码里实现函数。报告里用的号是 347这个号不是随便选的得看当前内核最后一个系统调用号是多少往后顺延。具体改三个地方第一/usr/src/linux-source-3.0.0/arch/x86/include/asm/unistd_32.h加一行#define __NR_sys_zcycopyfile 347同时把后面的__NR_syscall_count从 347 改成 348。第二/usr/src/linux-source-3.0.0/arch/x86/kernel/syscall_table_32.s加一行.long sys_zcycopyfile /* 347 */第三/usr/src/linux-source-3.0.0/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()); if ((fin sys_open(s_file, O_RDONLY, S_IRUSR)) -1) { return -1; } if ((fout sys_open(t_file, O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR)) -1) { return -2; } while (copy_count sys_read(fin, buf, BUF_SIZE)) { if (copy_count -1 || sys_write(fout, buf, copy_count) -1) { return -3; } } set_fs(fs); return 0; }这里的关键是set_fs(get_ds())和set_fs(fs)这对操作。内核默认的地址空间限制是KERNEL_DS但sys_open等函数期望的用户态地址空间是USER_DS。不切换的话传进去的指针会被当成用户态地址直接访问会出错。get_fs保存原值set_fs恢复这是内核里访问用户态内存的标准套路。注意sys_open、sys_read、sys_write这些函数名在不同内核版本里可能带前缀变化比如ksys_open或do_sys_open。3.0 内核里直接用sys_前缀是可行的但更高版本需要查include/linux/syscalls.h确认。3.2 内核配置与编译参数改完源码后编译前要清理和配置make mrproper make menuconfig make dep make cleanmake mrproper是彻底清理包括配置文件make menuconfig是文本菜单配置课设里不需要改选项直接退出保存即可make dep在 3.0 内核里基本是空操作但报告里保留了这一步说明当时的环境确实需要。编译时用多线程加速make bzImage -j9 make modules -j9-j9表示同时开 9 个编译任务具体数字看 CPU 核心数。报告里说“能很大提高效率”这是实话单线程编译 3.0 内核大概要 40 分钟以上多线程能压到 10 分钟以内。安装模块和内核sudo make modules_install sudo make install然后复制内核到 boot 目录建立 initrd更新 grubcp arch/i386/boot/bzImage /boot/vmlinuz-3.0.69.zcy mkinitramfs 3.0.69.zcy -o /boot/initrd.img-3.0.69-zcy update-grub2最后改/boot/grub/grub.cfg里的timeout为 100给自己留足选择启动项的时间。重启后uname -a看到新内核版本说明编译安装成功。3.3 测试程序与系统调用验证测试程序zcopy.c里用syscall(347, src, dst)调用新系统调用。编译运行后如果文件拷贝成功说明整条链路通了。如果返回 -1检查源文件路径返回 -2检查目标文件权限返回 -3检查磁盘空间或缓冲区大小。这里有个容易翻车的地方系统调用号 347 是 32 位系统下的号如果你在 64 位系统上编译unistd_64.h里的号可能不一样而且syscall_table_32.s对 64 位不适用。报告里特意提到“之前的 Ubuntu 13.04 因为是 64 位的行不通”就是这个原因。所以做这个实验32 位环境是硬性要求。4. 字符设备驱动模块注册、文件操作与 /proc 分析4.1 设备号分配与 file_operations 结构Linux 字符设备驱动的核心是file_operations结构体里面挂载open、read、write、release等函数指针。模块加载时用register_chrdev注册主设备号卸载时用unregister_chrdev注销。报告里没有贴完整驱动代码但描述了三个部分注册设备、定义功能函数、卸载设备。一个典型的字符设备驱动骨架#include linux/module.h #include linux/fs.h #include linux/cdev.h static int major; static struct cdev my_cdev; static int my_open(struct inode *inode, struct file *file) { printk(KERN_INFO device opened\n); return 0; } static ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *off) { return 0; } static ssize_t my_write(struct file *file, const char __user *buf, size_t len, loff_t *off) { return len; } static int my_release(struct inode *inode, struct file *file) { return 0; } static struct file_operations my_fops { .owner THIS_MODULE, .open my_open, .read my_read, .write my_write, .release my_release, }; static int __init my_init(void) { major register_chrdev(0, mydev, my_fops); if (major 0) return major; printk(KERN_INFO mydev registered, major%d\n, major); return 0; } static void __exit my_exit(void) { unregister_chrdev(major, mydev); } module_init(my_init); module_exit(my_exit); MODULE_LICENSE(GPL);register_chrdev的第一个参数传 0 表示让内核自动分配主设备号返回值就是分配到的号。file_operations里的函数指针不一定要全部实现但open和release通常要有否则用户态打开设备时可能出问题。编译模块用 Makefileobj-m mydev.o all: make -C /lib/modules/$(shell uname -r)/build M$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M$(PWD) cleanmake -C切到内核源码目录M$(PWD)告诉内核模块源码在当前目录。编译出mydev.ko后insmod mydev.ko加载dmesg看打印的主设备号然后mknod /dev/mydev c major 0创建设备节点。4.2 /proc 文件系统的运行时信息暴露/proc 是虚拟文件系统不占磁盘空间内容由内核动态生成。报告里第四个实验是分析 /proc常见做法是写一个内核模块在 /proc 下创建一个文件用户态cat时触发读函数返回信息。创建 /proc 文件的旧接口是create_proc_entry新内核用proc_create。以 3.0 内核为例#include linux/proc_fs.h #include linux/seq_file.h static int my_proc_show(struct seq_file *m, void *v) { seq_printf(m, mydev status: active\n); return 0; } static int my_proc_open(struct inode *inode, struct file *file) { return single_open(file, my_proc_show, NULL); } static const struct file_operations my_proc_fops { .owner THIS_MODULE, .open my_proc_open, .read seq_read, .llseek seq_lseek, .release single_release, }; static int __init my_proc_init(void) { proc_create(mydev_status, 0, NULL, my_proc_fops); return 0; } static void __exit my_proc_exit(void) { remove_proc_entry(mydev_status, NULL); }seq_file接口是推荐做法它帮你处理缓冲区分配和分页读取。single_open适合输出量小的场景seq_printf往缓冲区写内容。加载模块后cat /proc/mydev_status就能看到输出。提示/proc 下的文件权限由proc_create的第二个参数控制传 0 表示默认权限通常是 0444。如果要让用户态可写需要传 0222 并在file_operations里实现write。5. 避坑与排查那些让课设卡住三天的细节5.1 内核编译报错 version magic 不匹配现象insmod加载模块时提示disagrees about version of symbol module_layout或version magic 3.0.0-12-generic should be 3.0.69.zcy。原因模块编译时用的内核头文件版本和当前运行的内核版本不一致。常见于编译完新内核后没有重启进新内核或者 Makefile 里uname -r取到的是旧版本。解决先uname -r确认当前内核版本再检查/lib/modules/$(uname -r)/build是否指向正确的源码目录。如果编译的是新内核的模块必须重启进新内核后再编译加载。5.2 系统调用返回 -1 但源文件明明存在现象测试程序调用syscall(347, src, dst)返回 -1但src文件用ls能看到。原因内核态用sys_open打开文件时路径字符串是用户态地址如果没有set_fs(get_ds())切换地址空间限制内核会按内核态地址解析找不到文件。解决确认sys_zcycopyfile函数里set_fs调用在sys_open之前set_fs(fs)在返回之前。顺序错了就会出这个问题。5.3 GTK 程序编译报找不到 gtk-2.0现象pkg-config --cflags --libs gtk-2.0报Package gtk-2.0 was not found。原因libgtk2.0-dev没装或者PKG_CONFIG_PATH没包含 GTK 的 pc 文件目录。解决apt-get install libgtk2.0-dev然后用dpkg -L libgtk2.0-dev | grep pc确认 pc 文件位置。如果装在非标准路径export PKG_CONFIG_PATH/path/to/pkgconfig:$PKG_CONFIG_PATH。5.4 编译内核时 make menuconfig 报缺少 ncurses现象make menuconfig提示Unable to find the ncurses libraries。原因ncurses 开发库没装。解决apt-get install libncurses5-dev。这个包提供终端菜单界面所需的库文件不装的话menuconfig跑不起来只能手动改.config文件效率低且容易出错。5.5 新内核启动后旧内核入口消失现象重启后 grub 菜单里只有新内核找不到旧内核入口。原因update-grub2重新生成菜单时旧内核的vmlinuz和initrd文件可能被覆盖或路径变了。解决报告里提到“原版本内核比新内核小入口在 Previous Linux version 目录下”这是 grub 的折叠菜单。如果确实找不到检查/boot下是否还有旧内核的vmlinuz-*和initrd.img-*文件没有的话需要重新安装旧内核包。6. 进阶技巧用 QEMU 跑内核模块调试与 /proc 数据验证真机编译内核风险高一旦 grub 配置出错可能连系统都进不去。更稳妥的做法是用 QEMU 起一个虚拟机跑编译好的内核在里面加载模块、验证系统调用。这样即使内核 panic宿主机不受影响。基本流程是编译出bzImage后用 QEMU 指定内核和根文件系统启动qemu-system-i386 -kernel arch/i386/boot/bzImage \ -hda rootfs.img \ -append root/dev/sda consolettyS0 \ -nographicrootfs.img可以是一个最小化的文件系统镜像里面放 busybox 和测试程序。-nographic把串口输出重定向到终端方便看内核启动日志。启动后insmod加载模块cat /proc/mydev_status验证 /proc 输出syscall(347, ...)验证系统调用。另一个技巧是用printk的日志级别控制输出。printk(KERN_INFO msg)默认可能不显示在终端因为控制台日志级别可能高于 INFO。用dmesg -n 8把所有级别都打到控制台或者echo 8 /proc/sys/kernel/printk临时调整。验证 /proc 数据时注意seq_file的输出是流式的多次cat可能看到不同内容因为每次open都会重新触发show函数。如果要在 /proc 里暴露计数器记得用原子操作或者锁保护否则并发读会出脏数据。从那以后我每次编译内核前都强制走一遍uname -r确认版本、ls /usr/src确认源码目录、df -h /boot确认 boot 分区空间够用。这三步花不了一分钟但能省掉后面几个小时的排查。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站