相关概念
设备号
设备号概念
Linux系统将所有的字符设备抽象成文件呈献给用户,用户可以在/dev 目录下找到绝大部分的字符设备文件,比如/dev/tty等。应用程序访问设备的途径正是这些设备文件。这产生了一个问题:如何将这些设备文件跟具体的设备联系起来呢?事实上,Linux系统内部为了管理字符设备,给每个字符设备设置了唯一的“编号”,也叫设备号(其实就是一个32位的整数),通过这个设备号,内核可以知道哪个文件是对应哪个设备的。
每个设备号由两部分组成:主设备号和次设备号。前面说了设备号为设备文件和设备联系起来了,但是每个设备驱动可能不只处理一个设备,比如一个串口设备驱动可能要处理两个串口的收发。这时就可以将这主设备号和从设备号的功能细分了。即主设备号负责将设备文件与设备驱动联系起来,而次设备号就将设备驱动与具体的设备联系起来。
分配设备号:
静态分配设备号,用于设备号已知的情况,不常用:
int register_chrdev_region(dev_t first, unsigned int count,char* name);
动态分配设备号,用于设备号未知并由内核自行分配的情况,常用:
int alloc_chrdev_region(dev_t *dev,unsigned int firstminor,unsigned int count,char *name);
释放设备号
下面的函数用于将设备号释放,通常在驱动模块卸载的时候释放设备号:
int unregister_chrdev_region(dev_t dev, unsigned int count);
从设备号分出主、次设备号
int MAJOR(dev_t dev);//分离出主设备号
int MINOR(dev_t dev);//分离出次设备号
合成设备号
dev_t MKDEV(unsigned int major,unsigned int minor);
主要数据结构介绍
字符设备描述结构struct cdev
在内核中每个设备都由一个结构来描述,并且采用了面向对象的思想,即属性和行为集合在一个结构中。但内核并没有采用C++等面向对象语言,因此采用结构体描述,并且在结构体中包含一个函数集。
字符设备由结构体 struct cdev
来描述,其定义如下:
struct cdev{
struct kobject kobj; //模块kobject
struct module *owner; //模块所有者
const struct file_operations *ops; //驱动操作函数集
struct list_head list; //字符设备结构链表
dev_t dev; //设备号
unsigned int count; //子设备数
};
编写驱动主要关注 ops、dev、count 这三个字段,其余字段由内核维护,其中ops 指向这个设备所支持的操作集,dev 是这个设备的设备号,count 是这个设备的子设备数比如串口设备,这个变量指出串口的个数。
打开文件结构struct file
内核中每个打开的文件都由一个struct file
结构体描述,这个结构体包含了进程打开的这个文件的所有属性,因为这个结构体比较庞大,因此这里只列出与驱动编写相关的字段:
struct file {
...
struct file_operations *f_op; //这个文件支持的操作
loff_t f_pos; //当前读写的位置
void *private_data; //可用的额外指针
...
}
f_op 指向这个函数可以执行的操作集, f_pos是当前文件的读写位置,private_data 可以指向驱动其他空间,驱动工程师可以将这个指针指向自己开辟的内核空间。
文件操作集合struct file_operations
每个打开的文件都会对应自己的操作集合,用以响应用户空间调用的文件系统调用。如open、read、write、close、ioctl等系统调用,最终响应这些系统调用的就是驱动中定义在file_operations 的函数。其定义如下:
truct file_operations {
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);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
/* remove by cym 20130408 support for MT660.ko */
#if 0
//#ifdef CONFIG_SMM6260_MODEM
#if 1// liang, Pixtree also need to use ioctl interface...
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
#endif
#endif
/* end remove */
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 *, 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);
/* add by cym 20130408 support for MT6260 and Pixtree */
#if defined(CONFIG_SMM6260_MODEM) || defined(CONFIG_USE_GPIO_AS_I2C)
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
#endif
/* end add */
};
文件索引节点struct inode
内核用inode 结构在内部表示文件,但是不同于file结构,inode结构在系统中是唯一的,因为它描述了一个文件的存在。而file结构描述了一个进程打开文件后的状态,不同的进程可能打开同一个文件。不同的进程打开同一个文件后,每个进程都有各自的file结构,但是都指向了同一个inode 结构。inode结构中包含了大量有关文件的信息,但是这里我们只关注对驱动编写有关的两个字段:
struct inode{
...
dev_t i_rdev; //对表示设备文件的inode结构,该字段包含了真正的设备号。
struct cdev *i_cdev; //当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针。
...
};
设备注册与注销
设备注册
前面提到内部用struct cdev结构来表示字符设备,在内核调用设备的操作之前,我们必须将这个结构初始化并注册到内核中。
初始化cdev:
void cdev_init(struct cdev *dev,struct file_operations *fops);
这个函数将cdev 结构与具体的file_operations 联系,如果追踪这个函数的实现可以发现,其实这个函数就是将fops赋给cdev->ops。
注册cdev:
int cdev_add(struct cdev *dev,dev_t num, unsigned int count);
所谓注册设备就是告诉内核该结构的信息。在驱动中一旦调用这个函数成功,则我们的设备就“活”了,内核就能调用它的操作,因此我们的驱动在没准备好操作集(file_operations)之前,不要调用这个函数。
设备注销
通过下面的方法将设备注销掉:
void cdev_del(struct cdev *dev);
思维导图
根据上面提到的知识,画出编写一个字符设备驱动的思维导图如下:
范例代码
最后给出一个字符设备驱动的范例代码,该驱动中开辟一块内核内存模拟设备中的内存,应用程序可以对这个虚拟设备进行读写。
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/slab.h>
int dev1_registers[5];
int dev2_registers[5];
struct cdev cdev;
dev_t devno;
/*文件打开函数*/
int mem_open(struct inode *inode, struct file *filp)
{
/*获取次设备号*/
int num = MINOR(inode->i_rdev);
if (num==0)
filp->private_data = dev1_registers;
else if(num == 1)
filp->private_data = dev2_registers;
else
return -ENODEV; //无效的次设备号
return 0;
}
/*文件释放函数*/
int mem_release(struct inode *inode, struct file *filp)
{
return 0;
}
/*读函数*/
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
int *register_addr = filp->private_data; /*获取设备的寄存器基地址*/
/*判断读位置是否有效*/
if (p >= 5*sizeof(int))
return 0;
if (count > 5*sizeof(int) - p)
count = 5*sizeof(int) - p;
/*读数据到用户空间*/
if (copy_to_user(buf, register_addr+p, count))
{
ret = -EFAULT;
}
else
{
*ppos += count;
ret = count;
}
return ret;
}
/*写函数*/
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
int *register_addr = filp->private_data; /*获取设备的寄存器地址*/
/*分析和获取有效的写长度*/
if (p >= 5*sizeof(int))
return 0;
if (count > 5*sizeof(int) - p)
count = 5*sizeof(int) - p;
/*从用户空间写入数据*/
if (copy_from_user(register_addr + p, buf, count))
ret = -EFAULT;
else
{
*ppos += count;
ret = count;
}
return ret;
}
/* seek文件定位函数 */
static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
{
loff_t newpos;
switch(whence) {
case SEEK_SET:
newpos = offset;
break;
case SEEK_CUR:
newpos = filp->f_pos + offset;
break;
case SEEK_END:
newpos = 5*sizeof(int)-1 + offset;
break;
default:
return -EINVAL;
}
if ((newpos<0) || (newpos>5*sizeof(int)))
return -EINVAL;
filp->f_pos = newpos;
return newpos;
}
/*文件操作结构体*/
static const struct file_operations mem_fops =
{
.llseek = mem_llseek,
.read = mem_read,
.write = mem_write,
.open = mem_open,
.release = mem_release,
};
/*设备驱动模块加载函数*/
static int memdev_init(void)
{
/*初始化cdev结构*/
cdev_init(&cdev, &mem_fops);
/* 注册字符设备 */
alloc_chrdev_region(&devno, 0, 2, "memdev");
cdev_add(&cdev, devno, 2);
}
/*模块卸载函数*/
static void memdev_exit(void)
{
cdev_del(&cdev); /*注销设备*/
unregister_chrdev_region(devno, 2); /*释放设备号*/
}
MODULE_LICENSE("GPL");
module_init(memdev_init);
module_exit(memdev_exit);