Linux驱动基础

本文详细介绍了Linux驱动的基础知识,包括设备节点、主设备号和次设备号的作用,以及应用程序如何通过系统调用进入内核并找到对应的驱动程序。还探讨了字符设备的概念,内核模块的加载和卸载,以及如何使用设备树来匹配和管理驱动程序。此外,文章还提到了驱动程序设计中的面向对象思想,以及如何通过设备树来改进驱动程序的编写和调试技巧。
摘要由CSDN通过智能技术生成

#Linux驱动 (qq.com)https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxMjEyNDgyNw==&action=getalbum&album_id=1502410824114569216&scene=173&from_msgid=2247504793&from_itemidx=1&count=3&nolastread=1#wechat_redirectLinux中级——“驱动” 控制硬件必须学会的底层知识 (qq.com)https://mp.weixin.qq.com/s/xLp5W9291oGo5jWjerQH9Q

什么是设备节点和主设备号和次设备号?

一句话总结:我们可以通过设备节点访问到驱动程序。

主设备号用来表示一个特定的驱动程序。
次设备号用来表示使用该驱动程序的各设备。

设备节点被创建在/dev下,是连接内核与用户层的枢纽,就是设备是接到对应哪种接口的哪个ID 上。 相当于硬盘的inode一样的东西,记录了硬件设备的位置和信息
在Linux中,所有设备都以文件的形式存放在/dev目录下,都是通过文件的方式进行访问,设备节点是Linux内核对设备的抽象,一个设备节点就是一个文件。

对于相同主设备号 次设备号不同的设备节点通常保存在/dev目录下的同一个目录中,而不是直接以设备节点的形式保存在/dev目录下。

举个例子:/dev/input目录下保存着输入设备的设备节点,它们的主设备号都为13

应用程序通过一组标准化的调用执行访问设备,这些调用独立于任何特定的驱动程序。而驱动程序负责将这些标准调用映射到实际硬件的特有操作。

设备节点,驱动,硬件设备是如何关联到一起的呢?
这是通过设备号实现的,包括主设备号和次设备号。当我们创建一个设备节点时需要指定主设备号和次设备号。应用程序通过名称访问设备,而设备号指定了对应的驱动程序和对应的设备。主设备号标识设备对应的驱动程序,次设备号由内核使用,用于确定设备节点所指设备。

主设备号驱动程序在初始化时,会注册它的驱动及对应主设备号到系统中,这样当应用程序访问设备节点时,系统就知道它所访问的驱动程序了。 

/* 主设备号就表示这是系统中的第几号驱动程序 */

你可以通过/proc/devices文件来查看系统设备的主设备号。

次设备号驱动程序遍历设备时,每发现一个它能驱动的设备,就创建一个设备对象,并为其分配一个次设备号以区分不同的设备。这样当应用程序访问设备节点时驱动程序就可以根据次设备号知道它说访问的设备了。

在内核中使用一个32位的数来表示设备号:其中 12 位用来表示主设备号,20 位用来表示次设备号。

设备节点(设备文件)Linux中设备节点是通过“mknod”命令来创建的。一个设备节点其实就是一个文件,Linux中称为设备文件。有一点必要说明的是,在Linux中,所有的设备访问都是通过文件的方式,一般的数据文件程序普通文件,设备节点称为设备文件。

设备驱动:设备驱动程序(device driver),简称驱动程序(driver),是一个允许高级(High level)计算机软件(computer software)与硬件(hardware)交互的程序,这种程序建立了一个硬件与硬件,或硬件与软件沟通的界面,经由主板上的总线(bus)或其它沟通子系统(subsystem)与硬件形成连接的机制,这样的机制使得硬件设备(device)上的数据交换成为可能。想象平时我们说的写驱动,例如点led灯的驱动,就是简单的io操作。
 

ls -l 查看设备节点

其中权限前面的第一个字母代表了是什么类型驱动的设备节点

比如上图中的c(char)就是字符设备驱动

b(block)就是块设备驱动

其中又包含了主设备号 和 从设备号

主设备号用来表示是哪一个设备驱动

次设备号用来表示是这个设备驱动中的哪一个硬件

如上图中的i2c1 主设备号89 代表了内核中第89号驱动程序;次设备号为 1代表了访问1号i2c硬件

