linux设备驱动程序之简单字符设备驱动
⼀、linux系统将设备分为3类:字符设备、块设备、⽹络设备。使⽤驱动程序:
1、字符设备:是指只能⼀个字节⼀个字节读写的设备,不能随机读取设备内存中的某⼀数据,读取数据需要按照先后数据。字符设备是⾯向流的设备,常见的字符设备有⿏标、键盘、串⼝、控制台和LED设备等。
2、块设备:是指可以从设备的任意位置读取⼀定长度数据的设备。块设备包括硬盘、磁盘、U盘和SD卡等。
  每⼀个字符设备或块设备都在/dev⽬录下对应⼀个设备⽂件。linux⽤户程序通过设备⽂件(或称设备节点)来使⽤驱动程序操作字符设备和块设备。
⼆、字符设备驱动程序基础:
1、主设备号和次设备号(⼆者⼀起为设备号):
  ⼀个字符设备或块设备都有⼀个主设备号和⼀个次设备号。主设备号⽤来标识与设备⽂件相连的驱动程序,⽤来反映设备类型。次设备号被驱动程序⽤来辨别操作的是哪个设备,⽤来区分同类型的设备。
  linux内核中,设备号⽤dev_t来描述,2.6.28中定义如下:
  typedef u_long dev_t;
  在32位机中是4个字节,⾼12位表⽰主设备号,低12位表⽰次设备号。
可以使⽤下列宏从dev_t中获得主次设备号:                  也可以使⽤下列宏通过主次设备号⽣成dev_t: MAJOR(dev_t dev);                              MKDEV(int major,int minor);
MINOR(dev_t dev);
View Code
//宏定义:
#define MINORBITS    20
#define MINORMASK    ((1U << MINORBITS) - 1)
#define MAJOR(dev)    ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)    ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))
2、分配设备号(两种⽅法):
(1)静态申请:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
View Code
/怎么将linux系统改成中文
**
* register_chrdev_region() - register a range of device numbers
* @from: the first in the desired range of device numbers; must include
*        the major number.
* @count: the number of consecutive device numbers required
* @name: the name of the device or driver.
*
* Return value is zero on success, a negative error code on failure.
*/
(2)动态分配:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
View Code
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
/**
* 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.
*/
注销设备号:
void unregister_chrdev_region(dev_t from, unsigned count);
创建设备⽂件:
利⽤cat /proc/devices查看申请到的设备名,设备号。
(1)使⽤mknod⼿⼯创建:mknod filename type major minor
(2)⾃动创建;
  利⽤udev(mdev)来实现设备⽂件的⾃动创建,⾸先应保证⽀持udev(mdev),由busybox配置。在驱动初始化代码⾥调⽤
class_create为该设备创建⼀个class,再为每个设备调⽤device_create创建对应的设备。
3、字符设备驱动程序重要的数据结构:
(1)struct file:代表⼀个打开的⽂件描述符,系统中每⼀个打开的⽂件在内核中都有⼀个关联的struct file。它由内核在open时创建,并传递给在⽂件上操作的任何函数,直到最后关闭。当⽂件的所有实例都关闭之后,内核释放这个数据结构。
View Code
//重要成员:
const struct file_operations    *f_op;  //该操作是定义⽂件关联的操作的。内核在执⾏open时对这个指针赋值。
off_t  f_pos;    //该⽂件读写位置。
void            *private_data;//该成员是系统调⽤时保存状态信息⾮常有⽤的资源。
(2)struct inode:⽤来记录⽂件的物理信息。它和代表打开的file结构是不同的。⼀个⽂件可以对应多个file结构,但只有⼀个inode结构。inode⼀般作为file_operations结构中函数的参数传递过来。
  inode译成中⽂就是索引节点。每个存储设备或存储设备的分区(存储设备是硬盘、软盘、U盘 ... ... )被格式化为⽂件系统后,应该有两部份,⼀部份是inode,另⼀部份是Block,Block是⽤来存储数据
