首页 > 系统相关 >Linux平台下的进程地址空间

Linux平台下的进程地址空间

时间:2023-11-10 16:05:53浏览次数:30  
标签:int 空间 地址 页表 Linux 进程 环境变量

“地址空间”

在之前讨论C++内存管理,以及平常写C/C++程序时,有如下的存储空间布局:

Linux平台下的进程地址空间_页表

虽然不是所有的实例都按照上图所示的分区排布,但这是一种最典型的做法,足以说明问题。这个示意图与在C++内存管理中所示的相似,但还是需要说明一下:(方便起见,暂时将这个空间称为程序的“地址空间”)

  • 在32位机器下,地址空间的范围是[0, 232),这是由地址总线排列组合的范围决定的。
  • 地址空间被大体划分为两个部分:内核空间和用户空间。内核空间存放了操作系统内核相关的数据和信息,用户无法访问内核空间。
  • 栈。临时变量,以及函数调用时所需要保存的信息都存放在此段。栈是向下增长的,栈具有FILO的性质。栈的空间一般较小。
  • 堆。通常在此段中进行动态内存分配。堆的可用空间一般较大。
  • 正文代码。代码部分是只读的,不允许用户修改。
  • 未初始化数据段。此段保存未初始化的全局数据与静态数据。与初始化的数据不同的是,未初始化数据段内容不存放在磁盘中,这是因为其中的数据会被内核同意设置为0。需要存放在磁盘中的只有代码段和初始化数据段。
  • 对于命令行参数和环境变量区域,在下文进行详细讨论。

了解了上述的地址空间后,进行代码测试如下:

/*
*代码 1.1
*平台:centos7.6 x86_64
*/
#include <stdio.h>
#include <stdlib.h>

int global_init_var1 = 10;
int global_init_var2 = 20;
int global_init_var3 = 30;

int global_uninit_var1;
int global_uninit_var2;

int main()
{
  int st_var1 = 10;
  int st_var2 = 20;
  int st_var3 = 20;
  
  int* hp_var1 = (int*)malloc(sizeof(int));
  int* hp_var2 = (int*)malloc(sizeof(int));
  int* hp_var3 = (int*)malloc(sizeof(int));

  printf(" \
      global_init_var1: %p\n \
      global_init_var2: %p\n \
      global_init_var3: %p\n \
      global_uninit_var1: %p\n \
      global_uninit_var2: %p\n \
      st_var1: %p\n \
      st_var2: %p\n \
      st_var3: %p\n \
      hp_var1: %p\n \
      hp_var2: %p\n \
      hp_var3: %p\n",
      &global_init_var1, &global_init_var2, &global_init_var3, &global_uninit_var1, &global_uninit_var2, 
      &st_var1, &st_var2, &st_var3,
      hp_var1, hp_var2, hp_var3);

  return 0;
}

运行结果为:

Linux平台下的进程地址空间_页表_02

将输出的地址大致映射到地址空间:

Linux平台下的进程地址空间_环境变量_03

虽然具体的输出情况在不同的平台下会有所不同,但是通过观察现象,可以验证我们所做的说明是正确的。

虽然已经了解了“地址空间”空间划分情况,但是这个”地址空间”到底是什么,我们并不清楚。编写下面的代码并运行,观察现象:

/*
*代码 1.2
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

int main()
{
  int var = 10;
  printf("father: the value of var:%d the address of var:%p\n", var, &var);
  pid_t id = fork(); 
  if(id < 0) { perror("fork err"); exit(1); }
  else if(id == 0)
  {
    var = 20;
    printf("child: the value of var:%d the address of var:%p\n", var, &var);
  }
  else 
  {
    int status = 0;
    waitpid(id, &status, 0);
  }
  return 0;
}

运行结果为:

Linux平台下的进程地址空间_进程的独立性_04

我们看到了这样的现象:

  • 父进程和子进程的var变量的值不相等。这很好理解,子进程被创建后,与父进程共用代码部分和数据部分,直到子进程对变量进行修改,此时进行写时拷贝,为子进程另外创建一块空间单独存储 var 变量,并对 var 进行修改。
  • 父进程和子进程的var变量的地址相同。令人费解!毋庸置疑的是,子进程已经对父进程的变量 var 进行了写时拷贝,即此时父进程的 var 变量与子进程的 var 变量分别占据不同的空间,但是输出的二者的地址却相同,唯一的解释是:程序所输出的地址,是一个“虚拟”的地址。也就是说,我们之前讨论的“地址空间”,其中的地址其实并不是真实的物理地址,因为如果是物理地址,绝对不可能存在两个不同的值占据同一个位置的情况。我们称这个虚拟的地址为线性地址(linear address)

在C/C++语言中,我们所看到的地址全部都是线性地址(虚拟地址)。在现代操作系统中,用户无法看到真实的物理地址,物理地址由操作系统进行统一管理,同时,操作系统负责将虚拟地址转化为物理地址。

进程地址空间

之前所称“程序的地址空间”,准确应该称为进程地址空间(process address space)。进程地址空间是从某个最小值的存储位置(通常是0)到某个最大值的存储位置的列表。在进程地址空间中,进程可以进行读写。进程地址空间中存放有可执行程序的代码、数据以及程序的堆栈。在本质上,进程地址空间是地址总线排列组合形成的地址范围(32位机器下为[0, 232)),是进程可以引用的地址的集合。同时我们知道,这些地址都是虚拟地址。

进程地址空间是与每个进程都相关的,每个进程都有一个自己的进程地址空间,并且独立于其他进程的地址空间(一些需要进程共享地址空间的特殊情况除外)。在内核层面,进程地址空间被操作系统抽象为一个mm_struct结构体,这个结构体中记录了进程地址空间的分区情况等信息。在进程的 task_struct 结构体中,包含了一个struct mm_struct*指针,这个指针即指向了进程所对应的 mm_struct 结构体。

/*
*代码 2.1
linux-2.6.1\linux-2.6.1\include\linux\sched.h
*/
struct task_struct {
  /*…………*/
  
  //task_struct中的mm_struct指针
  struct mm_struct *mm, *active_mm;
  
  /*…………*/
};

进程地址空间的区域划分

进程地址空间的区域划分,即是我们已经所理解的,对未初始化全局数据、已初始化数据以及堆栈等的区域划分。在代码层面,“区域”本质是用“边界”来描述的,即描述一块区域,只需要描述这块区域的边界即可;而调整区域的大小,即是调整边界的位置。同时,一块区域中的任何地方,拥有者都是可以自由访问和使用的。

/*
*代码 2.2
linux-2.6.1\linux-2.6.1\include\linux\sched.h
*/
struct mm_struct {
  /*…………*/
  
  //mm_struct中对区域的划分
  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 rss, total_vm, locked_vm;
  unsigned long def_flags;
  cpumask_t cpu_vm_mask;
  
  /*…………*/
};

页表概述、缺页中断概述和cr3寄存器

现在对代码1.2运行过程中,操作系统的行为做一分析。先单独看父进程与物理内存之间的关系:

Linux平台下的进程地址空间_缺页中断_05

如上文所说,父进程 task_struct 中的 mm_struct 指针指向了父进程的进程地址空间(的抽象),进程地址空间中有 val 的虚拟地址。val 的虚拟地址,包括进程地址空间中用户区的其他区域的虚拟地址,都通过页表(page table)映射到物理内存的物理地址。

关于页表的更多内容,将在内存管理部分进行详细讨论,这里只针对相关部分进行说明。页表中大致有以下内容:

  • 进程地址空间的虚拟地址 - 物理内存的物理地址的映射。虚拟地址通过页表和MMU找到物理内存中对象的实际存储位置。
  • 内存中数据的访问权限标志位。针对对内存的越权操作,会被页表拦截。在进程运行时,父、子进程的数据区的页表项权限会被修改为只读,一旦某个进程要对数据进行修改,此时不做异常处理,而是另外申请物理内存完成写时拷贝,完成后,将父、子进程数据区的页表项权限修改为可写。
  • 存在(exist)标志位。在运行一个程序时,程序的代码和数据并非全部加载到物理内存中,页表可以对虚拟内存进行标记,如果需要用到某些代码和数据,而对虚拟内存的标记显示这些代码和数据不在内存中,就会触发缺页中断(page fault),此时操作系统便会将对应的代码和数据加载到内存中。写时拷贝本质属于一种缺页中断。

