Linux进程内核栈与thread_info结构详解--Linux进程的管理与调度(九)

ThreadInfo结构和内核栈的两种关系 本来本节是要学习内核启动的第一个进程的建立,也就是0号进程,也称idle进程,也称swapper进程。但是在学习第一个进程建立之前需要先学习threadinfo和内核栈的关系。 目前内核存在两种threadinfo和内核的关系,接下来我们通过画图一一举例说明。 ThreadInfo结构在内核栈中 Threadinfo结构存储在内核栈中,这种方式是最经典的。因为task_struct结构... 阅读详情

前言
为什么需要内核栈

进程在内核态运行时需要自己的堆栈信息, 因此linux内核为每个进程都提供了一个内核栈kernel stack,

struct task_struct
{
    // ...
    void *stack;    //  指向内核栈的指针
    // ...
};

内核态的进程访问处于内核数据段的栈,这个栈不同于用户态的进程所用的栈。

用户态进程所用的栈,是在进程线性地址空间中;

而内核栈是当进程从用户空间进入内核空间时,特权级发生变化,需要切换堆栈,那么内核空间中使用的就是这个内核栈。因为内核控制路径使用很少的栈空间,所以只需要几千个字节的内核态堆栈。

需要注意的是,内核态堆栈仅用于内核例程,Linux内核另外为中断提供了单独的硬中断栈和软中断栈

为什么需要thread_info

内核还需要存储每个进程的PCB信息, linux内核是支持不同体系的的, 但是不同的体系结构可能进程需要存储的信息不尽相同, 这就需要我们实现一种通用的方式, 我们将体系结构相关的部分和无关的部门进行分离

用一种通用的方式来描述进程, 这就是struct task_struct, 而thread_info就保存了特定体系结构的汇编代码段需要访问的那部分进程的数据,我们在thread_info中嵌入指向task_struct的指针, 则我们可以很方便的通过thread_info来查找task_struct

将两种结构融合在一起

linux将内核栈和进程控制块thread_info融合在一起, 组成一个联合体thread_union

通常内核栈和thread_info一同保存在一个联合体中, thread_info保存了线程所需的所有特定处理器的信息, 以及通用的task_struct的指针

内核数据结构描述
thread_union

对每个进程,Linux内核都把两个不同的数据结构紧凑的存放在一个单独为进程分配的内存区域中:

  • 一个是内核态的进程堆栈stack
  • 另一个是紧挨着进程描述符的小数据结构thread_info,叫做线程描述符。

这两个结构被紧凑的放在一个联合体中thread_union中,

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

这块区域32位上通常是8K=8192(占两个页框),64位上通常是16K,其实地址必须是8192的整数倍。

架构 THREAD_SIZE
x86arch/x86/include/asm/page_32_types.h, line 21
x86_64 arch/x86/include/asm/page_64_types.h, line 11
arm arch/arm/include/asm/thread_info.h, line 20
arm64 arch/arm64/include/asm/thread_info.h, line 32

 

 

 

 

 

 

出于效率考虑,内核让这8K(或者16K)空间占据连续的两个页框并让第一个页框的起始地址是213的倍数。

下图中显示了在物理内存中存放两种数据结构的方式。线程描述符驻留与这个内存区的开始,而栈顶末端向下增长。 下图摘自ULK3,进程内核栈与进程描述符的关系如下图:

在这个图中,

  • esp寄存器是CPU栈指针,用来存放栈顶单元的地址。在80x86系统中,栈起始于顶端,并朝着这个内存区开始的方向增长。从用户态刚切换到内核态以后,进程的内核栈总是空的。因此,esp寄存器指向这个栈的顶端。一旦数据写入堆栈,esp的值就递减。

同时我们可以看到,

  • 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存储在进程内核栈的最低端

task_struct中的内核栈stack

我们之前在描述task_struct时就提到了其stack指针指向的是内核栈的地址。

参见 Linux进程描述符task_struct结构体详解–Linux进程的管理与调度(一)

其被定义在include/linux/sched.h中

http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L1391

形式如下

struct task_struct
{
    // ...  
    void *stack;    //  指向内核栈的指针  
    // ...
};

在早期的linux内核中进程描述符中是不包含内核栈的, 相反包含着指向thread_info的指针

但是在2007年的一次更新(since 2.6.22)中加入了stack内核栈指针, 替代了原来的thread_info的指针

