Linux进程虚拟地址,进程终止,进程等待

1.环境变量

环境变量是系统为我们准备好的,我们的环境变量具有全局属性,所有进程都能拿到环境变量,这是因为我们的子进程会继承父进程的环境变量,比如我们的PATH就可以让我们无需具体目录就可以执行一些命令,ls,pwd等。下面我们进一步来讲解环境变量继承的具体实现,和进程虚拟地址的了解。

2.进程虚拟地址

        我们前面创建子进程的时候发现创建子进程,返回的pif_t,父进程和子进程的值不一样,但是打印出它们的地址是一样的,这是因为我们使用的都不是实际的物理地址,而是虚拟地址。

问题就来了:怎么一个地址相同的值,打印出来的值会不一样呢?

虽然我们不知道具体是怎么回事,但是我们可以确定我们所谓的地址一定不是我们实际的物理地址,因为如果我们使用的是实际的物理地址,值不可能不一样。我们历史上学的所有的地址都是虚拟地址而不是物理地址。

所以我们引入虚拟地址空间。

我们以前学习的堆区,栈区,代码,未初始化变量,初始化数据的地址都是在我们虚拟地址空间的,还有函数调用我们实际上是调函数的地址,这个地址也是虚拟的,我们的虚拟地址空间在PCB里,进程在进行内存访问的时候,不能直接去访问内存的地址,而是要先通过页表,将虚拟空间的地址映射到物理内存,然后再进行数据的访问。

页表就和我们的哈希表类似吧,一一映射的关系,key_value关系的映射,有点不准确,但可以先这么理解!

前面我们学过一个东西是,父进程和子进程是相互独立的。是怎么做到的呢?

其实我们父进程创建子进程的时候,它会把父进程的虚拟地址空间拷贝过去,包括页表也会拷贝过去,这个时候我们的子进程如果要做修改,其实不是修改虚拟地址空间,而是对页表的映射关系做修改,让页表映射的物理内存改变就可以做到,虚拟地址空间一样但是我们的值却不一样了,因为我们页表的映射关系变了,值自然不一样,我们现在就可以理解为什么一个值的地址一样但却有2个不同的值的原因了。

我们把这种修改页表映射关系的过程叫做写实拷贝,这个操作全程由操作系统完成,用户并不知道,完成这个过程需要开辟空间,拷贝内容,更改映射关系,那这里为什么还要把原来的值拷贝过去呢?这是因为我可能写出这样的代码啊:a++。如果我不知道原来a的值就无法++了,这是我们把原来旧的值拷贝过去的原因。

我们定义的变量其实都有地址,我们对我们的id进行写入的时候,不就会发生写实拷贝吗?

3.如何理解空间划分?

        我们的操作系统管理我们的进程,它会给我们的进程画饼,比如操作系统的内存是4GB,操作系统会让进程以为内存的全部空间都属于自己,所以每个进程都有自己的堆区,栈区等等,这是进程觉得自己拥有全部的内存,所以对自己的内存进行了规划,每个进程彼此之间都是这么以为的,进程虚拟地址空间是操作系统给进程画的饼,那么问题来了,饼需要被管理起来吗?

当然要啊,如果不管理每个进程的空间,怎么进行页表映射呢?那么怎么管理呢?先描述,再组织啊,我们先对饼进行描述,再用数据结构把它们组织起来。

怎么描述?

这就要提到空间划分了,我们知道38线吗?在桌子上画一个线,线的左边是张三的,线的右边是小美的,我们只需要知道区域的开始和结尾,那么在区域开始和区域结束中间的都是我的空间。

所以我们会定义int start,int end来进行区域划分,扩大区域和缩小区域就是就end和start做修改。

区域划分:本身是就有区域开始的位置和区域结束的位置即可。

怎么来证明呢?

虚拟地址空间,本身就是一个结构体。

在PCB里,我们有一个名为mm_struct的结构体就是我们的虚拟地址空间,我们想要访问用户空间可以通过地址直接访问,但是要访问内核空间必须要进行系统调用才可以访问。

所以,到现在为止,怎么理解进程是独立的呢?

