Hello World 模块

#include <linux/init.h>
#include <linux/module.h> MODULE_LICENSE("Dual BSD/GPL"); static int hello_init(void)
{
printk(KERN_ALERT "hello_world\n");
return 0;
} static void hello_exit(void)
{
printk(KERN_ALERT "goodbye\n");
} module_init(hello_init);
module_init(hello_exit);

核心模块与应用程序的对比

用户空间和内核空间

  • 不同的运行级别(超级用户态、用户态)
  • 各自的内存映射,即不同的地址空间

内核并发

内核编程需要考虑并发问题,即 Linux 内核代码(包括驱动代码)必须是重入的,必须能够同时运行在多个上下文中。

当前进程

current 是一个指向 struct task_struct (位于 linux/sched.h)的指针,通过访问该结构体获取当前进程信息。

早期 Linux 内核中 current 是全局变量,位于 asm.current.h ,kernel 2.6 后为了支持 SMP 系统,将指向 task_struct 的指针隐藏在内核栈中,只需包含 linux/sched.h 即可引用当前进程。

printk(KERN_INFO "The process is \"%s\" (pid %i)\n",
current->comm, current->pid);

其它细节

应用程序在虚拟内存中布局,有很大的栈空间。而内核的栈非常小,可能只有一个 4096 bytes 的页那样小,而且是整个内核空间共用的栈空间。

所以应避免大的自动变量,如果需要大的结构,应该在调用时动态分配该结构。

下划线前缀

以 __ 开头的函数通常是接口的底层组件,应谨慎使用。

浮点数

内核代码不能实现浮点数运算。如果打开了浮点支持,在某些架构上,需要在进出内核空间时保存和恢复浮点处理器的状态,这种额外开销没有价值。

编译和装载

编译

详细了解内核的构造过程,可阅读内核源码中 Documentation/kbuild 下的文件。

Documentation/Changes 文件列出了需要的工具版本。

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:
make -C $(KERNELDIR) M=$(PWD) modules clean endif

make -C $(KERNELDIR) M=$(PWD) modules: 首先改变目录到 -C 指定目录,其中保存有内核顶层的 Makefile 文件。 M= 选项让makefile 在构造 modules 目标之前返回指定目录。然后 modules 目标指向 obj-m 变量中指定的模块。

装载和卸载

  • insmod 用于装载,可以接受一些选项,并且可以在模块链接到内核前给模块的整型和字符串型变量赋值。
  • modprobe 同 insmod ,不同之处在于,它会考虑要装载的模块是否引用了一些内核中不存在的符号,如果有, modprobe 会在当前模块搜索路径中查找定义了这些符号的其它模块,如果找到,它会同时装载这些模块。
  • rmmod 用于卸载模块。但如果内核认为模块处于使用状态,或内核配置为禁止移除模块时,则无法移除该模块。可以但不建议将内核配置为模块忙时可“强制”移除模块。
  • lsmod 列出当前装载到内核中的所有模块及一些相关信息。

版本依赖

linux/module.h 中关于版本号的宏:

  • UTS_RELEASE: 内核版本字符串,如 "2.6.10"
  • LINUX_VERSION_CODE: 版本号的二进制表示,版本号的一部分对应一个字节,如 0x02060a
  • KERNEL_VERSION(major, minor, release): 转换版本号为二进制格式,如 KERNEL_VERSION(2, 6, 10) -> 0x02060a

内核符号表

内核符号表中包含了所有的全局内核项(函数和变量)的地址。有利于实现模块层叠。

导出符号:

EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);
  • GPL 版本导出的模块只能被 GPL 许可证下的模块使用
  • 必须在全局部分导出,导出的符号必须是全局的
  • 该变量将在可执行文件的特殊部分(即一个“ELF段”)中保存,装载时,根据这个段寻找导出的变量

预备知识

所有模块都包含的头文件

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

指定许可证

MODULE_LICENSE("GPL");

内核识别的许可证有:"GPL", "GPL v2", "GPL and additional rights", "Dual BSD/GPL", "Dual MPL/GPL", "Proprietary" ,如果未指定,则默认专有的,而内核装载这种模块就会被“污染”。

其它描述性定义

  • MODULE_AUTHOR
  • MODULE_DESCRIPTION
  • MODULE_VERSION
  • MODULE_ALIAS
  • MODULE_DEVICES_TABLE

可放在函数外的任意位置,一种习惯是放在文件末尾。

初始化和关闭

初始化

static int __init init_func()
{
// ...
} module_init(init_func);

__init 表明该函数仅在初始化期间使用。

__init__initdata 是可选的。 __devinit__devinitdata ,在内核配置为不支持热插拔的情况下才被翻译为 __init__initdata

模块可以注册不同类型的设施(设备、文件系统、密码交换等),每种设施对应有具体的内核函数完成注册。传递到注册函数的参数通常是指向描述新设施的数据结构(包含名称、模块函数指针等)的指针。

