字符设备驱动框架

初始化/移除字符设备
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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值