Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【Linux】深入理解进程和文件及内存管理

【Linux】深入理解进程和文件及内存管理

作者头像
s-little-monster
发布于 2025-03-18 04:54:39
发布于 2025-03-18 04:54:39
1680
举报

一、重谈Linux下一切皆文件

这个图画完之后截下来不太清楚,有需要的可以到我的Gitee中取:点击这里取图片~

我们说了一切皆文件,对于操作系统来说,磁盘键盘显示屏等等一系列的外设都是文件,举一个访问外设的例子:进程运行,从进程PCB中找到指针指向文件管理结构体,然后在这个结构体中我们可以找到struct file*类型的指针指向一个个的文件管理结构体struct file,在这些结构体中都有着一个专门放读写函数的结构体,调用这些读写函数可以访问到外设存放读写函数的结构体,而虽然每个外设的读写方式不同,但它们仅把处理好的代码封装后将接口漏出,方便上方函数的统一调用,这样虽然每个外设不同,但是我们通过一种求同存异的方法,将它们统一协调调度起来 类似于键盘一类的只有读或者显示屏一类的只有写的外设,我们也有读或写的接口,只是接口不做处理,方便统一

二、操作系统对物理内存的管理

1、物理内存与磁盘的数据交互

在操作系统的运行机制里,物理内存和磁盘之间的数据交换起着关键作用,这种交换一般就是以页page为单位,常见的页大小为 4KB,在物理内存中,一个 4KB 大小的空间被称作页框,而从磁盘加载到这个页框里的4KB数据块则被叫做页

采用这种以页为单位进行数据交换的方式,具有显著的优势,一方面,能有效减少IO操作的次数,进而提升系统效率,举例来说,如果需要读取数据,一次读取 4KB 与分四次读取每次 1KB 相比,前者的效率要高得多,对于硬盘来说,一次读取 4KB 时,CPU 只需与磁盘进行一次交互,而分四次读取 1KB 时,CPU 要与磁盘进行四次交互,且这四次操作很可能不连续,这就意味着效率低下,另一方面,这种方式还遵循基于局部性原理的预加载机制,即便当前 CPU 仅需访问 100 字节的内容,操作系统和磁盘之间依旧会以 4KB 为单位将数据加载进来,这是因为根据经验,CPU 在访问当前磁盘中的代码和数据时,后续有较大概率会访问附近空间的代码和数据,还有一方面,就是对齐,磁盘中的最小写入单位是页,因为计算机硬件的设计往往遵循一定的对齐规则,这样可以提高数据访问的效率内存和磁盘控制器在设计时,通常会按照特定的字节边界来组织和传输数据,以页为单位进行数据交换可以保证数据在内存和磁盘之间的传输是按照硬件对齐要求进行的,减少硬件处理的复杂性

2、操作系统对物理内存的管理

操作系统具备感知物理内存的能力,其对物理内存的管理遵循先描述再组织的原则,在内核中,struct page 结构体承担着描述物理内存的重要职责,一个 struct page 对象对应着一个 4KB 的内存页框,该结构体中记录了当前页框的诸多属性信息,像页框的状态、引用计数等

操作系统会把物理内存划分成一个个的struct page对象,再用数组的形式将它们组织起来,数组的下标即为对应的页号,若要确定一个物理地址所在的页号,只需将该物理地址除以 4096 b(4KB = 4 * 1024 = 4096 b),或者将该地址按位与上 0xFFFFF000(以 32 位系统为例),把低 12 位清零,得到的结果就是该地址所在的页号

在进行内存申请操作时,系统会访问 page 数组,查看 struct page 里的 flags 属性,通过这个属性,系统能够判断当前页框的状态,确定其是否已被使用,若未被使用,系统就会修改 flags 以表明该页框已被申请,此外,flags 除了能表示页框的使用状态外,还能指示该页框是只读还是可读写等状态, 当然这里所介绍的只是操作系统对物理内存管理的一个基础模型,实际上真正的内存管理系统要复杂得多

三、文件页缓冲区

在操作系统内核中,struct file 是一个重要的数据结构,用于描述一个已打开的文件,而 inode 这一概念是在介绍磁盘时引入的,磁盘上的每个文件都对应着一个inode,它存储了该文件的属性信息,struct fileinode 之间存在着紧密的联系,struct file中仅记录了文件的少量属性,而struct inode结构体则专门用于记录一个文件的所有属性,在 struct file 中有一个指针字段,它指向该文件的struct inode对象

