初始化/移除字符设备
Linux内核提供了两种方式来定义字符设备,如下所示:
//第一种方式:常见的变量定义
static struct cdev chrdev;
//第二种方式:内核提供的动态分配方式
struct cdev *cdev_alloc(void);
从内核中移除某个字符设备,则需要调用cdev_del函数,如下所示:
void cdev_del(struct cdev *p)
分配/注销设备号
Linux的各种设备都以文件的形式存放在/dev目录下,为了管理这些设备,系统为各个设备进行编号,每个设备号又分为主设备号和次设备号。主设备号用来区分不同种类的设备,如USB,tty等,次设备号用来区分同一类型的多个设备。我们可以通过命令”cat /proc/devices”查询内核分配的主设备号。
内核提供了一种数据类型:dev_t,用于记录设备编号,该数据类型实际上是一个无符号32位整型,其中的12位用于表示主设备号,剩余的20位则用于表示次设备号。
Linux内核为我们提供了生成设备号的宏定义MKDEV,用于将主设备号和次设备号合成一个设备号。除此之外,内核还提供了另外两个宏定义MAJOR和MINOR,可以根据设备的设备号来获取设备的主设备号和次设备号。
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS)) //通过设备号获取主设备号
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))//过设备号获取次设备号
#define MKDEV(ma,mi) (((ma) << MINORBITS) \| (mi))//通过主设备号和次设备号合成设备号
创建一个新的字符设备之前,我们需要为新的字符设备注册一个新的设备号。内核提供了三种方式,来完成这项工作。
(1) register_chrdev函数
register_chrdev函数用于分配设备号。该函数是一个内联函数,它不仅支持静态申请设备号,也支持动态申请设备号,并将主设备号返回,函数原型如下所示。
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
- major:用于指定要申请的字符设备的主设备号,等价于register_chrdev_region函数,当设置为0时,内核会自动分配一个未使用的主设备号。
- name:用于指定字符设备的名称
- fops:用于操作该设备的函数接口指针。
使用register_chrdev函数向内核申请设备号,同一类字符设备(即主设备号相同),会在内核中申请了256个,通常情况下,我们不需要用到这么多个设备,这就造成了极大的资源浪费。
(2)register_chrdev_region函数
register_chrdev_region函数用于静态地为字符设备申请一个或多个设备编号。该函数在分配成功时,会返回0;失败则会返回相应的错误码,函数原型如下所示。
int register_chrdev_region(dev_t from, unsigned count, const char *name)
- from:dev_t类型的变量,用于指定字符设备的起始设备号,如果要注册的设备号已经被其他的设备注册了,那么就会导致注册失败。
- count:指定要申请的设备号个数,count的值不可以太大,否则会与下一个主设备号重叠。
- name:用于指定该设备的名称,我们可以在/proc/devices中看到该设备。
(3)alloc_chrdev_region函数(推荐使用)
使用register_chrdev_region函数时,需要编程者自行确定未使用的设备号, 并不方便。因此,内核又为我们提供了一种能够动态分配设备编号的方式:alloc_chrdev_region。
调用alloc_chrdev_region函数,内核会自动分配给我们一个尚未使用的主设备号。我
们可以通过命令”cat /proc/devices”查询内核分配的主设备号。
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
- dev:指向dev_t类型数据的指针变量,用于存放分配到的设备编号的起始值;
- baseminor:次设备号的起始值,通常情况下,设置为0;
- count、name:同register_chrdev_region类型,用于指定需要分配的设备编号的个数以及设备的名称。
注销设备号:
当我们删除字符设备时候,我们需要把分配的设备编号交还给内核.
(1)unregister_chrdev函数
使用register函数申请的设备号,则应该使用unregister_chrdev函数进行注销。
static inline void unregister_chrdev(unsigned int major, const char *name)
{
__unregister_chrdev(major, 0, 256, name);
}
- major:指定需要释放的字符设备的主设备号,一般使用register_chrdev函数的返回值作为实参。
- name:执行需要释放的字符设备的名称。
(2)unregister_chrdev_region函数
对于使用register_chrdev_region函数以及alloc_chrdev_region函数分配得到的设备编号,可以使用unregister_chrdev_region函数进行注销。
void unregister_chrdev_region(dev_t from, unsigned count)
- from:指定需要注销的字符设备的设备编号起始值,我们一般将定义的dev_t变量作为实参。
- count:指定需要注销的字符设备编号的个数,该值应与申请函数的count值相等,通常采用宏定义进行管理。
关联设备的操作方式
编写一个字符设备最重要的事情,就是要实现file_operations这个结构体中的函数。实现之后,如何将该结构体与我们的字符设备结构相关联呢?内核提供了cdev_init函数,来实现这个工作。
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
- cdev:struct cdev类型的指针变量,指向需要关联的字符设备结构体;
- fops:file_operations类型的结构体指针变量,一般将实现操作该设备的结构体file_operations结构体作为实参。
注册设备
cdev_add函数用于向内核的cdev_map散列表添加一个新的字符设备,如下所示
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
- p:struct cdev类型的指针,用于指定需要添加的字符设备;
- dev:dev_t类型变量,用于指定设备的起始编号;
- count:指定注册多少个设备。
字符设备驱动框架
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define DEV_NAME "EmbedCharDev"
#define DEV_CNT (1)
#define BUFF_SIZE 128
//定义字符设备的设备号
static dev_t devno;
//定义字符设备结构体chr_dev
static struct cdev chr_dev;
//数据缓冲区
static char vbuf[BUFF_SIZE];
static int chr_dev_open(struct inode *inode, struct file *filp);
static int chr_dev_release(struct inode *inode, struct file *filp);
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos);
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos);
//定义初始化file_operation结构体
static struct file_operations chr_dev_fops =
{
.owner = THIS_MODULE,
.open = chr_dev_open,
.release = chr_dev_release,
.write = chr_dev_write,
.read = chr_dev_read,
}
static int chr_dev_open(struct inode *inode, struct file *filp)
{
printk("\nopen\n");
return 0;
}
static int chr_dev_release(struct inode *inode, struct file *filp)
{
printk("\nrelease\n");
return 0;
}
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
int ret;
int tmp = count ;
if(p > BUFF_SIZE)
return 0;
if(tmp > BUFF_SIZE - p)
tmp = BUFF_SIZE - p;
//读取用户空间的数据存入内核数据缓冲区
ret = copy_from_user(vbuf, buf, tmp);
*ppos += tmp;
return tmp;
}
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
int ret;
int tmp = count ;
static int i = 0;
i++;
if(p >= BUFF_SIZE)
return 0;
if(tmp > BUFF_SIZE - p)
tmp = BUFF_SIZE - p;
//将内核数据缓冲区的中的数据发送给用户空间
ret = copy_to_user(buf, vbuf+p, tmp);
*ppos +=tmp;
return tmp;
}
//入口函数
static int __init chrdev_init(void)
{
int ret = 0;
printk("chrdev init\n");
//第一步
//采用动态分配的方式,获取设备编号,次设备号为0,
//设备名称为EmbedCharDev,可通过命令cat /proc/devices查看
//DEV_CNT为1,当前只申请一个设备编号
ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);
if(ret < 0)
{
printk("fail to alloc devno\n");
goto alloc_err;
}
//第二步
//关联字符设备结构体cdev与文件操作结构体file_operations
cdev_init(&chr_dev, &chr_dev_fops);
//第三步
//添加设备至cdev_map散列表中
ret = cdev_add(&chr_dev, devno, DEV_CNT);
if(ret < 0)
{
printk("fail to add cdev\n");
goto add_err;
}
return 0;
add_err:
//添加设备失败时,需要注销设备号
unregister_chrdev_region(devno, DEV_CNT);
alloc_err:
return ret;
}
//出口函数
static void __exit chrdev_exit(void)
{
printk("chrdev exit\n");
unregister_chrdev_region(devno, DEV_CNT);
cdev_del(&chr_dev);
}
module_init(chrdev_init);
module_exit(chrdev_exit);
MODULE_LICENSE("GPL");
改进:一个驱动程序驱动多个不同次设备号的设备文件
...
//申请两个次设备号
#define DEV_CNT (2)
...
/*虚拟字符设备*/
struct chr_dev {
struct cdev dev;
char vbuf[BUFF_SIZE];
};
//字符设备1
static struct chr_dev vcdev1;
//字符设备2
static struct chr_dev vcdev2;
入口函数修改:
static int __init chrdev_init(void)
{
int ret;
dev_t cur_dev;
printk("chrdev init\n");
ret = alloc_chrdev_region(&devno, 0, DEV_CNT, DEV_NAME);
if (ret < 0)
goto alloc_err;
//关联第一个设备:vdev1
cdev_init(&vcdev1.dev, &chr_dev_fops);
cur_dev = MKDEV(MAJOR(devno), MINOR(devno) + 0);
ret = cdev_add(&vcdev1.dev, cur_dev, 1);
if (ret < 0) {
printk("fail to add vcdev1 ");
goto add_err1;
}
//关联第二个设备:vdev2
cdev_init(&vcdev2.dev, &chr_dev_fops);
cur_dev = MKDEV(MAJOR(devno), MINOR(devno) + 1);
ret = cdev_add(&vcdev2.dev, cur_dev, 1);
if (ret < 0) {
printk("fail to add vcdev2 ");
goto add_err2;
}
return 0;
add_err2:
cdev_del(&(vcdev1.dev));
add_err1:
unregister_chrdev_region(devno, DEV_CNT);
alloc_err:
return ret;
}
出口函数修改:
static void __exit chrdev_exit(void)
{
printk("chrdev exit\n");
unregister_chrdev_region(devno, DEV_CNT);
cdev_del(&(vcdev1.dev));
cdev_del(&(vcdev2.dev));
}
open函数修改:
文件节点inode中的成员i_cdev:为了方便访问设备文件,在打开文件过程中,内核将对应的字符设备结构体cdev自动保存到文件节点inode中的成员变量i_cdev中。
Linux提供了一个宏定义container_of,该宏可以根据结构体的某个成员的地址,来得到该结构体的地址。该宏需要三个参数,分别是代表结构体成员的真实地址,结构体的类型以及结构体成员的名字。
在chr_dev_open函数中,我们需要通过inode的i_cdev成员,来得到对应的虚拟设备结构体,并保存到文件指针filp的私有数据成员中。假如,我们打开虚拟设备1,那么inode->i_cdev便指向了vcdev1的成员dev,利用container_of宏,我们就可以得到vcdev1结构体的地址,也就可以操作对应的数据缓冲区了。
static int chr_dev_open(struct inode *inode, struct file *filp)
{
printk("open\n");
filp->private_data = container_of(inode->i_cdev, struct chr_dev, dev);
return 0;
}
write函数的修改:
通过filp->private_data获取到打开的字符设备所在的虚拟设备结构体变量的指针,同过该指针即可访问该虚拟设备的数据缓冲区vbuf。
static ssize_t chr_dev_write(struct file *filp, const char __user * buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
int ret;
//获取文件的私有数据
struct chr_dev *dev = filp->private_data;
char *vbuf = dev->vbuf;
int tmp = count ;
if (p > BUFF_SIZE)
return 0;
if (tmp > BUFF_SIZE - p)
tmp = BUFF_SIZE - p;
ret = copy_from_user(vbuf, buf, tmp);
*ppos += tmp;
return tmp;
}
read函数的修改:
与write函数的修改基本一致。
static ssize_t chr_dev_read(struct file *filp, char __user * buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
int ret;
int tmp = count ;
//获取文件的私有数据
struct chr_dev *dev = filp->private_data;
char *vbuf = dev->vbuf;
if (p >= BUFF_SIZE)
return 0;
if (tmp > BUFF_SIZE - p)
tmp = BUFF_SIZE - p;
ret = copy_to_user(buf, vbuf+p, tmp);
*ppos +=tmp;
return tmp;
}