本文仅作为学习记录,非商业用途,侵删,如需转载需作者同意。
一个任务执行的方方面面,都可以被task_struct 很好的管理起来。
执行过程中,一旦调用到系统调用,就需要进入内核继续执行。
struct thread_info thread_info; void *stack;
用户态中,程序的执行往往是一个函数调用另一个函数,函数调用通过栈来执行的,
函数调用就是指令跳转,重点是参数和返回地址怎么传递过去。
函数调用过程:
A 调用 B,调用C,调用D,然后返回C,返回B,返回A。
后进先出,栈也是后进先出
栈是一个从高地址到低地址,往下增长的结构,上面是栈底,下面是栈顶,入栈和出栈的操作都是从下面的栈顶开始的。
CPU中,ESP (Extended stack Pointer)栈顶指针寄存器,入栈操作PUSH 和出栈 POP 指令,会自动调整ESP的值,另外有一个寄存器EBP(Extended Base Pointer) 是 栈基地址指针寄存器,指向当前栈帧的最底部。
举例,对照上面的图理解:
a调用b
a的栈里面:最上面是包含a函数的局部变量,然后是要传给b的参数,然后是返回a的地址
b的栈帧:最上面是a 栈帧的栈底位置 EBP,b 函数中获取a给过来的参数就是通过这个指针获取的,然后是b的局部变量等。
当b 返回的时候,返回值保存在EAX 寄存器中,从栈中弹出返回地址,将指令跳转回去,参数也从栈中弹出,然后继续执行a
寄存器数目比较多
rax:保存函数调用的返回结果
rsp:栈顶指针寄存器
堆栈的pop和push 操作会自动调整rsp
rbp:栈基指针寄存器,指向栈帧的起始位置
rdi、rsi、rdx、rcx、r8、r9 寄存器:用于存储函数调用时的6个参数
比6个多的时候,还是需要放到栈里。
前面6个参数需要寻址的时候,因为在寄存器里是没有地址的,因而还是会被放到栈里。这个操作是 被调用函数做的。
以上的栈操作,都是在进程的内存空间里进行的。
成员变量 stack ,在内核中各种各样的函数调用就用到了。
Linux 给每个task 都分配了内核栈
32位系统上 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)
64位系统上 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 结构庞大但是通用,不同的体系结构就需要保存不同的东西,与体系结构有关的,都放在 therad_info 里。
内核代码里有这样一个 union ,将 thread_info 和 stack 放在一起,在 include/linux/sched.h 文件中就有
union thread_union { #ifndef CONFIG_THREAD_INFO_IN_TASK struct thread_info thread_info; #endif unsigned long stack[THREAD_SIZE/sizeof(long)]; };
这个 union 就是这样定义的,开头是 thread_info ,后面是 stack
在内核栈的最高地址端,存放的是另一个结构 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 上下文保存起来,主要就是保存在这个结构的寄存器变量里。
这样当从内核系统调用返回的时候,才能让进程在刚才的地方接着运行下去。
压栈的值的顺序和 struct pt_regs 中寄存器定义的顺序是一样的。
在内核中,CPU 的寄存器 ESP 或者 RSP ,已经指向内核栈的栈顶,在内核态里的调用都有和用户态相似的过程。
如果有一个当前在某个 CPU 上执行的过程,想知道自己的 task_strcut 在哪里,怎么办。
通过 thread_info 这个结构来获取:
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()->task 来获取task_struct
static inline struct thread_info *current_thread_info(void) { return (struct thread_info *)(current_top_of_stack() - THREAD_SIZE); }
thread_info 的位置就是内核栈的最高位置,减去THREAD_SIZE 就到了 thread_info 的起始地址。
但是现在变成这样了,只剩下一个 flags
struct thread_info { unsigned long flags; /* low level flags */ };
那这个时候怎么获取当前运行中的 task_struct 呢?
current_thread_info 有了新的实现方式
在 include/linux/thread_info.h 中定义了 current_thread_info
#include <asm/current.h> #define current_thread_info() ((struct thread_info *)current) #endif
在arch/x86/include/asm/current.h 中定义了current:
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 变量中。
多核情况下,CPU 是同时运行的,但是当他们共同使用其他硬件资源的时候,我们需要解决多个CPU 之间的同步问题。
Per CPU 变量是内核中一种重要的同步机制,顾名思义,Per CPU 变量就是为每个CPU 构造一个变量的副本,这样每个CPU 操作自己的副本,互不干涉。
比如当前进程的变量 current_task 就被声明为 Per CPU 变量。
要使用 Per CPU 变量,首先要声明这个变量,在 arch/x86/include/asm/current.h 中有:
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 被修改为将要切到的目标进程。例如:进程切换函数 _switch_to 就会变成 current_task
__visible __notrace_funcgraph struct task_struct * __switch_to(struct task_struct *prev_p, struct task_struct *next_p) { ...... this_cpu_write(current_task, next_p); ...... return prev_p; }
当要获取当前的运行中的 task_struct 的时候,就需要调用 this_cpu_read_stable 进行读取
#define this_cpu_read_stable(var) percpu_stable_op("mov", var)