一文讲解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() 简要描述模块的功能。

资料直通车:Linux内核源码技术学习路线+视频教程内核源码

学习直通车:Linuxc/c++高级开发【直播公开课】

零声白金VIP体验卡:零声白金VIP体验卡(含基础架构/高性能存储/golang/QT/音视频/Linux内核)

错误和消息打印

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

当遇到错误时,必须撤销在这个错误发生之前的所有设置。通常的做法是使用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

原文作者:【一起学嵌入式

相关推荐
1900431 分钟前
linux复习5:C prog
linux·运维·服务器
猫猫的小茶馆11 分钟前
【C语言】指针常量和常量指针
linux·c语言·开发语言·嵌入式软件
昵称p12 分钟前
杂项驱动开发
驱动开发·gpio子系统·杂项驱动开发
朝九晚五ฺ33 分钟前
【Linux探索学习】第十五弹——环境变量:深入解析操作系统中的进程环境变量
linux·运维·学习
ernesto_ji1 小时前
Jenkins下载安装、构建部署到linux远程启动运行
linux·servlet·jenkins
李迟1 小时前
某Linux发行版本无法使用nodejs程序重命名文件问题的研究
java·linux·服务器
施努卡机器视觉1 小时前
电解车间铜业机器人剥片技术是现代铜冶炼过程中自动化和智能化的重要体现
运维·机器人·自动化
徐浪老师1 小时前
深入实践 Shell 脚本编程:高效自动化操作指南
运维·chrome·自动化
King's King1 小时前
自动化立体仓库:详解
运维·自动化