linux 驱动开发 - 内核模块

一、Linux内核简介

1.宏内核与微内核

内核分为四大类:单内核(宏内核);微内核;混合内核;外内核。

  • 宏内核(Monolithickernel)是将内核从整体上作为一个大过程来实现,所有的内核服务都在一个地址空间运行,相互之间直接调用函数,简单高效。
    • Linux虽是宏内核,但已吸收了微内核的部分精华。Linux是模块化的、多线程的、内核本身可调度的系统,既吸收了微内核的精华,又保留了宏内核的优点,无需消息传递,避免性能损失。
  • 微内核(Microkernel)功能被划分成独立的过程,过程间通过IPC进行通信,模块化程度高,一个服务失效不会影响另外一个服务。
image

2.Linux体系架构

从两个层次上来考虑操作系统

  • 用户空间:包含了用户的应用程序和C库
    • GNU C Library (glibc)提供了连接内核的系统调用接口,还提供了在用户空间应用程序和内核之间进行转换的机制。
  • 内核空间:包含了系统调用,内核,以及与平台架构相关的代码
image

划分原因

  • 现代CPU通常都实现了不同的工作模式

    • 以ARM为例:ARM实现了7种工作模式,不同模式下CPU可以执行的指令或者访问的寄存器不同:

      • (1)用户模式 usr
      • (2)系统模式 sys
      • (3)管理模式 svc
      • (4)快速中断 fiq
      • (5)外部中断 irq
      • (6)数据访问终止 abt
      • (7)未定义指令异常;
    • 以X86为例:X86实现了4个不同级别的权限,Ring0—Ring3 ;Ring0下可以执行特权指令,可以访问IO设备;Ring3则有很多的限制。

  • 为了保护内核的安全,把系统分成了2部分:用户空间和内核空间是程序执行的两种不同状态,我们可以通过“系统调用”和“硬件中断“来完成用户空间到内核空间的转移;

3.Linux的内核结构

Linux内核是整体式结构(宏内核),各个子系统联系紧密,作为一个大程序在内核空间运行。

系统调用接口(system call interface,SCI)提供了某些机制执行从用户空间到内核的函数调用。

image
1)Linux内核组成(子系统)
  • 进程调度(SCHED):控制多个进程对CPU的访问。当需要选择下一个进程运行时,由调度程序选择最值得运行的进程。可运行进程实际上是仅等待CPU资源的进程,如果某个进程在等待其它资源,则该进程是不可运行进程。Linux使用了比较简单的基于优先级的进程调度算法选择新的进程。

  • 内存管理(memory management,MM):允许多个进程安全的共享主内存区域。Linux 的内存管理支持虚拟内存,即在计算机中运行的程序,其代码,数据,堆栈的总量可以超过实际内存的大小,操作系统只是把当前使用的程序块保留在内存中,其余的程序块则保留在磁盘中。必要时,操作系统负责在磁盘和内存间交换程序块。内存管理从逻辑上分为硬件无关部分和硬件有关部分。硬件无关部分提供了进程的映射和逻辑内存的对换;硬件相关的部分为内存管理硬件提供了虚拟接口。

    • 一般而言,Linux的每个进程享有4GB的内存空间,03GB属于用户空间,34GB属于内核空间。
  • 虚拟文件系统(Virtual File System,VFS):隐藏了各种硬件的具体细节,为所有的设备提供了统一的接口,VFS提供了多达数十种不同的文件系统。虚拟文件系统可以分为逻辑文件系统和设备驱动程序。逻辑文件系统指Linux所支持的文件系统,如ext2,fat等,设备驱动程序指为每一种硬件控制器所编写的设备驱动程序模块。

    image
  • 网络接口(NET):提供了对各种网络标准的存取和各种网络硬件的支持。网络接口可分为网络协议和网络驱动程序。网络协议部分负责实现每一种可能的网络传输协议。网络设备驱动程序负责与硬件设备通讯,每一种可能的硬件设备都有相应的设备驱动程序。

    image
  • 进程间通讯(inter-process communication,IPC): 支持进程间各种通信机制。

    • 共享内存
    • 管道
    • 信号量
    • 消息队列
    • 套接字

4.内核模块

Linux内核是模块化组成的,它允许内核在运行时动态地向其中插入或删除代码。

二、内核模块结构

1.头文件

内核模块头文件<linux/module.h>和<linux/init.h>是必不可少的 ,不同模块根据功能的差异,所需要的头文件也不相同 。

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

2.模块初始化

模块的初始化负责注册模块本身 ,只有已注册模块的各种方法才能够被应用程序使用并发挥各方法的实际功能。

模块并不是内核内部的代码,而是独立于内核之外,通过初始化,能够让内核之外的代码来替内核完成本应该由内核完成的功能,模块初始化的功能相当于模块与内核之间衔接的桥梁,告知内核已经准备好模块了。

内核模块初始化函数

