Chinaunix首页 | 论坛 | 博客
  • 博客访问: 5085147
  • 博文数量: 553
  • 博客积分: 13864
  • 博客等级: 上将
  • 技术积分: 11041
  • 用 户 组: 普通用户
  • 注册时间: 2011-11-28 21:25
个人简介

个人Blog: hhktony.com

文章分类

全部博文(553)

文章存档

2015年(1)

2014年(2)

2013年(12)

2012年(384)

2011年(154)

分类: LINUX

2012-05-13 16:35:24

在网上找了好久关于内核驱动学习的文章,但很多都是关注于某一点,没有系统化对于内核初学者来说,看那些文章最后你可能学到一些知识但是并不系统化,这也是为什么我写这篇博文的目的,本文是我通过看国嵌视频整理出来的笔记,如果你是个初学内核驱动的朋友,本文将会对你有些帮助!
                                               作者: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)     

点击(此处)折叠或打开

  1. #include <linux/init.h>
  2. #include <linux/module.h>

  3. static int __init hello_init(void)
  4. {
  5.     printk("Hello my friend!\n");
  6.     return 0;
  7. }

  8. static void __exit hello_exit(void)
  9. {
  10.     printk("Goodbye,friend!\n");
  11. }

  12. module_init(hello_init);
  13. module_exit(hello_exit);           
      
        程序结构
            1、模块加载函数(必须)
                安装模块时系统自动调用的函数,通过module_init宏来指定
            2、模块卸载函数(必须)
                卸载模块时被系统自动调用的函数,通过module_exit宏来指定

        内核模块makefile的编写:
            1、有一个源文件的内核模块的编写
            范例(hello.c 的 makefile)

点击(此处)折叠或打开

  1. ifneq ($(KERNELRELEASE),)
  2. obj-m := hello.o #根据具体需要这个要变
  3. else
  4. KDIR := /lib/modules/2.6.18.e15/build #这个要变
  5. all:
  6. make -C $(KDIR) M=$(PWD) modules
  7. clean:
  8. rm -f *.ko *.o *.mod.o *.mod.c *.sysmvers
  9. endif
            2、有多个源文件的内核模块编写(main.c add.c)

点击(此处)折叠或打开

  1. /*main.c*/
  2. #include <linux/module.h>
  3. #include <linux/init.h>

  4. MODULE_LICENSE("GPL");
  5. MODULE_AUTHOR("ButBueatiful");
  6. MODULE_DESCRIPTION("Hello my friend Module");
  7. MODULE_ALIAS("a simplest module");

  8. extern int add(int a, int b);
  9. static int __init hello_init()
  10. {
  11.     printk("Hello my friend!\n");
  12.     add(1, 2);
  13.     return 0;
  14. }

  15. static void __exit hello_exit()
  16. {
  17.     printk("<7>hello <0>exit\n");
  18. }

  19. module_init(hello_init);
  20. module_exit(hello_exit);

点击(此处)折叠或打开

  1. /*add.c*/
  2. int add(int a, int b)
  3. {
  4.     return a + b;
  5. }
##################### 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守护进程的版本以及具体的配置。


阅读(4842) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~