在基于多核硬件的当代计算平台上,可以同时并行地运行应用程序。因此,在请求同一个进程时,可以同时启动多个进程的内核模式切换。为了能够处理这种情况,内核服务被设计为可重入的,允许多个进程介入并使用所需的服务。这就要求请求进程维护它自己的私有内核栈,来跟踪内核函数调用顺序,存储内核函数的本地数据,等等。
内核栈直接映射到物理内存,强制排列在物理上处于连续的区域中。默认情况下,内核栈对于x86-32和大多数其他32位系统(在内核构建期间可以配置4KB内核栈的选项)为8KB,在x86-64系统上为16KB。
当内核服务在当前进程上下文中被调用时,它们需要在进行任何相关操作之前验证进程的特权。要执行这类验证,内核服务必须能够访问当前进程的任务结构体并查看相关字段。同样,内核例程可能需要访问当前task structure,以修改各种资源结构体(如信号处理程序表),查找被挂起的信号、文件描述符表和内存描述符等。为了能够在运行时访问task structure,内核将当前task structure的地址加载到处理器寄存器(所选的寄存器与体系结构相关)中,并通过称为current的内核全局宏提供访问(在体系结构特定的内核头文件asm/current.h中定义):
/* arch/ia64/include/asm/current.h */ #ifndef _ASM_IA64_CURRENT_H #define _ASM_IA64_CURRENT_H /* * Modified 1998-2000 * David Mosberger-Tang <davidm@hpl.hp.com>, Hewlett-Packard Co */ #include <asm/intrinsics.h> /* * In kernel mode, thread pointer (r13) is used to point to the current task * structure. */ #define current ((struct task_struct *) ia64_getreg(_IA64_REG_TP)) #endif /* _ASM_IA64_CURRENT_H */ /* arch/powerpc/include/asm/current.h */ #ifndef _ASM_POWERPC_CURRENT_H #define _ASM_POWERPC_CURRENT_H #ifdef __KERNEL__ /* * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version * 2 of the License, or (at your option) any later version. */ struct task_struct; #ifdef __powerpc64__ #include <linux/stddef.h> #include <asm/paca.h> static inline struct task_struct *get_current(void) { struct task_struct *task; __asm__ __volatile__("ld %0,%1(13)" : "=r" (task) : "i" (offsetof(struct paca_struct, __current))); return task; } #define current get_current() #else /* * We keep `current' in r2 for speed. */ register struct task_struct *current asm ("r2"); #endif #endif /* __KERNEL__ */ #endif /* _ASM_POWERPC_CURRENT_H */
然而,在寄存器受限的体系结构中,只有很少的寄存器可用,预留一个寄存器来保存当前任务结构体的地址是不可行的。在这样的平台上,当前进程的task structure可以直接在其拥有的内核栈的顶部使用。通过屏蔽栈指针的最低有效位,这种方法在确定task structure位置方面具有很大的优势。
随着内核的演变,task structure增长并变得太大而无法容纳在内核栈中,而内核栈已经被限制在物理内存中(8KB)。因此,除了一些关键字段(如定义进程的CPU状态和其他底层处理器相关的信息),task structure已经被移出内核栈。然后将这些字段封装在一个新创建的结构体中,称为struct thread_info。这个结构体包含在内核栈的顶部,并提供一个指向当前task structure的指针,该指针可以被内核服务所使用。
下面的代码片段展示了x86体系结构(内核3.10)的struct thread_info:
/* linux-3.10/arch/x86/include/asm/thread_info.h */ struct thread_info { struct task_struct *task; /* main task structure */ struct exec_domain *exec_domain; /* execution domain */ __u32 flags; /* low level flags */ __u32 status; /* thread synchronous flags */ __u32 cpu; /* current CPU */ int preempt_count; /* 0 => preemptable, <0 => BUG */ mm_segment_t addr_limit; struct restart_block restart_block; void __user *sysenter_return; #ifdef CONFIG_X86_32 unsigned long previous_esp; /* ESP of the previous stack in case of nested (IRQ) stacks */ __u8 supervisor_stack[0]; #endif unsigned int sig_on_uaccess_error:1; unsigned int uaccess_err:1; /* uaccess failed */ };
使用包含了进程相关信息的thread_info,除task structure之外,内核对当前进程结构体有多个视角:一个与体系结构无关的信息块struct task_struct和一个与体系结构相关的thread_info。图1-6描述了thread_info和task_struct。
图1-6
对于使用thread_info的体系结构,内核修改了当前宏的实现,以查看内核栈的顶部,从而获取对当前thread_info的引用,并通过它来获得当前的task structure。下面的代码片段所示为当前x86-64平台的实现。
#ifndef __ASM_GENERIC_CURRENT_H #define __ASM_GENERIC_CURRENT_H #include <linux/thread_info.h> #define get_current() (current_thread_info()->task) #define current get_current() #endif /* __ASM_GENERIC_CURRENT_H */ /* * how to get the current stack pointer in C */ register unsigned long current_stack_pointer asm ("sp"); /* * how to get the thread information struct from C */ static inline struct thread_info *current_thread_info(void) __attribute_const__; static inline struct thread_info *current_thread_info(void) { return (struct thread_info *) (current_stack_pointer & ~(THREAD_SIZE - 1)); }
随着近段时间越来越多地使用PER_CPU变量,进程调度器进行了优化,它在PER_CPU区域中缓存了当前与进程相关的关键信息。这一更改使得可以通过查找内核栈来快速访问当前进程数据。下面的代码片段所示为current宏通过PER_CPU变量获取当前任务数据的实现。
#ifndef _ASM_X86_CURRENT_H #define _ASM_X86_CURRENT_H #include <linux/compiler.h> #include <asm/percpu.h> #ifndef __ASSEMBLY__ struct task_struct; DECLARE_PER_CPU(struct task_struct *, current_task); static __always_inline struct task_struct *get_current(void) { return this_cpu_read_stable(current_task); } #define current get_current() #endif /* __ASSEMBLY__ */ #endif /* _ASM_X86_CURRENT_H */
使用PER_CPU数据会导致thread_info中的信息逐渐减少。随着thread_info规模的缩小,内核开发者正在考虑通过将thread_info移动到task structure中,从而完全清除thread_info。由于这涉及对底层体系结构代码的修改,目前只在x86-64体系结构中实现了,而其他体系结构也计划跟随这项改动。以下代码片段所示为只有一个元素的thread_info结构体的当前状态。
/* linux-4.9.10/arch/x86/include/asm/thread_info.h */ struct thread_info { unsigned long flags; /* low level flags */ };