一步一步学linux操作系统: 11 进程数据结构_task_struct字段_内核栈

系统调用如何将用户态的执行和内核态的执行串起来

两个重要的成员变量

struct thread_info    thread_info;
void  *stack;

在这里插入图片描述

用户态函数栈

方式:程序的执行往往是一个函数调用另一个函数,通过栈来进行,其汇编语言的代码就是指令跳转,通过 JMP + 参数 + 返回地址 调用函数

栈: 栈是一个从高地址到低地址,往下增长的结构,也就是上面是栈底,下面是栈顶,入栈和出栈的操作都是从下面的栈顶开始

32位栈结构

图片来自极客时间趣谈linux操作系统

  • 栈帧包含 前一个帧的 EBP + 局部变量 + N个参数 + 返回地址
  • ESP(Extended Stack Pointer)是栈顶指针寄存器
    入栈操作 Push 和出栈操作 Pop 指令,会自动调整 ESP 的值
  • EBP(Extended Base Pointer)是栈基地址指针寄存器,指向当前栈帧的最底部
  • 返回值保存在 EAX 中
  • A 调用 B
    A 的栈帧:包含 A 函数的局部变量、调用 B 的时候要传给它的参数、返回 A 时的返回地址
    B 的栈帧:A 栈帧的栈底位置(EBP)用于获取传入B的参数、B 的局部变量等等
  • B 返回
    返回值会保存在 EAX 寄存器中
    从栈中弹出返回地址,将指令跳转回去,参数也从栈中弹出,然后继续执行 A

64位栈结构

图片来自极客时间趣谈linux操作系统

  • rax 用于保存函数调用的返回结果
  • rsp栈顶指针寄存器,指向栈顶位置
    堆栈的 Pop 和 Push 操作会自动调整 rsp
  • rbp栈基指针寄存器,,指向当前栈帧的起始位置
  • 参数传递
    rdi、rsi、rdx、rcx、r8、r9 这 6 个寄存器,用于传递存储函数调用时的 6 个参数。如果超过 6 的时候,还是需要放到栈里面

内核态函数栈

成员变量 stack,内核栈

Linux 为每个 task 分配了内核栈, 32位(8K), 64位(16K)

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)

在这里插入图片描述

内核栈结构

图片来自极客时间趣谈linux操作系统
最低位置是thread_info 结构,这个结构是对 task_struct 结构的补充,task_struct 结构庞大且通用,但不同的体系结构就需要保存不同的东西,所以与体系结构有关的,都放在 thread_info 里面。

thread_union

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

  • 0x015bfff - 0x015b000 = 0x1000 = 4096 = 4k
  • thread_info和内核栈虽然共用了thread_union结构, 但是thread_info大小固定, 存储在联合体的开始部分, 而内核栈由高地址向低地址扩展, 当内核栈的栈顶到达thread_info的存储空间时, 则会发生栈溢出
  • 系统的current指针指向了当前运行进程的thread_union(或者thread_info)的地址
  • 进程task_struct中的stack指针指向了进程的thread_union(或者thread_info)的地址, 在早期的内核中这个指针用struct thread_info *thread_info来表示, 但是新的内核中用了一个更浅显的名字void *stack, 即内核栈,进程的thread_info存储在进程内核栈的最低端
pt_regs

内核栈的最高地址端,存放的是另一个结构 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 找内核栈

直接由 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;
}

在这里插入图片描述

通过 task_struct 得到相应的 pt_regs

\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 就到了最后的位置

  • 栈是一个从高地址到低地址,往下增长的结构
  • TOP_OF_KERNEL_STACK_PADDING
    • arch\x86\include\asm\
#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 的位置,就到了这个结构的首地址

通过内核栈找 task_struct
比较早的内核版本

通过 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的地址。

linux-4.13.16

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 <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 变量里面了。

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)

在这里插入图片描述

总结

图片来自极客时间趣谈linux操作系统

在用户态,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
欢迎大家来一起交流学习

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

墨1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值