Linux之进程地址空间

简介: Linux之进程地址空间

前言

内存区域划分:

在学习C/C++时我们都有接触过内存区域划分这个概念,也知道它表示的是程序加载到内存中不同的数据所分布的不同的区域,但是我们并不清楚它是什么东西,在哪里存储着,为什么要有它,它又是怎样实现的。今天我们就来解决这些疑惑。


一、是什么

进程地址空间是什么?

1.例子

我们先来看这样一个现象:

1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 int main()
  5 {
  6         pid_t pid = fork();
  7         int i = 10;
  8         if(pid < 0)//函调用失败
  9         {
 10                 perror("fork");
 11                 exit(1);
 12         }
 13         else if(pid == 0)//子进程
 14         {
 15                 int cnt = 0;
 16                 while(1)
 17                 {
 18                         printf("子进程,pid = %d, ppid = %d, i = %d, &i = %p\n",getpid(),getppid(),i,&i);
 19                         if(cnt == 5)
 20                         {
 21                                 i = 5;
 22                         printf("子进程已更改全局变量……\n");
 23                         }
 24                         cnt++;
 25                         sleep(1);
 26                 }
 27         }
 28         else
 29         {
 30                 //父进程
 31                 while(1)
 32                 {
 33                         printf("父进程,pid = %d, ppid = %d, i = %d, &i = %p\n",getpid(),getppid(),i,&i);
 34                         sleep(2);
 35                 }
 36         }
 37         return 0;
 38 }

我们发现在同一个地址空间的i变量,父进程和子进程访问时获得的值却不是相同的,这是什么原因呢?

首先,我们可以理解,父子进程的值不同是因为进程间具有独立性,但是这里的i的地址居然是相同的!!!我们可以先排除该地址是在物理磁盘上的地址的可能性,因为物理磁盘的同一个地址只能存唯一确定的一个值。

因此,这个地址只能是虚拟地址(线性地址)。在Linux中,特殊情况,我们将这种地址也成为逻辑地址。

2.感性的理解虚拟地址空间

从前有一个大富翁,他有10亿美元的资产。他有三个私生子,这三个私生子都不知道对方的存在,而大富翁也给他们画饼,说自己就他这一个儿子,自己走后,这10个亿都归他所有,因此他们都认为大富翁的10亿美元是被他们独占。当然大富翁也知道,这三个儿子虽然想着所有的钱都是他的,但是不可能一次性向大富翁要所有的钱,每次最多几十万、几百万的向大富翁要,所以大富翁画的饼从来没有被拆穿过。

我们的操作系统就相当于大富翁,而进程则相当于他的私生子,因为进程之间是相互独立的,因此每个进程都认为自己可以独占操作系统的所有资源,当然进程也不会一次性向申请操作系统申请操作系统的所有资源,一次只会申请一小部分资源。

为了给进程画饼(让进程认为自己独占操作系统资源),操作系统为每个进程都创建了独立的地址空间,地址空间的内容通过页表映射到物理内存中这样每个进程都能独立的运行。

3.现象的具体解释

父进程和子进程都有自己独立的进程地址空间,也有独立的页表结构。子进程由父进程创建,因此子进程的进程地址空间是拷贝父进程的进程地址空间。刚开始父子进程并未对进程地址空间做修改,因此i值在一开始指向同一个物理内存。

后来,子进程修改了i的值,操作系统通过页表映射发现i的值是两个进程共享的,操作系统为了保持进程的独立性,当子进程或者父进程任何一方尝试对共享的数据做写入,操作系统就会在物理内存上重新开辟一块新的内存空间拷贝原来的数据,然后修改映射关系,使其指向新的物理地址,再进行写入操作。整个修改的过程中,这些工作与父子进程的虚拟地址没有关系,只有底层经过页表映射到了新的物理地址,因此我们观察到的虚拟地址是相同的,但是内容却不同。

4.写时拷贝

父子进程中的任意一方试图对共享数据进行写入,操作系统就会先将原数据进行拷贝,然后改变要写入一方的页表映射,使它映射到新的物理内存中,然后再让进程进行写入的技术称为写时拷贝

二、为什么

为什么存在进程地址空间?

  1. 保证了数据的安全性
    如果进程出现越界非法访问、非法写入,页表会对进程进行拦截。直接对物理内存进行访问,对于账号信息等数据是不安全的(可能出现:意外损坏数据或者恶意读取用户信息等问题)。
  2. 方便进程之间的数据代码的解耦,保证了进程的独立性
    一个进程对数据的修改不会对另一个进程造成影响,保证了进程的独立性。
  3. 让进程以统一的视角看待进程的代码和数据所在的各个区域,同时方便了编译器以统一视角编译代码
    可执行程序再被编译器编译的时候代码和数据再内存中已经有虚拟地址(在磁盘上的这种地址称为逻辑地址),也就是说操作系统和编译器都是遵守地址空间这一理论的。
    在程序被加载到内存成为进程后,每个变量/函数都具备了物理地址。因此,我们现在有两套地址,一套是用于表示物理内存中代码和数据的物理地址;另一套是用于程序内部函数之间进行跳转的虚拟地址。
    加载完毕后,代码的各个区域的地址,操作系统和编译器都已经知道了。进程被调度时,CPU拿到虚拟地址,经过地址空间的页表的映射,就能查到物理地址,通过物理地址访问到代码,然后执行。
    CPU -> 虚拟地址 -> 页表 -> 物理地址 -> 执行。
    也就是说明,CPU运行的整个过程中,CPU都没有见到物理地址,而是用虚拟地址运行程序。

对于磁盘内编译过的可执行程序中的地址不叫虚拟地址,而是叫做逻辑地址。当然对于Linux而言,虚拟地址、线性地址、逻辑地址都是一样的。

