进程地址空间

进程地址空间_第1张图片

  • 我最近开了几个专栏,诚信互三!
    ====> |||《算法专栏》::刷题教程来自网站《代码随想录》。|||
    ====> |||《C++专栏》::记录我学习C++的经历,看完你一定会有收获。|||
    ====> |||《Linux专栏》::记录我学习Linux的经历,看完你一定会有收获。|||
    ====> |||《C#专栏》::记录我复习C#的经历,深度理解查漏补缺,不定期更新。|||
    ====> |||《计算机网络专栏》::记录我学习计算机网络,看完你一定会有收获。|||

进程地址空间

  • 进程地址空间介绍
    • 页表初识
  • 为什么要存在进程地址空间和页表
  • 进程控制周边
    • 进程终止
    • 进程等待

进程地址空间介绍

可执行程序会被加载到内存中,和pcb形成了进程,在之前的介绍中,我们没有对进程的存储形式进行描述,而进程地址空间就是对进程在内存中的存储形式进行描述的一个数据结构。
所有可执行都要被加载到内存中,每个程序的大小,截然不同,为了我们更加方便管理进程,我们对进程进行了内存级别的划分。
进程地址空间_第2张图片

可以看到,所有进程所占有的内存空间都被划分成了以上几个部分,上述部分我们并不陌生,在我们进行编程的时候,我们就是按照上述规则划分内存的,但是真实的物理内存也是这样划分的吗?

答案是错误的,上述内存区域,以及我们在语言层面进行的如取地址的操作,我们所取出/操作的地址都是虚拟地址。 每个进程都维护一个系统级的页表,页表的作用之一就是将虚拟地址映射到真实的物理地址。

  • 这就能解释之前的一个困惑了,之前在讲述父子进程的时候,我们会发现如果我们在子进程中修改了变量,则子进程和父进程中获得的相同变量的值不同,但是地址却是相同的, 实际上只是虚拟地址相同,在子进程要修改变量时,会发生写时拷贝,当操作系统检测到我们要进行写入操作时,操作系统就会另外开辟一块空间,将父进程内的数据拷贝到子进程新开辟的空间中,然后对新空间进行写入操作,但是与物理地址对应的虚拟地址却没有发生变化,这就是为什么地址相同,但是值却不同的原因。
  • 在我们进行动态内存管理时,我们申请内存的函数/操作符,如malloc,new我们最先申请的也是虚拟内存,这是为了资源利用率最大化,如果直接申请物理内存,但却不即使使用,则必然会导致操作系统的空间利用率下降,所以在malloc,new时先只申请虚拟地址,在使用地址的时候操作系统会检测到虚拟地址无物理地址对应的错误,于是在申请物理地址。

页表初识

页表采用了哈希表这种数据结构,将虚拟地址一一映射到物理地址中,页表中存储的不止只有地址数据,还有权限,在每个地址数据后,都会存在一个权限,如r,w,rw,x等等,这些权限是决定我们能对该地址数据做什么,举个例子。

我们对一个字符串常量进行修改,这是不可以的,在之前的学习中,我们将其解释为字符串常量存储在常量区,常量区的数据无法修改,但在现在,我们就能对此有更深入的了解,我们不能对字符串常量写入数据的根本原因在于字符串常量数据没有写权限
同时,我们还可以深度理解const关键字,const关键字并非是将数据移至常量区,实际上被const修饰的数据如果我们进行了修改,则编译器会在编译过程中就会报错,若不使用const,则会在执行过程报错,const关键字将报错的环节提前了,让我们更容易察觉到错误

为什么要存在进程地址空间和页表

1.进程地址空间的存在让进程可以以统一的,有序的视角看待所有在内存中的进程实体。
2.进程地址空间和页表使得关于内存的资源管理和进程的资源管理耦合度降低了。
3.屏蔽了进程直接访问内存的方式,保护了内存的安全。(如若没有页表则可能导致进程非法访问内存)

进程控制周边

进程终止,要么处于阻塞状态,需要某种资源,要么处于僵尸状态,父进程没对该子进程pcb信息进行回收,为了更好的了解进程等待,我们需要了解进程终止。

进程终止

