我们都运行过程序,但是程序到底是什么,程序是如何运行的?
程序只是一段可执行的代码文件,通俗的说在linux上就是文件。
然后程序运行时就被称为进程,即进程是运行状态的程序。
程序储存了一系列信息的文件,这些信息描述如何在运行时创建一个进程。包含如下:
二进制格式标示:描述可执行文件的元信息,内核利用该信息解释文件中的其它信息。
机器语言指令:对程序进行编码。
程序入口地址:标示程序开始执行的起始指令位置。
数据:程序所包含变量的初始值和程序所使用的字面常量值。
符号表和重定位表:描述程序中函数和变量的位置及位置。
共享库和动态链接信息:列出程序运行时需要使用的共享库以及加载共享库的动态链接器的路径名。
其它信息:描述如何创建进程。
内核在加载程序的时候会为其分配一个唯一标识符即进程号,linux内核限制进程号需要小于等于32767.
每当创建一个进程的时候内核会顺序将下一个可用进程号分配给其时候。
当进程号大于32767时,内核会重置进程号计数器。然后从下位开始重新分配。
因为内核会运行一些守护进程和系统进程,所以一般会预留一些进程号给这些程序使用,所以一般从300开始重置。就类似于端口号1-1024之间时不会被分配给程序。
在64位系统中进程号可以更大。
每个进程都有一个父进程,除了一个叫做init的系统进程(进程号为1),该进程由内核负责初始化
该进程是所有进程的始祖,如果一个用户程序启动时,那么由init负责其初始化。
可以使用pstree来查看进程树
父进程和子进程呈现出一种树状关系,这是在mac os下执行pstree的结果,mac os下的初始进程为launchd等同于init
进程的内存布局:
进程内存由多部分组成:
text(文本段):程序代码
data:包含了初始化的全局数据和静态数据
bss:未初始的全局数据和静态数据
heap:用户分配的内存区 由 malloc分配
stack:包含栈帧,每个栈帧中存储了函数的局部变量。
注意这里说的是虚拟地址,实际进程的地址空间并不是真正的物理地址空间,而是采用了虚拟空间映射产生的。
在linux32位操作系统中,会为每个进程分配4G的独立内存空间,这里就出现一个问题,计算机的虚拟内存只有4G那么多个进程的话不是早被用光了
解决这一问题的方式通过页表映射到实实际的物理内存中去。
在上表中,进程的内存空间按照text,data,bss,heap,stack分布有低到高分配地址空间。但是只增长到0xC00000000,这是因为最后的一部分内存被分配给了内核。0~3G内存被分配给用户使用称为用户内存,3~4G内存为系统内核内存区。
图中非灰色部分都会创建页表来管理虚拟内存。
系统定义了三个全局符号可以获得text,data,bss段结束处下一字节地址。
extern etext,edata,end;
#include <stdio.h>
/*
* etext:end of text address
* edata:end of init data address
* end: end of uninit data address
*/
extern char etext, edata, end;
int doCal(int x) {
return square(x, x);
}
int square(int x, int y) {
return x * x;
}
int main(int argc,char** argv) {
int i = 2;
printf("calculate:%d\n", doCal(i));
printf("etext:%ld\tedata:%ld\tend:%ld\n",(unsigned long)&etext,
(unsigned long)&edata,(unsigned long)&end);
return 0;
}
结果如下:
calculate:4
etext:4195917 edata:6295616 end:6295624
为了进行对比,我们增加bss中的未初始化变量的个数,可以看到,确实是增长了的。
#include <stdio.h>
/*
* etext:end of text address
* edata:end of init data address
* end: end of uninit data address
*/
extern char etext, edata, end;
int doCal(int x) {
return square(x, x);
}
int square(int x, int y) {
return x * x;
}
int a = 0;
static int b = 0;
int d;
static int e;
int main(int argc,char** argv) {
int i = 2;
printf("calculate:%d\n", doCal(i));
printf("etext:%ld\tedata:%ld\tend:%ld\n",(unsigned long)&etext,
(unsigned long)&edata,(unsigned long)&end);
return 0;
}
结果:
calculate:4
etext:4195917 edata:6295616 end:6295640
这里看到edata并没有增长,但是我明确的给其赋值为0,所以这里就有一个概念即初始赋值0等同于未初始化对于全局变量。
同时end增长了16 考虑到 int类型的字长为32位,即4byte,那么4 * 4 = 16;校验正确。
这里再稍做改动:
#include <stdio.h>
/*
* etext:end of text address
* edata:end of init data address
* end: end of uninit data address
*/
extern char etext, edata, end;
int doCal(int x) {
return square(x, x);
}
int square(int x, int y) {
return x * x;
}
int a = 3;
static int b = 3;
int d;
static int e;
int main(int argc,char** argv) {
int i = 2;
printf("calculate:%d\n", doCal(i));
printf("etext:%ld\tedata:%ld\tend:%ld\n",(unsigned long)&etext,
(unsigned long)&edata,(unsigned long)&end);
return 0;
}
结果如下:
calculate:4
etext:4195917 edata:6295624 end:6295640
这里我给a,b明确赋值为3,那么这些值就会成为初始化数据段的内容,2 * 4 = 8 结果也符合校验。
这里我在给正文段text进行一些改变:
#include <stdio.h>
/*
* etext:end of text address
* edata:end of init data address
* end: end of uninit data address
*/
extern char etext, edata, end;
int doCal(int x) {
return square(x, x);
}
int square(int x, int y) {
return x * x;
}
int a = 3;
static int b = 3;
int d;
static int e;
int main(int argc,char** argv) {
int x = 1;
int y = 1;
int z = 1;
int i = 2;
printf("calculate:%d\n", doCal(i));
printf("etext:%ld\tedata:%ld\tend:%ld\n",(unsigned long)&etext,
(unsigned long)&edata,(unsigned long)&end);
return 0;
}
结果如下:
calculate:4
etext:4195933 edata:6295624 end:6295640
这里正文段也是增长了,所以符合预期期望。
这里再来介绍一下栈,这里的栈是用户栈,栈里保存着程序的栈帧,栈帧包含变量信息。
就像之前的那个程序一样,当程序开始执行时,开始了一个main的函数调用,然后调用到doCal函数,然后doCal调用到了square函数。
这个过程中,栈是增长的,这里的增长只是逻辑上的增长,实际物理地址是减小的。
square调用结束之后,那么栈就将square的栈帧出栈。直到main函数也出栈,至此,程序运行结束。
这里说到的虚拟内存管理技术是基于以下两个局部性的基础:
1.空间局部性:程序倾向于访问最近访问过的内存地址附近的内存,因为指令是顺序的。
2.时间局部性:程序倾向于不久的将来再次访问最近访问过的内存,因为循环语句。
正是因为局部性特征,所以即是仅有部分地址存在于ram中,也依然可以执行。
这里说的物理内存和RAM都是指的的内存条上的概念
虚拟内存的规划之一就是将每个程序所使用的内存分割成小的,固定大小的“页”(page)单元,同时也将RAM划分成与虚拟页尺寸大小相同的页帧,程序当前正在执行的部分构成的驻留页帧,称之为驻留集。而未使用的页则保存在交换区。若进程要访问的内存并未驻留在物理内存中,那么将会从硬盘中载入到内存。
所以为了管理这种结构,内核需要为每一个进程维护一张页表,该页表中的每一页表示程序的虚拟内存空间位置,要么是在RAM中,要么是在硬盘中。
虚拟内存管理使得进程与RAM物理地址隔离,这就带来了以下好处:
1.进程都有独立的内存空间,都可以使用到完整的内存空间,进程与进程之间,内存与内核之间隔离。
2.适当情况下,两个进程可以共享内存。
3.无需关注实际的RAM物理布局,专注于开发。
4.驻留在内存中的仅是程序的一部分,可以加速程序加载和运行。