每个进程都有自己的页表,每个进程的页表是相互独立的。页表是由cr3寄存器进行维护的。cr3是CPU中的一个寄存器,它记录了当前正在运行的进程的页表的起始地址。一个进程的页表本质属于进程的上下文,当进程结束时,cr3中的地址会以进程上下文的形式被进程带走。

承上,当 fork() 子进程时,子进程会拷贝父进程的task_struct、mm_struct和页表,此时子进程被创建:

Linux平台下的进程地址空间_进程地址空间_06

由于子进程的各种内核数据结构都拷贝自父进程,所以父、子进程的 val 的虚拟地址,以及虚拟地址由页表映射到的物理地址都是相同的。当子进程尝试对val 的值进程修改时,操作系统会进行写时拷贝,在物理空间额外开一块内存,将数据拷贝到新空间,并将新的物理地址填入子进程的页表中,页表中的虚拟地址不变。写时拷贝完成后,子进程可以通过虚拟地址和页表找到新的物理地址,对值进行修改。这个过程对进程的地址空间和页表中的虚拟地址部分而言是0感知的,虚拟地址不关心这个过程。

完成上述过程后,父、子进程内核结构分布大概如下:

Linux平台下的进程地址空间_环境变量_07

至此,已经可以解释代码1.2的运行结果中,输出的父、子进程的 val 地址相同的原因:操作系统进行写时拷贝后,只是修改了页表中的物理地址部分,写时拷贝对于虚拟地址来说是0感知的,所以子进程val的虚拟地址仍然与父进程相同,而且我们所看到的只能是虚拟地址,所以输出的val的地址相同。

进程地址空间的意义

理解地址空间后,现在对进程地址空间的意义做一总结:

  • 进程地址空间可以让进程以统一的视角看待内存
  • 访问内存时,增加一个转换的过程,在这个转换过程中,由于有进程地址空间区域划分的存在,可以针对用户的越界寻址进行检查和非法拦截
  • 进程地址空间和页表的存在,将进程管理模块与内存管理模块进行了解耦

Linux平台下的进程地址空间_环境变量_08

环境变量

这里首先在使用和命令层面讨论环境变量,再在代码以及进程管理层面讨论环境变量。

环境变量概述和查看

变量就是以一组文字或符号等,来替换一些设置或一串保留的数据。

Linux平台下的进程地址空间_进程地址空间_09

环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。环境变量通常具有某些特殊用途,环境变量在系统中具有全局属性

使用env命令查看系统中的所有环境变量:

[@shr Thu Nov 09 21:48:37 mail]$ env
#......
HOSTNAME=VM-24-8-centos
TERM=xterm
SHELL=/bin/bash
HISTSIZE=3000
#......
SSH_TTY=/dev/pts/1
USER=shr
#......
MAIL=/var/spool/mail/shr
PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/shr/.local/bin:/home/shr/bin
PWD=/var/spool/mail
LANG=en_US.utf8
SHLVL=1
HOME=/home/shr
LOGNAME=shr
#......
XDG_RUNTIME_DIR=/run/user/1001
HISTTIMEFORMAT=%F %T 
_=/usr/bin/env
OLDPWD=/home/shr

一些常见的环境变量有:

  • HOME 代表用户的家目录。使用cd ~命令时,就是使用这个变量进入对应的目录。
  • PATH 指明了指定命令的查找路径,目录与目录之间以冒号( : )隔开。
  • SHELL 记录了当前使用的shell是哪个程序。
  • LANG 记录了语系数据。

环境变量相关命令和接口

关于环境变量的常用命令有:

  • echo 显示某个环境变量值 : echo $PATH
  • env 显示所有环境变量
  • export 新增一个环境变量
  • unset 清除一个环境变量
  • set 显示所有环境变量和本地定义的shell变量

