Linux实践:一步一步编写字符设备驱动程序

道哥分享

    作  者:道哥,10+年嵌入式开发老兵,专注于:C/C++、嵌入式、Linux。
    目录
    API 函数
    编写驱动程序
    编写应用程序
    卸载驱动模块
    小结
    别人的经验,我们的阶梯!
    大家好,我是道哥,今天我们继续讨论: Linux 中字符设备的驱动程序。
    在上一篇文章中Linux驱动实践:你知道【字符设备驱动程序】的两种写法吗?我们说过:字符设备的驱动程序,有两套不同的API函数,并且在文中详细演示了利用旧的API函数来编写驱动程序。
    这篇文章,我们继续这个话题,实际演示一下:字符设备驱动程序的另一套API函数的使用方法。
    API 函数
    这里主要关注下面这 3 个函数:
    // 静态注册设备
    int register_chrdev_region(dev_t from, unsigned count, const char *name);
    // 动态注册设备
    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
    // 卸载设备
    void unregister_chrdev_region(dev_t from, unsigned count);
    关于静态和动态注册,主要的区别就在于:主设备号由谁来主导分配!
    静态注册:由我们的驱动程序来指定主设备号,即参数1:from;
    动态注册:由操作系统来分配,驱动程序提供一个变量来接收该设备号,即参数1: dev 指针;
    另外,在Linux 2.6后期的内核版本中,引入了 cdev 结构来描述一个字符设备,它的结构体成员是:
    
    与这个结构体相关的处理函数有:
    void cdev_init(struct cdev *,struct file_operations *);
    初始化 cdev 的成员,主要是设置 file_operations。
    strcut cdev *cdev_alloc(void);
    动态申请 cdev 内存。
    void cdev_put(strcut cdev *p);
    与 count 计数相关的操作。
    int cdev_add(struct cdev *,dev_t ,unsigned );
    向系统中添加一个 cdev,注册字符设备,需要在驱动被加载的时候调用。
    void cdev_del(struct cdev *);
    从系统中删除一个 cdev,注销字符设备,需要在驱动被卸载的时候调用。
    后面在代码演示的时候,可以看到cdev结构是如何被使用的。
    编写驱动
    按照惯例,我们仍然按照步骤,来讨论如何利用上述的APIs,来手写一个字符设备的驱动程序。
    以下所有操作的工作目录,都是与上一篇文章相同的,即:~/tmp/linux-4.15/drivers/。
    创建驱动目录和驱动程序
    $ cd linux-4.15/drivers/
    $ mkdir my_driver2
    $ cd my_driver2
    $ touch driver2.c
    driver2.c 文件的内容如下(不需要手敲,文末有代码下载链接):
    
    
    这里看一下加载驱动模块时调用的 driver2_init( ) 函数,其中的 cdev_init 用来把cdev结构体与 file_operations 发生关联。
    在调用 alloc_chrdev_region( ) 时,操作系统分配了主设备号,并且保存在 dev_no 变量中,然后 cdev_add() 再把设备号与cdev结构体进行关联。
    创建 Makefile 文件
    $ touch Makefile
    内容如下:
    
    编译驱动模块$ make
    得到驱动程序: driver2.ko 。
    加载驱动模块
    在加载驱动模块之前,先来检查一下系统中,几个与驱动设备相关的地方。
    先看一下 /dev 目录下,目前还没有我们的设备节点( /dev/driver2 )。
    $ ll /dev/driver2
    ls: cannot access '/dev/driver2': No such file or directory
    再来查看一下 /proc/devices 目录下,也没有 driver2 设备的设备号。
    $ cat /proc/devices
    
    /proc/devices 文件: 列出字符和块设备的主设备号,以及分配到这些设备号的设备名称。
    为了方便查看打印信息,把dmesg输出信息清理一下:
    $ sudo dmesg -c
    执行如下指令,加载驱动模块:
    $ sudo insmod driver2.ko
    当驱动程序被加载的时候,通过 module_init( ) 注册的函数 driver2_init() 将会被执行,那么其中的打印信息就会输出。
    还是通过 dmesg 指令来查看驱动模块的打印信息:
    $ dmesg
    
    此时,驱动模块已经被加载了!
    来查看一下 /proc/devices 目录下显示的设备号:
    $ cat /proc/devices
    
    设备已经注册了,主设备号是: 244 。
    但是,此时在/dev目录下,还没有我们需要的设备节点。
    在上一篇文章中介绍过,还可以利用 Linux 用户态的 udev 服务来自动创建设备节点。
    现在,我们手动创建设备节点:
    $ sudo mknod -m 660 /dev/driver2 c 244 0
    主设备号 244 是从 /proc/devices 查到的。
    检查一下是否创建成功:
    $ ll /dev/driver2
    
    现在,设备的驱动程序已经加载了,设备节点也被创建好了,应用程序就可以来操作(读、写)这个设备了。
    应用程序
    应用程序仍然放在 ~/tmp/App/ 目录下。
    $ mkdir ~/tmp/App/app_driver2
    $ cd ~/tmp/App/app_driver2
    $ touch app_driver2.c
    文件内容如下:
    
    接下来就是编译和测试了:
    $ gcc app_driver2.c -o app_driver2
    $
    $ sudo ./app_driver2
    [sudo] password for xxx: <输入用户密码>
    read ret = 0
    write ret = 0
    从返回值来看,成功打开了设备,并且调用读函数、写函数都成功了!
    继续用dmesg命令查看一下:
    
    卸载驱动模块
    卸载指令:
    $ sudo rmmod driver2
    此时,/proc/devices 下主设备号 244 的 driver2 已经不存在了。
    
    再来看一下 dmesg的打印信息:
    
    可以看到:驱动程序中的 driver2_exit( ) 被调用执行了!
    小结
    以上就是利用“新的” API 函数,来编写字符设备的驱动程序。
    代码结构还是非常清晰的,这得益于Linux良好的驱动程序架构设计!这也是每一名架构师需要学习、努力模仿的地方。
    文中的测试代码,已经放在网盘了。