linux内核—从可执行文件到进程启动

本文参考: https://www.jianshu.com/p/84d96a6385b0
https://zhuanlan.zhihu.com/p/148426554?from_voters_page=true
https://www.cnblogs.com/qscfyuk/p/11697816.html
https://www.cnblogs.com/icecri/p/4438351.html

一、概念

1.可执行文件
可执行文件属于elf文件,进程加载程序就是加载可执行文件,可执行文件包含很多section,如以代码段为代表的权限为可读可执行的段;以数据段和BSS段位代表的权限为可读可写的段;以只读数据段位代表的权限为只读的段,进程创建时所加载的程序信息即为这些部分。Linux中保存节的时候是以“页”为单位的(物理内存也同样是分页的,他们一一对应,一页一般是4096字节,也就是4k),不够一页的也要占用一页的空间;
ELF可执行文件默认加载到内存0x8048000这个位置,从这个位置开始加载。前面加载ELF可执行文件的头部信息,但因不同文件大小不同,程序的实际入口为:0x8048x00,图例为0x8048300,也就是说这个位置是程序的实际入口地址,即刚加载过可执行文件的进程(一个进程加载了新的可执行文件之后,开始执行的入口点),就是从这个地方开始执行

elf文件相关详见:https://www.cnblogs.com/qscfyuk/p/11697816.html

2.进程虚拟地址空间
每个进程都有自己4G(32位系统)的虚拟地址空间,实际上并不是每个进程在磁盘或内存上有这么大空间,而是进程的可寻址空间为4G(32位系统,寻址范围为0x0000 0000~0xFFFF FFFF)
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存的使用也更有效率。当处理器读取或写入内存位置时,都会使用虚拟地址。在读取或写入操作过程中,处理器会将虚拟地址转换为物理地址。
优点:
程序员无需操心如何存储数据或者程序等内容
程序可以使用一系列连续的虚拟地址来访问物理内存中不连续的大内存区域,用户看到的是连续地址,而无需关心更底层物理地址的排布。
通过使用虚拟内存,程序可以使用大于实际可用物理内存的空间,当物理内存不够用时,操作系统会将物理内存页保存在磁盘文件,数据页或者代码页会根据需要在物理内存和磁盘之间移动。
不同进程使用的虚拟地址彼此隔离,用户无需担心会影响到其它程序内存地址中的数据,操作系统的内存管理模块会将虚拟地址映射到物理地址。
3. 一个进程在内存中主要占用了以下几个部分,分别是代码段、数据段、BSS,栈,堆,等参数。其中,代码、数据、BSS的内容是可执行文件中对应的内容,加载程序并不是把它们的内容从可执行程序中填充到内存中,而是将它们的信息(基地址、长度等)更新到进程控制块(task_struct)中,当CPU第 一次实际寻址执行的时候,就会引起缺页中断,操作系统再将实际的内容从可执行文件中复制内容到物理内存中。
堆的内容是程序执行中动态分配的,所以加载程序 只是将它的起始地址更新到进程控制块中,执行过程中遇到动态分配内存的操作的时候再在物理内存分配实际的页。参数区在新进程加载的时候要存入环境变量和命令行参数列表。栈在程序加载时候存入的内容就是环境参数列表和命令行参数列表的指针和命令行参数的个数。

参考:https://blog.csdn.net/weixin_29058331/article/details/113368011?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-4.control&dist_request_id=1328626.10471.16153736270096707&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-4.control

4. 页表
可参考《操作系统精髓与设计原理》

二、进程创建

在这里插入图片描述

1.创建task_struct进程描述符
2.申请mm_struct内存描述符
内存描述符即进程地址空间,也就是我们所说的4G虚拟内存,从名称可以看出来这个结构体是用来描述内存的,而并不是真的给他开辟了4G的内存空间。
3.加载可执行程序
可执行程序分为代码段、数据段等部分,系统加载时则分别给这些段信息(并不是把可执行文件实际的段内容全部拷贝过来,只是该段的信息,可以标志该段的位置就可以)创建一个vm_area_struct结构体(虚拟内存区域),结构体中的vm_start/vm_end分别指向该段需要映射到虚拟内存区的哪个区间,如上图每个vm_area_struct都指向右边虚拟内存区域的一个区间。
ELF可执行文件默认加载到虚拟内存0x8048000这个位置,从这个位置开始加载。前面加载ELF可执行文件的头部信息,但因不同文件大小不同,程序的实际入口为:0x8048x00
这一步相当于建立的虚拟内存与可执行文件的映射关系