在C/C++代码层面,有两种方式获取环境变量:

  • main()函数的第三个参数。main()函数的参数有两张表:命令行参数表和环境变量表,环境变量表以NULL结束。这两张表是由操作系统维护的,进程被创建时,这两张表会被加载。
/*
代码 3.1
*/
#include <stdio.h>

int main(int argc, char* argv[], char* env[])
{
  for(int i = 0; env[i]; ++i) {
    printf("%s\n", env[i]);
  }
  return 0;
}
  • 第三方变量environ。libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明。
/*
代码 3.2
*/
#include <stdio.h>

int main()
{
  extern char** environ;
  for(int i = 0; environ[i]; ++i) {
    printf("%s\n", environ[i]);
  }
  return 0;
}

在C/C++中,可以使用getenv(3)putenv(3)获取环境变量的内容和设置环境变量:

/*
代码 3.3
#include <stdlib.h>
char *getenv(const char *name);
*/

#include <stdio.h>
#include <stdlib.h>
int main()
{
  putenv("NUM=9999");
  printf("PATH:%s\n", getenv("PATH"));
  printf("NUM:%s\n", getenv("NUM"));
  return 0;
}

关于 getenv 和 putenv 的行为会在下文讨论。

环境变量的传递性

正如在开篇所说的,进程地址空间中专门有一个区域用来存放当前进程的环境变量。承上,环境变量具有全局属性,即环境变量会被所有的子进程继承,环境变量在子进程被创建时就已经存在。每个进程都会收到一张环境变量表,环境变量表是一个字符指针数组,每个指针指向一个以'\0'为结尾的环境字符串,这即是环境变量在进程中的组织形式。

Linux平台下的进程地址空间_进程地址空间_10

/*
代码 3.4
*/
int main()
{
  putenv("NUM=9999"); //父进程设置一个环境变量
  pid_t id = fork();
  if(id < 0) { perror("fork err"); exit(1); }
  else if(id == 0)
  {
    printf("NUM=%s\n", getenv("NUM")); //子进程获取新的环境变量
    exit(0);
  }
  else
  {
    int status = 0;
    waitpid(id, &status, 0);
    exit(0);
  }
  return 0;
}

在用户层面,用户启动的进程都是shell的子进程,所以用户的环境变量表是从shell进程继承而来的。当用户登录时,系统会启动一个shell进程,shell会读取用户目录下的.bash_profile文件,里面保存了导入环境变量的方式,shell的环境变量表就此而来。

在进程地址空间中,环境变量的存储和组织可以抽象为如下结构:

Linux平台下的进程地址空间_进程地址空间_11

可见,环境变量的实际存储区域与 env[] 是分开的。当使用 getenv 获取环境变量的内容时,会以 env[] 为中转,在存储区域拿到实际的环境变量字符串的内容。同理,当使用 putenv 设置环境变量时,只会将字符串指针放入 env[],当字符串实际内容发生变化时,通过 getenv 拿到的对应的环境变量也会不同。

int main()
{
  char envVal[256] = "NUM=9999";
  putenv(envVal);
  printf("envVal:%s\n", getenv("NUM"));
  strcpy(envVal, "NUM=114514");
  //envVal的内容发生变化,通过getenv拿到的内容也会变化
  printf("envVal:%s\n", getenv("NUM"));
  return 0;
}

Linux平台下的进程地址空间_进程地址空间_12

反观进程

linux下的进程概念

至此,我们对进程有了更深刻的让认识,即 进程 = 进程的内核数据结构 + 代码和数据,其中已经谈到的内核数据结构有 task_struct, mm_struct 和 page table,在后续的讨论中,会接触到更多的关于进程的内核数据结构。

在Linux中,各模块的解耦是必要的,但是作为一个庞大的系统,会不可避免地存在耦合部分,这点在后续的讨论中也会体现。

如何做到进程的独立性?

