By Jonathan Corbet https://lwn.net/Articles/732107/ 古二

与大多数处于活跃开发的程序一样,内核的大小也随着时间增长。到目前为止,仅有两个开发周期(2.6.36和3.17),内核作为一个整体比其前一个周期更小。 内核的内部API在规模和复杂性伴随着其他方面持续增长。 然而,内部API的好处是它完全处于开发社区的控制之下,可以随时更改。 除此之外,这也意味着如果部分内核的内部API不再需要,或者认为添加这些函数是错误的,就可以将其删除。 内存管理领域的的两个函数的清除展示了整个过程。 [编辑] GFP_TEMPORARY

内核的内存管理子系统面临的诸多挑战之一是碎片化。 如果分配操作处理的不好,系统的空闲内存可能会分裂成许多不能合并的小块,即使系统的大部分内存空闲,也可能导致分配失败。 对于内核本身使用的内存分配尤其如此。 这些分配的内存有比较长的生命周期,在整个生命周期中内存是不能被迁移的。 一个小的分配可以阻止整个页面被重用,进而会阻止在该页面周围创建更大块的内存。

长期以来,内核中内存块生命周期的差异性是众所周知的。 一些数据结构对系统的运行至关重要,不能被删除。例如,描述挂载的文件系统结构体或正在运行的进程的结构体。 但是,其他一些以提高系统性能为目的内存,则是可以在系统需要内存的时候被回收,虚拟文件系统层中的inode和dentry缓存可能是这种结构的最好的例子。 后一种结构称之为“可回收”结构。

内存管理子系统的一个重要的启发式应用是尝试将内存分配分离为可回收和不可回收两部分。 一张充满可回收内存的页面至少在理论上可以在内存紧张时被回收并用于其他用途。 但是,一个不可回收的内存分配将阻止整个页面的重用。 包含可回收内存的页面实际上可以在需要时回收,分离这两种类型的内存分配实际上增加了这种类型页面的回收概率。

早在2007年,Mel Gorman就加入了GFP_TEMPORARY分配类型,试图使内存分配更加灵活。 原因是这样的:一些内存分配持续应用很长时间,而另外一些则是短暂应用后被释放。 表示新添加的设备的结构体在分配后会在系统的整个生命周期内保持其分配而不会释放,而分配用于满足系统调用的内存可以在毫秒内归还。 如果分配时间很短,是否可以回收是无关紧要的,因为不管什么时候它都会归还,这种分配不可能阻止一个页面的回收。所以GFP_TEMPORARY的分配即使没有可以回收的机制,仍然被允许从“可回收”的池中分配内存。

今年早些时候,GFP_TEMPORARY是一个广泛讨论的主题,一开始就集中在一个看似简单的问题上:“临时”是什么意思? 是否对分配内存持有的时间有一定限制? GFP_TEMPORARY分配的持有者是否允许阻塞或允许进行获得锁操作? 事实证明,这些信息在添加GFP_TEMPORARY时并没有详细说明。 讨论也未能填补这个空白,而且对GFP_TEMPORARY内核调用点的代码复查中揭示了一些明显的非临时性的用途。 很明显,没有人真正知道什么是“临时”分配。

有人试图确定这个定义,但是Michal Hocko则推动另外的方案:完全删除GFP_TEMPORARY。 他说,目前的用途没有理由继续保持这个标志:

   我随机的检查了一些用户,他们都没有在添加这个flag的时候提供具体的理由。 我怀疑他们大多数是从其他现有的使用者那里复制的这个flag,
          只是认为这么用可能是一个好主意,没有任何测量。 这表明,GFP_TEMPORARY只是一种“货物崇拜”式的应用,没有任何理由。
             (货物崇拜:英文--Cargo Cults,指与世隔绝的小岛的原住民看见外来的先进科技物品,会将其当作神祇般崇拜。)


	     有一些关于删除这个flag的抱怨,但没有严重反对这一变化的。包括Neil Brown在内的其他开发人员也同意这一变化:

	            如果我们有一个flag却没有很好的定义含义,这对该flag的应用行为产生影响,它不会被持续使用,如果我们更改了它的行为,即使这种更改是准确的,
		           可以预期,那些误解它本意的使用仍然会出错。 因此,即便没有flag,也比有一个定义不清的flag要好。

			   他建议引入“可回收能力”这个内核概念来代替之前的解决方案。 这可能会在更远未来发生,而GFP_TEMPORARY则将很快被删除。 这些补丁现在已经在linux-next分支上,这意味着它们在4.14合并进入主线。 那样的话,GFP_TEMPORARY将证明自己是“临时的” -- 一个工作了10年的“临时工”。
			   [编辑] dma_alloc_noncoherent()

