从代码到可执行到运行的整体流程与原理

个人主页:Lei宝啊 

愿所有美好如期而遇


前言

本篇文章将会介绍,磁盘中是如何存储一个文件,磁盘中的文件是如何从磁盘中加载进内存,与进程又有怎样的关系,我们写的代码变成可执行程序执行起来时如何完成对文件的一系列操作。

那么接下来我们将会是一块整体长篇大论去提及。

详谈

我们这里提到的磁盘是机械磁盘,他大概长这样:

我们可以看到几个盘片,这些盘片就是磁盘,用来保存数据,在微观上,他是一块块的小磁块,分为南北极,也对应着我们常说的比特位的0和1,对每一块盘片,我们又给他分了磁道和扇区,就像这样:

这样一个扇区能够存储数据的大小为512字节,当然,也可以更多,我们这里提及的是一般情况下。

那么磁盘是如何定位数据的呢?我们上面的图中,有一个磁头,通过磁头定位盘面,再确定磁道,最后是扇区(磁盘IO的基本单位是扇区)。

上述就是磁盘的物理层面,那么我们是如何管理这些数据的?这就要提到操作系统,由操作系统对他们进行管理,怎么管理呢?一个磁盘能够保存的数据并不小,几百GB到几个T都有。

于是有了逻辑层面上,将磁盘的物理存储抽象为逻辑存储,操作系统对磁盘的管理也就变成了对一个个结构体的管理,先描述,再组织,而这种管理方式就需要加载文件系统来进行管理。

我们说,磁盘很大,我们不好管理,于是就有了分区管理,将磁盘进行分区,每个分区的管理可以借由不同或相同的文件系统进行管理,而每个分区又有分组,借由这种方式来管理磁盘。

分组中的管理方式,不同的文件系统有不同,我们这里介绍ext2文件系统中分组的管理方式:

super Block,超级块,存储着文件系统的信息,这个块如果损坏,这个区的信息就损坏了,但是,并不是每个分组都会有这个超级块,由操作系统决定部分分组有这个超级块,并且他们的信息是同步的,一个损坏了,去其他分组再同步过来。

Group Descriptor Table(GDT),块组描述符,保存着这个组的一系列信息,块损坏,这个组信息损坏。

Block Bitmap,Data Blocks,Data Blocks就是操作系统分配的只存储文件内容的数据块,里面有一个个的基本数据块,对应着逻辑抽象的8块扇区,能够存储数据的大小为4KB。Bitmap的作用就是标记使用和未使用的数据块,在保存文件内容时直接遍历位图,查询bit位为0的位置,并记录下这个位置在位图中相对于起始位置的偏移量,在Data BLocks中直接根据这个偏移量索引找到空闲数据块。

inode Bitmap,inode Table,inode是一个结构体,保存着文件的各种信息,同时里面保存着一个inode编号,以及int block[15]。

我们可以根据这个block[15]数组来将文件的内容和属性对应起来,但是block看起来只能存储60KB的内容?文件不是可以很大吗?这个数组有15个下标,从0~14,其中0~11存储的LBA地址对应的数据块里,存放的就是文件内容,而下标为12,13的LBA地址对应的数据块里,存放的不是文件内容,他存放的仍然是LBA地址,而这个地址再去索引找到的数据块里,存放着的才是文件内容,我们也将这个过程叫做二级索引,但是即使这样,文件的内容依然不是很大,也就扩大了1024倍,也就是60KB*1024,那么最后一个下标14,他是三级索引,这样就再次扩大了文件大小,我们可以依据这样去存放更大的文件。

inode编号,在一个分区内唯一标识一个文件,关于inode编号,我们需要到后面串起来才能理解,这里暂时不做过多解释。

inode Bitmap,这个位图标识着哪个inode是空闲的,未存储文件信息。每一个分组中,都有一个inode起始编号,同时GDT中会保存着这个分组中可以保存多少文件信息,我们在创建一个文件时,操作系统会扫描一个分组的inode Bitmap,找到在位图中为0的位置,并记录下偏移量,那么这个文件的inode编号就是当前分组的起始inode编号加上这个偏移量。

