Linux内核模块

1、什么是内核模块?

内核模块是Linux提供的一种机制,允许在内核运行时动态加载进内核中,具有两个特点:

         1)内核模块本身不编译入内核映像,有效控制缩减内核镜像大小

         2)内核模块一旦被加载,他就和内核中的其他部分完全一样

2、为什么需要内核模块?

如果在内核编译时把所有的功能都编译进去,就会导致内核很大,而且要往内核中添加或删除功能时必须重新编译内核

比如在Ubuntu在通用PC平台上,预先无法知道需要什么设备,就不知道预先编译什么驱动。

3、内核模块和应用程序的区别

 

工作模式

工作性质

层次

权限

影响

竞态

运行方式

应用程序

USR模式

策略性

用户层

局部

局部

主动

内核模块

SVC模式

功能性

内核层

全局

全局

被挡

          

4、内核模块的基本构成

|——两个函数(一般需要)
|        |——模块初始化(加载)函数:当内核模块加载进内核的时候,做一些准备工作
|        |——模块卸载函数:回收、清理资源
|
|——授权(许可证声明)(必须):Linux内核受GPL(General Public License)授权约束
|——模块参数(可选):模块被加载时可以被传递给它的值,本身对应模块内的全局变量
|——模块导出符号(可选)
|——模块信息说明(可选)

 5、模块加载(初始化)函数

一般以 __init标识声明

函数命名规则 xxx_init           xxx设备名       init功能名(初始化)

函数形式:

static ini __init  xxx_init(void)
{
         /* 初始化代码
            * 返回值:           成功:0                    失败:负数,绝对值是错误码
* 应用层得到的返回值是-1,错误码保存到errno(每个进程有一个);      标准化errno.h已经明确定义linux/errno.h
    */
}

注册方式: module_init(x);  x为模块初始化函数的首地址

 6、模块卸载函数

一般以 __exit标识声明

函数命名规则 xxx_exit          xxx设备名       exit功能名(卸载)

static ini __exit  xxx_exit(void)
{
         /* 释放代码 */
}

注册方式: module_exit(x); x为模块卸载函数的首地址

7、模块许可证声明

MODULE_LICENSE(_license)   //_license就是授权名称的字符串
//"GPL"                            [GNU Public License v2 or later]
//"GPL v2"                         [GNU Public License v2]
//"GPL and additional rights"     [GNU Public License v2 rights and more]
//"Dual BSD/GPL"                     [GNU Public License v2 or BSD license choice]
//"Dual MIT/GPL"                     [GNU Public License v2 or MIT license choice]
//"Dual MPL/GPL"                     [GNU Public License v2 or Mozilla license choice]

 8、模块声明与描述

在Linux内核模块中,我们可以用MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION、MODULE_DEVICE_TABLE、MODULE_ALIAS分别来声明模块的作者、描述、版本、设备表和别名,例如:

MODULE_AUTHOR(author);
MODULE_DESCRIPTION(description);
MODULE_VERSION(version_string);
MODULE_DEVICE_TABLE(table_info);
MODULE_ALIAS(alternate_name);

对于USB、PCI等设备驱动,通常会创建一个MODULE_DEVICE_TABLE,表明该驱动模块支持的设备,如:

/* 对应此驱动的设备列表 */

static struct usb_device_id skel_table [ ] = {
         {
                   USB_DEVICE(USB_SKEL_VENDOR_ID,
      USB_SKEL_PRODUCT_ID) },
       { } /* 表结束 */
         }
};
MODULE_DEVICE_TABLE (usb, skel_table);

9、模块参数:在加载模块时,可以给模块传参

头文件 linux/moduleparam.h 

A、传递普通变量

