前言

  • 以野火i.M 6U为例

3. 字符设备驱动

需要明确的是模块驱动是两回事。

本笔记开始记录驱动的相关知识。

3.1 Linux设备分类

Linux设备可分为三:字符设备、块设备和网络设备。

网络设备:是一种特殊设备,它并不存在于/dev下面,主要用于网络数据的收发。

Linux系统将设备分别抽象为 struct cdev, struct block_device,struct net_devce 三个对象,具体的设备都可以包含着三种对象从而继承和三种对象属性和操作, 并通过各自的对象添加到相应的驱动模型中,从而进行统一的管理和操作。

3.2 设备相关概念

3.2.1 设备号

设备号分 主设备号次设备号

主设备号区别设备类型。

次设备号表示具体的设备。、

设备号用 dev_t 来表示,dev_t 是32 bit的数,其中高 12 bit 表示主设备号,低 20 bit 表示次设备号。

但是需要注意的是,在内核源码中会设定一个宏 CHRDEV_MAJOR_MAX,主设备号限定在 0-CHRDEV_MAJOR_MAX 中

三个函数来处理设备号:

  • MAJOR(dev):使用该函数来获得设备号中的主设备号。
  • MINOR(dev):使用该函数来获得设备号中的次设备号。
  • MKDEV(ma,mi):使用该函数来获得设备号中的设备号。

3.2.2 设备节点

设备节点(设备文件):Linux设备节点是通过 mknod 命令来创建的;也可以通过代码函数来创建(如:device_creat)。

一般的数据文件称为普通文件,而设备节点的文件称为设备文件。

设备节点被创建在 /dev 路径下,通过设备节点就可以操作对应的设备了。

3.2.3 APP open 文件理解 **

APP 每打开一个文件时,都会获得一个文件句柄(一个整数),每一个句柄在内核中都有与之对应的 struct file打开文件时创建的)。

APP 关闭一个文件时,句柄对应的 struct file 会被内核释放,句柄值也会交回系统。

同样的道理,APP 打开设备节点,即是设备文件时,内核也会创建一个 struct file 的数据结构。

但是需要注意的是该结构体里面的 struct file_operations *f_op 是由驱动程序提供的。

内核使用 inode 结构体在内核内部表示一个文件,

3.3 数据结构

主要介绍几个数据结构:struct file、sruct file_operations、struct inode

3.3.1 struct file

看节点 《3.2.3 APP open 文件理解》 即可

struct file 源码在 内核源码/include/linux

其中包含 sruct file_operations ,就是让 APP 能够操作该文件。

struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op; /*
¦* Protects f_ep_links, f_flags.
¦* Must not be taken from IRQ context.
¦*/
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra; u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data; #ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
errseq_t f_wb_err;
} __randomize_layout

3.3.2 struct file_operations

file_operations 就是把系统调用和驱动程序关联起来,提供文件操作函数(在这里即是驱动函数)。

在编写驱动程序的时候,创建一个该类型的结构体,然后编写部分数,填充该结构体部分内容即可。

struct file 源码在 内核源码/include/linux

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 (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*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 *);
unsigned long mmap_supported_flags;
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 (*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);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *, loff_t, size_t, unsigned int);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,u64);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