注册设施类型:串口、杂项设备、 sysfs 入口、 /proc 文件、可执行域、线路规程( line discipline )等。

清除

static void __exit cleanup_func(void)
{
// ...
} module_exit(cleanup_func);

__exit 标记的函数仅用于模块卸载,被放在特殊 ELF 段中。如果模块被内嵌到内核中,或内核配置为不允许卸载模块,则被标记函数被简单地丢弃。该函数仅在卸载模块或在系统关闭时被调用。

如果一个模块未定义清除函数,则内核不允许卸载该模块。

初始化过程的错误处理

模块代码必须始终检查初始化。

如果发生某个错误后无法继续装载模块,则要将出错之前的任何注册工作撤销(因为 Linux 中没有记录每个模块都注册了那些设施)。否则内核会处于不稳定状态。

内核经常使用 goto 处理错误,一个例程:

int __init init_func(void)
{
int err; err = register_this(ptr1, "shull");
if (err) goto fail_this;
err = register_that(ptr2, "shull");
if (err) goto fail_that;
err = register_those(ptr3, "shull");
if (err) goto fail_those; return 0; fail_those:
unregister_that(ptr2, "shull");
fail_that:
unregister_this(ptr1, "shull");
fail_this:
return err;
}

err 错误编码定义在 linux/errno.h 中。

习惯上清除函数以相反于注册的顺序撤销设施。

为减少 init 和 cleanup 函数的重复代码,可以用全局变量记录注册设施的注册结果,init 中在注册出错时 goto 至末尾调用 cleanup 函数, cleanup 通过记录变量进行撤销动作。因为要在 init 函数中调用 cleanup 函数,所以 cleanup 函数不能标记为 __exit

模块装载竞争

  • 情况一:

在注册完成后,内核的某些部分就可能立即使用注册的设施。

所以,在首次注册完成后,代码就应该准备好被调用;注册某个设施前,必须保证相关的内部初始化已完成。

  • 情况二:

初始化失败,但可能此时内核其它部分已经使用了注册的某些设施。

如果初始化失败,则必须仔细处理内核其它部分正在进行的操作,并且要等待这些操作完成。

模块参数

insmod hellop howmany=10 whom="Somebody"

为使参数对 insmod 可见,参数必须用 module_param 宏来声明。该宏在 moduleparam.h 中定义。

static char *whom = "world";
static int howmany = 1;
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);

module_param 三个参数:变量名称、类型、用于 sysfs 入口项的访问许可掩码。这个宏必须在函数外,通常在文件头部。

module_param 支持的类型:

bool invbool
布尔值( true 或 false ),关联的变量应该是 int 型。invbool 类型反转其值。
charp
字符指针值,内核会为用户提供的字符串分配内存,并相应设置指针。
int long short uint ulong ushort
不同类型的整数值 (模块代码中的钩子可让我们自定义一些类型,参阅 moduleparam.h 文件)

数组参数: module_param_array(name, type, num, perm) ,num 被设置为用户提供的元素个数(不能超过数组长度)。

所有模块参数都应该给定一个默认值。模块可以根据默认值来判断是否是一个显式指定的参数。

访问许可值 perm ,应使用 linux/stat.h 中存在的定义。如果 perm 为 0 ,则不会有对应的 sysfs 入口项;否则模块参数会在 /sys/module 中出现。 S_IRUGO 任何人只读, S_IRUGO | S_IWUSR 允许 root 修改该参数(可通过 sysfs 修改)。

用户空间驱动程序

优点:

  • 可以与整个 C 库链接
  • 可以使用通常的调试器调试代码
  • 如果被挂起,简单杀掉进程即可,且不会导致整个系统挂起
  • 用户内存可以换出
  • 容易避免因修改内核接口导致的不明确许可问题

缺点:

  • 中断在用户空间不可用
  • 只有通过 mmap 映射 /dev/mem 才能访问内存,且只有特权用户可以执行这个操作
  • 只有调用 ioperm 或 iopl 后才可以访问 I/O 端口,但并不是所有平台都支持这两个系统调用,并且访问 /dev/port 可能非常慢,同样只有特权用户可以执行
  • 相应时间很慢(客户端与硬盘之间传递数据和动作需要上下文切换)
  • 驱动程序可能被换出到磁盘,使用 mlock 系统调用可以缓解,但由于可能链接多个库,通常需要占用多个内存页,mlock 只有特权用户可用
  • 用户空间不能处理一些重要设备,如网络接口、块设备等

