美文网首页
简单的字符设备驱动

简单的字符设备驱动

作者: 付凯强 | 来源:发表于2022-07-05 23:44 被阅读0次

    日常生活中使用Linux的设备有很多。对操作系统来说,可以抽象为三大类型,分别是字符设备、块设备、网络设备。简单说,字符设备是以字节为单位进行I/O传输的设备,常见的字符设备有鼠标、键盘、触摸屏等。块设备是以块为单位进行I/O传输的设备,常见的快设备是磁盘。网络设备是一类比较特殊的设备,涉及网络协议层,单独把它们分为一类设备。
    想要学习Linux内核,最好从学习字符设备驱动开始。而为了写好字符设备驱动,需要从以下几个方面入手。
    第一,学习Linux内核字符设备驱动的架构,包括字符设备驱动是如何组织的,应用程序是如何与驱动交互的。
    第二,学习Linux内核字符设备驱动相关的API,包括字符设备相关的基础知识,如字符设备的描述、设备号的管理、file_operations的实现、ioctl交互的设计和Linux设备模型的管理等。
    第三,学习Linux内核内存管理相关的API,包括设备的数据如何与用户程序交互,如设备的内存如何映射到用户空间。
    第四,学习Linux内核管理中断相关的API,包括中断管理相关的接口函数,如注册中断、编写中断等。
    第五,学习Linux内核中同步和锁等相关的API。Linux是多进程、多用户的操作系统,支持内核抢占,需处理同步、异步、竞争等问题。
    第六,学习编写驱动所需的芯片原理。设备驱动用于运行设备,需要认真研究设备的数据手册。

    一个简单的字符设备

    char_device.c

    #include <linux/init.h>
    #include <linux/module.h>
    #include <linux/cdev.h>
    #include <linux/uaccess.h>
    #include <linux/fs.h>
    
    #define DEMO_NAME "char_device"
    static dev_t dev;
    static struct cdev *char_device;
    static signed count = 1;
    
    static int char_device_open(struct inode *inode, struct file *file)
    {
        int major = MAJOR(inode->i_rdev);
        int minor = MINOR(inode->i_rdev);
        printk("%s: major=%d, minor=%d\n", __func__, major, minor);
        return 0;
    }
    
    static ssize_t char_device_read(struct file *file, char __user *buf,size_t lbuf, loff_t *ppos)
    {
        printk("%s enter\n",__func__);
        return 0;
    }
    
    static int char_device_release(struct inode *inode, struct file *file)
    {
        return 0;
    }
    
    static ssize_t char_device_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos)
    {
        printk("%s enter\n",__func__);
        return 0;
    }
    
    static const struct file_operations char_device_fops = {
        .owner = THIS_MODULE,
        .open = char_device_open,
        .release = char_device_release,
        .read = char_device_read,
        .write = char_device_write
    };
    
    static int __init simple_char_device_init(void)
    {
        int ret;
        ret = alloc_chrdev_region(&dev, 0, count, DEMO_NAME);
        if (ret) {
            printk("failed to allocate char device region");
            return ret;
        }
        char_device = cdev_alloc();
        if (!char_device) {
            printk("cdev_alloc fialed\n");
            goto unregister_chrdev;
        }
        cdev_init(char_device, &char_device_fops);
        ret = cdev_add(char_device, dev, count);
        if (ret) {
            printk("cdev_add failed\n");
            goto cdev_fail;
        }
        printk("succeeded register char device: %s\n", DEMO_NAME); 
        printk("Major number = %d,minor number = %d\n", MAJOR(dev), MINOR(dev));
        return 0;
    
        cdev_fail:
        cdev_del(char_device);
        unregister_chrdev:
        unregister_chrdev_region(dev, count);
        return ret;
    }
    
    static void __exit simple_char_device_exit(void) {
        printk("removing device\n");
        if (char_device)
            cdev_del(char_device);
        unregister_chrdev_region(dev, count);
    }
    
    module_init(simple_char_device_init);
    module_exit(simple_char_device_exit);
    
    MODULE_AUTHOR("fkq");
    MODULE_LICENSE("GPL v2");
    MODULE_DESCRIPTION("simple character device");
    

    Makefile

    KVERS = $(shell uname -r)
    
    obj-m := char_device.o
    
    all:
            $(MAKE) -C /lib/modules/$(KVERS)/build M=$(PWD) modules
    
    clean:
            $(MAKE) -C /lib/modules/$(KVERS)/build M=$(PWD) clean
            rm -rf *.ko;
    

    执行make命令进行编译

    { fukaiqiang@ubuntu /home/fukaiqiang/linux/char_device }
    $make
    make -C /lib/modules/5.13.0-44-generic/build M=/home/fukaiqiang/linux/char_device modules
    make[1]: 进入目录“/usr/src/linux-headers-5.13.0-44-generic”
      CC [M]  /home/fukaiqiang/linux/char_device/char_device.o
      MODPOST /home/fukaiqiang/linux/char_device/Module.symvers
      CC [M]  /home/fukaiqiang/linux/char_device/char_device.mod.o
      LD [M]  /home/fukaiqiang/linux/char_device/char_device.ko
      BTF [M] /home/fukaiqiang/linux/char_device/char_device.ko
    Skipping BTF generation for /home/fukaiqiang/linux/char_device/char_device.ko due to unavailability of vmlinux
    make[1]: 离开目录“/usr/src/linux-headers-5.13.0-44-generic”
    

    会在当前目录下生成char_device.ko内核模块文件。然后通过insmod命令加载char_device.ko内核模块。

    $sudo insmod char_device.ko
    

    接着使用dmesg命令来查看内核日志

    [ 2842.706783] succeeded register char device: char_device
    [ 2842.706785] Major number = 234,minor number = 0
    

    以上两句内核日志,正好是在char_device.c中书写的:系统为这个设备分配的主设备号是234,分配的次设备号是0.查看/proc/devices这个proc虚拟文件系统的devices节点信息,可以看到生成了char_device为名称的设备,主设备号是234.

    $cat /proc/devices 
    Character devices:
    ...
    234 char_device
    ...
    

    生成的设备都需要在/dev/目录下生成对应的节点,这里只能手动生成。

    $sudo mknod /dev/char_device c 234 0
    

    通过ls命令查看下节点信息

    $ls -al /dev/
    ...
    crw-r--r--   1 root       root    234,   0 6月  26 20:37 char_device
    ...
    

    编写测试程序

    char_device_test.c

    #include <stdio.h>
    #include <fcntl.h>
    #include <unistd.h>
    
    #define DEMO_DEV_NAME "/dev/char_device"
    
    int main() {
        char buffer[64];
        int fd;
    
        fd = open(DEMO_DEV_NAME, O_RDONLY);
        if (fd < 0) {
            printf("open device %s failed\n", DEMO_DEV_NAME);
            return -1;
        }
    
        read(fd, buffer, 64);
        close(fd);
        return 0;
    }
    

    编译测试程序,生成可执行文件

    gcc char_device_test.c -o char_device_test
    

    执行可执行文件,查看内核日志进行查看。

    [ 8071.270374] char_device_open: major=234, minor=0
    [ 8071.270382] char_device_read enter
    

    到此时,测试程序已经成功访问了内核模块char_device。

    详解

    通过以上示例,你会发现创建一个字符设备驱动,是有步骤可循的。下面就来总结下。

    1. 分配设备号

    linux提供了两个接口函数完成设备号的申请。设备号分别主设备号和次设备号,主设备号只能有一个,次设备号可以有多个。

    int register_chrdev_region(dev_t from, unsigned count, const char *name)
    

    register_chrdev_region需要指定主设备号看,可以连续分配多个。内核文档documentation/devices.txt描述了系统中已经分配的主设备号,假如使用此函数指定主设备号,应该避免使用系统已占用的设备号。

    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
    

    alloc_chrdev_region自动分配一个主设备号,避免和系统已占用的设备号发生冲突。
    假如想释放设备号,可以调用以下函数:

    void unregister_chrdev_region(dev_t from, unsigned count)
    

    设备号的抽象为dev_t,定义在include/linux/types.h中:

    typedef __u32 __kernel_dev_t;
    typedef __kernel_dev_t dev_t;
    typedef unsigned int __u32;
    

    所以,dev_t其实就是unsigned int类型,是一个32位的数据类型。这32位数据构成了主设备号和次设备号两部分,其中高12位为主设备号,低20位为次设备号。因此Linux系统中主设备号范围为0~4095,所以大家在选择主设备号的时候一定不要超过这个范围。在文件include/linux/kdev_t.h中提供了几个关于设备号的宏:

    #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))
    

    宏MINORBITS表示次设备号位数,一共是20位。
    宏MINORMASK表示次设备号掩码。
    宏MAJOR用于从devt中获取主设备号,将devt右移20位即可。
    宏MINOR用于从dev_t中获取次设备号,取dev_t的低20位的值即可。
    宏MKDEV用于将给定的主设备号和次设备号的值组合成dev_t类型的设备号。

    1. 抽象字符设备

    字符设备需要用一个数据结构来进行抽象和描述,在linux里它就是cdev数据结构。

    struct cdev {
            struct kobject kobj;
            struct module *owner;
            const struct file_operations *ops;
            struct list_head list;
            dev_t dev;
            unsigned int count;
    };
    

    kojb: 用于linux设备驱动模型。
    owner:字符设备驱动所在的内核模块对象指针。
    ops:字符设备驱动中最关键的一个操作函数,在和应用程序交互过程中起枢纽作用。
    list:用来将字符设备串成一个链表。
    dev: 字符设备的设备号,由主设备号和次设备号组成。
    count:同属于主设备号的次设备号的个数。
    数据结构有了之后,还需创建,有两种方式,一种是使用全局静态变量,一种是使用内核提供的cdev_alloc接口函数。

    static struct cdev device_char_cdev;
    或者
    struct char_cdev = cdev_alloc();
    

    创建完成之后,还需要建立设备与驱动操作方法集file_operations之间的连接关系,函数为cdev_init。

    void cdev_init(struct cdev *cdev, struct file_operations *fops)
    

    除此之外,还需要把创建的字符设备添加到系统中,函数为

    int cdev_add(struct cdev *p, dev_t dev, unsigned count)
    

    p 表示设备的cdev数据结构,dev 表示设备的设备号,count 表示主设备号可以有多少个次设备号。
    如果想从系统中删除此设备,可以使用以下函数

    void cdev_del(struct cdev *p)
    
    1. 创建字符设备操作方法集
    static const struct file_operations char_device_fops = {
        .owner = THIS_MODULE,
        .open = char_device_open,
        .release = char_device_release,
        .read = char_device_read,
        .write = char_device_write
    };
    

    这里创建了类型为file_operations的字符设备操作方法集,里面是多个函数指针。这个方法集通过cdev_init方法和设备建立连接关系,当用户空间的程序通过open打开设备的时候,就会执行char_device_open方法。

    fd = open(DEMO_DEV_NAME, O_RDONLY);
    

    open方法的第一个参数是设备文件名,第二个参数用来指定文件打开的属性。若open方法执行成功,会返回一个文件描述符,否则返回-1。
    以下是常用的一些file_operations接口。

    <include/linux/fs.h>
    struct file_operations {
        struct module *owner;
        loff_t (*llseek) (struct file *, loff_t, int);
        ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
        ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
        ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
        ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long,loff_t);
        ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
        ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
        int (*iterate) (struct file *, struct dir_context *);
        unsigned int (*poll) (struct file *, struct poll_table_struct *);
        long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
        long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
        int (*mmap) (struct file *, struct vm_area_struct *);
        int (*mremap)(struct file *, struct vm_area_struct *);
        int (*open) (struct inode *, struct file *);
        int (*flush) (struct file *, fl_owner_t id);
        int (*release) (struct inode *, struct file *);
        int (*fsync) (struct file *, loff_t, loff_t, int datasync);
        int (*aio_fsync) (struct kiocb *, int datasync);
        int (*fasync) (int, struct file *, int);
        int (*lock) (struct file *, int, struct file_lock *);
        ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
        unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
        int (*check_flags)(int);
        int (*flock) (struct file *, int, struct file_lock *);
        ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t,unsigned int);
        ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t,unsigned int);
        int (*setlease)(struct file *, long, struct file_lock **, void **);
        long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
        void (*show_fdinfo)(struct seq_file *m, struct file *f);
    

    open: 打开设备
    release:关闭设备
    llseek:修改文件的当前读写位置,并返回新位置。
    read:从设备驱动中读取数据到用户空间,并返回成功读取的字节数。若为负数,则说明读取失败。
    write: 把用户空间的数据写入设备,并返回成功写入的字节数。
    poll:方法用来查询设备是否可以立即读写。
    unlocked_ioctl和compat_ioctl:提供与设备相关的控制命令的实现。
    mmap:将设备内存映射到进程的虚拟地址空间中。
    aio_read和aio_write:异步IO读写方法。
    fsync:实现了一种称为异步通知的特性。

    1. 为设备添加节点

    设备要想获取上层的访问,必须创建节点,所谓节点即设备文件,它是连接内核空间驱动和用户空间应用程序的桥梁。节点都存放于/dev/目录中。

    drwxr-xr-x   2 root       root        4040 7月   5 23:17 char/
    crw--w----   1 root       tty       5,   1 7月   5 23:17 console
    

    第一列中的d代表块设备,c代表字符设备。字符设备这里显示主设备号和次设备号。主设备号代表一类设备,次设备号代表同一个类的不同个体,每个次设备号都有一个不同的设备节点。
    节点的生成有两种方式,一种是使用mknod命令手动生成,一种是使用udev机制动态生成。这里主要介绍第一种。

    sudo mknod /dev/char_device c 234 0
    

    参考

    1. 奔跑吧内核 入门篇第2版 第六章 字符设备驱动详解
    2. https://blog.csdn.net/weixin_44502943/article/details/121427675

    相关文章

      网友评论

          本文标题:简单的字符设备驱动

          本文链接:https://www.haomeiwen.com/subject/aygbhctx.html