设备文件
在Linux中有一切皆文件的概念(网络设备除外),这意味着设备也会被抽象成一个文件,应用程序对设备的访问最终转变成了对文件的访问。Linux的设备文件通常在/dev目录下,通过命令ls -l /dev可以查看设备文件:
另外还可以通过命令“mknod /dev/设备文件名 设备类型(c或者d) 主设备号 次设备号”创建设备文件(关于在驱动中创建设备文件的内容在后续章节介绍)
设备号
Linux系统通过设备号来区分不同的设备和驱动(所以设备文件有对应的设备号与之关联),Linux的设备号用一个32位的变量来表示(数据类型dev_t),它分为主设备号和次设备号,其中主设备号占高12位,用于区分驱动程序,次设备号占低20位,用于区分设备。
设备号相关的函数和宏
/* 通过主设备号和次设备号生成一个设备号
1. ma 主设备号
2. mi 次设备号
3. 返回合成的设备号
*/
#define MKDEV(ma,mi)
//从设备号中提取次设备号
#define MINOR(dev)
//从设备号中提取主设备号
#define MAJOR(dev)
/* 注册字符设备号,可以连续注册多个字符设备号
4. from 起始设备号
5. count 要注册的数量,注册多个时设备号依次递增
6. name 标记主设备号名称
7. 成功返回0
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name)
/* 动态分配字符设备号,可以连续分配多个字符设备号
8. dev 用于返回起始设备号
9. baseminor 次设备号起始值
10. count 要分配的数量,注册多个时设备号依次递增
11. name 标记主设备号名称
12. 成功返回0
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
/* 注销字符设备号
13. from 起始设备号
14. count 要注销的数量,与创建或分配时传入的值一样
*/
void unregister_chrdev_region(dev_t from, unsigned count)
struct cdev 对象
在Linux内核中struct cdev对象管理字符设备驱动(Linux内核采用C语言编写,但是大量使用了面向对象的编程思维),其核心成员如下:
//指向所关联的设备操作函数集合
const struct file_operations *ops;
//设备号
dev_t dev;
//驱动所管理的设备数量,一个驱动可以同时支持多个设备
unsigned int count;
struct file_operations 对象
struct file_operations 对象对设备的操作进行了抽象,驱动开发过程中应根据设备特性和功能实现相应的函数,其核心成员如下:
//定位读写位置,与应用层的lseek函数对应
loff_t (*llseek) (struct file *, loff_t, int);
//读数据,与应用层的read函数对应
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
//写设备,与应用层的write函数对应
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
//控制设备,与应用层的iotl函数对应
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
//功能与unlocked_ioctl一样,只有在64位系统中运行32位程序时才会调用,与应用层的iotl函数对应
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
//将设备内存映射到用户空间,与应用层的mmap函数对应
int (*mmap) (struct file *, struct vm_area_struct *);
//打开设备,与应用层的open函数对应
int (*open) (struct inode *, struct file *);
//释放设备,当应用层最后一次调用close时执行
int (*release) (struct inode *, struct file *);
//多路复用I/O,与应用层的poll、select、epoll函数对应
unsigned int (*poll) (struct file *, struct poll_table_struct *);
struct file 对象
在执行open的过程中系统会分配一个struct file 对象用于表示打开的文件(包括设备文件),其核心成员如下:
//指向文件对应的inode节点
struct inode *f_inode;
//文件打开方式,对应系统调用open的int flags参数
unsigned int f_flags;
//文件权限,对应系统调用open的mode_t mode参数
fmode_t f_mode;
//文件的当前读写位置,需要在struct file_operations 的read、write进行维护和更新
loff_t f_pos;
//供驱动使用的私有数据,驱动程序可以在其中暂存一些参数
void *private_data;
struct inode 对象
struct inode 对象表示文件的inode节点,内核在第一次打开文件时会为其创建相应的inode节点,其核心成员如下:
//设备号
dev_t i_rdev;
//如果是块设备则指向对应的block_device对象
//如果是字符设备则指向对应的cdev对象
union {
struct block_device *i_bdev;
struct cdev *i_cdev;
};
编写一个简单的字符设备
实现一个字符设备的步骤如下:
- 构建struct file_operations对象
- 注册/分配设备号
- 利用struct file_operations对象构造struct cdev 对象
- 将struct cdev 对象添加到系统的cdev_map中
如下是一个简单的字符设备驱动框架:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
//使能自动分配设备号
#define USING_AUTO_REGION
//主设备号
#define CDEV_MAJOR 300
//次设备号
#define CDEV_MINOR 0
//此驱动支持的设备数量
#define CDEV_CNT 2
//主设备号名称
#define CDEV_NAME "test_cdev"
//设备号
static dev_t dev;
//cdev 对象,用于管理字符设备驱动
static struct cdev char_dev;
static int cdev_open(struct inode *inode, struct file *filep)
{
dev_t rdev;
//inode节点的i_rdev即是当前设备的设备号
rdev = inode->i_rdev;
//file对象的private_data是专为驱动预留的一个成员,这里用于存储设备号
filep->private_data = (void*)rdev;
printk("%s\r\n", __FUNCTION__);
printk("major = %d,", MAJOR(rdev));
printk("minor = %d\r\n", MINOR(rdev));
return 0;
}
static int cdev_release(struct inode *inode, struct file *filep)
{
dev_t rdev = (dev_t)filep->private_data;
printk("%s\r\n", __FUNCTION__);
printk("major = %d,", MAJOR(rdev));
printk("minor = %d\r\n", MINOR(rdev));
return 0;
}
static ssize_t cdev_read(struct file *filep, char __user *buffer, size_t size, loff_t *pos)
{
dev_t rdev = (dev_t)filep->private_data;
printk("%s\r\n", __FUNCTION__);
printk("major = %d,", MAJOR(rdev));
printk("minor = %d\r\n", MINOR(rdev));
return 0;
}
static ssize_t cdev_write(struct file *filep, const char __user *buffer, size_t size, loff_t *pos)
{
dev_t rdev = (dev_t)filep->private_data;
printk("%s\r\n", __FUNCTION__);
printk("major = %d,", MAJOR(rdev));
printk("minor = %d\r\n", MINOR(rdev));
return size;
}
/***************************1. 构建struct file_operations对象***************************/
//字符设备操作方法
static struct file_operations ops =
{
.owner = THIS_MODULE,
.open = cdev_open,
.release = cdev_release,
.read = cdev_read,
.write = cdev_write,
};
static int __init char_init(void)
{
int i;
int ret;
printk("cdev init\r\n");
/***************************2. 注册/分配设备号***************************/
#ifndef USING_AUTO_REGION
/* 合成设备号
* ma 主设备号
* mi 次设备号
* 返回合成的设备号
*/
dev = MKDEV(CDEV_MAJOR, CDEV_MINOR);
/* 注册一系列字符设备号
* from 起始设备号
* count 要注册的数量,注册多个时设备号依次递增
* name 标记主设备号名称
* 成功返回0
*/
ret = register_chrdev_region(dev, CDEV_CNT, CDEV_NAME);
if(ret != 0)
{
printk("region device ID failed\r\n");
return ret;
}
for(i=0; i<CDEV_CNT; i++)
{
printk("device%d ", i);
/* 提取主设备号
* dev 设备号
* 返回主设备号
*/
printk("major = %d,", MAJOR(dev+i));
/* 提取次设备号
* dev 设备号
* 返回次设备号
*/
printk("minor = %d\r\n", MINOR(dev+i));
}
#else
/* 动态分配一系列字符设备号
* dev 用于返回第一个设备号
* baseminor 次设备号起始值
* count 要分配的数量,注册多个时设备号依次递增
* name 标记主设备号名称
* 成功返回0
*/
ret = alloc_chrdev_region(&dev, CDEV_MINOR, CDEV_CNT, CDEV_NAME);
if(ret != 0)
{
printk("alloc device ID failed\r\n");
return ret;
}
for(i=0; i<CDEV_CNT; i++)
{
printk("device%d ", i);
printk("major = %d,", MAJOR(dev+i));
printk("minor = %d\r\n", MINOR(dev+i));
}
#endif
/***************************3. 利用struct file_operations对象构造struct cdev 对象**************************/
/* 初始化 cdev 对象
* cdev 要初始化的对象
* fops 设备操作函数
*/
cdev_init(&char_dev, &ops);
char_dev.owner = THIS_MODULE;
/***************************4. 将struct cdev 对象添加到系统的cdev_map中**************************/
/* 添加cdev对象到内核
* p 指向要添加的cdev对象
* dev 设备号
* count cdev对象所管理的设备数量
*/
ret = cdev_add(&char_dev, dev, CDEV_CNT);
if(ret != 0)
{
unregister_chrdev_region(dev, CDEV_CNT);
printk("add cdev failed\r\n");
return ret;
}
return 0;
}
static void __exit char_exit(void)
{
printk("cdev exit\r\n");
/* 从内核删除cdev对象
* p 要删除的内核对象
*/
cdev_del(&char_dev);
/* 注销一系列字符设备号
* from 起始设备号
* count 要注销的数量
*/
unregister_chrdev_region(dev, CDEV_CNT);
}
module_init(char_init);
module_exit(char_exit);
MODULE_LICENSE("GPL");
实验
- 在这里下载代码后,进行编译,并拷贝到模板板root目录中
- 执行命令insmod cdev_frame.ko安装内核模块,可以看到有两个字符设备,主设备号均为241,次设备号分别是0和1(这里采用自动分配设备号,具体的设备号依实际情况而定)
- 执行mknod /dev/cdev_test0 c 241 0为主设备号为241次设备号为0的设备创建一个设备文件,创建成功后的/dev/目录下会有相应的设备文件名(同样也可以用mknod /dev/cdev_test0 c 241 1为主设备号为241次设备号为1的设备创建一个设备文件)
- 通过命令cat /dev/cdev_test0可以执行到设备驱动的open raed release函数(因为read函数返回0,所以认为读取结束,cat自动结束)
- 通过命令echo “123456” >> /dev/cdev_test0可以执行到设备驱动的open write release函数(write函数直接返回写入字节数,表示数据全部写入成功)