//模块初始化函数一般都需声明为 static
//__init 表示初始化函数仅仅在初始化期间使用,一旦初始化完毕,将释放初始化函数所占用的内存
static int __init module_init_func(void)
{
    初始化代码
}
module_init(module_init_func);
//module_init宏定义会在模块的目标代码中增加一个特殊的代码段,用于说明该初始化函数所在的位置。

当使用 insmod 将模块加载进内核的时候,初始化函数的代码将会被执行。

3.模块退出

模块的退出相当于告知内核“我要离开了,将不再为您服务了”。

内核模块退出函数

//模块退出函数没有返回值;
//__exit 标记这段代码仅用于模块卸载;
static void __exit module_exit_func(void)
{
    //模块退出代码
}
module_exit(module_exit_func);
//没有 module_exit 定义的模块无法被卸载

当使用 rmmod 卸载模块时,退出函数的代码将被执行。

注意:如果模块被编译进内核,而不是动态加载,则__init的使用会在模块初始化完成后丢弃该函数并回收所占内存, _exit宏将忽略“清理收尾”的函数。

4.模块许可证声明

Linux 内核是开源的,遵守 GPL 协议,所以要求加载进内核的模块也最好遵循相关协议。

为模块指定遵守的协议用 MODULE_LINCENSE 来声明 :

MODULE_LICENSE("GPL");
  • 内核能够识别的协议有
    • “GPL”
    • “GPL v2”
    • “GPL and additional rights(GPL 及附加权利)”
    • “Dual BSD/GPL(BSD/GPL 双重许可)”
    • “Dual MPL/GPL(MPL/GPL 双重许可)”
    • “Proprietary(私有)”

5.模块导出符号 【可选】

使用模块导出符号,方便其它模块依赖于该模块,并使用模块中的变量和函数等。

  • 在Linux2.6的内核中,/proc/kallsyms文件对应着符号表,它记录了符号和符号对应的内存地址。

    $ cat /proc/kallsyms 
    ...
    ffffff80084039b8 t shash_digest_unaligned
    ffffff8008403a30 T crypto_shash_digest
    ffffff8008403ac0 t shash_async_final
    ffffff8008403af0 T shash_ahash_update
    ffffff8008403b50 t shash_async_update
    ffffff8008403b80 t crypto_exit_shash_ops_async
    ffffff8008403bb0 t crypto_shash_report
    ffffff8008403c18 t crypto_shash_show
    ffffff8008403c78 T crypto_alloc_shash
    ffffff8008403cc8 T crypto_register_shash
    ffffff8008403d00 T crypto_unregister_shash
    ffffff8008403d30 T crypto_register_shashes
    ffffff8008403df8 T crypto_unregister_shashes
    ffffff8008403e90 T shash_register_instance
    ffffff8008403ed0 T shash_free_instance
    ffffff8008403f08 T crypto_init_shash_spawn
    ffffff8008403f58 T shash_attr_alg
    ffffff8008403fb0 T shash_ahash_finup
    ffffff8008404068 t shash_async_finup
    ffffff80084040b0 T shash_ahash_digest
    ffffff80084041e0 t shash_async_digest
    ...
    
  • 使用一下宏定义导出符号

    EXPORT_SYMBOL(module_symbol);
    //或
    EXPORT_GPL_SYMBOL(module_symbol);
    

6.模块描述 [可选]

模块编写者还可以为所编写的模块增加一些其它描述信息,如模块作者、模块本身的描述或者模块版本等

MODULE_AUTHOR("Abing <Linux@zlgmcu.com>");
MODULE_DESCRIPTION("ZHIYUAN ecm1352 beep Driver");
MODULE_VERSION("V1.00");

模块描述以及许可证声明一般放在文件末尾。

三、向Linux内核添加新内核模块

1.添加模块驱动文件

在linux/drivers/下新建目录hello,并且在hello/目录下新建hello.c、Makefile、Kconfig三个文件。

1)内核模块程序hello.c
/* hello world module */

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>

static int __init hello_init(void)
{
  printk(KERN_INFO "Hello, I'm ready!\n");
  return 0;
}
static void __exit hello_exit(void)
{
  printk(KERN_INFO "I'll be leaving, bye!\n");
}
module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("michael");
MODULE_DESCRIPTION("hello world module");
  • 内核通过 printk() 输出的信息具有日志级别,日志级别是通过在 printk() 输出的字符串前加一个带尖括号的整数来控制的,如 printk(“<6>Hello, world!/n”);。内核中共提供了八种不同的日志级别

    // 在 linux/kernel.h 中有相应的宏对应
    #define KERN_EMERG    "<0>"    /* system is unusable */
    #define KERN_ALERT    "<1>"    /* action must be taken immediately */
    #define KERN_CRIT     "<2>"    /* critical conditions */
    #define KERN_ERR      "<3>"    /* error conditions */
    #define KERN_WARNING  "<4>"    /* warning conditions */
    #define KERN_NOTICE   "<5>"    /* normal but significant */
    #define KERN_INFO     "<6>"    /* informational */
    #define KERN_DEBUG    "<7>"    /* debug-level messages */
    
2)Kconfig
menu "HELLO TEST Driver "
comment "HELLO TEST Driver Config"
 
