Linux字符设备驱动

字符驱动设备

字符设备架构

在 Linux 中使用 cdev 结构体表示一个字符设备,cdev中最重要的有设备号dev_t和file_operations这两个结构体 。file_operations中包含了字符设备的操作函数,其中比较重要的有的 read(),write(),ioctl(), open(),close()这几个函数。

cdev 结构体定义如下

c 复制代码
<include/linux/cdev.h>
 
struct cdev { 
	//内嵌的内核对象.
    struct kobject kobj;            
    //该字符设备所在的内核模块的对象指针,一般为THIS_MODULE,可以防止设备的方法正在被使用时,设备所在模块被卸载。
    //如果 owner 变量的值为THIS_MODULE,表示file_operations只应用于当前驱动模块。
    struct module *owner; 
    //文件操作结构体,自己定义的那个
    const struct file_operations *ops;    
    //用来将已经向内核注册的所有字符设备形成链表.
    struct list_head list;   
    //字符设备的设备号,由主设备号和次设备号构成.        
    dev_t dev;  
    //隶属于同一主设备号的次设备号的个数.               
    unsigned int count;                   
};

字符设备开发流程

字符驱动的编写

驱动模块的装载和卸载
c 复制代码
//注册模块加载函数
module_init(xxx_init);

//注册模块卸载函数
module_exit(xxx_exit);
  1. module_init 函数用来向 Linux 内核注册一个模块加载函数,参数 xxx_init 就是需要注册的具体函数
    1. "insmod"命令加载驱动的时候,xxx_init 这个函数就会被调用。
    2. modeprode命令相比于insmod更智能。insmod无法解决模块的依赖问题,即1.ko如果依赖2.ko。那你必须手先安装2.ko才能安装1.ko。modeprode会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中.modprobe 命令默认会去/lib/modules/<kernel-version>目录中查找模块,自制的文件系统一般不会有这个目录,需要手动创建。推荐使用modeprode。
  2. module_exit()函数用来向 Linux 内核注册一个模块卸载函数,参数 xxx_exit 就是需要注册的具体函数
    1. 当使用"rmmod"命令卸载具体驱动的时候 xxx_exit 函数就会被调用。
    2. modprobe -r drv.ko ,命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,否则就不能使用 modprobe 来卸载驱动模块。所以对于模块的卸载,还是推荐使用 rmmod 命令
      扩展链接:[[#驱动模块的装载方式]]
字符设备的注册与注销

一般驱动设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块

的出口函数 xxx_exit 中进行。

根据设备号的使用情况,和动态/静态申请设备号可分为以下几种情况。[[#主次设备号]]

设备号的申请
所有次设备号被占用
c 复制代码
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
  1. register_chrdev 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:
    major:主设备号。使用该函数注册的设备会将一个主设备号的后的次设备号全部占用,造成设备号的浪费。
    name:设备名字,指向一串字符串。
    fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
  2. unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:
    major:要注销的设备对应的主设备号。
    name:要注销的设备对应的设备名。
动态申请设备号
c 复制代码
//major为0时,设备号动态申请,其会返回申请的设备号
//该函数不仅会完成设备号的申请,还会完成cdev_init.cdev_add.
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)

//动态申请设备号
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
  1. alloc_chrdev_region 用于申请设备号,此函数有 4 个参数:
    • dev:保存申请到的设备号。
    • baseminor:次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
    • count:要申请的设备号数量。
    • name:设备名字。
静态申请设备号
c 复制代码
//自己决定主次设备号传给register_chrdev_region
devno = MKDEV(major,minor);  
int register_chrdev_region(dev_t from, unsigned count, const char *name)

//可以自己决定主设备号
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
  1. register_chrdev_region把一段 (major, minor) 设备号区间,登记到内核的"字符设备号管理表"里,声明:这段号我占了,别人别用。
    • from:起始设备号(包含 major + minor
    • count:连续多少个 minor,一般为1
    • name:显示在 /proc/devices / 内核表里的名字
设备号的注销
c 复制代码
//注销一定范围内的设备号
//register_chrdev_region和alloc_chrdev_region申请的设备号,都用以下函数注销
void unregister_chrdev_region(dev_t from, unsigned count)

//register_chrdev申请的设备用这个注销
static inline void unregister_chrdev(unsigned int major, const char *name)
{
__unregister_chrdev(major, 0, 256, name);
}
实现具体的操作函数

具体的操作函数即为file_operations 结构体

c 复制代码
/*

* 设备操作函数结构体

*/

static struct file_operations chrdevbase_fops = {

.owner = THIS_MODULE,

.open = chrdevbase_open,

.read = chrdevbase_read,

.write = chrdevbase_write,

.release = chrdevbase_release,

};
cdev结构体的注册和删除

在准备好设备号和file_operations后,就需要将二者与cdev结构体关联起来。[[#字符设备架构]]

  1. 创建一个cdev
  2. 初始化cdev,将file_operations填入结构cdev结构体中
  3. 将设备号填入结构体cdev中,同时向内核注册cdev
cdev_init
c 复制代码
//创建一个结构体,用cdev结构体完成初始化
struct cdev test_cdev;

void cdev_init(struct cdev *cdev, const struct file_operations *fops);

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
  1. cdev_init:初始化一个cdev结构体,并与file_operations关联
    • cdev:自己创建的字符设备结构体
    • file_operations:具体的操作函数
  2. cdev_add:向内核注册一个cdev结构体,并将其与设备号绑定。
    • p:cdev的结构体指针
    • dev:设备号
    • count:要添加设备的数量
cdev_alloc
c 复制代码
test_cdev=struct cdev *cdev_alloc(void);

test_cdev.ops=xxx_ops;

int cdev_add(struct cdev *p, dev_t dev, unsigned count)
  1. cdev_alloc:返回一个初始化过的cdev结构体,但还没有与file_operations关联需显示指定。
cdev结构体的注销
c 复制代码
void cdev_del(struct cdev *p)

cdev_del 和 unregister_chrdev_region 这两个函数合起来的功能相当于 unregister_chrdev 函数。

类与设备节点的创建和删除

\[#如何创建和删除设备节点与类\]

创建在module_init()

删除在module_exit()

指定驱动相关信息
c 复制代码
MODULE_AUTHOR("xxx"); //作者
MODULE_DESCRIPTION("SW DECODER DIRVER"); //描述
MODULE_LICENSE("GPL"); //许可证

modinfo ./xxx.ko可以查看驱动模块相关信息(注查看的不是已经装载在内核里的驱动,而是具体的.ko文件)

测试应用的编写

驱动与应用的编译

驱动的编译
c 复制代码
KERNELDIR := /lib/modules/$(shell uname -r)/build //指定内核源码树的位置,uname表示当前的内核版本

CURRENT_PATH := $(shell pwd) //获取当前驱动源码的位置,方便后面告诉内核编译系统"我们要编译的代码就在这里"。

obj-m := chrdevbase.o   //将chrdevbase.o,编译成一个可加载的内核模块

build: kernel_modules  //编译的默认目标

kernel_modules:

// $(MAKE):调用系统 make 工具。
// -C $(KERNELDIR):这是"借鸡生蛋"。它让 make 先跳转到内核源码目录(KERNELDIR)去执行那里的 Makefile。
// M=$(CURRENT_PATH):告诉内核 Makefile,在处理完内核环境后,回到这个路径来编译我们的驱动源码。
//modules:这是内核 Makefile 里的一个目标,表示只编译模块,不编译整个内核
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules 

clean:

	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

Make的流程:

  1. $(MAKE):调用系统 make 工具。
  2. -C $(KERNELDIR):切换到指定的内核目录去执行相应的Makefile
  3. M=$(CURRENT_PATH:告诉内核 Makefile要编译的源码在哪
  4. modules:告诉Makefile,只编译模块
  5. obj-m := chrdevbase.o:告诉Makefile,将哪些文件编译成模块,以及编译的方式。
    1. 其会根据xxx.o去自动查找,同名的xxx.c文件。[[universe/Software and Environment/Makefile#Makefile的隐式规则|Makefile的隐式编译规则]]
应用的编译
c 复制代码
//指定的编译工具 源码.c -o 可执行文件
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp

测试与调试

复制代码
测试程序 设备节点 相应操作

字符设备相关函数/结构体详解

register_chrdev

  1. register_chrdev:其本质上是调用了__register_chrdev。__register_chrdev在major为0时会动态申请设备号。register_chrdev给__register_chrdev传的次设备号的起始数为0,申请数量为256。故register_chrdev会浪费,一个主设备号下的所有次设备号。
c 复制代码
static inline int register_chrdev(unsigned int major, const char *name,

const struct file_operations *fops)

{

return __register_chrdev(major, 0, 256, name, fops);

}


/**

* __register_chrdev() - create and register a cdev occupying a range of minors

* @major: major device number or 0 for dynamic allocation

* @baseminor: first of the requested range of minor numbers

* @count: the number of minor numbers required

* @name: name of this range of devices

* @fops: file operations associated with this devices

*

* If @major == 0 this functions will dynamically allocate a major and return

* its number.

*

* If @major > 0 this function will attempt to reserve a device with the given

* major number and will return zero on success.

*

* Returns a -ve errno on failure.

*

* The name of this device has nothing to do with the name of the device in

* /dev. It only helps to keep track of the different owners of devices. If

* your module name has only one type of devices it's ok to use e.g. the name

* of the module here.

*/

int __register_chrdev(unsigned int major, unsigned int baseminor,

unsigned int count, const char *name,

const struct file_operations *fops)
{

struct char_device_struct *cd;

struct cdev *cdev;

int err = -ENOMEM;

  

cd = __register_chrdev_region(major, baseminor, count, name);

if (IS_ERR(cd))

return PTR_ERR(cd);

  

cdev = cdev_alloc();

if (!cdev)

goto out2;

  

cdev->owner = fops->owner;

cdev->ops = fops;

kobject_set_name(&cdev->kobj, "%s", name);

  

err = cdev_add(cdev, MKDEV(cd->major, baseminor), count);

if (err)

goto out;

  

cd->cdev = cdev;

  

return major ? 0 : cd->major;

out:

kobject_put(&cdev->kobj);

out2:

kfree(__unregister_chrdev_region(cd->major, baseminor, count));

return err;

}

register_chrdev_region和alloc_chrdev_region

register_chrdev_region和alloc_chrdev_region

以上两者本质上都调用了__register_chrdev_region。最大的区别在于alloc_chrdev_region给__register_chrdev_region传major为0,register_chrdev_region传的major是具体的。

二者都只是先内核申请了设备号,并告诉内核与这设备号绑定的设备名字。

c 复制代码
/**

* register_chrdev_region() - register a range of device numbers

* @from: the first in the desired range of device numbers; must include

* the major number.

* @count: the number of consecutive device numbers required

* @name: the name of the device or driver.

*

* Return value is zero on success, a negative error code on failure.

*/

int register_chrdev_region(dev_t from, unsigned count, const char *name)

{

struct char_device_struct *cd;

dev_t to = from + count;

dev_t n, next;

  

for (n = from; n < to; n = next) {

next = MKDEV(MAJOR(n)+1, 0);

if (next > to)

next = to;

cd = __register_chrdev_region(MAJOR(n), MINOR(n),

next - n, name);

if (IS_ERR(cd))

goto fail;

}

return 0;

fail:

to = n;

for (n = from; n < to; n = next) {

next = MKDEV(MAJOR(n)+1, 0);

kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));

}

return PTR_ERR(cd);

}

/**

* alloc_chrdev_region() - register a range of char device numbers

* @dev: output parameter for first assigned number

* @baseminor: first of the requested range of minor numbers

* @count: the number of minor numbers required

* @name: the name of the associated device or driver

*

* Allocates a range of char device numbers. The major number will be

* chosen dynamically, and returned (along with the first minor number)

* in @dev. Returns zero or a negative error code.

*/

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,

const char *name)

{

struct char_device_struct *cd;

cd = __register_chrdev_region(0, baseminor, count, name);

if (IS_ERR(cd))

return PTR_ERR(cd);

*dev = MKDEV(cd->major, cd->baseminor);

return 0;

}

/*

* Register a single major with a specified minor range.

*

* If major == 0 this function will dynamically allocate an unused major.

* If major > 0 this function will attempt to reserve the range of minors

* with given major.

*

*/

static struct char_device_struct *

__register_chrdev_region(unsigned int major, unsigned int baseminor,

int minorct, const char *name)

{

struct char_device_struct *cd, *curr, *prev = NULL;

int ret;

int i;

  

if (major >= CHRDEV_MAJOR_MAX) {

pr_err("CHRDEV \"%s\" major requested (%u) is greater than the maximum (%u)\n",

name, major, CHRDEV_MAJOR_MAX-1);

return ERR_PTR(-EINVAL);

}

  

if (minorct > MINORMASK + 1 - baseminor) {

pr_err("CHRDEV \"%s\" minor range requested (%u-%u) is out of range of maximum range (%u-%u) for a single major\n",

name, baseminor, baseminor + minorct - 1, 0, MINORMASK);

return ERR_PTR(-EINVAL);

}

  

cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);

if (cd == NULL)

return ERR_PTR(-ENOMEM);

  

mutex_lock(&chrdevs_lock);

  

if (major == 0) {

ret = find_dynamic_major();

if (ret < 0) {

pr_err("CHRDEV \"%s\" dynamic allocation region is full\n",

name);

goto out;

}

major = ret;

}

  

ret = -EBUSY;

i = major_to_index(major);

for (curr = chrdevs[i]; curr; prev = curr, curr = curr->next) {

if (curr->major < major)

continue;

  

if (curr->major > major)

break;

  

if (curr->baseminor + curr->minorct <= baseminor)

continue;

  

if (curr->baseminor >= baseminor + minorct)

break;

  

goto out;

}

  

cd->major = major;

cd->baseminor = baseminor;

cd->minorct = minorct;

strlcpy(cd->name, name, sizeof(cd->name));

  

if (!prev) {

cd->next = curr;

chrdevs[i] = cd;

} else {

cd->next = prev->next;

prev->next = cd;

}

  

mutex_unlock(&chrdevs_lock);

return cd;

out:

mutex_unlock(&chrdevs_lock);

kfree(cd);

return ERR_PTR(ret);

}

相关函数对比

表中同块的函数,功能相同。

复制代码
相关推荐
是娇娇公主~5 小时前
Redis 悲观锁与乐观锁
linux·redis·面试
晚风_END6 小时前
Linux|服务器运维|diff和vimdiff命令详解
linux·运维·服务器·开发语言·网络
HIT_Weston6 小时前
83、【Ubuntu】【Hugo】搭建私人博客:文章目录(二)
linux·运维·ubuntu
.普通人6 小时前
树莓派4Linux 单个gpio口驱动编写
linux
luckily灬6 小时前
Docker执行hello-world报错&Docker镜像源DNS解析异常处理
linux·docker
一路往蓝-Anbo6 小时前
STM32单线串口通讯实战(二):链路层核心 —— DMA环形缓冲与收发切换时序
c语言·开发语言·stm32·单片机·嵌入式硬件·物联网
散峰而望6 小时前
【算法竞赛】C++入门(三)、C++输入输出初级 -- 习题篇
c语言·开发语言·数据结构·c++·算法·github
REDcker7 小时前
C++ 崩溃堆栈捕获库详解
linux·开发语言·c++·tcp/ip·架构·崩溃·堆栈
技术小李...7 小时前
Linux7.2安装Lsync3.1.2文件同步服务
linux·lsync