文件由内容和属性两部分构成,在磁盘上,文件的属性由 inode存储,文件的内容则由数据块存储,那么,在操作系统内核中,文件的内容(数据)是如何表示的呢?答案就是通过文件页缓冲区, 在 struct file 结构中有一个指向 struct address_space 结构体的指针,在 struct address_space 结构体中,有一个 struct radix_tree_root 结构体对象,它实际上是一种树状结构,即基数树(也叫字典树),树中的每个节点都是 struct radix_tree_node 类型,该类型中有一个名为 slotsvoid* 类型数组,数组中存储的其实就是 struct page 对象的地址,简单来说,在 struct file 结构体中有指向物理内存页框的指针,我们把这些物理内存区域称为文件页缓冲区

向文件写入数据的过程

当使用C/C++库函数向文件中写入数据时,整个过程分为几个阶段,首先,数据有可能会被写入到语言层面的用户缓冲区,然后,在合适的时机,这些数据会被从用户缓冲区写入到该文件对应的文件页缓冲区中,最后,还是在合适的时机,数据会从文件页缓冲区被写入到磁盘

将物理内存中的数据刷新到磁盘这一操作由IO子系统负责执行,进程通常无需关注具体的执行过程,在操作系统中,会存在大量的IO操作,可能有很多进程都需要将数据写入磁盘,为了有效管理这些操作,操作系统会按照先描述再组织的方式对所有的IO操作进行管理,内核中的struct request结构就是专门用来描述一个IO操作的

Linux操作系统中,每个进程打开的每个文件都有自己的 struct inode 对象和对应的文件页缓冲区,也就是所谓的内核缓冲区,它们共同保障了文件操作的高效和稳定

四、动态库是如何被加载的

动态库在进程运行时要被加载到内存,一般我们常用的动态库是要被所有的可执行程序动态链接的,所以动态库在系统中加载完成后,会被所有的进程所共享

在操作系统的进程管理与库使用机制中,进程与动态库的交互有着独特的方式,一个进程在运行过程中,是可以同时链接多个动态库的,不过,当系统中存在多个进程时,不能简单地认为系统中必然存在多个不同的动态库,多个进程可能会依赖相同的动态库,操作系统对动态库采用“先描述,再组织”的策略进行管理,它会为每个动态库创建相应的数据结构来描述其属性、位置等信息,然后将这些描述信息组织起来,以便高效地进行查找、加载和管理,凭借这种管理方式,操作系统对系统中所有动态库的加载状态了如指掌

a.exe 为例,它在编译链接阶段选择使用动态库,当 a.exe 运行成为 a 进程后,CPU 会按照程序的指令顺序依次执行代码,假设在执行过程中遇到了一个库函数调用,此时,操作系统会检查该函数所在的动态库是否已经被加载到内存中,若尚未加载,操作系统会负责将该动态库加载到内存,这一加载过程本质上与文件加载一致,因为动态库本身也是以文件形式存在的,并且具有 inode 来标识其在文件系统中的元数据

动态库加载完成后,操作系统会在 a 进程的页表中建立该动态库与 a 进程地址空间中共享区的映射关系,这样,当 CPU 需要执行上述函数时,就可以从代码段跳转到共享区去执行动态库中该函数的代码,执行完毕后,CPU 会跳转回代码段,继续执行后续的程序指令

b.exe 同样在编译链接时采用动态库,之后被加载到内存成为 b 进程,当 CPU 执行 b 进程的代码并遇到上面函数调用时,因为在 a 进程已经将该所在的动态库加载到了内存,操作系统不会再次重复加载该动态库,而是直接在 b 进程的页表中建立该动态库与 b 进程共享区的映射关系,通过这种方式,同一个动态库可以被多个进程共享使用,所以动态库又被称为共享库

关于动态库中的全局变量

动态库确实可以被多个进程共享,但对于动态库中的全局变量(例如 errno),需要特殊的处理机制来保证各个进程之间的数据独立性,errno 是 C 语言标准库提供的一个全局变量,用于存储最近一次库函数调用失败时的错误码

如果简单地让所有进程共享 errno,会引发严重的问题,例如,当 a 进程调用库函数失败,errno 被设置为 1,此时如果 b 进程也使用这个共享的 errno,就会导致 b 进程错误地获取到 a 进程的错误码,这显然不符合逻辑

