向下之旅(十六):内存管理(二)

  在栈上的静态分配

  在任意一个函数中,你都必须尽量节省栈资源。内核没有在管理内核栈上做足工作,因此,当栈溢出时,多出的数据就会直接溢出来,覆盖掉紧邻堆栈末端的东西。首先面临考验的就是thread_info结构。在堆栈外,任何内核数据都可能存在潜在的危险。当栈溢出时,最好的情况是机器宕机,最坏的情况是悄无声息的破坏数据。

  因此,进行动态分配是一种明智的选择。

  高端内存的映射

  根据定义,在高端内存中的页不能永久的映射到内核地址空间上。因此,通过alloc_pages()函数以_GFP_HIGHMEM标志获得的页不可能有逻辑地址。

  在x86体系结构上,高于896MB的所有物理内存的范围大都是高端内存,它并不会永久的或自动的映射到内核地址空间,尽管x86处理器能够寻址物理RAM的范围达到4GB(启用PAE可以寻址到64GB)。一旦这些也被分配,就必须映射到内核的逻辑地址空间上。在x86上,高端内存中的页被映射到3GB到4GB之间。

  永久映射

  要映射一个给定的page结构到内核地址空间。可以使用:

  void *kmap(struct page *page)

  该函数可以在高端内核或低端内存上都能用。如果page结构对应的是低端内存中的一页,函数只会单纯的返回该页的虚拟地址。如果页位于高端内存,则会建立一个永久映射,再返回地址。这个函数可以睡眠,因此kmap()只能用在进程上下文中。

  因为允许永久映射的数量是有限的,当不再需要高端内存时,应该解除映射,这可以通过下列函数完成:

  void kunmap(struct page *page)

  该函数解除对给定页的映射。

  临时映射

  当必须创建一个映射而当前的上下文又不能睡眠时,内核提供了临时映射(也就是所谓的原子映射)。有一组保留的映射,它们可以存放新创建的临时映射。内核可以原子的把高端内存中的一个页映射到某个保留的映射中。因此临时映射可以用在不能睡眠的地方,比如中断处理程序中,因为获取映射时绝不会阻塞。

  通过下面的函数建立一个临时映射:

  void *kmap_atomic(struct page *page, enum km_type type)

  参数type是下列枚举类型之一,这些枚举类型描述了临时映射的目的。具体如下:

  

  这个函数不会阻塞,因此可以用在中断上下文和其他不能重新调度的地方。它还能禁止内核抢占,这是有必要的。因为映射对每个处理器都是唯一的(调度可能对哪个处理器执行哪个进程做变动)。

  可通过下列函数取消映射:

  void knumap_atomic(vodi *kvaddr, enum km_type type)

  这个函数也不会阻塞。除非激活了内核抢占,否则kmap_atomic()根本就无事可做,因为只有在下一个临时映射到来前上一个临时映射才有效。下一个原子映射将自动覆盖前一个映射。

  每个CPU的分配

  支持SMP的现代操作系统使用每个CPU上的数据,对于给定的处理器其数据是唯一的。一般来说,每个CPU的数据存放在一个数组中。数组中的每一项对应着系统上一个存在的处理器。当前处理器号确定这个数据的当前元素。像如下声明数据:

  

  上面的代码并没有出现锁,这是因为所操作的数据对当前处理器来说是惟一的。除了当前处理器之外,没有其他处理器可以接触到这个数据,不存在并发访问的问题。

  因此,内核抢占成为了唯一要关注的问题:

  1.如果你的代码被其他处理器抢占并重新调度,那么这时cpu变量就会无效,因为它指向的是错误的处理器(通常,代码获得当前处理器后是不可以睡眠的)。

  2.如果别的任务抢占了你的代码,那么有可能在同一处理器上发生并发访问My_percup的情况,显然这处于一种竞争状况。

  不必惊慌,应为在获取当前处理器号,即调用get_cpu()时,就已经禁止了内核抢占。相应的smp_processor_id()在调用put_cpu()时又会重新激活当前处理器号。注意,如果你使用对smp_processor_id()的调用来获得当前处理器号,只要你总是用上述方法来保护数据安全,那么内核抢占并不需要你自己去禁止。

  新的每个CPU接口

  2.6内核为了方便创建和操作每个CPU数据,从而引进了新的操作接口,称作percpu。该接口归纳了前面所述的操作行为,并使每个CPU数据的创建和操作得以简化。

  编译时的每个CPU数据

  DEFINE_PRE_CPU(type, name);

  这个语句为系统中的每一个处理器都创建了一个内型为type,名字为name的变量实例,如果你需要在别处声明变量,以防范编译时警告,那么下面的宏将是你的好帮手:

  DECLARE_PER_CPU(type, name);

  你可以利用get_cpu_var()和put_cpu_var()例程操作变量。调用get_cpu_var()返回当前处理器上的指定变量。同时它将禁止抢占,另一方面put_cpu_var()将相应的重新激活抢占。

  使用此方法要格外小心,因为per_cpu()函数既不会禁止内核抢占,也不会提供任何形式的锁保护。如果一些处理器可以接触到其他处理器的数据。那么就必须要给数据上锁。此外,如果你需要从模块中访问每个CPU数据,或者如果你需要动态创建这些数据。

  运行时的每个CPU数据

  内核实现每个CPU数据的动态分配方法类似于kmalloc()。该例程为系统上的每个处理器创建所需内存的实例:

  

  宏alloc_perpu()给系统中的每个处理器分配一个指定类型对象的实例。它其实是宏__alloc_percpu的一个封装,这个原始的宏接收的参数有两个:一个是要分配的实际字节数,一个是分配时要按多少字节对齐。而封装后的_alloc_percpu()按照单字节对齐——按照给定类型的自然边界对齐,比如:

  _alignof_结构是gcc的一个功能,它会返回指定的类型或lvalue所需的对齐节数。它的语意和sizeof一样,比如:

  _alignof_(unsigned long)

  在x86体系中将返回4。如果指定一个lvalud,那么将返回lvalud的最大对齐字节数。比如一个结构中的lvalue相比结构外的lvalue可能有更大的对齐字节数需求,这是结构本身的对齐要求的缘故。

  调用函数 free_percpu() 将释放所有处理器上指定的每个CPU数据。

  无论是alloc_percpu()还是__alloc_percpu都会返回一个指针,用来间接引用动态创建的每个CPU数据,内核提供了两个宏来利用指针获取每个CPU数据:

  get_cpu_ptr()宏返回了一个指向当前处理器数据的特殊实例。它同时会禁止内核抢占,而在put_cpu_ptr()宏中会重新激活内核抢占。

  使用每个CPU数据的原因

  使用每个CPU数据好处如下:

  1.减少了数据锁定,因为按照每个处理器访问每个CPU数据的逻辑,你可以不在需要任何锁。

  2.可以大大的减少缓存失效,失效发生在处理器视图使他们的缓存保持同步时,如果一个处理器操作某个数据,而该数据又存放在其他处理器缓存中,那么存放该数据的那个处理器必须清理或刷新它自己的缓存。持续不断的缓存失效称为缓存抖动,这样对系统性能影响颇大。

  它唯一的安全要求就是禁止内核抢占。

  分配函数的选择

  如果你需要连续的物理页,就可以使用某个低级页分配器或kmalloc()。这是内核中内存分配的常用方式,也是大多数情况下你自己应该使用的内存分配方式。

  如果你想从高端内存进行分配,就使用alloc_pages()。该函数返回一个执行struct page结构的指针,而不是一个指向某个逻辑地址的指针。因为高端内存很可能并没有被映射,因此,访问它的唯一方式就是通过相应的struct page结构。为了获得真正的指针,应该是用kmap(),把高内存映射到内核的逻辑空间。

  如果你不需要物理上连续的页,而仅仅需要虚拟地址上连续的页,那么就使用vmalloc()。

  如果你要创建和销毁很多较大的数据结构,那么应该考虑建立slab高速缓存。slab层会给每个处理器维持一个对象高速缓存(空闲链表)。

 

  参考自:《Linux Kernel Development》.

posted on 2016-03-25 15:50  画家丶  阅读(161)  评论(0编辑  收藏  举报