module_param(name, type, perm);
声明内核模块参数
/*
name - 接收参数的变量名
type - 变量类型
  Standard types are:
  byte, short, ushort, int, uint, long, ulong
  charp: a character pointer
  bool: a bool, values 0/1, y/n, Y/N.
  invbool: the above, only sense-reversed (N = true)
perm - 权限
  头文件 linux/stat.h
  #define S_IRWXUGO        (S_IRWXU|S_IRWXG|S_IRWXO)
  #define S_IALLUGO (S_ISUID|S_ISGID|S_ISVTX|S_IRWXUGO)
  #define S_IRUGO              (S_IRUSR|S_IRGRP|S_IROTH)
  #define S_IWUGO             (S_IWUSR|S_IWGRP|S_IWOTH)
  #define S_IXUGO              (S_IXUSR|S_IXGRP|S_IXOTH)
*/

 范例:

int i = 0;
module_param(i, int, 0644);

运行:

# insmod xxx.ko i=10

 B、传递数组参数

module_param_array(name, type, nump, perm)
/*
声明内核模块数组参数
name - 数组名
type - 数组成员类型
nump – 一个指向保存数组长度的整型变量的指针
perm - 权限
*/

范例:

int arr[] = {1,2,3,4,5,6};
int len=0;
module_param(arr, int, &len, 0644);

运行:

# insmod xxx.ko arr=1,2,3,4,5

 C、传递字符串参数

module_param_string(name, string, len, perm)
/*
声明内核模块字符串参数
name - 字符串缓存的外部名(传入变量名)
string - 字符串缓存的内部名
nump - 数组的数量
perm - 权限
*/

范例:

char insidestr[] = "hello world";
module_param(extstr, insidestr, szieof(insidestr), 0644);

运行:

# insmod xxx.ko extstr="hello"

 10、编译内核模块

如果一个内核模块要加载到某个内核中运行,则这个模块必须使用编译该内核镜像的源码进行编译,否则运行时会出错

A、头文件(语法问题)

B、编译结果(最主要影响)

  • 编译时符号表(只在编译时使用)
  • 运行时内核符号表
  • # cat /proc/kallsyms 运行时内核符号表

C、编译系统

示例Makefile:

# 内核模块的Makefile(模块源码在内核源码外,且内核先编译)
# 1、找内核的Makefile
# 2、内核的Makefile找内核模块的Makeifle
内核模块的Makeifle定义要编译对象
ifneq ($(KERNELRELEASE),)
#要编译对象表示把demo.c编译成demo.ko
  obj-m = demo.o
else
#内核源码目录
KERNELDIR :=  /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
modules:
  $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
clean:
  rm -rf .tmp_versions Module.symvers modules.order .tmp_versions .*.cmd *.o *.ko *.mod.c

KERNELRELEASE 是在内核源码的顶层Makefile中定义的一个变量,在第一次读取执行此Makefile时,KERNELRELEASE没有被定义,所以make将读取执行else之后的内容。

当make的目标为all时,-C $(KERNELDIR) 指明跳转到内核源码目录下读取那里的Makefile;M=$(PWD) 表明然后返回到当前目录继续读入、执行当前的Makefile。

当从内核源码目录返回时,KERNELRELEASE已被被定义,kbuild也被启动去解析kbuild语法的语句,make将继续读取else之前的内容。else之前的内容为kbuild语法的语句, 指明模块源码中各文件的依赖关系,以及要生成的目标模块名。

每个内核的名字都包含了它的版本号,这也是 uname -r 命令显示的值。

11、操作内核模块

A、加载模块

  1. # insmode 模块文件名 
  2. # modprobe 模块名   # depmod
  3. # modinfo  可以产看模块信息

B、查看模块

# lsmod

模块名       模块大小     使用计数         使用者(by没有内容的是用户模块,有没用的是内核模块)

Module           size          Used     by

demo            2333               0                                 (Used是当前有多少在用,为0时才允许被卸载)

mptscsih      39530               1   mptspi

C、卸载模块

# rmmod 模块名(Used为0时才允许被卸载)

D、查看内核输出信息

# dmesg | tail –n 10                  /* 查看内后最后十行信息 */

F、导出符号表

#define EXPORT_SYMBOL(导出符号表),符号可以是全局变量,也可以是函数

原文地址:https://www.cnblogs.com/chen-farsight/p/6128561.html