⽤的。⽽inode呢,就是⽤来存储这些数据的信息,这些信息包括⽂件⼤⼩、属主、归属的⽤户组、读写权限等。inode为每个⽂件进⾏信息索引,所以就有了inode的数值。操作系统根据指令,能通过inode值最快的到相对应的⽂件。
View Code
dev_t i_rdev;    //对表⽰设备⽂件的inode结构,该字段包含了真正的设备编号。
struct cdev *i_cdev;    //是表⽰字符设备的内核的内部结构。当inode指向⼀个字符设备⽂件时,该字段包含了指向struct cdev结构的指针。
//我们也可以使⽤下边两个宏从inode中获得主设备号和此设备号:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
(3)struct file_operations
本部分来源于:,感谢的分享。
View Code
struct file_operations ***_ops={
.owner =  THIS_MODULE,
.llseek =  ***_llseek,
.read =  ***_read,
.write =  ***_write,
.ioctl =  ***_ioctl,
.open =  ***_open,
.release = ***_release,
。。。。。。
};
struct module *owner;
/*第⼀个 file_operations 成员根本不是⼀个操作; 它是⼀个指向拥有这个结构的模块的指针.
这个成员⽤来在它的操作还在被使⽤时阻⽌模块被卸载. ⼏乎所有时间中, 它被简单初始化为
THIS_MODULE, ⼀个在 <linux/module.h> 中定义的宏.这个宏⽐较复杂,在进⾏简单学习操作的时候,⼀般初始化为THIS_MODULE。*/
loff_t (*llseek) (struct file * filp , loff_t  p,  int  orig);
/*(指针参数filp为进⾏读取信息的⽬标⽂件结构体指针;参数 p 为⽂件定位的⽬标偏移量;参数orig为对⽂件定位
的起始地址,这个值可以为⽂件开头(SEEK_SET,0,当前位置(SEEK_CUR,1),⽂件末尾(SEEK_END,2))
llseek ⽅法⽤作改变⽂件中的当前读/写位置, 并且新位置作为(正的)返回值.
loff_t 参数是⼀个"long offset", 并且就算在 32位平台上也⾄少 64 位宽. 错误由⼀个负返回值指⽰.
如果这个函数指针是 NULL, seek 调⽤会以潜在地⽆法预知的⽅式修改 file 结构中的位置计数器( 在"file 结构" ⼀节中描述).*/
ssize_t (*read) (struct file * filp, char __user * buffer, size_t    size , loff_t *  p);
/*(指针参数 filp 为进⾏读取信息的⽬标⽂件,指针参数buffer 为对应放置信息的缓冲区(即⽤户空间内存地址),
参数size为要读取的信息长度,参数 p 为读的位置相对于⽂件开头的偏移,在读取信息后,这个指针⼀般都会移动,移动的值为要读取信息的长度值)
这个函数⽤来从设备中获取数据. 在这个位置的⼀个空指针导致 read 系统调⽤以 -EINVAL("Invalid argument") 失败.
⼀个⾮负返回值代表了成功读取的字节数( 返回值是⼀个 "signed size" 类型, 常常是⽬标平台本地的整数类型).*/
ssize_t (*aio_read)(struct kiocb *  , char __user *  buffer, size_t  size ,  loff_t  p);
/*可以看出,这个函数的第⼀、三个参数和本结构体中的read()函数的第⼀、三个参数是不同的,
异步读写的第三个参数直接传递值,⽽同步读写的第三个参数传递的是指针,因为AIO从来不需要改变⽂件的位置。
异步读写的第⼀个参数为指向kiocb结构体的指针,⽽同步读写的第⼀参数为指向file结构体的指针,每⼀个I/O请求都对应⼀个kiocb结构体);
初始化⼀个异步读 -- 可能在函数返回前不结束的读操作.如果这个⽅法是 NULL, 所有的操作会由 read 代替进⾏(同步地).
(有关linux异步I/O,可以参考有关的资料,《linux设备驱动开发详解》中给出了详细的解答)*/
ssize_t (*write) (struct file *  filp, const char __user *  buffer, size_t  count, loff_t * ppos);
/*(参数filp为⽬标⽂件结构体指针,buffer为要写⼊⽂件的信息缓冲区,count为要写⼊信息的长度,
ppos为当前的偏移位置,这个值通常是⽤来判断写⽂件是否越界)
发送数据给设备. 如果 NULL, -EINVAL 返回给调⽤ write 系统调⽤的程序. 如果⾮负, 返回值代表成功写的字节数.
(注:这个操作和上⾯的对⽂件进⾏读的操作均为阻塞操作)*/
ssize_t (*aio_write)(struct kiocb *, const char __user *  buffer, size_t  count, loff_t * ppos);
/*初始化设备上的⼀个异步写.参数类型同aio_read()函数;*/
int (*readdir) (struct file *  filp, void *, filldir_t);
/*对于设备⽂件这个成员应当为 NULL; 它⽤来读取⽬录, 并且仅对⽂件系统有⽤.*/
unsigned int (*poll) (struct file *, struct poll_table_struct *);
/*(这是⼀个设备驱动中的轮询函数,第⼀个参数为file结构指针,第⼆个为轮询表指针)
这个函数返回设备资源的可获取状态,即POLLIN,POLLOUT,POLLPRI,POLLERR,POLLNVAL等宏的位“或”结果。
每个宏都表明设备的⼀种状态,如:POLLIN(定义为0x0001)意味着设备可以⽆阻塞的读,POLLOUT(定义为0x0004)意味着设备可以⽆阻塞的写。(poll ⽅法是 3 个系统调⽤的后端: poll, epoll, 和 select, 都⽤作查询对⼀个或多个⽂件描述符的读或写是否会阻塞.
poll ⽅法应当返回⼀个位掩码指⽰是否⾮阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息⽤来使调⽤进程睡眠直到 I/O 变为可能.
如果⼀个驱动的 poll ⽅法为 NULL, 设备假定为不阻塞地可读可写.
(这⾥通常将设备看作⼀个⽂件进⾏相关的操作,⽽轮询操作的取值直接关系到设备的响应情况,可以是阻塞操作结果,同时也可以是⾮阻塞操作结果)*/ int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
/*(inode 和 filp 指针是对应应⽤程序传递的⽂件描述符 fd 的值, 和传递给 open ⽅法的相同参数.
cmd 参数从⽤户那⾥不改变地传下来, 并且可选的参数 arg 参数以⼀个 unsigned long 的形式传递, 不管它是否由⽤户给定为⼀个整数或⼀个指针.
如果调⽤程序不传递第 3 个参数, 被驱动操作收到的 arg 值是⽆定义的.
因为类型检查在这个额外参数上被关闭, 编译器不能警告你如果⼀个⽆效的参数被传递给 ioctl, 并且任何关联的错误将难以查.)
ioctl 系统调⽤提供了发出设备特定命令的⽅法(例如格式化软盘的⼀个磁道, 这不是读也不是写). 另外, ⼏个 ioctl 命令被内核识别⽽不必引⽤ fops 表.
如果设备不提供 ioctl ⽅法, 对于任何未事先定义的请求(-ENOTTY, "设备⽆这样的 ioctl"), 系统调⽤返回⼀个错误.*/
int (*mmap) (struct file *, struct vm_area_struct *);
/*mmap ⽤来请求将设备内存映射到进程的地址空间. 如果这个⽅法是 NULL, mmap 系统调⽤返回 -ENODEV.
(如果想对这个函数有个彻底的了解,那么请看有关“进程地址空间”介绍的书籍)*/
int (*open) (struct inode * inode , struct file *  filp ) ;
/*(inode 为⽂件节点,这个节点只有⼀个,⽆论⽤户打开多少个⽂件,都只是对应着⼀个inode结构;
但是filp就不同,只要打开⼀个⽂件,就对应着⼀个file结构体,file结构体通常⽤来追踪⽂件在运⾏时的状态信息)
尽管这常常是对设备⽂件进⾏的第⼀个操作, 不要求驱动声明⼀个对应的⽅法. 如果这个项是 NULL, 设备打开⼀直成功, 但是你的驱动不会得到通知.
与open()函数对应的是release()函数。*/
int (*flush) (struct file *);
/
*flush 操作在进程关闭它的设备⽂件描述符的拷贝时调⽤; 它应当执⾏(并且等待)设备的任何未完成的操作.
这个必须不要和⽤户查询请求的 fsync 操作混淆了. 当前, flush 在很少驱动中使⽤;
SCSI 磁带驱动使⽤它, 例如, 为确保所有写的数据在设备关闭前写到磁带上. 如果 flush 为 NULL, 内核简单地忽略⽤户应⽤程序的请求.*/
int (*release) (struct inode *, struct file *);
/*release ()函数当最后⼀个打开设备的⽤户进程执⾏close()系统调⽤的时候,内核将调⽤驱动程序release()函数:
void release(struct inode inode,struct file *file),release函数的主要任务是清理未结束的输⼊输出操作,释放资源,⽤户⾃定义排他标志的复位等。
在⽂件结构被释放时引⽤这个操作. 如同 open, release 可以为 NULL.*/
int(*synch)(struct file *,struct dentry *,int datasync);
//刷新待处理的数据,允许进程把所有的脏缓冲区刷新到磁盘。
int (*aio_fsync)(struct kiocb *, int);
/*这是 fsync ⽅法的异步版本.所谓的fsync⽅法是⼀个系统调⽤函数。系统调⽤fsync
把⽂件所指定的⽂件的所有脏缓冲区写到磁盘中(如果需要,还包括存有索引节点的缓冲区)。
相应的服务例程获得⽂件对象的地址,并随后调⽤fsync⽅法。通常这个⽅法以调⽤函数__writeback_single_inode()结束,
这个函数把与被选中的索引节点相关的脏页和索引节点本⾝都写回磁盘。*/
int (*fasync) (int, struct file *, int);
//这个函数是系统⽀持异步通知的设备驱动,下⾯是这个函数的模板:
static int ***_fasync(int fd,struct file *filp,int mode)
{
struct ***_dev * dev=filp->private_data;
return fasync_helper(fd,filp,mode,&dev->async_queue);//第四个参数为 fasync_struct结构体指针的指针。
//这个函数是⽤来处理FASYNC标志的函数。(FASYNC:表⽰兼容BSD的fcntl同步操作)当这个标志改变时,驱动程序中的fasync()函数将得到执⾏。
}
/*此操作⽤来通知设备它的 FASYNC 标志的改变. 异步通知是⼀个⾼级的主题, 在第 6 章中描述.
这个成员可以是NULL 如果驱动不⽀持异步通知.*/
int (*lock) (struct file *, int, struct file_lock *);
//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 *);
/*这些⽅法实现发散/汇聚读和写操作. 应⽤程序偶尔需要做⼀个包含多个内存区的单个读或写操作;
这些系统调⽤允许它们这样做⽽不必对数据进⾏额外拷贝. 如果这些函数指针为 NULL, read 和 write ⽅法被调⽤( 可能多于⼀次 ).*/
ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *);
/*这个⽅法实现 sendfile 系统调⽤的读, 使⽤最少的拷贝从⼀个⽂件描述符搬移数据到另⼀个.
例如, 它被⼀个需要发送⽂件内容到⼀个⽹络连接的 web 服务器使⽤. 设备驱动常常使 sendfile 为 NULL.*/
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
/*sendpage 是 sendfile 的另⼀半; 它由内核调⽤来发送数据, ⼀次⼀页, 到对应的⽂件. 设备驱动实际上不实现 sendpage.*/
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
/*这个⽅法的⽬的是在进程的地址空间⼀个合适的位置来映射在底层设备上的内存段中.
这个任务通常由内存管理代码进⾏; 这个⽅法存在为了使驱动能强制特殊设备可能有的任何的对齐请求. ⼤部分驱动可以置这个⽅法为 NULL.[10]*/
int (*check_flags)(int)
//这个⽅法允许模块检查传递给 fnctl() 调⽤的标志.
int (*dir_notify)(struct file *, unsigned long);
//这个⽅法在应⽤程序使⽤ fcntl 来请求⽬录改变通知时调⽤. 只对⽂件系统有⽤; 驱动不需要实现 dir_notify.
三、字符设备驱动程序设计:
1.设备注册:
在linux2.6内核中,字符设备使⽤struct cdev来描述;
struct cdev
{
struct kobject kobj;//内嵌的kobject对象
struct module *owner;//所属模块
struct file_operations *ops;//⽂件操作结构体
struct list_head list;
dev_t dev;//设备号,长度为32位,其中⾼12为主设备号,低20位为此设备号
unsigned int count;
};
字符设备的注册分为三个步骤:
(1)分配cdev: struct cdev *cdev_alloc(void);
(2)初始化cdev: void cdev_init(struct cdev *cdev, const struct file_operations *fops);
(3)添加cdev: int cdev_add(struct cdev *p, dev_t dev, unsigned count)
View Code
/**
* cdev_add() - add a char device to the system
* @p: the cdev structure for the device
* @dev: the first device number for which this device is responsible
* @count: the number of consecutive minor numbers corresponding to this
*        device
*
* cdev_add() adds the device represented by @p to the system, making it
* live immediately.  A negative error code is returned on failure.
*/
2.设备操作的实现:file_operations函数集的实现(要明确某个函数什么时候被调⽤?调⽤来做什么操作?)
特别注意:驱动程序应⽤程序的数据交换:
  驱动程序和应⽤程序的数据交换是⾮常重要的。file_operations中的read()和write()函数,就是⽤来在驱动程序和应⽤程序间交换数据的。通过数据交换,驱动程序和应⽤程序可以彼此了解对⽅的情况。但是驱动程序和应⽤程序属于不同的地址空间。驱动程序不能直接访问应⽤程序的地址空间;同样应⽤程序也不能直接访问驱动程序的地址空间,否则会破坏彼此空间中的数据,从⽽造成系统崩溃,或者数据损坏。安全的⽅法是使⽤内核提供的专⽤函数,完成数据在应⽤程序空间和驱动程序空间的交换。这些函数对⽤户程序传过来的指针进⾏了严格的检查和必要的转换,从⽽保证⽤户程序与驱动程序交换数据的安全性。这些函数有:
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
put_user(local,user);
get_user(local,user);
3.设备注销:void cdev_del(struct cdev *p);
四、字符设备驱动⼩结:
  字符设备是3⼤类设备(字符设备、块设备、⽹络设备)中较简单的⼀类设备,其驱动程序中完成的主要⼯作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_operation结构体中操作函数,并实现file_operations结构体中的read()、write()、ioctl()等重要函数。如图所⽰为cdev结构体、file_operations和⽤户空间调⽤驱动的关系。
五:字符设备驱动程序分析:
(1)memdev.h
View Code
#ifndef _MEMDEV_H_
#define _MEMDEV_H_
#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;
};
#endif /* _MEMDEV_H_ */
(2)memdev.c
static mem_major = MEMDEV_MAJOR;
module_param(mem_major, int, S_IRUGO);
struct mem_dev *mem_devp; /*设备结构体指针*/
struct cdev cdev;
/*⽂件打开函数*/
int mem_open(struct inode *inode, struct file *filp)
{

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。