LDD字符驱动学习
Posted Welljia
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LDD字符驱动学习相关的知识,希望对你有一定的参考价值。
字符驱动程序
scull ,即“simple character utility for loading localities, 区域装载的简单字符工具’”。
是一个操作内存区域的字符设备驱动程序,这片内存区域就相当于一个字符设备。
字符设备的执行流程
获取设备号 -> 注册设备 -> 关联File operations结构 -> open(打开设备) -> write ->read -> release资源 -> close(关闭设备)
主设备号与次设备号
主设备号标识设备对应的驱动程序,次设备号由内核使用,用于正确确定设备文件所指的设备。
设备编号
在内核中,dev_t类型用来保存设备编号,在
#include <linux/kedv_t.h>
MAJOR(dev_t dev);
MINOR(dev_t dev);
如果需要将主设备号和次设备号转换成dev_t类型,则使用:
MKDEV(int major, int minor);
分配和释放设备编号
在建立字符设备之前,驱动程序首先要做的是获得一个或多个设备的编号。完成该工作的必要函数是register_chrdev_region,该函数在
int register_chrdev_region(dev_t first, unsigned int count, char *name);
first是要分配的设备编号范围起始值 ,其次设备号经常被置为0。count是所有请求的连续设备编号的个数,name是和该编号范围关联的设备名称,它将出现在/proc/devices和sysfs中。该函数分配成功时返回0,错误返回负的错误码。
以上的固定分配,有时不明确所需要的设备编号,故通常采用动态分配,函数如下:
int alloc_chrdev_region(dev_t, unsigned int firstminor, unsigned int count, char *name);
dev 是一个只输出的参数, 它在函数成功完成时持有你的分配范围的第一个数。 fisetminor 应当是请求的第一个要用的次编号;它常常是 0。count 和 name 参数如同给 request_chrdev_region 的一样。
为了防止选定的设备号出现冲突和麻烦,驱动程序应尽量使用动态分配而不是静态分配。
不管哪种方法,不使用时需要释放设备号,函数如下:
void unregister_chrdev_region(dev_t first, unsigned int count);
通常我们会在模块的清除函数中调用unregister_chrdev_region函数。
由于动态分配的主设备号无法预先创建设备节点,所以需要从/proc/devices中读取得到。可以将对insmod的调用替换成一个脚本,该脚本在调用insmod之后读取/proc/devices以获得分配的主设备号,然后创建对应的设备文件。其脚本为:
#!/bin/sh
module="scull"
device="scull"
mode="664"
# 使用传入到该脚本的所有参数调用insmod,同时使用路径名来指定具体模块位置
# 这是因为新的modutiles默认不会在当前目录中查找模块
/sbin/insmod ./$module.ko $* || exit 1
# 删除原有节点
rm -f /dev/$device[0-3]
major=$(awk "\\\\$2==\\"$module\\" print \\\\$1" /proc/devices)
mknod /dev/$device0 c $major 0
mknod /dev/$device1 c $major 1
mknod /dev/$device2 c $major 2
mknod /dev/$device3 c $major 3
# 给予适当的组属性许可,并修改组
# 并非所有的发行版都具有staff组,有些有wheel组
group="staff"
grep -q '^staff:' /etc/group || group="wheel"
chgrp $group /dev/$device[0-3]
chmod $mode /dev/$device[0-3]
分配主设备号的最佳方式是:默认采用动态分配, 同时保留在加载甚至是编译时批定设备号的余地。常用的获取主设备号的代码为:
if (scull_major)
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev, scull_nr_devs, "scull");
else
result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");
scull_major = MAJOR(dev);
if (result < 0)
printk(KERN_WARNING "scull: can't get major %d\\n", scull_major);
return result;
数据结构
file_operation结构
struct file_operations scull_fops =
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.ioctl = scull_ioctl,
.open = scull_open,
.release = scull_release,
;
这种结构体初始化定义是C语言最新C99标准,称为指定初始化(designated initializer)。采用这种方式的优势就在于由此初始化不必严格按照定义时的顺序。这带来了极大的灵活性。
file结构
此file结构不是用户空间中的FILE,两者无关系,FILE是在C库中定义的,不会出现在内核代码中,而struct file是一个内核结构,它不会出现在用户程序中。file结构代表一个打开的文件,同open创建,直到最后的close之后才释放这个结构,指向struct file的指针为file和filp,其中file指的是结构本身,filp指向该结构的指针。
inode结构
内核用inode结构在内部表示文件,和file结构不同,file表示打开的文件描述符,对单个文件,可能会有许多个表示打开的文件描述符的file结构,但它们都指向单个inode结构。
字符设备注册
struct cdev结构表示字符设备,获取一个独立的cdev结构并嵌入到自己的设备特定结构中,其代码如下:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
之后初始化已分配到的结构:
void cdev_init(struct cdev *cdev, struct file_operations *fops);
cdev的字段需有初始化,它有一个所有者字段,应该设置成THIS_MODULE。
cdev设置好后,通过下面代码告诉内核该结构的消息:
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
dev 是 cdev 结构,num 是这个设备对应的第一个设备号,count 是应当关联到设备的设备号的数目.通常是1, 但在某些情况下,会有多个设备号对应于一个特定的设备。
如果要移除一个字符设备,做如下调用:
void cdev_del(struct cdev *dev);
注册实例,scull:
struct scull_dev //quantum,量子,一个内存区称为一个量子,而这个指针数组称为量子集
struct scull_qset *data; /* Pointer to first quantum set */
int quantum; /* the current quantum size */
int qset; /* the current array size */
unsigned long size; /* amount of data stored here */
unsigned int access_key; /* used by sculluid and scullpriv */
struct semaphore sem; /* mutual exclusion semaphore */
struct cdev cdev; /* Char device structure */
;
内核和设备间的接口struct cdev必须如上所述的被初始化并添加到系统中,其代码如下:
static void scull_setup_cdev(struct scull_dev *dev, int index)
int err, devno = MKDEV(scull_major, scull_minor + index);
cdev_init(&dev->cdev, &scull_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &scull_fops;
err = cdev_add (&dev->cdev, devno, 1);
/* Fail gracefully if need be */
if (err)
printk(KERN_NOTICE "Error %d adding scull%d", err, index);
open和release
open原型:
int (*open)(struct inode *inode, struct file *filp);
其作用为:
- 检查设备特定的错误(例如设备没准备好, 或者类似的硬件错误
- 如果它第一次打开, 初始化设备
- 如果需要, 更新 f_op 指针.
- 分配并填充要放进 filp->private_data 的任何数据结构
scull_open代码:
int scull_open(struct inode *inode, struct file *filp)
struct scull_dev *dev; /* device information */
dev = container_of(inode->i_cdev, struct scull_dev, cdev);
filp->private_data = dev; /* for other methods */
/* now trim to 0 the length of the device if open was write-only */
if ( (filp->f_flags & O_ACCMODE) == O_WRONLY)
scull_trim(dev); /* ignore errors */
return 0; /* success */
release方法作用与open相反,该方法实现被称为device_close或device_release,作用如下:
- 释放 open 分配在 filp->private_data 中的任何东西
- 在最后的 close 关闭设备
scull中的release代码:
int scull_release(struct inode *inode, struct file *filp)
return 0;
并不是每个close系统调用都会引起release方法的调用,只有真正释放设备数据的close调用才会调用这个方法,所以scull中的release代码这么少。
read和write
read和write原型相似,如下 :
ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp);
ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);
filp 是文件指针,count 是请求的传输数据大小, buff 参数指向用户空间的缓存区, 最后, offp 是一个指针指向一个”long offset type,长偏移类型”对象, 它指出用户正在存取的文件位置. 返回值是一个”signed size type,有符号的尺寸类型”;
大多数read和write方法实现的核心部分如下,用于拷贝任意的一段字节序列。
unsigned long copy_to_user(void __user *to,const void *from,unsigned long count);
unsigned long copy_from_user(void *to,const void __user *from,unsigned long count);
这两个函数的作用并不限于在内核空间和用户空间之间拷贝数据,它们还检查用户空间的指针是否有效。
快速参考
#include <linux/types.h>
dev_t
dev_t 是用来在内核里代表设备号的类型.
int MAJOR(dev_t dev);
int MINOR(dev_t dev);
从设备编号中抽取主次编号的宏.
dev_t MKDEV(unsigned int major, unsigned int minor);
从主次编号来建立 dev_t 数据项的宏定义.
#include <linux/fs.h>
"文件系统"头文件是编写设备驱动需要的头文件. 许多重要的函数和数据结构在此定义.
int register_chrdev_region(dev_t first, unsigned int count, char *name)
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name)
void unregister_chrdev_region(dev_t first, unsigned int count);
允许驱动分配和释放设备编号的范围的函数. register_chrdev_region 应当用在事先知道需要的主编号时; 对于动态分配, 使用alloc_chrdev_region 代替.
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
老的( 2.6 之前) 字符设备注册函数. 它在 2.6 内核中被模拟, 但是不应当给新代码使用. 如果主编号不是 0, 可以不变地用它; 否则一个动态编号被分配给这个设备.
int unregister_chrdev(unsigned int major, const char *name);
恢复一个由 register_chrdev 所作的注册的函数. major 和 name 字符串必须包含之前用来注册设备时同样的值.
struct file_operations;
struct file;
struct inode;
大部分设备驱动使用的 3 个重要数据结构. file_operations 结构持有一个字符驱动的方法; struct file 代表一个打开的文件, struct inode 代表磁盘上的一个文件.
#include <linux/cdev.h>
struct cdev *cdev_alloc(void);
void cdev_init(struct cdev *dev, struct file_operations *fops);
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
void cdev_del(struct cdev *dev);
cdev 结构管理的函数, 它代表内核中的字符设备.
#include <linux/kernel.h>
container_of(pointer, type, field);
一个传统宏定义, 可用来获取一个结构指针, 从它里面包含的某个其他结构的指针.
#include <asm/uaccess.h>
这个包含文件声明内核代码使用的函数来移动数据到和从用户空间.
unsigned long copy_from_user (void *to, const void *from, unsigned long count);
unsigned long copy_to_user (void *to, const void *from, unsigned long count);
在用户空间和内核空间拷贝数据
以上是关于LDD字符驱动学习的主要内容,如果未能解决你的问题,请参考以下文章
字符设备驱动详解(主次设备号注册/卸载字符设备驱动创建设备节点地址映射)