概念

在每一个进程的生命周期中,经常会通过系统调用(SYSCALL)陷入内核。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先用户空间中的栈,而是一个内核空间的栈,这个称作进程的“内核栈”。

每个task的栈分成用户栈和内核栈两部分,进程内核栈在kernel中的定义是:

union thread_union {struct thread_info thread_info;unsigned long stack[THREAD_SIZE/sizeof(long)];
};

每个task的内核栈大小THREAD_SIZE :

x86:#define THREAD_SIZE_ORDER 1#define THREAD_SIZE        (PAGE_SIZE << THREAD_SIZE_ORDER)因此是8K
x86_64:#define THREAD_SIZE_ORDER (2 + KASAN_STACK_ORDER)#define THREAD_SIZE  (PAGE_SIZE << THREAD_SIZE_ORDER)PAGE_SIZE默认4K,KASAN_STACK_ORDER没有定义时为0,因此是16KARM:8k
ARM64:16K

在32位系统是8KB,64位系统里是16KB。

thread_info 有什么用?

进程在内核中相关的主要数据结构有进程描述符task_struct、threadinfo和mm_struct。上面的共同体thread_union 里,就有thread_info。我们都熟悉进程描述符task_struct,那么thread_info有什么用?

实际上在linux kernel中,task_struct、thread_info都用来保存进程相关信息,即进程PCB信息。然而不同的体系结构里,进程需要存储的信息不尽相同,linux使用task_struct存储通用的信息,将体系结构相关的部分存储在thread_info中。这也是为什么struct task_struct在include/linux/sched.h中定义,而thread_info 在arch/ 下体系结构相关头文件里。

thread_info 、内核栈、task_struct 关联

三者都是密切相关的,服务于进程的关键数据结构,在内核中定义截取如下:

struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASKstruct thread_info thread_info;
#endif
… …void         *stack;
… …
}/* * */
union thread_union {
#ifndef CONFIG_ARCH_TASK_STRUCT_ON_STACKstruct task_struct task;
#endif
#ifndef CONFIG_THREAD_INFO_IN_TASKstruct thread_info thread_info;
#endifunsigned long stack[THREAD_SIZE/sizeof(long)];
};/* x86 */
struct thread_info {unsigned long       flags;      /* low level flags */u32            status;     /* thread synchronous flags */
};/* ARM */
struct thread_info {unsigned long       flags;      /* low level flags */int            preempt_count;  /* 0 => preemptable, <0 => bug */mm_segment_t        addr_limit; /* address limit */struct task_struct   *task;      /* main task structure */
… …
};

根据宏“CONFIG_THREAD_INFO_IN_TASK”的存在与否,三者在内核中存在两种不同关联:

(1)thread_info 结构在进程内核栈中

即当“CONFIG_THREAD_INFO_IN_TASK = N”时,thread_info和栈stack 在一个联合体thread_union内,共享一块内存,即thread_info在栈所在物理页框上。

进程描述符task_struct 中的成员“void *stack”指向内核栈。不同的是,在ARM中,struct thread_info 结构体有成员“struct task_struct *task”指向进程描述符task_struct,而x86文件中没有。实际上早期内核3.X版本中,x86下的 thread_info 里也有task_struct的指针,后续版本被删除,具体原因到后面介绍“current”宏时再详细介绍。

至此三者关系可以描述如下(x86中没有info.task指针这条线):

图一

因为thread_info 结构和stack是 联合体,thread_info的地址就是栈所在页框的基地址。因此当我们获得当前进程内核栈的sp寄存器存储的地址时,根据THREAD_SIZE对齐就可以获取thread_info结构的基地址(后面介绍current宏会详细分析)。

(2)thread_info 结构在进程描述符中(task_struct)

即当“CONFIG_THREAD_INFO_IN_TASK = N”时,thread_info就是struct task_struct的第一个成员。union thread_union 中只有栈,即栈和thread_info 结构不再共享一块内存。task.stack依旧存在。三者关系可描述为:

