Linux驱动编程必备基础知识分享

简介: Linux驱动编程必备基础知识分享

驱动程序是专用于控制和管理特定硬件设备的软件,因此也被称作设备驱动程序。从操作系统的角度来看,它可以位于内核空间(以特权模式运行),也可以位于用户空间(具有较低的权限)。

对于 Linux 驱动程序来说,其运行在内核空间,把硬件功能提供给用户程序。

本篇文章主要介绍Linux驱动程序的一些基础知识。后面的文章再逐步展开。

 

内核空间和用户空间

内核空间和用户空间的概念有点抽象,主要涉及内存的访问权限。内核是有特权的,而用户应用程序则是受限制的。

内核空间

内核驻留和运行的地址空间。

内核内存受访问标志保护,只能由内核访问,用户应用程不能访问。另一方面,内核可以访问整个系统内存,因为它在系统上以更高的优先级运行。在内核模式下,CPU可以访问整个内存(内核空间和用户空间)。

用户空间

用户程序运行的地址空间。

在用户模式下,CPU只能访问标有用户空间访问权限的内存。用户应用程序运行到内核空间的唯一方法是通过系统调用。

当进程执行系统调用时,软件中断被发送到内核,这将打开特权模式,以便该进程可以在内核空间中运行。系统调用返回时,内核关闭特权模式,进程再次受限。

模块

Linux内核可以在运行时扩展。当系统运行时,我们可以向内核添加、删除功能。

可以在运行时添加到内核中的代码被称为“模块”。内核模块是即插即用的,一旦插入就可以使用。

模块要运行,应该先把它加载到内核,可以用 insmod 或 modprobe 来实现,前者需要指定模块路径作为参数,这是开发期间的首选;后者更智能化,是生产系统中的首选。

insmod /test/mydrv.ko

常用的模块卸载命令是 rmmod,使用该命令时,应该把要卸载的模块名作为参数向其传递。当卸载某个模块时,不会有其他影响,则会直接卸载;若有不良影响,内核会阻止这次卸载。

rmmod mymodule

或者使用下边的指令

modeprobe -r mymodule

设备模块分类

Linux系统的模块有三种基本类型:

  • 字符模块
  • 块模块
  • 网络模块

对应的设备设备驱动程序:

  • 字符设备驱动
  • 块设备驱动
  • 网络设备驱动

字符设备是个能够像字节流一样被访问的设备,由字符设备驱动程序来实现。

块设备每次只能传输一个或者多个完整的块,每块包含512字节(或者2的更高次幂字节的数据)。

网络接口由内核中的网络子系统驱动,负责发送和接收数据包。网络驱动程序不需要知道各个连接的相关信息,它只负责处理数据包即可。

当然还有其他划分驱动程序模块的方法,此处不再赘述。

驱动程序框架

Linux 驱动程序是有固定框架的,我们按照既定的框架,填写内容即可。

先看一个简单的内核模块程序 helloworld.c

#include <linux/init.h>

#include <linux/module.h>

#include <linux/kernel.h>


/* 模块入口点函数 */

static int helloworld_init(void)

{

pr_info("Hello world!\n");

return 0;

}


/* 模块出口点函数 */

static void helloworld_exit(void)

{

pr_info("End of the world\n");

}


/* 指定函数用途 */

module_init(helloworld_init);

module_exit(helloworld_exit);


MODULE_AUTHOR("zsky");

MODULE_LICENSE("GPL");

内核驱动程序与用户空间的程序是有很大区别的。

内核模块驱动程序有入口点和出口点,函数名字可以任意。用户程序的入口函数名称一般为 main()。

对于内核模块程序来说,需要开发人员指定入点和出点函数。在上例中,module_init()用于声明模块加载(使用 insmod 或 modprobe)时应该调用的函数为 helloworld_init,入口函数中要完成的操作是定义模块的行为。

module_exit() 用于声明模块卸载(使用 rmmod )时应该调用的函数为 helloworld_exit。

模块加载或者卸载后,init 函数或者 exit 函数立即运行一次。

在编写驱动程序的时候,需要包含很多头文件,以便获取函数、数据类型、变量的定义。有几个头文件是专门用于模块的:

#include <linux/init.h>

#include <linux/module.h>

module.h包含可装载模块需要的大量符号和函数的定义。init.h 用于指定入口函数和出口函数。

模块信息

内核模块使用其 .modinfo 部分来存储关于模块的信息,所有MODULE_*宏都用参数传递的值更新这部分的内容 。