我们使用open/read/write这些系统调用 也是通过访问这些设备节点进而访问到驱动程序


 应用程序怎么进入内核?

我们使用read、open这些函数实际上是使用了glibc库接口  这些接口会触发异常然后进入内核态 调用对应的 sys_read、sys_open 

因为异常操作系统就会进入内核态运行对应的程序

一定要仔细看下面的图!!!!!!!!

在这里插入图片描述

 (上面中(1)(2)(3)是不断改进的指令)

用户态应用程序App在编写代码时,使用open/read/write等进行文件操作。

open/read/write向下调用glibc库中与open/read/write相关的接口函数。

无论是App还是glibc都是出于Linux用户态,怎么切换到Linux内核态呢?

对于32位处理器,需要使用swi指令;对于64位处理器,需要svc指令。

在切换到内核态后,内核怎么知道你是从glibc的open接口进来的还是从glibc的read接口进来的呢?这就需要在切换时传个可以区分的参数进来。

  • 在old ABI时期,glibc执行swi指令时,会传一个参数,例如swi __NR_open或者swi __NR_read,切换到内核态之后,从swi指令中获取对应的__NR_open或者__NR_read值
  • 在EABI时期,glibc不再通过swi指令传递参数,而是先将参数保存的通用寄存器R7中,通过swi指令切换到内核态之后,再从R7寄存器中获取参数值。
  • 对于64位ARM处理器,在执行svc切换到内核态之前,先把参数保存到通用寄存器R8中,切换到内核态之后,再从R8寄存器获取参数值。

得到参数值__NR_open或者__NR_read之后,通过sys_call_table[__NR_open]或者sys_call_table[__NR_read]调用对应的内核态函数接口。sys_call_table[]是一个有内核态函数指针组成的数组,通过下标可以直接找到对应的函数进行调用。

进入了内核,内核的sys_open、sys_read会做什么?

这里以sys_open为例子:

普通文件是通过文件系统保存在块设备驱动中的 所以对于普通文件和字符设备文件 内核的sys_open做的事也就不一样。

 所以普通文件需要通过文件系统来调用块设备驱动

而字符设备驱动就可以直接通过字符设备节点的主设备号来寻找驱动程序

举个例子:如果使用了应用程序的 read  然后触发异常进入内核态调用 sys_read

就可以直接通过字符设备节点的主设备号来寻找驱动程序 然后使用字符驱动程序提供的读函数直接返回设备的信息

普通文件则需要通过文件系统来找到块设备驱动然后使用块设备驱动提供的读函数然后返回文件的信息

对于sys_xxx的注意事项

以字符设备驱动为例,一般对字符设备的操作都如下框图

 我们在应用程序调用open mmap poll 这些函数 实际上是调用了sys_xxx 既sys_开头的系统调用,注意的是sys_xxx里面会调用我们编写的驱动程序 来实现具体的功能。我们可以认为我们编写的驱动程序是一个函数 在sys_xxx里面还会调用很多其他的函数和我们编写的驱动程序一起来完成功能。

就比如POLL机制中:

 我们在drv_poll驱动程序中只需要实现将 1.线程放入等待队列,但是不休眠 2.返回事件的状态  而休眠的操作是在sys_poll中由其他函数来完成的。

这就是为什么我们应用程序的open mmap poll 这些函数的参数和返回值 可能和我们驱动程序的参数和返回值有一些不一样!(可以在file_operitons看参数和返回值)


面向对象的思想

C的面向过程和C++的面向对象到底有什么区别,大象装冰箱一个例子搞懂_哔哩哔哩_bilibiliC是面向过程的语言,C++是面向对象的,今天就通过大象装进冰箱这一例子,今天就和大家聊一聊C语言的面向过程和C++的面向对象的理解。https://www.bilibili.com/video/BV1mi4y1579d?spm_id_from=333.1007.top_right_bar_window_custom_collection.content.click在Linux内核中,面向对象的思想是无处不在的,所以我们在学习驱动基础前,要先对面向对象的思想 有所了解。先观看上面的视频。

看到过一个很有意思的描述面向对象和面向过程:

面向对象是 (狗)吃屎
面相过程是 狗(吃)屎。
一个是以对象来操作,一个是过程

我们在C语言中 使用

1.结构体来模拟类

