在Linux-4.14之前,Linux内核栈都是位于线性映射区,该区域对应的虚拟地址和物理地址具有一个固定的偏移,并且是在系统刚启动是进行过pre-mapped,因此内核在使用时不需要另外做页表映射。
在2016年的时候内核引入了vmap_stack机制,它是采用vmalloc申请的内存作为内核栈的一种机制。只需要使能 CONFIG_VMAP_STACK
配置选项即可打开该功能。这个功能带来了如下一些优势:
1.利用vmalloc的guard page增强了栈溢出检测能力
2.减少了内存碎片化
当然除了有这些优势,同时也对系统中带来了一些兼容性的问题,不过这些都是可以解决,比如:
使能了CONFIG_VMAP_STACK
,那么将使用vmalloc申请的内存作为内核栈,这些内存在物理上可能是不连续的,这就会对一些驱动的DMA操作造成影响,因为一些DMA设备要求数据传输时的物理地址是连续的,如果申请了栈上的内存作为传输数据的缓冲区,那么就会遇到问题。
栈溢出检测
栈溢出检测功能,我们可以利用guard page来判断是否发生了栈溢出错误,但是对于传统的内核栈,guard page就会占用更多的物理内存,而对于vmap_stack则不然,因为guard page对应的虚拟地址可以不做任何映射,这样可以利用MMU的特性来检测栈溢出,也能够节省更多的物理内存。vmap_stack特性不用再特意实现guard page,因为vmalloc本身就自带这种guard page溢出检测功能,vmap_stack利用vmalloc申请内存因此也就带有了该功能。
反碎片
对于内核栈是每个进程都会有各自独立的内核栈,当系统中不断创建和销毁进程时,如果内核栈存在于线性映射区,那么内存也就是越来越趋于碎片化。而使用了vmalloc申请内存作为内核栈,则可以在一定程度上减轻内存碎片化,因为本身vmalloc就是把物理不连续的内存页映射到虚拟地址连续的空间内。
thread_info和stack的关系
提到stack内核栈,很多人会想到它和thread_info结构体的关系,比如在ARM32平台上有一个联合体:
union thread_union {
#ifndef CONFIG_ARCH_TASK_STRUCT_ON_STACK
struct task_struct task;
#endif
#ifndef CONFIG_THREAD_INFO_IN_TASK
struct thread_info thread_info;
#endif
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
简化之后为:
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
因此利用这个关系,我们可以通过sp寄存器获取到对应的stack所在的地址,然后在转换到对应的thread_info结构体,而该结构体中保存的有当前进程的task_struct结构体指针。这就是current的实现原理。
而对于ARM64来说,一般会使能 CONFIG_THREAD_INFO_IN_TASK
这个宏,该宏会导致这个联合体结构发生变化,此时thread_info已经不和stack有什么关系了。此时thread_info是存在于task_struct结构体中的:
struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
/*
* For reasons of header soup (see current_thread_info()), this
* must be the first element of task_struct.
*/
struct thread_info thread_info;
#endif
void *stack;
那么这时的current怎么找到当前运行的进程呢?实际上这里的实现已经和ARM32不一样了,对于ARM64平台,记录当前进程的task_struct地址是利用sp0_el1寄存器,当内核执行进程切换时会把当前要运行的进程task_struct地址记录到该寄存器中。因此我们current查找task_struct时也是很简单了,不用通过sp和thread_info去定位了。
看到这里,会想到另一个问题:task_struct这里的stack和前面的thread_union中的stack有什么不同,该怎么理解呢?
task_struct结构体中一直都存在一个stack成员,它是用来记录每个进程的内核栈起始地址的,内核为每个进程都分配内核栈空间,并记录在此。而每个进程的thread_union中的stack实际上也是同一个stack地址,只不过我们定义一个联合体的目的,是为了方便我们把进程的thread_info结构体保存进stack的最低地址处。当然对于使能了 CONFIG_THREAD_INFO_IN_TASK
的系统,我们完全可以不必关注这个联合体。
说了这么多,至于VMAP_STACK,和上述的唯一差异点就是使用了vmalloc来分配内存并赋值给stack成员,它和thread_info也没有太大关系。
参考链接:
https://lwn.net/Articles/692208/
https://zhuanlan.zhihu.com/p/84591715