对字符设备的访问是通过文件系统内的设备名称来进行的。那些名称被成为特殊文件、设备文件或者称之为文件系统树的节点,通常位于/dev目录下面。
crw--w---- 1 root tty 4, 0 7月 17 15:19 tty0 字符设备
brw-rw---- 1 root disk 7, 5 7月 17 15:19 loop5 块设备
其中4和7表示的主设备号,0和5表示的次设备号。
通常而言,主设备号标识设备对应的驱动程序。次设备号由内核使用,用于正确确定设备文件所指的设备。例如有5个led灯,可以用同一个驱动程序来控制,所以主设备号都是相同的,假设为100,但是为了区分5个led所以次设备号就分别为0~4。
crw--w---- 1 root root 100, 0 7月 17 15:19 led0
crw--w---- 1 root root 100, 1 7月 17 15:19 led1
crw--w---- 1 root root 100, 2 7月 17 15:19 led2
字符设备号是无符号整型值,高12位为主设备号,低20位为次设备号。
查看当前系统中的设备号指令:cat /proc/devices
,其中Character devices下所述的就是当前所被占用的字符设备号。
设备号的操作宏如下:
MAJOR(dev_t) // 提取主设备号
MINOR(dev_t) // 提取次设备号
MKDEV(major, minor) // 根据主次设备号,构建dev_t类型的设备号
注意: 主设备号,不能冲突,可以通过cat /proc/devices
,找一个没有被占用的设备号。
分别使用如下函数进行:
int register_chrdev_region(dev_t from, unsigned int cout, const char* name); // 注册
int unregister_chrdev_region(dev_from, unsigned int count); // 注销
示例
#include
#include
#include
#include
static unsigned int major = 222;
static unsigned int minor = 0;
static dev_t dev_no;
static int hello_init(void) {
int result = 0;
unsigned int dev_major, dev_minor;
dev_no = MKDEV(major, minor);
printk(KERN_EMERG "dev_no: %d\n", dev_no);
dev_major = MAJOR(dev_no);
dev_minor = MINOR(dev_no);
printk(KERN_EMERG "major: %d, minor: %d", dev_major, dev_minor);
result = register_chrdev_region(dev_no, 1, "hello");
if (result < 0) {
printk(KERN_EMERG "register chrdev failed! result: %d\n", result);
return result;
}
return 0;
}
static void hello_exit(void) {
unregister_chrdev_region(dev_no, 1);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
通过LOG和/proc/devices
能看到注册设备号成功,当销毁的时候,/proc/devices
中将删除222的记录。
内核态中的基本结构如下,我们需要实现的部分,其实是struct cdev和struct file_operations这两个结构。
相关结构体的释义
file_operations、file、inode结构释义
内核内部使用struct cdev结构来表示字符设备,所以我们需要利用该结构来完成字符设备的注册。具体操作函数如下:
static struct cdev cdev; // cdev定义
void cdev_init(struct cdev *cdev, struct file_operations *fops); // cdev初始化一个所有者
int cdev_add(struct cdev *dev, dev_t num, unsigned int count); // 告诉内核该cdev结构的信息
void cdev_del(struct cdev *dev); // 移除
编写代码的步骤:
示例
#include
#include
#include
#include
#include
static unsigned int major = 222;
static unsigned int minor = 0;
static dev_t dev_no;
static struct cdev cdev;
static int hello_open(struct inode* inode, struct file* file) {
printk("hello_open\n");
return 0;
}
static int hello_close(struct inode* inode, struct file* file) {
printk("hello_close\n");
return 0;
}
static struct file_operations hello_ops = {
.open = hello_open,
.release = hello_close,
};
static int hello_init(void) {
int result = 0;
dev_no = MKDEV(major, minor);
printk(KERN_EMERG "dev_no: %d\n", dev_no);
result = register_chrdev_region(dev_no, 1, "hello");
if (result < 0) {
printk(KERN_EMERG "register chrdev failed! result: %d\n", result);
return result;
}
cdev_init(&cdev, &hello_ops);
result = cdev_add(&cdev, dev_no, 1);
if (result < 0) {
printk("cdev add failed! result: %d\n", result);
return result;
}
return 0;
}
static void hello_exit(void) {
cdev_del(&cdev);
unregister_chrdev_region(dev_no, 1);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
使用mknod命令来创建设备节点,一般都是创建在/dev目录下,具体用法如下:
mknod /dev/节点名称 -c 主设备号 次设备号
c表示的是字符设备
配合我们上述的字符设备注册代码,所以我们创建设备节点的命令为:
sudo mknod /dev/hello c 222 0
命令执行完成之后,可以去/dev目录下查看,会看到hello这个文件,后续应用程序将通过该节点文件操作
通过创建一个应用层程序来测试设备,方式与直接操作文件一样,因为linux下一切皆是文件。测试文件如下:
#include
#include
#include
#include
int main(int argc, char const *argv[]) {
int fd = open("/dev/hello", O_RDWR);
close(fd);
return 0;
}
使用的open和close,对应在内核中将调用到hello_open和hello_close,打印LOG如下
/*
* major:主设备号,如果传的是0的话,那么将自动分配一个主设备号,并且通过返回值返回
* name:驱动名称(出现在/proc/devices)
* fops:file_operations结构
* return 0表示成功,如果失败则是一个负值
*/
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops); // 申请
/*
* major:主设备号,保持和register_chrdev一致
* name:驱动名称, 保持和register_chrdev一致
*/
int unregister_chrdev(unsigned int major, const char *name); // 释放
示例
#include
#include
#include
#include
#include
static unsigned int major = 222;
static int hello_open(struct inode* inode, struct file* file) {
printk("hello_open 2\n");
return 0;
}
static int hello_close(struct inode* inode, struct file* file) {
printk("hello_close 2\n");
return 0;
}
static struct file_operations hello_ops = {
.open = hello_open,
.release = hello_close,
};
static int hello_init(void) {
int result = 0;
result = register_chrdev(major, "hello", &hello_ops);
if (result < 0) {
printk("register chrdev failed! result: %d\n", result);
return result;
}
return 0;
}
static void hello_exit(void) {
unregister_chrdev(major, "hello");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
mknod命令的方式是手动创建,是静态的,是不管设备是否真的存在的。
动态创建的方式是采用udev的方法,是有被内核检测到的设备才会为其创建设备节点。流程大致:设备插入 > 加载驱动模块 > 在sysfs上注册设备数据 > udev为设备创建设备节点。
动态创建节点所用到的函数如下:
/* 创建一个类
*
* owner: 一般都是THIS_MODULE
* name: class的名称,名称会显示在/sys/class目录下
* return: struct class* 通过IS_ERR(cls)来判断是否失败,成功IS_ERR()返回0,失败通过PTR_ERR(cls)来获取错误码
*/
#define class_create(owner, name)
/* 注销类 */
void class_destroy(struct class* cls);
/* 注册设备
*
* cls: 待创建的设备所属的类,即class_create返回的指针
* parent: 待创建设备的父设备,没有为NULL
* devt: 设备号
* drvdata: 给到设备的参数,回调函数,没有为NULL
* fmt: 可变参数,与Printf的用法类似
*/
struct device *device_create(struct class* cls, struct device* parent, dev_t devt, void* drvdata, const char* fmt, ...);
/* 注销设备 */
void device_destroy(struct class* cls, dev_t devt);
示例
#include
#include
#include
#include
#include
static unsigned int major = 222;
static unsigned int minor = 0;
static dev_t dev_no;
static struct class* cls = NULL;
static struct device* cls_dev = NULL;
static int hello_open(struct inode* inode, struct file* file) {
printk("hello_open 3\n");
return 0;
}
static int hello_close(struct inode* inode, struct file* file) {
printk("hello_close 3\n");
return 0;
}
static struct file_operations hello_ops = {
.open = hello_open,
.release = hello_close,
};
static int hello_init(void) {
int result = 0;
printk("hello init enter!");
dev_no = MKDEV(major, minor);
result = register_chrdev(major, "hello", &hello_ops);
if (result < 0) {
printk("register chrdev failed! result: %d\n", result);
return result;
}
cls = class_create(THIS_MODULE, "hello_cls");
if (IS_ERR(cls) != 0) {
printk("class create failed!");
result = PTR_ERR(cls);
goto err_1;
}
cls_dev = device_create(cls, NULL, dev_no, NULL, "hello_dev");
if (IS_ERR(cls_dev) != 0) {
printk("device create failed!");
result = PTR_ERR(cls_dev);
goto err_2;
}
return 0;
err_2:
class_destroy(cls);
err_1:
unregister_chrdev(major, "hello");
return result;
}
static void hello_exit(void) {
printk("hello exit enter!");
device_destroy(cls, dev_no);
class_destroy(cls);
unregister_chrdev(major, "hello");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
class_create
执行成功之后,会在/sys/class目录下创建hello_cls目录
device_create
执行之后会在/sys/devices的目录下创建一个hello_dev目录,在/sys/class目录下会生成一个指向hello_dev的链接文件
hello_dev目录下内容如下:
在dev文件中保存的是主次设备号,uevent中保存了主次设备号和设备名称。
在/dev目录下,也有创建成功hello_dev节点,不过默认权限仅root用户才可以操作,与mknod有差异。
应用层测试程序可以直接使用之前的,更换设备节点名称为/dev/hello_dev即可,不过因为权限问题,需要用root权限来执行,否则open失败。
当卸载hello模块的时候,/dev/hello_dev节点会被自动删除。