2.结构体成员中的函数指针来模拟方法


什么是字符设备?

(三)字符设备_Kownzird的博客-CSDN博客_字符设备一、linux设备驱动的分类1、字符设备—c应用程序和驱动程序之间进行数据读写的时候,数据是以“字节”为单位。数据交互的时候,是按照固定的顺序传输的;数据是实时传输的,是没有缓存的。字符设备是没有文件系统的。绝大部分设备驱动是字符设备:LED、BEEP、按键、键盘、触摸屏、摄像头、液晶屏、声卡、IIC、SPI、…应用程序:系统IO函数open("/dev/led_drv", O_RDWR)read()write()ioctl()mmap()close()2、块设备—b应用程序和驱动https://blog.csdn.net/qq_38878046/article/details/109076595

字符设备:
应用程序和驱动程序之间进行数据读写的时候,数据是以“字节”为单位。数据交互的时候,是按照固定的顺序传输的;数据是实时传输的,是没有缓存的。字符设备是没有文件系统的。
绝大部分设备驱动是字符设备:LED、BEEP、按键、键盘、触摸屏、摄像头、液晶屏、声卡、IIC、SPI、…
应用程序:系统IO函数
open("/dev/led_drv", O_RDWR)
read()
write()
ioctl()
mmap()
close()

 


Linux 内核模块简介



linux有个很好的特性:内核提供的特性可在运行时进行拓展。这意味着当系统启动时,我们可以向内核添加功能(当然也可以移除功能)。
可在运行时添加到内核中的代码被称为:模块

Linux驱动有两种运行方式,第一种就是将驱动编译进Linux内核中,这样当Linux内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux下模块扩展名为.ko),在Linux内核启动以后使用“insmod”命令加载驱动模块。在调试驱动的时候一般都选择将其编译为模块,这样修改驱动以后只需要编译一下驱动代码即可,不需要编译整个Linux代码。而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。总之,将驱动编译为模块最大的好处就是方便开发,当驱动开发完成,确定没有问题以后就可以将驱动编译进Linux内核中,当然也可以不编译进Linux内核中,具体看自己的需求。
 

/*************************************************************************************************/

其中

init(入口)函数是模块加载函数 :顾名思义就是在模块装载是会被调用的函数。

exit(出口)函数是模块卸载函数:顾名思义就是在模块卸载时被调用的函数。

Linux下模块扩展名为.ko文件,一个.ko文件可能是由多个.c文件编译来的,也就是说我们在1.装载模块 (insmod  .ko文件)时会有可能调用多个模块加载函数 既 init函数(入口函数)。

2.卸载模块时(rmmod .ko文件), 可能调用多个模块卸载函数 既 exit函数 (出口函数)。

/*************************************************************************************************/

我们后面将我们编写的驱动程序编译成.ko文件后 就是使用:

insmod + .ko文件名

就会将我们的.ko文件代表的模块装载到内核中 所以说我们常说的驱动程序 其实就是模块

module_init函数用来向Linux内核注册一个模块加载函数,参数xxx_init就是需要注册的具体函数,当使用“insmod”命令加载驱动的时候,xxx_init这个函数就会被调用。module_exit()函数用来向Linux内核注册一个模块卸载函数,参数xxx_exit就是需要注册的具体函数,当使用“rmmod”命令卸载具体驱动的时候xxx_exit函数就会被调用。

一般来说:

一个.ko文件 (也就是一个模块)就对应一组init(入口)和exit(出口)函数

(当然也可以在编译的时候将多个.c文件编译成.ko文件这样.ko文件里面就有多组Init和exit函数了)


对于sys_xxx系统调用和驱动程序的注意事项

有几个问题:

1.为什么应用程序调用的函数 和 我们编写的驱动程序 比如:open()和drv_open()的参数不一样?

2.我们在应用程序调用了open() poll() mmap() 就只是调用了我们编写的驱动程序吗?

不是的。

我们先来复习一下:

我们知道我们调用的 open write这些 实际上是使用了sys_xxx系统调用的。(调用会触发异常,然后调用对应的sys_xxxopen write)

以字符设备驱动为例,一般对字符设备的操作都如下框图 

 我们知道:

我们在应用程序调用open mmap poll 这些函数 实际上是调用了sys_xxx 既sys_开头的系统调用,注意的是sys_xxx里面会调用我们编写的驱动程序 来实现具体的功能。