进程描述符task_struct结构中没有直接指向thread_info结构的指针,而是用一个void指针类型的成员表示,然后通过类型转换来访问thread_info结构。

stack指向了内核栈的地址(其实也就是thread_info和thread_union的地址),因为联合体中stack和thread_info都在起始地址, 因此可以很方便的转型

相关代码在include/linux/sched.h中 
task_thread_info用于通过task_struct来查找其thread_info的信息, 只需要一次指针类型转换即可

#define task_thread_info(task)  ((struct thread_info *)(task)->stack)


内核栈数据结构描述thread_info

thread_info是体系结构相关的,结构的定义在thread_info.h中,保存了进程所有依赖于体系结构的信息, 同时也保存了一个指向进程描述符task_struct的指针

架构    定义链接
x86    linux-4.5/arch/x86/include/asm/thread_info.h, line 55
arm    linux-4.5arch/arm/include/asm/thread_info.h, line 49
arm64    linux/4.5/arch/arm64/include/asm/thread_info.h, line 47

函数接口
内核栈与thread_info的通用操作

原则上, 只要设置了预处理器常数__HAVE_THREAD_FUNCTIONS通知内核, 那么各个体系结构就可以随意在stack数组中存储数据。

在这种情况下, 他们必须自行实现task_thread_info和task_stack_page, 这两个函数用于获取给定task_struct实例的线程信息和内核栈。

另外, 他们必须实现dup_task_struct中调用的函数setup_thread_stack, 以便确定stack成员的具体内存布局, 当前只有ia64等少数架构不依赖于内核的默认方法

下标给出了不同架构的task_thread_info和task_stack_page的实现

架构    定义链接
ia64    arch/ia64/include/asm/thread_info.h, line 53
通用    include/linux/sched.h, line 2812

 

// 未定义__HAVE_THREAD_FUNCTIONS的时候使用内核的默认操作
#ifndef __HAVE_THREAD_FUNCTIONS  

//  通过进程的task_struct来获取进程的thread_info
#define task_thread_info(task)  ((struct thread_info *)(task)->stack)
//  通过进程的task_struct来获取进程的内核栈
#define task_stack_page(task)   ((task)->stack)

//  初始化thread_info, 指定其存储结构的内存布局
static inline void setup_thread_stack(struct task_struct *p, struct task_struct *org)
{
    *task_thread_info(p) = *task_thread_info(org);
    task_thread_info(p)->task = p;
}

/*
 * Return the address of the last usable long on the stack.
 *
 * When the stack grows down, this is just above the thread
 * info struct. Going any lower will corrupt the threadinfo.
 *
 * When the stack grows up, this is the highest address.
 * Beyond that position, we corrupt data on the next page.
 */
static inline unsigned long *end_of_stack(struct task_struct *p)
{
#ifdef CONFIG_STACK_GROWSUP
    return (unsigned long *)((unsigned long)task_thread_info(p) + THREAD_SIZE) - 1;
#else
    return (unsigned long *)(task_thread_info(p) + 1);
#endif
}

#endif


在内核的某个特定组建使用了较多的栈空间时, 内核栈会溢出到thread_info部分, 因此内核提供了kstack_end函数来判断给出的地址是否位于栈的有效部分

#ifndef __HAVE_ARCH_KSTACK_END
static inline int kstack_end(void *addr)
{
    /* Reliable end of stack detection:
     * Some APM bios versions misalign the stack
     */
    return !(((unsigned long)addr+sizeof(void*)-1) & (THREAD_SIZE-sizeof(void*)));
}
#endif

前面我们在讲_do_fork创建进程的时候, 提到dup_task_struct会复制父进程的task_struct和thread_info实例的内容, 但是stack则与新的thread_info实例位于同一个内存, 这意味着父子进程的task_struct此时除了栈指针之外完全相同。

获取当前在CPU上正在运行进程的thread_info

所有的体系结构都必须实现两个current和current_thread_info的符号定义宏或者函数,

  • current_thread_info可获得当前执行进程的thread_info实例指针, 其地址可以根据内核指针来确定, 因为thread_info总是位于起始位置,因为每个进程都有自己的内核栈, 因此进程到内核栈的映射是唯一的, 那么指向内核栈的指针通常保存在一个特别保留的寄存器中(多数情况下是esp)
  • current给出了当前进程进程描述符task_struct的地址,该地址往往通过current_thread_info来确定 current = current_thread_info()->task

因此我们的关键就是current_thread_info的实现了,即如何通过esp栈指针来获取当前在CPU上正在运行进程的thread_info结构。

