系统调用如何将用户态的执行和内核态的执行串起来
两个重要的成员变量
struct thread_info thread_info;
void *stack;
方式:程序的执行往往是一个函数调用另一个函数,通过栈来进行,其汇编语言的代码就是指令跳转,通过 JMP + 参数 + 返回地址 调用函数
栈: 栈是一个从高地址到低地址,往下增长的结构,也就是上面是栈底,下面是栈顶,入栈和出栈的操作都是从下面的栈顶开始
Linux 为每个 task 分配了内核栈, 32位(8K), 64位(16K)
arch/x86/include/asm/page_32_types.h
一个 PAGE_SIZE 是 4K,左移一位就是乘以 2,也就是 8K
#define THREAD_SIZE_ORDER 1
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
arch/x86/include/asm/page_64_types.h
PAGE_SIZE 的基础上左移两位,也即 16K,并且要求起始地址必须是 8192 的整数倍
#ifdef CONFIG_KASAN
#define KASAN_STACK_ORDER 1
#else
#define KASAN_STACK_ORDER 0
#endif
#define THREAD_SIZE_ORDER (2 + KASAN_STACK_ORDER)
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
最低位置是thread_info 结构,这个结构是对 task_struct 结构的补充,task_struct 结构庞大且通用,但不同的体系结构就需要保存不同的东西,所以与体系结构有关的,都放在 thread_info 里面。
include/linux/sched.h
有个union将 thread_info 和 stack 放在一起
union thread_union {
#ifndef CONFIG_THREAD_INFO_IN_TASK
struct thread_info thread_info;
#endif
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
thread_info保存了线程所需的所有特定处理器的信息, 以及通用的task_struct的指针(注 linux-4.13.16版本x86的struct thread_info只有unsigned long flags; )
物理内存中存放两种数据结构的方式
图片来自 https://blog.csdn.net/gatieme/article/details/51577479
内核栈的最高地址端,存放的是另一个结构 pt_regs
32 位和 64 位的定义不一样。
#ifdef __i386__
struct pt_regs {
unsigned long bx;
unsigned long cx;
unsigned long dx;
unsigned long si;
unsigned long di;
unsigned long bp;
unsigned long ax;
unsigned long ds;
unsigned long es;
unsigned long fs;
unsigned long gs;
unsigned long orig_ax;
unsigned long ip;
unsigned long cs;
unsigned long flags;
unsigned long sp;
unsigned long ss;
};
#else
struct pt_regs {
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long bp;
unsigned long bx;
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long ax;
unsigned long cx;
unsigned long dx;
unsigned long si;
unsigned long di;
unsigned long orig_ax;
unsigned long ip;
unsigned long cs;
unsigned long flags;
unsigned long sp;
unsigned long ss;
/* top of stack page */
};
#endif
当系统调用从用户态到内核态的时候,首先要做的第一件事情,就是将用户态运行过程中的 CPU 上下文保存起来,其实主要就是保存在这个结构的寄存器变量里。这样当从内核系统调用返回的时候,才能让进程在刚才的地方接着运行下去。
参见 06 系统调用 https://blog.csdn.net/leacock1991/article/details/106773065,压栈的值的顺序和 struct pt_regs 中寄存器定义的顺序是一样的
直接由 task_struct 内的 stack 直接得到指向 thread_info 的指针
\include\linux\sched\task_stack.h
static inline void *task_stack_page(const struct task_struct *task)
{
return task->stack;
}
\arch\x86\include\asm\processor.h
/*
* TOP_OF_KERNEL_STACK_PADDING reserves 8 bytes on top of the ring0 stack.
* This is necessary to guarantee that the entire "struct pt_regs"
* is accessible even if the CPU haven't stored the SS/ESP registers
* on the stack (interrupt gate does not save these registers
* when switching to the same priv ring).
* Therefore beware: accessing the ss/esp fields of the
* "struct pt_regs" is possible, but they may contain the
* completely wrong values.
*/
#define task_pt_regs(task) \
({ \
unsigned long __ptr = (unsigned long)task_stack_page(task); \
__ptr += THREAD_SIZE - TOP_OF_KERNEL_STACK_PADDING; \
((struct pt_regs *)__ptr) - 1; \
})
1、 从 task_struct 找到内核栈的最低位置(低地址), (unsigned long)task_stack_page(task);
2、 加上 THREAD_SIZE(高地址) 再减去TOP_OF_KERNEL_STACK_PADDING 就到了最后的位置
#ifdef CONFIG_X86_32
# ifdef CONFIG_VM86
# define TOP_OF_KERNEL_STACK_PADDING 16
# else
# define TOP_OF_KERNEL_STACK_PADDING 8
# endif
#else
# define TOP_OF_KERNEL_STACK_PADDING 0
#endif
32 位机器上是 8,其他是 0,CPU 用 ring 来区分权限,从而 Linux 可以区分内核态和用户态,涉及权限的改变,会压栈保存 SS、ESP 寄存器的,这两个寄存器共占用 8 个 byte
3、 强转为struct pt_regs再减一,就相当于减少了一个 pt_regs 的位置,就到了这个结构的首地址
通过 thread_info 这个结构获取
https://elixir.bootlin.com/linux/v4.5.7/source/arch/x86/include/asm/thread_info.h#L55
struct thread_info {
struct task_struct *task; /* main task structure */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
mm_segment_t addr_limit;
unsigned int sig_on_uaccess_error:1;
unsigned int uaccess_err:1; /* uaccess failed */
};
成员变量 task 指向 task_struct,常用 current_thread_info() 来获取 thread_info 进而通过变量task获取 task_struct。(v4.5.7版本实现如下,更早的不同版本实现会不一样)
https://elixir.bootlin.com/linux/v4.5.7/source/arch/x86/include/asm/thread_info.h#L164
static inline struct thread_info *current_thread_info(void)
{
return (struct thread_info *)(current_top_of_stack() - THREAD_SIZE);
}
current_top_of_stack() 的位置就是内核栈的最高位置(高地址),减去 THREAD_SIZE,就到了 thread_info 的起始地址(低地址)
不同内核版本实现不同
屏蔽了esp的低十三位,最终得到的是thread_info的地址
当前的栈指针(current_stack_pointer == sp)就是esp,
THREAD_SIZE为8K,二进制的表示为0000 0000 0000 0000 0010 0000 0000 0000。
~(THREAD_SIZE-1)的结果刚好为1111 1111 1111 1111 1110 0000 0000 0000,低十三位是全为零,也就是刚好屏蔽了esp的低十三位,最终得到的是thread_info的地址。
thread_info变为
include/linux/thread_info.h
struct thread_info {
unsigned long flags; /* low level flags */
};
怎么获取当前运行中的 task_struct 呢?current_thread_info 有了新的实现方式。
current_thread_info 新的实现方式
include/linux/thread_info.h
#include
#define current_thread_info() ((struct thread_info *)current)
#endif
current 又是什么
arch/x86/include/asm/current.h
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
新的机制里面,每个 CPU 运行的 task_struct 不通过 thread_info 获取了,而是直接放在 Per CPU 变量里面了。
Per CPU 变量
多核情况下,CPU 是同时运行的,但是它们共同使用其他的硬件资源的时候,我们需要解决多个 CPU 之间的同步问题。
Per CPU 变量是内核中一种重要的同步机制,Per CPU 变量就是为每个 CPU 构造一个变量的副本,这样多个 CPU 各自操作自己的副本,互不干涉。
当前进程的变量 current_task 就被声明为 Per CPU 变量。
arch/x86/include/asm/current.h
DECLARE_PER_CPU(struct task_struct *, current_task);
DECLARE_PER_CPU(struct task_struct *, current_task);变量定义
arch/x86/kernel/cpu/common.c
DEFINE_PER_CPU(struct task_struct *, current_task) = &init_task;
系统刚刚初始化的时候,current_task 都指向 init_task,当某个 CPU 上的进程进行切换的时候,current_task 被修改为将要切换到的目标进程
要获取当前的运行中的 task_struct 的时候,需要调用 this_cpu_read_stable 进行读取。
/arch/x86/include/asm/percpu.h
#define this_cpu_read_stable(var) percpu_stable_op("mov", var)
在用户态,32 位和 64 的传递参数的方式稍有不同,32 位的就是用函数栈,64 位的前 6 个参数用寄存器,其他的用函数栈。
在内核态,32 位和 64 位都使用内核栈,格式也稍有不同,主要集中在 pt_regs 结构上。
在内核态,32 位和 64 位的内核栈和 task_struct 的关联关系不同。32 位主要靠 thread_info,64 位主要靠 Per-CPU 变量。
趣谈Linux操作系统(极客时间)链接:
http://gk.link/a/10iXZ
Linux进程内核栈与thread_info结构详解–Linux进程的管理与调度(九):
https://blog.csdn.net/gatieme/article/details/51577479
欢迎大家来一起交流学习