我们可以认为:我们编写的驱动程序是一个函数 。在sys_xxx里面还会调用很多其他的函数和我们编写的驱动程序一起来完成功能。

就比如POLL机制中:

 我们在drv_poll驱动程序中只需要实现将 1.线程放入等待队列,但是不休眠 2.返回事件的状态  而休眠等其他操作是在sys_poll中由其他函数来完成的。

这就是为什么我们应用程序的open mmap poll 这些函数的参数和返回值 可能和我们驱动程序的参数和返回值有一些不一样!(可以在file_operitons看参数和返回值)

所以我们要有个概念:我们可以认为我们编写的驱动程序 实际上就是一个功能函数,它在sys_xxx系统调用中被使用来决定这个sys_xxx系统调用的功能。

让我们的应用程序可以调用


APP打开的文件在内核中如何表示?

我们在前面学习文件IO的过程中知道 当打开一个文件时,内核就会创建一个新的 打开文件表表项,也就是file结构体。file结构体保存了关于这个文件的许多信息。

APP打开文件时,可以得到一个整数,这个整数被称为文件句柄(文件描述符)。对于APP的每一个文件句柄,在内核里面都有一个“struct file”与之对应。

可以猜测,我们使用 open 打开文件时,传入的 flags、mode 等参数会被记录在内核
中对应的 struct file 结构体里(f_flags、f_mode)
int open(const char *pathname, int flags, mode_t mode);
去读写文件时,文件的当前偏移地址也会保存在 struct file 结构体的 f_pos 成员里。

在我们打开字符设备节点时,内核中也同样会创建对应的 struct file

注意这个结构体中的结构体:struct file_operations *f_op,这个结构体的成员是由驱动程序提供的。

 

 结构体 struct file_operations 的定义如下:

我们要创建一个自己的file_operation结构体 里面存放我们自己编写的驱动程序 供应用程序调用,这样我们在打开指定的设备节点的时候内核就会通过文件描述符找到对应的 file结构体里面的file_operation结构体 里面的对应的驱动。

/*************************************************************************************************************/

所以想要使用对应设备节点的驱动,就得先dev_fd = open(/dev/our_dev_name, O_RDWR);

得到一个设备节点的文件描述符 dev_fd(此处使用open其实也是调用的这个设备节点的驱动程序的open函数)

后续我们使用的read write ioctl等系统调用也是通过这个文件描述符 来执行对应设备节点驱动程序的read write ioctl 

我们通过file_operation结构体将指定的驱动程序和用户空间的read write open等函数绑定在一起。比如:

static struct file_operations led_foperations = {
        .owner        = THIS_MODULE,
        .open        = led_open,
        .write        = led_write,

        .read        = button_read,

};

就是将用户空间的open 和 驱动程序led_open绑定在一起,用户空间的write 和驱动程序led_write绑定在一起

用户空间write定义: ssize_t write(int fd, const void *buf, size_t count);

这样 我们在用户空间调用 write (dev_fd, buf, 1)

相当于是给 led_write 传参

但是用户空间的buf是不能直接使用的 我们需要在驱动程序中调用 copy_from_user ()使用一个内核空间的kernel_buf来接收用户空间的数据

static ssize_t led_write (struct file *file, const char __user *buf, size_t count, loff_t *ppos)

{

        copy_from_user (kernel_buf, buf, 1);

}

同样的驱动程序也可以向应用程序来传递数据 比如read:

static ssize_t button_read(struct file *filp, char __user *buf,size_t count, loff_t *ppos)
{
    int err;
    int status;
    unsigned int minor = iminor(file_inode(file));
    status = p_button_opr->read(minor);
    err = copy_to_user(void * to, &status, sizeof(status));
    return 0;
}

我们只要在应用程序调用 read(“/dev/led”, buf, 1)

就会调用到驱动程序的button_read函数 将status的数据 传给 用户空间的buf中了

这里只是举例了write和read函数 当然不只是这两个函数,许多函数都可以通过这样来使用

我们可以去内核源码查看file_operations结构体的定义:

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 *);
	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 (*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);
	ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
			u64);
};

我们只要在我们自己定义的file_operations结构体中定义了某个函数;我们就可以在应用程序中调用和file_operations结构体成员同名的函数,这样应用程序就可以调用我们驱动实现的函数了。

