IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> linux内核架构解析-进程管理 -> 正文阅读

[系统运维]linux内核架构解析-进程管理

打开电脑的任务管理器:最直观的进程
在这里插入图片描述
在这里插入图片描述

操作系统的分层结构
在这里插入图片描述

首先提出三个问题

  1. 进程的创建过程?
  2. 进程的执行过程??
  3. 进程的销毁过程???

32位系统的虚拟地址空间
在这里插入图片描述
上图就是进程的虚拟地址空间,它并不是内存!!而是一个结构体(mm_struct)。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
进程概述
可执行程序:是静态的,就是个存放在磁盘里的 可执行文件,就是一系列的指令集合。
进程(Process):是动态的,是程序的一次执行过程,同一个程序多次执行会对应 多个进程

在这里插入图片描述
虚拟地址与物理地址的关系
在这里插入图片描述

在这里插入图片描述

对于每一个可执行程序,其在经历预处理、编译、汇编之后,都要经过链接器将其链接成一个单一的可执行文件。
当执行这个可执行程序时,

#include <stdio.h>
#include <stdlib.h>

int bss_var;
int data_var0 = 1;

int main()
{
printf("Test location:\n");
printf("\tAddress of main(Code Segment):%p\n",main);
printf("_____________________________________\n");
int stack_var0 = 2;
printf("Stack location:\n");
printf("\tInitial end of stack:%p\n",&stack_var0);
int stack_var1 = 3;
printf("\tNew end of stack:%p\n",&stack_var1);

printf("_____________________________________\n");

printf("Data location:\n");
printf("\tAddress of data_var(Data Segment):%p\n",&data_var0);
static int data_var1 = 4;
printf("\tNew end of data_var(Data Segment):%p\n",&data_var1);

printf("_____________________________________\n");
printf("BSS location:\n");
printf("\tAddress of bss_var:%p\n",&bss_var);

printf("_____________________________________\n");
printf("Heap location:\n");
char *p = (char *)malloc(10);
printf("\tAddress of head_var:%p\n",p);
return 0;
}

在这里插入图片描述
这个地址是虚拟地址,其实在我们编译的时候,这些地址就已经确定了,不管执行多少次,地址都不会改变

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

请添加图片描述
在这里插入图片描述

进一步解析
请添加图片描述在这里插入图片描述

Linux提供了三个系统调用用于创建进程,分别是fork,vfork,clone:
在这里插入图片描述
do_fork()函数 部分代码

long do_fork(unsigned long clone_flags,
          unsigned long stack_start,
          unsigned long stack_size,
          int __user *parent_tidptr,
          int __user *child_tidptr)
{
    struct task_struct *p;
  
    p = copy_process(clone_flags, stack_start, stack_size,
             child_tidptr, NULL, trace); //得到一个进程描述符, 内核栈, thread_info与父进程一样的子进程描述符

}

copy_process()函数 部分代码

static struct task_struct *copy_process(unsigned long clone_flags,
                    unsigned long stack_start,
                    unsigned long stack_size,
                    int __user *child_tidptr,
                    struct pid *pid,
                    int trace)
{
    int retval;
    struct task_struct *p;

    p = dup_task_struct(current); //新创建一个进程描符, 线程描述符, 内核栈
    //此时父子进程的内容完全一样, 接下来就是重新设置一些子进程的值了
}

dup_task_struct()函数 部分代码

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
    struct task_struct *tsk;
    struct thread_info *ti;
    unsigned long *stackend;
    int node = tsk_fork_get_node(orig); 
    int err;

    tsk = alloc_task_struct_node(node); //申请进程描述符
   
    ti = alloc_thread_info_node(tsk, node); // 申请线程描述符
}

在这里插入图片描述

进程关系

  • parent:指向父进程
  • children:所有子进程的组成的链表的链表头
  • sibling:兄弟链表,又相当于父进程的 children链表中的一个节点

在这里插入图片描述
程序的运行需要使用到栈,所以不管进程是在内核态运行还是在用户态运行都需要用到栈
Linux将进程地址空间分为内核空间和用户空间,它们之间是不能直接访问的,而一个进程某些时候可能在用户态运行,某些时候可能在内核态运行(发生系统调用时),所以一个进程既需要用户栈又需要内核栈

在 task_struct 中,有一个变量指向该进程的内核栈,如下

struct task_struct {
    ...
    void *stack;
	...	
};

内核栈的大小在内核中的定义如下

