14 | 进程数据结构(下):项目多了就需要项目管理系统

2021/4/12 7:29:58

本文主要是介绍14 | 进程数据结构(下):项目多了就需要项目管理系统,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

本文仅作为学习记录,非商业用途,侵删,如需转载需作者同意。

一个任务执行的方方面面,都可以被task_struct 很好的管理起来。
执行过程中,一旦调用到系统调用,就需要进入内核继续执行。

struct thread_info    thread_info;
void  *stack;

1、用户态函数栈

用户态中,程序的执行往往是一个函数调用另一个函数,函数调用通过栈来执行的,

函数调用就是指令跳转,重点是参数和返回地址怎么传递过去。

函数调用过程:
A 调用 B,调用C,调用D,然后返回C,返回B,返回A。
后进先出,栈也是后进先出

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

在这里插入图片描述

32位操作系统上:

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

64位操作系统上:

寄存器数目比较多
rax:保存函数调用的返回结果
rsp:栈顶指针寄存器
堆栈的pop和push 操作会自动调整rsp
rbp:栈基指针寄存器,指向栈帧的起始位置

rdi、rsi、rdx、rcx、r8、r9 寄存器:用于存储函数调用时的6个参数
比6个多的时候,还是需要放到栈里。

前面6个参数需要寻址的时候,因为在寄存器里是没有地址的,因而还是会被放到栈里。这个操作是 被调用函数做的。

在这里插入图片描述

以上的栈操作,都是在进程的内存空间里进行的。

2、内核态函数栈

成员变量 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 ,已经指向内核栈的栈顶,在内核态里的调用都有和用户态相似的过程。

3、通过 task_struct 找内核栈

如果有一个当前在某个 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)

在这里插入图片描述



这篇关于14 | 进程数据结构(下):项目多了就需要项目管理系统的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程