【Linux】进程地址空间

简介: Linux下的进程地址空间。

1. 进程地址空间的引出

我们在学习C语言的过程中,可能听说过这样的空间布局图:

image.png

但是他是真正的内存吗,下面我们来写一份代码验证一下:

image.png

执行一下代码:

image.png

当在子进程中将全局变量g_value修改后,并不会影响父进程中g_value的值,这是因为fork函数在创建子进程后,子进程会拷贝一份父进程的代码和数据,并创建自己的task_struct,由于进程间的独立性,子进程对于全局变量的改变并不会父进程中数据的改变。

但是,他们的地址为什么会是相同的呢?这就恰恰说明了一个问题:这个地址肯定不是物理地址,因为同一时间内的一个物理地址只能存储一个进程的数据,那么他是什么呢?这就和我们本次要讲解的虚拟地址空间有关了。


2. 进程地址空间是什么?

操作系统会给每一个进程都创建一个独立的虚拟地址空间,虚拟地址也叫线性地址,因为虚拟地址空间中的地址是线性增长的。通过页表将虚拟地址中地址和物理内存中的地址进行一个映射,所以我们平常看到的地址都是虚拟地址空间中的虚拟地址,而不是真实的物理地址。

image.png

这时候,我们就可以解释以上代码中的现象了,为什么子进程对全局变量g_value的修改不会影响父进程中g_value得值。其实是因为发生了写实拷贝。最开始的时候由于子进程的地址空间是从父进程拷贝而来的,所以二者指向的是同一块虚拟地址。同时,他们映射的也是同一块物理内存。

子进程想要修改自己的g_value,操作系统通过页表发现g_value是被父进程和子进程同时指向的一块物理内存,因为进程间是有独立性的,所以操作系统会在物理内存中寻找一块新的空间,将原来的值拷贝到新的物理内存中,然后在将其修改,同时子进程将原来g_value的虚拟地址与新的物理内存建立映射关系。这个现象被称为写时拷贝。当然因为父进程和子进程的虚拟地址是相同的,所以我们在运行结果中看到他们的地址还是相同的。

image.png

当然,进程地址空间中的地址是按照从全0到全1进行排列的,所以这个地址是连续的,因此,他被称为线性地址。在Linux中,虚拟地址、线性地址和逻辑地址都是一样的。


3. 进程地址空间的管理

操作系统中的每一个进程都会有一个进程地址空间,但是OS中会存在很多的进程,为了保证各个进程能够正常运行,所以操作系统需要将每个进程的进程地址空间进行管理。

我们知道,管理的本质是先描述,再组织。所以,操作系统会使用一种内核数据结构对进程地址空间进行管理,操作系统会创建一个mm_struct的结构体。同时为每个进程创建一个mm_struct类型的结构体对象。

下面我们来看一下Linux中mm_struct的源码:

struct mm_struct {
   
   
    struct vm_area_struct * mmap;        /* list of VMAs,指向线性区对象的链表头部 */
    struct rb_root mm_rb;                   /* 指向线性区对象的红黑树*/
    struct vm_area_struct * mmap_cache;    /* last find_vma result 指向最近找到的虚拟区间 */
#ifdef CONFIG_MMU 

/*用来在进程地址空间中搜索有效的进程地址空间的函数*/

    unsigned long (*get_unmapped_area) (struct file *filp,
                unsigned long addr, unsigned long len,
                unsigned long pgoff, unsigned long flags);
/*释放线性区的调用方法*/
 void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
#endif
    unsigned long mmap_base;        /* base of mmap area ,内存映射区的基地址*/
    unsigned long task_size;        /* size of task vm space */
    unsigned long cached_hole_size;     /* if non-zero, the largest hole below free_area_cache */
    unsigned long free_area_cache;        /* first hole of size cached_hole_size or larger */
    pgd_t * pgd;                            /* 页表目录指针*/
    atomic_t mm_users;            /* How many users with user space?,共享进程的个数 */
    atomic_t mm_count;            /* How many references to "struct mm_struct" (users count as 1),主使用计数器,采用引用计数,描述有多少指针指向当前的mm_struct */
    int map_count;                /* number of VMAs ,线性区个数*/
    struct rw_semaphore mmap_sem;
    spinlock_t page_table_lock;        /* Protects page tables and some counters,保护页表和引用计数的锁 (使用的自旋锁)*/

    struct list_head mmlist;        /* List of maybe swapped mm's.    These are globally strung
                         * together off init_mm.mmlist, and are protected
                         * by mmlist_lock
                         */


    unsigned long hiwater_rss;    /* High-watermark of RSS usage,进程拥有的最大页表数目 */
    unsigned long hiwater_vm;    /* High-water virtual memory usage ,进程线性区的最大页表数目*/

    unsigned long total_vm, locked_vm, shared_vm, exec_vm;
    unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
    unsigned long start_code, end_code, start_data, end_data;     /*维护代码区和数据区的字段*/
    unsigned long start_brk, brk, start_stack;       /*维护堆区和栈区的字段*/
    unsigned long arg_start, arg_end, env_start, env_end;  /*命令行参数的起始地址和尾地址,环境变量的起始地址和尾地址*/

    unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

    /*
     * Special counters, in some configurations protected by the
     * page_table_lock, in other configurations by being atomic.
     */
    struct mm_rss_stat rss_stat;

    struct linux_binfmt *binfmt;

    cpumask_t cpu_vm_mask;

    /* Architecture-specific MM context */
    mm_context_t context;

    /* Swap token stuff */
    /*
     * Last value of global fault stamp as seen by this process.
     * In other words, this value gives an indication of how long
     * it has been since this task got the token.
     * Look at mm/thrash.c
     */
    unsigned int faultstamp;
    unsigned int token_priority;
    unsigned int last_interval;

