Linux进程地址空间

简介: Linux进程地址空间

零、前言


本章主要讲解学习进程地址空间的知识


一、程序内存空间


在学习C/C++中我们知道了程序内存的空间开辟以及内存分区的基本概念


  • 示图:


各分区作用:

内核空间:用户代码无法读写


命令行参数环境变量:储存命令行参数环境变量


栈区:存放运行函数而分配的局部变量、函数参数、返回数据、 返回地址等,栈区地址向下生长


共享区:储存文件映射,匿名映射,动态库


堆区:存放动态分配的变量,堆区地址向上生长


数据段(初始化数据/未初始化数据区):存放全局变量、静态数据


代码区:存放函数体(类成员函数和全局函数)的二进制代码


代码验证示例:


#include<stdio.h>
#include<stdlib.h>
int g_unval;
int g_val=1;
int main(int argc,char* argv[],char* env[])//命令行参数以及环境变量
{
    printf("code addr:%p\n",main);//代码区
    char* str="hello world";
    printf("read only addr:%p\n",str);//只读常量区:str是常量字符串的地址
    printf("init addr:%p\n",&g_val);//初始化数据区
    printf("uninit addr:%p\n",&g_unval);//未初始化数据区
    int* p1=(int*)malloc(10);
    int* p2=(int*)malloc(10);
    printf("heap addr:%p\n",p1);//堆区
    printf("heap addr:%p\n",p2);//堆区向上生长
    printf("stack addr:%p\n",&str);//栈区
    printf("stack addr:%p\n",&p1);//栈区向下生长
    for(int i=0;argv[i];i++)
        printf("args addr:%p\n",argv[i]);//命令行参数区 
    for(int i=0;i<2;i++)
        printf("env addr:%p\n",env[i]);//环境变量区
    return 0;
}


二、进程地址空间


1、引入及概念


对于上述的程序地址空间,其实它的真实面貌为进程地址空间,对于进程地址空间本质上来说是一个虚拟地址空间,并非真实的物理空间


  • 示例:


#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
    int i=10;
    pid_t id=fork();//创建子进程
    if(id<0)
    {
        perror("fork fail\n");
        return 1;
    }
    else if(id==0)
    {
        //child
        int cnt=0;
        while(1)
        {
            printf("I am child:  i:%3d &i:%p pid:%d\n",i,&i,getpid());
            sleep(1);
            if(cnt==3)//修改i的值
            {
                i=100;
            }
            cnt++;
        }
    }
    else
    {
        //father
        while(1)
        {
            printf("I am father: i:%3d &i:%p pid:%d\n",i,&i,getpid());
            sleep(1);
        }    
    }
    return 0;
}


分析:

我们知道,父进程创建子进程时,子进程以父进程为模板构建进程,代码父子共享,数据各有一份(谁进行写入谁发生拷贝)


而我们发现子进程数据发生修改时,子进程数据的地址和父进程的地址一样,没有发生改变


对于变量内容不一样,但地址值是一样的,说明该地址绝对不是物理地址,因为是物理地址根本不会有这种事发生


2、进程地址空间


概念:

在Linux地址下,这种地址叫做 虚拟地址,我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理,OS必须负责将 虚拟地址 转化成 物理地址


进程地址空间本质是进程看待内存的方式,抽象出来的一个概念,对于每个进程来说,系统会给他们创建对应的PCB进程块结构体,同时也相应的分配了对应的mm_struct进程地址空间(PCB中储存了该进程对应的进程地址空间的地址),也就是每个进程都认为自己独占内存资源


对于进程来说,进程控制块以及进程地址空间以及相应的资源,随进程的创建而创建,随进程的退出而回收


进程地址空间的内容:

进程地址空间是由0x00000000到0xffffffff的线性地址空间,按照刻度被划分为各个区域,例如代码区、堆区、栈区等


在结构体mm_struct当中,便记录了各个边界刻度(对于堆向上增长以及栈向下增长实际就是改变mm_struct当中堆和栈的边界刻度)


示图:


注:在结构体mm_struct中各个刻度之间的每一个刻度都代表一个虚拟地址,这些虚拟地址通过页表映射与物理内存建立联系


程序执行流程:

程序运行,进程被加载到CPU上,系统在内核为进程创建PCB记录进程属性,分配进程空间地址,由页表构建虚拟地址与物理地址的映射关系,程序查找或者修改数据会通过PCB找到对应的进程地址空间,再由进程地址空间上的虚拟地址由页表找到物理空间上分配的数据


示图:


对于父子进程变量地址相同数据不同:

父进程创建子进程时,子进程以父进程为模板构建进程,代码数据父子共享,当子进程进行修改数据时,由页表发现该数据是父子进程共享的,所以系统会找到另一个物理空间进行拷贝数据,拷贝数据后再修改数据,达到数据各有一份互不干扰的目的


注:这种在需要进行数据修改时再进行拷贝的技术称为写时拷贝


示图:


3、相关问题


为什么数据要进行写时拷贝

进程需要保证独立性,多进程运行,需要独享各种资源,多进程运行期间互不干扰,数据写实拷贝让子进程的修改不影响到父进程


为什么不在创建子进程的时候就进行数据的拷贝

子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间


如果fork函数在子进程创建的同时即创建对应的数据结构还要拷贝数据的话,会降低fork的效率


fork就是在向系统获取资源,如果再拷贝的话,即获取更多的资源,容易造成fork失败


代码会不会进行写时拷贝

90%的情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝


为什么要有进程地址空间

保护物理内存,不让程序直接进行访问物理地址,方便进行合法性校验,控制以及管理了访问的权限

如果可以直接访问,那么看到的地址就是物理地址,对于野指针,越界访问等问题则不能进行很好的控制,不能保证程序的独立性;当通过物理地址暴露,恶意程序通过物理地址进行读取或者修改数据,无法保证信息和数据安全;控制以及管理了访问的权限,以常量区不能的常属性来说,当常量定义出来的时候不就是修改数据了么,但是再次修改时,通过页表访问时,页表发现是常量区数据则拒绝修改的访问,以此保护了数据的常属性


将内存管理与进程管理进行解耦

如果直接使用物理地址,那么进程一创建就需要立即将数据写到物理内存中,当进程退出就需要将数据立即释放,也就是说内存的管理需要特别关注进程的状态,这之间具有强相关性(耦合度高);具有进程地址空间后,进程管理只需管理PCB以及进程地址空间,而内存管理只需管理物理地址空间,也就是内存管理只需要通过智能指针知道内存区域那些是有效的哪些是无效的就能管理好内存,实现了进程管理与内存管理的解耦


让程序以同样的方式看待代码和数据

可执行程序实际上也被分为了各个区域,例如初始化区、未初始化区等(便于程序编译时进行查找和链接)。当该可执行程序运行起来时,操作系统则将对应的数据加载到对应内存当中即可,同时分区有利于执行的效率,大大提高了操作系统的工作效率。


相关文章
|
13天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
44 4
linux进程管理万字详解!!!
|
4天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
41 8
|
13天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
46 4
|
13天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
15天前
|
消息中间件 存储 Linux
|
21天前
|
运维 Linux
Linux查找占用的端口,并杀死进程的简单方法
通过上述步骤和命令,您能够迅速识别并根据实际情况管理Linux系统中占用特定端口的进程。为了获得更全面的服务器管理技巧和解决方案,提供了丰富的资源和专业服务,是您提升运维技能的理想选择。
23 1
|
1月前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
21 1
|
1月前
|
消息中间件 Linux API
Linux c/c++之IPC进程间通信
这篇文章详细介绍了Linux下C/C++进程间通信(IPC)的三种主要技术:共享内存、消息队列和信号量,包括它们的编程模型、API函数原型、优势与缺点,并通过示例代码展示了它们的创建、使用和管理方法。
30 0
Linux c/c++之IPC进程间通信
|
1月前
|
Linux C++
Linux c/c++进程间通信(1)
这篇文章介绍了Linux下C/C++进程间通信的几种方式,包括普通文件、文件映射虚拟内存、管道通信(FIFO),并提供了示例代码和标准输入输出设备的应用。
26 0
Linux c/c++进程间通信(1)
|
1月前
|
Linux C++
Linux c/c++进程之僵尸进程和守护进程
这篇文章介绍了Linux系统中僵尸进程和守护进程的概念、产生原因、解决方法以及如何创建守护进程。
20 0