三、进程执行

经过步骤二,进程已经将可执行程序的信息加载到了自己的结构体中,那么程序运行时是如何把要执行的数据加载到物理内存呢?
1.页表、页框
虚拟内存和物理内存都是基于分页的,也就是都被分为一个个4k大小的区域,虚拟内存上的这些区域称为页表,物理内存上称为页框;具体原理不细讲
2.运行过程
程序被调度时,系统会给该进程分配一些页框,并加载对应的可执行文件进去,进程开始执行,当程序执行到某个地址时,发现页框(此处设计根据虚拟地址计算实际物理地址的内容)里并没有即将要执行的那些数据(也就是当前页框里的部分程序都执行完了),需要将接下来的可执行程序部分加载进来,此时会触发缺页中断,然后系统就会将接下来的可执行程序加载到页框中继续执行
一个进程在内存中主要占用了以下几个部分,分别是代码段、数据段、BSS,栈,堆,等参数。其中,代码、数据、BSS的内容是可执行文件中对应的内容,加载程序并不是把它们的内容从可执行程序中填充到内存中,而是将它们的信息(基地址、长度等)更新到进程控制块(task_struct)中,当CPU第 一次实际寻址执行的时候,就会引起缺页中断,操作系统再将实际的内容从可执行文件中复制内容到物理内存中
堆的内容是程序执行中动态分配的,所以加载程序 只是将它的起始地址更新到进程控制块中,执行过程中遇到动态分配内存的操作的时候再在物理内存分配实际的页。参数区在新进程加载的时候要存入环境变量和命令行参数列表。栈在程序加载时候存入的内容就是环境参数列表和命令行参数列表的指针和命令行参数的个数。
缺页中断:
在这里插入图片描述
在这里插入图片描述

引用其他文章

1)在shell界面输入./可执行文件名
经shell分析,该参数非shell内建命令,则认为是加载可执行文件。于是调用fork函数开始创建新进程,产生0x80中断,映射到函数sys_fork()中,调用find_empty_process()函数,为新进程申请一个可用的进程号。
2)为可执行程序的管理结构找到存储空间
为了实现对进程的保护,系统为每个进程的管理专门设计了一个结构,即task_struct。内核通过调用get_free_page函数获得用于保存task_struct和内核栈的页面只能在内核的线性地址空间。
3)shell进程为新进程复制task_struct结构
为可执行程序复制了task_struct后,新进程便继承了shell的全部管理信息。但由于每个进程呢的task_struct结构中的信息是不一样的,所以还要对该结构进行个性化设置(为防止在设置的过程中被切换到该进程,应先设置为不可中断状态)。个性化设置主要包括进程号、父进程、时间片、TSS段(为进程间切换而设计的,进程的切换时建立在对进程的保护的基础上的,在进程切换时TSS用来保存或恢复该进程的现场所用到的寄存器的值)。这些都是通过函数copy_process来完成的。
4)复制新进程页表并设置其对应的页目录项
现在调用函数copy_mem为进程分段(LDT),更新代码段和数据段的基地址,即确定线性地址空间(关键在于确定段基址和限长)。接着就是分页,分页是建立在分段的基础上的。
5)建立新进程与全局描述符(GDT)的关联
将新进程的TSS和LDT挂接在GDT的指定位置处。(注:TSS和LDT对进程的保护至关重要)
6)将新进程设置为就绪状态
7)加载可执行文件
进入do_execve函数之后,将可执行文件的头表加载到内存中并检测相关信息。加载执行程序(讲程序按需加载到内存)。

猜你喜欢

转载自blog.csdn.net/chengcheng1024/article/details/114669992