Linux之进程管理第一讲(可视化理解+源码分析)——进程PCB以及虚拟地址空间初探,大致理解可执行程序执行的底层原理

   综述

    本篇文章旨在加深Linux操作系统中的进程管理模块的理解。进程管理模块是一个及其宏大的管理体系,几篇小小的博客是不可能讲述完整的。所以,我更希望用一种独特的方式,用自己的逻辑去大致讲述其框架以及部分细节,希望能给大家以启示。

    本篇,我们旨在对进程有一个初步的理解。理解进程管理的基本框架。至于进程的细节属性,以及进程控制和编码部分,我们在后续博客中详谈。

    讲述思路如下:

1.浅谈OS的管理思想

2.明确进程在OS中承担的角色

3.初探进程地址空间——加深进程的理解

基于源码以及逻辑图,前三步,我们逐层加深对Linux中进程的理解;后三步,我们借助前三步的知识铺垫,详谈进程管理中的相关概念,对进程管理有一个全面的认识。

当我们学习完所有进程管理知识,我们将用以上知识,自主实现简易的shell,旨在加深对进程控制的理解,并将所学知识加以运用。

浅谈OS的管理思想

    我们知道,OS中无时无刻不存在大量正在运行的程序。要想这大量的程序有序运行,就必须对其进行管理。在现实生活中,每个人都是被不同机构管理起来的,机构的管理者,并不与我们会面,我们甚至没见过哪些上层管理者。那他们是如何实现对我们的管理呢?很简单,拿到我们的信息。在学校中,管理者通过教务系统拿到我们的姓名、学号、成绩等等信息就能对我们进行合理的分配与管理。OS身为管理者,亦是如此。想要管理大量的程序,就需要拿到这些程序的属性信息——即需要对程序进行描述!那么,在编码层,描述事物,就用到结构体来封装其各种属性。只有信息足够吗?答案是不够。学校拿到了我们的信息后,还要进行分班、分级等的组织行为,以便进行有序管理。OS也是一样的,描述了大量程序后,还要讲其组织起来!组织的具体实现方式,我们在之后详谈。

    综上,OS的管理思想,我们可以用一句话概括:先描述,在组织!这句话会贯穿我们Linux的学习!

进程在OS中扮演的角色

什么是进程

说了半天,什么是进程?其实,我们上面所说的正在运行的程序,就是进程。或者说,一个已经加载到内存中的程序,叫做进程。

理解OS管理进程的一般思路

进程在OS中,是典型的被管理者。OS需要将多个进程管理起来,即先描述再组织。

任何一个进程,在加载到内存,形成进程时,OS要先创建描述进程属性的结构体对象——PCB(Process Control Block,进程控制块,即进程属性的集合,本质是struct结构体)

属性包括:进程编号、进程状态、优先级......

那么,OS具体要干什么?
1.创建PCB结构体对象——教务信息
2. 进程数据加载到内存——人来学校
两件事和起来形成一个进程

所以,我们可以这样理解:进程 = 内核PCB数据结构对象 + 自己的代码和数据。

下面,我们做逻辑图进行可视化理解:

具体Linux的实现方式

谈了半天PCB,那么具体到Linux操作系统,PCB是如何实现的,或者说如何定义的?在Linux中,PCB被实现成task_struct结构体。我们大体看一下源码中的部分重要字段:

struct task_struct {  
    volatile long state;            /* -1 unrunnable, 0 runnable, >0 stopped */  
    unsigned int flags;             /* per process flags, defined below */  
    int pid;  
    int tgid;  
  
    /* 进程调度相关信息 */  
    struct sched_entity se;  
    struct sched_rt_entity rt;  
    struct task_struct *real_parent; /* real parent process */  
    struct task_struct *parent;      /* recipient of SIGCHLD, wait4() reports */  
    struct list_head children;       /* list of my children */  
    struct list_head sibling;        /* linkage in my parent's children list */  
    struct task_group *sched_task_group;  
  
    /* CPU-specific state of this task */  
    struct thread_struct thread;  
    unsigned int cpu;                /* actual cpu */  
    unsigned int wake_cpu;           /* cpu where last woken up */  
  
    unsigned long atomic_flags;  
  
    /* 文件系统信息 */  
    struct fs_struct *fs;  
    struct files_struct *files;  
  
    /* 信号处理 */  
    struct signal_struct *signal;  
    struct sighand_struct *sighand;  
  
    /* 进程间通信 */  
    struct pid *pid_links[PIDTYPE_MAX];  
    struct pid_namespace *pid_ns_for_children;  
  
    /* 虚拟内存管理 */  
    struct mm_struct *mm, *active_mm;  
  
    /* 时间统计 */  
    struct timespec start_time;  
    struct timespec real_start_time;  
    unsigned long min_flt, maj_flt;  
  
    /* 定时器 */  
    struct hrtimer real_timer;  
    struct pid *leader_pid;  
  
    /* 进程间同步,如互斥锁等 */  
    struct robust_list_head __user *robust_list;  
    struct list_head pi_state_list;  
    struct futex_pi_state *pi_state_cache;  
  
    /* 进程名字 */  
    char comm[TASK_COMM_LEN];  
  
    /* 安全相关的属性 */  
    u32 security;  
    void *security_cookie;  
  
    /* ... 更多字段 ... */  
};

我们目前是处于理解进程管理框架的阶段,所以每部很多的成员我们可能不懂。但我们可以得知的是,task_struct即进程PCB确确实实包含了进程的诸多属性。不必着急,其中的成员在后文我们大部分都会谈到。

    值得注意的是,这些字段中包含如下字段:

struct task_struct *real_parent; /* real parent process */  
struct task_struct *parent;      /* recipient of SIGCHLD, wait4() reports */  
struct list_head children;       /* list of my children */  
struct list_head sibling;  

task_struct中,又包含本身类型的指针。且list_head,是链表头。根据这些字段,我们可以猜想,在Linux中,进程PCB是用链表组织起来的!且事实就是如此!至于其中parent,child的父子关系,我们在后文详细说明。

一个一个的PCB相互链接起来,成为一个链表。那么,通过先描述再组织,OS对进程的管理,就变成的对链表进行增删查改操作!!!

进一步的,我们画图进行可视化理解:

初探进程地址空间——进一步加深理解

有了前面的知识,我们已经能理解进程在Linux中的大致是如何被管理的了。但上述的阐述,侧重点放在了进程管理的宏观把握,以及对进程和进程PCB对理解。接下来谈到的进程地址空间,我们将进一步谈谈进程代码和数据是如何与进程PCB联系,并被OS管理的。

地址空间

相信在学习编程语言时,我们都接触过地址空间的分布。

但是,其实我们并不理解它。这个地址空间是进程的,那它是如何与进程联系的?进程地址空间与我们的内存又是什么关系?

这里,必须要提出的是,这里的进程地址空间,其实是虚拟的!也就是说,我们在编码时,语句的地址,都是虚拟的!并不是我们真正的内存地址!

那它又是如何与进程联系的?细心的伙计在看task_struct字段时,应该已经发现蹊跷了!

 /* 虚拟内存管理 */  
 struct mm_struct *mm, *active_mm;

这个指针字段,其实就是只指向我们的进程地址空间!在这里,它被称为虚拟内存。

我知道,现在大家会有很对疑问。为什么要搞一个假的地址空间?如果是假的,那它是如何与真多内存联系,以达到访问加载到内存的数据的呢?不急,我们慢慢道来。

可执行程序的执行原理

已经知道了进程地址空间,下面我们通过讲解用C/C++或其他语言编写的程序执行的内核级原理,来彻底打消我们目前的所有疑问。

虚拟地址(逻辑地址)

在我们编码完成后,编译器已经以平坦模式(即将使得整个4GB或更大的地址空间被视为单一的线性地址空间)从0x00000...到0xFFFFF...给我们的每一行代码编好了地址,此时的地址是逻辑地址,也就是虚拟地址

之后,将整个程序的各个部分(如代码段、数据段、堆栈等)按照逻辑顺序编排在一个连续的虚拟地址空间中。这样的编排,为后续虚拟地址与物理地址的映射做了准备。

物理地址

当我们开始运行程序后,可执行程序的部分加载到内存(部分加载,内存一般按需申请释放,不会一次全部加载进内存),也就使得程序天然的具备了物理地址,也就是在内存中的真正的绝对地址。所以,此时的代码数据本身同时具备物理地址和虚拟地址!!

随后shell向OS发送执行可执行程序的请求,OS创建进程PCB以及进程的虚拟地址空间。

到这一步,我们作图进行梳理和理解:

那么问题来了,所谓进程地址空间,也就是虚拟地址空间,如何与内存联系??你的代码数据不都在内存中吗????

这里,我们引入页表的概念。就是页表完成了从虚拟地址到物理地址到映射。因为加载到内存的部分代码和数据本身具备双重地址,页边将虚拟地址填到左边的表中,物理地址填到右边的表中。我们暂时这样理解,页表的具体细节结构,我们之后会谈,不是本节的重点。

执行程序

在程序真正开始执行之前,操作系统会设置CPU的一系列寄存器,以准备执行新的程序。特别是,指令指针寄存器(Instruction Pointer Register, IP或PC)会被设置为程序的入口点地址(Entry Point Address, EPA)。此处的入口点地址,是程序本身编好的虚拟地址。CPU拿着入口地址,到进程地址空间中执行。此时,依靠页表完成虚拟地址到物理地址到映射,找到要执行的代码并加以执行。之后配合其他寄存器,存储、识别下一次要执行的代码的地址,我们的程序也就一行一行的被执行了。

此处会有一个衍生问题:既然是部分加载,也就一定存在未加载的代码数据。如果执行到了未加载进内存的数据,会发生什么?

页表识别到无法完成映射,会发生缺页中断(Page Fault)。当缺页中断发生时,操作系统会执行以下步骤来处理:

  1. 保存当前状态:操作系统会保存当前的状态,包括寄存器的值、程序计数器等,以便在处理完缺页中断后能够恢复程序的执行。
  2. 查找空闲内存块:操作系统会检查主存分配表,找一个空闲的内存块。如果没有空闲块,则可能通过页面调度算法来解决。
  3. 装入内存:将从磁盘上读出的信息装入找到的主存块中。
  4. 修改页表:在页表中修改相应表目,表示该页已在主存中。
  5. 恢复执行:重新执行被中断的指令,程序继续执行。

这样,就完成的对未加载的内容进行分配内存,建立映射等操作,即可继续执行下面的代码。

总结

本篇主要讲解的进程的概念,进程PCB以及进程地址空间,初步对进程有了框架性的理解。并通过对可执行程序的执行原理的讲解,我们深入理解了的进程地址空间是如何与内存建立联系的。

本节的讲解,为后续讲解进程属性内容做了铺垫。有了本节的知识储备,我们能更好的理解以后枯燥的知识。

下一章,我们依据PCB的源码,谈谈进程的具体属性。

  • 14
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值