实际上,操作系统采用了写时拷贝技术来解决这个问题,当某个进程要修改 errno 时,操作系统会通过引用计数来判断该动态库是否被多个进程共享,如果该动态库被多个进程共享,操作系统会为该进程复制一份动态库中相关数据(包括 errno)的副本,而不是直接修改共享的数据,这样,每个进程都有自己独立的 errno 副本,从而保证了各个进程之间的错误码不会相互干扰,只有当进程对数据进行写操作时才会发生拷贝,而在只读的情况下,多个进程仍然可以共享同一份动态库数据,从而充分发挥了动态库共享的优势

五、深入理解地址

1、程序地址

在一个程序编译好后形成了可执行文件,在它的内部是有地址的概念的,这里程序内部的地址我们称为逻辑地址,我们计算机一般采用的是平坦模式编址,平坦模式编址是一种简化的内存编址模型,在这种模式下,整个内存空间被视为一个连续的、线性的地址空间,程序可以直接访问这个连续地址空间内的任意内存位置,而不需要像分段模式那样进行复杂的段地址和偏移地址组合计算,在平坦模式中,内存地址是一个单一的、连续的数值,从 0 开始一直到系统所支持的最大内存地址

32位下的4GB内存地址

2、进程地址

我们知道,可执行程序内部采用的是逻辑地址(也叫虚拟地址)进行编址,物理内存本身有其固定的物理地址,无论可执行程序是否加载,物理内存的地址体系是一直存在的,当可执行程序被加载到内存后,程序中的每一条指令和数据都会对应一个物理地址,这是通过地址映射机制实现的

那么,CPU 是如何知道可执行程序的第一条指令位置呢?在编译生成可执行程序时,除了生成代码段、数据段等程序内容外,还会生成一个文件头,这个文件头包含了诸多重要信息,其中就有可执行程序的入口地址,此地址是逻辑地址(虚拟地址)

在 CPU 中有一个关键的寄存器,即程序计数器—PC 寄存器,它存储着接下来要执行指令的地址,实际上,在程序启动阶段,不会立刻把整个可执行程序加载到内存(可以想象我们打游戏的时候不是打开游戏就能玩的,需要等待加载)而是先将可执行文件的头部加载进来,操作系统读取文件头,从中获取可执行程序的入口地址,并将该地址设置到 PC 寄存器中

CPU 拿到这个虚拟地址后,会借助内存管理单元去查询页表,页表记录了虚拟地址和物理地址的映射关系,若查询发现该虚拟地址对应的页表项无效,也就是此页面尚未建立内存映射,操作系统会触发缺页中断,缺页中断发生后,操作系统暂停当前程序的执行,从磁盘把对应的程序页面加载到物理内存的空闲页框中,同时更新页表,建立起虚拟地址到物理地址的映射,之后,恢复程序执行,CPU 就能访问到物理内存中对应的指令了

CPU 凭借其内置的指令集,能够明确识别每条指令的长度,在正常运行状态下,CPU 按照 PC 寄存器所存储的地址顺序执行指令,每执行完一条指令,PC 寄存器会自动更新为下一条指令的地址,当程序执行过程中遇到函数调用指令或跳转指令时,PC 寄存器的值会被修改为新的虚拟地址,CPU 会依据这个新的虚拟地址再次查询页表,若发现页面未在内存中,将再次触发缺页中断

由此可见,CPU 正是通过将虚拟地址转换为物理地址的方式,来执行可执行程序中的指令以及访问可执行程序中的变量的,这种基于虚拟内存和地址映射的机制,为程序提供了独立的地址空间,增强了内存管理的灵活性和安全性

3、动态库地址

在计算机系统的程序执行机制中,可执行程序内部采用逻辑地址进行编址,CPU通过将虚拟地址转换为物理地址的方式来执行指令

静态库在编译链接过程中会被整合到可执行文件中,当可执行程序启动运行时,操作系统会将包含静态库代码的可执行文件加载到物理内存,之后 CPU 方可执行其中的指令,静态库中的函数采用绝对编址,这是因为它们已成为可执行文件的组成部分,其地址在编译链接阶段就已经确定下来

