在网上找了好久关于内核驱动学习的文章,但很多都是关注于某一点,没有系统化对于内核初学者来说,看那些文章最后你可能学到一些知识但是并不系统化,这也是为什么我写这篇博文的目的,本文是我通过看国嵌视频整理出来的笔记,如果你是个初学内核驱动的朋友,本文将会对你有些帮助!
作者:xutao (butbueatiful)
时间:2012年5月13日 17:06:20
一、内核开发基础:
Linux体系结构:由[用户空间] 和 [内核空间]
图1 Linux体系结构 X86实现了4个不同的级别:Ring0 - Ring3。Ring0下,可以执行特权指令,可以访问IO设备等,Ring3下则有更多的限制。Linux系统利用了CPU的这一特性,使用了其中的两句来分别运行Linux内核与应用程序,这样使操作系统本身得到充分的保护。
内核空间与用户空间是程序执行的两种不同的状态,通过[系统调用]和[硬件中断]能够完成从用户空间到内核空间的转移。
Linux内核体系结构: 图2 Linux内核体系结构图 系统调用接口:SCI层为用户空间提供了一套[标准的系统调用函数]来访问Linux内核,搭起了用户空间到内核空间的桥梁。
进程管理:它的重点是创建进程(fork exec),停止进程(kill、exit),并控制它们之间的通信(signal、POSIX机制)。进程管理还包括控制活动进程如何共享CPU,即[进程调度]。
内存管理:主要作用是控制多个进程安全地共享内存区域。
ARCH: architecture。内核支持的每种CPU体系
设备驱动程序
网络协议栈:内核协议栈为Linux提供了丰富的网络协议栈实现。
虚拟文件系统:VSF隐藏各个文件系统的具体细节,为文件操作提供统一的接口。
二、Linux内核源代码:
目录结构:
Linux内核源代码采用树形结构进行组织,非常合理地[把功能相关的文件都放在同一个子目录下],使得程序更具可读性。
arch目录:内核所支持的每种CPU体系,在该目录下都有对应的子目录。每个CPU的子目录,又进一步分解为boot,mm,kernel等子目录。
|--x86 /*英特尔cpu及与之相兼容体系结构的子目录*/
||--boot /*引导程序*/
|||--compressed /*内核解压缩*/
||--tools /*生成压缩内核映像的程序*/
||--kernel /*相关内核特性实现方式,如信号处理,时钟处理*/
||--lib /*硬件相关工具函数*/
block目录:部分块设备驱动程序
crypto目录:加密、压缩、CRC校验算法
documentation:内核文档
drivers目录:设备驱动程序
fs目录:存放各种文件系统的实现代码。每个子目录对应一种文件系统的实现,公用的源程序用于实现[虚拟文件系统vfs]
||--devpts /*/dev/pts虚拟文件系统*/
||--ext2 /*第二扩展文件系统*/
||--fat /*MS的fat32文件系统*/
||--isofs /*ISO9660光盘cd-rom上的文件系统*/
include目录:内核所需要的头文件。与平台无关的头文件在/include/linux目录下,与平台相关的头文件在相应的子目录中
lib目录:库文件代码(不是指的是C库,在编译内核产生的库)
mm目录:用于实现内存管理中[与体系结构无关的部分,与体系结构有关的代码在arch目录里]
net目录:网络协议的实现代码
||--802 /*802无限通讯协议核心支持代码*/
||--appletalk /*与苹果系统联网的协议*/
||--ax25 /*AX25无限INTERNET协议*/
||--bridge /*桥接设备*/
||--ipv4 /*ip协议族V4版32位寻址模式*/
||--ipv6 /*ip协议族V6版*/
samples:内核编程的一些范例
scripts:配置内核的脚本
security:SElinux的模块
sound:音频设备的驱动程序
usr:cpio命令实现
virt:内核虚拟机
三、内核配置与编译
Linux内核具有可定制的优点
配置与编译步骤:
1、清除临时文件、中间文件和配置文件
make clean
remove most generated files but keep the config
make mrproper
remove all generated files + config files
make distclean
mrproper + remove editor backup and patch file
2、确定目标系统的软硬件配置情况,比如CPU类型、网卡的型号等
3、配置内核:
make config
基于文本模式的交互式配置。
make menuconfig
基于文本模式的菜单配置。(推荐使用)
make oldconfig
使用已有的配置文件(.config),但会询问新增的配置选项
make xconfig
图形化配置
配置方法:
根据已有的内核配置文件(每个体系结构目录的configs目录里有很多配置文件)去修改,将你需要的配置文件拷贝到源码顶层目录,然后再去配置、修改
4、编译内核:
make zImage
make bzImage
区别:在X86平台,zImage只能用于小于512K的内核
如需获取详细的编译信息,可使用:
make zImage V=1
make bzImage V=1
/*编译好的内核位于 arch//boot/ 目录下*/
5、编译内核模块:
make modules
6、安装内核模块
make modules_install
/*结果:将编译好的内核模块从内核源代码目录拷贝到 /lib/modules 下*/
7、制作init ramdisk
mkinitrd initrd-$version $version
例:
mkinitrd initrd-2.6.29 2.6.29
/*$version 可以通过查询/lib/modules下的目录得到*/
说明:2.6.29这个目录是通过第六步make modules_intall产生的
内核安装(X86平台)
1、cp arch/x86/boot/bzImage /boot/vmlinuz-$version
2、cp $initrd /boot/
3、修改/etc/grub.conf 或者 /etc/lilo.conf
修改方法:把原来的 title 段 拷贝一份,进行修改
/*$version 为所编译的内核的版本号*/
四、内核模块开发:
模块功能:
Linux内核的整天结构非常庞大,其包含的组件也非常的多,如何使用需
要的组件:
方法一:把所有的组件都编译进内核文件,即:zImage 或 bzImage
这样导致两个问题:
一:生成的内核文件过大;
二:如果要添加或删除某个组件,需要重新编译整个内核.
方法二:内核模块:
让内核文件(zImage 或 bzImage)本身并不包含某个组件,而是在该组件需要被使用时,[动态地添加到正在运行的内核里]。
内核模块的特点:
模块本身并不被编译进内核文件(zImage 或 bzImage)可以根据需求,在内核运行期间动态的安装或卸载
范例(hello.c) - #include <linux/init.h>
- #include <linux/module.h>
- static int __init hello_init(void)
- {
- printk("Hello my friend!\n");
- return 0;
- }
- static void __exit hello_exit(void)
- {
- printk("Goodbye,friend!\n");
- }
- module_init(hello_init);
- module_exit(hello_exit);
程序结构
1、模块加载函数(必须)
安装模块时系统自动调用的函数,通过module_init宏来指定
2、模块卸载函数(必须)
卸载模块时被系统自动调用的函数,通过module_exit宏来指定
内核模块makefile的编写:
1、有一个源文件的内核模块的编写
范例(hello.c 的 makefile)- ifneq ($(KERNELRELEASE),)
- obj-m := hello.o #根据具体需要这个要变
- else
- KDIR := /lib/modules/2.6.18.e15/build #这个要变
- all:
- make -C $(KDIR) M=$(PWD) modules
- clean:
- rm -f *.ko *.o *.mod.o *.mod.c *.sysmvers
- endif
2、有多个源文件的内核模块编写(main.c add.c)
- /*main.c*/
- #include <linux/module.h>
- #include <linux/init.h>
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("ButBueatiful");
- MODULE_DESCRIPTION("Hello my friend Module");
- MODULE_ALIAS("a simplest module");
- extern int add(int a, int b);
- static int __init hello_init()
- {
- printk("Hello my friend!\n");
- add(1, 2);
- return 0;
- }
- static void __exit hello_exit()
- {
- printk("<7>hello <0>exit\n");
- }
- module_init(hello_init);
- module_exit(hello_exit);
- /*add.c*/
- int add(int a, int b)
- {
- return a + b;
- }
##################### Makefile ##########################
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
hello-objs := main.o add.o
else
KDIR := /lib/modules/2.6.32-71.el6.i686/build #这个要变
all:
make -C $(KDIR) M=$(PWD) modules
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.unsigned *.order
endif
内核模块的安装与卸载:
加载 insmod (insmod hello.ko)
卸载 rmmod (rmmod hello.ko)
查看 lsmod
加载 modprobe (modprobe hello)
modprobe 如同 insmod,也是加载一个模块到内核。它们的不同之处在于modprobe会根据文件 /lib/modules/<$version>/modules.dep 来查看加载的模块,看它是否还依赖于其他模块,如果是,modprobe会首先找到这些模块,把它们先加载到内核。
模块可选信息
1、许可证申明
宏MODULE_LICENSE用来告知内核,该模块带有一个许可证,没有这样的说明,加载模块时内核会抱怨。有效的许可证有“GPL"、“GPL 2"、“GPL and additional rigths”、“Dual BSD/GPL”、“Dual MPL/GPL”和 “Proprietary"。
2、作者申明(可选)
MODULE_AUTHOR("ButBueatiful");
3、模块描述(可选)
MODULE_DESCRIPTION("Hello Module");
4、模块版本(可选)
MODULE_VERSION("V1.0");
5、模块别名(可选)
MODULE_ALIAS("a simple module");
6、模块参数
通过宏module_param指定模块参数,模块参数用于在加载模块时传递参数给模块。
module_param(name, type, perm);
name是模块参数的名称,type是这参数的类型,
perm是模块参数的访问权限。
type常见值:
bool,int,charp(字符串型)
perm常见值:
S_IRUGO:任何用户都对/sys/module中出现的该参数具有读权限
S_IWUSR:允许root用户修改/sys/module中出现的该参数
例:
int a = 3;
char *st;
module_param(a, int, S_IRUGO);
module_param(st, charp, S_IRUGO);
内核符号导出:
/proc/kallsyms记录了内核中所有导出的符号的名字与地址。
内核符号的导出使用:
EXPORT_SYMBOL(符号名)
EXPORT_SYMBOL_GPL(符号名)
其中EXPORT_SYMBOL_GPL只能用于包含GPL许可证的模块
常见问题:版本不匹配
内核模块的版本由其所依赖的内核代码版本所决定,在加载内核模块时,insmod程序会将内核模块与当前正在运行的内核模块版本比较,如果不一致时,就会出错.
解决方法:
1、使用modprobe --force-modversion 强行加载内核模块
2、确保编译内核模块时,所依赖的内核代码版本与现在的相同
uname -r 命令可查看内核版本
2.6与2.4内核模块对比
2.6是.ko 2.4是.o;.ko是由.o的进一步封装而成
总结---对比应用程序:
对比应用程序,内核模块具有一下不同:
应用程序是从头 main 到尾执行任务,执行结束后从内存中消失。内核模块则是先在内核中注册自己以便服务于将来的某个请求,然后它的初始化函数结束,此时模块仍然存在与内核中,直到卸载函数被调用,模块才从内核中消失。
内核打印:
printk 与 printf区别在于:printk有优先级
函数printk在Linux内核中定义,功能和标准C库中的函数printf类似。内核需要自己单独的打印输出函数,这是因为它在运行时不能依赖于C库。模块能够调用printk是因为在insmod函数装入模块后,模块就连接到了内核,因此可以访问内核的共用符号(包括函数和变量)。
在中定义了8中记录级别。按照优先级递减的顺序是:
KERN_EMERG "<0>" 用于紧急事件消息,他们一般是系统崩溃之前提示的消息
KERN_ALERT "<1>" 用于需要立即采取动作的情况
KERN_CRIT "<2>" 临界状态,通常涉及严重的硬件或软件操作失败
KERN_ERR "<3>" 用于报告错误状态,设备驱动程序会经常使用KERN_ERR来报告来自硬件的问题。
KERN_WARNING"<4>" 对可能出现问题的情况进行警告,但这类情况通常不会对系统造成严重问题。
KERN_NOTICE "<5>" 有必要进行提示的正常情形,许多与安全相关的状况用这个级别进行汇报。
KERN_INFO "<6>" 提示性信息,很多驱动程序在启动的时候以这个级别来打印他们找到的硬件信息。
KERN_DEBUG "<7>" 用于调试信息。
没有指定优先级的printk默认使用DEFAULT_MESSAGE_LOGLEVEL优先级,它是一个在kernle/printk.c中定义的整数。在2.6.29内核中:
#define DEFAULT_MESSAGE_LOGLEVEL 4 /*KERN-WARNING*/
控制台优先级配置:
/proc/sys/kernel/printk
4 4 1 7
Console_loglevel
Default_message_loglevel
Minimum_console_level
Default_console_loglevel
在printk中显式地指定优先级的原因在于:具有默认优先级的消息可能不会输出在控制台上。这依赖于内核版本、klogd守护进程的版本以及具体的配置。
阅读(4909) | 评论(0) | 转发(1) |