字符设备驱动是linux驱动比较基础的一类设备,它比较典型的特征是按字节流的形式进行操作。下面分两种方式来介绍。
1、cdev实现的字符驱动
linux内核提供了比较成熟的cdev操作步骤,方便驱动开发者进行编写
- 申请设备号
设备号包含主设备号和次设备号,主设备号一般标识这某一类的设备,次设备号标识这类设备具体是第几个设备,例如串口设备
[root@jingdomain ~]# ls /dev/ttyS* -l
crw--w----. 1 root tty 4, 64 Mar 15 22:19 /dev/ttyS0
crw-rw----. 1 root dialout 4, 65 Mar 15 22:19 /dev/ttyS1
crw-rw----. 1 root dialout 4, 66 Mar 15 22:19 /dev/ttyS2
crw-rw----. 1 root dialout 4, 67 Mar 15 22:19 /dev/ttyS3
4个串口设备节点的主设备号相同,都为4,但每一个串口设备的次设备号各不相同,分别标识各自的设备。
申请接口如下:
/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
使用者需要传入dev_t 指针类型的变量,指定次设备号的起始号(一般为0),次设备号的个数,最后传入此设备号标识的设备名称。例如如下代码:
ret = alloc_chrdev_region(&devt, 0, count, "cdev_drv");
if (ret) {
printk("failed to alloc chrdev dev_t\n");
return ret;
}
- 初始化cdev结构
首先,定义一个全局的strcut cdev指针变量,通过cdev_alloc申请空间,并通过cdev_init将single_fops结构关联起来,形如下面的代码:
static struct cdev *single_cdev;
//alloc cdev
single_cdev = cdev_alloc();
if (!single_cdev) {
printk("cdev alloc failed\n");
goto unregister_chrdev;
}
//init cdev struct
cdev_init(single_cdev, &single_fops);
single_fops变量是struct file_operations类型的,此类型展开如下:
struct file_operations {
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 *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *,
unsigned int flags);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
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);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;
可以看到这个结构体中定义了很多函数指针,用于实现各个操作功能,如open、read、write、ioctl、poll等等,而这些接口,与用户态的open read write ioctl poll是相互对应的,从而实现了用户态与驱动的交互。
对于本文,简单实现如下:
static int
single_open(struct inode *inode, struct file *file)
{
return 0;
}
static const struct file_operations single_fops = {
.owner = THIS_MODULE,
.open = single_open,
};
- 为cdev设备填充设备号信息
通过cdev_add接口,将上面刚刚申请的设备号关连到single_cdev中
ret = cdev_add(single_cdev, devt, count);
if (ret) {
printk("cdev_add failed\n");
goto free_cdev;
}
- 创建设备类
通过class_create接口为此驱动创建一个设备类,如cdev_class,此时在系统/sys/class目录下就会出现cdev_class目录,里面有此驱动的详细的设备信息。
cdev_class = class_create(THIS_MODULE, "cdev_class");
if (IS_ERR(cdev_class)) {
ret = PTR_ERR(cdev_class);
goto free_cdev;
}
- 创建设备节点
通过device_create对各个设备进行创建,如果申请了多个次设备号,此时在/dev/目录下就会看到几个设备,比如count为3,就会生成3个设备,如/dev/cdev0 /dev/cdev1 /dev/cdev2
代码如下:
for (i = 0; i < count; i++) {
dev[i] = device_create(cdev_class, NULL, MKDEV(MAJOR(devt), i), NULL, "cdev%d", i);
if (IS_ERR(dev)) {
ret = PTR_ERR(dev);
goto free_class;
}
}
至此,cdev驱动的编写骨架就完成了,完成代码如下:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
static dev_t devt;
static struct cdev *single_cdev;
struct class *cdev_class;
struct device *dev[100];
static int count = 1;
module_param(count, int, 0644);
MODULE_PARM_DESC(count, "cdev minor count (default: 1)");
static int
single_open(struct inode *inode, struct file *file)
{
return 0;
}
static const struct file_operations single_fops = {
.owner = THIS_MODULE,
.open = single_open,
};
static int __init
cdev_drv_init(void)
{
int ret;
int i;
// alloc dev_t
ret = alloc_chrdev_region(&devt, 0, count, "cdev_drv");
if (ret) {
printk("failed to alloc chrdev dev_t\n");
return ret;
}
//alloc cdev
single_cdev = cdev_alloc();
if (!single_cdev) {
printk("cdev alloc failed\n");
goto unregister_chrdev;
}
//init cdev struct
cdev_init(single_cdev, &single_fops);
ret = cdev_add(single_cdev, devt, count);
if (ret) {
printk("cdev_add failed\n");
goto free_cdev;
}
cdev_class = class_create(THIS_MODULE, "cdev_class");
if (IS_ERR(cdev_class)) {
ret = PTR_ERR(cdev_class);
goto free_cdev;
}
for (i = 0; i < count; i++) {
dev[i] = device_create(cdev_class, NULL, MKDEV(MAJOR(devt), i), NULL, "cdev%d", i);
if (IS_ERR(dev)) {
ret = PTR_ERR(dev);
goto free_class;
}
}
printk("cdev drv init success!!!\n");
return 0;
free_class:
class_destroy(cdev_class);
free_cdev:
cdev_del(single_cdev);
unregister_chrdev:
unregister_chrdev_region(devt, 1);
return -1;
}
static void __exit
cdev_drv_exit(void)
{
int i;
for (i = 0; i < count; i++) {
device_destroy(cdev_class, MKDEV(MAJOR(devt), i));
}
class_destroy(cdev_class);
if (single_cdev)
cdev_del(single_cdev);
unregister_chrdev_region(devt, count);
printk("cdev drv exit success\n");
}
module_init(cdev_drv_init);
module_exit(cdev_drv_exit);
MODULE_LICENSE("GPL v2");
通过编译,insmod命令加载到内核, 即可出现/dev/cdev0
2、misc实现的字符驱动
misc顾名思义为杂项,内核中主要针对一些无法进行分类的字符设备,而且实现方式比cdev简单的多,通过一个misc_register即可完成驱动的注册。下面我们简单介绍一下步骤:
1、定义misc_device结构体
static struct miscdevice single_dev = {
.minor = MISC_DYNAMIC_MINOR,
.fops = &single_ops,
.name = "single_misc",
};
miscdevice结构体中主要对如上三个成员进行赋值,
①、指定minor次设备号,本文使用MISC_DYNAMIC_MINOR,代表让系统自动分配次设备号,用户也可以自行指定系统还未使用的次设备号。
②、填充fops指针,填充用户自定义的fops结构体指针即可,在本文中使用single_ops。
③、为这个misc 设备定义驱动名称,如single_misc,此名字与/dev/"devname"对应。
2、定义fops结构体
static const struct file_operations single_fops = {
.owner = THIS_MODULE,
.open = single_open,
};
本文作为一个demo例子,简单地填充了两个成员,owner及open。
3、注册misc
misc设备通过misc_register函数进行注册到内核中,与之对应的卸载通过misc_degister进行实现
ret = misc_register(&single_dev);
misc_deregister(&single_dev);
misc驱动的实现的大致步骤就介绍完了,现在贴出完整的demo例子如下:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/miscdevice.h>
static int
single_open(struct inode *inode, struct file *file)
{
printk("minor: %d\n", MINOR(inode->i_rdev));
return 0;
}
static const struct file_operations single_fops = {
.owner = THIS_MODULE,
.open = single_open,
};
static struct miscdevice single_dev = {
.minor = MISC_DYNAMIC_MINOR,
.fops = &single_fops,
.name = "single_misc",
};
static int __init
misc_drv_init(void)
{
int ret;
ret = misc_register(&single_dev);
if (ret)
return ret;
printk("misc drv init success\n");
return 0;
}
static void __exit
misc_drv_exit(void)
{
misc_deregister(&single_dev);
printk("misc drv exit success\n");
}
module_init(misc_drv_init);
module_exit(misc_drv_exit);
MODULE_LICENSE("GPL v2");
3、总结
在内核中,实现字符驱动主要是上面介绍的两种方式,cdev及misc。cdev使用步骤比较复杂,但一般用于抽象某类字符设备,比如uio(后面会详细介绍),代表一类的字符驱动,主要特征是主设备号相同,此设备号不同的一类驱动。而misc字符驱动应用于实现简单、不太好分类的驱动。用户可以根据这两种特征,选择性使用。