(3)有一点需要注意,进程描述符中的 task_struct.stack指针,是指向栈区域内存基地址,即thread_union.stack 数组基地址,既不是栈顶也不是栈底,栈顶存在寄存器rsp中,栈底是task_struct.stack+THREAD_SIZE,代码中引用时需要注意。

current 宏

内核中经常通过current宏来获得当前进程对应的struct task_sturct结构,其原理离不开进程内核栈,在介绍完了thread_info、task_sturct和内核栈关系后,我们来看下current宏的具体实现。由于内核栈和体系结构相关,本文分别摘选x86和ARM的源码进行分析:

1、arm

查看arm架构的源码发现,前面提到的CONFIG_THREAD_INFO_IN_TASK宏是关闭的,且没有提供对外kconfig接口。也就是说在32位 arm架构中,thread_info 结构肯定在进程内核栈中。下面这种current宏适用于所有符合“thread_info 结构在内核栈中”的架构:

//arch/arm/include/asm/thread_info.hregister unsigned long current_stack_pointer asm ("sp");static inline struct thread_info *current_thread_info(void){return (struct thread_info *)(current_stack_pointer & ~(THREAD_SIZE - 1));}//include/asm-generic/current.h#define get_current() (current_thread_info()->task)#define current get_current()

先通过“sp”栈顶寄存器获取到当前进程的栈地址,通过mask计算,根据page对齐原理就可以拿到位于栈内存区域底部的struct thread_info地址。info->task就是当前进程的进程描述符。

2、ARM64

ARM64增加了很多通用寄存器,使用寄存器传递进程描述符显然效率更高。因此在ARM64架构里,current宏不再通过栈偏移量得到进程描述符地址,而是借用专门的寄存器:

//arch/arm64/include/asm/current.hstatic __always_inline struct task_struct *get_current(void){unsigned long sp_el0;asm ("mrs %0, sp_el0" : "=r" (sp_el0));return (struct task_struct *)sp_el0;}#define current get_current()

ARM64使用sp_el0,在进程切换时暂存进程描述符地址。

sp就是堆栈寄存器。在ARM64里,CPU运行在四个级别(或者叫运行空间),分别是el0、el1、el2、el3,el0则就是用户空间,el1则是内核空间。sp_el0就是用户栈,本文不再详细扩展,感兴趣的可以阅读网络博客《ARMv8学习》一文。

3、x86

在早期内核代码中(2.x 3.x),thread_info结构中还有指向struct task_sturct结构的指针成员,在x86上也可以采用和32位ARM类似的获取方式(CONFIG_THREAD_INFO_IN_TASK = N时)。然而在x86体系结构中,linux kernel一直采用的是另一种方式:使用了current_task这个每CPU变量,来存储当前正在使用的cpu的进程描述符struct task_struct。源码如下:

//arch/x86/include/asm/current.hDECLARE_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()

x86上通用寄存器有限,无法像ARM中那样单独拿出寄存器来存储进程描述符task_sturct结构的地址。由于采用了每cpu变量current_task来保存当前运行进程的task_struct,所以在进程切换时,就需要更新该变量。在arch/x86/kernel/process_64.c文件中的__switch_to函数中有如下代码来更新此全局变量:

this_cpu_write(current_task, next_p);

SYSCALL过程调用规范

篇幅有限,本文只选取x86_64架构来分析SYSCALL过程调用和内核栈的结构。内核栈和用户空间的栈帧结构是一样的,可参考之前写的一篇《x86栈帧原理》。

不过由于syscall属于特殊的过程调用,涉及到栈切换,和用户空间过程调用不同之处有:

1)进程内核栈除了需要保存内核空间过程调用外,还需要保存用户空间栈的数据和返回地址,以便 在返回用户空间继续执行。

