linux字符设备概念

1、啥是设备号,用来干嘛的

2、file_operations结构体的作用

3、cdev是啥

上面这三个应该就是字符设备驱动的关键了,把它们弄熟了才能更好的编写字符设备驱动

概念理解

1、字符设备驱动框架

字符设备是Linux三大设备之一,另外两种我就不提了,字符设备就是字节流形式通讯的I/O设备,绝大部分设备都是字符设备,所以学好它是我们嵌入式开发人员势在必行的。

常见的字符设备包括鼠标、键盘、显示器、串口等,用ls -l /dev指令可以查看很多设备文件,c就是字符设备, 后面的数值就是主设备号和次设备号,这个特别重要!

2、啥是设备号,用来干嘛的?

设备号又分为主设备号和次设备号

主设备号用来区分不同硬件设备类型,如串口和USB之间的区别,表示对应的驱动程序,也就是说一个主设备号对应一个驱动程序。

次设备号用来区分同一类型的多个设备,如串口1和串口2之间的区别,由内核使用,用于确定/dev下的设备文件对应的具体设备。

作用:有句话应该大家都知道,linux下兼文件,各种设备都以文件的形式存放在/dev目录下,称为设备文件

应用程序可以打开、关闭和读写这些设备文件,完成对设备的操作,就像操作普通的数据文件一样。为了管理这些设备,系统为设备编了
号,所以每个设备号就分为了主设备号和次设备号。

其中与设备号相关的比较重要的三个宏【代码中尽量使用宏,减少系统兼容性问题,不同系统主次设备号位数可能不同】

#define MAJOR(dev)        ((dev)>>8)
#define MINOR(dev)        ((dev) & 0xff)
#define MKDEV(ma,mi)        ((ma)<<8 | (mi))

3、file_operations结构体的作用

file_operations结构体是字符设备驱动与内核的接口,是用户空间对Linux进行系统调用最终的落实者, 这个结构体包含对文件打开,关闭,读写,控制的一系列成员函数。

//结构体定义如下:
static struct file_operations chrdev_fops = {
        .owner                = THIS_MODULE,        //指向拥有这个结构的模块的指针.用来在它的操作还在被使用时阻止模块被卸载

        .open                = chrdev_open,                //以下都是些常用接口,应用层调用时对应的驱动层实现
        .release                = chrdev_release,
        .write                = chrdev_write,
        .read                        = chrdev_read,
};


4、cdev是啥

cdev是字符设备对象结构体,是linux用来管理字符设备的,其在内核中采用数组结构设计,这样系统中有多少个主设备号就约定了数组大小,此设备号采用链表管理,同一主设备号下可以有多个子设备。

cdev 结构体中包含 设备号 dev_t 和 file_operations 结构指针

说白了,cdev 就是用来描述一个字符设备的,结构体如下:
struct cdev {
        struct kobject kobj;                              //内嵌的内核对象.
        struct module *owner;                           //该字符设备所在的内核模块的对象指针.
        const struct file_operations *ops;          //该结构描述了字符设备所能实现的方法,是非常关键的一个结构体.
        struct list_head list;                             //用来将已经向内核注册的所有字符设备形成链表.
        dev_t dev;                                            //字符设备的设备号,由主设备号和次设备号构成.
        unsigned int count;                             //同一主设备号下的次设备号的个数.
};

跟cdev相关的有四个比较重要的函数:

struct cdev *cdev_alloc(void);        //申请一个cdev,也可以定义变量一样弄一个

void cdev_init(struct cdev*, const struct file_opeartions*); //初始化cdev设备对象,其实就是跟file_opeartions进行绑定, 相当于两个人私定终身了

int cdev_add(struct cdev* , dev_t, unsigned);         //注册字符设备对象cdev到内核,相当于两人结婚领证了,从此系统可查,受国家法律保护

void cdev_del(struct cdev* );        //从内核注销cdev设备对象


扩展:应用程序是怎么跟驱动程序产生联系的呢,比如open函数,其调用大致过程如下:

1、应用程序open打开一个设备节点文件:int open("/dev/text", O_RDWR);

2、会产生open系统调用,然后进入内核,调用sys_open函数,就直接到VFS层了,并产生struct file表示一个打开的文件

3、然后VFS虚文件系统open根据传进来的路径转换为inode,通过 inode 节点获取到文件设备号

4、遍历 cdev 链表,与此文件的设备号进行比较,如果相同则表示匹配成功

5、将匹配成功的 cdev 结构体中的 file_operations 赋值给此文件的 struct file,从而关联到驱动层中的file_operations

6、最后根据file_operations中函数指针就可以找到该结构体中对该种文件操作的所有方法

大致过程就是:应用层open --> 系统调用sys_open --> VFS层 --> 得到inode --> 获取设备号 --> 遍历cdev --> 设备号匹配就进行关联

明白了字符设备驱动基本框架与相关概念之后,我们知道一个字符设备驱动如果想要像文件一样去操作,首先肯定需要有设备号,还需要有个驱动名,然后需要填充file_operations结构体,这样才能实现对设备的具体操作,可以开始写代码了,just do it!

#  接口介绍
1、写代码之前,先介绍两个宏
module_init()        //这个是加载模块的时候调用的,相当于模块入口
module_exit()        //这个是卸载模块的时候调用的,相当于模块出口

