gitbook/趣谈Linux操作系统/docs/93014.md
2022-09-03 22:05:03 +08:00

344 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 14 | 进程数据结构(下):项目多了就需要项目管理系统
上两节,我们解读了task\_struct的大部分的成员变量。这样一个任务执行的方方面面,都可以很好地管理起来,但是其中有一个问题我们没有谈。在程序执行过程中,一旦调用到系统调用,就需要进入内核继续执行。那如何将用户态的执行和内核态的执行串起来呢?
这就需要以下两个重要的成员变量:
```
struct thread_info thread_info;
void *stack;
```
## 用户态函数栈
在用户态中,程序的执行往往是一个函数调用另一个函数。函数调用都是通过栈来进行的。我们前面大致讲过函数栈的原理,今天我们仔细分析一下。
函数调用其实也很简单。如果你去看汇编语言的代码,其实就是指令跳转,从代码的一个地方跳到另外一个地方。这里比较棘手的问题是,参数和返回地址应该怎么传递过去呢?
我们看函数的调用过程,A调用B、调用C、调用D,然后返回C、返回B、返回A,这是一个后进先出的过程。有没有觉得这个过程很熟悉?没错,咱们数据结构里学的栈,也是后进先出的,所以用栈保存这些最合适。
在进程的内存空间里面,栈是一个从高地址到低地址,往下增长的结构,也就是上面是栈底,下面是栈顶,入栈和出栈的操作都是从下面的栈顶开始的。
![](https://static001.geekbang.org/resource/image/ae/2e/aec865abccf0308155f4138cc905972e.jpg?wh=1846*1619)
我们先来看32位操作系统的情况。在CPU里,**ESP**(Extended Stack Pointer)是栈顶指针寄存器,入栈操作Push和出栈操作Pop指令,会自动调整ESP的值。另外有一个寄存器**EBP**(Extended Base Pointer),是栈基地址指针寄存器,指向当前栈帧的最底部。
例如,A调用B,A的栈里面包含A函数的局部变量,然后是调用B的时候要传给它的参数,然后返回A的地址,这个地址也应该入栈,这就形成了A的栈帧。接下来就是B的栈帧部分了,先保存的是A栈帧的栈底位置,也就是EBP。因为在B函数里面获取A传进来的参数,就是通过这个指针获取的,接下来保存的是B的局部变量等等。
当B返回的时候,返回值会保存在EAX寄存器中,从栈中弹出返回地址,将指令跳转回去,参数也从栈中弹出,然后继续执行A。
对于64位操作系统,模式多少有些不一样。因为64位操作系统的寄存器数目比较多。rax用于保存函数调用的返回结果。栈顶指针寄存器变成了rsp,指向栈顶位置。堆栈的Pop和Push操作会自动调整rsp,栈基指针寄存器变成了rbp,指向当前栈帧的起始位置。
改变比较多的是参数传递。rdi、rsi、rdx、rcx、r8、r9这6个寄存器,用于传递存储函数调用时的6个参数。如果超过6的时候,还是需要放到栈里面。
然而,前6个参数有时候需要进行寻址,但是如果在寄存器里面,是没有地址的,因而还是会放到栈里面,只不过放到栈里面的操作是被调用函数做的。
![](https://static001.geekbang.org/resource/image/77/c0/770b0036a8b2695463cd95869f5adec0.jpg?wh=1840*1973)
以上的栈操作,都是在进程的内存空间里面进行的。
## 内核态函数栈
接下来,我们通过系统调用,从进程的内存空间到内核中了。内核中也有各种各样的函数调用来调用去的,也需要这样一个机制,这该怎么办呢?
这时候,上面的成员变量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)
```
内核栈是一个非常特殊的结构,如下图所示:
![](https://static001.geekbang.org/resource/image/31/2d/31d15bcd2a053235b5590977d12ffa2d.jpeg?wh=2126*1553)
这段空间的最低位置,是一个thread\_info结构。这个结构是对task\_struct结构的补充。因为task\_struct结构庞大但是通用,不同的体系结构就需要保存不同的东西,所以往往与体系结构有关的,都放在thread\_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,已经指向内核栈的栈顶,在内核态里的调用都有和用户态相似的过程。
## 通过task\_struct找内核栈
如果有一个task\_struct的stack指针在手,你可以通过下面的函数找到这个线程内核栈:
```
static inline void *task_stack_page(const struct task_struct *task)
{
return task->stack;
}
```
从task\_struct如何得到相应的pt\_regs呢?我们可以通过下面的函数:
```
/*
* 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; \
})
```
你会发现,这是先从task\_struct找到内核栈的开始位置。然后这个位置加上THREAD\_SIZE就到了最后的位置,然后转换为struct pt\_regs,再减一,就相当于减少了一个pt\_regs的位置,就到了这个结构的首地址。
这里面有一个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。这是为什么呢?因为压栈pt\_regs有两种情况。我们知道,CPU用ring来区分权限,从而Linux可以区分内核态和用户态。
因此,第一种情况,我们拿涉及从用户态到内核态的变化的系统调用来说。因为涉及权限的改变,会压栈保存SS、ESP寄存器的,这两个寄存器共占用8个byte。
另一种情况是,不涉及权限的变化,就不会压栈这8个byte。这样就会使得两种情况不兼容。如果没有压栈还访问,就会报错,所以还不如预留在这里,保证安全。在64位上,修改了这个问题,变成了定长的。
好了,现在如果你task\_struct在手,就能够轻松得到内核栈和内核寄存器。
## 通过内核栈找task\_struct
那如果一个当前在某个CPU上执行的进程,想知道自己的task\_struct在哪里,又该怎么办呢?
这个艰巨的任务要交给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
```
那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 变量里面了。
多核情况下,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)
```
好了,现在如果你是一个进程,正在某个CPU上运行,就能够轻松得到task\_struct了。
## 总结时刻
这一节虽然只介绍了内核栈,但是内容更加重要。如果说task\_struct的其他成员变量都是和进程管理有关的,内核栈是和进程运行有关系的。
我这里画了一张图总结一下32位和64位的工作模式,左边是32位的,右边是64位的。
* 在用户态,应用程序进行了至少一次函数调用。32位和64的传递参数的方式稍有不同,32位的就是用函数栈,64位的前6个参数用寄存器,其他的用函数栈。
* 在内核态,32位和64位都使用内核栈,格式也稍有不同,主要集中在pt\_regs结构上。
* 在内核态,32位和64位的内核栈和task\_struct的关联关系不同。32位主要靠thread\_info,64位主要靠Per-CPU变量。
![](https://static001.geekbang.org/resource/image/82/5c/82ba663aad4f6bd946d48424196e515c.jpeg?wh=3811*2824)
## 课堂练习
这一节讲函数调用的时候,我们讲了函数栈的工作模式。请你写一个程序,然后编译为汇编语言,打开看一下,函数栈是如何起作用的。
欢迎留言和我分享你的疑惑和见解,也欢迎你收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习、进步。
![](https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg?wh=1110*659)