在我们查询一个文件时,也是先由文件的inode编号确定分组,再由inode编号减去分组起始inode编号,剩下的值就是在位图中的偏移量,根据这个值我们可以在inode Block中找到文件的信息,并由int block[15]找到文件的内容。

其实这里有一个小点,就是我们怎么知道文件的inode编号?inode里虽然保存着inode编号,但是我要找的就是inode,我怎么知道inode编号?这个问题我们后面解释。

现在我们知道了文件在磁盘中的存储,现在,我们要打开一个文件了,怎么打开?写一个代码,编译链接成可执行,跑起来就打开了?表面上看起来的确是这样,现在我们深入来探究一下。

好,我们先来写一个代码:

我们以只读方式打开一个文件,并且读取他的数据,可是这些代码在整个流程中扮演了怎样的角色,底层到底是怎样完成这一切的?

头文件,里面保存着一系列库函数的声明,这个我们是知道的。库函数的实现自然就在库中保存着,在我们编译这个代码后,和库文件进行链接形成可执行程序。

接下来我们介绍库,库分为动态库和静态库,在Linux中,库默认是动态库,静态库需要我们自己下载,在Linux中,动态库的后缀是.so,静态库的后缀是.a。

库也是文件,是由一些.o目标文件打包形成的,那么为什么要打包? 我们假设,今天老师布置了一个任务,写一个程序,只要可执行程序和main.c就行,这在你看来非常轻松,你分分钟在一个项目工程中写了7个源文件,6个头文件,编译运行,通过了,非常轻松,但是你的同学他不会啊,就问你要源代码,但是呢,你辛苦写的,不想给他源文件,可是又不好意思不给,于是你转念一想,反正老师只要可执行和main.c,那好说,我把头文件和除main.o的其他.o文件发给他,他自己写一个main.c,编译链接一下就行了,于是你发给了他这些东西,于是你的同学拿到了这些头文件和.o文件,结合他自己的main.c,gcc main.c xxx.o xxx.o ...... -o xxx,写了一长串,可算弄完了。

但是如果是很大的一个工程项目,几百上千的文件,还这么编译,写都写烦了,于是,你又转念一想,那怎么办?那好,把.o文件打包成库吧,于是,这样你就自己搞了一个库,通过某种方式,你分别弄出了动静态库。

将来你的同学再次使用时,就很方便了。

我们写的代码也是如此,在下载好一个编译器时,他把一系列库文件也下载了下来,当我们写代码时,只需要包含头文件,头文件中函数的实现我们链接时找库要就行。

接下来到了FILE* fp = fopen...,fopen就是一个库函数,返回值是FILE*,我们直接看来,是这行代码他打开了文件,实际上不是的,而是可执行程序跑起来变成进程,进程执行到这行代码时,才打开了文件。

这里我们需要说到,事实上,fopen这种函数,是为了更好的给初学者使用,所以封装了这么一层,对于访问硬件这种事情,操作系统是不允许普通用户进行访问的,他会提供一个系统调用(也是函数,由C/C++编写,大部分是C),只有使用这个系统调用才能够进行访问,操作系统认为这才是安全的,因为这个函数是操作系统提供的,所以我们要想访问一个文件,就需要在fopen中封装访问硬件中文件的系统调用,而这个系统调用就是open()。

open这个系统调用,返回值是int类型的fd,叫做文件描述符,参数是文件路径和打开方式,以及一个模式:

文件路径似乎我们挺熟啊,是的,其实我们不熟,什么是文件路径?  /dir/dir1/dir2,是这样吗?这么说是没错,但是我们这里要谈到的路径,在磁盘中是没有这个概念的,也没有目录这样的概念,只有文件,只有操作系统将他们的信息加载进内存中,才形成了目录以及路径这个概念,那么现在我们来解释inode编号的问题。

目录是不是文件,是的,那么目录也有内容,他的内容保存什么呢?文件的inode编号和文件名的映射关系。

