字符设备是Linux三大设备之一(另外两种是块设备,网络设备),字符设备是字节流形式通讯的I/O设备,绝大部分设备都是字符设备,常见的字符设备包括鼠标、键盘、显示器、串口等等,当我们执行 ls -l /dev 的时候,能看到大量的设备文件, c 是字符设备, b 是块设备,网络设备没有对应的设备文件。编写一个外部模块的字符设备驱动,除了要实现编写一个模块所需要的代码之外,还需要编写作为一个字符设备的代码。
  驱动模型
  Linux一切皆文件,那么作为一个设备文件,它的操作方法接口封装在 struct file_operations ,当我们写一个驱动的时候,一定要实现相应的接口,这样才能使这个驱动可用,Linux的内核中大量使用"注册+回调"机制进行驱动程序的编写,所谓注册回调,简单的理解,是当我们open一个设备文件的时候,其实是通过VFS找到相应的inode,并执行此前创建这个设备文件时注册在inode中的open函数,其他函数也是如此,所以,为了让我们写的驱动能够正常的被应用程序操作,首先要做的是实现相应的方法,然后再创建相应的设备文件。
#include <linux/cdev.h> //for struct cdev
#include <linux/fs.h>   //for struct file
#include <asm-generic/uaccess.h>    //for copy_to_user
#include <linux/errno.h>            //for error number
/* 准备操作方法集 */
/*
struct file_operations {
struct module *owner;   //THIS_MODULE
//读设备
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
//写设备
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
//映射内核空间到用户空间
int (*mmap) (struct file *, struct vm_area_struct *);
//读写设备参数、读设备状态、控制设备
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
//打开设备
int (*open) (struct inode *, struct file *);
//关闭设备
int (*release) (struct inode *, struct file *);
//刷新设备
int (*flush) (struct file *, fl_owner_t id);
//文件定位
loff_t (*llseek) (struct file *, loff_t, int);
//异步通知
int (*fasync) (int, struct file *, int);
//POLL机制
unsigned int (*poll) (struct file *, struct poll_table_struct *);
。。。
};
*/
ssize_t myread(struct file *filep, char __user * user_buf, size_t size, loff_t* offset)
{
return 0;
}
struct file fops = {
.owner = THIS_MODULE,
.read = myread,
...
};
/* 字符设备对象类型 */
struct cdev {
//public
struct module *owner;               //模块所有者(THIS_MODULE),用于模块计数
const struct file_operations *ops;  //操作方法集(分工:打开、关闭、读/写、...)
dev_t dev;                          //设备号(第一个)
unsigned int count;                 //设备数量
//private
...
};
static int __init chrdev_init(void)
{
...
/* 构造cdev设备对象 */
struct cdev *cdev_alloc(void);
/* 初始化cdev设备对象 */
void cdev_init(struct cdev*, const struct file_opeartions*);
/* 为字符设备静态申请设备号 */
int register_chedev_region(dev_t from, unsigned count, const char* name);
/* 为字符设备动态申请主设备号 */
int alloc_chedev_region(dev_t* dev, unsigned baseminor, unsigned count, const char* name);
MKDEV(ma,mi)    //将主设备号和次设备号组合成设备号
MAJOR(dev)      //从dev_t数据中得到主设备号
MINOR(dev)      //从dev_t数据中得到次设备号
/* 注册字符设备对象cdev到内核 */
int cdev_add(struct cdev* , dev_t, unsigned);
...
}
static void __exit chrdev_exit(void)
{
...
/* 从内核注销cdev设备对象 */
void cdev_del(struct cdev* );
/* 从内核注销cdev设备对象 */
void cdev_put(stuct cdev *);
/* 回收设备号 */
void unregister_chrdev_region(dev_t from, unsigned count);
...
}
  实现read,write
  Linux下各个进程都有自己独立的进程空间,即使是将内核的数据映射到用户进程,该数据的PID也会自动转变为该用户进程的PID,由于这种机制的存在,我们不能直接将数据从内核空间和用户空间进行拷贝,而需要专门的拷贝数据函数/宏:
  long copy_from_user(void *to, const void __user * from, unsigned long n)
  long copy_to_user(void __user *to, const void *from, unsigned long n)
  这两个函数可以将内核空间的数据拷贝到回调该函数的用户进程的用户进程空间,有了这两个函数,内核中的read,write可以实现内核空间和用户空间的数据拷贝。
  ssize_t myread(struct file *filep, char __user * user_buf, size_t size, loff_t* offset)
  {
  long ret = 0;
  size = size > MAX_KBUF?MAX_KBUF:size;
  if(copy_to_user(user_buf, kbuf,size)
  return -EAGAIN;
  }
  return 0;
  }