该系列博客只是个人记录,笔者本人也是菜鸟,如果出现大量错误,欢迎留言评论,请不要进行人身攻击等行为。
让我们先从drivers/input/input.c入手,让我们想一想,当我们modprobe input1.ko时,内核会按时间顺序从1开始分配对应的位号,如果你一直重复modprobe xx.ko然后又rm掉它,那么位号的大小会一直增加,让我们来细究一下底层原理。
当我们使用modprobe命令时,可以看出,shell使用了pr_info打印了相关信息,比如设备的name和路径,以及分配的位号:
跳转到input.c文件:
与上文进行对比,可以看出,这里的pr_info正是modprobe加载后打印的内核日志。
可以看出,本质上还是在使用device_add进行设备的添加,也就是说,最终还是要归类到kobject_add函数:
下面的device_create_file等函数主要是创建对应的目录及文件,我们的目的是寻找引用计数的相关flags。
参考书籍:linux设备驱动程序
在linux内核中,kobject被用来统一描述内核对象模型,设备对象也不例外。
通过函数形参,我们要明白我们追踪的参数是kobj。
跳转到kobject_add函数:
再按f12继续追踪:
此时我们已经离真相非常接近了。
继续跳转到kobject_add_internal(),我们终于找到了kobject_get函数,当被调用时,它会发生递增:
kobject_add_internal函数会先判断kobject是否存在父对象,然后设置父对象,之后创建sysfs目录。
其实到这里已经够了,但还是让我们研究一下kobject_get函数:
现在,我们终于找到了它:kref。它才是真正负责引用计数的flag。
在linux内核中存在一个kref数据结构,它被用来负责引用计数,
简单理解就是这样:
现在,我们来到了这里:
经过跳转,我们会发现丢失了方向,因为这里充满了c语言的高级用法:
进行跳转,我们会发现光标移动到了ATOMIC_OPS这个函数这里,这里的c让笔者这个菜鸟大感震惊:宏函数,静态内联汇编等形成原子操作。
笔者个人理解,在linux kernel中,虽然全局变量被避免了,但是对应统一的对象描述结构来说,时常会受到多方的控制,使用宏是为了模拟高级语言的模板,也就是元编程思想,使用内联汇编是为了完成原子操作,防止被其他对象打断。
ps:笔者从未想过在gnu c中还有如此高级的用法。
简单介绍一下这里的宏:通过#define ATOMIC_OP_RETURN(op),生成一个名为 atomic_##op##_return 的静态内联函数,## 运算符用于在宏展开时连接多个符号。在这里,##op## 将 op 参数插入到 atomic_ 和 _return 之间,形成函数名字,这非常像高级语言中的元编程。
static inline 关键字组合用于定义内联函数,这意味着编译器会尝试将函数内联展开,而不是通过调用来执行代码。
__asm是内联汇编的用法,volatile的作用是为了确保数据的有效性,随便一提,不要小看c语言中的关键字,这个关键字的详细用法如下:
防止编译器优化:编译器在优化代码时,会对变量的读取和写入进行重排或者去掉一些看似不必要的读写操作。对于被声明为 volatile 的变量,编译器必须确保每次访问该变量时都从内存中读取值,而不是使用寄存器中的缓存值。
多线程或中断处理:在多线程或中断处理环境中,变量的值可能会在不同的上下文中被修改,这时候使用 volatile 可以确保正确地获取最新的值。
硬件映射:在嵌入式系统中,通常会将硬件寄存器映射到内存中作为变量来访问,这些寄存器的值可能会在硬件操作之外发生变化,因此需要使用 volatile 来确保对这些寄存器的正确访问。
汇编代码的具体解释:
"1: %0 = memw_locked(%1);\n": 从内存中读取 v->counter 的值到 output 中,memw_locked 表示使用加锁的内存访问。
" %0 = "#op "(%0,%2);\n": 执行具体的原子操作,#op 会被宏展开成不同的操作符(如 add、sub 等)。
" memw_locked(%1,P3)=%0;\n": 将计算后的结果存回到 v->counter 中,同样使用了加锁的内存访问。
" if !P3 jump 1b;\n": 如果加锁失败,则跳转到标签 1 处重新尝试执行操作。
"%0", "%1", "%2": 这些是占位符,分别用来引用汇编代码中操作数的位置。
"=&r" (output): 输出操作数约束,表示 output 变量被当作寄存器且是输出的,& 表示这是一个写入操作。
"r" (&v->counter), "r" (i): 输入操作数约束,&v->counter 是 v->counter 的地址,i 是函数参数 i 的值。
"memory", "p3": 附加的内存和寄存器约束,确保内存操作的正确性和可见性。
笔者的简单解释:
" %0 = "#op "(%0,%2);\n",这里的op被替换成add,也就是:" %0 = "#add "(%0,%2);\n",表示两个参数相加,回到之前:
其实就是加1而已。不得不说在linux内核中加1也是如此的麻烦,只能感慨操作系统是多么的庞大与复杂,一个小小的操作必须要被层层保护。
到此,笔者勉强理清了添加设备时引用计数的逻辑,但是input1文件的递增,关于文件及位号的具体逻辑还没讲,笔者接下来将会研究input子系统的文件及位号关系。
先放张图,避免我忘了下次从哪里开始讲。