【3】如何创建字符设备

字符设备号

对字符设备的访问是通过文件系统内的设备名称来进行的。那些名称被成为特殊文件、设备文件或者称之为文件系统树的节点,通常位于/dev目录下面。

crw--w----  1 root tty       4,   0 717 15:19 tty0 字符设备
brw-rw----  1 root disk      7,   5 717 15:19 loop5 块设备

其中4和7表示的主设备号,0和5表示的次设备号。

通常而言,主设备号标识设备对应的驱动程序。次设备号由内核使用,用于正确确定设备文件所指的设备。例如有5个led灯,可以用同一个驱动程序来控制,所以主设备号都是相同的,假设为100,但是为了区分5个led所以次设备号就分别为0~4。

crw--w----  1 root root       100,   0 717 15:19 led0 
crw--w----  1 root root       100,   1 717 15:19 led1 
crw--w----  1 root root       100,   2 717 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 <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>

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); // 移除

编写代码的步骤:

  1. 实现module_init/module_exit框架
  2. 实现struct file_operations结构
  3. 创建设备号
  4. 注册字符设备
  5. 填充module_init/module_exit函数

示例

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>

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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

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 <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>

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 <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>

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节点会被自动删除。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值