我们假设有这样一个路径:/dir/test/haha.c,那么haha.c的inode编号和他的文件名就保存在test目内容中,可是test目录也是文件啊,所以test的inode编号和文件名保存在dir内容中,dir也是文件,dir的inode编号和文件名也保存在根目录下/,那么根目录呢?他的inode编号是确定的,在操作系统加载后,他就确定了,我们之前提到过分区,操作系统管理文件不是通过分区吗?怎么会根据根目录呢?是的,根目录是挂载到分区上的,分区的访问,都是通过挂载到一个目录下进行访问的!

于是我们就可以根据文件路径逆向递归般的可以找到文件inode编号。

可是,我怎么找到文件的路径呢?通过进程,进程的路径是确定的,在运行起来时,就已经确定了,所以在我们查找一个文件时,如果我们给出文件的绝对路径,那么我们就使用这个绝对路径,如果没有给出,那么就使用这个进程的路径+文件名去查找这个文件。

于是,到这里,我们就清楚了路径的问题。

flag又是什么鬼?他是宏,通过一系列宏来决定打开文件的方式,只写,只读,读写,清空,追加等等,mode就是创建的文件的权限,决定了拥有者,所属组,其他人对文件的读写执行权限。

于是,通过这个系统调用,我们就打开了文件,但是怎么打开的呢?这里就提到文件描述符,我们说open这个系统调用返回了文件描述符,文件描述符又是什么?

这里提到stdin(标准输入),stdout(标准输出),stderr(标准错误),三者都是FILE*类型,这三个文件在进程启动时会自动打开,并默认分配0,1,2这三个文件描述符,保存在他们指向的FILE中的fileno这个值中。

流程是这样的,open打开一个文件,这个文件被从磁盘加载进内存中,创建struct file结构体,里面保存着这个文件的信息和内容,接着这个文件的地址被填入一个数组中,填入位置的下标就是文件描述符,接着这个下标被返回给open,open被封装在FILE中,于是这个fd给到了FILE中的fileno,最后由fopen返回这个FILE的地址,最终我们就打开了这个文件。

fwrite,fread等等函数,与文件相关的函数,都需要这个fd,所以这也就是为什么我们打开文件后返回的fp要传递给他们,因为它里面封装了fd。

当我们向文件中写入内容的时候,也并不是直接就写进了struc file结构体里,而是写进了一个缓冲区,这个缓冲区就是FILE里的一个buffer[],然后由write系统调用将这个缓冲区里的内容写进struct file里的缓冲区,再由操作系统刷新到磁盘。

fclose就是通过fd清空fd_arrry对应位置的内容,这个文件也就被关闭了。

现在我们解释了代码后,代码就要开始编译链接成可执行程序了,那么可执行程序内部是什么样子呢?我们对他进行反汇编,可以看到这样样一张图:

我们可以看到一些汇编语言,以及一些地址,事实上,这些地址就是我们在调试程序时所看到的地址,都是已经提前编译好的,这些地址也被叫做虚拟地址,注意,不是物理地址。

当程序要执行时,操作系统会为他创建一个进程控制块task_struct,然后将程序的代码和数据加载进内存,这两者结合起来就叫做进程。

当我们的可执行程序的代码和数据加载进内存时也就有了物理地址,此时操作系统会将他的物理地址和虚拟地址在页表中建立映射关系,并使用表头初始化进程地址空间。

如果程序链接使用的是静态库,那么在可执行程序在链接时就已经将所需代码从库中拷贝进来,在执行时就已经不需要这个库了,如果使用的是动态库,那么在执行时仍然需要将动态库加载进内存,下面这张图就是链接静态库的程序。

那么现在,我们来整体理一理这个过程:

首先,我们写好一份代码,代码里使用了操作文件的函数(如果没写,那其实解释起来更简单点),于是我们开始编译链接文件(假设我们链接静态库,这个说起来省事),在形成可执行程序文件后,这个文件里就有了一系列信息,被分成了几个段,代码段,数据段,以及表头等等,同时里面的代码和数据都会有相应的地址,在磁盘中我们称逻辑地址,加载进内存后我们称虚拟地址,其实二者是等价的;接着要开始执行这个程序,创建进程控制块,加载程序代码和数据,进程形成,页表构建完成,进程地址空间以及CPU内的寄存器初始化完成,开始执行我们的代码, 由于main函数地址在初始化时就将地址交给了cpu中的程序计数器,所以操作系统他是可以找到主函数入口的,当执行到打开文件的指令时(如果是读文件),首先根据我们提供的路径(绝对路径就使用,不是就拼接当前进程路径+文件名)逆向递归找到这个文件的根目录,也就找到了这个文件所在的分区,因为分区是挂载在这个根目录下的,而根目录的inode编号是确定的,于是我们可以从磁盘中读到根目录的内容,里面存放着他下面一系列文件的inode编号和文件名的映射关系,这样一路解析下去,我们就找到了我们要打开的文件;(如果是写文件,并且文件不存在),那么操作系统就先创建文件,在一个分区的一个分组下去创建一个文件,并返回inode编号,将这个编号和我们输入的文件名建立映射关系填到父目录内容中,一个文件也就创建好了。找到要打开的文件后,将要打开的文件读进内存中,建立struct_file结构体,里面填充这个文件的信息和内容,然后将这个结构体的地址填入到进程控制块中的struct files_struct *file指向的struct files_struct中的fd_arrry中,并返回这个填入位置的下标,于是后面对于文件的操作也就可以完成了。当代码全部执行完成后,释放文件,释放程序的代码和数据,接着释放进程控制块,程序执行完成。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/777024.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LLM: Prompt的使用

本文使用的LLM是OpenAI的gpt系列。刚开始学大模型,如果出现错误内容或着描述不恰当的部分,后续会修改。 Prompt是输入给大模型的文本或语句,用来引导大模型(Large Language Model, LLM)生成相关的输出。好的prompt可以提高大模型输出的准确性…

ubuntu系统下如何使用vscode编译和调试#小白入门#

编程环境:ubuntu系统为18.04.1,vscode版本为1.66.2 一、VSCode切换中文显示: 1、vscode安装完成后启动,在左侧externsions中搜索“简体中文”插件,并完成安装: 2、选择右下角齿轮形状的"Manage"&#xff…

YOLOv5全网独家改进: 红外小目标 | 注意力机制改进 | 并行化注意力设计(PPA)模块,红外小目标暴力涨点 | 2024年3月最新成果

💡💡💡本文独家改进:红外小目标涨点利器,在多个数据集下进行验证,其中并行化 patch-aware 注意力(PPA)模块,解决目标的大小微小以及红外图像中通常具有复杂的背景的问题点 💡💡💡红外小目标实现暴力涨点,只有几个像素的小目标识别率大幅度提升 改进结构图如…

centos7 安装后不能联网怎么解决

如果您在安装 CentOS 7 后无法连接到互联网,则可以尝试以下步骤来解决该问题: 确认网络连接是否正确配置 请检查您的网络连接是否配置正确。您可以通过运行以下命令来检查您的网络连接状态: 复制 ip addr show如果您看到类似于 enp0s3 或 …

pta L1-008 求整数段和

L1-008 求整数段和 分数 10 全屏浏览 切换布局 作者 杨起帆 单位 浙大城市学院 给定两个整数A和B,输出从A到B的所有整数以及这些数的和。 输入格式: 输入在一行中给出2个整数A和B,其中−100≤A≤B≤100,其间以空格分隔。 输…

Javascript作用域—Javascript基础篇

作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。 作用域可以分为以下几种类型: 全局作用域(Global Scope):全局作用域是整个…

算法---动态规划练习-7(按摩师)【类似打家劫舍】

