阅读引言: 从linux文件的种类、字符设备的创建、设备号、申请设备号、cdev对象和字符设备的对应关系、应用层调用到我们编写的设备驱动方法合集的流程。
目录
一、Linux文件的种类
二、Linux对设备的分类
三、驱动程序如何向应用层提供接口
四、Linux中设备的划分
五、设备号的申请和注销
六、申请设备并根据设备号创建设备文件演示
七、字符设备驱动框架分析
一、Linux文件的种类
linux的文件种类:
1. -:普通文件
2. d:目录文件
3. p:管道文件
4. s:本地socket文件
5. l:链接文件
6. c:字符设备
7. b:块设备
相关说明
普通文件里面里面有内容
目录文件里面有内容
管道文件无内容, 它是内核维护的一段半双工发缓冲区管道,用于进程间通信
本地socket文件: 用于本地socket通信
链接文件: 分为软连接和硬链接, 软链接文件的内容就是一个路径, 硬链接其实就是将inode对象, 和内容拷贝了一份
字符设备: 无内容, 只是通过vfs抽象给应用层操作该设备的一个文件
块设备: 无内容和字符设备文件同理。
假设有这样一个问题, 有人问你linux下的文件有哪几种, 这里有一个顺口溜: bcd-lsp, 对应的就是上面其中文件类型。
设备号马上就说。
二、Linux对设备的分类
Linux内核按驱动程序实现模型框架的不同,将设备分为三类:
1. 字符设备:按字节流形式进行数据读写的设备,一般情况下按顺序访问,数据量不大,一般不设缓存
2. 块设备:按整块进行数据读写的设备,最小的块大小为512字节(一个扇区),块的大小必须是扇区的整数倍,Linux系统的块大小一般为4096字节(4k),随机访问,设缓存以提高效率
3. 网络设备:针对网络数据收发的设备
这里有一个小补充: 我们都知道在类Unix系统中一切皆文件, 其实这句话应该打上引号, 因为网络设备没有看成文件。大家想想看是不是这个道理。
网卡的表现形式
三、驱动程序如何向应用层提供接口
大家思考这样一个问题, 不同的驱动程序难道要向上层都提供一套操作该设备的API吗, 这样的话应用层的开发人员可能会想死的。
那又是如何解决这个问题的呢?
通过VFS虚拟文件系统来做, 对应用层的API还是那一套open close read write, 这样就大大减轻了上层开发的压力和难度。
四、Linux中设备的划分
内核用设备号来区分同类里不同的设备,设备号是一个无符号32位整数,数据类型为dev_t,设备号分为两部分:
1. 主设备号:占高12位,用来表示驱动程序相同的一类设备
2. 次设备号:占低20位,用来表示一类设备中的具体哪一个设备
应用程序打开一个设备文件时,通过设备号来查找定位内核中管理的设备。
MKDEV宏用来将主设备号和次设备号组合成32位完整的设备号,用法:
dev_t devno;
int major = 251;//主设备号
int minor = 2;//次设备号
devno = MKDEV(major,minor);
MAJOR宏用来从32位设备号中分离出主设备号,用法:
dev_t devno = MKDEV(249,1);
int major = MAJOR(devno);
MINOR宏用来从32位设备号中分离出次设备号,用法:
dev_t devno = MKDEV(249,1);
int minor = MINOR(devno);
这些宏本质也就是简简单单的位操作吧了。
如果已知一个设备的主次设备号,应用层指定好设备文件名,那么可以用mknod命令在/dev目录创建代表这个设备的文件,即此后应用程序对此文件的操作就是对其代表的设备操作,mknod用法如下:
在应用程序中如果要创建设备可以调用系统调用函数mknod,其原型如下:
int mknod(const char *pathname,mode_t mode,dev_t dev);
pathname:带路径的设备文件名,无路径默认为当前目录,一般都创建在/dev下
mode:文件权限 位或 S_IFCHR/S_IFBLK
dev:32位设备号
返回值:成功为0,失败-1
五、设备号的申请和注销
字符驱动开发的第一步是通过模块的入口函数向内核添加本设备驱动的代码框架,主要完成:
1. 申请设备号
2. 定义、初始化、向内核添加代表本设备的结构体元素
int register_chrdev_region(dev_t from, unsigned count, const char *name)
功能:手动分配设备号,先验证设备号是否被占用,如果没有则申请占用该设备号
参数:
from:自己指定的设备号
count:申请的设备数量
name:/proc/devices文件中与该设备对应的名字,方便用户层查询主设备号
返回值:
成功为0,失败负数,绝对值为错误码
int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count, const char *name)
功能:动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号
参数:
dev:分配设备号成功后用来存放分配到的设备号
baseminior:起始的次设备号,一般为0, 次设备号
count:申请的设备数量
name:/proc/devices文件中与该设备对应的名字,方便用户层查询主次设备号
返回值:
成功为0,失败负数,绝对值为错误码
分配成功后在/proc/devices 可以查看到申请到主设备号和对应的设备名,mknod时参数可以参考查到的此设备信息
void unregister_chrdev_region(dev_t from, unsigned count)
功能:释放设备号
参数:
from:已成功分配的设备号将被释放
count:申请成功的设备数量
释放后/proc/devices文件对应的记录消失
六、申请设备并根据设备号创建设备文件演示
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#define DEV_NUM 1 /* 设备号数量 */
int major = 200; /* 静态的主次设备号 */
int minor = 0;
static int __init mychar_init(void) /* 模块入口函数 */
{
int ret;
dev_t num;
ret = register_chrdev_region(num, DEV_NUM, "mychar");
if(ret) {
ret = alloc_chrdev_region(&num, minor, DEV_NUM, "mychar");
if(ret) {
printk("alloc_chrdev_region failed");
}
major = MAJOR(num);
}
return 0;
}
static void __exit mychar_exit(void)
{
dev_t num = MKDEV(major, minor);
/* 将申请的设备号归还给内核 */
unregister_chrdev_region(num, DEV_NUM);
}
MODULE_LICENSE("GPL");
module_init(mychar_init);
module_exit(mychar_exit);
在/proc/devices文件中查看设备号, mknod创建设备文件。
七、字符设备驱动框架分析
struct inode
{
//....
dev_t i_rdev;//设备号
struct cdev *i_cdev;//如果是字符设备才有此成员,指向对应设备驱动程序中的加入系统的struct cdev对象
//....
}
1. 内核中每个该结构体对象对应着一个实际文件,一对一
2. open一个文件时如果内核中该文件对应的inode对象已存在则不再创建,不存在才创建
3. 内核中用此类型对象关联到对此文件的操作函数集(对设备而言就是关联到具体驱动代码)
struct file
{
//...
mode_t f_mode;//不同用户的操作权限,驱动一般不用
loff_t f_pos;//position 数据位置指示器,需要控制数据开始读写位置的设备有用, 使用指针描述实际上是不准确的。
unsigned int f_flags;//open时的第二个参数flags存放在此,驱动中常用
struct file_operations *f_op;//open时从struct inode中i_cdev的对应成员获得地址,驱动开发中用来协助理解工作原理,内核中使用
void *private_data;//本次打开文件的私有数据,驱动中常来在几个操作函数间传递共用数据
struct dentry *f_dentry;//驱动中一般不用,除非需要访问对应文件的inode,用法flip->f_dentry->d_inode
int refcnt;//引用计数,保存着该对象地址的位置个数,close时发现refcnt为0才会销毁该struct file对象
//...
};
1. open函数被调用成功一次,则创建一个该对象,因此可以认为一个该类型的对象对应一次指定文件的操作
2. open同一个文件多次,每次open都会创建一个该类型的对象
3. 文件描述符数组中存放的地址指向该类型的对象
4. 每个文件描述符都对应一个struct file对象的地址
对于一个字符设备来讲, 内核使用了cdev这样一个对象来描述
我在学习的时候, 有一个文件, 对于一个设备的文件操作方法的合集
有这样的对应关系, inode结构体和外存中文件一一对应, struct file *类型的数组和进程对应, 每打开一个文件又和一个struct file_operations对象一一对应。
画了一幅图:
最后, 当我们实现了设备的驱动程序之后, 我们在应用层调用的read write ioctl等和文件描述符相关的函数后, 该函数的系统调用会根据文件描述符去到文件描述符数组, 根据下标值, 找到对应的struct file对象, 接着通过struct file对象里面的struct file_operations指针找到相应发设备驱动程序。
最后提一句, 文件描述符其实本质是数组的下标, 那这个数组是什么类型的呢, 类型是struct file *, 文件操作引擎指针类型的数组。