在编写 readwrite 函数时需要用到两个函数来 copy 数据的:

  • static inline long copy_from_user(void *to, const void __user * from, unsigned long n):用于 write 函数时 copy 来自APP的数据。(即是 APP-->内核
  • static inline long copy_to_user(void __user *to, const void *from, unsigned long n):用于 read 函数时 copy 数据到APP。(即是 内核-->APP

后源:一般都是前面目的地址,后面是源地址

3.3.2 struct inode

内核使用 inode 结构体在内核内部表示一个文件。

与文件描述符的结构体(struct file)不同。

可以使用多个文件结构体表示同一个文件的多个描述符,但是都得指向同一个 inode 结构体,因为同一个文件,对应同一个 inode 结构体

inode 结构体 包含很多文件相关的信息,但是对于字符设备文件,只需要关心两个域即可:

  • dev_t i_rdev:设备文件的结点,包含了设备号。
  • struct cdev *i_cdevstruct cdev 是内核的一个内部结构,它是用来表示字符设备的,当 inode 结点 指向一个字符设备文件时,此域为指向inode结构体

3.4 字符设备驱动程序框架 **

字符设备驱动程序框架(框架是一样的,只是这里介绍新函数):

  1. 实现设备驱动:填充 struct operations 结构体部分内容。
  2. 驱动初始化
    • 分配设备编号dev_t):

      • 静态分配:register_chrdev_region()
      • 动态分配:alloc_chrdev_region()
    • 初始化 cdev:创建并初始化一个 cdev 结构体cdev_init()
    • 注册 cdev:向内核注册一个 cdev 结构体cdev_add()
    • 新建一个设备节点:建立设备节点,绑定设备号和 cdev
  3. 驱动注销
    • 释放 cdevcdev_del()
    • 归还申请的主设备号unregister_chrdev_region()

3.4.1 实现设备驱动

实现设备驱动其实就是填充 struct operations 结构体部分内容,并关联到主设备号。

3.4.2 驱动初始化和注销

3.4.2.1 设备号的申请和归还

register_chrdev_region()

  • 该函数用于静态申请一个或多个设备号。
  • 注:使用该函数时最好去内核源码的 Documentation/devices.txt 查看一下使用规则
  • 函数原型:int register_chrdev_region(dev_t from, unsigned count, const char *name)
    • from:dev_t类型的变量,用于指定字符设备的起始设备号,如果要注册的设备号已经被其他的设备注册了,那么就会导致注册失败。
    • count:指定要申请的设备号个数,count的值不可以太大,否则会与下一个主设备号重叠。
    • name:用于指定该设备的名称,我们可以在/proc/devices中看到该设备。
    • 成功返回 0;失败返回错误码。

alloc_chrdev_region()

  • 该函数用于动态申请设备号。
  • 函数原型:int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
    • dev:dev_t类型的指针变量,用于存放分配到的设备编号的起始值。
    • baseminor:次设备号的起始值,通常情况下为 0。
    • count:次设备号的个数。
    • name:用于指定该设备的名称,我们可以在/proc/devices中看到该设备。
    • 成功返回 0;失败返回错误码。

unregister_chrdev_region()

  • 该函数用于注销设备号,归还给内核。
  • 函数原型:void unregister_chrdev_region(dev_t from, unsigned count)
    • from:指定需要注销的字符设备的设备编号起始值,我们一般将定义的dev_t变量作为实参。
    • count:指定需要注销的字符设备编号的个数,该值应与申请函数的count值相等,通常采用宏定义进行管理。
    • 成功返回 0;失败返回错误码。

以上三个函数都属于新的函数,当然也就旧版的,目前依旧兼容,主要区别是:

  1. 旧版的申请了主设备号后,跟着的256个次设备号会全部被注册,导致资源浪费。
  2. register_chrdev() 包含了 cdev_init()cdev_add(),而新版没有。**

register_chrdev()

  • 该函数用于申请设备号。该函数包含了 cdev_init()cdev_add()
  • 函数原型:
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
  • major:用于指定主设备号,若为0,则动态分配。
  • name:字符设备的名称。
  • fops:字符设备驱动,即是struct file_operations。
  • 返回主设备号。

unregister_chrdev()

  • 该函数用于注销设备号,归还给内核。该函数包含了 cdev_del()
  • 函数原型:
static inline void unregister_chrdev(unsigned int major, const char *name)
{
__unregister_chrdev(major, 0, 256, name);
}
  • major:用于指定主设备号。
  • name:字符设备的名称。
  • 无返回。
3.4.2.2 初始化 cdev

cdev 在内核,表示字符设备文件。(相当于其它文件的 inode

初始化 cdev 使用函数 void cdev_init(struct cdev *cdev, const struct file_operations *fops):

  • cdevstruct cdev 类型的指针变量,指向需要关联的字符设备结构体;
  • fopsfile_operations 类型的结构体指针变量,一般将实现操作该设备的结构体 file_operations 结构体作为实参。

3.4.3 设备的注册和注销(设备节点)

注册和注销设备都有两种方法

  • 代码
  • 命令

设备的注册(代码

  • cdev_add() 函数用于向内核的cdev_map散列表添加一个新的字符设备。
  • 函数原型:int cdev_add(struct cdev *p, dev_t dev, unsigned count)
    • p:struct cdev类型的指针,用于指定需要添加的字符设备;
    • dev:dev_t类型变量,用于指定设备的起始编号;
    • count:指定注册多少个设备。

设备的注销(代码

  • cdev_del() 用于注销设备。

    • 函数原型:void cdev_del(struct cdev *p)

      • p:struct cdev类型的指针,用于指定需要删除的字符设备。
  • 从系统中删除cdev,cdev设备将无法再打开,但任何已经打开的cdev将保持不变, 即使在cdev_del返回后,它们的FOP仍然可以调用。因为 struct file 还在

设备的注册(命令

方法:mknod 设备名 设备类型 主设备号 次设备号

  • 类型

    • P:创建先进先出(FIFO)特殊文件。可不指定主设备号和次设备号(其它类型必须指定主次设备号);
    • b:创建(有缓冲的)区块特殊文件;
    • c:创建(没有缓冲区)字符特殊文件;
    • u:创建(没有缓冲区)字符特殊文件。

3.5 实战例程

例程源码地址:李柱明gitee

3.6 字符设备驱动框架总结

字符设备驱动程序框架(框架是一样的,只是这里介绍新函数):

  1. 实现设备驱动:填充 struct operations 结构体部分内容。
  2. 驱动初始化
    • 分配设备编号dev_t):

      • 静态分配:register_chrdev_region()
      • 动态分配:alloc_chrdev_region()
    • 初始化 cdev:创建并初始化一个 cdev 结构体cdev_init()
    • 注册 cdev:向内核注册一个 cdev 结构体cdev_add()
    • 新建一个设备节点:建立设备节点,绑定设备号和 cdev
  3. 驱动注销
    • 释放 cdevcdev_del()
    • 归还申请的主设备号unregister_chrdev_region()

驱动模块实现分点实现步骤

  1. 模块的实现:

    1. 实现入口函数;
    2. 实现出口函数;
    3. 标注协议等信息。
  2. 驱动的实现:
    1. 实现驱动内容,struct operations 结构体内容;
    2. 实现驱动框架:
      1. 向内核申请设备号
      2. 初始化内核设备文件结构体 cdev;(一个设备号对应一个结构体
      3. 把设备文件结构体 cdev 绑定设备号驱动内容 struct operation,然后注册到内核;
  3. 设备节点的实现:
    1. 创建一个设备类
    2. 在该类下创建设备节点并绑定设备号。(一个设备号对应一个设备节点
  4. 注销:
    1. 删除设备节点
    2. 删除设备文件结构体 cdev
    3. 归还设备号
    4. 删除设备类

参考

【linux】驱动-3-字符设备驱动的更多相关文章

  1. Linux驱动设计——字符设备驱动(一)

    Linux字符设别驱动结构 cdev结构体 struct cdev { struct kobject kobj; struct module *owner; const struct file_ope ...

  2. 【转】深入浅出:Linux设备驱动之字符设备驱动

    深入浅出:Linux设备驱动之字符设备驱动 一.linux系统将设备分为3类:字符设备.块设备.网络设备.使用驱动程序: 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据 ...

  3. 【Linux驱动】字符设备驱动

    一.linux系统将设备分为3类:字符设备.块设备.网络设备.使用驱动程序: 1.字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据.字符设备是面 ...

  4. linux设备驱动之字符设备驱动模型(2)

    在上一篇中我们已经了解了字符设备驱动的原理,也了解了应用层调用内核函数的机制,但是我们每次操作设备,都必须首先通过mknod命令创建一个设备文件名,比如说我们要打开u盘,硬盘等这些设备,难道我们还要自 ...

  5. linux设备驱动之字符设备驱动模型(1)

    一:字符设备驱动 在linux下面,应用层看到的一切皆为文件(名字)所有的设备都是文件,都可以调用open,read,write来操作,而在内核中每个中每个设备有唯一的对应一个设备号: APP   ( ...

  6. 蜕变成蝶~Linux设备驱动之字符设备驱动

    一.linux系统将设备分为3类:字符设备.块设备.网络设备.使用驱动程序: 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据.字符设备是面向流 ...

  7. 深入浅出:Linux设备驱动之字符设备驱动

    一.linux系统将设备分为3类:字符设备.块设备.网络设备.使用驱动程序: 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据.字符设备是面向流 ...

  8. 第一个驱动之字符设备驱动(二)mdev

    mdev是busybox提供的一个工具,用在嵌入式系统中,相当于简化版的udev,作用是在系统启动和热插拔或动态加载驱动程序时, 自动创建设备节点.文件系统中的/dev目录下的设备节点都是由mdev创 ...

  9. Linux字符设备驱动实现

    Linux字符设备驱动实现 要求 编写一个字符设备驱动,并利用对字符设备的同步操作,设计实现一个聊天程序.可以有一个读,一个写进程共享该字符设备,进行聊天:也可以由多个读和多个写进程共享该字符设备,进 ...

  10. Linux字符设备驱动基本结构

    1.Linux字符设备驱动的基本结构 Linux系统下具有三种设备,分别是字符设备.块设备和网络设备,Linux下的字符设备是指只能一个字节一个字节读写的设备,不能随机读取设备内存中某一数据,读取数据 ...

随机推荐

  1. convert image to base64 in javascript

    convert image to base64 in javascript "use strict"; /** * * @author xgqfrms * @license MIT ...

  2. CSS Dark Mode

    CSS Dark Mode https://kevq.uk/automatic-dark-mode/ https://kevq.uk/how-to-add-css-dark-mode-to-a-web ...

  3. VAST助推NGK公链热度升温,日活超过以太坊!

    在区块链市场,如果说过去是比特币和以太坊的时代,那么现在和未来绝对是NGK的时代. NGK公链的出现,让区块链市场看到了新的希望.它不仅仅是开放的和可编程的,而且是低Gas燃耗的,以及创新共识机制的. ...

  4. NGK项目八大板块是什么?

    公链项目生态各板块中,应用生态繁荣与实体经济联系作为密切,也被看做公链平台追求的终极目标,NGK公链以实体企业粘合客户,致力于重塑金融体系,构建全球区块链生态. NGK让经济权益上链发行,目前已有八大 ...

  5. CSS前端性能优化

    1.Google 资深web开发工程师Steve Souders对CSS选择器的效率从高到低做了一个排序: 1. id选择器(#myid) 2. 类选择器(.myclassname) 3. 标签选择器 ...

  6. UDP编程详解

    目录 报文格式 通信过程 UDP客户端流程 UDP客户端编码 UDP服务器流程 UDP服务器编码 参考文献 UDP与TCP的不同之处是:他的通信不需要建立连接的过程.中文名称用户数据报协议.时OSI参 ...

  7. 一次 MySQL 线上死锁分析实战

    关键词:MySQL Index Merge 前言 MySQL 的锁机制相信大家在学习 MySQL 的时候都有简单的了解过,那既然有锁就必定绕不开死锁这个问题.其实 MySQL 在大部分场景下是不会存在 ...

  8. docker数据卷的操作

    一般情况下会比较频繁的修改容器内部的文件 频繁docker cp 不太方便 使用数据卷可以将宿机的某个目录映射至容器的目录 修改会方便点 1.创建数据卷 docker volume create 数据 ...

  9. 关于string【】 数组 进行 toString() 之后无法将数组的内容连接起来组合成 string 字符串 的问题

    string[] to string 如果直接对一个string[] 数组进行 tostring()的操作,得到的值都是 system.string[] 如果想要将 string[] 数组内容转换为一 ...

  10. 使用ASP.NET Blazor Server 写混合桌面程序的疯狂想法

    开发本地桌面程序,使用进程内浏览器+进程内BLAZOR服务器,然后任性写功能,自由分发,放飞自我,大家看怎么样? 求评估,求批评 https://github.com/congzhangzh/desk ...