进程=内核数据结构+代码和数据,我们父进程创建子进程的时候,子进程会把父进程的内核数据结构拿过去,这个时候它和父进程用的是同一块空间,但是当子进程进行数据的修改时,操作系统会页表的映射关系,不会让子进程去改父进程的数据,发生写实拷贝,这个时候我们读的时候数据是一样的,写的时候发生写实拷贝,通过这种方式我们实现了进程独立。

我们得出结论1:全局变量,全局有效。

这是为什么,这是因为全局变量在我们的初始化数据区,,它和地址空间是绑定的,只要地址空间存在,全局变量就存在,当我们进程结束,销毁PCB时,全局变量也会被销毁。

结论2:字符串变量其实和代码是编译在一起的,都是只读的。

我们的代码区的数据是只读的,不可以进行修改的,如果我们要进行修改,代码会报错。这是怎么做到的,字符串常量被页表转化时,会有权限约束,不让写入操作进行转换。如果发现进行转换,操作系统会直接杀掉这个进程,因为进程越界了。

我们在字符串常量前加const就是去约束编译器。

我们对我们的虚拟地址空间描述完了,那是怎么进行组织?

4.为什么要有虚拟地址空间

        理由1:因为有了虚拟地址空间,就必须转换为物理地址才可以进行内存访问。

要访问内存必须先进行转换,计算机中的问题都可以通过添加一层软件层进行解决,这个内存转换相当于添加了一层软件层,在你进行虚拟地址转换的时候我们就可以对你进行安全检查,保证物理内存的安全,维护进程的独立性、

        理由2: 让无序变有序,进程看到自己的代码和数据都是有序看待,而我们的可执行程序加载到内存都是随机加载的,可以在内存的任意位置。

        理由3:可以进行进程管理和内存管理的解耦合。

创建一个进程的时候是现有PCB还是先有代码和数据,我们加载的时候一定要立即进行进程的代码和数据加载吗?不一定啊,我可不可以一行都不加载,等你要执行的时候边执行变加载,这个叫惰性加载,让你不执行的时候不占用内存,提高内存的使用效率,我们加载到内存就必须进行内存申请,这样进程管理就和内存管理解耦合了。

我们的写实拷贝也是一种惰性申请,等到你要修改数据的时候再修改页表的映射,在这之前, 数据的读取和父进程是读同一份的。

        

我们的进程地址空间,在父进程创建子进程时,页表的权限被改为只读,然后子进程复制过去的页表也是只读的,当你对子进程进行写入时,操作系统会报错,如果是真的权限越界,它会杀掉进程,如果是正常的写入,操作系统会进行写实拷贝,并且更改权限。

问题是为什么创建子进程之后不直接把它们的数据分开,直接拷贝不行吗?为什么要写实拷贝?

还有对子进程进行修改为什么还要把原来的值拷贝过来,直接写入新的值不行吗?

首先我们进行父进程和子进程读取时用同一块空间本质是按需获取,读取你就用父进程的就可以,反正你两也一样,等到你需要对某个进程进行修改的时候,在进行写实拷贝,修改页表的映射关系就可以了,节省了资源,实现了按需获取。

为什么要把原来的值拷贝过来,还是a++你需要原来的值啊!

扩展问题:从今天开始,在C或C++上new或者malloc申请开空间的时候,需要在物理内存里面开辟空间吗?

不需要,我们只会给你开虚拟地址空间,并不会在内存中给你申请空间,只有你要用到这个空间的时候,才会给你做内存级申请吗,这个过程由操作系统完成。对于用户来说是不知道的。这个是惰性申请,也是提高了内存的利用效率。

我们的fork的目的只有2个,一个是父进程复制自己,另一个是子进程去执行一个程序。

但是fork是有可能失败的,原因可能是:系统中有太多的进程,还有是实际用户的进程超过了限制,比如一个用户操作系统只允许你开50个进程,你就只能开50个,不能超过这个限制。

5.进程终止 

        问题是:进程终止操作系统要做什么?return 0到底return给谁了?

