linux设备驱动第三篇:如何实现一个简单的字符设备驱动

linux设备驱动第一篇:设备驱动程序简介中简单介绍了字符驱动,本篇简单介绍如何写一个简单的字符设备驱动。本篇借鉴LDD中的源码,实现一个与硬件设备无关的字符设备驱动,仅仅操做从内核中分配的一些内存。node

下面就开始学习如何写一个简单的字符设备驱动。首先咱们来分解一下字符设备驱动都有那些结构或者方法组成,也就是说实现一个可使用的字符设备驱动咱们必须作些什么工做。linux

一、主设备号和次设备号

对于字符设备的访问是经过文件系统中的设备名称进行的。他们一般位于/dev目录下。以下:程序员

?ubuntu

1
2
3
4
5
6
7
8
9
xxx@ubuntu :~$ ls -l /dev/
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
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
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 管理,而不一样的终端分别有不一样的次设备号。微信

1.一、设备编号的表达

在内核中,dev_t用来保存设备编号,包括主设备号和次设备号。在2.6的内核版本种,dev_t是一个32位的数,其中12位用来表示主设备号,其他20位用来标识次设备号。数据结构

经过dev_t获取主设备号和次设备号使用下面的宏:app

MAJOR(dev_t dev);微信公众平台

MINOR(dev_t dev);async

相反,经过主设备号和次设备号转换为dev_t类型使用:函数

MKDEV(int major, int minor);

1.二、分配和释放设备编号

在构建一个字符设备以前,驱动程序首先要得到一个或者多个设备编号,这相似一个营业执照,有了营业执照才在内核中正常工做营业。完成此工做的函数是:

int register_chrdev_region(dev_t first, unsigned int count, const char *name);

first 是要分配的设备编号范围的起始值。count是连续设备的编号的个数。name是和该设备编号范围关联的设备名称,他将出如今/proc/devices 和sysfs中。此函数成功返回0,失败返回负的错误码。此函数是在已知主设备号的状况下使用,在未知主设备号的状况下,咱们使用下面的函数:

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, const char *name);dev用于输出申请到的设备编号,firstminor要使用的第一个此设备编号。

在不使用时须要释放这些设备编号,已提供其余设备程序使用:

void unregister_chrdev_region(dev_t dev, unsigned int count);

此函数多在模块的清除函数中调用。

分配到设备编号以后,咱们只是拿到了营业执照,虽然说如今已经准备的差很少了,可是咱们只是从内核中申请到了设备号,应用程序仍是不能对此设备做任何 事情,咱们须要一个简单的函数来把设备编号和此设备能实现的功能链接起来,这样咱们的模块才能提供具体的功能.这个操做很简单,稍后就会提到,在此以前先 介绍几个重要的数据结构。

二、重要的数据结构

注册设备编号仅仅是完成一个字符设备驱动的第一步。下面介绍大部分驱动都会包含的三个重要的内核的数据结构。

2.一、文件操做file_operations

file_operations是第一个重要的结构,定义在 <linux/fs.h>, 是一个函数指针的集合,设备所能提供的功能大部分都由此结构提供。这些操做也是设备相关的系统调用的具体实现。此结构的具体实现以下所示:

?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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。

2.二、文件结构struct file

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 是一个有用的资源, 在系统调用间保留状态信息, 咱们大部分例子模块都使用它

2.三、inode 结构

inode 结构由内核在内部用来表示文件. 所以, 它和表明打开文件描述符的文件结构是不一样的. 可能有表明单个文件的多个打开描述符的许多文件结构, 可是它们都指向一个单个 inode 结构。

inode 结构包含大量关于文件的信息。但对于驱动程序编写来讲通常不用关心,暂且不说。

三、字符设备的注册

内核在内部使用类型 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 结构创建, 最后的步骤是把它告诉内核:

int cdev_add(struct cdev *dev, dev_t num, unsigned int count)

dev 是 cdev 结构, num 是这个设备响应的第一个设备号, count 是应当关联到设备的设备号的数目. 经常 count 是 1。

从系统去除一个字符设备, 调用:

void cdev_del(struct cdev *dev);

四、一个简单的字符设备上面大体介绍了实现一个字符设备所要作的工做,下面就来一个真实的例子来总结上面介绍的内容。源码中的关键地方已经做了注释。

?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
#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);
五、总结本篇主要介绍了简单字符设备的编写与实现以及其中的关键点。下一篇会主要讲解下驱动的一些经常使用的调试技巧。

第一时间得到博客更新提醒,以及更多技术信息分享,欢迎关注我的微信公众平台:程序员互动联盟(coder_online)

1.直接帮你解答linux设备驱动疑问点

2.第一时间得到业内十多个领域技术文章

3.针对文章内疑点提出问题,第一时间回复你,帮你耐心解答

4.让你和原创做者成为很好的朋友,拓展本身的人脉资源

扫一扫下方二维码或搜索微信号coder_online便可关注,咱们能够在线交流。

相关文章
相关标签/搜索