/*************************************************************************************************************/

请猜猜怎么编写驱动程序?

① 确定主设备号,也可以让内核分配

② 定义自己的file_operations结构体

③ 实现对应的drv_open/drv_read/drv_write等函数,填入file_operations结构体

④ 把file_operations结构体告诉内核:使用内核提供的帮助函数 register_chrdev

⑤ 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数

在入口函数里面调用register_chrdev()来注册驱动函数

⑥ 有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev

⑦ 其他完善:提供设备信息,自动创建设备节点: class_create(), device_create( )

驱动程序末尾:moduel_init(), module_exit() ,MODULE_LICENSE("GPL"),

个人理解:

(1)实现驱动程序

   (2) 将我们实现的驱动程序定义在自己的file_operations结构体

(3)在入口函数(通常是xxx_init ( ) )中调用 :

        1.register_chrdev ()注册主设备号为major对应的驱动程序将我们编写的fiel_operations结构体和主设备号关联起来,然后注册到内核中 我们之前学过主设备号就是代表为系统中的第几号驱动程序 所以register_chrdev其中的name参数就是系统中major号驱动程序的名字

        2.class_create()注册类, 

        3.device_create( )注册主设备号为x 次设备号为y的设备节点

(我们也可以注册多个设备节点 再调用一次device_create 其中改变一下次设备号就可以了 这样多个主设备号相同 次设备号不同的设备节点就都可以使用这个主设备号的驱动程序了)

(4)在出口函数(通常是xxx_exit( ) )中调用

        1.device_destroy()销毁设备节点

        2.class_destroy ()销毁类

        3.unregister_chrdev ( ) 销毁驱动程序

/* 注意到注册和销毁的顺序是相反的 比如是先注册驱动程序 最后注册设备节点;那么销毁的时候就要先销毁设备节点 最后销毁驱动程序。这叫倒影式编程 */

(5)在程序末尾添加

moduel_init( xxx_init ), module_exit( xxx_exit ) ,

这两个宏相当于是修饰着两个函数分别成为入口函数和出口函数

MODULE_LICENSE("GPL"),


------定义并初始化一个字符设备---------
1、定义一个字符设备—>struct cdev
2、定义并初始化字符设备的文件操作集—>struct file_operations
3、给字符设备申请一个设备号—>设备号=主设备号<<20 + 次设备号
4、初始化字符设备
5、将字符设备加入内核

-------自动生成设备文件---------
6、创建class
7、创建device,其中device是属于class的
————————————————
 

 /************************************************************************************************************/

从上面可以知道 :  入口函数 就是驱动程序和内核交流的窗口。

我们在入口函数 调用的:register_chrdev在内核中注册我们的驱动程序, device_create函数向内核中注册设备节点  class_create函数等,都是通过入口函数来完成的。

由此可见,如果我们需要往内核中注册某些东西 都是需要入口函数来完成的

(不仅仅局限于注册驱动和设备节点,比如我们后面学习的platform_device和platform_drive)

PS:register_chrdev会将 主设备号major和我们编写的file_operations关联起来,然后注册到内核中,这样我们在访问 主设备号为major的设备节点时,我们就会调用我们编写的file_operation中对应的函数指针。

/* register_chrdev中name这个参数是加载到内核的模块(也就是主设备号major对应的驱动程序)的名字 可以通过lsmod来查看 */

/* 关于入口函数 */

入口函数是在我们将模块(我们写的驱动程序)装载到内核的时候调用的,也就是当我们在命令行输入 insmod时 入口函数被调用 。同理,出口函数是在我们卸载模块时,在命令行输入 rmmod时被调用的

/* 内核如何调用驱动入口函数 ? */
/* 答: 使用module_init()函数,
module_init()函数定义一个结构体,这个结构体里面有一个函数指针,
指向hello_drv_init()这个驱动入口函数,当我们加载或安装一个驱动程序时,
内核就会自动找到这样一个结构体,然后调用这个结构体中的函数指针,
从而调用了驱动入口函数hello_drv_init(void)
,该驱动入口函数中有
register_chrdev(主设备号,设备名,struct file_operations结构体指针)函数,
该函数会将驱动程序的file_operations结构体连同其主设备号一起向内核进行注册,
从而该驱动入口函数将一个struct file_operations 类型的结构体传送给内核,
内核可以调用这个结构中的函数指针,从而调用具体的驱动操作函数,
    应用程序操作设备文件时所调用的open, read,write等函数,最终都会
调用struct file_operations类型的结构体,例如在应用程序中用Open()函数打开驱动文件时,

linux内核会根据该设备文件的类型以及主设备号找到在内核中注册的file_operations类型的结构体*


/*************************************************************************************************/