【Linux设备驱动程序】Chapter 2 - 构造和运行模块的更多相关文章

  1. linux设备驱动程序--hello-world

    linux字符设备驱动程序--hello_world 基于4.14内核, beagleBone green平台 PC端的设备驱动程序 有过电脑使用经验的人都知道,当我们将外部硬件设备比如鼠标键盘插入到 ...

  2. 嵌入式Linux设备驱动程序:在运行时读取驱动程序状态

    嵌入式Linux设备驱动程序:在运行时读取驱动程序状态 Embedded Linux device drivers: Reading driver state at runtime 在运行时了解驱动程 ...

  3. Linux设备驱动程序学习----1.设备驱动程序简介

    设备驱动程序简介 更多内容请参考Linux设备驱动程序学习----目录 1. 简介   Linux系统的优点是,系统内部实现细节对所有人都是公开的.Linux内核由大量复杂的代码组成,设备驱动程序可以 ...

  4. Linux设备驱动程序学习----3.模块的编译和装载

    模块的编译和装载 更多内容请参考Linux设备驱动程序学习----目录 1. 设置测试系统 第1步,要先从kernel.org的镜像网站上获取一个主线内核,并安装到自己的系统中,因为学习驱动程序的编写 ...

  5. Linux设备驱动程序学习----目录

    目录 设备驱动程序简介 1.设备驱动程序简介 构造和运行模块 2.内核模块和应用程序的对比 3.模块编译和装载 4.模块的内核符号表  5.模块初始化和关闭  6.模块参数  7.用户空间编写驱动程序 ...

  6. linux设备驱动程序该添加哪些头文件以及驱动常用头文件介绍(转)

    原文链接:http://blog.chinaunix.net/uid-22609852-id-3506475.html 驱动常用头文件介绍 #include <linux/***.h> 是 ...

  7. Linux设备驱动程序 第三版 读书笔记(一)

    Linux设备驱动程序 第三版 读书笔记(一) Bob Zhang 2017.08.25 编写基本的Hello World模块 #include <linux/init.h> #inclu ...

  8. Linux设备驱动程序学习之分配内存

    内核为设备驱动提供了一个统一的内存管理接口,所以模块无需涉及分段和分页等问题. 我已经在第一个scull模块中使用了 kmalloc 和 kfree 来分配和释放内存空间. kmalloc 函数内幕 ...

  9. Linux设备驱动程序学习----2.内核模块与应用程序的对比

    内核模块与应用程序的对比 更多内容请参考Linux设备驱动程序学习----目录 1. 内核模块与应用程序的对比 内核模块和应用程序之间的不同之处: 大多数中小规模的应用程序是从头到尾执行单个任务,而模 ...

随机推荐

  1. BZOJ 3083 遥远的国度(树链剖分+线段树)

    [题目链接] http://www.lydsy.com/JudgeOnline/problem.php?id=3083 [题目大意] 链修改,子树最小值查询和换根操作 [题解] 树链剖分练习题. [代 ...

  2. 【模拟退火】poj1379 Run Away

    题意:平面上找一个点,使得其到给定的n个点的距离的最小值最大. 模拟退火看这篇:http://www.cnblogs.com/autsky-jadek/p/7524208.html 这题稍有不同之处仅 ...

  3. Java并发(三):重排序

    在执行程序时为了提高性能,提高并行度,编译器和处理器常常会对指令做重排序.重排序分三种类型: 编译器优化的重排序.编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序. 指令级并行的重排序 ...

  4. Problem F: 零起点学算法42——多组测试数据输出II

    #include<stdio.h> int main() { ; while(scanf("%d%d%d",&a,&b,&c)!=EOF) { ...

  5. opencv中SiftDescriptorExtractor所做的SIFT特征向量提取工作简单分析

    SiftDescriptorExtractor对应于SIFT算法中特征向量提取的工作,通过他对关键点周围邻域内的像素分块进行梯度运算,得到128维的特征向量.具体有如下几个操作: 0.首先,我们假设在 ...

  6. Linux系统/etc/sysconfig目录下没有iptables文件

    在新安装的linux系统中,防火墙默认是被禁掉的,一般也没有配置过任何防火墙的策略,所有不存在/etc/sysconfig/iptables文件. 解决办法: 1.键入以下命令,新建文件 2.复制以下 ...

  7. 翻译:Spring-Framework-Reference Document:15.2-DispatcherServlet

    写在前面的话:   最近被项目的代码折腾的死去活来的,其实框架也没有那么难理解,只是自己的Web基础太差,被Request和Response这一对神雕侠侣坑到泪流满面!今天捣腾了一下Spring We ...

  8. Android内存优化7 内存检测工具1 Memory Monitor检测内存泄露

    上篇说了一些性能优化的理论部分,主要是回顾一下,有了理论,小平同志又讲了,实践是检验真理的唯一标准,对于内存泄露的问题,现在通过Android Studio自带工具Memory Monitor 检测出 ...

  9. 云计算与OpenStack(虚拟机Nova篇)

    <云计算与OpenStack(虚拟机Nova篇)> 基本信息 作者: 伯龙 程志鹏 张杰 出版社:电子工业出版社 ISBN:9787121201202 上架时间:2013-8-5 出版日期 ...

  10. EasyUI datagrid 复选框可以多选但不能全选功能实现

    1.功能需求:  实现多选,但是不能够全选功能 2.js代码 //帮卖列表页面,可以多选但是不能够全选实现 $(".datagrid-header-check").children ...