【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,内存和固态磁盘不断的进行着数据交换。所以电池了各种设备了压力都比较大。因此手机就会发热。


相关文章
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
394 67
|
11月前
|
Web App开发 Linux 程序员
获取和理解Linux进程以及其PID的基础知识。
总的来说,理解Linux进程及其PID需要我们明白,进程就如同汽车,负责执行任务,而PID则是独特的车牌号,为我们提供了管理的便利。知道这个,我们就可以更好地理解和操作Linux系统,甚至通过对进程的有效管理,让系统运行得更加顺畅。
304 16
|
11月前
|
Unix Linux
对于Linux的进程概念以及进程状态的理解和解析
现在,我们已经了解了Linux进程的基础知识和进程状态的理解了。这就像我们理解了城市中行人的行走和行为模式!希望这个形象的例子能帮助我们更好地理解这个重要的概念,并在实际应用中发挥作用。
213 20
|
JavaScript Linux Python
在Linux服务器中遇到的立即重启后的绑定错误:地址已被使用问题解决
总的来说,解决"地址已被使用"的问题需要理解Linux的网络资源管理机制,选择合适的套接字选项,以及合适的时间点进行服务重启。以上就是对“立即重启后的绑定错误:地址已被使用问题”的全面解答。希望可以帮你解决问题。
577 20
|
10月前
|
监控 Shell Linux
Linux进程控制(详细讲解)
进程等待是系统通过调用特定的接口(如waitwaitpid)来实现的。来进行对子进程状态检测与回收的功能。
227 0
|
10月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
297 0
|
10月前
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
178 0
|
10月前
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
240 0
|
存储 Linux 调度
【Linux】进程概念和进程状态
本文详细介绍了Linux系统中进程的核心概念与管理机制。从进程的定义出发,阐述了其作为操作系统资源管理的基本单位的重要性,并深入解析了task_struct结构体的内容及其在进程管理中的作用。同时,文章讲解了进程的基本操作(如获取PID、查看进程信息等)、父进程与子进程的关系(重点分析fork函数)、以及进程的三种主要状态(运行、阻塞、挂起)。此外,还探讨了Linux特有的进程状态表示和孤儿进程的处理方式。通过学习这些内容,读者可以更好地理解Linux进程的运行原理并优化系统性能。
469 4
|
Linux Shell
Linux 进程前台后台切换与作业控制
进程前台/后台切换及作业控制简介: 在 Shell 中,启动的程序默认为前台进程,会占用终端直到执行完毕。例如,执行 `./shella.sh` 时,终端会被占用。为避免不便,可将命令放到后台运行,如 `./shella.sh &`,此时终端命令行立即返回,可继续输入其他命令。 常用作业控制命令: - `fg %1`:将后台作业切换到前台。 - `Ctrl + Z`:暂停前台作业并放到后台。 - `bg %1`:让暂停的后台作业继续执行。 - `kill %1`:终止后台作业。 优先级调整:
1237 5

热门文章

最新文章