一文讲解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

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

相关推荐
Michaelwubo16 分钟前
Docker dockerfile镜像编码 centos7
运维·docker·容器
远游客071322 分钟前
centos stream 8下载安装遇到的坑
linux·服务器·centos
马甲是掉不了一点的<.<22 分钟前
本地电脑使用命令行上传文件至远程服务器
linux·scp·cmd·远程文件上传
jingyu飞鸟23 分钟前
centos-stream9系统安装docker
linux·docker·centos
好像是个likun1 小时前
使用docker拉取镜像很慢或者总是超时的问题
运维·docker·容器
超爱吃士力架1 小时前
邀请逻辑
java·linux·后端
LIKEYYLL2 小时前
GNU Octave:特性、使用案例、工具箱、环境与界面
服务器·gnu
云云3213 小时前
搭建云手机平台的技术要求?
服务器·线性代数·安全·智能手机·矩阵
云云3213 小时前
云手机有哪些用途?云手机选择推荐
服务器·线性代数·安全·智能手机·矩阵
cominglately3 小时前
centos单机部署seata
linux·运维·centos