一些重要的补充知识!

==必须知道的知识:==

  1. 在Linux文件系统中,每个文件都用一个 struct inode结构体来描述,这个结构体记录了这个文件的所有信息,例如文件类型,访问权限等。

  2. 在linux操作系统中,每个驱动程序在应用层的/dev目录或者其他如/sys目录下都会有一个文件与之对应。

  3. 在linux操作系统中, 每个驱动程序都有一个设备号

  4. 在linux操作系统中,每打开一次文件,Linux操作系统会在VFS(虚拟文件系统)层分配一个struct file结构体来描述打开的文件

***上图中

(1) 当open函数打开设备文件时,可以根据设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备),还会分配一个struct file结构体(当关闭文件file结构体就会消失)。

(2) 根据struct inode结构体里面记录的设备号去cdev_map找(只要设备号固定,即可在cdev_map中查找到对应的cdev类型变量。),可以找到对应的驱动程序。这里以字符设备为例。在Linux操作系统中每个字符设备都会对应一个struct cdev结构体(不过不同的字符设备可能对应的cdev结构体是一样的!这是因为他们都在这个cdev结构体对应的次设备号范围内)。此结构体描述了字符设备所有信息,其中最重要的一项就是字符设备的操作函数接口。

(3) 找到struct cdev结构体后,linux内核就会将struct cdev结构体所在的内存空间首地址记录在struct inode结构体i_cdev成员中,将struct cdev结构体中的记录的函数操作接口地址记录在struct file结构体的f_ops成员中。

(4) 任务完成,VFS层会给应用返回一个文件描述符(fd)。这个fd是和struct file结构体对应的接下来上层应用程序就可以通过fd找到struct file,然后在struct file找到操作字符设备的函数接口file_operation了。

其中,cdev_init和cdev_add在驱动程序的入口函数中就已经被调用,分别完成字符设备与file_operation函数操作接口的绑定,和将字符驱动注册到内核的工作。

***也就是说:

使用open打开设备节点 返回一个fd 然后通过fd 调用write read等函数的原理是这样的:

我们先通过register_chrdev_region /alloc_chrdev_region 向内核的chardevs数组中添加一个数组成员 既向内核注册了一个驱动程序 对应主设备号major ; 然后实现驱动drv_read、drv_write等函数 ; 将这些函数赋值给file_operations ;然后通过cdev_alloc cdev_init cdev_add 将file_operations和主次设备号通过cdev绑定起来 然后注册到内核中 ;再通过class_create device_create 创建设备节点。

然后

open(设备节点) ->访问到struct inode -> 分配一个file结构体 ->通过struct inode找到该设备节点对应的cdev 结构体 将其赋值给 file结构体的成员 ->返回一个与该file结构体关联的文件描述符fd。

通过这个fd 调用write read等函数 -> 访问到之前创建的file结构体 ->访问到file结构体中的file_operations ->访问到drv_write drv_read等函数

!****!内核是如何管理驱动程序 以及 cdev 的?

这篇博客一定要看!

LINUX字符设备驱动模块分析(一)相应的驱动模型分析_jerry_chg的博客-CSDN博客在前面几个模块的介绍中,我们主要以vfs为起始,完成了sysfs、设备-总线-驱动模型、platform设备驱动模型、i2c设备驱动模型、spi设备驱动模型的分析。在对这些模块进行分析的时候,我们或多或少均对字符设备驱动进行了一些说明,此前认为字符设备驱动模型比较简单,也没打算进行分析,但为了让本次学习的内容能够全面和关联,本次还是打算开一次字符设备驱动模型的分析。关于字符设备驱动模...https://blog.csdn.net/lickylin/article/details/103841792

 1.char_device_struct结构体