三、怎么办

  1. 操作系统要为每一个进程分配地址空间,那么操作系统是否要管理这些地址空间呢?当然是要管理的。
  2. 那么,操作系统怎么管理进程的地址空间
    说到管理,那就是管理数据,管理的方法是先描述,再组织
    首先,进程本身就是需要被管理的,操作系统管理它的方式是将进程的信息存入结构体PCB(即,task_struct)中,再用链式结构将每一个进程的PCB对象组织起来。
    而地址空间也是需要用内核数据结构mm_struct进行管理,OS会为每一个进程创建一个mm_struct(结构体)对象,进行管理。该结构体对象保存在它所对应进程的PCB中。(PCB中的一个属性mm_struct)
  3. 区域划分和调整
    地址空间有很多区域:栈区、堆区、数据段、代码段等,那么进程地址空间是如何进行区域划分的呢?
    举个简单的小栗子:

    上小学的小蓝和小粉是同桌,小粉并不想和小蓝一起玩,因此将桌子上用一条“三八线”划分为了两个区域,左边属于小蓝,右边属于小粉两人约定不能过线(即,不能非法访问别人的区域)。
    虚拟地址空间是连续的,因此将地址空间划分为不同区域的方法与上面例子的做法类似,我们用一个区域的起始地址start和终止地址end来调整和维护这一块区域。
struct mm_struct
{
  uint32_t code_start,code_end;
  uint32_t data_start,data_end;
  uint32_t heap_start,heap_end;
  uint32_t stack_start,stack_end;
}

所谓的区域调整,本质就是修改对应区域的start和end的值。

补充说明:

对于区域划分,进程地址空间的划分实际上是这样的:

0-3G是用户空间,命令行参数和环境变量是在用户空间,这也是为什么我们可以在main函数通过第三个参数env获取环境变量。3-4G是内核空间。


总结

以上就是今天要讲的内容,本文介绍了进程地址空间的相关概念。本文作者目前也是正在学习Linux相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。

最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!

相关文章
|
9天前
|
存储 Linux API
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
在计算机系统的底层架构中,操作系统肩负着资源管理与任务调度的重任。当我们启动各类应用程序时,其背后复杂的运作机制便悄然展开。程序,作为静态的指令集合,如何在系统中实现动态执行?本文带你一探究竟!
【Linux进程概念】—— 操作系统中的“生命体”,计算机里的“多线程”
|
3月前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
129 1
|
22天前
|
存储 网络协议 Linux
【Linux】进程IO|系统调用|open|write|文件描述符fd|封装|理解一切皆文件
本文详细介绍了Linux中的进程IO与系统调用,包括 `open`、`write`、`read`和 `close`函数及其用法,解释了文件描述符(fd)的概念,并深入探讨了Linux中的“一切皆文件”思想。这种设计极大地简化了系统编程,使得处理不同类型的IO设备变得更加一致和简单。通过本文的学习,您应该能够更好地理解和应用Linux中的进程IO操作,提高系统编程的效率和能力。
69 34
|
5天前
|
Linux
Linux:守护进程(进程组、会话和守护进程)
守护进程在 Linux 系统中扮演着重要角色,通过后台执行关键任务和服务,确保系统的稳定运行。理解进程组和会话的概念,是正确创建和管理守护进程的基础。使用现代的 `systemd` 或传统的 `init.d` 方法,可以有效地管理守护进程,提升系统的可靠性和可维护性。希望本文能帮助读者深入理解并掌握 Linux 守护进程的相关知识。
25 7
|
4天前
|
Linux Shell
Linux 进程前台后台切换与作业控制
进程前台/后台切换及作业控制简介: 在 Shell 中,启动的程序默认为前台进程,会占用终端直到执行完毕。例如,执行 `./shella.sh` 时,终端会被占用。为避免不便,可将命令放到后台运行,如 `./shella.sh &`,此时终端命令行立即返回,可继续输入其他命令。 常用作业控制命令: - `fg %1`:将后台作业切换到前台。 - `Ctrl + Z`:暂停前台作业并放到后台。 - `bg %1`:让暂停的后台作业继续执行。 - `kill %1`:终止后台作业。 优先级调整:
28 5
|
4天前
|
Linux 应用服务中间件 nginx
Linux 进程管理基础
Linux 进程是操作系统中运行程序的实例,彼此隔离以确保安全性和稳定性。常用命令查看和管理进程:`ps` 显示当前终端会话相关进程;`ps aux` 和 `ps -ef` 显示所有进程信息;`ps -u username` 查看特定用户进程;`ps -e | grep &lt;进程名&gt;` 查找特定进程;`ps -p &lt;PID&gt;` 查看指定 PID 的进程详情。终止进程可用 `kill &lt;PID&gt;` 或 `pkill &lt;进程名&gt;`,强制终止加 `-9` 选项。
16 3
|
26天前
|
消息中间件 Linux C++
c++ linux通过实现独立进程之间的通信和传递字符串 demo
的进程间通信机制,适用于父子进程之间的数据传输。希望本文能帮助您更好地理解和应用Linux管道,提升开发效率。 在实际开发中,除了管道,还可以根据具体需求选择消息队列、共享内存、套接字等其他进程间通信方
61 16
|
2月前
|
消息中间件 Linux
Linux:进程间通信(共享内存详细讲解以及小项目使用和相关指令、消息队列、信号量)
通过上述讲解和代码示例,您可以理解和实现Linux系统中的进程间通信机制,包括共享内存、消息队列和信号量。这些机制在实际开发中非常重要,能够提高系统的并发处理能力和数据通信效率。希望本文能为您的学习和开发提供实用的指导和帮助。
170 20
|
3月前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
134 13
|
3月前
|
SQL 运维 监控
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具