目录
字符设备驱动
字符设备通常缩写为cdev,它是不可寻址的,仅提供数据的流式访问,通过字符(一个接一个的字符)以字节流方式向用户程序传递数据。字符设备驱动通过/dev目录下的特殊文件公开设备的属性和功能,通过这个文件可以在设备和用户程序之间进行数据的交换。
1.1字符设备
字符设备驱动程序时内核中最基本的设备驱动程序,字符设备在内核中表示为struct cdev的实例,它定义在include/linux/cdev.h中:
struct cdev {
struct kobject kobj;//内核kobject对象,
struct module *owner; //所属模块 一般填写THIS_MODULE指向本模块
const struct file_operations *ops;//文件操作结构体
struct list_head list;//该使驱动的在字符设备构成一个链表(双向链表)
dev_t dev; //设备号 字符设备的起始设备号,一个设备可以有多个设备号
unsigned int count;//使用该字符设备驱动的设备数量
};
内核中提供了一组函数关于操作cdev结构体:
/*用于初始化cdev,建立cdev和file_operations之间的连接*/
void cdev_init(struct cdev* ,struct file_operations* );
/*动态申请一个cdev内存*/
struct cdev * cdev_alloc(void);
void cdev_put(struct cdev* p);
/*向系统添加一个cdev,完成字符设备的注册*/
int cdev _add(struct cdev*, dev_t , unsigned);、
/*从系统中删除cdev,进行字符设备的注销*/
void cdev_del(struct cdev*);
1.2设备号
1.2.1主设备号和次设备号
ddev_t成员定义了设备号,它是一个u32类型(32位无符号长整形),高12位为主设备号,低20位为次设备号,我们用主设备号和次设备号来表示设备,并将其与驱动程序进行绑定。主设备号用来标识某一类型设备,次设备号用来标识同一类型下的具体哪个设备(用作同一类型设备列表中的数组索引),因为同一个驱动程序的实例可以处理多个设备。以下是操作设备号相关的宏:
MAJOR(dev_t dev);//通过设备号获取主设备号
MINOR(dev_t dev);//通过设备号获取次设备号
MKDEV(int major,int minor);//通过主设备号和次设备号获取设备号
1.2.2设备号的分配和释放
设备号的分配主要指的是主设备号的分配。设备号的分配有两种方法,一种是静态分配,另一中是动态分配。
静态分配:需要给设备手动指定一个设备号。在静态分配设备号之前我们需要提前检查当前系统中所有被使用了的设备号,因为已经分配掉的主设备号我们就不能使用了。我们可以使用 “cat /proc/devices”命令查看当前系统中已经使用了的设备号,不推荐使用这方法,防止引起设备号的冲突。静态分配设备号的原型:
int register_chrdev_region(dev_t first,unsigned int count, char* name);
/*
@first : 首个设备号
@count :要申请设备号的个数
@name : 相关设备或者驱动程序的名字
返回值 : 成功返回0,失败返回负的错误码
*/
动态分配:系统自动分配一个没有被使用的设备号。推荐使用动态分配,更加清晰、安全。动态分配设备号的原型:
int alloc_chrdev_region(dev_t *dev,unsigned int firstminor,\
unsigned int count, char* name);
/*
@dev : 内核分配的第一个设备号
@firstminor :首位次设备号
@count : 次设备的数量
@name : 相关设备或者驱程序的名字
返回值 : 成功返回0,失败返回负的错误码
*/
设备号的释放:注销字符设备之后要释放掉设备号,设备号释放函数如下:
int unregister_chrdev_region(dev_t from,unsigned int count);
/*
@form : 要释放的设备号
@count : 从from开始,要释放的设备号的数量
返回值 : 成功返回0,失败返回负的错误码
*/
1.3设备文件操作
在Linux中一切皆文件,驱动程序加载成功之后,会在/dev目录下生成相应的设备文件,可以在这个文件上执行的操作取决于管理这个文件的设备驱动程序,应用程序对该文件的操作即可实现对设备的操作,我所提到的操作在内核定义为struct file_operation的实例,定义在内核include/linux/fs.h中。
struct file_operations {
/*拥有该结构体的模块指针,一般设置为THIS_MODULE*/
struct module *owner;
/*用于修改文件当前的读写位置*/
loff_t (*llseek) (struct file *, loff_t, int);
/*读取设备文件*/
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
/*向设备文件写入数据*/
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
/*轮询函数,用于查询设备是否可以进行非阻塞的读写*/
unsigned int (*poll) (struct file *, struct poll_table_struct *);
/*用于对设备的控制功能,与应用程序中的iotl函数对应*/
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
/*与unlocked_ioctl函数功能一样,区别在于64位系统上应用程序使用ioctl调用此函数,
32为系统上引用程序调用的是unlocked_ioctl*/
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
/*用于将设备的内存映射到用户空间中,一般帧缓冲设备会使用此函数,数据不用在用户空间和内核空间 来回复制*/
int (*mmap) (struct file *, struct vm_area_struct *);
/*用于打开设备文件*/
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
/*用于释放(关闭)设别文件,与应用程序中close函数对应*/
int (*release) (struct inode *, struct file *);
/*用于刷新处理的数据,将缓冲区中的数据刷新到磁盘中*/
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
/*与fsync函数功能类似,aio_fsync是异步刷新待处理的数据*/
int (*aio_fsync) (struct kiocb *, int datasync);
...
};
1.4字符设备驱动内部实现过程
问题:应用程序中对设备文件的操作是怎么实现对设备的操作的呢?
因为用户空间不能直接对内核空间进行操作,因此我们必须使用系统调用的方法来实现从用户空间陷入到内核空间,这样才能实现对底层驱动的操作。 具体操作如下:
1.我们在应用程序(进程)中open打开一次文件就会产生一个fd文件描述符,在进程描述符结构体中(task_struct)中我们可以找到文件相关的记录信息:
struct task_struct {
volatile long state;/*进程的状态 -1 unrunnable, 0 runnable, >0 stopped */
int on_cpu;*cpu处理器的编号*/
int prio;/*进程的优先级 0-139 (0-99,100-139)*/
pid_t pid;/*全局的进程号*/
pid_t tgid;/*全局的线程组标识符*/
struct task_struct *real_parent; /*真实的父进程*/
struct task_struct *parent;/*父进程*/
struct list_head children;/*子进程*/
struct list_head sibling;/*兄弟进程*/
struct task_struct *group_leader;/*组长进程*/
struct pid_link pids[PIDTYPE_MAX];/*进程号,进程组标识符和会话标识*/
struct fs_struct *fs; /*文件系统信息*/
struct files_struct *files;/*打开文件的详细信息*/
...
}
2.其中files_struct成员是一个结构体指针,其指向的结构体中有一个file结构体成员,记录着所打开的文件列表。open打开文件的时候记录文件信息的结构体就是struct file,将file放到fd_array数组中,将这个下标作为返回值返回,fd文件描述符就是fd_array的数据下标。
struct files_struct {
atomic_t count;/*原子计数器,记录对该结构体的引用计数*/
bool resize_in_progress;
wait_queue_head_t resize_wait;
truct fdtable __rcu *fdt;/*指向当前文件描述符表的指针*/
struct fdtable fdtab;/*文件描述符表的副本,用于读取操作。*/
spinlock_t file_lock ____cacheline_aligned_in_smp;
int next_fd;/*下一个可用的文件描述符值*/
unsigned long close_on_exec_init[1];/*初始化的位图数组,表示进程初始状态下需要在执行时自动关闭的文件描述符。*/
unsigned long open_fds_init[1];/*初始化的位图数组,表示进程初始状态下已打开的文件描述符。*/
unsigned long full_fds_bits_init[1];/*初始化的位图数组,表示进程初始状态下已使用的文件描述符。*/
struct file __rcu * fd_array[NR_OPEN_DEFAULT];/*当前打开文件的数组。是一个指向 struct file 结构体的指针数组,表示打开的文件列表。*/
};
3.struct file位于VFS层,用于表示进程打开的文件,我们每打开一个文件VFS层都会分配一个struct file结构体描述打开的这个设备文件,struct file是已经打开的文件在内存中的表示,存储与文件操作和状态相关的信息。其中我们只需要关注两位成员,一个是f_inode,另一个是 struct file_operations。通过struct file我们可以找到f_inode结构体。
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
/*f_path 字段存储文件的路径信息,包括挂载点和文件名。
它用于定位文件在文件系统中的位置,struct path封装了下面两部分的信息:
文件名和inode之间的关联。
文件所在文件系统有关的信息。*/
struct path f_path;
/*f_inode 字段是指向与文件关联的 struct inode 的指针
struct inode 包含文件的元数据,如权限、时间戳和文件大小。
通过 f_inode 可以访问与文件相关的其他属性和操作*/
struct inode *f_inode;
/*f_op 字段是指向文件操作函数表(struct file_operations)的指针。
文件操作函数表包含读取、写入、打开、释放等文件操作的函数指针。
可以通过 f_op 调用这些函数来执行各种文件操作,用来操作文件的实际数据内容。*/
const struct file_operations *f_op;
spinlock_t f_lock;
/*f_count 字段是原子长整型变量,表示文件的引用计数。
它跟踪文件的打开引用次数。当计数值为零时,可以安全地关闭和释放文件。*/
atomic_long_t f_count;
/*f_flags 打开文件的方式。*/
unsigned int f_flags;
/*f_mode 访问权限*/
fmode_t f_mode;
struct mutex f_pos_lock;
/*f_pos 光标的位置*/
loff_t f_pos;
struct fown_struct f_owner;
/*f_cred 字段是指向与文件相关联的凭证(struct cred)的指针。
凭证包含与文件访问权限相关的信息,如用户和组的标识。*/
const struct cred *f_cred;
#ifdef CONFIG_SECURITY
/*f_security 字段是指向与文件相关的安全性相关数据的指针。
仅在内核配置中启用了安全模块时存在。*/
void *f_security;
#endif
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
4.通过inode节点可以获取到文件的设备号,然后遍历cdev链表获取设备号,若cdev成员中的设备号与inode文件的设备号相同,则匹配成功,然后将匹配成功的cdev结构体中的file_operations文件操作结构体赋值给VFS层struct file结构体的file_operations结构体指针,VFS层给应用层返回文件描述符fd的=,应用程序就可以通过fd找到struct file中的file_operations结构体获取操作设备的函数接口,从而操作对应的设备。
struct inode {
...
umode_t i_mode; /*inode权限 */
uid_t i_uid; /* inode拥有者的id */
gid_t i_gid; /* inode 所属组的id */
dev_t i_rdev; /*若是设备文件,记录设备的设备号*/
loff_t i_size; /*inode所代表的文件*/
struct timespec i_atime; /* inode最后一次的存起时间*/
struct timespec i_mtime; /* inode最后一次的修改时间*/
struct timespec i_ctime; /* inode的产生时间*/
unsigned long i_blksize; /* inode在做IO时的区块大小 */
unsigned long i_blocks; /* inode所使用的block数*/
struct list_head i_devices;//指向其他的inode节点idevices成员,构成双向链表
struct block_device *i_bdev; /*若是块设置,则执行块设备结构体*/
struct cdev *i_cdev; /*若是字符设备,则指向对应的字符设备结构体*/
...
};
4.struct file_operations中的每一个回调调用函数都会和系统调用链接到一起。当应用程序在指定的设备文件上,进行相应的系统调用时(open read write...),内核会查找负责这个文件的驱动程序(尤其是创建该文件的驱动程序),定位它的struct file_operations结构体,检测和该系统调用的方法是否已经定义,如果定义了就运行它;如果没有定义,则根据系统调用函数不同返回不同的错误码。
1.5字符设备驱动程序
我们上面说到,字符设备在内核中表示为struct cdev的实例。在编写字符设备驱动时,目标就是创建并注册与struct file_operations相关联的cdev的结构实例,为用户空间提供一组可以操作该设备的函数接口。为了实现这个目标,我们因为执行以下步骤:
1.创建一个字符设备对象
2.对字符设备进行初始化
3.申请设备号
4.注册字符设备
5.创建设备节点
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#define CNAME "mycdev"
struct cdev* cdev;
int major = 129; // major==0动态申请,major > 0静态指定
int minor = 0;
const unsigned int count = 3;
struct class* cls;
struct device* dev;
char kbuf[128] = { 0 };
int mycdev_open(struct inode* inode, struct file* file)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
ssize_t mycdev_read(struct file* file,
char __user* ubuf, size_t size, loff_t* offs)
{
int ret;
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
if (size > sizeof(kbuf))
size = sizeof(kbuf);
ret = copy_to_user(ubuf, kbuf, size);
if (ret) {
printk("copy data to user error\n");
return -EIO;
}
return size;
}
ssize_t mycdev_write(struct file* file,
const char __user* ubuf, size_t size, loff_t* offs)
{
int ret;
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
if (size > sizeof(kbuf))
size = sizeof(kbuf);
ret = copy_from_user(kbuf, ubuf, size);
if (ret) {
printk("copy data from user error\n");
return -EIO;
}
return size;
}
int mycdev_close(struct inode* inode, struct file* file)
{
printk("%s:%s:%d\n", __FILE__, __func__, __LINE__);
return 0;
}
const struct file_operations fops = {
.open = mycdev_open,
.read = mycdev_read,
.write = mycdev_write,
.release = mycdev_close,
};
static int __init mycdev_init(void)
{
int ret, i;
dev_t devno;
// 1.分配对象
cdev = cdev_alloc();
if (cdev == NULL) {
printk("cdev alloc error\n");
ret = -ENOMEM;
goto ERR1;
}
// 2.对象初始化
cdev_init(cdev, &fops);
// 3.设备号的申请
if (major > 0) {
// 静态申请设备号
ret = register_chrdev_region(MKDEV(major, minor), count, CNAME);
if (ret) {
printk("static: request device number error\n");
goto ERR2;
}
} else {
// 动态申请设备号
ret = alloc_chrdev_region(&devno, minor, count, CNAME);
if (ret) {
printk("dynamic:request device number error\n");
goto ERR2;
}
major = MAJOR(devno);
minor = MINOR(devno);
}
printk("major = %d,minor = %d,count = %d\n", major, minor, count);
// 4.注册
ret = cdev_add(cdev, MKDEV(major, minor), count);
if (ret) {
printk("cdev register error\n");
goto ERR3;
}
// 5.自动创建设备节点
cls = class_create(THIS_MODULE, CNAME);
if (IS_ERR(cls)) {
printk("class create error\n");
goto ERR4;
}
for (i = minor; i < minor + count; i++) {
dev = device_create(cls, NULL, MKDEV(major, i), NULL, "%s%d", CNAME, i);
if (IS_ERR(dev)) {
printk("device create error\n");
goto ERR5;
}
}
return 0; //!!!!!!!!!!!!!!!!!不要忘记写!!!!!!!!!!!!!!!!!!
ERR5:
for (--i; i >= minor; i--) {
device_destroy(cls, MKDEV(major, i));
}
class_destroy(cls);
ERR4:
cdev_del(cdev);
ERR3:
unregister_chrdev_region(MKDEV(major, minor), count);
ERR2:
kfree(cdev);
ERR1:
return ret;
}
static void __exit mycdev_exit(void)
{
int i;
for (i = minor; i < minor + count; i++) {
device_destroy(cls, MKDEV(major, i));
}
class_destroy(cls);
cdev_del(cdev);
unregister_chrdev_region(MKDEV(major, minor), count);
kfree(cdev);
}
module_init(mycdev_init);
module_exit(mycdev_exit);
MODULE_LICENSE("GPL");