按摩师 1. 题目解析2. 讲解算法原理3. 编写代码 1. 题目解析 题目地址:点这里 2. 讲解算法原理 首先,给定一个整数数组 nums,其中 nums[i] 表示第 i 天的预约时间长度。 定义两个辅助数组 f 和 g,长度都为 n(n 是数组…

【WPF应用21】WPF 中的 TextBox 控件详解与示例

在 Windows Presentation Foundation (WPF) 中,TextBox 控件是一个强大的输入控件,允许用户输入、编辑和选择文本。TextBox 控件在各种应用程序中都非常常见,例如表单、对话框和编辑器。本文将详细介绍 TextBox 控件的功能、使用方法、属性、…

python爬虫----python列表高级

小伙伴们,大家好!今天学习的内容是python列表高级。 1、添加元素 append:在列表末尾添加元素 A [xiaoWang, xiaoZhang, xiaoHua] print("添加之前,列表A的数据:", A)temp input(请输入要添加的学生姓名:) A.append…

Gartner 公布 2024 年八大网络安全预测

近日,Gartner 安全与风险管理峰会在悉尼举行,旨在探讨网络安全的发展前景。 本次峰会,Gartner 公布了 2024 年及以后的八大网络安全预测。 Gartner 研究总监 Deepti Gopal 表示,随着 GenAI 的不断发展,一些长期困扰网…

【项目技术介绍篇】若依管理系统功能介绍

作者介绍:本人笔名姑苏老陈,从事JAVA开发工作十多年了,带过大学刚毕业的实习生,也带过技术团队。最近有个朋友的表弟,马上要大学毕业了,想从事JAVA开发工作,但不知道从何处入手。于是&#xff0…

Android ViewBinding 使用

Android ViewBinding 使用 一、引言 随着 Android 开发的不断发展,对于视图绑定的需求也日益增长。ViewBinding 是 Android Jetpack 中的一个功能,它可以帮助开发者以更安全、更简洁的方式引用视图。本教程将详细介绍如何在 Android 项目中使用 ViewBi…

【Web应用技术基础】CSS(5)——表格样式

第一题&#xff1a;表格边框 .html <!DOCTYPE html> <html><head><meta charset"UTF-8" /><title>HTML – 简单表格</title><link rel"stylesheet" href"step1/CSS/style.css"></head><bod…

使用改头换面之后的ExoPlayer实现Android视频播放

使用 ExoPlayer 实现 Android 视频播放 概览 在 Android 应用开发中实现视频播放是一个常见需求&#xff0c;尤其在视频流和 IPTV 应用中更为突出。本文将介绍如何在 Android 应用中使用 ExoPlayer 实现高效、流畅的视频播放&#xff0c;内容将包括 Java 和 Kotlin 两种语言的…

2024年数字IC秋招-海康威视-数字逻辑设计工程师-笔试题

文章目录 前言一、多选题1、下面属于AXI定义的传输通道的是2、在SystemVerilog语言中&#xff0c;如果想要约束随机变量x在a和b之间&#xff0c;以下代码中正确的是?3、UVM的通信接口类型包括哪些?4、常见的代码覆盖率收集包括哪些?5、关于linux和windows操作系统的使用&…

Windows无法安装torch==1.4.0

在conda中&#xff0c;每创建一个虚拟环境&#xff0c;就要重新配置其中的pytorch 这次我创建的虚拟环境需要torch1.4.0的版本。 torch网址&#xff1a;https://pytorch.org/get-started/previous-versions/ 解决办法 按以下代码进行安装&#xff1a; pip install torch0.4.0…

JAVA HTTP大文件断点续传上传

功能&#xff1a;大文件上传&#xff0c;断点续传&#xff0c;文件夹上传&#xff0c;重复文件检测&#xff0c;离线进度信息保存&#xff0c;音视频信息读取 优势&#xff1a;无限授权&#xff0c;开源开放&#xff08;自主可控&#xff09;&#xff0c;长期维护 场景&#…

线程创建的几种方式

1.继承Thread类 class MyThread extends Thread {public void run() {// 线程执行的任务for (int i 0; i < 5; i) {System.out.println("Thread: " i);try {Thread.sleep(1000); // 使线程休眠 1 秒} catch (InterruptedException e) {e.printStackTrace();}}}…

unity中 鼠标按下移动端与pc端的位置

if (Input.GetMouseButtonDown(0)) { Vector2 V Input.touchCount > 0 ? Input.GetTouch(0).position : new Vector2(Input.mousePosition.x, Input.mousePosition.y); } 射线检测 if (Input.GetMouseButtonDown(0)) { …

动态规划入门(数字三角形模型)

备战2024年蓝桥杯&算法学习 -- 每日一题 Python大学A组 试题一&#xff1a;摘花生 试题二&#xff1a;最低通行费用 试题三&#xff1a;方格取数 试题四&#xff1a;传纸条 试题一&#xff1a;摘花生 【题目描述】 Hello Kitty想摘点花生送给她喜…