在linux设备驱动第一篇:设备驱动程序简介中简单介绍了字符驱动,本篇简单介绍如何写一个简单的字符设备驱动。本篇借鉴LDD中的源码,实现一个与硬件设备无关的字符设备驱动,仅仅操做从内核中分配的一些内存。node
下面就开始学习如何写一个简单的字符设备驱动。首先咱们来分解一下字符设备驱动
都有那些结构或者方法组成,也就是说实现一个可使用的字符设备驱动咱们必须作些什么工做。linux
对于字符设备的访问是经过文件系统中的设备名称进行的。他们一般位于/dev目录下。以下:程序员
[cpp] view plaincopyshell
xxx@ubuntu :~$ ls -l /dev/ ubuntu
total 0 微信
brw-rw---- 1 root disk 7, 0 3月 25 10:34 loop0 数据结构
brw-rw---- 1 root disk 7, 1 3月 25 10:34 loop1 app
brw-rw---- 1 root disk 7, 2 3月 25 10:34 loop2 微信公众平台
crw-rw-rw- 1 root tty 5, 0 3月 25 12:48 tty async
crw--w---- 1 root tty 4, 0 3月 25 10:34 tty0
crw-rw---- 1 root tty 4, 1 3月 25 10:34 tty1
crw--w---- 1 root tty 4, 10 3月 25 10:34 tty10
其中b表明块设备,c表明字符设备。对于普通文件来讲,ls -l会列出文件的长度,而对于设备文件来讲,上面的7,5,4等表明的是对应设备的主设备号,然后面的0,1,2,10等则是对应设备的次设备号。那么主设备号和次设备号分别表明什么意义呢?通常状况下,能够这样理解,主设备号标识设备对应的驱动程序,也就是说1个主设备号对应一个驱动程序。固然,如今也有多个驱动程序共享主设备号的状况。而次设备号有内核使用,用于肯定/dev下的设备文件对应的具体设备。举一个例子,虚拟控制台和串口终端有驱动程序4管理,而不一样的终端分别有不一样的次设备号。
在内核中,dev_t用来保存设备编号,包括主设备号和次设备号。在2.6的内核版本种,dev_t是一个32位的数,其中12位用来表示主设备号,其他20位用来标识次设备号。
经过dev_t获取主设备号和次设备号使用下面的宏:
MAJOR(dev_t dev);
MINOR(dev_t dev);
相反,经过主设备号和次设备号转换为dev_t类型使用:
MKDEV(int major, int minor);
在构建一个字符设备以前,驱动程序首先要得到一个或者多个设备编号,这相似一个营业执照,有了营业执照才在内核中正常工做营业。完成此工做的函数是:
[cpp] view plaincopy
int register_chrdev_region(dev_t first, unsigned int count, const char *name);
first是要分配的设备编号范围的起始值。count是连续设备的编号的个数。name是和该设备编号范围关联的设备名称,他将出如今/proc/devices和sysfs中。此函数成功返回0,失败返回负的错误码。此函数是在已知主设备号的状况下使用,在未知主设备号的状况下,咱们使用下面的函数:
[cpp] view plaincopy
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);
dev用于输出申请到的设备编号,firstminor要使用的第一个此设备编号。
在不使用时须要释放这些设备编号,已提供其余设备程序使用:
[cpp] view plaincopy
void unregister_chrdev_region(dev_t dev, unsigned int count);
函数多在模块的清除函数中调用。
分配到设备编号以后,咱们只是拿到了营业执照,虽然说如今已经准备的差很少了,可是咱们只是从内核中申请到了设备号,应用程序仍是不能对此设备做任何事情,咱们须要一个简单的函数来把设备编号和此设备能实现的功能链接起来,这样咱们的模块才能提供具体的功能.这个操做很简单,稍后就会提到,在此以前先介绍几个重要的数据结构。
注册设备编号仅仅是完成一个字符设备驱动的第一步。下面介绍大部分驱动都会包含的三个重要的内核的数据结构。
file_operations是第一个重要的结构,定义在 <linux/fs.h>, 是一个函数指针的集合,设备所能提供的功能大部分都由此结构提供。这些操做也是设备相关的系统调用的具体实现。此结构的具体实现以下所示:
[cpp] view plaincopy
struct file_operations {
//它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操做还在被使用时阻止模块被卸载. 几乎全部时间中, 它被简单初始化为 THIS_MODULE
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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*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 *);
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 (*aio_fsync) (struct kiocb *, 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 **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
int (*show_fdinfo)(struct seq_file *m, struct file *f);
};
须要说明的是这里面的函数在驱动中不用所有实现,不支持的操做留置为NULL。
struct file, 定义于 <linux/fs.h>, 是设备驱动中第二个最重要的数据结构。文件结构表明一个打开的文件. (它不特定给设备驱动; 系统中每一个打开的文件有一个关联的 struct file 在内核空间). 它由内核在 open 时建立, 并传递给在文件上操做的任何函数, 直到最后的关闭. 在文件的全部实例都关闭后, 内核释放这个数据结构。file结构的详细可参考fs.h,这里列出来几个重要的成员。
struct file_operations *f_op:就是上面刚刚介绍的文件操做的集合结构。
mode_t f_mode:文件模式肯定文件是可读的或者是可写的(或者都是), 经过位 FMODE_READ 和 FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函数中检查这个成员的读写许可, 可是你不须要检查读写许可, 由于内核在调用你的方法以前检查. 当文件尚未为那种存取而打开时读或写的企图被拒绝, 驱动甚至不知道这个状况
loff_t f_pos:当前读写位置. loff_t 在全部平台都是 64 位。驱动能够读这个值, 若是它须要知道文件中的当前位置, 可是正常地不该该改变它。
unsigned int f_flags:这些是文件标志, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驱动应当检查 O_NONBLOCK 标志来看是不是请求非阻塞操做。
void *private_data:open 系统调用设置这个指针为 NULL, 在为驱动调用 open 方法以前. 你可自由使用这个成员或者忽略它; 你可使用这个成员来指向分配的数据, 可是接着你必须记住在内核销毁文件结构以前, 在 release 方法中释放那个内存. private_data 是一个有用的资源, 在系统调用间保留状态信息, 咱们大部分例子模块都使用它
inode 结构由内核在内部用来表示文件. 所以, 它和表明打开文件描述符的文件结构是不一样的. 可能有表明单个文件的多个打开描述符的许多文件结构, 可是它们都指向一个单个 inode 结构。
inode 结构包含大量关于文件的信息。但对于驱动程序编写来讲通常不用关心,暂且不说。
内核在内部使用类型 struct cdev 的结构来表明字符设备. 在内核调用你的设备操做前, 你编写分配并注册一个或几个这些结构。
有 2 种方法来分配和初始化一个这些结构. 若是你想在运行时得到一个独立的 cdev 结构, 你能够为此使用这样的代码:
[cpp] view plaincopy
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
更多的状况是把cdv结构嵌入到你本身封装的设备结构中,这时须要使用下面的方法来分配和初始化:
void cdev_init(struct cdev *cdev, struct file_operations *fops);
后面的例子程序就是这么作的。一旦 cdev 结构创建, 最后的步骤是把它告诉内核:
[cpp] view plaincopy
int cdev_add(struct cdev *dev, dev_t num, unsigned int count)
<span style="font-family: Simsun;">这里, dev 是 cdev 结构, num 是这个设备响应的第一个设备号, count 是应当关联到设备的设备号的数目. 经常 count 是 1。</span>
<span style="font-family: Simsun;"></span><p style="font-family: Simsun;">从系统去除一个字符设备, 调用:</p>
[cpp] view plaincopy
void cdev_del(struct cdev *dev);
<h3 style="margin: 0px; padding: 0px;">四、一个简单的字符设备</h3><div>上面大体介绍了实现一个字符设备所要作的工做,下面就来一个真实的例子来总结上面介绍的内容。源码中的关键地方已经做了注释。</div>
[cpp] view plaincopy
#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);
Makefile文件以下:
[cpp] view plaincopy
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
五、<strong>总结</strong>
本篇主要介绍了简单字符设备的编写与实现以及其中的关键点。下一篇会主要讲解下驱动的一些经常使用的调试技巧。
第一时间得到博客更新提醒,以及更多技术信息分享,欢迎关注我的微信公众平台:程序员互动联盟(coder_online)
1.直接帮你解答linux设备驱动疑问点
2.第一时间得到业内十多个领域技术文章
3.针对文章内疑点提出问题,第一时间回复你,帮你耐心解答
4.让你和原创做者成为很好的朋友,拓展本身的人脉资源
扫一扫下方二维码或搜索微信号coder_online便可关注,咱们能够在线交流。