早期的版本中,不需要对64位处理器的支持,所以,内核通过简单的屏蔽掉esp的低13位有效位就可以获得thread_info结构的基地址了。

我们在下面对比了,获取正在运行的进程的thread_info的实现方式

架构    版本    定义链接    实现方式    思路解析
x86    3.14    current_thread_info(void)    return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));    

屏蔽了esp的低十三位,最终得到的是thread_info的地址


x86    3.15    current_thread_info(void)    ti = (void *)(this_cpu_read_stable(kernel_stack) + KERNEL_STACK_OFFSET - THREAD_SIZE);    


x86    4.1    current_thread_info(void)    (struct thread_info *)(current_top_of_stack() - THREAD_SIZE);    

早期版本

当前的栈指针(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的地址。

进程最常用的是进程描述符结构task_struct而不是thread_info结构的地址。为了获取当前CPU上运行进程的task_struct结构,内核提供了current宏,由于task_struct *task在thread_info的起始位置,该宏本质上等价于current_thread_info()->task,在include/asm-generic/current.h中定义:

#define get_current() (current_thread_info()->task)
#define current get_current()

这个定义是体系结构无关的,当然linux也为各个体系结构定义了更加方便或者快速的current

请参见 :http://lxr.free-electrons.com/ident?v=4.5;i=current

 

分配和销毁thread_info

进程通过alloc_thread_info_node函数分配它的内核栈,通过free_thread_info函数释放所分配的内核栈。

# if THREAD_SIZE >= PAGE_SIZE
static struct thread_info *alloc_thread_info_node(struct task_struct *tsk,
                          int node)
{
    struct page *page = alloc_kmem_pages_node(node, THREADINFO_GFP,
                          THREAD_SIZE_ORDER);

    return page ? page_address(page) : NULL;
}

static inline void free_thread_info(struct thread_info *ti)
{
    free_kmem_pages((unsigned long)ti, THREAD_SIZE_ORDER);
}
# else
static struct kmem_cache *thread_info_cache;

static struct thread_info *alloc_thread_info_node(struct task_struct *tsk,
                          int node)
{
    return kmem_cache_alloc_node(thread_info_cache, THREADINFO_GFP, node);
}

static void free_thread_info(struct thread_info *ti)
{
    kmem_cache_free(thread_info_cache, ti);
}


其中,THREAD_SIZE_ORDER宏的定义请查看       
 

架构 版本定义链接实现方式 思路解析
x864.5arch/x86/include/asm/page_32_types.h, line 20define THREAD_SIZE_ORDER 1__get_free_pages函数分配2个页的内存(它的首地址是8192字节对齐的)
x86_644.5arch/x86/include/asm/page_64_types.h, line 10define THREAD_SIZE_ORDER (2 + KASAN_STACK_ORDER)   

 

深入linux内核架构--进程&线程 进程和线程这两个词,每个程序员都十分熟悉,但是想要很清晰的描述出来却有一种不知道从何说起的感觉。所以今天结合一个具体的例子来描述一下进程与线程的相关概念:在terminal上敲出a.out这个自己编译出来可执行程序路径后,这个程序是怎么在linux系统中运行起来的?如果对于这个问题有兴趣的同学可以继续往下看。对于linux内核下进程和线程的理解程度我经历过以下几个阶段:那么我们就通过结合linux源码以及一些man文档一起来深入了解一下进程和线程的实现,结合简介中说的例子来看一下linux用户进程及线程是如 阅读详情

相关推荐

Linux内核--进程管理(二)内核进程管理(创建与唤醒)原理详解

Linux是一种动态系统,能够适应不断变化的计算需求。Linux计算需求的表现是以进程的通用抽象为中心的。进程可以是短期的(从命令行执行的一个命令),也可以是长期的(一种网络服务)。因此,对进程及其调度进行一般管理就显得极为重要。

文艺小少年的博客 1165

Linux进程的内核栈和thread_info数据结构

Linux进程的内核栈和thread_info数据结构;

bcbobo21cn的专栏 443

OpenHarmony v4.1 Release设置应用随系统自动启动

OpenHarmony 4.1 版本中的 Ability Manager Service (AMS) 提供了应用自启动管理功能,允许系统控制哪些应用可以在系统启动时自动运行。这需要以下两个主要步骤:启用系统的自启动管理支持开发一个系统应用来管理自启动配置。

afufufufu的博客 1898

关于THREADINFO结构

线程为窗口维护一个threadinfo结构,  threadinfo数据结构:  ------------------  |登记消息队列指针|  ------------------  |虚拟输入队列指针|  ------------------  |发送消息队列指针|  ------------------  |应答消息队列指针|  ------------------

Tommy(凌飞)的专栏 6217

C# Code-Behind 技术全面解析

Code-Behind 是 ASP.NET Web Forms 中的一种技术模式,通过将用户界面标记(.aspx文件)与程序逻辑代码(.aspx.cs文件)分离,实现表现层与逻辑层的解耦。其核心思想是前端展示与后台逻辑的物理文件分离,通过ASP.NET编译器将两者合并编译成一个Page类并执行。Code-Behind 的优势包括较好的代码组织、较高的开发效率,尤其适合快速开发数据驱动应用。然而,与现代开发模式如ASP.NET MVC、ASP.NET Core和Blazor相比,Code-Behind 在可维

记录学习的过程 956

深入理解 Linux 内核---进程

进程的静态特性 进程是程序执行时的一个实例。 从内核观点看,进程的目的就是担当分配系统资源(CPU 时间、内存等)的实体。 一个进程创建时,几乎与父进程相同。父子进程共享包括程序代码(正文)的页,但有各自独立的栈和堆。 实现多线程应用程序的一个简单方式是把轻量级进程与每个线程关联起来,既可以共享一些资源,如地址空间、打开的文件等,又能使得每个线程被内核独立调度。 进程描述符 为了管理进程,内核通过...

新博客:https://aping-dev.com/ 1550

国产系统迁移实战:在银河麒麟V10上搞定Wine32位支持,让老Windows软件跑起来

本文详细介绍了在银河麒麟V10操作系统上构建Wine32位支持的全过程,帮助用户解决Windows应用迁移难题。从系统架构差异分析到Wine环境配置,再到实战应用部署与优化,提供了一套完整的解决方案,确保老Windows软件在国产系统上稳定运行。

weixin_32288959的博客 648

Linux进程内核栈与thread_info结构详解

转自:https://blog.youkuaiyun.com/gatieme/article/details/51577479 日期 内核版本 架构 作者 GitHub 优快云 2016-06-03 Linux-4.5 X86 & arm gatieme LinuxDeviceDrivers Linux进程管理与调度-之-进程的描述 ...

SweeNeil 2599

告别复杂配置!用GuidosToolbox 3.0做MSPA景观格局分析,从安装到出图全记录

本文详细介绍了如何使用GuidosToolbox 3.0进行MSPA景观格局分析,从安装配置到出图全流程。涵盖环境准备、数据预处理、参数设置、结果解读及可视化技巧,帮助用户快速掌握这一生态安全评估工具。特别针对安装教程中的常见问题提供解决方案,提升分析效率。

weixin_29248313的博客 488

通俗傻瓜式理清光栅化渲染和光线追踪渲染的原理

用通俗语言傻瓜式的讲清楚光栅渲染和光线追踪渲染的原理,方便没有相关理论基础的朋友理解入门。

qq_37366618的博客 2万+

Linux内核进程管理:进程的“内核栈”、current宏、进程描述符

目录 linux 进程内核栈 概念 thread_info 有什么用? thread_info 、内核栈、task_struct 关联 current 宏 1、arm 2、ARM64 3、x86 SYSCALL过程调用规范 x86_64进程栈切换 参考 Linux调度——神奇的current current的作用 current的通用实现方法 current在x86架构上的实现 实验示例 Linux调度——进程描述符 进程描述符:task_struct 推荐阅读 .

RToax 1718

Mac终端SSH连接腾讯云服务器保姆级教程(附秘钥管理技巧)

本文提供Mac终端SSH连接腾讯云服务器的详细教程,涵盖密钥生成、权限设置、连接优化及高级管理技巧。通过清晰的步骤和实用配置,帮助用户安全高效地管理远程服务器连接,特别适合开发者和运维人员参考。

weixin_29248313的博客 455

Linux-进程的管理与调度9(基于6.1内核)

Linux-进程的管理与调度9(基于6.1内核)---Linux进程内核栈

ListQ的博客 704

Python爬虫实战:高效爬取中国专利数据库的完整指南

在当今信息爆炸的时代,专利数据作为技术创新的重要指标,对企业研发、市场分析和学术研究都具有极高价值。中国作为全球专利申请量最大的国家,其专利数据库包含了海量的技术创新信息。本文将详细介绍如何使用Python最新技术构建一个高效、稳定的中国专利数据库爬虫系统。中国专利数据库主要由国家知识产权局(CNIPA)维护,包含发明专利、实用新型专利和外观设计专利三大类。截至2023年,中国专利数据库已收录超过400万件发明专利和300万件实用新型专利。

2201_76125261的博客 1570

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

Linux内核进程栈的两种架构

参考博文: Linux进程内核栈与thread_info结构详解--Linux进程的管理与调度(九)_OSKernelLAB(gatieme)-优快云博客_thread_info浅析Linux下的task_struct结构体_lc_29503203的博客-优快云博客_task_struct linux 进程内核栈 - 知乎 ThreadInfo结构和内核栈的两种关系_半月旋空-优快云博客 一. 内核进程栈是什么 1. 什么是进程 进程是程序的一个执行实例,进程是一种动态描述,但需要注意的.

liaojunwu的博客 589

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

【Linux内核设计与实现】第三章——进程管理01

内核栈的定义如下,内核栈在理论上被定义为一个union,该结构被称之为联合体或者共用体,该类型的特性是 “一个联合体的大小等于其内部所占空间最大的成员大小,并且所有成员都共享同一段内存”。根据这个特性,我们也就知道了一个内核栈的大小就是。#endif为什么这里笔者要强调“理论上”一词,这是由于在以往较旧版本的内核中的确是这样做的,在网络上有很多博客和文章都在介绍内核栈也是用的较老版本的内核代码,因此他们的说法也是对的。较老版本的内核中的内核栈是将与 真正的内核栈stack绑定在一起通过。

Imagine Miracle的博客 1064

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

【linux进程管理-01】task_struct、thread_info、stack之间的关系(x86)

linux进程管理 task_struct、thread_info、stack之间的关系

rentong123的博客 899

Linux Kernel:thread_info与task_struct

其他Exception level下,可以使用SP_EL0或者当前Exception level所对应的SP_ELn寄存器;具体使用SP_EL0还是SP_EL1是由PSTATE.SP决定,对应的寄存器是Spsel。若Spsel==0,那么强制使用SP_EL0,否则使用用SP_ELn。,每当切换开始时首先需要做的是保存当前进程上下文到内核栈中,准备next进程环境之后切到next进程堆栈。内核栈中的最低地址中,相较之前的版本避免了栈溢出的问题。用来描述进程的通用数据,而针对不同架构的数据则存储在。

qq_34968572的博客 1410

linux 进程及调度基础知识

介绍了进程,线程,调度的相关基础知识

vertor11的博客 1027

Linux-进程的管理与调度5(基于6.1内核)

Linux-进程的管理与调度5(基于6.1内核)---Linux下0号进程

ListQ的博客 1036

Linux 内核学习:进程管理(上)

我们的开头就是阐述Linux的进程管理。进程是Unix下的一个极其重要的(事实上,也是学习操作系统不可能绕开的话题)的一个抽象。这段话我相信任何上过操作系统课程的人都会很熟悉进程是那些正在活动的二进制程序。对于多线程应用程序随处可见的时代,进程如今也被认为是系统分配任务资源的最小单位,而真正执行的程序流则是线程(Threads),也就是那些真正在干活的程序流(所以对于只存在一个线程的进程被称为单线程的应用程序,反之是多线程的应用程序,其从事编写这样的多线程的程序活动也就被称为并发编程)。

charlie114514191的博客 1295

详解Linux内核 进程管理

能搞明白这两个问题基本进程切换就清晰了,我们慢慢捋一下,比如现在有A,B两个进程,A表示当前的进程,现在A要进行切换了,选择的下一个进程是B,那么就开始执行switch_to, 此时prev是A,next是B,执行完后CPU就会切换B的进程上下文执行,这时候switch_to之后的代码就不会被执行了,因为CPU进行加载了B的硬件上下文,寄存器中已经都是进程B的上下文信息了,就执行进程B的指令了,那switch_to后的代码啥时候会被执行呢?本篇介绍下Linux 中进程管理相关的内容,包括进程状态,切换等。

m0_74282605的博客 857

Linux内核进程与调度实现

进程管理和调度

aigourensheng的博客 792
上一篇: docker load 报 Error processing tar file unexpected EOF 解决
下一篇: Linux MPLS功能详解
废言Pro
博客等级 码龄16年 1092粉丝 142原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值