【Linux操作系统】进程概念

目录

  • 一、进程概念
    • [1.1 什么是进程](#1.1 什么是进程)
  • 二、task_struct内容分类
    • [2.1 标识符](#2.1 标识符)
    • [2.2 进程状态](#2.2 进程状态)
      • [2.2.1 进程排队](#2.2.1 进程排队)
      • [2.2.2 关于进程状态的表述------运行、阻塞、挂起](#2.2.2 关于进程状态的表述——运行、阻塞、挂起)
      • [2.2.3 Linux中具体的进程状态](#2.2.3 Linux中具体的进程状态)
      • [2.2.4 孤儿进程](#2.2.4 孤儿进程)
    • [2.3 进程优先级](#2.3 进程优先级)
  • 三、Linux的调度与切换
    • [3.1 进程切换](#3.1 进程切换)
    • [3.2 进程调度](#3.2 进程调度)
  • 四、环境变量
    • [4.1 main函数------命令行参数](#4.1 main函数——命令行参数)
    • [4.2 环境变量](#4.2 环境变量)
  • 五、地址空间
    • [5.1 内存分布](#5.1 内存分布)
    • [5.2 进程地址空间](#5.2 进程地址空间)

一、进程概念

1.1 什么是进程

常见的说法中,有:加载到内存中的程序是进程或者正在运行的程序是进程。

程序要先加载到内存中去,而且不一定只有一个程序,可以有多个程序加载到内存中。多个程序加载到内存中,那么操作系统要对它们进行管理,管理方式是:先描述,再组织。

先描述:某一程序加载到内存中,操作系统首先是对它的属性信息进行描述,有:

每个加载到内存中的程序都要有这样的描述,这个描述的结构体对象叫做PCB,process ctrl block进程控制块。

所以PCB是什么? 是描述进程的属性信息的结构体对象。

为什么要有PCB? 目的是为了让操作系统对进程进行管理。

再组织:操作系统不是直接对加载到内存中的程序进行管理,而是对指向对应的程序的PCB进行管理,就好比学生刚来学校,校长不认识,但是可以通过学生的信息对他进行管理。这些PCB是结构体对象,可以看作是链表的每个节点,所以对这些PCB对象的管理就相当于对链表这个数据结构的管理(增删查改)。

回到第一个问题:什么是进程? 进程不是加载到内存中的程序本身,也不只是程序对应的PCB对象,而是PCB对象和程序共同组成的整体。总结:进程=内核PCB对象+可执行程序。PCB对象可以用到任何数据结构中,不只是链表,所以:进程 = 内核数据结构+可执行程序。

二、task_struct内容分类

PCB是操作系统学科的叫法,Linux中叫task_struct

task_struct的所有属性一览:

  • 标识符:描述该进程的唯一标志,用来区别其他进程
  • 状态:任务状态、退出代码、退出信号等
  • 优先级:相对于其他进程的优先级
  • 程序计数器:程序中即将被执行的下一条指令的地址
  • 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
  • 上下文数据:进程执行时处理寄存器中的数据

2.1 标识符

标识符是进程的id,即进程的编号,每个进程都有自己的编号信息。用户如果想知道该进程的pid怎么办呢?首先要清楚,pid是task_struct的一个属性,而task_struct是由操作系统管理的,所以pid等属性信息是在操作系统内核中的。这里我们先用查看进程状态的指令来查看进程的pid。

ps ajx | head -1 && ps ajx | grep myprocess查看某个程序的进程:

这里的程序所写的代码是while死循环,方便用指令取查看进程,如果没有用while循环一直让进程跑下去的话,进程刚创建出来就结束了。进程是有生命周期的,并不是说刚创建出来然后一下没了,就没有进程,它也有运行起来,只是生命周期短而已。

通过图发现,第一个是我们运行起来的进程,但是第二个是啥?要说明的是:一切的指令运行起来也是进程。所以这里显示的一个是我们的程序myprocess,另一个是指令grep。

去掉指令grep显示的信息:ps ajx | head -1 && ps ajx | grep myprocess | grep -v grep

前面说进程的信息是在操作系统内核中的,用户怎么访问操作系统中的信息呢?系统调用接口。操作系统提供了getpid()函数,可以查看当前进程的pid信息

除此之外,OS还提供了可以查看当前进程的父进程的id:getppid()

先观察一个现象:

当程序启动多次时,pid每次都会发生变化,但是它父进程总是不变,为什么?它的父进程是谁呢?

通过指令查看发现,pid为23752的进程的父进程的id是23083,23083这个进程是bash进程,bash是命令行解释器,用户登录xshell时,系统都会提供一个bash进程,我们在xshell里创建的其他程序运行起来的进程都是bash的子进程。

用循环打印的指令查看进程,可以很好的表现出进程启动到结束的过程

指令:ls /proc/pid -l,查看进程的属性信息

exe是该进程运行起来的程序,cwd是当前工作目录

现象:先让程序跑起来,然后再把它杀掉,进程还在跑

红色的说明该进程已经被杀掉了,但是为什么进程还能跑?程序是从磁盘(硬件)上加载到内存的,在内存中运行起来。根据冯诺依曼体系,一个程序从一个设备到另一个设备之间的流动,本质是数据的来回拷贝,所以程序是从磁盘拷贝到内存的。而这里杀掉的是磁盘中,不是在内存中运行的,在内存中的与磁盘中没有直接关系,所以还能跑。

图的上面还有框起来的东西,这是进程的当前工作目录,有什么用呢?我们以前写C语言的文件操作时,打开、关闭,在文件里写东西首先是不是要创建一个文件,那这个文件创建的位置在哪?放文件的位置不是随意的,而是默认在该运行起来的进程的当前工作目录。

当前工作目录是可以修改的,chdir函数

2.2 进程状态

2.2.1 进程排队

进程是不是一直在运行?

不是,比如我们用scanf时,程序并没有直接往后,而是要先等我们输入一个数据才行。

进程在CPU中是不是一直在运行?

不是,这与时间片有关。什么是时间片?一个程序运行起来变成进程,CPU是要给一定时间范围,即在这个时间范围内执行任务。我们以前有写过死循环的代码,为什么死循环后会变得有点卡,但是其他的进程照样可以运行?因为死循环的代码CPU也是有给它一定时间的,只是死循环是一直占用,但是时间一用完,其他的进程也要运行,这时CPU就要腾出来给其他的进程运行。这里的时间范围就是前面说的时间片,不是所有的系统的时间片都相同。

进程为什么排队?

因为要等待某种资源,比如前面的scanf的例子。

进程排队是什么?

进程 = 内核数据结构(PCB)+可执行程序,排队是PCB对象还是可执行程序呢?小栗子:找工作时,投简历后是简历排队还是人过去排队,肯定简历排队,然后找到对应的人。这里的简历指啥?是人的属性信息,等价于PCB对象,因为PCB对象是描述可执行程序,所以进程排队是PCB对象排队。

进程怎样排队?

既然是排队了,肯定要有一种数据结构,叫队列。首先要引入一个观念:task_struct对象是可以连入多种数据结构中。PCB对象内部许多属性信息,假设我们给其中的一个信息排队(假设叫listnode),这个listnode也可能是一个结构体,然后在每个task_struct对象中,都有这样的一个listnode,给这些listnode以队列的形式排队起来,(队列也是链表),而task_struct对象还是用双链表的形式链接起来,就可以在task_struct对象中实现多种数据结构的连入。所以怎样排队:就是根据相应的成员属性信息,用相应的数据结构将它们关联起来,方便进行管理,这里适合排队的数据结构就是队列

2.2.2 关于进程状态的表述------运行、阻塞、挂起

进程在哪排队?

进程的状态有运行、阻塞、挂起,进程在这些状态下要进行排队

状态的本质?

说白了就是什么是状态?状态其实可以看作是一个整型变量,比如

根据不同的整数,来表示对应的状态。

1️⃣运行状态

什么是运行状态?在运行队列中的进程就是运行状态。一个CPU一个运行队列,多个就有多个运行队列。在运行队列里的进程说明已经准备好了,可以随时被调度。

2️⃣阻塞状态

这里先谈硬件,硬件有磁盘、网卡、键盘等,它们是由操作系统进程管理的,管理方式:先描述,再组织。然后把它们的描述对象像前面一样用队列连接起来。我们前面写了一段scanf的例子,运行时要用户输入数据,这个过程就是进程在等待资源的过程。那么在用户输入数据之前,即进程还在等待资源,此时进程的状态就是阻塞状态,它也在队列中,这个队列就是等待队列。

当资源就绪的时候,进程的状态就会从阻塞状态变成运行状态。过程:OS是硬件的管理者,它知道进程此时此刻的状态是什么,进程接收到资源后,进程的状态会调整,然后OS会把它连入到运行队列当中,进程就变成运行状态了。注意,这个过程都是由OS来实现的。所以状态的变化其实是:进程被操作系统移动到不同的队列中。

3️⃣挂起状态

挂起状态发生的前提:内存资源比较紧张的时候。

一个程序从外设被加载到内存,操作系统会对它进行管理,给这个可执行程序一个task_struct对象,用来描述这个程序,方便管理,然后再交给CPU处理。当内存的资源比较紧张时,说白了就是内存快要满了,如果继续下去,会导致系统崩溃。所以要对内存中的代码和数据进行转移,为什么要移?内存快要满了。移到哪里去?swap分区,在磁盘中。内存中那么多数据,移谁?前面已经学过两种状态,运行状态和阻塞状态,运行状态就是进程在运行队列中,随时被使用。阻塞状态是在等待某种资源,进程在等待队列里。所以把谁移出内存合适?肯定是正在等待资源的进程,把该进程的代码和数据移到swap分区叫唤出,从swap分区移到内存中叫唤入。

注意是代码和数据唤出和唤入,进程的PCB对象可没有。为什么?因为当内存的空间足够了,操作系统就可以将代码和数据唤入到内存中,它怎么知道的?PCB对象。PCB对象是描述它对应的可执行程序的属性信息。如果连task_struct都一起出去了,操作系统怎么知道可不可以让那个程序再进来。所以还有一点:程序在加载到内存的过程中,先有PCB对象,然后程序才最终加载到内存。

swap分区是有大小的,一般是内存的二分之一,可以修改空间大小,但是一般不建议太大。根据冯诺依曼体系结构,数据的流动内存与外设之间的效率是比较低的,那这样设计是不是不好呢?其实前面已经说过了,如果不先到Swap分区缓一缓,内存太紧张可能就崩了,与其慢点也不要让系统崩掉,这是用时间慢点的代价换取空间的稳定性。

注意:前面的移动其实是拷贝

2.2.3 Linux中具体的进程状态

1️⃣R运行状态

要么在运行,那么在运行队列中,现象:

原因:因为printf是向显示器打印,也就是说printf是跟输出设备打交道的,根据冯诺依曼体系,只要与外设之间互动的,那么注定是比较慢的,而CPU是非常快的,相对一下,在CPU看来,他其实是一直在等,并不是一直都在运行的,所以是S睡眠状态。

去掉printf:

此时它没有与任何外设互动,单纯的高速运行。但是用grep查看该进程时有一个现象,为什么grep是R状态?因为grep是指令,这个指令在执行查看进程信息的过程也是一个程序,指令执行起来它也变成进程了,所以它也有它自己的状态。但是为什么是R呢?因为grep要执行这个程序,所以是R,就好比一个人能打球,玩游戏,学习,说明他一定是醒着的,只有处于醒着的状态,他才能做这些事情。同理,grep能执行,查看进程的信息,那它一定是要在运行状态下才行。

2️⃣S睡眠状态

S状态在前面已经看过了,printf往显示器打印时要与外设进行数据流动,在CPU看来是比较慢的,相当于是等待资源。更好的例子是前面的scanf,运行时卡住了,就是要等待用户输入,所以像这样(等待资源)是睡眠状态,它也是阻塞状态。也叫可中断睡眠。

3️⃣D磁盘睡眠状态

这个状态通常会等待IO结束,也叫不可中断睡眠。栗子:操作系统叫进程去办事,让它往磁盘里写100kb的文件数据,进程开始执行,磁盘已经快要完成了工作,但是这个过程相对有点久。操作系统不耐烦了,问进程咋还没好,进程看了看磁盘,磁盘还是没有出来跟它交代。于是操作系统一气之下就把该进程给干掉了,好巧不巧,这时候磁盘的工作完成,它出来看了看,进程跑到哪去了,结果这些数据就丢失了。操作系统为什么很着急?因为这个时候往往内存就比较吃紧了,而这个进程一直还没反馈结果,所以就把它干掉了。但是它只是比较慢的而已,有什么办法可以让进程在等待磁盘结果的时候不要让操作系统杀掉进程?D状态。该进程只要把自己的状态设置为D状态,操作系统就知道了,这个进程还在等待结果,并没有摸鱼,所以就不杀掉它。

4️⃣T暂停状态

可以通过信号控制:

5️⃣Z僵尸状态
为什么有僵尸状态?

父进程创建子进程,是要让子进程去办事的,不管结果做的怎样,最后都要给父进程交代清楚。但是有时候子进程的任务做完了,它的代码和数据的资源已经释放,可是它反馈结果的信息并没有给父进程读到,即pcb对象没有被父进程读取,此时这个状态叫做僵尸状态。

什么是僵尸状态?

子进程已经退出,父进程还在,父进程没有读到子进程的信息,该子进程的状态就是僵尸状态。


僵尸进程的危害:即父进程如果不读取子进程的信息会怎样?子进程的PCB会一直占内存,导致内存泄漏。

父进程会变成僵尸吗?

不会,因为父进程的父进程是bash,命令行解释器,自动回收资源。

大部分被创建出来的子进程都要经过僵尸状态,因为通常都是代码和数据这些资源先被释放掉,然后父进程再读取信息。只是这个过程很快。

2.2.4 孤儿进程

什么是孤儿进程?

与父进程相反的过程,父进程先退出,子进程还在,且该子进程被1号进程回收,这个子进程就是孤儿。

只有子进程在跑了,但是谁回收它?它的父进程是1,1号进程是操作系统,由操作系统来回收。同时它会从前台进程变成后台进程,要kill杆9命令杀掉它才能停下。

2.3 进程优先级

什么是进程的优先级?

多个进程要得到某种资源,必须经过一种方式去获取(比如说排队),才能确定每个进程得到资源的先后顺序。这个先后顺序就是优先级。

为什么有优先级?

主要是资源问题。以食堂打饭为例,打饭窗口少,学生很多,就要排队。多个进程都要某种资源,但是资源较少,所以才让进程排队得到资源。

优先级怎么操作?或者说Linux下的优先级是怎样实现的?

运行一个程序,指令如下,有一个PRI就是它的优先级。优先级的默认值都是80,它本质是数字,数字越小,优先级越大。

优先级是可以修改的,但是修改有范围:【60,99】,sudo top,r,进程pid,修改的优先级值。多了一个nice值,它是优先级的修正数据,其实修改优先级本质是修改nice值,而不是直接修改PRI,PRI=原来的PRI+nice。nice值是有极值的,在【-20,19】,它是直接覆盖在PRI上的,而且每次的PRI都是从80开始加或减:原来PRI是80,nice= +10 ,PRI变成90,然后nice= -10,注意,PRI可不是80,而是每次都是从80开始,变成70;nice值如果超过极值【-20,19】,比如nice= +50,那它就会按最大值19处理。

为什么调整优先级要有限制?

为了公平调度,否则导致进程饥饿问题。

三、Linux的调度与切换

几个知识点:

  • 进程在运行时,放在cpu上,并不是直接要把进程的代码跑完才行。因为现代操作系统,都是基于时间片进行轮转执行。时间片是每个进程操作系统提供给它的时间范围,即要让这个进程时间内执行任务,如果没有完,就给另一个进程执行。比如死循环,死循环的代码是跑不完的,所以有时间片,在死循环的同时其他进程也能够执行。
  • 竞争性:系统进程数目众多,而CPU的资源是少量的,所以进程之间是具有竞争性的,为了高效的完成任务,更合理竞争相关资源,便具有了优先级。
  • 独立性,多进程运行,需要独享各种资源,多进程运行期间互不干扰
  • 并行,多个进程在多个CPU下分别同时进行运行
  • 并发,多个进程在一个CPU下采用进程切换的方式,在一段时间内,让多个进程可以同时进行

3.1 进程切换

CPU中有许多寄存器,这些寄存器有不同的功能。进程在CPU上运行时会产生大量的临时数据,这些临时数据在寄存器中,临时数据也叫进程的上下文。

进程切换是什么?

进程切换也叫上下文切换,每个进程都有自己的状态,根据状态来判定这个进程是否运行,一个进程的时间片到了,就该到另一个进程运行,如果还要前面的进程再次运行,原先保存的上下文恢复,重新回到原来运行的位置继续运行。

为什么要有进程切换?

为了合理分配,以食堂排队吃饭为例,要考虑进程的状态,优先级等,让每个进程都可以享受资源,对该进程进行调度。在计算机的视角,就像食堂打饭一样。但是在用户的视角,整个过程是很快的,在一段时间内是同时发生。

进程切换的过程是怎样的?

当一个进程运行时,它会产生大量的临时数据在CPU上,具体是在CPU的寄存器上。这个进程的时间片到了,比如说它要到阻塞队列中去,那么此时它会将寄存器上的所有数据带走,保存在自己的PCB中。另一个进程来了,它是第一个来,即第一次被调度,它也会产生临时数据在寄存器上,注意,第一次调度产生的临时数据是覆盖式的放在寄存器中,进程走后是拷贝带走。第二个进程的时间片也到了,第一个进程又回来了此时是第二次被调度,注意,这时候就会将保存在PCB中的数据进行恢复,也叫恢复上下文,然后回到上次运行的位置继续运行。所以整个进程切换的过程就是:保存上下文,恢复上下文;保存是为了恢复,恢复是为了回到上次运行位置继续运行。

寄存器 != 寄存器的内容

在CPU中寄存器只有一套,但是保存的数据可以有多套。也就是说CPU是可以给所有进程共享的,但是每个进程的数据是自己私有的,当一个进程在CPU上运行完了,就会带走自己的数据。类似于图书馆的桌子椅子是共享的,但每个人的书、笔记是自己的,用完了带走。

3.2 进程调度

一个CPU一个运行队列,队列中正在排队的进程可以随时被调度。排队是根据进程的优先级来确定的,有一个进程从阻塞队列中到运行队列要根据优先级排队,然后等待被调度。那么在运行队列中进程的调度具体是如何实现的呢?

Linux实现进程调度的算法要考虑的有:优先级、进程饥饿问题、效率,以下是运行队列。这里主要看array[0]和array[1]和*active和 * expired

在array数组中nr_active是表示队列中还有多少个任务(进程);bitmap是用来判断每个优先级链表是空还是非空,bitmap的查找效率高;queue是队列,队列中每个位置是一个单链表,插入链表的每个元素是一个进程,一个进程如何插入到一个位置中即一个单链表中,是根据进程的优先级来确定的。这里的队列中一共有140个位置,其中0--99是实时的,100--139是分时的(数字是下标)。实时是指现在需要那么现在必须优先给这个进程先运行;分时就是前面说的排序。分时一共有40个位置,刚好对应了优先级的范围【60------99】,数字越小,优先级越高

一个进程来到运行队列,根据优先级插入到指定的位置,如果有多个进程都是相同的优先级就尾插到链表,某个位置没有进程,那么该位置的链表指向空。进程运行时是基于时间片轮转的,时间片到了,给下个进程运行。

如果只讨论进程插入到运行队列中没有问题,只讨论进程运行后给下个进程运行,当前进程退出运行队列,这个好像也没什么。但是问题在于,假如上面的运行队列中,优先级为80的进程已经累积很多了,这时候又源源不断的有新进程进入,这些进程的优先级是60,按照优先级高的进程先运行的原则,那么导致上面很多优先级为80的进程一直不能运行。所以,运行队列的设计,不仅仅只有一个queue,前面的图中还有一个array,里面的元素与上面都相同,分别表示的是该队列的进程是活跃进程和过期进程。

活跃进程和过期进程

活跃进程用来运行进程的,一个进程的时间片到了,这个进程退出,给下个进程运行,就这样不断消耗活跃队列中的进程;如果有新的进程来到运行队列中是怎样的?它会去过期进程,此时活跃进程的任务就是让进程在CPU上运行,对于新来的进程,活跃进程不管它,新来的进程根据优先级插入到链表中。活跃进程不断减少,过期进程不断增多。

当CPU执行完活跃进程的所有进程的调度,就会去执行过期进程的,但是它是怎么找到过期进程?这与两个指针有关。*active指向哪个array,那个就是活跃进程, * expired指向谁,谁就是过期进程。前面说CPU将活跃进程执行完了,它要去执行过期进程,并不是说真的去执行过期进程,其实CPU还是执行活跃进程。什么意思?原来的活跃进程中的进程不是都被执行完了吗?咋还执行它呢?

CPU只执行活跃进程的进程,过期进程是用来处理新增的进程。是活跃进程还是过期进程关键看*active和 * expired两个指针指向谁。原来活跃进程被执行完了,这时候,*active和 * expired两个指针交换它们指向的内容,此时,原来的活跃进程(被执行完了,没有进程了)变成了过期进程,因为 * expired指向它;而原来的过期进程变成了活跃进程,因为 *active指向它,然后CPU继续执行它的任务。

四、环境变量

4.1 main函数------命令行参数

bash给我们输出的命令行字符串:

main函数中的参数:argc和argv。argc是int类型的变量,argv是char*类型的指针数组。我们输入的命令就是该数组的内容。

输入的一个字符串,以空格为分割,分成一个一个的字串。可是为什么要这样呢?下面看一个小样例:通过输入不同的字符串得到不同的功能

可以发现,同一个程序可以通过不同的选项实现不同的功能,这些选项的本质就是命令行参数,我们输入的一些指令,如下,第一个是指令的名称,第二个就是它的选项,通过不同的选项执行指令内部不同的功能。所以命令行参数是Linux指令的选项的基础。

加减乘除demo:

4.2 环境变量

环境变量不是只有一个,它是有很多个的。一般是指系统内置的、具有特殊用途的变量。对于变量,定义一个变量的本质是开辟空间,运行一个程序也要开辟空间。系统的环境变量也是变量,本质是系统自己给自己开辟空间,然后给名字和内容。

现象:我们输入一个指令,比如ls,为什么直接输入ls就行了;而我们自己写的程序,myprocess,要./myprocess才能运行起来。

这与环境变量有关。ls是系统已经设置好的,直接输入ls可以找到它的路径,而myprocess,系统没有设置它的环境变量,所以找不到。所以才要./来找它的位置在哪,才能运行。ls指令除了直接输入ls外,还可以输入它的绝对路径:

通过which加指令可以查找该指令的位置:ls可以找到,我们写的myprocess找不到

配置myprocess的环境变量。查看环境变量

如何配置呢?把myprocess拷贝到/use/bin中就行,然后直接输入myprocess也能运行了,记得sudo

还有一个办法是:将当前路径配置到环境变量中去,这样也能找到我们的程序:注意!!后面要带上 : $PATH,否则相当于覆盖了原来的环境变量,这时候如果输入ls、cd等指令就找不到了。如果不小心覆盖了,可以重新登录xshell就好了。

使用which查看下:

我们要查找当前路径输入的pwd,查找用户名等信息,这些信息都是存在环境变量里的,所以只要输入这个指令,它就会去找对应的路径的环境变量,然后显示出来。(echo $USER)

env---查看所有环境变量

和环境变量相关的命令:

  • echo:显示某个环境变量值
  • export:设置一个新的环境变量
  • env:显示所有环境变量
  • unset:清除所有环境变量
  • set:显示本地定义的shell变量和环境变量

main函数的参数不仅有两个,还有第三个参数,env,char * 类型的指针数组。

下面一段代码,看第三个参数有什么用:

通过上面的图发现,env[i]可以显示每个环境变量的信息,bash进程不仅可以给argv一张表(表是一个char指针类型的指针数组),用来放指令的每个选项;还可以给env一张表,用来放每个环境变量的信息。argv是实现功能的选项的子串,env是查找环境变量信息的子串,argc是子串的个数。

main函数提供这样的参数,给我们的进程(bash创建的子进程)有环境变量,通过传参的方式传递,所以环境变量是可以被子进程继承的。下面一段代码看下:

总结:环境变量具有全局属性,可以被所有的子进程继承。

代码获取环境变量

函数------getenv------获取一个环境变量的信息


这个获取环境变量有什么用?可以根据环境变量的信息来确定是否可以执行一段代码:防止别人使用我们的核心代码,只能自己使用。


我们将用户换成root看下:

获取环境变量的第三种方式------environ,像main函数的第三个参数一样把环境变量显示出来

本地变量

直接在命令行给一个随便创建的变量赋值

查看环境变量与本地变量,env只能查环境变量,set可以两种都查

下面一段代码,验证本地变量不能被子进程继承,只在bash内部有效

我们创建的进程都是bash的子进程,这些子进程都会继承bash的环境变量,即子进程的环境变量是它的父进程bash拿过来的,那么问题是,bash的环境变量是从哪里来的?是从磁盘的文件来的,每次启动xshell,bash都会从磁盘的文件中读取环境变量的信息。具体:bash_profile,它是配置文件,可以管理用户的环境变量和启动脚本的功能,用户也可以自己在bash_profile文件中设置环境变量。

为什么要有环境变量?

为了方便用户使用,root用户有环境变量,每个普通用户也有自己的环境变量,不同工作目录下的环境变量是不一样。在不同的场景下执行任务,可能需要其他属性信息,环境变量就起作用了

五、地址空间

5.1 内存分布

内存布局图:

从代码区到命令行参数环境变量的地址是越来越高的,先看下正文代码、初始化数据、未初始化数据、堆和栈的地址:

堆是逐渐向高地址增加的,栈是逐渐向低地址减少的:

命令行参数环境变量的地址无论是表(取地址)还是内容都在栈的上面:

上面的地址不是物理内存的地址,是虚拟地址,也叫进程地址空间

代码---现象:

变量val是10,地址是一样的,下面改下代码:在子进程中val从10到20

发现当cnt的值变成5后,子进程val的值从10->20,父进程还是10,因为有写时拷贝,子进程用子进程的,父进程用父进程的。但是为什么它们两个的地址还是一样的?说明这个地址不是物理地址,而是虚拟地址。

5.2 进程地址空间

先直接用结论解释现象:

每个进程都有自己的进程地址空间,前面我们打印出来的地址都是地址空间的虚拟地址,在物理内存中其实有变量val,此时它的值是100,物理地址如图,那如何获取变量的值?在进程地址空间与物理内存之间有一张表(暂时不管表是什么),这个表可以一边放虚拟地址,另一边放物理地址,类似于哈希的映射关系,通过这样的映射关系虚拟地址找到对应的物理地址,然后从物理内存中获取变量的值。这个是一个父进程,接着fork后创建一个子进程,子进程也有自己的task_struct,同样也有自己的进程地址空间。子进程是继承父进程的,所以它的初始化数据的那个变量于父进程的相同,通过中间那张表的映射关系,也能获取变量的值,所以前面的代码父进程和子进程的val都是相同的。后面把在子进程中,把val值改为200,这时候,在物理内存中,会新开辟一块空间,放val值=200,假设地址为0x114455,然后该物理地址也放入表中,通过映射关系,子进程获取val=200。注意,此时父进程和子进程在进程地址空间的地址(虚拟地址)都是一样的,但是在物理内存中的不一样,所以val值也是不一样的。而进程具有独立性,所以父子进程各做各的,父进程得到的val值是100,子进程得到的val值是200,所以现在也解释了为什么fork返回值一个变量可以同时等于两个值。

什么是进程地址空间?

首先,对于每一个进程,它们都有自己的进程地址空间和一张映射表,都是在操作系统内部的。进程地址空间其实类似于操作系统给每个进程画的大饼,比如公司领导跟员工说好好干就能升职加工资。饼是操作系统给的,每个进程都有,那操作系统要不要对这些饼做管理?要的,就跟每个进程都有它的PCB结构体对象,要对这些PCB结构体对象做管理。管理这些饼的方式是:先描述,再组织。用某种数据结构比如链表的方式连接起来进行管理。所以具体来说,进程地址空间是特定的数据结构对象。这个数据结构对象也是进程PCB结构体对象中的一个属性,前面学过,PCB对象的属性有进程id,状态、优先级等,根据这点,所以进程可以有它对应的进程地址空间。

进程地址空间的属性是什么?

还是那张布局图,属性就是在布局图中的栈、堆、正文代码等区域划分,这些字段每个都有自己的区域划分,可以判断是否越界和进行扩大和缩小。它们共同组成的整体就是我们所看到的进程地址空间,进程在创建时操作系统就把这个进程地址空间的每个字段给已经初始化好了,空间划分本质是连续的空间的每个区域都可以被进程使用。

进程地址空间不具备保存代码和数据的能力,所以代码和数据存放在物理内存中,如果要将地址空间转化到物理空间中,需要通过前面说的一张映射表------页表 。如图,页表分为虚拟地址和物理地址,两者是映射关系的。在将地址空间转化到物理空间,即虚拟地址通过映射找到物理地址再获取物理内存中的代码和数据的过程,是由CPU处理的。CPU里面有一个寄存器,叫CR3,它指向页表的起始位置。当CPU读到一行代码,比如int a,它会把该代码的地址通过CR3,去页表中找到对应的物理地址,然后从物理内存获取代码和数据。整个过程由CPU操作,更具体来说是硬件MMU和CR3来完成的。有一个问题:CR3是虚拟地址还是物理地址?是物理地址。虚拟地址是给进程的,进程是用户创建的,所以也可以说虚拟地址是给用户的。

为什么要有进程地址空间+页表?

进程地址空间和页表之间的互动可以叫做进程管理,磁盘和物理内存的互动可以叫做内存管理。在内存管理中,磁盘将多个可执行程序加载到物理内存中,不一定是按照顺序的,大概是有进程加载进来,就随机开辟一块空间给它。这样操作对于左边的进程地址空间和页表,即进程管理是没有影响的,为什么?因为进程地址空间把它的正文代码中的虚拟地址放到页表中,物理内存也将物理地址放在页表中,它们之间通过映射关系就能够快速找到对方。单从内存管理上看,这些可执行程序的在物理内存的位置是很乱的,没有顺序。可是有了页表,在页表中将两者的地址通过映射的关系进行联系起来,从无序变成有序。地址空间只需要从正文代码区给页表提供代码的虚拟地址即可,而物理内存中的操作是开辟空间放可执行程序,就这样进程管理做自己的,内存管理也做自己的,不需要管对方怎样,反正最后也能通过页表有效结合。所以,为什么要有进程地址空间+页表,第一点:将物理内存从无序变有序,让进程以统一视角看内存。第二点:将进程管理和内存管理进行解耦合。


第三点:是保护内存安全的重要手段。虚拟地址给页表,即用户发送请求,不一定都是好的,如果没有通过页表的筛选、辨别,直接到内存中可能会对内存安全有影响。所以有了页表,可以有效保护内存安全。

总结:进程被创建时除了自己的PCB对象,还要有自己的进程地址空间+页表。我们操作一个程序,即进程,发送一个请求,即将虚拟地址给页表,由页表通过映射关系找到对应的物理地址,进而获取物理内存中的代码和数据。当创建一个子进程时子进程是按照父进程的模板创建的,它也有自己的PCB对象、进程地址空间+页表。父子进程代码共享,此时虚拟地址是一样的,在没有对子进程写入新的数据前,子进程的地址空间给页表的虚拟地址从物理内存中找到的变量是父进程一样的,当重新对子进程的变量进行写入,这时候物理内存中会发生写时拷贝,随机开辟一块区间给子进程重新写入的变量,然后这新的物理地址,给页表,再通过映射关系,虚拟地址找到这新的区间的变量值,因此同一个变量,虚拟地址相同,可以等于两个值。

解释一些问题:

1、malloc和new申请的内存会直接使用吗

不一定,一个进程不是总在运行的,比如某个程序等待用户输入,此时它在阻塞状态,所以申请的内存的并不是直接就会使用的。

2、在哪申请的内存?

在进程地址空间,不是物理内存,因为用户申请后得到内存不一定会直接使用,如果让用户直接得到物理内存,可能会浪费内存资源,操作系统要对内存资源和使用率负责,所以用户申请的内存,本质是在虚拟地址空间申请。

关于缺页中断

用户创建了一个进程,在该进程的地址空间申请内存,已知这个区域是合法的,可以给用户使用。申请的虚拟地址给页表,但是此时物理内存中还没有开辟内存建立物理地址给页表,所以这时候页表中还没有虚拟地址的映射关系,于是操作系统就会拦截虚拟地址访问物理内存。然后操作系统 重新在物理内存申请内存,页表也就可以建立映射关系,虚拟地址再访问物理内存。缺页中断就是在页表还没有映射关系的情况下,操作系统把地址空间的操作中断了,此时不让虚拟地址访问到物理内存。

相关推荐
Ajiang282473530441 分钟前
对于C++中stack和queue的认识以及priority_queue的模拟实现
开发语言·c++
幽兰的天空1 小时前
Python 中的模式匹配:深入了解 match 语句
开发语言·python
内核程序员kevin3 小时前
TCP Listen 队列详解与优化指南
linux·网络·tcp/ip
Theodore_10224 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
网易独家音乐人Mike Zhou4 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
‘’林花谢了春红‘’5 小时前
C++ list (链表)容器
c++·链表·list
----云烟----6 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024066 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
开心工作室_kaic6 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
向宇it6 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