其中一些宏是 MODULE_DESCRIPTION()、MODULE_AUTHOR() 和 MODULE_LICENSE()。

MODULE_LICENSE() 告诉内核模块采用何种许可,他对模块行为有影响,如果与指定的许可不兼容将导致内核模块被污染。

MODULE_AUTHOR() 用于声明模块的作者。

MODULE_DESCRIPTION() 简要描述模块的功能。

错误和消息打印

在模块函数处理过程中,一定要检测返回值,确保所有的请求操作已经真正成功。

当遇到错误时,必须撤销在这个错误发生之前的所有设置。通常的做法是使用goto语句。

ptr = kmalloc(sizeof (device_t));

if(!ptr)

{

 ret = -ENOMEM

 goto err_alloc;

}

ev = init(&ptr);

if(dev)

{

ret = -EIO

goto err_init;

}

eturn 0;


err_init:

 free(ptr);

err_alloc:

 return ret;

若模块装载过程中出错,要将出错之前的任何注册工作全部撤销,否则内核会处于一种不稳定的状态,因为内核中包含了一些指向并不存在的代码内部指针。

错误有时会跨越内核空间,传播到用户空间。如果返回的错误是对系统调用(open、read、ioctl、mmap)的响应,则该值将自动赋给用户空间 errno 全局变量,在该变量上调用 strerror(errno) 可以将错误转换为可读字符串。

当返回指针的函数返回错误时,通常返回的是NULL 指针。而去检查为什么会返回空指针是没有任何意义的,因为无法准确了解为什么会返回空指针。为此,内核提供了3个函数 ERR_PTR、IS_ERR 和 PTR_ERR:

void *ERR_PTR(long error);

long IS_ERR(const void *ptr);

long PTR_ERR(const void *ptr);

ERR_PTR 函数实际上把错误值作为指针返回。假若函数在内存申请失败后要执行语句 return -ENOMEM,则必须改为这样的语句:return ERR_PTR (-ENOMEM);。

IS_ERR 函数用于检查返回值是否是指针错误:if(IS_ERR(foo))。

PTR_ERR 函数返回实际错误代码:return PTR_ERR(foo);。

消息打印--printk()

不同于用户空间的 printf()函数。printk()是在内核空间使用的,其作用和在用户空间使用 printf() 一样,执行 dmesg 命令可以显示 printk() 写入的信息。

根据所打印消息的重要性不同,可以选用 include/linux/kern_levels.h 中定义的八个级别的日志消息,每个级别对应iyge字符串格式的数字,其优先级与该数字的值成反比:

#define KERN_SOH "\001" /* ASCII头开始 */

#define KERN_SOH_ASCII '\001'


#define KERN_EMERG KERN_SOH   "0"   /* 系统不可用*/

#define KERN_ALERT KERN_SOH   "1"   /* 必须立即采取行动*/

#define KERN_CRIT KERN_SOH     "2"   /* 重要条件*/

#define KERN_ERR KERN_SOH     "3"   /* 错误条件*/

#define KERN_WARNING KERN_SOH "4"   /* 警报条件*/

#define KERN_NOTICE KERN_SOH   "5"   /* 正常但重要的情况*/

#define KERN_INFO KERN_SOH     "6"   /* 信息 */

#define KERN_DEBUG KERN_SOH   "7"   /* 调试级别消息 */

举例:

printk(KERN_ERR "This is an error\n");

实际上可以使用以下宏,其名称更有意义,它们是对前面所定义内容的包装—— pr_emerg、pr_alert、pr_crit、pr_err、pr_warning、pr_notice、pr_info和 pr_debug。

printk() 的实现是这样的:调用它时,内核会将消息日志级别与当前控制台的日志级别进行比较;如果前者比后者更高(值更低),则消息会立即打印到控制台。

模块参数

像用户程序一样,内核模块也可以接收命令行参数。这样能够根据给定的参数动态地改变模块的行为,开发者不必在测试/调试期间无限期地修改/编译模块。

为了对此进行设置,首先应该声明用于保存命令行参数值的变量,并在每个变量上使用 module_param() 宏:

module_param(name, type, perm);

  • name:用作参数的变量的名称。
  • type:参数的类型(bool、charp、byte、short、ushort、int、uint、long、ulong),其中 charp 代表字符指针。
  • perm:代表/sys/module/<module>/parameters/<param>文件的权限,其中包括S_IWUSR、S_IRUSR、S_IXUSR、S_IRGRP、S_WGRP和S_IRUGO 。

当使用模块参数时,应该用 MODULE_PARM_DESC 描述每个参数。这个宏将把每个参数的描述填充到模块信息部分。

