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

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

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

1、主设备号和次设备号

对于字符设备的访问是通过文件系统中的设备名称进行的。他们通常位于/dev目录下。如下:

?
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.1、设备编号的表达

在内核中,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);

1.2、分配和释放设备编号

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

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、重要的数据结构

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

2.1、文件操作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.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.3、inode 结构

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

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

3、字符设备的注册

内核在内部使用类型 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);

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

?
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);
5、总结本篇主要介绍了简单字符设备的编写与实现以及其中的关键点。下一篇会主要讲解下驱动的一些常用的调试技巧。

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

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

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

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

4.让你和原创作者成为很好的朋友,拓展自己的人脉资源

扫一扫下方二维码或搜索微信号coder_online即可关注,我们可以在线交流。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值