在可执行程序的编译阶段,对动态库内函数的引用表现为未解析的符号,当程序进入运行状态时,动态链接器承担起解析这些符号的任务,由于动态库在运行时能够被加载到虚拟内存共享区的任意位置,要将动态库加载到固定位置存在较大困难,这是由于一个可执行程序往往会同时使用多个动态库,各个动态库的大小不尽相同,并且每个动态库都采用独立的编址方式,不同动态库中可能出现相同的编址情况,为了实现动态库在虚拟内存共享区任意位置的加载,动态库内部采用相对编址的方法,对于动态库中的函数,仅需明确其在库中的偏移量即可鉴于库中函数的偏移量是已知的(在编译动态库时,编译器会按照一定的规则对库中的代码和数据进行布局,编译器知道每个函数在代码段中的起始位置以及函数内部代码的长度),动态库便能够被加载到虚拟内存共享区的任意位置

在此机制下,操作系统只需记录每个动态库在虚拟内存中的起始地址,当需要执行某个动态库函数时,通过将该函数所在动态库的起始地址与该函数的偏移量相加,即可得到该函数在程序地址空间中的虚拟地址,随后,依据此虚拟地址查询页表,找到该函数在物理内存中的对应地址,进而执行该库函数,GCC 编译器中的 -fPIC 选项,其作用就是让编译器在生成动态库文件时,直接使用偏移量对库中的函数进行编址,从而实现代码的位置无关性

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-03-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【Linux】详解动态库链接和加载&&对可执行程序底层的理解
我们的动态库默认就是一个磁盘级别的文件。当我们的程序开始运行时,当程序运行到需要用到库中的实现方法时,库的代码和数据就会被加载到物理内存当中。库的实现方法一定是要跟程序运行起来所形成的进程产生关联的,动态库加载后,会被映射到该进程的地址空间中,准确来说,是先在页表中填写好对应虚拟地址和物理地址之间的映射关系,才被映射到进程地址空间中的共享区中。
用户10923276
2024/04/09
2.2K0
【Linux】详解动态库链接和加载&&对可执行程序底层的理解
【Linux】ELF可执行程序和动态库加载
  Linux操作系统上的可执行文件格式是ELF(Executable and Linkable Format)。ELF是一种灵活的、可扩展的文件格式,用于存储可执行程序、共享库和目标文件等二进制文件。
大耳朵土土垚
2024/11/15
3260
【Linux】ELF可执行程序和动态库加载
【Linux】从零开始认识多线程 --- 线程概念与底层实现
在学习多线程之前,我们先来了解一些背景知识,我们需要这些背景知识来辅助我们理解多线程!
叫我龙翔
2024/07/16
4130
【Linux】从零开始认识多线程 --- 线程概念与底层实现
Linux:基础IO(三.软硬链接、动态库和静态库、动精态库的制作和加载)
上次介绍了基础IO(二):Linux:基础IO(二.缓冲区、模拟一下缓冲区、详细讲解文件系统)
是Nero哦
2024/07/01
3390
Linux:基础IO(三.软硬链接、动态库和静态库、动精态库的制作和加载)
Linux进程概念(三)
我们所有写的程序都需要指定路径才能运行,就像这样:(程序里面是打印DLC循环)
有礼貌的灰绅士
2023/03/28
5990
Linux进程概念(三)
万字讲解Linux进程概念
 有些书上对进程的描述是这样一句话:进程是在内存中的程序。一个运行起来(加载到内存)的程序称作进程。