    unsigned long flags; /* Must use atomic bitops to access the bits */

    struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_AIO
    spinlock_t        ioctx_lock;
    struct hlist_head    ioctx_list;
#endif
#ifdef CONFIG_MM_OWNER
    /*
     * "owner" points to a task that is regarded as the canonical
     * user/owner of this mm. All of the following must be true in
     * order for it to be changed:
     *
     * current == mm->owner
     * current->mm != mm
     * new_owner->mm == mm
     * new_owner->alloc_lock is held
     */
    struct task_struct *owner;
#endif

#ifdef CONFIG_PROC_FS
    /* store ref to file /proc/<pid>/exe symlink points to */
    struct file *exe_file;
    unsigned long num_exe_file_vmas;
#endif
#ifdef CONFIG_MMU_NOTIFIER
    struct mmu_notifier_mm *mmu_notifier_mm;
#endif
};

进程地址空间被分为很多的区域,例如:堆区、栈区、未初始化数据区、初始化数据区、代码段等区域。操作系统会使用两个整形变量区间【start,end】来维护每一个区域边界的内存区域的。

例如:

image.png

所以想要对地址空间区域进行调整,只需要调整区域变量startend即可。


4. 进程地址空间存在的三个意义

进程地址空间存在的意义是什么呢?我们直接使用物理内存代替进程地址空间不行吗?

💕 防止地址随意被访问,保护物理内存及其他进程

image.png

如果直接使用物理地址,如果我们的代码写错了,就有可能导致越界非法访问或修改其他地址处的数据,有了虚拟地址和页表的出现,如果出现进程非法访问或者非法读写的操作,页表就可以直接对其进行拦截。

💕 将进程管理和内存管理进行解耦合,保证了进程独立性的特点

每一个进程都需要有独立的进程地址空间及页表,并通过页表映射到不同的物理内存中,一个进程数据的改变并不会影响另一个进程。对于父子进程也是同样的道理,父进程/子进程在修改数据时会发生写时拷贝,并不会影响彼此,所以保证了进程的独立性。

💕 可以让进程以统一的视角来看待自己的代码和数据

image.png

可执行程序被编译器编译的时候每个代码和数据在内存中已经有虚拟地址了(在磁盘上称为逻辑地址),也就是说,地址空间对于操作系统和编译器都是遵守的。所以当程序被加载到内存成为进程后,每个变量/函数都具备了物理地址。

所以我们现在有两套地址:

  • 标识物理内存中代码和数据的地址
  • 在程序内部互相跳转的时候的虚拟地址

加载完成之后,代码的各个区域的地址已经知道。进程被调度时,CPU拿到虚拟地址,经过地址空间查页表通过映射,进行访问查到物理地址往后执行。也就是CPU通过了虚拟地址——页表映射——物理地址执行。也就是在整个CPU运行过程中,CPU并没有见到物理地址,用的都是虚拟地址。

进程的代码和数据必须一直在内存中吗?

答案是不一定,举个简单的例子:比如我们平成玩的王者荣耀,一般都是十几个G,可是我们的手机内存一般都只有8个G或者16个G,这么大的内存不可能全部加载到内存中去,因为虚拟地址空间的存在,所以我们需要用多少就加载多少,执行完的代码直接扔掉,这样就可以边加载边执行。需要的时候就将代码和数据换到内存里,不需要的时候就扔掉。这样就可以保证我们控制在一个很低的内存使用量的同时还能保证将这个很大的软件跑起来。所以平常我们打游戏的时候手机会发热呢?这是因为做了比其他App更多的操作,大部分的时间我们的手机可能都在进行网络IO,内存和固态磁盘不断的进行着数据交换。所以电池了各种设备了压力都比较大。因此手机就会发热。


相关实践学习
CentOS 7迁移Anolis OS 7
龙蜥操作系统Anolis OS的体验。Anolis OS 7生态上和依赖管理上保持跟CentOS 7.x兼容,一键式迁移脚本centos2anolis.py。本文为您介绍如何通过AOMS迁移工具实现CentOS 7.x到Anolis OS 7的迁移。
相关文章
|
14天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
39 1
|
3天前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
35 13
|
10天前
|
SQL 运维 监控
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
|
17天前
|
运维 监控 Linux
Linux操作系统的守护进程与服务管理深度剖析####
本文作为一篇技术性文章,旨在深入探讨Linux操作系统中守护进程与服务管理的机制、工具及实践策略。不同于传统的摘要概述,本文将以“守护进程的生命周期”为核心线索,串联起Linux服务管理的各个方面,从守护进程的定义与特性出发,逐步深入到Systemd的工作原理、服务单元文件编写、服务状态管理以及故障排查技巧,为读者呈现一幅Linux服务管理的全景图。 ####
|
1月前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
137 4
linux进程管理万字详解!!!
|
23天前
|
缓存 算法 Linux
Linux内核的心脏:深入理解进程调度器
本文探讨了Linux操作系统中至关重要的组成部分——进程调度器。通过分析其工作原理、调度算法以及在不同场景下的表现,揭示它是如何高效管理CPU资源,确保系统响应性和公平性的。本文旨在为读者提供一个清晰的视图,了解在多任务环境下,Linux是如何智能地分配处理器时间给各个进程的。
|
1月前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
75 8
|
1月前
|
网络协议 Linux 虚拟化
如何在 Linux 系统中查看进程的详细信息?
如何在 Linux 系统中查看进程的详细信息?
61 1
|
1月前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?
|
1月前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
70 4
下一篇
DataWorks