2、再介绍两个函数
①.注册字符设备驱动函数
用register_chrdev函数注册一个字符设备驱动,这个接口非常方便,传入些参数就一个字符设备注册完事了,虽然该函数算是老的接口了,以前认为要学就学新接口,老接口都淘汰了,学了也没啥用,其实不然,该老接口并没有弃用,并且新接口也只是对它做了拆分,走的还是它内部那些套路,其原型为:
static inline int register_chrdev(unsigned int major,  const char *name, const struct file_operations *fops);
major是主设备号,当它为0时系统会给你动态分配一个,也可以你自己找一个系统中没有用到的,name是字符设备驱动名,insmod之后cat /proc/devices可以看到的名字,*fops是该设备驱动所有操作的接口

②.注销字符设备驱动函数
unregister_chrdev函数,输入需要注销的设备驱动设备号与设备名称就可以了
原型为:static inline void unregister_chrdev(unsigned int major, const char *name)

③.找到一个可用的设备号
在开发板上查看,cat /proc/devices,看哪个设备号没用,一般是0~255,从大的开始选
当然这一步也不是必须的,因为上面说到的那个register_chrdev函数可以让系统给你分配,但是在我们学习过程中,这一步是应该了解的

//驱动代码
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/ide.h>

MODULE_LICENSE("GPL");

#define HELLO_CNT        1
#define HELLO_MAJOR    222
#define HELLO_NAME            "hello"

static dev_t dev_num;

//文件打开函数
static int hello_open(struct inode *inode, struct file *file)
{
        printk(KERN_INFO "\nhello_open\n");
       
        return 0;
}

//文件释放函数
static int hello_release(struct inode *inode, struct file *file)
{
        printk(KERN_INFO "\nhello_release\n");

        return 0;
}

static struct file_operations chrdev_fops = {
        .owner                 = THIS_MODULE,        //这里都是这么赋值的,用来在它的操作还在被使用时阻止模块被卸载
       
        //应用层间接调用的就是如下接口, 应用层如果调用没有定义的接口是会出错的
        .open                = hello_open,               
        .release        = hello_release,       
};

//模块加载函数
static int hello_init(void)
{
    printk(KERN_ALERT "hello world!\n");

    //register_chrdev是注册字符设备, 这个接口可以一步到位
        dev_num = register_chrdev(HELLO_MAJOR, HELLO_NAME, &chrdev_fops);
        if (dev_num < 0) {
                printk(KERN_ERR "register_chrdev fail\n");
                return -EINVAL;
        }
        printk(KERN_INFO "register_chrdev success...\n");
    return 0;
}

//模块卸载函数
static void hello_exit(void)
{
        //注销字符设备驱动
    unregister_chrdev(HELLO_MAJOR, HELLO_NAME);
    printk(KERN_ALERT "hello_exit, goodbye!\n");
}

module_init(hello_init);
module_exit(hello_exit);

//测试代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

#define FILE "/dev/hello"

int main(void)
{
        int fd = -1;
        int i = 0;

        fd = open(FILE, O_RDWR);
        if (fd < 0) {
                printf("\nopen %s fail:%d\n",  FILE, fd);
                return -1;
        }
        printf("App open %s ok\n", FILE);
       
        close(fd);
}

Makefile内容如下:
KERNELDIR := /home/mstar/ubuntu64/linux
CURRENT_PATH := $(shell pwd)
obj-m := hello_drv.o

build: kernel_modules

kernel_modules:
        $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
       
clean:
        $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

编译驱动与测试app

 


然后将hello_drv.ko与helloApp拷贝到开发板的共享盘
我是将ubuntu下的一个文件挂载到linux开发板上的,如通过如下指令:
mount -t nfs -o nolock,nfsvers=3 192.168.0.103:/home/mstar/ubuntu64/share mnt/ttt

# 开始测试
①.驱动加载
使用insmod指令加载驱动
root@ALIENTEK-IMX6U:/mnt/ttt# insmod hello_drv.ko
hello world!
register_chrdev success...

②.查看驱动
使用cat /proc/devices查看


说明驱动已经加载成功了,主设备号是222

③.创建设备文件
使用mknod指令创建设备文件,mknod 的标准形式为:mknod DEVNAME {b | c}  MAJOR  MINOR
如:mknod /dev/hello c 222 0 //其中/dev/hello是设备文件名,在应用程序中就可以通过该名进行访问,该名称是跟设备号绑定在一起的,后面的c代表字符设备,222是主设备号,0是次设备号,次设备号还可以有很多,代表同一种类型的不同文件!
如下:
root@ALIENTEK-IMX6U:/mnt/ttt# mknod /dev/hello c 222 0
root@ALIENTEK-IMX6U:/mnt/ttt# mknod /dev/hello1 c 222 1

于是通过ls -ll /dev查看到了两个文件


④.运行应用程序


下面打印出的字符很明显应用程序能正常运行

⑤.卸载驱动
通过rmmod指令

其中/dev下生成的hello与hello1两个文件需要手动rm删除

#  总结
一个简单的字符设备驱动,代码上的东西并不多,很多步骤都是需要我们手动去完成,像确定设备号、创建/删除设备文件等,这虽然比较麻烦,但在我们前期学习的过程中是有帮助的,我们对整个过程有了一个基本的了解,后面就算切换到由程序完成也有了一个更好的过渡!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

newzhpfree

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值