二肥是只大懒蓝猫
2023/03/30
5850
万字讲解Linux进程概念
【Linux】虚拟地址空间 --- 虚拟地址、空间布局、内存描述符、写时拷贝、页表…
1. 从程序的运行结果可以看出一些端倪,就是一个全局变量在地址并未改变的情况下,竟然出现了不同的值,这说明什么呢?首先一个变量肯定是只能有一个值的,但是地址只有一个,而变量的值却出现了两个,那么就必须说明一个结论,现在在内存中应该出现了两个变量了,因为一个变量是绝对不可能出现两个值的,所以我们可以推导出的结论就是内存中现在一定出现了两个全局变量global_value。
举杯邀明月
2023/04/12
1.6K0
【Linux】虚拟地址空间 --- 虚拟地址、空间布局、内存描述符、写时拷贝、页表…
【Linux】多线程概念再理解
物理内存的宽度为1字节 如使用c语言,可以定义出char类型(1字节),在虚拟地址空间上可以把1字节的单位映射到内存中
lovevivi
2023/10/16
2050
【Linux】多线程概念再理解
【Linux】翻山越岭——进程地址空间
有了这个基本框架,我们对于语言的学习更加易于理解,但是地址空间究竟是什么❓我们对其并不了解,是不是内存呢?对于是什么这个问题,我们需要通过一个例子来进行切入,见一见现象
平凡的人1
2022/11/21
7770
【Linux】翻山越岭——进程地址空间
【Linux课程学习】: 进程地址空间,小故事理解虚拟地址,野指针
Linux学习笔记: https://blog.csdn.net/djdjiejsn/category_12669243.html
用户11396661
2024/12/09
1500
【Linux课程学习】: 进程地址空间,小故事理解虚拟地址,野指针
动静态库:选择与应用的全方位指南
那么这样inode中一定有一个引用计数的变量用于记录这个inode编号有多少段映射关系。
绝活蛋炒饭
2024/12/16
1160
动静态库:选择与应用的全方位指南
【Linux】动态库与静态库的底层比较
我们前两篇文章讲解了如何建立动静态库与如何使用动静态库。 接下来我们就来深入聊聊动静态库。
叫我龙翔
2024/05/13
4160
【Linux】动态库与静态库的底层比较
深入浅出动静态库
  当你在Linux系统上编写和运行程序时,动态库和静态库是两个非常重要的概念。它们不仅影响着程序的编译和执行效率,还直接关系到程序的可移植性和灵活性
用户11029129
2024/06/04
1780
深入浅出动静态库
一文读懂 Linux mmap 内存映射
mmap(memory map)即内存映射,用于将一个文件或设备映射到进程的地址空间,或者创建匿名的内存映射。
恋喵大鲤鱼
2024/05/24
7.4K0
一文读懂 Linux mmap 内存映射
Linux用户态进程的内存管理
上一篇我们了解了内存在内核态是如何管理的,本篇文章我们一起来看下内存在用户态的使用情况,如果上一篇文章说是内核驱动工程师经常面对的内存管理问题,那本篇就是应用工程师常面对的问题。
刘盼
2018/07/26
2.9K0
Linux用户态进程的内存管理
【图片+代码】:Linux 动态链接过程中的【重定位】底层原理
在上一篇文章中,我们一起学习了Linux系统中 GCC编译器在编译可执行程序时,静态链接过程中是如何进行符号重定位的。
IOT物联网小镇
2022/04/06
2.8K0
【图片+代码】:Linux 动态链接过程中的【重定位】底层原理
Linux 内存管理
      程序到运行主要经过程序(外存)编译,链接,装入(内存)。《程序如何运行:编译、链接、装》:
黄规速
2022/06/15
8.1K0
Linux 内存管理
【Linux系统】代码星辰里的积木与流萤:动静态库的编程诗篇
本文将以清晰的逻辑脉络,带领读者从基础概念入手,逐步掌握动静态库的制作、生成、发布、使用及安装全流程。通过具体的代码示例与 Makefile 配置解析,结合编译链接原理与操作系统内存管理机制,深入理解静态库的 “空间换时间” 特性与动态库的 “运行时加载” 优势。同时,结合地址空间与程序执行原理,剖析库函数在内存中的定位与调用机制,帮助读者构建从理论到实践的完整知识体系。
suye
2025/06/01
810
【Linux系统】代码星辰里的积木与流萤:动静态库的编程诗篇
Linux:理解动静态库
    所以为了学习如何创建静态库和动态库以及理解静态链接和动态链接的本质。我们得从以下两个角度来理解:
小陈在拼命
2024/11/12
2640
Linux:理解动静态库
解锁动静态库的神秘力量2:从代码片段到高效程序的蜕变(续篇)
我们在上一篇(传送门:解锁动静态库的神秘力量1:从代码片段到高效程序的蜕变-CSDN博客)讲解了关于动静态库如何使用的要点及规则;下面肯定会有很多疑问;为什么要那么操作;此篇我们为上一篇的补充;续集;将带大家了解动静态链接的底层原理完成对上一篇所用的规则和指令展开讲解分析;准备好,那我们就出发了!!!
羑悻的小杀马特.
2025/01/23
1660
解锁动静态库的神秘力量2:从代码片段到高效程序的蜕变(续篇)
推荐阅读
相关推荐
【Linux】详解动态库链接和加载&&对可执行程序底层的理解
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档