模块机制的作用:
该机制有助于缩短模块的开发周期。即:注册和卸载都很灵活方便。模块化还涉及依赖关系。
1.有助于维护和功能扩充
2.程序结构清晰
3.保证系统稳定
4.提高开发速率
主设备号、次设备号与设备名字name的区别
主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的其他设备。都是用整数表示;位于/sys/class/目录下
设备名字:指向一个字符串,通常用于用户界面,位于/dev/目录下
设备、驱动、模块的区别
设备:硬件或者软件的实体,可以被识别或应用的组件
驱动:软件,让操作系统能与硬件进行交互
模块:代码片段,作为内核的一部分,可以在运行时加载或者卸载。在linux中设备驱动常被实现为内核模块
设备节点是什么?
是一种特殊文件,表示了硬件的抽象。设备节点允许用户程序通过标准的文件操作接口(open,read,write,ioctl等)来访问硬件设备
设备节点与设备驱动的关联是通过设备号实现的,包括主设备号和次设备号。应用程序通过访问设备节点读取主设备号和次设备号,通过主设备号找对应的驱动,通过次设备号对应到具体物理设备。
静态分配设备号,创建节点的过程
Cat /proc/devices 命令可以查看系统中所有已经使用了的设备号
静态分配设备号是指在驱动程序中预先设定好主从设备号,然后通过系统调用将这些设备号注册到内核中
-
定义设备号
-
注册设备号:驱动程序加载成功后需要注册;
static inline int register_chrdev(unsigned int major, const char *name,const
struct file_operations *fops)
register_chrdev(CHRDEVBASE_MAJOR,"hello_driver",&hello_world_fops);
3.注销设备号:在驱动程序退出时使用
static inline void unregister_chrdev(unsigned int major, const char *name)
unregister_chrdev(CHRDEVBASE_MAJOR,"hello_driver");
一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。
4.生成节点 mknod /dev/hello c 200 0
C:字符驱动设备
200:主设备号
0:次设备号
/dev/hello:设备文件名称
当我们使用该命令创建好了/dev/hello设备文件,当我们在用户态对该文件进行open()、write()、read()时,就会调用到/dev/hello设备文件对应的设备驱动的file_operations对应的.open、.write、.read回调函数。
自动创建节点
自动创建设备节点,在驱动中实现自动创建设备节点的功能以后,使用insmod加载驱动模块成功的话就会自动在/dev 目录下创建对应的设备文件。
1.分配设备号
静态分配设备号:
register_chrdev(CHRDEVBASE_MAJOR,"hello_driver",&hello_world_fops);
动态分配设备号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
dev:保存申请到的设备号。
baseminor: 次设备号起始地址,次设备号以 baseminor 为起始地址,地址开始递增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
count: 要申请的设备号数量。
name:设备名字。
2.创建设备类
Class类是一个结构体class_create(owner,name)类创建函数
struct class *class_create (struct module *owner, const char *name)
参数 owner 一般为 THIS_MODULE
参数 name 是类名字。
函数返回值是个指向结构体 class 的指针,也就是创建的类。
3.创建设备节点
使用device_create()函数在类的下面创建设备节点
struct device *device_create(struct class *class,struct device *parent,dev_t devt,void *drvdata,const char *fmt, ...)
parent 是父设备,一般为 NULL,也就是没有父设备
devt 是设备号;
drvdata 是设备可能会使用的一些数据,一般为 NULL;
fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx这个设备文件。
返回值就是创建好的设备。
4.注册字符设备
在linux中使用cdev结构体表示一个字符设备,其中包含文件操作函数与设备号
初始化:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
5.添加字符设备:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数 p 指向要添加的字符设备(cdev 结构体变量)
参数 dev 就是设备所使用的设备号
参数 count 是要添加的设备数量。
6.注销字符设备
void cdev_del(struct cdev *p)
参数 p 就是要删除的字符设备。
7.删除设备节点
卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy,
void device_destroy(struct class *class, dev_t devt)
参数 classs 是要删除的设备所处的类,参数 devt 是要删除的设备号。
8.删除设备类
卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy,函数原型如下:
void class_destroy(struct class *cls);
参数 cls 就是要删除的类。
9.注销设备号
unregister_chrdev(CHRDEVBASE_MAJOR,"hello_driver");
void unregister_chrdev_region(dev_t from, unsigned count)
此函数有两个参数:
from:要释放的设备号。
count: 表示从 from 开始,要释放的设备号数量。
部分代码:
static int __init hello_driver_init(void)
{
int ret;
printk("hello_driver_init\r\n");
alloc_chrdev_region(&hello_devid, 0, 1, "hello");//动态分配设备号
hello_major = MAJOR(hello_devid);
hello_minor = MINOR(hello_devid);
printk("hello driver major=%d,minor=%d\r\n",hello_major, hello_minor);
hello_cdev.owner = THIS_MODULE;
cdev_init(&hello_cdev, &hello_world_fops);//字符设备的初始化
cdev_add(&hello_cdev, hello_devid, 1);//添加字符设备
hello_class = class_create(THIS_MODULE,"hello_class");//创建类
device_create(hello_class,NULL,hello_devid,NULL,"hello"); /* /dev/hello */ //创建设备节点
return 0;
}
static void __exit hello_driver_cleanup(void)
{
printk("hello_driver_cleanup\r\n");
cdev_del(&hello_cdev);//删除字符设备
unregister_chrdev_region(hello_devid, 1);//删除设备号
device_destroy(hello_class,hello_devid);//注销设备节点
class_destroy(hello_class);//注销设备类
}
Lseek()函数
在Linux系统中,`lseek`系统调用用于移动文件读写指针到文件中的指定位置。如果你想要编写一个设备驱动模块的`lseek`函数,你需要实现`llseek`操作,这是在内核中用于处理`lseek`系统调用的底层函数。
以下是一个简单的例子,展示了如何在设备驱动模块中实现`llseek`操作:
#include <linux/fs.h>
#include <linux/uaccess.h>
#define MYDEV_SIZE (1024 * 1024) // 设备文件大小为1MB
loff_t mydev_llseek(struct file *file, loff_t offset, int whence)
{
loff_t newpos;
switch (whence) {
case SEEK_SET: // 从文件开始处偏移
newpos = offset;
break;
case SEEK_CUR: // 从当前位置偏移
newpos = file->f_pos + offset;
break;
case SEEK_END: // 从文件末尾处偏移
newpos = MYDEV_SIZE + offset;
break;
default: // 未知的whence值
return -EINVAL;
}
if (newpos < 0 || newpos >= MYDEV_SIZE) {
// 如果新的位置超出了设备的范围,返回错误
return -EINVAL;
}
// 更新文件的位置
file->f_pos = newpos;
return newpos;
}。
在上述代码中,`mydev_llseek`函数实现了`llseek`操作。这个函数根据`whence`参数来确定新的文件位置,可以是文件的开头(`SEEK_SET`),当前位置(`SEEK_CUR`),或文件的末尾(`SEEK_END`)。`offset`参数指定了偏移量。
在设备驱动中,你需要确保`file->f_pos`字段正确地反映了当前的文件位置,并且处理任何非法的偏移值。
要使`mydev_llseek`函数生效,你需要在你的文件操作结构体中指定它:
static const struct file_operations mydev_fops = {
.owner = THIS_MODULE,
.llseek = mydev_llseek,
// ... 其他文件操作函数 ...
};
确保在加载模块时注册了`file_operations`结构,并且在卸载模块时注销它。这样,当用户空间程序调用`lseek`系统调用操作你的设备文件时,你的`mydev_llseek`函数将被调用。
欢迎指正批评。