(2)过程调用中寄存器调用约定不同。用户空间进程过程调用约定在上一篇《x86通用寄存器》。内核SYSCALL 过程调用约定遵循C ABI ,规定如下:

Registers on entry:* rax  system call number* rcx  return address* r11  saved rflags (note: r11 is callee-clobbered register in C ABI)* rdi   arg0* rsi    arg1* rdx  arg2* r10  arg3 (needs to be moved to rcx to conform to C ABI)* r8    arg4* r9    arg5* (note: r12-r15, rbp, rbx are callee-preserved in C ABI)

主要区别在SYSCALL时,使用rcx寄存器保存 rip的值(即返回地址),第四个参数就用r10 来保存!内核中参数使用例子:

图三

x86_64进程栈切换

前面花了大篇幅介绍thread_info和stack关系、过程调用规范,是为了能更加清晰认识本文的主角:内核栈。进程通过syscall陷入内核时进行栈切换,我们通过分析整个栈切换流程来逐步描绘内核栈结构。

因为进程内核栈和体系结构密切相关,本文只选取x86_64架构来分析内核栈的结构。下面先来介绍一个重要的数据结构:struct pt_regs 。linux kernel 使用它来格式化内核栈:

//arch/x86/include/asm/ptrace.h
struct pt_regs {
/** C ABI says these regs are callee-preserved. They aren't saved on kernel entry* unless syscall needs a complete, fully filled "struct pt_regs".*/unsigned long r15;unsigned long r14;unsigned long r13;unsigned long r12;unsigned long rbp;unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */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;
/* Return frame for iretq */unsigned long ip;unsigned long cs;unsigned long flags;unsigned long sp;unsigned long ss;
/* top of stack page */
};

内核栈按照这个顺序缓存各个寄存器存储的用户空间数据/地址,下面会结合源码详细分析。

内核SYSCALL 入口代码在entry_64.S中,了解进程栈结构,需要看在陷入内核后,CPU都做了哪些堆栈操作。下面看下入口处部分汇编源码:

//arch/x86/entry/entry_64.S
ENTRY(entry_SYSCALL_64)UNWIND_HINT_EMPTY/* Interrupts are off on entry. */swapgs// 将用户栈偏移保存到 per-cpu 变量 rsp_scratch 中movq   %rsp, PER_CPU_VAR(rsp_scratch)// 切换到进程内核栈movq   PER_CPU_VAR(cpu_current_top_of_stack), %rsp/* 在栈中倒序构建 struct pt_regs */pushq    $__USER_DS          /* pt_regs->ss */pushq   PER_CPU_VAR(rsp_scratch)    /* pt_regs->sp */pushq   %r11                /* pt_regs->flags */pushq    $__USER_CS          /* pt_regs->cs */pushq   %rcx                /* pt_regs->ip */
GLOBAL(entry_SYSCALL_64_after_hwframe)//rax 保存着系统调用号pushq   %rax                /* pt_regs->orig_ax */PUSH_AND_CLEAR_REGS rax=$-ENOSYSTRACE_IRQS_OFF/* 保存参数到寄存器,调用do_syscall_64函数 */movq %rax, %rdimovq  %rsp, %rsi
call    do_syscall_64       /* returns with IRQs disabled */

(1)指令“movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp”使栈顶寄存器载入进程内核栈地址,实现了用户栈到进程内核栈的切换;

(2)后续依次将用户空间寄存器压栈,和上面的数据结构struct pt_regs 成员一一对应(顺序固定且是倒序)。有三点需要注意:

1)%rcx寄存器保存在了pt_regs->ip 位置,是因为根据 Intel SDM,syscall 会将当前 rip 存到 rcx ,然后将 IA32_LSTAR 加载到 rip 。因此用户空间下一条指令就是从%rcx寄存器中获取;
2)系统调用号(sys_call_table索引号)保存在%rax中;
3)PUSH_AND_CLEAR_REGS 宏包含剩余寄存器入栈指令,展开如下:

