字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED设备等
每一个字符设备或块设备都在/dev目录下对应一个设备文件。linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备。
struct cdev {
struct kobject kobj; /*内嵌的kobject结构,用于内核设备驱动模型的管理*/
struct module *owner; /*指向包含该结构的模块的指针,用于引用计数*/
const struct file_operations *ops; /*指向字符设备操作函数集的指针*/
struct list_head list; /*该结构将使用该驱动的字符设备连接成一个链表*/
/*与cdev对应的字符设备文件的inode->i_devices的链表头*/
dev_t dev; /*该字符设备的起始设备号,一个设备可能有多个设备号*/
unsigned int count; /*使用该字符设备驱动的设备数量*/
};
cdev 结构体的dev_t 成员定义了设备号,为32位,其中12位是主设备号,20位是次设备号
我们只需使用二个简单的宏就可以从dev_t 中获取主设备号和次设备号:
MAJOR(dev_t dev)
MINOR(dev_t dev)
相反地,可以通过主次设备号来生成dev_t:
MKDEV(int major,int minor)
struct cdev btn_cdev;
/*申请设备号,如果申请失败采用动态申请方式*/
if(major)
{
//静态
dev_id = MKDEV(major, 0);
register_chrdev_region(dev_id, 1, "button");
}
else
{
//动态
alloc_chardev_region(&dev_id, 0, 1, "button");
major = MAJOR(dev_id);
}
静态申请:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
/*功能:申请使用从from开始的count个设备号(主设备号不变,次设备号增加)*/
from :要分配的设备编号范围的初始值(次设备号常设为0);
Count:连续编号范围.
name:编号相关联的设备名称. (/proc/devices);
静态申请相对较简单,但是一旦驱动被广泛使用,这个随机选定的主设备号可能会导致设备号冲突,而使驱动程序无法注册。
动态申请:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
/*功能:请求内核动态分配count个设备号,且次设备号从baseminor开始。*/
//返回值小于0表示分配失败,系统自动返回没有占用的设备号
baseminor:是请求的最小的次编号;
count:是请求的连续设备编号的总数;
name:为设备名
然后通过major=MMOR(dev)获取主设备号
动态申请简单,易于驱动推广,但是无法在安装驱动前创建设备文件(因为安装前还没有分配到主设备号)。
销毁设备号
void unregister_chrdev_region(dev_t first, unsigned int count);
first为第一个设备号,count为申请的设备数量
内核在内部使用类型 struct cdev 的结构来代表字符设备.有 2 种方法来分配和初始化一个这些结构.
①如果你想在运行时获得一个独立的 cdev 结构, 你可以为此使用这样的代码:
struct cdev *my_cdev = cdev_alloc(); my_cdev->ops = &my_fops;
②另一种是把cdv结构嵌入到你自己封装的设备结构中,这时需要使用下面的方法来分配和初始化:
void cdev_init(struct cdev *cdev, struct file_operations *fops);
cdev_ init()函数用于初始化 cdev 的成员,并建立 cdev 和 file_operations 之间的连接。
cdev_init()操作源代码:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
int cdev_add(struct cdev *dev, dev_t num, unsigned int count)
//向系统添加一个cdev,完成字符设备的注册。
dev 是 cdev 结构, num 是这个设备响应的第一个设备号, count 是应当关联到设备的设备号的数目. 常常 count 是 1。
file_operations结构
struct file_operations {
struct module *owner; //THIS_MODULE, <linux/module.h> 中定义的宏.
/*用于修改文件当前的读写位置*/
loff_t(*llseek) (struct file *, loff_t, int);
ssize_t(*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t(*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
ssize_t(*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t(*aio_write) (struct kiocb *, const char __user *, size_t, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t(*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t(*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
ssize_t(*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void __user *);
ssize_t(*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area) (struct file *, unsigned long,
unsigned long, unsigned long, unsigned long);
};
参考:file_operations中各项解析
file结构,文件结构代表一个打开的文件
struct file{
mode_t fmode; //文件模式,如FMODE_READ,FMODE_WRITE*/
......
loff_t f_pos; //当前读写位置.off_t是一个64位的数,驱动可以读这个值,如果它需要知道文件中的当前位置,但是正常地不应该改变它。
unsigned int f_flags; //文件标志,例如O_RDONLY,O_NONBLOCK,和O_SYNC.驱动应当检查 O_NONBLOCK标志来看是否是请求非阻塞操作
struct file_operations *f_op;
void *private_data; //非常重要,用于存放转换后的设备描述结构指针*/
.......
};
inode 结构
内核用inode 结构在内部表示文件,它是实实在在的表示物理硬件上的某一个文件,且一个文件仅有一个inode与之对应,同样它有二个比较重要的成员:
struct inode{
dev_t i_rdev; //设备编号
struct cdev *i_cdev;
};
//从inode中获取主次设备号
unsigned int imajor(struct inode *inode);
unsigned int iminor(struct inode *inode);
7、字符设备驱动模块加载与卸载函数
在字符设备驱动模块加载函数中应该实现设备号的申请和cdev 结构的注册
在卸载函数中应该实现设备号的释放与cdev结构的注销。
我们一般习惯将cdev内嵌到另外一个设备相关的结构体里面,该设备包含所涉及的cdev、私有数据及信号量等等信息。常见的设备结构体、模块加载函数、模块卸载函数形式如下:
/*设备结构体*/
struct xxx_dev{
struct cdev cdev;
char *data;
struct semaphore sem;
......
};
/*模块加载函数*/
static int __init xxx_init(void)
{
.......
初始化cdev结构;
申请设备号;
注册设备号;
申请分配设备结构体的内存;
/*非必须*/
}
/*模块卸载函数*/
static void __exit xxx_exit(void)
{
.......
释放原先申请的设备号;
释放原先申请的内存;
注销cdev设备;
}
参考:深入浅出:Linux设备驱动之字符设备驱动
#ifndef MEMDEV_MAJOR
#define MEMDEV_MAJOR 251 /*预设的mem的主设备号*/
#endif
#ifndef MEMDEV_NR_DEVS
#define MEMDEV_NR_DEVS 2 /*设备数*/
#endif
#ifndef MEMDEV_SIZE
#define MEMDEV_SIZE 4096
#endif
/*mem设备描述结构体*/
struct mem_dev
{
char *data;
unsigned long size;
};
static mem_major = MEMDEV_MAJOR;
module_param(mem_major, int, S_IRUGO);
struct mem_dev *gp_mem_dev; /*设备结构体指针*/
struct cdev g_cdev;
/*文件打开函数*/
int mem_open(struct inode *inode, struct file *filp)
{
struct mem_dev *dev;
/*获取次设备号*/
int num = MINOR(inode->i_rdev);
if (num >= MEMDEV_NR_DEVS)
return -ENODEV;
dev = &gp_mem_dev[num];
/*将设备描述结构指针赋值给文件私有数据指针*/
filp->private_data = dev;
return 0;
}
/*文件释放函数*/
int mem_release(struct inode *inode, struct file *filp)
{
return 0;
}
/*读函数*/
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos; /*记录文件指针偏移位置*/
unsigned int count = size; /*记录需要读取的字节数*/
int ret = 0; /*返回值*/
struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
/*判断读位置是否有效*/
if (p >= MEMDEV_SIZE) /*要读取的偏移大于设备的内存空间*/
return 0;
if (count > MEMDEV_SIZE - p) /*要读取的字节大于设备的内存空间*/
count = MEMDEV_SIZE - p;
/*读数据到用户空间:内核空间->用户空间交换数据*/
if (copy_to_user(buf, (void*)(dev->data + p), count))
{
ret = - EFAULT;
}
else
{
*ppos += count;
ret = count;
printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);
}
return ret;
}
/*写函数*/
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct mem_dev *dev = filp->private_data; /*获得设备结构体指针*/
/*分析和获取有效的写长度*/
if (p >= MEMDEV_SIZE)
return 0;
if (count > MEMDEV_SIZE - p) /*要写入的字节大于设备的内存空间*/
count = MEMDEV_SIZE - p;
/*从用户空间写入数据*/
if (copy_from_user(dev->data + p, buf, count))
ret = - EFAULT;
else
{
*ppos += count; /*增加偏移位置*/
ret = count; /*返回实际的写入字节数*/
printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
}
return ret;
}
/* seek文件定位函数 */
static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
{
loff_t newpos;
switch(whence) {
case 0: /* SEEK_SET */ /*相对文件开始位置偏移*/
newpos = offset; /*更新文件指针位置*/
break;
case 1: /* SEEK_CUR */
newpos = filp->f_pos + offset;
break;
case 2: /* SEEK_END */
newpos = MEMDEV_SIZE -1 + offset;
break;
default: /* can't happen */
return -EINVAL;
}
if ((newpos<0) || (newpos>MEMDEV_SIZE))
return -EINVAL;
filp->f_pos = newpos;
return newpos;
}
/*文件操作结构体*/
static const struct file_operations mem_fops =
{
.owner = THIS_MODULE,
.llseek = mem_llseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
};
/*设备驱动模块加载函数*/
static int memdev_init(void)
{
int result;
int i;
dev_t devno = MKDEV(mem_major, 0);
/* 申请设备号,当xxx_major不为0时,表示静态指定;当为0时,表示动态申请*/
/* 静态申请设备号*/
if(mem_major)
result = register_chrdev_region(devno, 2, "memdev");
else /* 动态分配设备号 */
{
result = alloc_chrdev_region(&devno, 0, 2, "memdev");
mem_major = MAJOR(devno); /*获得申请的主设备号*/
}
if (result < 0)
return result;
/*初始化cdev结构,并传递file_operations结构指针*/
cdev_init(&g_cdev, &mem_fops);
g_cdev.owner = THIS_MODULE; /*指定所属模块*/
g_cdev.ops = &mem_fops;
/*注册字符设备*/
cdev_add(&g_cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);
/*为设备描述结构分配内存*/
gp_mem_dev = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);
if (!gp_mem_dev) /*申请失败*/
{
result = - ENOMEM;
goto fail_malloc;
}
memset(gp_mem_dev, 0, sizeof(struct mem_dev));
/*为设备分配内存*/
for (i=0; i < MEMDEV_NR_DEVS; i++)
{
gp_mem_dev[i].size = MEMDEV_SIZE;
gp_mem_dev[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);
memset(gp_mem_dev[i].data, 0, MEMDEV_SIZE);
}
return 0;
fail_malloc:
unregister_chrdev_region(devno, 1);
return result;
}
/*模块卸载函数*/
static void memdev_exit(void)
{
cdev_del(&g_cdev); /*注销设备*/
kfree(gp_mem_dev); /*释放设备结构体内存*/
unregister_chrdev_region(MKDEV(mem_major, 0), 2); /*释放设备号*/
}
MODULE_AUTHOR("David Xie");
MODULE_LICENSE("GPL");
module_init(memdev_init);
module_exit(memdev_exit);
参考:Linux 内核开发 - 第一个字符驱动程序
Makefile文件如下:
ifneq ($(KERNELRELEASE),)
obj-m := cdevdemo.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions modules.order Module.symvers
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <linux/timer.h>
#include <asm/atomic.h>
#include <linux/slab.h>
#include <linux/device.h>
#define CDEVDEMO_MAJOR 255 /*预设cdevdemo的主设备号*/
static int cdevdemo_major = CDEVDEMO_MAJOR;
/*设备结构体,此结构体可以封装设备相关的一些信息等 信号量等也可以封装在此结构中,后续的设备模块一般都 应该封装一个这样的结构体,但此结构体中必须包含某些 成员,对于字符设备来说,我们必须包含struct cdev cdev*/
struct cdevdemo_dev
{
struct cdev cdev;
};
struct cdevdemo_dev *cdevdemo_devp; /*设备结构体指针*/
/*文件打开函数,上层对此设备调用open时会执行*/
int cdevdemo_open(struct inode *inode, struct file *filp)
{
printk(KERN_NOTICE "======== cdevdemo_open ");
return 0;
}
/*文件释放,上层对此设备调用close时会执行*/
int cdevdemo_release(struct inode *inode, struct file *filp)
{
printk(KERN_NOTICE "======== cdevdemo_release ");
return 0;
}
/*文件的读操作,上层对此设备调用read时会执行*/
static ssize_t cdevdemo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
printk(KERN_NOTICE "======== cdevdemo_read ");
}
/* 文件操作结构体,文中已经讲过这个结构*/
static const struct file_operations cdevdemo_fops =
{
.owner = THIS_MODULE,
.open = cdevdemo_open,
.release = cdevdemo_release,
.read = cdevdemo_read,
};
/*初始化并注册cdev*/
static void cdevdemo_setup_cdev(struct cdevdemo_dev *dev, int index)
{
printk(KERN_NOTICE "======== cdevdemo_setup_cdev 1");
int err, devno = MKDEV(cdevdemo_major, index);
printk(KERN_NOTICE "======== cdevdemo_setup_cdev 2");
/*初始化一个字符设备,设备所支持的操作在cdevdemo_fops中*/
cdev_init(&dev->cdev, &cdevdemo_fops);
printk(KERN_NOTICE "======== cdevdemo_setup_cdev 3");
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &cdevdemo_fops;
printk(KERN_NOTICE "======== cdevdemo_setup_cdev 4");
err = cdev_add(&dev->cdev, devno, 1);
printk(KERN_NOTICE "======== cdevdemo_setup_cdev 5");
if(err)
{
printk(KERN_NOTICE "Error %d add cdevdemo %d", err, index);
}
}
int cdevdemo_init(void)
{
printk(KERN_NOTICE "======== cdevdemo_init ");
int ret;
dev_t devno = MKDEV(cdevdemo_major, 0);
struct class *cdevdemo_class;
/*申请设备号,如果申请失败采用动态申请方式*/
if(cdevdemo_major)
{
printk(KERN_NOTICE "======== cdevdemo_init 1");
ret = register_chrdev_region(devno, 1, "cdevdemo");
}else
{
printk(KERN_NOTICE "======== cdevdemo_init 2");
ret = alloc_chrdev_region(&devno,0,1,"cdevdemo");
cdevdemo_major = MAJOR(devno);
}
if(ret < 0)
{
printk(KERN_NOTICE "======== cdevdemo_init 3");
return ret;
}
/*动态申请设备结构体内存*/
cdevdemo_devp = kmalloc(sizeof(struct cdevdemo_dev), GFP_KERNEL);
if(!cdevdemo_devp) /*申请失败*/
{
ret = -ENOMEM;
printk(KERN_NOTICE "Error add cdevdemo");
goto fail_malloc;
}
memset(cdevdemo_devp,0,sizeof(struct cdevdemo_dev));
printk(KERN_NOTICE "======== cdevdemo_init 3");
cdevdemo_setup_cdev(cdevdemo_devp, 0);
/*下面两行是创建了一个总线类型,会在/sys/class下生成cdevdemo目录 这里的还有一个主要作用是执行device_create后会在/dev/下自动生成 cdevdemo设备节点。而如果不调用此函数,如果想通过设备节点访问设备 需要手动mknod来创建设备节点后再访问。*/
cdevdemo_class = class_create(THIS_MODULE, "cdevdemo");
device_create(cdevdemo_class, NULL, MKDEV(cdevdemo_major, 0), NULL, "cdevdemo");
printk(KERN_NOTICE "======== cdevdemo_init 4");
return 0;
fail_malloc:
unregister_chrdev_region(devno,1);
}
void cdevdemo_exit(void) /*模块卸载*/
{
printk(KERN_NOTICE "End cdevdemo");
cdev_del(&cdevdemo_devp->cdev); /*注销cdev*/
kfree(cdevdemo_devp); /*释放设备结构体内存*/
unregister_chrdev_region(MKDEV(cdevdemo_major,0),1); //释放设备号
}
MODULE_LICENSE("Dual BSD/GPL");
module_param(cdevdemo_major, int, S_IRUGO);
module_init(cdevdemo_init);
module_exit(cdevdemo_exit);
参考:linux设备驱动–字符设备模型