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);

}

相关函数对比

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

复制代码
相关推荐
阿钱真强道12 分钟前
21 嵌入式操作系统 | JSON 与 json-c:生成与解析
c语言·开发语言·json·json-c
心中有你02143 小时前
C语言埃氏筛法(素数筛选)
c语言·数据结构·算法
雯宝6 小时前
mnt_init 函数
linux
律宏阔12 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔12 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴12 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千12 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei3712 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei3712 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei3712 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化