https://lwn.net/Articles/692953/ 

雏雁

Virtually mapped kernel stacks

在我们熟悉的3.10内核中,内核栈是通过alloc_thread_info_node()函数直接从buddy system分配,即栈空间的物理地址连续。该方式有以下几个不足:

  1. 内核栈空间太小(4k for 32-bit system, 8K for 64-bit system),容易导致栈溢出;因此,在kernel 3.15版本之后,栈空间被扩大(8k for 32-bit system, 16K for 64-bit system)。

  2. 当内存碎片化严重时,分配高阶页可能失败,导致进程创建失败;

  3. 如果使用保护页(guard page)防止栈溢出刷掉其邻近页,该保护页将会占用一个物理页,导致内存浪费,因此内核默认没有开启保护页机制;

  4. 在关闭保护页机制时,缺乏手段检测栈溢出,最终,往往是由于邻近页内容被篡改(memory corruption),导致内核panic

  5. 存在潜在安全性问题,thread_info结构被放置在内核栈的底部,精心设计的内核栈溢出可能会修改thread_info,导致安全隐患;

基于上述不足,目前主流思路是通过vmalloc分配内核栈,该方案有效的解决上述缺点:

  1. 便于内核栈扩张,且不会出现分配高阶页失败而导致创建进程失败的情况;

  2. 保护页不需要占用一个物理页,只需要在页表上添加一个页表项,并标记为禁止访问;

  3. 栈溢出能够被保护页及时检查出来,溢出时邻近的页不会被篡改,因此只需要kill当前进程,不会导致内æ ¸panic。这使内核栈溢出便于调试。

但是该方案存在几个小问题,比如:

  1. performace regression:即通过clone()创建一个进程会多消耗1.5微秒,在大量创建和销毁进程的场景下,影响性能,Linus要求该patch进入upstream之前必须修复该问题。

  2. TLB miss增加;对于64位系统来说,以前内核栈对应的虚拟地址属于直接地址映射,在页表中使用1G的大页,因此只需一个TLB entry就能装下。而vmalloc对应的虚拟地址空间使用单页映射;内核栈需要对应多个TLB entry。

  3. 有些非常老的代码竟然在内核栈中执行DMA操作,这些代码需要重写

其中最值得关注的是一个问题如何解决这个regression:

很显然主要是由vmalloc导致了performace regression,调用vmalloc()比alloc_pages()这类函数代价更高。很自然想到的一种方法就是,预先分配一定数量的内核栈数据结构。

作者惊奇的发现,这不能解决问题,进程退出时并没有立刻释放资源(包括内核栈)。因为使用了RCU机制来保证该进程的资源在释放前没有被其它代码应用,只有在下一个RCU grace period,这些资源才会被释放。

这造成一个效果:在大量创建和销毁进程的场景下,会导致内核栈结构被批量申请,然后批量释放;由于释放的数量超出缓存管理的上限,超出的内核栈结构被直接丢弃。这导致内核栈缓存效果很差。

原则上来说,进程退出后,其资源不应该再被其它代码应用,但由于历史原因,thread_info中的一些数据仍然被应用,而thread_info 被放在内核栈的底部,连累内核栈所占的空间也不能被直接释放。

如果thread_info与内核栈完全独立开,这个问题就能愉快的解决了。因此,开发者正在想办法将thread_info中的数据结构挪动到 task_struct中去。thread_info在内核代码中被大量用到,显然这不是一个简单的工作。