//arch/x86/entry/calling.h
.macro PUSH_AND_CLEAR_REGS rdx=%rdx rax=%rax save_ret=0.if \save_retpushq   %rsi            /* pt_regs->si */movq    8(%rsp), %rsi   /* temporarily store the return address in %rsi */movq    %rdi, 8(%rsp)   /* pt_regs->di (overwriting original return address) */.elsepushq   %rdi            /* pt_regs->di */pushq   %rsi            /* pt_regs->si */.endifpushq   \rdx            /* pt_regs->dx */xorl    %edx, %edx      /* nospec   dx */pushq   %rcx            /* pt_regs->cx */xorl    %ecx, %ecx      /* nospec   cx */pushq   \rax            /* pt_regs->ax */pushq   %r8             /* pt_regs->r8 */xorl    %r8d, %r8d      /* nospec   r8 */pushq   %r9             /* pt_regs->r9 */xorl    %r9d, %r9d      /* nospec   r9 */pushq   %r10            /* pt_regs->r10 */xorl    %r10d, %r10d    /* nospec   r10 */pushq   %r11            /* pt_regs->r11 */xorl    %r11d, %r11d    /* nospec   r11*/
//后面的寄存器是caller-saved,这里可能是空的pushq   %rbx            /* pt_regs->rbx */xorl    %ebx, %ebx      /* nospec   rbx*/pushq   %rbp            /* pt_regs->rbp */xorl    %ebp, %ebp      /* nospec   rbp*/pushq   %r12            /* pt_regs->r12 */xorl    %r12d, %r12d    /* nospec   r12*/pushq   %r13            /* pt_regs->r13 */xorl    %r13d, %r13d    /* nospec   r13*/pushq   %r14            /* pt_regs->r14 */xorl    %r14d, %r14d    /* nospec   r14*/pushq   %r15            /* pt_regs->r15 */xorl    %r15d, %r15d    /* nospec   r15*/

在x86_64中,在内核栈中,rbx rbp r12 r13 r14 r15不是必须保存的项(为了访问不越界相应空间必须保留),根据需要保存,linux后续版本采取都保存方式;

(3)和IA32相比,x86_64内核栈起始位置没有预留8KB空间(STACK_PADDIN),是因为在x86_64中,SYCALL过程内核栈所有寄存器都由软件压栈保存,不存在硬件可能没有压栈,防止越界预留位置的情况。在这里贴上内核中关于STACK_PADDING定义:

/* x86_64 has a fixed-length stack frame */
#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

在x86_64中,linux内核栈、struct pt_regs、current宏、struct task_struct关系总结如下图:

图四

整个图四就是linux SYSCALL,x86_64栈切换的完整过程。图中表格第一列是数据结构struct pt_regs 逆序成员,第二列是栈切换后,依次压栈的寄存器,第三列是寄存器中存放的数据类型。

参考

本文涉及到的源码均来自linux kernel 4.18.0

https://zhuanlan.zhihu.com/p/296750228