DMA(Direct Memory Access)操作的内存对于分配来说并不像看起来那么简单。 通常来说设备所能看到的内存地址和范围不同于CPU所能看到的,因此针对DMA的内存分配操作必须弥补这一差距。 这些分配通常必须是物理地址连续的,并且它们必须位于目标设备能够访问的内存区域中。 一种特殊属性的内存分配是由dma_alloc_noncoherent()处理的:

    void *dma_alloc_noncoherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag);

    调用dma_alloc_noncoherent()是一个明确的请求,在内存的noncoherent区域分配一个DMA缓冲区。 cache-coherent的内存对于CPU和I / O设备来说看起来都是一样的。 如果CPU写入该内存,则其写入操作将对设备可见;同样,如果设备写入一个内存区域,CPU将立即看到新的数据。 Noncoherent内存区域则没有这种保证,如果CPU想要读取内存中的数据,而这些数据是通过DMA操作写入的,则必须要在I / O操作完成之后并且在CPU访问之前,CPU执行一条cache无效指令使自己的Cache失效。

    Noncoherent内存显然更难处理,不加以小心,就很容易导致数据损坏。 所以人们可能会想知道为什么有人会特别要求分配这种类型的内存。 答案是,在某些架构(例如ARM)中,coherence内存要慢得多。 打开coherence通常涉及关闭cache,对性能产生比较大的影响。 对于任何重要数据的处理,尽量不要使用coherent内存。(译者注:这里特指设备DMA的内存操作。coherence内存是SMP的基础,对于支持SMP的处理器系统,所有CPU需要接入coherence,但设备是否接入coherence则是可选的)

    因此能够为DMA缓冲区分配noncoherent内存非常重要,这就引发了Christoph Hellwig为什么要去除dma_alloc_noncoherent()的问题。 答案是,在任何合理的当前系统上,对存储器访问模式的控制比简单地打开或关闭cache更为复杂。 内存访问可以配置为允许写组合(Write combining:为了提高性能,CPU将多个写入操作合并为一次写操作),或者可以设置为允许对操作进行重新排序。 许多这些功能可以一起配置。 为每个组合创建新的分配函数显然不是一个合适的策略,因此内核开发人员在3.4开发周期中添加了一组新的函数,其中包括:

        void *dma_alloc_attrs(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag, unsigned long attrs);

	attrs字段可用于指定整个分配空间的属性,包括DMA_ATTR_NON_CONSISTENT以获取noncoherent映射。 这个函数显然和dma_alloc_noncoherent()一样,除了dma_alloc_noncoherent()之外,还有一点理由保留dma_alloc_coherent()函数。 Hellwig一直在努力将其删除,这意味着将其所有调用者更新为使用dma_alloc_attrs()。 大部分工作都在4.13合并窗口进行,只剩下三个调用的地方。 他目前的补丁更改了最后三个调用点并删除了函数主体。 当然,没有通过主线维护的驱动,如果使用了dma_alloc_noncoherent()函数需要单独更新。

	在这两种情况下,内核的内部API越来越小,但没有功能丢失。 这项工作就是在不需要维护API兼容性的情况下可以进行的那种清理的例子。 暴露给用户空间的接口必须被保留下来,尽管刚刚庆祝了26岁生日,但内核进化的能力仍然是内核保持可维护性的重要组成部分。

	译者注:文章回帖第一个人就抱怨DMA的API很乱,混淆了Coherent和Consistent,这一点从设备的视角看没问题,单从体系结构来说是很奇怪的。