首先在内核中存在一个名为chardevs 的指针数组  成员是指向char_device_struct结构体链表的指针。里面的数组成员有256个 对应0~255个主设备号,如果主设备号为major 则在数组对应 chardevs[major] ,这个数组的作用就是用来管理内核中的驱动程序。

/*****************************************************************************************************/

从上图中可以看出

chardevs [ ] 成员就像是一个指向一个结构体链表头结点的指针。

也就是说 一个主设备号就对应一个char_device_struct链表,所以我们每次调用register_chrdev_region/alloc_chrdev_region 就相当于在这个链表中添加一个节点,也就是添加一个char_device_struct结构体。

这个链表的节点就代表在这个主设备号下的一段次设备号区域!

这个结构体中的baseminor 和 minorct 就代表了一个次设备号范围 从我们调用register_chrdev_region/alloc_chrdev_region 传入的设备号dev 和 count获得。


 

/*****************************************************************************************************/

有两个疑问:

1.char_device_struct结构体中的cdev的作用是?这个cdev是我们调用cdev_add传入到chrdevs成员中的节点的吗?

先去了解内核是怎么管理cdev的!

char_device_struct类型变量与cdev类型变量之间的关联如下图橙色虚线箭头与绿色虚线箭头所示,这两者之间的关联主要是通过char_device_struct类型变量的成员cdev实现关联。但这种关联不是必须的,当调用接口__register_chrdev实现字符设备号申请以及cdev创建及添加时,才会实现这种关联。若调用接口register_chrdev_region+cdev_add实现字符设备号的申请以及cdev的添加时,则不会进行char_device_struct类型变量与cdev类型变量之间的关联。针对char_device_struct类型变量与cdev类型变量而言,真正使它们进行关联的是设备号,只要设备号固定,即可在cdev_map和chrdevs中查找到对应的char_device_struct类型变量与cdev类型变量。
 

下图中的 cdev_map是一个指向  struct kobj_map结构体的指针

static struct kobj_map *cdev_map;


 

2.为什么chrdevs这个数组的成员是一个指向char_device_struct结构体链表的指针?也就是说在一个主设备号下(一个chrdevs数组成员下)为什么需要多个char_device_struct结构体?只使用一个不可以吗?

对于chardevs 数组项指向的char_device_struct结构体中的next成员实现多个char_device_struct结构体之间的关联,在系统中主要是完成该类型变量的链接(主设备号相同,次设备号不同且次设备号区间不交叉的结构体变量才会链接在一起

我们在使用register_chrdev_region ( )/alloc_chrdev_region ( ) 会传入设备号dev (包含主设备号和

Linux驱动是连接硬件和操作系统之间的重要桥梁,负责将硬件设备的功能映射为操作系统所能识别和使用的接口。学习和掌握Linux驱动基础知识对于深入理解和应用Linux操作系统具有重要意义。 首先,了解Linux设备模型是学习Linux驱动基础Linux将硬件设备抽象为设备结构体,并将其组织成设备树形结构。理解设备树的组织方式可以帮助我们理解设备的层级关系,加深对设备驱动的认识。 其,掌握设备驱动的注册和初始化过程是理解Linux驱动基础的关键。驱动的注册需要通过构造设备结构体并调用相应的函数实现,这样操作系统才能识别和加载驱动。在驱动注册之后,需要进行设备的初始化,包括设置设备所需的资源、注册中断处理函数等。 理解设备操作方法和中断处理是考察Linux驱动基础的重点。设备操作方法包括打开设备、关闭设备、读取设备和写入设备等功能。通过掌握设备操作方法的实现,我们可以对设备进行控制和读写。中断处理是设备与操作系统交互的重要方式,学习中断处理可以帮助我们理解设备与操作系统之间的异步通信机制。 最后,理解设备的文件系统接口也是考察Linux驱动基础的重要方面。Linux设备抽象成文件,并通过文件系统接口来进行设备的读写操作。掌握设备文件的创建和管理,了解如何通过文件系统接口进行设备的读写,对于理解驱动的应用场景以及与用户空间的交互具有关键作用。 总之,考察Linux驱动基础需要理解Linux设备模型、设备驱动的注册和初始化过程、设备操作方法和中断处理、设备的文件系统接口等多个方面的知识。只有全面掌握这些基础知识,才能深入理解和应用Linux驱动的原理和技术。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值