linux 进程内核栈相关推荐

  1. Linux进程管理与调度-之-目录导航【转】

    转自:http://blog.csdn.net/gatieme/article/details/51456569 版权声明:本文为博主原创文章 && 转载请著名出处 @ http:// ...

  2. Linux进程描述符task_struct结构体简析

    进程是处于执行期的程序以及它所管理的资源(如打开的文件.挂起的信号.进程状态.地址空间等等)的总称 Linux内核通过一个被称为进程描述符的task_struct结构体来管理进程,这个结构体包含了一个 ...

  3. Linux进程管理 (7)实时调度

    关键词:RT.preempt_count.RT patch. 除了CFS调度器之外,还包括重要的实时调度器,有两种RR和FIFO调度策略.本章只是一个简单的介绍. 更详细的介绍参考<Linux进 ...

  4. Linux进程退出详解(do_exit)--Linux进程的管理与调度(十四)

    Linux进程的退出 linux下进程退出的方式 正常退出 从main函数返回return 调用exit 调用_exit 异常退出 调用abort 由信号终止 _exit, exit和_Exit的区别 ...

  5. Linux进程描述符task_struct结构体详解--Linux进程的管理与调度(一)

    转自:http://blog.csdn.net/gatieme/article/details/51383272 日期 内核版本 架构 作者 GitHub CSDN 2016-05-12 Linux- ...

  6. Linux进程模型总结

    一个进程在CPU上运行可以有两种运行模式(进程状态):用户模式和内核模式.如果当前运行的是用户程序(用户代码),那么对应进程就处于用户模式(用户态),如果出现系统调用或者发生中断,那么对应进程就处于内 ...

  7. Linux下0号进程的前世(init_task进程)今生(idle进程)----Linux进程的管理与调度(五)【转】...

    前言 Linux下有3个特殊的进程,idle进程(PID = 0), init进程(PID = 1)和kthreadd(PID = 2) idle进程由系统自动创建, 运行在内核态 idle进程其pi ...

  8. linux 进程调度类型 总结,Linux进程模型总结

    来源于网络 原创不详 Linux进程通过一个task_struct结构体描述,在linux/sched.h中定义,通过理解该结构,可更清楚的理解linux进程模型.   包含进程所有信息的task_s ...

  9. Linux 进程管理数据结构

    文末集赞留言抽奖,我会选出留言点赞数前 3 名送出小米耳机. 别刷赞啊,刷赞被举报无效,相信真的是公众号粉丝的读者,不会做这样的行为,刷赞指的是购买外挂刷,如果是转发到朋友圈和微信群的,不算刷赞行为. ...

  10. linux进程上下文切换的具体过程,Linux实验三 结合中断上下文切换和进程上下文切换分析Linux内核一般执行过程...

    fork系统调?创建?进程,也就?个进程变成了两个进程,两个进程执?相同的代码,只是fork系统调?在?进程和?进程中的返回值不同. 打开linux-5.4.34/arch/x86/entry/sys ...

最新文章

  1. Windows 2008 R2+iis7.5环境下Discuz!X3论坛伪静态设置方法
  2. springmvc集成oracle,SpringMVC整合druid
  3. .NETFramework-Web.Mvc:ActionResult
  4. tp5 ajax 返回数据正常状态码却为500
  5. python3安装过程中出现的ssl问题,No module named _ssl或者renaming “_ssl“ since importing it failed
  6. 读《DTS分析模型、设计模型》有感
  7. Oracle学习 第26天 Toad试用感受
  8. UIPickView 和 UIDatePicker
  9. 机械设计课程设计含设计说明书
  10. 为什么选择Mapabc
  11. 家谱世系图一键生成家谱软件
  12. 2022 极术通讯-基于安谋科技 “星辰” STAR-MC1的灵动MM32F2570开发板深度评测
  13. 【2020】win10java(jdk安装)环境变量配置和相关问题解决
  14. 12306排队是什么意思_12306网上购票解答 车票购买排队中怎么办
  15. 路由器中宽带密码查看
  16. Toontrack EZDrummer for Mac - 鼓音乐制作工具
  17. 第10周---信息熵与压缩编码基础
  18. kali系统破解WiFi密码(二)
  19. 手机被DNS劫持后的更改方案
  20. 真·稳如狗:中国团队推出四足机器人,对标波士顿动力

热门文章

  1. apache配置Options详解
  2. 在gfs2中关闭selinux
  3. PB数据窗口自动换下一页
  4. Go语言实战 - 网站性能优化第一弹“七牛云存储”
  5. Query and transform XML
  6. 那些不需要你知道的Chrome DevTool - 使用技巧篇
  7. java B2B2C Springboot电子商城系统-消息队列之 RabbitMQ
  8. final关键字深入解析
  9. 网站建设解决了传统的销售模式
  10. 初学linux网络服务之DHCP实验