#define THREAD_SIZE_ORDER  1
#define THREAD_SIZE    (PAGE_SIZE << THREAD_SIZE_ORDER)

一个 PAGE_SIZE 是4K,左移一位就是乘以2,所以 THREAD_SIZE 就是8K,所以大体结构如下
在这里插入图片描述
接下来我们看这8K的空间的结构分布
在这段空间的最底部,存放着一个 struct thread_info 结构体,何以证明呢?

在Linux中有一个 union thread_union 共用体,其定义如下

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

Linux中发生系统调用时,会从用户态变成内核态,然后执行内核代码,当内核代码执行完之后,又会回到用户态执行用户代码

在进程从用户态变成内核态的时候,内核需要将用户态运行时寄存器的值保存下来,然后修改寄存器,当内核代码执行完之后,又将寄存器的值恢复,这些寄存器的值保存在哪里呢?

在内核栈的最高端,存放着一个 pt_regs 结构,这个结构包含了相关寄存器的定义,用户态寄存器的值就保存在这里,对于X86 32 位其定义如下

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;
};

此外剩余的空间才是用作函数栈,栈是向下生长的,所以进程的内核栈就变成下面这个样子
在这里插入图片描述
接下来看 thread_info 的定义,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 */
};

在这里插入图片描述
Linux内核中可以通过 current 宏来获取当前正在运行的进程

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

current 通过 get_current(),进而调用 current_thread_info()->task

current_thread_info 的定义

static inline struct thread_info *current_thread_info(void)
{
	return (struct thread_info *)
		(current_stack_pointer & ~(THREAD_SIZE - 1));
}

current_stack_pointer 表示当前栈顶寄存器的值,对于 X86 就是 esp,在内核态的时候,current_stack_pointer 表示内核栈中的某一个位置

THREAD_SIZE 我们上面说过是8K,THREAD_SIZE - 1 就是8K剩下的所有位,如下
在这里插入图片描述

(current_stack_pointer & ~(THREAD_SIZE - 1)

意思就是将 current_stack_pointer 的低12位清空

我们从这个 current_thread_info 函数可以看出,通过这个操作就可以获得 thread_info 对象,这是为什么呢?

这是因为,内核栈在申请的时候,总是 8K 对齐的,也就是说地址的低12位肯定为0

当进程在内核态运行的时候,栈顶指针总是指向这块申请的内核栈中的某一个区域,内核栈的大小最大也就8K,所以将当前栈顶指针的低12位置零就可以得到内核栈的基址

而 thread_info 存在于内核栈的栈底处,所以也就获取到了该进程对应的 thread_info 结构

在这里插入图片描述
thread_info 结构中有一个 task_struct* 成员,指向该进程的 task_struct,所以也就可以获得该进程的 task_struct 结构

创造进程资源的copy

接下来会调用许多形如copy_xyz的例程,以便复制或共享特定的内核子系统的资源。task_struct包含了一些指针,指向具体数据结构的实例,描述了可共享或可复制的资源。由于子进程的task_struct是从父进程的task_struct精确复制而来,因此相关的指针最初都指向同样的资源,或者说同样的具体资源实例,如图2-10所示

在这里插入图片描述
假定我们有两个资源:res_abc和res_def。最初父子进程的task_struct中的对应指针都指向了资源的同一个实例,即内存中特定的数据结构。

如果CLONE_ABC置位,则两个进程会共享res_abc。此外,为防止与资源实例关联的内存空间释放过快,还需要对实例的引用计数器加1,只有进程不再使用内存时,才能释放。如果父进程或子进程修改了共享资源,则变化在两个进程中都可以看到。

如果CLONE_ABC没有置位,接下来会为子进程创建res_abc的一份副本,新副本的资源计数器初始化为1。 因此在这种情况下,如果父进程或子进程修改了资源,变化不会传播到另一个进程。

退出进程

进程必须用exit系统调用终止。这使得内核有机会将该进程使用的资源释放回系统
程序员可以显式调用exit。但编译器会在main函数(或特定语言使用的main函数)末尾自动添加相应的调用。

该调用的入口点是sys_exit函数,需要一个错误码作为其参数,以便退出进程。其定义是体系结构无关的,见
kernel/exit.c。它很快将工作委托给do_exit。
如果引用计数器归0而没有进程再使用对应的结构,那么将相应的内存区域返还给内存管理模块

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2021-08-25 12:36:42  更:2021-08-25 12:37:09 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/29 9:07:44-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计