我们可以看到在我们Linux源码中,PCB里面存有进程里的退出码,我们还记得僵尸进程吗?进程退出的时候不会立刻全部释放,它还需要父进程来回收它的PCB里的一些数据,看一看它任务完成的怎么样,这样进程才会被彻底释放,我们的return被父进程拿到,用来让系统判别该进程的执行情况,为什么我们的C/C++中会用到return 0呢?这个0其实是表示我的程序执行成功,退出了,这个0主要是被父进程拿到的,判断这个子进程执行的怎么样。

为什么一定要有退出码?这是因为子进程把父进程交给他的任务办的怎么样,父进程得知道啊,,0是成功了,非0就是失败,失败我得知道他失败的原因。

echo $?可以在Linux中可以拿到上一个进程执行完毕后的退出码。

进程终止无非是三种情况:

代码跑完,任务完成。

代码跑完,任务未完成。

代码未跑完。

当代码异常了,退出码就没有意义了。这个时候为什么会异常,会发出信号编号,管理者OS需要知道,一般情况下代码异常,都是直接杀掉这个进程。

我们的退出码有3种方法。

exit和return的区别是我们return 是返回,只有在main()函数中return 进程才会退出,但是我们的exit在任意函数中使用进程都会直接退出,并会带上相应的退出码。

我们的exit是库函数,_exit是系统调用,exit的底层实现一定是调用了系统调用_exit了。我们前面进行进度条实现的时候,我们了解了相应的缓冲区刷新,exit进程终止会自动刷新缓冲区,但是我们的系统调用_exit直接终止进程不会刷新缓冲区。

我们使用的最佳实践是一般要去调用exit,因为调exit底层也会调系统调用_exit,exit在库里,库有库缓冲区,我们知道我们的操作系统软硬件结构是层状的,系统的_exit在系统调用,它不会刷新我们的缓冲区,但是我们的_exit会刷新缓冲区,说明缓冲区不再系统调用层面,而是是库里。

6.进程等待

        1.进程为什么等待?

还是那个子进程退出的时候,父进程不管不顾,子进程会成为僵尸进程,占用内存空间,进而造成内存泄漏。

进程成为僵尸进程,kill -9也无法终结掉它,因为谁也没有办法杀死一个已经死去的进程。

最后我们父进程派给子进程的任务,我们需要知道子进程完成的怎么样,结果是对还是不对。

父进程可以通过等待的方式回收子进程的资源,获得子进程的退出信息。

如何判定子进程任务完成的怎么样,退出码可以知道!

        2.进程等待是什么?

        让父进程通过等待的方式,回收子进程PCB,如果需要,获得子进程的退出信息。

        3.进程等待怎么办?

多进程的回收中,父进程往往最先创建,最后退出。

我们实践中,一般不用wait,它的功能比较单一,只返回s退出码和退出信号,而且只支持阻塞等待,不支持轮训等待。

我们一般用waitpid,它支持等待任意的进程,也支持等待指定的进程,他也会返回退出码和信号。

而且它支持阻塞等待,也支持非阻塞轮询。

我们的status里面有子进程的退出码和终止信号,前8位是退出码,后7位是终止信号,通过它我们可以知道进程的代码是否跑完,是否成功,还是代码未跑完出异常,它的异常信息和退出码我们都可以拿到。

我们的进程未出异常它的信号数字是0,非0就是出异常了,OS给你发信号,我们可以通过OS发送的信号知道进程是哪里出异常了。

所以通过这个操作,我们知道父进程会处理子进程死掉后的僵尸进程,我们也知道僵尸进程的内核数据结构里保存了一个status里面通过16个bit位,包含了我们的子进程执行情况,退出码和退出信号我们都可以拿到,我们的父进程通过status就可以知道子进程执行的怎么样,设计的非常巧妙。

我们进程等待的最佳实践是:waitpid,我们的阻塞等待是父进程啥也不干就在那里等子进程退出,拿到它的退出码,但是我们也可以不去等,我们也可以直接检查子进程有没有退出,不管他有没有退出我们都会立即返回,退出拿它的退出码等信息,未退出立即返回,去做其他事情,通过多次检查的方式我们父进程不会只在那里等待子进程,这个时候我们的父进程也可以去做其他的事,一直在那里等叫做阻塞等待,多次检查是阻塞轮询。非常的形象啊!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值