前景提要:一个程序的运行必不可少的两张表:1.命令行参数表;2.环境变量表
常量区和代码是只读的,在堆区里面地址是向上走,而栈区地址是向下走的,中间是有一个共享区的
例如:这个就会运行错误,首先str指向的是字符串的首地址h,但是这个字符串存储在常量区,因为常量区只能读不能写,所以编译的时候会出错
1.进程的地址空间:
首先想一个问题:一个父进程和一个子进程,它们运行起来,为什么打印出来的地址是相同的但是进程输出的val是不同的这是为什么?从下图的运行结果可以看出来:
对于这运行结果可以肯定的揭示一个问题就是平时我们写的程序用的指针,这个指针指的的地址一定不是物理地址!(都是虚拟地址)
在我们编写的一段程序里面,例如你定义的一些int a=0;这个程序会经过预处理-编译-汇编-连接,其实在你编译的时候,你程序中的这些变量都会消失,全部都会成为地址!
所以说所有的语言都是看不到内存的!
地址空间:
地址空间是操作系统为进程或程序抽象出的一个虚拟的内存地址集合,它为程序提供了独立且连续的内存视图,与实际物理内存相对应,方便程序的编写和运行。
看下图理解一下:
下面这个图可以帮助你看一看地址空间的地址实际情况:打印出来的情况
在地址空间中每个区域都是经过区域划分的,就如同课桌上的38线!互相不越界!
所以通俗的说,所谓的进程地址空间,本质是一个描述进程的可视范围大小,地址空间内一定要存在各种区域划分,对线性地址进性start和end既可!
2.增加进程虚拟地址空间:
可以让我们访问内存的时候,增加一个转换的过程,在这个转化的过程中,可以对我们的寻址请求进行审查,所以一旦异常访问,直接拦截,该请求不会到达物理内存,保护物理内存。
进程的地址空间其实也算是一个中间商,一个进程的完整运行中间和夹在这其他的东西。
3.页表:
页表主要是把地址空间的虚拟地址转为指向物理内存的物理地址!
<1>:页表可以起到一个很好的权限管理(看页表后格子的rw):过程就是当你进程开始运行的时候,你读取到一个虚拟地址,会进入cpu的cr3寄存器,寄存器里面存着一个页表,页表里面包含了一个虚拟地址和物理地址,和权限标志位,当你正确的进程页表会帮助你,获取正确的物理地址,如果你权限不对,则页表直接就会拒绝,也就是编译过程中出显的报错。这也就是为什么代码区和字符常量区是只读的,就是因为页表权限是只读
<2>:操作系统可以对大文件进行分批加载,当你发现你要找的代码和数据没有被加载到内存中,此时的操作系统就会触发一个关键叫做缺页中断(写时拷贝也是缺页中断)
当你创建一个进程,你需要是代码和数据等资源都没有被加载到内存中也是可以,当你的进程开始运行的时候,os会帮你自动构型加载,让你可以边使用边加载(它是从磁盘中给你的物理内存加载)然后你物理内存中在进程运行整个过程中需要申请内存、释放内存,和缺页中断,重新填充页表,加载都是linux内存管理模块在处理。进程是不知道的页表对应的地址是保存在cpu的cr3寄存器
<3>:进程切换因为一进程的地址空间是由pcb指针指向的,而地址空间里的虚拟地址,又指向cpu的cr3寄存器,寄存器里放的是页表对应的物理地址,通过这一连串的衔接,我们可以得出结论当你切换了pcb时候,你的一些列相关都会随着改变!这就是进程切换。
答案是:先要创建内核数据结构,即先要为该进程维护的pcb和地址空间和页表的对应关系处理好了以后,才会加载对应的可执行程序
所以到目前为止的知识,可以对进程重新定义一下:
什么叫做进程:进程=内核数据结构(task_struct&&mm_struct(地址空间)&&页表结构)+程序代码和数据
4.进程具有独立性怎么做到的?
我们每个进程都有自己的task_struct(pcb)、 地址空间、页表,所以在内核数据结构上我们进程之间是相互独立的加载到内存的代码和数据,因为在页表上虚拟地址相同但是物理地址可以不同的,我们只需要在页表上映射到屋里内存的不同物理地址上,此时每个进程代码和数据就互相解耦了,这就做到了进程之间互相独立了
让进程以统一的视角看待内存!
---------------------------------------------------------------------------------------------------------------------------------
1.进程控制
进程创建
fork 函数初识
在 linux 中 fork 函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回 0,父进程返回子进程 id,出错返回 - 1
当我们使用fork的时候操作系统会:1.分配新的内存块和内存数据结构给子进程;2.会把父进程的部分数据结构内容拷贝(写时拷贝)给子进程;3.添加子进程到系统进程列表当中;4.fork返回的时候,系统调度器开始调度
Fork常用做法:
- 一个父进程希望复制自己,让父子进程同时运行,例如父进程等待客户端请求,生成子进程处理客户端请求
- 一个进程要执行不同的程序,例如子进程从fork返回后调用exec函数
Fork调用失败原因:
- 系统中有太多进程
- 实际进程用户数超过了限制
下面展示创建五个子进程:
#include<unistd.h>
#include<stdlib.h>
#define N 5void run()
{int cnt=10;while(cnt){printf("i am child: [%d] , ppid: [%d]\n",getpid(),getppid());sleep(3);cnt--;}
}int main()
{for(size_t i=0;i<N;i++){pid_t id =fork();if(id==0)//首先这五个子进程都来同一个父进程!!!{run();exit(0);}}sleep(1000);return 0;
}
父子进程谁先运行是不可知的!由调度器决定!
进程终止
进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
一般而言:你运行的进程成功与否只有父进程会关心子进程!
因为是用户创建发送的创建的子进程请求,但是用户并不能直观的知道,子进程运行的成功与否会告知父进程,然会父进程会转交给用户!然后进行下一步的执行决策!
你可以通过查看你的进程退出码来确定是否成功!
这个指令是查看保存最近一次进程退出的时候的退出码!
这里的130是因为我在程序运行过程中中断了它!正常运行结束他应该是0!
我们也可以通过改return x的值,来设置多个进程判断是否成功!如下图:
- 代码异常终止
异常本质可能就是代码根本没有跑完!--所以进程的退出码就已经没有意义了!
进程常见退出方法
正常终止(可以通过 echo $? 查看进程退出码):
- 从 main 返回
- 调用 exit:这个函数在任意地方被调用,整个程序直接就停止了!
- _exit
exit 和_exit之间的区别:
(打印前一定是先进入缓冲区的!)
2.调试技巧:
可以通过strerror()来查看你所有的退出码的意思!
系统提供的错误码和错误码描述是有对应关系的!