Category: Code analysis
Linux kernel学习-block层
本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-learning-block-layer/ Linux 内核中的 block I/O 层又是非常重要的一个概念,它相对字符设备的实现来说复杂很多,而且在现今应用中,block 层可以说是随处可见,下面分别介绍 kernel block I/O 层的一些知识,你需要对块设备、字符设备的区别清楚,而且对 kernel 基础有一些了解哦。 1、buffer_head 的概念: buffer_head 是 block 层中一个常见的数据结构(当然和下面的 bio 之类的结构相比就差多了哦,HOHO)。 当块设备中的一个块(一般为扇区大小的整数倍,并不超过一个内存 page 的大小)通过读写等方式存放在内存中,一般被称为存在 buffer 中,每个 buffer 和一个块相关联,它就表示在内存中的磁盘块。kernel 因此需要有相关的控制信息来表示块数据,每个块与一个描述符相关联,这个描述符就被称为 buffer head,并用 struct buffer_head 来表示,其定义在 <linux/buffer_head.h> 头文件中。 b_state 字段说明这段 buffer 的状态,它可以是 bh_state_bits 联合(也在上面的代码中,注释说明状态,应该比较好明白哦)中的一个或多个与值。b_count 为 buffer 的引用计数,它通过 get_bh、put_bh 函数进行原子性的增加和减小,需要操作 buffer_head 时调用 get_bh,完成之后调用 put_bh。b_bdev 表示关联的块设备,下面会单独介绍 block_device 结构,b_blocknr 表示在 b_bdev 块设备上 buffer 所关联的块的起始地址。b_page 指向的内存页即为 buffer 所映射的页。b_data 为指向块的指针(在 b_page 中),并且长度为 b_size。 在 Linux 2.6 版本以前,buffer_head 是 kernel 中非常重要的数据结构,它曾经是 kernel 中 I/O 的基本单位(现在已经是 bio 结构),它曾被用于为一个块映射一个页,它被用于描述磁盘块到物理页的映射关系,所有的 block I/O 操作也包含在 buffer_head 中。但是这样也会引起比较大的问题:buffer_head 结构过大(现在已经缩减了很多),用 buffer head 来操作 I/O 数据太复杂,kernel 更喜欢根据 page 来工作(这样性能也更好);另一个问题是一个大的 buffer_head 常被用来描述单独的 buffer,而且 buffer 还很可能比一个页还小,这样就会造成效率低下;第三个问题是 buffer_head 只能描述一个 buffer,这样大块的 I/O 操作常被分散为很多个 buffer_head,这样会增加额外占用的空间。因此 2.6 开始的 kernel (实际 2.5 测试版的 kernel 中已经开始引入)使用 bio 结构直接处理 page 和地址空间,而不是 buffer。 2、bio: 说了一堆 buffer_head 的坏话,现在来看看它的替代者:bio,它倾向于为 I/O 请求提供一个轻量级的表示方法,它定义在 <linux/bio.h> 头文件中。 该定义中已经有详细的注释了哦。bi_sector 为以 512 字节为单位的扇区地址(即使物理设备的扇区大小不是 512 字节,bi_sector 也以 512 字节为单位)。bi_bdev 为关联的块设备。bi_rw 表示为读请求还是写请求。bi_cnt 为引用计数,通过 bio_get、bio_put 宏可以对 bi_cnt 进行增加和减小操作。当 bi_cnt 值为 0 时,bio 结构就被销毁并且后端的内存也被释放。 I/O 向量: bio 结构中最重要的是 bi_vcnt、bi_idx、bi_io_vec...Linux kernel学习-内存管理
本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-learning-memory-management/ 接着之前的 Linux kernel 学习步伐,来到极其重要的内存管理部分,继续本文内容,需要先了解内存寻址的基础知识,见之前的 [内存寻址] 博文。 1、内存页及内存区域: 正如之前所说,Linux kernel 使用物理页作为内存管理的基本单位,其中重要的线程地址和物理地址的转换操作由页单元 MMU 来完成,系统的页表也由 MMU 来维护。kernel 使用 struct page 来表示一个物理页,它的定义在 include/linux/mm_types.h 头文件中: 其中的 flags 用于表示页的状态(是否为脏或者被锁定等),_count 即为页的引用计数,kernel 一般使用 page_count 宏调用 atomic_read 函数原子的读取此值,page_count 返回 0 表示此页可用。如果一个页被作为 page cache 使用,则 page 的 mapping 字段指向映射的 inode 的 address_space 对象,如果页被作为私有数据(作为 buffer_heads 缓冲、buddy 系统等),则 private 常包含对应的信息。注意其中的 virtual 字段为页的虚拟地址,结合之前的知识,对于高端内存来说,其并没有被固定映射到 kernel 地址空间中,因此如果 virtual 字段为 NULL,则表示此页必须被动态映射。 kernel 使用 page 结构记录系统中的所有页,因此 struct page 的大小应该要尽量小以减少内存占用,另外 kernel 必须知道页是否空闲,如果不空闲则拥有者是谁。 由于实际硬件限制,Linux kernel 不可能使用全部的物理内存,kernel 为此将内存划分为不同的区域,一个区域中的内存属性应该也相同。kernel 中常见的内存区域有 ZONE_DMA(可用于 DMA 的页)、ZONE_DMA32(与 ZONE_DMA 类似,但只对 32 位设备可用)、ZONE_NORMAL、ZONE_HIGHMEM(并没有被固定映射的高端内存区域),这些内存区域一般都是硬件相关的,例如在 x86 架构下,ZONE_DMA 的范围为 0MB - 16MB,ZONE_HIGHMEM 为高于 896MB 的物理内存,而在 x86_64 架构下 ZONE_HIGHMEM 则为空。需要注意的是内存的分配不会跨域这些不同的内存区域。内存区域在 kernel 中由 struct zone 结构来表示,其中的 name 字段即为内存区域名称。 2、获取页: 分配和释放内存是 Linux kernel 中极其重要又用的极多的接口。先看看 kernel 提供的直接获取以内存页面为单位的 alloc_pages 函数: struct page * alloc_pages(gfp_t gfp_mask, unsigned int order) 此函数是最基本的用于分配大小为 2^order 并且连续的物理页的函数,其返回分配到的第一个页面的 page 指针。 来看看比较重要的 gfp_t 类型的 gfp_mask 值: gfp_t 实际上就是 unsigned int 类型,gfp_mask 常用于指定行为方式、区域方式、类型等信息。常见的行为方式标志有:__GFP_WAIT(标志分配器可以睡眠,明显不适用于中断上下文中)、__GFP_IO(分配器可以启动磁盘 I/O)等。区域方式指定内存从哪里分配,对应的就有:__GFP_DMA、__GFP_DMA32、__GFP_HIGHMEM(从高端内存或普通内存中分配)。类型标志则用于简化分配时的指定操作,常见的有:GFP_ATOMIC(高优先级并不可睡眠,常用于中断、中断下半部、持有自旋锁等环境中)、GFP_NOIO(表示分配可中断但不可以发起 I/O 操作)、GFP_NOFS(分配时不可发起文件 I/O 操作)、GFP_KERNEL(最常见的分配标志,常用于可以睡眠的进程上下文中)、GFP_USER(用于分配内存给用户进程)、GFP_DMA 等。 需要注意的是对 __get_free_pages 和 kmalloc 函数(下面会分别说明)不能指定 __GFP_HIGHMEM 标志,因为它们都是直接返回的虚拟地址,而非 page 结构指针,如果指定了 __GFP_HIGHMEM,则他们可能分配到的内存并没有被映射到 kernel 地址空间,因此这样得不到虚拟地址。只有 alloc_page 函数可以分配高端内存,这个限制在下面的...Linux kernel percpu变量解析
本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-percpu-variable/ Linux 2.6 kernel 中的 percpu 变量是经常用到的东西,因为现在很多计算机都已经支持多处理器了,而且 kernel 默认都会被编译成 SMP 的,相对于原来多个处理器共享数据并进行处理的方式,用 percpu 变量在 SMP、NUMA 等架构下可以提高性能,而且很多情况下必须用 percpu 来对不同的处理器做出数据区分。 本文以 kernel 中的 softirq 为例简单说下 percpu 变量,我们先来看看 kernel 中唤醒 ksoftirqd 的实现,ksoftirqd 在 ps 命令看到的进程列表中很容易找到,是每个处理器都有一个(如果有 4 个处理器,则有 4 个 kernel 线程名称分别从 ksoftirqd/0 到 ksoftirqd/3),关于 softirq 本身的实现不在本文讨论范围内,唤醒 ksoftirqd 的实现在 kernel/softirq.c 文件中: 这里就用到了 percpu 变量 ksoftirqd,它是通过 DEFINE_PER_CPU 宏来进程定义的 percpu task_struct 列表,通过 __get_cpu_var 宏来得到相应处理器的 ksoftirqd/n 的 task_struct,然后调用 wake_up_process 函数唤醒进程(也就是 ksoftirqd/n kernel 线程),关于 wake_up_process 等进程调度的相关实现在之前的日志中有介绍的,请参考 [这里]。 __get_cpu_var、DEFINE_PER_CPU 等 percpu 宏的实现在 include/linux/percpu.h、include/asm-generic/percpu.h 等头文件中。先看看 include/asm-generic/percpu.h 中的一些定义: 通常所有的 percpu 变量是一起存放在特定的 section 里的,像上面头文件中的 .data.percpu 基础 section( 当然非 SMP 系统下就是 .data 了)、.shared_aligned、.first section。使用 objdump 可以看到编译 kernel 时的 vmlinux 文件的 section(结果没有完全显示): 可以看到 vmlinux 文件中的 .data 和 .data.percpu section。 percpu 变量的地址实际上就是其在上面说到的 section 里的偏移量,这个偏移量还要加上特定处理器的偏移量(也就是上面头文件中的 per_cpu_offset、my_cpu_offset 等)得到最终的变量地址,并最终以指针引用的方式得到值,这样访问的效果就有点类似于访问全局变量了。percpu 变量通常用于更新非常频繁而访问机会又相对比较少的场合,这样的处理方式可以避免多处理器环境下的频繁加锁等操作。 从上面的注释也可以看到 per_cpu_offset 是在一个 percpu 变量上增加的偏移量,大多数系统架构下使用 __per_cpu_offset 数组来作为偏移量,而 x86_64 等架构下处理方式则不同。my_cpu_offset 是在调用 per_cpu_offset 时使用 smp_processor_id() 得到当前处理器 ID 作为参数,__my_cpu_offset 则是用 raw_smp_processor_id() 的值作为 per_cpu_offset 的参数(smp_processor_id() 在抢占被关闭时是安全的)。SHIFT_PERCPU_PTR 宏用于给指针增加偏移量,它使用的 RELOC_HIDE 宏在不同的编译器下实现不同,在 include/linux/compiler.h 头文件中,看看 gcc 编译下的处理: 可以看到 gcc 中使用内嵌汇编先将 ptr 值赋给 __ptr(unsigned long 类型),然后在 __ptr 基础上增加偏移量,这样可以避免编译报错,ptr...Categories: kernelLinuxCode analysisTechnology
Linux kernel kfifo分析
本文同步自(如浏览不正常请点击跳转):https://zohead.com/archives/linux-kernel-kfifo/ kfifo 是 Linux kernel 中的一个通用队列实现,对于 kernel 中常见的 FIFO 队列应用还是很有用的,本文主要简单介绍分析下 Linux kernel kfifo。实际上 ChinaUnix 上有个 kfifo 的分析文章,但已经比较老(基于 Linux 2.6.10),而且我现在用的 2.6.34 版本 kernel 中 kfifo 实现有很多改动,故自己简单写下,ChinaUnix 上的 kfifo 介绍帖子在这里: http://bbs.chinaunix.net/thread-1994832-1-1.html kfifo 定义在 include/linux/kfifo.h 头文件中,我们经常使用的就是 kfifo 结构,看看它的定义: kfifo 也像其它队列那样提供了两个主要操作:入队列(in) 和 出队列(out),对应于上面结构中的 in 和 out 两个偏移量,in 偏移量为下次入队列的位置,out 为下次出队列的位置,很容易也能想到 out 值必须小于等于 in 值,当 out 值等于 in 值时表示队列为空,kfifo 中 buffer 为队列的空间,size 为空间大小,必须为 2 的 k 次幂值(原因在下面说明)。当然如果 in 值等于队列长度了,就表示队列已经满了。 先看看 kfifo 最简单的一些操作实现,在 kernel/kfifo.c 文件中: 调用 kfifo_alloc 可以自动分配空间并初始化,你也可以调用 kfifo_init 函数使用自己的空间来初始化队列,可以看到这两个函数中都用 is_power_of_2 做了检查队列空间的操作。kfifo_free 释放队列,它会调用 _kfifo_init 函数(参数为 NULL 和 0 清空队列),调用 kfifo_reset 可以重置队列(将 in 和 out 都设为 0)。你也可以用 DECLARE_KFIFO 和 INIT_KFIFO 静态定义一个 kfifo 队列,尽管这不太会被用到。 调用 kfifo_in 函数将数据加入队列,kfifo_out 将数据从队列中取出并从队列中删除(增加 out 值),Linux 还提供了 kfifo_out_peek 函数从队列中取数据但并不删除(不增加 out 值)。kfifo_in 中会先调用 __kfifo_in_data 将输入加入队列,然后调用 __kfifo_add_in 增加 in 的值,kfifo_out 相反则调用 __kfifo_out_data 和 __kfifo_add_out 函数取出数据并删除。 kfifo 中同时提供了 kfifo_from_user 函数用户将用户空间的数据加入到队列中,它会先调用 __kfifo_from_user_data 将用户空间的数据加入队列,再调用 __kfifo_add_in 增加 in 的值。看看 __kfifo_from_user_data 的实现: 可以看到 __kfifo_from_user_data 中是直接调用 copy_from_user 将用户空间的数据拷贝到 kfifo 队列的空间中。相应的也有 kfifo_to_user 函数将队列中的数据取出到用户空间的地址,他就调用 copy_to_user 将队列中数据拷贝到用户空间。 需要注意的是 __kfifo_from_user_data 中用到的 __kfifo_off 函数: __kfifo_off 是根据指定的偏移量得到索引值,由这里也可以看出为什么队列的大小为什么必须是 2 的...Categories: kernelLinuxCode analysisTechnology