举例:

module_param(myint, int, S_IRUGO);

module_param(mystr, charp, S_IRUGO);

module_param_array(myarr, int,NULL, S_IWUSR|S_IRUSR);


MODULE_PARM_DESC(myint,"this is my int variable");

MODULE_PARM_DESC(mystr,"this is my char pointer variable");

MODULE_PARM_DESC(myarr,"this is my array of int");

要在加载该模块时提供参数,请执行以下操作:

# insmod hellomodule-params.ko

mystring="packtpub" myint=15 myArray=1,2,3

在加载模块之前,执行modinfo可以显示该模块支 持的参数说明:

modinfo ./helloworld-params.ko


 

目录
相关文章
|
3月前
|
Shell Linux
Linux shell编程学习笔记30:打造彩色的选项菜单
Linux shell编程学习笔记30:打造彩色的选项菜单
|
2天前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
35 13
|
1月前
|
运维 监控 Shell
深入理解Linux系统下的Shell脚本编程
【10月更文挑战第24天】本文将深入浅出地介绍Linux系统中Shell脚本的基础知识和实用技巧,帮助读者从零开始学习编写Shell脚本。通过本文的学习,你将能够掌握Shell脚本的基本语法、变量使用、流程控制以及函数定义等核心概念,并学会如何将这些知识应用于实际问题解决中。文章还将展示几个实用的Shell脚本例子,以加深对知识点的理解和应用。无论你是运维人员还是软件开发者,这篇文章都将为你提供强大的Linux自动化工具。
|
3月前
|
Shell Linux
Linux shell编程学习笔记82:w命令——一览无余
Linux shell编程学习笔记82:w命令——一览无余
|
3月前
|
Linux Shell
Linux系统编程:掌握popen函数的使用
记得在使用完 `popen`打开的流后,总是使用 `pclose`来正确关闭它,并回收资源。这种做法符合良好的编程习惯,有助于保持程序的健壮性和稳定性。
150 6
|
3月前
|
Linux Shell
Linux系统编程:掌握popen函数的使用
记得在使用完 `popen`打开的流后,总是使用 `pclose`来正确关闭它,并回收资源。这种做法符合良好的编程习惯,有助于保持程序的健壮性和稳定性。
161 3
|
4月前
|
项目管理 敏捷开发 开发框架
敏捷与瀑布的对决:解析Xamarin项目管理中如何运用敏捷方法提升开发效率并应对市场变化
【8月更文挑战第31天】在数字化时代,项目管理对软件开发至关重要,尤其是在跨平台框架 Xamarin 中。本文《Xamarin 项目管理:敏捷方法的应用》通过对比传统瀑布方法与敏捷方法,揭示敏捷在 Xamarin 项目中的优势。瀑布方法按线性顺序推进,适用于需求固定的小型项目;而敏捷方法如 Scrum 则强调迭代和增量开发,更适合需求多变、竞争激烈的环境。通过详细分析两种方法在 Xamarin 项目中的实际应用,本文展示了敏捷方法如何提高灵活性、适应性和开发效率,使其成为 Xamarin 项目成功的利器。
55 1
|
4月前
|
安全 Linux 开发工具
探索Linux操作系统:从命令行到脚本编程
【8月更文挑战第31天】在这篇文章中,我们将一起潜入Linux操作系统的海洋,从最基础的命令行操作开始,逐步深入到编写实用的脚本。无论你是初学者还是有一定经验的开发者,这篇文章都将为你提供新的视角和实用技能。我们将通过实际代码示例,展示如何在日常工作中利用Linux的强大功能来简化任务和提高效率。准备好了吗?让我们一起开启这段旅程,探索Linux的奥秘吧!
|
4月前
|
Linux
揭秘Linux心脏:那些让你的编程事半功倍的主要系统调用
【8月更文挑战第31天】Linux中的系统调用是操作系统提供给应用程序的接口,用于请求内核服务,如文件操作、进程控制等。本文列举了22种主要系统调用,包括fork()、exec()、exit()、wait()、open()、close()、read()、write()等,并通过示例代码展示了如何使用fork()创建新进程及使用open()、write()、close()操作文件。这些系统调用是Linux中最基本的接口,帮助应用程序与内核交互。
67 1
|
3月前
|
Shell Linux Python
python执行linux系统命令的几种方法(python3经典编程案例)
文章介绍了多种使用Python执行Linux系统命令的方法,包括使用os模块的不同函数以及subprocess模块来调用shell命令并处理其输出。
69 0