各个进程是独立的:

  • 首先,各个进程的内核数据结构是独立的,每个进程都有一套属于自己的内核数据结构体系(内核数据结构终归也是在物理内存中存储的)。
  • 在物理内存中,各个进程的代码和数据是独立的,页表的存在使进程不必关心具体的物理内存分布细节。即使进程地址空间中的虚拟地址相同,只要页表将其映射到物理内存不同的位置,进程之间就不会相互干扰。

标签:int,空间,地址,页表,Linux,进程,环境变量
From: https://blog.51cto.com/158SHI/8303700

相关文章

  • 【docker】Mac M1 构建 x64 linux镜像
    亲测教程,跨平台镜像构建[toc]首先首先你需要有一个Dockerfile比如:这里以一个python项目举例FROMpython:3.10-slimWORKDIR/appCOPYrequirements.txtrequirements.txtRUNpipinstall--no-cache-dir-rrequirements.txtCOPY..CMD["python","bin/run.py"]构建......
  • linux diff求两个文件的差集
    awk从文本中过滤出需要的ipqueryId_20231109214653_ipBatchQueryResult.json{"id":0,"ip":"121.204.216.130","type":1,"domain":"","agreement":"","mode":"","postalCo......
  • Linux系统常用审计命令
    1、https://blog.51cto.com/u_10401840/5927529Linux中常见日志以及位置/var/log/cron记录了系统定时任务相关的日志/var/log/auth.log记录验证和授权方面的信息/var/log/secure同上,只是系统不同/var/log/btmp登录失败记录使用lastb命令查看/var/log/wtmp登录失成功记录......
  • 《Unix/linux系统编程》教材第6章学习笔记
    |第5章|信号和信号处理信号和中断“中断”是从I/O设备或协处理器发送到CPU的外部请求,它将CPU从正常执行转移到中断处理。与发送给CPU的中断请求一样,“信号”是发送给进程的请求,将进程从正常执行转移到中断处理。在讨论信号和信号处理之前,先来回顾中断的概念和机制,这有助于正确......
  • mac地址老化时间
    老化时间是一个影响交换机学习进程的参数。在老化时间内,如果地址未被使用,那么,这些地址将从动态转发地址表(由源mac地址、目的mac地址和它们相对应的交换机的端口号)中被删除。老化时间的数值范围从10秒~1,000,000秒,缺省值为300秒。过长的老化时间会导致交换机内的mac地址表超......
  • Linux基础命令(一)
    cd命令 绝对路径:cd/home/admin查看当前目录:pwd返回上一目录:cd..回到admin:cd~返回倒数第二个目录:cd-ls命令:查看目录内容ls:查看普通文件ls-a:查看所有文件(隐藏文件.xxxx)ls-l(ll):查看文件详细信息ls-lh:人性化显示详细列表ls权限drwxr-xr--:d代表文件夹 -代......
  • Linux常用命令-docker
     1、进入容器: dockercontainerexec-it容器id/bin/bash①直接进入容器中的mongodb:sudodockerexec-itmongomongosh②dockerexec-itcontainerName/bin/bash2、容器开机自启动:①docker开机自启动:systemctlenabledocker.servic......
  • linux系统centos7安装docker
    1、Docker官网安装地址https://docs.docker.com/engine/install/centos/#prerequisites2、离线安装下载地址https://download.docker.com/linux/static/stable/x86_64/3、使用yum工具安装如果之前安装需要先卸载sudoyumremovedocker\docker-cl......
  • Linux的一些指令
    这里主要是记录下平时工作中所使用到的Linux系统下的指令 查找指令find-name"*.mk"-o-name"*.bp"|xargsgrep"***"//用于在项目代码中的mk和bp文件查找对应的字段,最后的"***"就是要查找的字段grep-rn***//用于在某个目录下查找关键字***,参数-r是可......
  • man命令总结linux常用基本命令用法以及查看帮助文档的方法
       Linux中的常见命令1查看系统相关信息命令(1)查看内核版本uname-r(2)显示操作系统发行版本cat/etc/os-release(3)查看当前主机名hostname2查看硬件信息(1)查看CPUlscpucat/proc/cpuinfo(2)查看内存大小free-hcat/proc/meminfo(3)查看硬盘分区情况lsblkcat/proc/partiti......