不积小流,无以成江海。
分类: LINUX
2023-01-10 09:55:05
该书作者 陈学松,用笔精细,技术大牛可见一斑。
以下是个人摘录,仅做学习之用。
2022-11-17周四
1.2 EXPORT_SYMBOL的内核实现
摘选:
如何导出符号、如何使用导出的符号?背后的内核机制
1、“处理未解决引用”问题的本质是在模块加载期间找到当前“未解决的引用“符号在内存中的实际目标地址。
通过“符号表”的形式向外界导出符号信息。
由EXPORT_SYMBOL等宏导出的符号,与一般的变量定义并没有实质性的差异,唯一的不同点在于它们被放在了特定的section中。
对这些section的使用需要经过一个中间环节,即链接脚本与链接器部分。
2022-11-18周五
kernel_symbol结构的定义:
struct kernel_symbol
{
unsigned long value;
const char *name;
};
其中,value是该符号在内存中的地址,name是符号名。
1.3 模块的加载过程
insmod demodev.ko
step1:insmod先利用文件系统的接口将其数据读取到用户空间的一段内存
step2: 通过系统调用sys_init_module让内核去处理模块加载的整个过程;
sys_init_module
|__load_module 调用load_module
11.22周二
1.3.2 struct module
一个struct module对象代表着现实中的一个内核模块在Linux系统中的抽象
一些重要的成员变量简单描述如下:
enum module_state state
用于记录模块加载过程中不同阶段的状态。module_state的定义如下:
enum module_state
{
//模块被成功加载进系统时的状态
MODULE_STATE_LIVE,
//模块正在加载中
MODULE_STATE_COMING,
//模块正在加载中
MODULE_STATE_GOING,
};
struct list_head list
用来将模块链接到系统维护的内核模块链表中,内核用一个链表来管理系统中所有被成功加载的模块。
struct kernel_param *kp
内核模块参数所在的起始地址。
int (*init)(void)
指向内核模块初始化函数的指针,在内核模块源码中有module_init宏指定。
struct list_head source_list
struct list_head target_list
用来在内核模块间建立依赖关系。
1123周三
1.3.3 load_module
在内核空间构造出devmdev.ko的一个ELF静态的内存试图,称为HDR视图,HDR视图所占用的内存空间在load_module结束时通过vfree予以释放。
字符串表(String Table)
字符串表示ELF文件中的一个section,用来保存ELF文件中各个section的名称或符号名。
2个字符串表:
{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国第一个:section名称字符串表的基地址 secstrings
第二个: 符号表名称字符串的基地址strtab,留作将来使用。
1124周四
HDR视图的{BANNED}中的{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国第一次改写
------------------ 没太看懂!!!
1128周一
HDR视图的第二次改写
使其中Section header table中各entry的sh_addr指向新的也是{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}最佳佳佳佳佳佳佳佳佳佳终的内存地址。
layout_symtab来把符号表搬移到CORE section内存区。
当一个模块被成功加载进系统,初始化工作完成之后,{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}最佳佳佳佳佳佳佳佳佳佳终留下的仅仅是CORE section中的内容,因此CORE section中的数据应是模块在系统中整个存活期会使用到的数据。(图1-4)
1129 周二
模块导出的符号及find_symbol函数
总体上,each_symbol函数分为2个部分,{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国{BANNED}中国第一部分在内核导出的符号表中查找对应的符号,如果找到,就同fsa返回该符号的信息,否则,在第二部分中查找,第二部分在系统已加载模块的导出符号表中查找对应的符号。
1130 周三
对“未解决的引用“符号(unresolved symbol)的处理
所谓的“未解决的引用”符号,就是模块的编译工具链在对模块进行链接生成{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}最佳佳佳佳佳佳佳佳终的.ko文件是,对于模块中调用的一些函数,{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}最佳佳佳佳佳佳佳佳简单的比如printk函数,编译工具无法在该模块的所有目标文件中找到这个函数的具体指令码。
内核中,simplify_symbols的函数实现这一功能。
1202 周五
重定位
重定位主要用来解决静态链接时的符号引用与动态加载时实际符号地址不一致的问题。
这一过程简单地说,就是根据导出符号所在section的relocation section,结合导出符号表section,修改导出符号的地址为内存中{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}{BANNED}最佳佳佳佳佳佳终的地址值。
1205周一
模块参数
page32: module_param宏还是很复杂的。这一小节还没有看完。
1206周二
模块的版本控制
主要用来解决内核模块和内核之间的接口一致性的问题。
使用接口的校验和,也叫接口CRC校验码,根据函数的参数生成一个大小为4字节的CRC校验码,当双方校验码相等时视为相同接口,否则为不同接口。
内核必须首先启用CONFIG_MODVERSIONS这个宏。
模块的信息:modinfo
模块的{BANNED}{BANNED}{BANNED}{BANNED}最佳佳佳佳终ELF文件中都会有一个名为.modinfo的section,这个section以文本的形式保留着模块的一些相关信息。
1208周四
模块的license
模块的license在内核源码中已MODULE_LICENSE宏引出,该宏的主题是MODULE_INFO
non-GPL的模块无法使用内核或其他内核模块用EXPORT_SYMBOL_GPL导出的符号,在加载这样的模块是将出现“Unknown symbol in module”类似的错误信息。
vermagic
也可以看做是模块版本控制的一部分。
1.3.4 sys_init_module(第二部分)
1.3.5 模块的卸载
1、首先将来自用户空间的域加载模块名用strncpy_from_user函数复制内内核空间
2、find_module返回模块的mod结构,没有找到,返回null
3、检查模块的依赖关系。
4、free_module函数更新模块的状态为MODULE_STATE_GOING,将卸载的模块从modules链表中移除,将模块占用的CORE section空间释放,释放模块从用户空间接收的参数所占的空间等。
1209周五
1.4 本章小结
1、内核模块可以在系统运行期间动态扩展系统的功能,这是其{BANNED}最佳大的优势。
2、在用户空间中,加载和卸载面孔使用的是一组称为mod utis的工具包,其中包括{BANNED}最佳基本的insmod和rmmod工具。
3、内核模块在文件格式上是一种可定位的ELF文件,由Linux系统中的内核模块加载器负责加载和卸载。
4、系统中所有成功加载的模块都以链表的形式存放在内核的一个全局变量modules中。
5、模块在编译时需要指定一个内核源码树,这种指定不是出于链接的需要,而是模块需要内核源码头文件中的一些定义,包括以头文件形式出现的内核配置信息。
6、如果内核模块以开发源码的形式向外发布,则版本不一致并不会成为一个问题,用户可以在新版本的内核上重新编译构造新的.ko文件。
1212周一
第二章 字符设备驱动程序
字符设备驱动程序提供的功能是以设备文件的形式提供给用户空间程序使用。
简单的makefile来编译上述的内核模块:
obj-m := demo_chr_dev.o
KERNELDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
rm -f *.o *.ko *.mod.c
如果一切顺利,将得到一个名为demo_chr_dev.ko的内核模块。
1213周二
2.2 struct file_operations
__this_module是内核模块的编译工具链为当前模块产生的struct module类型对象。所以,THIS_MODULE实际上是当前内核模块对象的指针。
2.3 字符设备的内核抽象
struct cdev {
struct kobject kobj; //内嵌的内核对象
struct module *owner;
const struct file_operations *ops;
struct list_head list; //用来将系统中的字符设备形成链表
dev_t dev;
unsigned int count; //隶属于同一主设备号的次设备号的个数,用于表示由当前设备驱动程序控制的实际同类设备的数量
};
cdev_init:初始化一个cdev对象。
一个struct cdev对象在被{BANNED}最佳终加入系统前,都应该被初始化,理由很简单,这是linux系统中字符设备驱动程序框架设计的需要。
2.4 设备号的构成和分配
MAJOR、MINOR、和MKDEV
2.4.2 设备号的分配与管理
1219周一
内核源码中,涉及设备号分配与管理的函数主要有以下2个:
register_chrdev_region:
|__ __register_chrdev_region
函数要完成的主要功能是将当前设备驱动程序要使用的设备号记录到chrdevs数组中。
备注:还没有看完。
1221周三
如果使用的设备号已经被之前某个驱动程序使用了,调用将不会成功。
对主设备号相同的若干struct char_device_struct对象,当系统将其加入链表时,将根据其baseminor成员的大小进行递增排序。
alloc_chrdev_region函数
该函数由系统协助分配设备号,分配的主设备号范围将在1-254之间。
归还设备号:
void unregiser_chrdev_region(dev_t from, unsigned
count);
作为设备驱动程序的实际开发者,没有理由不去遵循这些规则。
1222 周四 冬至
2.5 字符设备的注册
page73:
简单地说,设备驱动程序通过调用cdev_add把它所管理的设备对象的指针嵌入到一个类型为struct probe的节点之中,然后再把该节点加入到cdev_map所实现的哈希链表中。
对系统而言,当设备驱动程序成功调用了cdev_add之后,就意味着一个字符设备对象已经加入到了系统,在需要的时候,系统可以找到它。
对用户态的程序而言,cdev_add调用之后,就已经可以通过文件系统的接口呼叫到我们的驱动程序。
对应的移除函数:cdev_del
1223 周五
设备文件节点的生成
内核空间则使用inode来表示相应的文件。
对于前面的mknod命令,它将通过系统调用sys_mknod进入内核空间,这个系统调用的原型是:
------------------------------------------------------------------------------------
long sys_mknod(const char __user *filename, int mode, unsigned dev)
dev参数是用户空间的mknod命令构造出的设备号。
sys_mknod系统调用将通过/dev目录上挂载的文件系统接口来为/dev/demodev生成一个新的inode,
设备号将被记录到这个新的inode对象上。
(page75基本上看完了)
1226周一
2.7 字符设备的打开操作
page83/84需要再理解下。
第二章基本看完
1227周二
第3章 分配内存
Linux下对内存的管理总体上分为两大类:一是对物理内存的管理;二是对虚拟内存的管理。
前者用于特定的平台架构上;
后者用于特定的处理器体系架构上;
3.1 物理内存的管理
内存节点(node)、内存区域(zone)、内存页(page)
对物理内存的管理总体上分为两大部分:页面级内存管理、基于页面级管理之上的slab内存管理;
两种物理内存管理模型:参见图3-1
UMA: 所有处理器对内存访问速度一致;
NUMA: 本地处理器对内存的访问速度高于其他处理器对该内存的访问。
1229周四
3.1.2 内存区域zone
HIGHMEM:高端内存区域,该区域无法从内核虚拟地址空间直接做线性映射,所以为访问改区域必须经内核做特殊的也映射。在i386体系上,高于896MB以上的物理地址空间叫做ZONE_HIGHMEM区域。
3.1.3 内存页
物理内存管理中的{BANNED}最佳小单位。
系统为每个页都创建一个struct page对象,系统用一个全局变量struct page *mem_map来存在所有物理页page对象的指针,页的大小取决于系统中的MMU,后者用于将虚拟空间的地址转化为物理空间地址。
3.2 页面分配器(page allocator)
页面级的伙伴系统。
页面分配函数的核心成员 alloc_pages、__get_free_pages, 都会调用到alloc_pages_node,
__get_free_page不能在高端内存区分配页面。
1230 周五
3.2.1 gfp_mask
只是页面分配函数中一个重要的参数,告诉内核应该到哪个zone中分配物理内存页面。
{BANNED}最佳常见的2种掩码,
GFP_ATOMIC,用于原子分配,此掩码告诉页面分配器,在分配内存页时,绝对不能中断当前进程或者把当前进程移出调度器。用于中断处理例程或者非进程上下文中使用。
GFP_KERKEL: 内核模块中{BANNED}最佳常使用的掩码之一,带有该掩码的内存分配可能导致当前进程进入睡眠状态。
注意:这2个标志都会使得页面分配器在低端内存域中分配物理页面。且对于slab分配器而言,它只能在低端内存区域分配物理页面。(参见106页)
2023-1-3周二
页面分配函数,比如
__get_dma_pages
3.3 slab分配器
基本思想:先利用页面分配器分配出单个或者一组连续的物理页面,然后在此基础上将整块页面分割成多个相等的小内存单元,以满足小内存空间分配的需要。
2个数据结构:
struct kmem_cache
struct slab
图3-3 slab分配器框架图
2023-01-04周三
slabs_full、slabs_partial、slabs_free
size_cache: 是kmalloc函数实现的基础。
3.3.2 kmalloc与kzalloc
void *kmalloc(size_t size, gfp_t flags)
分配的是一块连续的内存,
flags掩码会影响到伙伴系统对空闲内存页的查找行为。
125页。
2023-01-05周四
kmalloc函数的主体脉络:
建立在slab分配器之上,它的实现主要围绕size_cache展开
void *kmalloc(size_t size, int flags)
{
struct cache_sizes *csizep = malloc_sizes;
struct kmem_cache *cachep;
while (size > csizesp->cs_size)
csizep++;
cachep = csizep->cs_cachep;
return kmem_cache_alloc(cachep, flags);
}
kzalloc函数是kmalloc在设置了__GFP_ZERO情况下的简化版本,kzalloc(size,
flags)就等于kmalloc(size, flags |
__GFP_ZERO), 所以kzalloc函数会用0来填充分配出来的内存空间。
20230106周五
kfree:
void kfree(void *objp)
{
struct kmem_cache *c;
struct page *page = virt_to_page(objp);
c = (struct kmem_cache *)page->lru.next;
__cache_free(c, (void *)objp);
}
kfree释放的内存只能来自kmalloc, 只使用ZONE_NORMAL和ZONE_DMA的物理页。
20230109周一
3.3.3 kmem_cache_create与kmem_cache_alloc
kmem_cache_create来创建内核对象的缓存。/proc/slabinfo可以查看有多少kmem_cache
20230110周二
3.4 内存池(mempool)
总体思想:预先为将来要使用的数据对象(比如a)分配几个内存空间,把这些空间地址存放在内存池对象中。当代码真正需要为a分配空间时,正常调用前面几节提到的分配函数,如果分配失败,那么此时便可从内存池中取得预先分配单好的a的地址空间。
其对实际内存分配失败时的补救措施也只限于预先分配的那些空间。
3.5 虚拟内存的管理
内核代码中用PAGE_OFFSET宏来标示虚拟空间中内核部分的起始地址。
3.5.1 内核虚拟地址空间构成
3.5.2 vmalloc和vfree
vmalloc函数主要对图3-5中的vmalloc区进行操作,它返回的地址就来自于该区域。
实现原理可简单概括为三大步骤:
(1) 在vmalloc区分配出一段连续的虚拟区域。
(2) 通过伙伴系统获得物理页。
(3) 通过对页表的操作将步骤1中分配的虚拟内存映射到步骤2中获得的物理页上。
在vmalloc去中每一个分配出来的虚拟内存块,内核用struct vm_struct对象来表示内核之所以在把size对齐到页面大小之后再加上一个页面的大小,是为了防止可能出现的越界访问。当有访问进入到这个区间时,处理器将会产生异常。
vmalloc用来分配大块内存但无须保证在物理内存空间上的连续性。
图3-6 vmalloc的页面映射
Vfree用来释放vmalloc获得的虚拟地址块,它执行的是vmalloc的反操作;红黑树算法释放vmalloc生成的节点,清除内核页表中对应表项,调用伙伴系统一页一页的释放有vmalloc映射的物理页,kfree掉管理数据所占用的内存。
3.5.3 Ioremap
Ioremap函数及其变种用来将vmalloc区的某段虚拟内存块映射到I/O空间,其实现原理与vmalloc函数基本上完全一样,都是通过在vmalloc区分配虚拟地址块,然后修改内核页表的方式将其映射到设备的内存区,也就是设备的I/O地址空间。
与vmalloc函数不同的是,ioremap并不需要通过伙伴系统分配物理页,因为ioremap要映射的目标地址是I/O空间,不是物理内存。
20230113周五3.6 Per-CPU变量
Per-CPU变量是Linux内核中一个非常有趣的特性,它为系统中每个处理器部分都分配了该变量的一个副本。这样做的好处是,在多处理器系统中,当处理器操作属于它的变量副本时,不需要考虑与其他处理器竞争的问题,同时该副本还可以充分利用处理器本地的硬件缓存以提高访问速度。
典型应用是在统计计数方面,eg,lib/percpu_counter.c
网络系统中,内核统计接收到的各类数据包的数量。
20230116周一
关于动态per-CPU变量的分配机制,内核中的相关代码比较繁琐,但是核心思想同静态per-CPU是一样的,大体可分为两部分:第一部分,为系统中的每个CPU分配副本的空间;第二部分,通过某种机制实现对CPU特定的副本空间的访问。
3.7 本章小结
至于系统启动阶段的bootmem内存分配机制,因为只存在于系统启动阶段内核内存管理模块框架建立起来之前使用, 所以设备驱动程序使用这种内存分配机制的机会非常少。
20230117周二
第4章 互斥与同步
互斥是指对资源的排他性访问,而同步则要对进程执行的先后顺序做出妥善的安排。
所谓竞态,简而言之,就是多个执行路径有可能对同一资源进行操作时可能导致的资源数据紊乱的行为。
并发的来源:
中断处理路径:中断处理函数和被中断的进程之间形成的并发。
调度器的可抢占性:在单处理器上,因为调度器的可抢占特性,导致的进程与进程之间的并发。这种行为非常类似多处理器系统上进程间的并发。
多处理器的并发执行:多处理器系统上进程与进程之间是严格意义上的并发,每个处理器都可以独自调度运行一个进程,在同一个时刻有多个进程在同时运行。
20230128 周六---春节后第一天
4.2 local_irq_enable/local_irq_disable
在单处理器不可强制系统中,local_irq_enable/local_irq_disable是消除异步并发原的有效方式。
变体:local_irq_save/local_irq_restore
是通过关中断的方式进行互斥保护,所以必须确保两者之间的代码执行时间不能太长,否则将影响到系统的性能。
4.3 自旋锁
设计自旋锁的目的是在多处理器系统中提供对共享数据的保护,其背后的核心思想是:
设置一个在多处理器之间共享的全局变量锁V,并定义当V=1时为上锁状态,V=0时为解锁状态。
代码实现时的关键之处在于,必须确保处理器A“读取V,判断V的值与更新V”这一操作序列是个原子操作(atomic operation)。所谓原子操作,简单地说就是执行这个操作的指令序列在处理器上执行时等同于单条指令,也即该操作序列在执行时是不可分割的。
20230129周日
4.3.1 spin_lock
page 131~133还需要看下。
20230131周二
4.3.1 spin_lock
不同的处理器上有不同的指令用以实现上述的原子操作。
比如arm上的实现:
先要关闭内核的可抢占性。
真正的上锁操作发生在后面的do_raw_spin_lock函数中。嵌入汇编代码
Arm处理器上专门用以实现互斥访问的指令ldrex、strex来达到原子操作的目的。
ldr、str
spin_unlock用来释放此前获得的自旋锁。先do_raw_spin_unlock,再 preempt_enable恢复可抢占性。
20230202 周四
4.3.4 读取者与写入者自旋锁rwlock
这种锁比较有意思的地方在于:它允许任意数量的读取者同时进入临界区,但写入者必须进行互斥访问。
一个进程想去读的话,必须检查是否有进程正在写,有的话必须自旋,否则可以获得锁。
一个进程想去写的话,必须先检查是否有进程正在读或者写,有的话必须自旋。
重点:写入者的上锁操作、解锁操作;读取者的上锁、解锁操作的过程还没有了解;
4.4 信号量(semaphore)
相对于自旋锁,信号量的最大特点是允许调用它的线程进入睡眠状态。这意味着试图获得某一信号量的进程会导致对处理器拥有权的丧失,也即出现进程的切换。
20230203 周五
4.4.1 信号量的定义与初始化
定义如下:
struct semaphore {
spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
其中,lock是个自旋锁变量,用于实现对信号量的另一个成员count的原子操作。
Count用于表示通过该信号量允许进入临界区的执行路径的个数。
Wait_list用于管理所有在该信号量上睡眠的进程,无法获得该信号量的进程将进入睡眠状态。
信号量上的操作主要是DOWN和UP
驱动程序使用最频繁的是down_interruptible来获得信号量。
即使不是信号量的拥有者,也可以调用up函数来释放一个信号量,这点与下节介绍的mutex是不同的。
Ps:使用的示例代码demo_write挺好。
4.4.4 读取者与写入者信号量rwsem
类似于读取者与写入者自旋锁,不再记录。
20230206周一
4.5互斥锁mutex
Linux内核针对count=1的信号量重新定义了一个新的数据结构struct mutex,一般称其为互斥锁或者互斥体。
4.6 顺序锁 seqlock
顺序锁的设计思想是,对某一个共享数据读取时不加锁,写的时候加锁。
typede struct {
unsigned sequence;
spinlock_t lock;
} seqlock_t;
无符号整型sequence用来协调读取者与写入者的操作 spinlock变量在多个写入者之间做互斥使用。
2023-02-07 周二
要求写入者在开始写入的时候要更新sequence的值。
Seqlock在写的时候只与其他写入者互斥,而rwlock在写的时候与读取者和写入者都互斥。
因此当要保护的资源很小很简单,会很频繁被访问并且写入操作发生且必须快速时,就可以使用seqlock。
4.7 RCU
全称是Read-Copy-Update,意即读/写-复制-更新,在Linux提供的所有内核互斥设施当中属于一种免锁机制。
读至156页。
Ps:smp_rmb() 适用于多处理器的读内存屏障。 smp_wmb() 适用于多处理器的写内存屏障。
网上的帖子: Linux RCU机制+内存屏障
https://
blog.csdn.net/lqy971966/article/details/118993557
20230208 周三
4.7.1 读取者的RCU临界区
对于读取者的一个明确的规则是,对指针的引用必须在临界区中完成。临界区中的代码不能发生睡眠。简言之,临界区中的代码不应该导致任何形式的进程切换。
rcu_read_lock和rcu_read_unlock实际要做的工作仅仅是分别关闭和打开内核的可抢占性而已。
内核确定没有对老指针的引用的条件是:系统中所有处理器上都至少发生了一次进程切换。
4.7.3 RCU使用的特点
RCU实质上是对读取者与写入者自旋锁rwlock的一种优化。
RCU的设计思想决定了必须要以指针的方式来访问被保护资源。
20230209 周四
4.8 原子变量与位操作
atomic_t的原子变量
Typedef struct {
int counter;
} atomic_t;
X86上有一条带有“lock”前缀的inc指令来保证原子操作v加1操作的原子性。
“lock”前缀在x86上的作用是在执行inc指令时独占系统总线。
与原子变量相对的是位操作的原子性,其实现原理和原子变量一样,依赖于特定的处理器指令实现对变量上的位进行原子性的操作和测试。
4.9 等待队列
等待队列并不是一种互斥机制。
等待队列本质上是一双向链表,由等待队列头和队列节点头构成。当运行的进程要获得某一资源而暂不可得时,进程有时候需要等待,此时它可以进入睡眠状态,内核为此生成一个新的等待队列节点将睡眠的进程挂载到等待队列中。
定义一个等待队列有两种方法。
一是通过DECLARE_WAIT_QUEUE_HEAD宏来完成等待队列头对象的静态定义与初始化。
二是通过init_waitqueue_head宏在程序运行期间初始化一个头节点对象。
4.9.3 等待队列的应用
等待队列常用的模式便是实现进程的睡眠等待。内核中对等待队列的核心操作是等待(wait)与唤醒(wake up)。
4.10 完成接口 completion
该机制被用来在多个执行路径间作同步使用,也即协调多个执行路径的执行顺序。
相关实现:completion.h
注:未看完此节。
20230210 周五
(接上)
内核中用一个数据结构struct completion表示,定义如下:
struct completion {
unsigned int done;
wait_queue_head_t wait;
};
其中,done表示当前completion的状态, wait是一个等待队列,用例管理当前等待在该completion上的所有进程。
完成接口completion对执行路径间的同步可以通过等待者与完成者模型来表述。
2023-02-13 周一
5.1 中断的硬件框架
可编程中断控制器PIC(Pragrammable Interrupt Controller)
外部设备(1….n)-------------- PIC ------------ CPU
PIC的输出中断信号线连接到处理器的INT引脚上。
PIC有的在CPU外部;有的被封装到了CPU的内部,这广泛见于嵌入式领域,一颗SoC芯片内部集成了处理器和各种外部设备的控制器,其中包括PIC。
5.2 PIC与软件中断号
中断向量表中的内容由操作系统在初始化阶段来填写,对于外部中断,操作系统负责实现一个通用的外部中断处理函数,然后把这个函数的入口地址放到中断向量表中的对应位置。
20230214 周二
当有外部中断发生时,预先设计好的处理器硬件逻辑常常会做一些特定的动作,为从软件层面发起的中断处理做准备。
但这些动作常常包括把当前任务的上下文寄存器保存在一个特定的中断栈中,屏蔽掉处理器响应外部中断的能力等。
在这些动作的结束部分,硬件逻辑会根据中断向量表中的外部中断对应的入口地址,开始调用有操作系统提供的通用中断处理函数。
在开始部分,都会设法从PIC中得到导致本次中断发生的外部设备对应的软件中断号irq,这部分代码通常都用汇编语言实现。
然后调用一个C函数,大部分平台上叫do_IRQ
在do_IRQ函数中,对irq_enter的调用可以认为是HARDIRQ部分的开始,而SOFTIRQ则是在irq_exit中完成。
5.4 do_IRQ函数
do_IRQ的核心是调用generic_handle_irq函数,后者在其函数调用链中负责对当前发生的中断进行实际的处理:
static inline void generic_handle_irq(unsigned int irq)
{
struct irq_desc *desc = &irq_desc[irq];
desc->handle_irq(irq, desc);
}
struct irq_desc的组成结构见5-2 irq_desc数组的构成形式(page174)
20230215 周三Struct irq_data中的struct irq_chip *chip成员用来表示一个PIC对象。PIC对象用来实现对PIC的配置,配置工作主要包括设定外部设备的中断触发信号的类型、屏蔽或者启动某一个设备的中断信号、向发出中断请求的设备发送中断响应信号等。
5.6 struct irqaction
设备驱动程序通过这个结构将其中断处理函数挂载在action上。
20230217 周五
Request_irq
|___request_threaded_irq
Desc->action不为空,这种情形表明当前irq所对应的中断线此前已经被安装了中断处理函数,换言之,这意味着正在安装一个共享该irq的中断处理例程。
request_irq_proc在/proc/irq目录下创建类似/proc/irq/125这样的目录项。
5.10 中断处理的irq_thread机制
内核为中断处理提供的另一种机制,这种机制在设备驱动程序通过调用request_threaded_irq函数来安装一个中断时,需要在struct irqaction对象中实现他的thread_fn成员。request_threaded_irq函数内部会生成一个名为irq_thread的独立线程。
Irq_thread线程被创建出来时将以TASK_INTERRUPTIBLE的状态睡眠等待中断的发生,当中断发生时,action->handler只负责唤醒睡眠的irq_thread, 后者将调用action->thread_fn进行实际的中断处理工作。因为irq_thread本质上是系统中的一个独立进程,所以采用这种机制将使实质的中断处理工作发生在进程空间,而不是中断上下文中。
20230220 周一SOFTIRQ的处理是在do_IRQ()函数的irq_exit中实现的。
irq_exit()
|_invoke_softirq
|_ _do_softirq() 或do_softirq()
HARDIRQ部分结束之后,内核已经启动可抢占性。
下面这段没有读懂:
只限于非共享中断的情况。
探测前的情形是,该设备关联到了某个irq,但是因为设备驱动程序还不清楚是哪个irq,因此不可能调用request_irq来向该irq安装中断处理例程,所以对应的irq的action为空。
探测要完成的任务是找到该设备所关联的irq。
5.14 中断处理例程
一个实际的中断处理例程应该这样声明自己:
Irqreturn_t demo_isr(int irq, void *dev_id);
Enum irqreturn {
IRQ_NONE,
IRQ_HADNLED,
IRQ_WAKE_THREAD,
};
Typedef enum irqreturn irqreturn_t;
IRQ_NONE
中断例程发现正在处理一个不是自己的设备触发的中断,此时它唯一要做的就是返回该值。
IRQ_HANDLED
中断处理例程成功的处理了自己设备的中断,返回该值。
IRQ_WAKE_THREAD
中断处理例程被用来唤醒一个等待在它的irq上的一个进程使用,此时它返回该值。
确保中断处理上下文不出现进程切换
最后,中断处理例程作为系统中的一种并发源头,可能会去访问一些共享的资源,需要考虑互斥的机制来保护。防止出现睡眠的可能性,因此信号量和互斥锁首先就会被排除掉,绝大多数的情况下你需要使用自旋锁spin_lock及其变体。
5.15 中断共享
场景:即便PIC已经提供足够多的中断引脚供外部设备使用,但也有不够用的时候,此时中断共享机制可能就会派上用场。
所谓中断共享是指多个设备共享一根中断线,使用同一个irq。
对于一个共享的中断,驱动程序在调用request_irq时应该使用IRQF_SHARED标志,同时提供dev_id, 提供的dev_id在中断处理例程中并没什么特别的用处,主要是在free_irq时能在action链中找到它。
中断共享时的中断处理例程实现:当irq上的中断发生时,内核会调用irq上的每个action中的hander,因此即便不是你的设备产生的中断,你的中断处理例程ISR也会被调用到。
因此共享中断时的ISR需要能判断是否是自己的设备产生的中断,这主要靠读取自己设备的中断状态寄存器来完成。如果发现你的设备没有产生中断,那么ISR只需要返回一个IRQ_NONE就好了。
2023-02-22 周二内核给驱动程序提供了一个基于SOFTIRQ的任务延迟的实现机制tasklet。因为tasklet需要在中断上下文中执行,所以有些延时的操作无法用tasklet来完成。为此内核又提供了一个基于进程的延时操作实现机制---------工作队列workqueue。
定时器timer也可以用来实现延时的操作。
ps备注:tasklet描述的比较详细,没有仔细研读。工作中尚未接触到。
20230223 周四
6.2 工作队列
数据结构:
struct work_struct {
Atomic_long_t data;
Struct list_head entry;
work_func_t func;
};
备注:这部分内容比较难,先粗略的“扫”一下。需要专门研读。
6.3 本章小结
相对于tasklet,工作队列的延迟函数是在一个独立的进程环境下运行的。所以,允许睡眠。
同tasklet对象一样,当work对象被处理完毕后除非再次提交,否则将不再有执行的机会。
20230224 周五
设备文件的高级操作
包括驱动程序最常用的ioctl、阻塞性I/O、poll,以及异步通知机制等。
7.1 ioctl文件操作
Ioctl一般用来在用户空间的应用程序和驱动程序模块之间传递控制参数,而很少用于大量数据的传递。
1、用户空间打开一个文件,内核将为之分配一个文件描述符fd
2、把fd作为参数传递给ioctl函数
3、在ioctl的系统调用函数sys_ioctl中 将会用fd作为进程管理的文件描述符表的索引,继而得到fd所对应的struct file对象的指针filp,这个filp对象在之前打开文件的操作中也被创建并初始化,其中最重要的初始化是把设备对象cdev中的ops指针赋给了filp->f_op, filp->f_op将调用到驱动程序提供的文件操作函数。
Lock_kernel和unlock_kernel是一种粗粒度的所谓大内核锁BKL(big Kernel Lock),会明显降低系统的性能。
现代的设备驱动程序应该使用unlocked_ioctl, 它们已经脱离了大内核锁的保护,因此驱动程序在实现unlocked_ioctl函数时,应该使用自己的互斥锁机制。
20230227 周一
7.1.2 ioctl的命令编码
参考图7-1 ioctl cmd参数构成。
7.1.3 copy_from_user和copy_to_user
Ps:下面这段没有读懂,应该是“返回用户空间的值,来作为交换的一种方式,不好”
20230301 周三
7.2 字符设备的I/O模型
图7-2简单描述了4种I/O:同步阻塞、同步非阻塞、异步阻塞、异步非阻塞。
’Linux系统的设备中,块设备和网络设备的I/O模型属于异步非阻塞型。
7.3 同步阻塞型I/O
驱动程序需要实现read、write方法。核心函数是wait_event系列和wake_up系列。
7.3.1 wait_event_interruptible
这个宏用来将当前调用它的进程睡眠等待在一个event上知道进程被唤醒并且需要的condition条件为真。
20230302 周四
7.3.2 wake_up_interruptible
宏wake_up_interruptible用来唤醒一个等待队列上的睡眠进程
虽然使用的规则可简单归纳为:wait_event和wait_up, wait_event_interruptible和wake_up_interruptible分别配对使用,但最好对各个函数背后的行为机制有个清晰的认识。
7.5 异步阻塞型I/O
本节讨论驱动程序中如何支持设备的异步阻塞型I/O操作模式,反映到file_operations对象上,就是讨论在设备驱动程序中如何实现poll方法。
在用户空间,应用程序将要操作的一组文件的描述符加入到一个集合中,然后在这个集合的基础上使用这些函数来监控其中的每个文件描述符;倘若集合中的每个文件目前都不可以进行读取写入操作,进行也许会因此而被阻塞,直到该集合中的任一文件可读或者可写。
3个函数:poll、select和epll。
Ps:这节比较长,今天先到这里。
用户空间poll
->sys_poll系统调用->do_sys_poll核心功能
这一部分确实比较多,限于业务繁忙,今天暂且到这里。ToPage 253.
备注:读到257页。重要的是图7-3 poll实现框架。
驱动程序对poll特性的支持实际上可以分为两部分:第一部分是poll例程本身,那里它将某一等待节点对象加入到自己管理的等待队列中;第二部分是数据就绪后的唤醒操作。
对于一个驱动程序而言,为了实现自己的poll例程,需要构造自己的等待队列,然后通过调用poll_wait将一个等待节点加入到自己的等待队列中,poll_wait函数内部负责从pwq对象中申请容纳等待节点的空间并对其初始化,其中的唤醒函数是pollwake。等待节点对象来自于内核,因此内核可以在随后的poll_freewait函数中将这些等待节点清理掉。
7.7 驱动程序的fsync例程
fsync用来同步设备的写入操作。
驱动程序需要针对不同的设备实现write和fsync例程,以满足上层应用程序调用两者时所期望的语义。
7.10 访问权限
当前看起来跟工作不相关,先不记录了。
7.11 本章小结
fasync用来实现一个异步通知机制,用户程序通过fcntl函数来向设备驱动程序表明是否希望在某一事件出现时得到通知。设备驱动程序在实现fasync例程时主要依赖两个内核提供的函数:fasync_helper和kill_fasync,前者将需要通知的进程加入一个链表,后者在应用程序关注的事件发生时通过信号发送的方式来通知应用程序。
20230317 周五
设备驱动程序需要对时间进行操作,典型的可以分为两大类:延时与定时。
时间滴答:jiffies
除了时钟中断处理例程中对jiffies进程更新外,其他任何模块都只是读取该值以获得当前时钟计数。
Linux设备驱动程序中使用jiffies的几个常用的场景:时间比较、时间转换、以及定时器。
20230407 周五
延时的典型场景:
Write_command_reg();
Delay();
Read_status_reg();
延时典型的2种模式: 忙等待与让出cpu;
P282有些地方没有看懂;
Cpu_relax的实现有空指令(NOP)等,schedule是主动让出cpu;
20230410-周一
(长延时续)
使用schedule_timeout可以确保在指定的延迟时间到期时进程可以重新获得调度的机会,因为结合了对set_current_state(TASK_UNINTERRUTIBLE)的一起使用,使得进程在指定的延时时间段内不会出现在运行队列中,这就很好的解决了单纯调用schedule函数带来的问题。
在“让出处理器”实现长延时的方案中,直接调用内核模块提供的msleep类的函数是最简单最方便的一种方式了。
20230411 周二
短延时:基于忙等待实现;
内核定时器:设备驱动程序中经常用到的另一个重要的基础设施。用来实现轮询机制。
Ps:8.3这段没有看得详细,用到时再仔细看下。
第九章 linux设备驱动模型
内核创建这块vfs树主要用来沟通系统中总线、设备与驱动,同时向用户空间提供接口及展示系统中各种设备的拓展视图等,事实上它并不用来作为其他实际文件系统的挂载点。
Sysfs_get_sb
|__sysfs_fill_super
|___sysfs_init_inode
Sysfs文件系统时个基于RAM实现的文件系统,如果编译内核是指定了CONFIG_SYSFS选项,那么这个系统就会包含到内核中。
9.2 kobject和kset
Linux实现设备驱动模型的底层数据结构kobject和kset
设备驱动模型中的bus、device和driver已经是整座大厦向外界展示的那部分了。
如果内核对象加入系统,那么它的name将会出现在sysfs文件系统中(表现形式是一个新的目录名)。
Kset对象代表一个subsystem,其中容纳了一系列同类型的kobject对象。
Kobject_add
主要功能有两个,一是建立kobject对象间的层次关系,二是在sysfs文件系统中建立一个目录。在将一个kobject对象通过kobject_add函数调用加入系统前,kobject对象必须已被初始化。
Ps:看到p304
内核通过kobject属性文件的方式给用户空间程序提供了一种显示与更新某一内核对象kobject上属性信息的接口。
9.2.3 kset
Kset可以认为是一组kobject的集合,是kobject的容器。
Kset本身也是一个内核对象,所以需要内嵌一个kobject对象。
P310:kset将所以隶属于他的kobject对象放到一个链表list中,同时可以看到kset的数据结构中内嵌了一个kobject成员。
9.2.4 热插拔中的uevent和call_usermodehelper
Read to p314
在linux系统上有两种机制可以在设备状态发生变化时,通知用户空间去加载或卸载该设备所对应的驱动模块:一个是udev,另一个是/sbin/hotplug(早期阶段,它的幕后推手是call_usermodehelper函数,后者能够从内核空间启动一个用户空间的应用程序)。
Udev的实现基于内核中的网络机制,它通过创建标准的socket接口来监听来的内核的网络广播包,并对接收到的包进行分析处理。
热插拔的实现:kobject_uevent函数。
(继续热插拔的函数实现)
第三部分是kobject_uevent_env函数的亮点,也是最有趣的地方,主要用来和用户空间进程进行交互(或者在内核空间启动执行一个用户空间的程序)。
在Linux内核中,有两种方式完成这项任务,一个是代码中有CONFIG_NET宏包含的部分,这部分代码通过netlink的方式向用户空间广播当前kset对象中的uevent消息。另一种是在内核空间启动一个用户空间的进程,通过该进程传递内核设定的环境变量的方式来通知用户空间kset对象中的uevent事件。
Call_usermodehelper函数
__call_usermodehelper
|___kernel_thread
Kernel_thread会产生一个新的进程,当改进程被调度执行时,____call_usermodehelper函数会被调用。
____call_usermodehelper
|___kernel_execve 用来在内核空间运行一个用户空间的进程。
kernel_execve是个系统架构相关的函数。(内嵌汇编指令, int $0x80, 导致一个系统调用。)
ps: 相关代码在P319,有个印象,还没有看的很明白。
20230421 周五
9.2.5 实例
(作者功力的体现)
一个完整的源码来展示如何创建、初始化并向系统添加一个kobject对象,以及如何通过sysfs文件系统接口在用户空间和内核空间进行沟通,另一个有趣的事情是它通过/sbin/hotplug机制来通知用户空间某一个kobject状态的变化。
见P327
如果系统中有udevd守护进程,那么它应该一直在监听kobject_uevent通过netlink广播出去的uevent数据包。无论如何,内核空间通过kobject_uevent这个函数实现了将内核中发生的一些事件通知到了用户空间。
9.3 总线、设备与驱动
9.3.1 总线及其注册
看到331页。
bus_type结构定义、各成员的说明。
图9-3 bus、dev与drv的层次关系。
Linux内核中针对总线的一些主要操作有:
Buses_init
揭示了总线在系统中的起源。
Buses_init将在sysfs文件系统的跟目录下建立一个“bus”目录,在用户空间看来,就是/sys/bus, buses_init函数创建的“bus”总线将是系统中所有后续注册总线的祖先。
Bus_register
向系统中注册一个bus
Ps:图9-4 通过bus_register向系统注册一根总线bus1 比较重要有意义。
9.3.2 总线的属性
总线属性代表着该总线特有的信息与配置。
根据实际需要,可以为总线创建不止一个属性文件,每个文件代表该总线的一个或一组属性信息。
9.3.3 设备与驱动的绑定
绑定,简单地说就是将一个设备与能控制它的驱动程序结合到一起的行为。
在总线上发生的两类事件将导致设备与驱动绑定行为的发生:
一种通过device_register函数向某一bus上注册一设备,这种情况下内核除了将设备加入到bus上的设备链表的尾端,同时会试图将此设备与总线上的所有驱动对象进行绑定操作。
二是通过driver_register将某一驱动注册到所属的bus上,内核此时除了将该驱动对象加入到bus的所有驱动对象构成的链表的尾部,也会试图将该驱动与其上的所有设备进行绑定操作。
Klist_add_tail
从代码的角度看,两者之间通过某种数据结构的使用建立了一种关联的渠道。
9.3.4 设备
数据结构:struct device
内核将系统中的设备类型分为两大类:block和char,内核对象分别为sysfs_dev_block_kobj, sysfs_dev_char_kobj, 上级内核对象为dev_kobj。
在Linux系统初始化期间有devices_init来完成。
Device_add是个非常重要的函数。其重要的功能分成如下几个部分:
1、 在sysfs文件系统中建立系统硬件拓扑关系结构图
2、 在sysfs文件树中创建与改dev对象对应的属性文件
Ps:读至344页。
图9-5 device_add添加一名“demodev”设备后的sysfs拓扑图
属性文件以文件的形式向用户空间的程序提供了一个显示和更改内核对象属性的方法。这种显示和更改内核对象属性的方法由创建该内核对象的模块提供。
通过devtmpfs文件系统,就可以在device_register注册设备时自动向/dev目录添加设备节点,该节点的名字就是dev->init_name。
关于devtmpfs文件系统,它是内核建立的另一颗独立的VFS树,最终挂载(mount)到用户空间的/dev目录之上,在内核中devtmpfs主要用来动态生成设备节点。
一个体现设备驱动模型中总线、设备与驱动相互沟通的重要函数调用bus_probe_device(dev)
Ps: 读到348页。
9.3.5驱动
数据结构:struct device_driver
操作:
Driver_find:在一个bus的drivers_kset集合中查找指定的驱动。
Driver_register:向系统注册一个驱动
Driver_unregister:将一个指定的驱动从系统中注销掉。基本上是driver_register的反向工作。
9.4 class
相对于设备device,是一种更高层次的抽象,用于对设备进行功能上的划分,有时候也被称为设备类。Linux的设备模型引入类,是将其用来作为同类型功能设备的一个容器。
9.5 小结
基础底层数据结构:kobject、kset
然后通过sysfs文件系统向用户空间展示发生在内核空间中的个组件见的互联层次关系,并以文件系统接口的方式为用户空间程序提供了访问内核对象属性信息的简易方法。
Linux设备模型通过总线将系统中的设备和驱动关联起来,由于设备和驱动的分离,增加了系统设计的灵活性,伴随而来的代价就是增加了复杂度。
第10章 内存映射与DMA
10.1 设备缓存与设备内存
设备缓存是由驱动程序管理的位于系统主存RAM中的一段内存区域,而设备内存则是设备所固有的一段存储空间(比如某些设备的FIFO,显卡设备的Frame Buffer等),从设备驱动程序的角度,它应该属于特定设备的硬件范畴,与设备是紧密相关的。
Linux系统下,典型用法是在两者之间建立DMA通道,这样当设备内存中收到的数据达到一定的阈值时,设备将启动DMA通道将数据从数据内存传输到位于主存中的设备缓存中,
发送数据则正好相反,需要发送的数据首先被放到设备缓存中,然后在设备驱动程序的介入下启动DMA传输,将缓存中的数据传输到设备内存中。
10.2.2 用户空间虚拟地址布局
Mmap_is_legacy()来判断采用传统布局还是新式布局。
传统布局和新式布局的主要区别在于MMAP区域的扩展方向。
一个进程虚拟地址空间中MMAP区域的状态如图10-2 所示。
每个区域有一个struct vm_area_struct对象表示。
大体上,这种用户虚拟地址映射到设备内存的过程可以概括为:
内核先在进程虚拟地址空间的MMAP区域分配一个空闲(尚未映射)的struct vm_area_struct对象,然后通过页目录表项的方式将struct vm_area_struct对象所代表的虚拟地址空间映射到设备的存储空间中。
如此,用户进程将可以直接访问设备的存储区,从而提高系统性能。
页目录表现的继而也意味着每个vm_area_struct对象表示的地址空间应该是页对齐的,大小是页的整数倍。
10.2.3 mmap系统调用过程
用户空间mmap
|_____系统调用 sys_mmap_pgoff (进入内核)
|_____do_mmap_pgoff 完成后续的内存映射工作
Ps:读至365页。
error = file->f_op->mmap(file, vma)
内核为即将进行的内存映射准备了struct vm_area_struct对象,并且调用了设备驱动程序中的mmap方法。
10.2.4 驱动程序中mmap方法的实现
驱动程序需要在自己的mmap方法的实现代码中完成页目录项的配置以便将vma对象表示的虚拟地址映射到对应的物理内存上。
Ps:将开始369页。
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn, unsigned long size, pgprot_t prot);
函数可以将参数addr开始的大小为size的虚拟地址空间映射到pfn表示的一组连续的物理页面上,pfn是页框号(page frame number),在页面大小为4KB的系统中,一个物理地址右移12位即可得到该物理地址对应的页框号。简言之,函数为【addr,addr+size】范围的虚拟地址建立页目录表项,将其映射到以pfn开始的物理页面上。
Ps:这个函数的总体流程这块看的不是很明白,需要重新再看以理解。
10.2.5 mmap使用范例
内核源码 cpia2驱动程序实现了自己mmap方法来将用户空间地址映射到期设备内存(frame buffer)上。
SetPageReserved设置的页面可以确保不会被交换出去。
Ps:to376
图10-5 cpia的remap_pfn_range
驱动通过vmalloc函数分配设备内存的虚拟地址,然后将这段虚拟地址映射的物理页面(可能是不连续的)映射到用户进程的地址空间。如此应用程序就可以直接使用这些给设备帧缓存使用的物理页面,而无须经内核周转。
现实中设备缓存的典型用法实在设备内存与设备缓存间建立一个DMA通道,这样设备的数据将以极高性能传递到驱动程序所管理的设备缓存中。
Ps:to378页。有些疲惫,瞌睡。。。
反映到驱动程序的file_operations对象上,里面只有mmap方法而无对应的munmap。
设备驱动程序无须为munmap行为做特定的工作。
10.3 DMA
直接内存访问DMA用来在设备内存与主存RAM之间直接进行数据交换,无CPU的干预。
内核的DMA层、DMA内存映射(一致性DMA映射、流式DMA映射和分散/聚集映射)。
Ps: to P386。
10.3.2 物理地址与总线地址
所谓CPU物理地址,即CPU的地址信号线上产生的地址。
对于X86架构而言,需要专门的I/O指令,至于访问的是系统主存还是I/O空间,需由特定的控制信号线决定。
而对于ARM架构,主存和I/O空间统一编址,由统一的指令访问。
而总线地址,可以简单认为是从设备角度看到的地址,不同类型的总线具有不同类型的总线地址,目前最常见的是PCI总线。
对于设备驱动程序员而言,其实更关心的是DMA地址,它用来在设备和主存之间寻址。
从内核的角度,它被叫做DMA地址,数据结构是dma_addr_t.
10.3.3 dma_set_mask
该函数用来查询设备的DMA寻址范围。
设备对象dev在其成员dma_mask中来标识其DMA寻址范围。
为了支持dma_set_mask功能,dev需要提供起dma_ops成员上的dma_supported方法的实现。
Ps:toPage388
10.3.4 DMA映射
主要为在设备与主存之间建立DMA数据传输通道时,在主存中为该DMA通道分配内存空间的行为,该内存空间也称为DMA缓冲区。
DMA映射的3种情况:
一致性DMA映射:x86通过硬件解决,arm通过软件来解决。
Ps:toPage392
10.3.4 DMA映射
主要为在设备与主存之间建立DMA数据传输通道时,在主存中为该DMA通道分配内存空间的行为,该内存空间也称为DMA缓冲区。
DMA映射的3种情况:
一致性DMA映射:x86通过硬件解决,arm通过软件来解决。
Ps:toPage392.
一致性映射最根本的操作是获得一组连续的物理页用作后续DMA操作的缓冲区。
对于x86平台而言,因为硬件保证了cache一致性,所以x86平台的一致性映射最为简单;
对于ARM平台而言, 因为没有硬件参与解决cache一致性的问题,所以在软件层面上,通过重新映射新获得的物理地址空间,在页目录和页表面中关闭了这端映射区间上cache功能,所以使得cache一致性也不再成为问题。
适用:驱动程序自己分配的内存。
流式DMA映射
特点:
DMA传输通道所使用的缓冲区往往不是当前驱动程序自身分配的,而且往往对每次DMA传输都会重新建立一个流式映射的缓冲区。
此外由于无法确定处理外部模块传入的DMA缓冲区的映射情况,所以使用流式DMA映射是,设备驱动程序必须小心处理可能出现的cache一致性问题。
只有内核空间中的线性映射区中的地址才可以通过__pa和__va宏来作物理地址和虚拟地址的转换,更具体地,kmalloc分配的虚拟地址可以做__pa操作,vmalloc则不可以。
场景:
当驱动程序主导去分配一个DMA缓冲区并且该缓冲区存在周期与所在的驱动模块一样长时,就是用一致性DMA映射的好时机,这种映射类型的缓冲区因为在驱动程序一开始为DMA分配缓冲区是就解决了cache一致性问题,所以后续的DMA相关操作无须再考虑这一问题。
如果驱动程序使用从别的模块传进来的地址空间作为DMA缓冲区,那么就需要考虑使用流式DMA映射。
关键点:建立流式DMA映射的关键点有两个:
一是确保CPU侧的虚拟地址所对应的物理地址能够被设备DMA正确访问;
二是确保cache一致性的问题。
Ps:toPage397,很多细节要理解。
分散/聚集映射(scatter/gather map)
分散/聚集映射通过将虚拟地址上分散的DMA缓冲区通过一个类型为struct scatterlist的数组或者链表组织起来,然后通过一次的DMA传输操作在主存RAM与设备之间传输数据,如图10-9所示:
分散/聚集DMA映射本质上是通过一次DMA操作把主存中分散的数据快在主存与设备之间进行传输,对于其中的每个数据块内核都会建立对应的一个流式DMA映射。
另外,分散/聚集DMA映射需要设备的支持,而不完全有内核或者驱动程序决定。
Ps:toPage401
10.3.5 回弹缓冲区
如果CPU侧虚拟地址对应的物理地址不适合设备的DMA操作,那么需要建立所谓的回弹缓冲区,它相当于一个中转站的作用,把数据往设备方向传输时,驱动程序需要把CPU给的数据拷贝到回弹缓冲区,然后再启动DMA操作,反之亦然。
10.3.6 DMA池
Ps:待看完这一节再记录。
DMA池机制非常类似于Linux内存管理中的slab机制,它的实现建立在一致性DMA映射所获得的连续物理页面的基础之上,通过DMA池的接口函数在物理页面之上分配所谓块大小的DMA缓冲区(本书称这样的块为DMA缓冲块)
20230605 周一
第11章 块设备驱动程序
本章主要有3个部分:
第一部分讨论块设备与系统的交互,即块设备如何注册进系统,以及块设备在系统中的存在形式等;
第二部分将从块设备驱动程序的角度出发,探讨块设备驱动程序的整体框架,包括外部接口函数的讨论等;
第三部分将讨论块设备如何完成其真正的功能 —— 让其所控制的设备完成上层的I/O请求。
11.1 块子系统初始化
__init genhd_device_init 系统启动的时候被调用。
11.2 ramdisk源码实例
略
Ps ToPage420
11.3 块设备号的注册与管理
register_blkdev
unregister_blkdev 当驱动程序不再使用一个设备号时,该函数将所占用的设备号释放掉,这样后续的设备才可以重新使用它。
如果一个设备驱动程序不调用register_blkdev函数就直接使用设备号,那么它就变成了一个不遵守系统规则的破坏者,系统因无法跟踪设备号的使用情况,将导致潜在的设备号使用冲突的问题。
Ps:toPage424.
11.4 block_device 到 block_device_operations
和字符设备的file_operations结构的一个很大的区别是:
block_device_operations结构中没有类似的read和write函数,块设备的读写操作由另一个重要的组件读/写请求队列来完成。
相对于需要和系统进行大量数据传输的块设备,字符设备和系统的交互往往很少。
另一方面,应用程序在使用字符设备驱动程序时,通过file_operations作中转,这个调用过程相当清晰而直白,但是对于块设备而言,它主要被系统中的文件系统组件所使用,一般的用户程序很少会像使用字符设备那样使用块设备。
ToP440. 这部分暂时没有用到,笼统地看了一下。
11.10 块设备文件的打开
块设备文件节点的生成
在支持动态设备节点生成的系统中,device_add将在/dev目录下生成一个块设备文件节点。
Ps:头脑不清醒,扫视了一下,需要细读。
第12章 网络设备驱动程序
与块设备的异同:
相似之处:都被用来与系统进行大量的数据交互,根据上层模块的需求进行数据的发送与接收
区别:
1、 网络设备在/dev目录下没有入口点,换句话说,网络设备在系统中并不像块设备那样以一个设备文件的形式存在,在应用层,用户通过套接口API的socket函数来使用网络设备。
2、 其次,网络设备除了响应来来自内核的请求外,还需要异步地处理来自外部世界的数据包,而块设备只需响应来内内核的请求。除处理数据外,网络设备驱动程序还需要完成诸如地址设置、配置网络传输参数及流量统计等一些管理类的任务。
Ps:toPage475.
12.1 net_device
网络设备由数据结构net_device表示,它存储着特定网络设备的所有信息,是一个及其庞大的数据结构。
对于网络设备驱动程序而言,首先要分配一个net_device类型的对象来代表所管理的网卡设备,内核为此提供了一个分配net_device对象的宏alloc_netdev.
对于以太网设备的驱动程序而言,应该使用alloc_etherdev函数来分配net_device对象,其内部会调用ether_setup将新分配出的net_device对象初始化为一个以太网设备。
12.2 网络设备的注册
相关函数:register_netdev -> register_netdevice
网络设备内嵌的dev成员作为一个内核对象被加到了系统中,并通过sysfs文件系统向用户空间披露了它的存在,这正是netdev_register_kobject函数所要完成的核心功能。
只当设备所要完成的功能接口函数全部就绪后,设备模块才最终想系统注册该设备。
对应的设备销毁函数:unregister_netdev。
Ps:toPage492.
12.3设备方法
重点关注 图12-4 网络设备驱动程序数据包发送模型
由于DMA通道的源端数据所在的缓冲区skb->data来自于内核的网络系统上层,换言之它不属于驱动程序所能控制的范围之内,所以现实中为了建立对应的DMA映射,一般多采用流式DMA映射,当然原理上采用一致性映射也是可行的,不过因为需要在skb->data与一致性缓冲区进行拷贝操作,因而可能会付出性能上的代价。
如果对数据包的发送过程作个简单小结,那就是:
一个数据包的发送过程逻辑上可以分成两个独立的部分,按照时间顺序,分别是网络子系统部分和设备驱动程序部分。
网络子系统部分在整个Linux网络部分源码中是独立于底层的网络硬件的,处于性能及可靠性等因素的考虑,网络子系统部分实现有一个传输队列,系统中每个cpu都拥有自己的传输队列,每个要发送的数据包都会先放到传输队列中。
真正的发送过程发生在网络设备驱动程序所实现的ndo_start_xmit函数中,后者的实现依赖于具体的硬件设备,通常硬件在当前帧传输结束时会以中断的方式通知驱动程序。
Ps:toPage500
12.3.4 网络数据包发送过程中的流控机制
为什么需要流控?
当ndo_start_xmit函数返回时,并不意味着实际的硬件设备(网卡)已将设备内存中刚刚获得的数据包全部成功发送了出去。
换句话说,软件层面的ndo_start_xmit调用过程与网络设备的实际数据发送行为之间是异步的,其各自的行为是独立的。
Netif_start_queue只是简单地清除发送队列的__QUEUE_STATE_XOFF比特位,并不触发数据包的发送流程;
而netif_wake_queue在清除__QUEUE_STATE_XOFF之后,还有机会重新触发网络子系统数据包的传输流程。
netif_wake_queue使用的两种典型场景:
1、 看门狗定时器超时,此种情况下驱动程序的ndo_tx_timeout函数会被内核调用以重新配置NIC
2、 当设备通过中断通知驱动程序可以进行数据包的传输时(在此之前,驱动程序通常的做法是:因设备无法进行数据包的传输而通知高层关闭传输队列)
Ps:ToPage503
12.3.5 传输超时(watchdog timeout)
函数的总体思想:如果发现某一传输队列处于停止状态并且当前已经过了指定的超时时间,将导致对当前网络设备的ndo_tx_timeout函数的调用,因此设备驱动程序有机会对该问题进行处理。
常见的操作包括在接口的统计信息中记录本次的传输错误,调用netif_wake_queue函数以重启传输队列,有时甚至需要重新reset当前网络设备等,总之跟手边的硬件以及要完成的功能息息相关。
To506
12.3.6 数据包的接收
相对于网络数据包的发送来说,接收过程要稍微复杂些,因为对驱动程序来说,数据包的到达是随机的,类似于一个异步的过程,通常当网络设备成功接收到一个数据包时,需要通过中断的方式引起驱动程序的干预。
接收模型:图12-6 网络设备驱动程序数据包接收模型
软中断的背景:
Netif_rx函数的调用是在中断处理例程中发生的,换句话说netif_rx要运行在中断上下文中,所以需要尽可能快地返回以使CPU可以接收下一个中断。不过基于网络子系统的多层协议的复杂性,如果要通过netif_rx发动的调用链将接收到的数据包最终传递到最后的接收这那里,时间上的开销一定不会小而且也不现实。
内核对此的解决方法是大家都熟悉的所谓软中断softirq。Netif_rx将接收到的数据包放到一个接收队列上,然后触发NET_RX_SOFTIRQ软中断,而对应该软中的处理例程早在Linux系统启动的初始化阶段便由net_dev_init函数完成了。
接收数据包依然使用了DMA传输的方式,也是典型的流式DMA映射。
ToPage510.
12.4 套接字缓冲区
Struct sk_buff
因为在Linux网络子系统中分配和释放的频率非常高,所以内核采用kmem_cache的内存分配方式来分配sk_buff的空间。
Ps:主要讲述几个接口的应用,看得笼统,此处不在抄录。
ToPage518.
12.5 中断处理
设备驱动程序实现的中断处理例程必须完成最关键的操作而迅速返回以为下一次的中断到来做好准备,它应该将一些耗时的工作延迟到softirq中来完成。内核为此定义了两个softirq,分别用于应对发送和接收触发的软中断处理,它们是NET_TX_SOFTIRQ和NET_RX_SOFTIRQ.
12.6 NAPI
New API
设计思想是结果了中断与轮询的各自优势,虽然在设备驱动程序中,轮询的名声不大好,但并非无一是处,比如在NAPI的机制中。简单地说,就是当有数据包到达时将会触发硬件中断,在中断处理中关闭中断,系统对硬件的掌控将进入轮询模式,直到所有的数据包接收完毕,再重新开启中断,进入下一个中断轮询周期。显然在系统对硬件进行轮询期间,硬件可能会接收到大量进入的数据包,但是它们不会产生中断。
Ps:细节代码还需再看下。
本书第一遍读完了,准备结合实际代码再过一遍。
20230628 周三