config HELLO
    tristate "hello module test"
    default m
    help
    This is the hello test driver.
 
endmenu
  • 在menuconfig的“driver”菜单下添加“HELLO TEST Driver”子菜单,并加入“HELLO”配置选项,选项默认为m。
  • 保存menuconfig后,会在kernel根目录下的.config文件中生成“CONFIG_HELLO=m”,在编译的时候会添加到临时环境变量中。
3)Makefile
obj-$(CONFIG_HELLO) += hello.o

可用于动态模块外部编译的写法

  • 编译模块的内核配置必须与所运行内核的编译配置一样 。

    ifneq ($(KERNELRELEASE),)
             obj-m += hello.o
    else
    KERNELDIR ?= /lib/modules/$(shell uname -r)/build  # 定义内核路径
    PWD := $(shell pwd)
    default:
            $(MAKE) -C $(KERNELDIR) M=$(PWD) modules   # 表示在当前目录下编译
    clean:
            rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions
    endif
    
    • KERNELRELEASE是在内核源码的顶层Makefile中定义的一个变量,在第一次读取执行此Makefile时,KERNELRELEASE没有被定义,所以make将读取执行else之后的内容。
    • 当从内核源码目录返回时,KERNELRELEASE已被定义,kbuild也被启动去解析kbuild语法的语句,make将继续读取else之前的内容,生成的目标模块名。

2.修改上一级目录的Kconfig和Makefile

进入linux/drivers/

  • 编辑Makefile,在后面添加一行:

    obj-$(CONFIG_HELLO) += hello/
    
  • 编辑Kconfig,在后面添加一行:

    source "drivers/hello/Kconfig"
    
    • 注:某些内核版本需要同时在arch/arm/Kconfig中添加:source "drivers/hello/Kconfig"

3.make menuconfig配置和编译

  • 执行:make menuconfig ARCH=arm进入配置菜单
  • 选择并进入:Device Drivers选项
image
  • 进入 HELLO TEST Driver选项

    image
    • 可以选择<m> <y> <n>,分别为编译成内核模块、编译进内核、不编译。

如果选择编译成动态模块<m>

  • 编译内核过程中,会有如下输出:

    LD drivers/hello/built-in.o
    CC [M] drivers/hello/hello.o
    CC drivers/hello/hello.mod.o
    LD [M] drivers/hello/hello.ko
    

如果选择编译进内核<y>

  • 编译内核过程中,会有如下输出:

    CC drivers/hello/hello.o
    LD drivers/hello/built-in.o
    

4.动态模块加载和卸载

加载模块使用 insmod 命令,卸载模块使用 rmmod 命令。

$ insmod hello.ko
$ rmmod hello.ko
#加载和卸载模块必须具有 root 权限 。

对于可接受参数的模块,在加载模块的时候为变量赋值即可,卸载模块无需参数。

$ insmod hello.ko num=8
$ rmmod hello.ko

四、带参数的内核模块

模块参数必须使用 module_param 宏来声明,通常放在文件头部。

module_param 需要 3个参数:变量名称、类型以及用于 sysfs 入口的访问掩码。

static int num = 5;
module_param(num, int, S_IRUGO);
  • 内核模块支持的参数类型有: bool、 invbool、 charp、 int、 short、 long、 uint、 ushort和 ulong。
  • 访问掩码的值在<linux/stat.h>定义, S_IRUGO 表示任何人都可以读取该参数,但不能修改。
  • 支持传参的模块需包含 moduleparam.h 头文件。

能够接收参数的模块范例

#include <linux/module.h>
#include <linux/init.h>
// moduleparam.h 文件已经包含在 module.h 文件中

static int num = 3;
static char *whom = "master";

module_param(num, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);

static int __init hello_init(void)
{
  printk(KERN_INFO "%s, I get %d\n", whom, num); //KERN_INFO 表示这条打印信息的级别
  return 0;
}

static void __exit hello_exit(void)
{
  printk("I'll be leaving, bye!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("luxiaodai");
MODULE_DESCRIPTION("this is my first module");
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容

  • make menuconfig过程解析作者 codercjg 在 28 九月 2015, 5:27 下午 make...
    codercjg阅读 925评论 0 1
  • 1 Linux 内核模块简介 Linux 内核是一个十分庞大的系统,如何能够为其瘦身,订制适合自己应用场景的 li...
    守拙圆阅读 1,060评论 0 2
  • 一、Linux内核模块简介 1.1 Linux内核模块介绍 Linux内核的整体结构已经非常庞大,而其包含的组件也...
    konishi5202阅读 2,850评论 0 4
  • 本人在开发Android Nfc POS之初,探索调试了一番驱动,目前在Nexus 5X 7.1.1上已经调成,之...
    Eric_Y15阅读 1,569评论 0 3
  • 告诫自己—— 帮助被教者最好的方式, 并非是“修正TA”。 而是在TA身上找到最大的优点。 约翰.麦斯威尔把这个方...
    小旅阅读 617评论 0 0