在了解Linux字符设备先了解一下Linux设备的分类。
Linux设备分类
Linux设备主要分为字符设备、块设备、网络设备。
字符设备:能够像字节流一样被访问且没有缓冲是按顺序访问的设备,当对字符设备发出读写请求,相应的IO操作立即发生。Linux系统中很多设备都是字符设备,绝大多数的设备都是字符设备。比如LED、按键、键盘、串口、传感器、LCD。字符设备驱动通过字符设备文件来访问。
块设备:按照数据块来访问且有缓冲是具有随机访问能力的设备,比如访问硬盘就不是一个字节一个字节的访问,而是直接访问数据块,数据块的大小是固定的但是不同的系统不一样。比如内存、磁盘、SD卡、U盘。块设备驱动通过块设备文件来访问。
网络设备:网络设备由Linux的网络子系统驱动,负责数据包的发送和接收,而不是面向流设备,因此在Linux系统文件系统中网络设备没有节点。访问网络设备不通过文件,通过套接字(网络通信地址)访问。
本文重点讲字符设备。
字符设备
驱动是沟通硬件和上层应用的媒介,字符设备驱动通过字符设备文件来访问,访问设备文件使用文件IO,在用户层访问设备文件和普通文件的方法是没有区别。
那如何找到设备文件相对应的驱动呢?
Linux中所有的设备文件都在/dev目录下,而通过设备号就可以将设备文件和驱动联系起来。
设备号
设备号分为主设备号和次设备号。其中主设备号是用来区分不同类别的设备,而次设备号用来区分一类设备中的不同个体。例如输入设备和输出设备是不同的主设备号,但是输入设备不只一种,像鼠标和键盘都是输入设备就要使用次设备号来区分。
可以在/proc/devices文件中查询哪些主设备号以及被使用了。
字符设备:
块设备:
设备号数据类型:dev_t原型是一个32位无符号整形类型的值(unsigned int ),其中高12位表示主设备号,低20位表示次设备号。
内核中提供了操作设备号的宏
MAJOR(设备号);//通过设备号获取主设备号
MINOR(设备号);//通过设备号获取次设备号
MKDEV(主设备号,次设备号);//通过主设备号和次设备号构造设备号
一起看看宏的原型
#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))
获取主设备号的宏MAJOR是通过将设备号右移20位留下高12位的主设备号。
获取次设备号的宏MINOR是通过将1左移20位(1U表示无符号整型1,C语言默认常数是有符号整型)再减一就变成了前12位为0,后20位为1了。然后与上设备号就获得了低20位的次设备号。
获取设备号的宏MKDEV是通过将主设备左移20位再或上次设备号就得到了完整的设备号。
设备号在内存中属于资源(虽然有很多,估计也用不完,但毕竟是有限的且不同共用),所以需要使用设备号就要向系统申请。
申请设备号
首先需要加上两个头文件。
#include <linux/cdev.h>
#include <linux/fs.h>
系统提供了两种申请方法:静态申请和动态申请。
静态申请
- 选择一个内核中未被使用的主设备号(在/proc/devices中查看)。
- 根据设备个数分配次设备号,一般从0开始。
- 使用宏构造完整的设备号。
- 调用函数register_chrdev_region向系统申请。
- 不再使用设备号需要注销,通过函数unregister_chrdev_region实现注销。
int register_chrdev_region(dev_t from, unsigned count, const char *name);
参数:
from - 要申请的起始设备号
count - 设备号个数
name - 设备号在内核中对应的名称
返回0表示成功,返回非0表示失败
void unregister_chrdev_region(dev_t from, unsigned count);
参数:
from - 要注销的起始设备号
count - 设备号个数
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
//主设备号
#define CDD_MAJOR 220
//起始次设备号
#define CDD_MINOR 0
//设备号个数
#define CDD_COUNT 1
//设备号
dev_t dev;
//加载函数
int cdd_init(void)
{
int ret;
//构造设备号
dev = MKDEV(CDD_MAJOR, CDD_MINOR);
// 1.静态申请设备号
ret = register_chrdev_region(dev, CDD_COUNT, "cdd_demo");
if(ret<0){
printk("register_chrdev_region failed!\n");
return ret;
}
printk("register_chrdev_region success!\n");
return 0;
}
//卸载函数
void cdd_exit(void)
{
//注销设备号
unregister_chrdev_region(dev, CDD_COUNT);
}
//声明为模块的入口和出口
module_init(cdd_init);
module_exit(cdd_exit);
MODULE_LICENSE("GPL");//GPL模块许可证
动态申请
就是向系统申请一个设备号,系统自动分配一个没有使用过的设备号。
使用函数alloc_chrdev_region向系统申请。
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name);
参数:
dev - 设备号的地址
baseminor - 起始次设备号
count - 设备号个数
name - 设备号在内核中对应的名称
返回0表示成功,返回非0表示失败
注销使用的也是函数register_chrdev_region。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
//起始次设备号
#define CDD_MINOR 0
//设备号个数
#define CDD_COUNT 1
//设备号
dev_t dev;
//加载函数
int cdd_init(void)
{
int ret;
// 2.动态态申请设备号
ret = alloc_chrdev_region(&dev, CDD_MINOR, CDD_COUNT, "cdd_demo");
if(ret<0){
printk("register_chrdev_region failed!\n");
return ret;
}
printk("register_chrdev_region success!\n");
printk("major number:%d\n",MAJOR(dev));
return 0;
}
//卸载函数
void cdd_exit(void)
{
//注销设备号
unregister_chrdev_region(dev, CDD_COUNT);
}
//声明为模块的入口和出口
module_init(cdd_init);
module_exit(cdd_exit);
MODULE_LICENSE("GPL");//GPL模块许可证
好了,最后如果有什么说的不对的地方欢迎在评论区指正。