• Linux内核模块学习


    注:本文是《Linux设备驱动开发详解:基于最新的Linux 4.0内核 by 宋宝华 》一书学习的笔记,大部分内容为书籍中的内容。

    书籍可直接在微信读书中查看:Linux设备驱动开发详解:基于最新的Linux4.0内核-宋宝华-微信读书 (qq.com)

    1 简介

    模块(Module)具有以下特点:

    • 模块本身不编译进内核映像
    • 内核加载之后,和其它内核中的部分完全一样。

    一个简单的示例:

    #include <linux/init.h>
    #include <linux/module.h>
    
    static int __init hello_init(void)
    {
        printk(KERN_INFO "Hello world enter\n");
        return 0;
    }
    module_init(hello_init);  //内核加载函数
    
    static void __exit hello_exit(void)
    {
        printk(KERN_INFO "Hello world exit\n");
    }
    module_exit(hello_exit);  //内核卸载函数
    
    MODULE_AUTHOR("Test Hello");
    MODULE_LICENSE("GPL v2");
    MODULE_DESCRIPTION("A simple Hello World Module");
    MODULE_ALIAS("A simple module");
    

    Makefile:

    KVERS = $(shell uname -r)
    
    # Kernel modules
    obj-m += hello.o
    
    # Specify flags for the module compilation.
    #EXTRA_CFLAGS=-g -O0
    
    build: kernel_modules
    
    kernel_modules:
    	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
    
    clean:
    	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean
    
    

    编译,并且插入ko:

    $ make 
    $ sudo insmod hello.ko 
    

    插入ko时,可能没有打印信息,可以使用dmesg命令查看:

    可以通过lsmod命令查看当前系统插入了哪些ko,lsmod命令查看的结果对应/proc/modules文件,内核中已经加载模块信息也存在于/sys/module目录中:

    $ lsmod | grep "hello"
    Module                  Size  Used by
    hello                  12425  0 
    $ cat /proc/modules | grep "hello"
    hello 12425 0 - Live 0xffffffffc06fc000 (OE)
    $ ls /sys/module/hello/ 
    coresize  initsize   notes/  rhelversion  srcversion  uevent
    holders/  initstate  refcnt  sections/    taint
    

    modinfo命令可以获得模块的信息,包括模块作者、模块的说明、模块所支持的参数以及vermagic:

    $ modinfo hello.ko 
    filename:       /home/grace/driver_study/code/modules/hello.ko
    alias:          A simple module
    description:    A simple Hello World Module
    license:        GPL
    author:         Test Hello
    rhelversion:    7.4
    srcversion:     E4D5379B55084D8ED2D94E8
    depends:        
    vermagic:       3.10.0-693.el7.x86_64 SMP mod_unload modversions 
    

    卸载模块命令rmmod:

    $ rmmod hello
    

    2 模块程序结构

    一个Linux内核模块主要由以下几个部分组成:

    (1)模块加载函数

    通过insmod或者modprobe命令加载模块时,模块的加载函数会自动被内核执行,完成模块的初始化工作。

    (2)模块卸载函数

    通过rmmod命令卸载模块时,模块的卸载函数会自动被内核执行,完成模块相关的卸载功能。

    (3)模块许可证声明

    许可证(LICENSE)声明描述了内核模块的许可权限,如果不申明LICENSE,模块加载时会收到内核被污染(Kernel Tainted)的警告。

    [10688.585888] hello: module license 'unspecified' taints kernel.
    

    在Linux内核模块中可接受的LICENSE包括:GPL、GPL v2等。大多数情况下,内核模块应遵循GPL兼容许可证。

    (4)模块参数(可选)

    模块参数是模块被加载时可以传递给它的值,它本身对应模块内部的全局变量。

    (5)模块导出符号(可选)

    内核模块可以导出的符号(symbol,对应于函数或变量),若导出,其它模块可以使用本模块的变量或函数。

    (6)模块作者等信息声明(可选)

    3 模块加载函数

    Linux模块加载函数一般以__init标识声明,典型的模块加载函数的形式如下:

    static int __init init_func(void)
    {
    	/* 初始化代码 */
    }
    module_init(init_func);
    

    模块加载函数module_init(函数名)的方式指定,返回整型值,若初始化成功,返回0;初始化失败,返回错误编码。

    Linux内核中,错误码是一个接近0的负值,定义在<lnux/errno.h>中。

    Linux内核中,可以使用request_module(const char *fmt, ...)函数加载内核模块,使用方式:

    request_module(module_name);
    

    初始化数据可以定义为__initdata,对于只在初始化阶段所需要的数据,内核在初始化完成之后会释放它们占用的内存。

    static int hello_data __initdata = 1;
    static int __init hello_init(void)
    {
        printk(KERN_INFO "Hello world enter %d\n", hello_data);
        return 0;
    }
    module_init(hello_init);  //内核加载函数
    

    4 模块卸载函数

    模块卸载函数一般以__exit标识声明,常见的用法如下:

    static void __exit clean_func(void)
    {
        /* 释放代码 */
    }
    module_exit(clean_func);  //内核卸载函数
    

    模块卸载函数在模块卸载的时候执行,不返回任何值,且必须以module_exit(函数名)的方式使用。

    5 模块参数

    可使用"module_param(参数名, 参数类型, 参数读/写权限)"为模块定义一个参数。

    在加载模块时,用户可以向模块传递参数,形式为:

    insmod 模块名 参数名=参数值
    

    如果不传递,参数使用模块定义的缺省值。如果模块被内置无法insmod,在bootloader中可以通过bootargs设置"模块名.参数名=值"的形式给内核的模块传递参数。

    参数的类型可以是:

    byte short ushort int uint long ulong charp(字符指针) bool invbool(布尔的反)
    

    在模块编译时会将module_param中的声明类型和变量定义的类型进行比较,判断是否一致。

    模块也可以有参数数组,形式为:

    module_param_array(数组名, 数组类型, 数组长, 参数读/写权限)
    

    模块参数举例:

    #include <linux/init.h>
    #include <linux/module.h>
    
    static char *book_name = "dissection Linux Device Driver";
    module_param(book_name, charp, S_IRUGO);
    
    static int book_num = 400;
    module_param(book_num, int, S_IRUGO);
    
    static int __init hello_init(void)
    {
        printk(KERN_INFO "book name:%s\n", book_name);
        printk(KERN_INFO "book  num:%d\n", book_num);
    
        return 0;
    }
    module_init(hello_init);  //内核加载函数
    
    static void __exit hello_exit(void)
    {
        printk(KERN_INFO "Hello world exit\n");
    }
    module_exit(hello_exit);  //内核卸载函数
    
    MODULE_AUTHOR("Test Hello");
    MODULE_LICENSE("GPL");
    MODULE_DESCRIPTION("A simple Hello World Module");
    MODULE_ALIAS("A simple module");
    

    编译并且加载不带参数,可以看出输出的是默认的参数值:

    $ make
    $ insmod para.ko 
    $ dmesg 
    [13186.787598] book name:dissection Linux Device Driver
    [13186.787601] book  num:400
    

    加载时带参数,打印的是输入的参数:

    $ insmod para.ko book_name="LDD3" book_num=500 
    $ dmesg
    [13423.036831] book name:LDD3
    [13423.036835] book  num:500
    

    在/sys/module/模块名/parameters目录下也可以看到模块的参数:

    $ ls /sys/module/para/parameters/
    book_name  book_num
    $ cat /sys/module/para/parameters/book_num 
    500
    $ cat /sys/module/para/parameters/book_name 
    LDD3
    

    6 导出符号

    Linux下内核符号表在/proc/kallsyms下,它记录了符号以及符号所在的内存地址:

    $ more /proc/kallsyms
    ffffffff8109f320 T sys_kill
    ffffffff8109f330 T SyS_tgkill
    

    模块可以使用以下方式导出符号到符号表中,导出的符号可以被其它模块使用,使用前需要进行声明:

    EXPORT_SYMBOL(符号名);
    EXPORT_SYMBOL_GPL(符号名);  //只适用于包含GPL许可权的模块
    

    测试用例:

    #include <linux/init.h>
    #include <linux/module.h>
    
    int add_integer(int a, int b)
    {
        return a + b;
    }
    EXPORT_SYMBOL_GPL(add_integer);
    
    int sub_integer(int a, int b)
    {
        return a - b;
    }
    EXPORT_SYMBOL_GPL(sub_integer);
    
    MODULE_LICENSE("GPL v2");
    
    

    加载并且查看相关信息:

    $ insmod export_symb.ko 
    ffffffffc0701000 t add_integer  [export_symb]
    ffffffffc0701010 t sub_integer  [export_symb]
    

    7 模块声明与描述

    Linux内核模块中,使用以下函数进行声明:

    MODULE_AUTHOR  //作者
    MODULE_DESCRIPTION //描述
    MODULE_VERSION  //版本
    MODULE_DEVICE_TABLE  //设备表,对于USB、PCI驱动,表明该驱动模块所支持的设备
    MODULE_ALIAS //别名
    

    8 模块的使用计数

    Linux2.6之后的模块计数管理接口为:try_module_get(&module)和module_put(&module)。模块的使用计数不用模块自己管理。

    /* 用于增加模块使用计数;返回为0,表示调用失败,希望使用的模块没有加载或正在卸载 */
    int try_module_get(struct module *module);
    
    /* 用于减少模块的使用计数 */
    void module_put(struct module *module);
    

    9 模块的编译

    简单的Makefile:

    KVERS = $(shell uname -r)
    
    # Kernel modules
    obj-m += hello.o
    
    # Specify flags for the module compilation.
    #EXTRA_CFLAGS=-g -O0  #是否使用调试信息
    
    build: kernel_modules
    
    kernel_modules:
    	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
    
    clean:
    	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean
    
    

    该makefile和源码hello.c在同一个目录,运行make命令得到模块hello.ko。

    如果需要包括多个.c文件,需要更改:

    obj-m += modulename.o
    modulename-objs := file1.o file2.o
    
  • 相关阅读:
    并发编程的核心问题
    线程池的作用
    结构化并发编程:并发编程的分解方式与组织形式
    Dispatch Group
    yii视频地址哦
    Redis--各个数据类型最大存储量
    php开发中处理emoji表情和颜文字的兼容问题
    面向对象的三个基本特征(讲解)
    详解Ajax请求(四)——多个异步请求的执行顺序
    red入门学习笔记
  • 原文地址:https://www.cnblogs.com/mrlayfolk/p/15781248.html
Copyright © 2020-2023  润新知