进程是加载到内存中的可执行程序,程序是由计算机语言写出来的,我们以C/C++为例,我们在写代码的时候,主函数内的return语句其实是该进程的退出码,我们一般返回0,这代表该代码没有发生错误,返回非0,则代表了有错误产生,所有的错误都被errno这个变量维护,每一个错误码,代表了一个错误。
在这里插入图片描述
我们可以打印以下每个错误码代表什么错误。
进程地址空间_第3张图片
main函数也是函数,对于其他函数来说,返回值代表错误码,可以通过返回值来判断该函数出现了 什么错误,同时也可以用返回值来传递该函数的运行结果。
还有一个函数,exit,该函数的作用是结束当前进程,和return 一样,返回值依旧是推出码。

我们可以使用echo $?来打印出最进的退出码。
进程地址空间_第4张图片

这是在代码执行结束的时候,及进程执行完毕的时候会报出的错误,其实有可能在进程未执行完毕时就会报出错误,这些错误都被维护成了各种宏,被称为信号编号,信号编号也是数字,每个数字代表一个运行异常。
通过kill - l,来列出所有的信号选项,我们之前所使用的9号信号就是杀死进程的信号。
进程地址空间_第5张图片
综上,进程的错误信息一共有两种,一种时退出码,一种时信号,父进程需要收集这些信息,以辨别其子进程是否正常运行结束。

进程等待

父进程往往需要收集子进程的信息,否则子进程就会变为僵尸进程,僵尸进程已经处于死亡状态,但是其pcb一直占用内存空间会造成资源浪费,我们如何让父进程收集子进程的信息呢?

wait函数,让进程处于等待状态,并且收集任意子进程的信息,返回值>0则代表pid为该值的进程回收成功,否则代表回收失败。
进程地址空间_第6张图片

进程地址空间_第7张图片
这是我们进行wait函数检测的函数,status可以带回错误信息,我们来看看结果。
进程地址空间_第8张图片

由结果可以得出,wait函数若收集信息成功确实是返回任意子进程的pid,但是在子进程中,我们设置的退出码明明是1,但是为什么status返回的值是256呢?

status要带回2个信息,一个是退回码,一个是信号编号,但是一个整形如何带回2个数值呢?
其实status是有格式的。
进程地址空间_第9张图片
status的前16位无用,后16位的前8个比特位代表退出码,退出码的后一个比特位代表core dump表示,最后7个比特位代表信号编码,这就能解释为什么status返回的是256了,代表没有终止信号,且退出码为1.

  • 我们可以通过位运算得到退出码和终止信号。
    1.status & 0x7f得到终止信号。
    2.status >> 8 & 0x7f得到退出码。
  • 我们一般不会这么做,C提供了两个宏函数WIFEXITED,WEXITSTATUS。
    进程地址空间_第10张图片
    WIFEXITED子进程未收到终止信号,退出,则该宏函数为真。
    WEXITSTATUS函数返回退出码。
    前五秒
    进程地址空间_第11张图片
    后五秒
    进程地址空间_第12张图片

waitpid函数与wait函数的差别在于第一个参数和第三个参数,第一个参数可以指定某一个子进程等待,第三个参数是一个选项,这个选项代表了父进程对子进程的状态。

option = 0,则代表父进程进行等待,直到收集到子进程才返回,是阻塞调用。
进程地址空间_第13张图片
该程序运行后,前五秒只打印前2个printf内容,因为当wait的option参数为0时,父进程会等待子进程,知道子进程结束。
前五秒
进程地址空间_第14张图片
后五秒
进程地址空间_第15张图片


option = WNOHANG这个宏时父进程会进行轮询访问,是为阻塞调用。
进程地址空间_第16张图片
运行后会打印出前两行printf,随后会打印父进程的while循环内的printf,并且此时child_pid=0,5秒后父进程收集到子进程,打印最后的结果。
五秒前
进程地址空间_第17张图片
五秒后
进程地址空间_第18张图片
该函数的返回值代表了几个方面,首先返回0代表函数调用成功,但是没回收到子进程,返回大于0的数,代表回收到子进程了,小于0代表函数没调用成功

你可能感兴趣的:(开发语言,linux)