Linux学习进程概念(下)

Posted 一只大喵咪1201

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux学习进程概念(下)相关的知识,希望对你有一定的参考价值。

🐱作者:一只大喵咪1201
🐱专栏:《Linux学习》
🔥格言:你只管努力,剩下的交给时间!

进程概念(下)

🍟进程优先级

CPU只有一个,而进程是有多个的,所以CPU在处理进程的时候势必会有先后顺序,而决定进程获取CPU资源先后顺序的就是优先级。

  • 概念:CPU资源分配的先后顺序,就是指进程的优先权。

随便写一段代码,然后让它执行起来,成为一个进程,再使用ps指令来查看该进程的优先级。

  • UID:执行该进程的身份
  • PID:当前进程的唯一标识符
  • PPID:当前进程的父进程的标识符
  • PRI:表示优先级,默认值是80
  • NI:nice值,用于修改一个进程的优先级。

修改进程的优先级:

  • 指令:top。 有时需要使用sudo top指令来提高权限。
  • 使用方法:在进入top后,按r,然后输入进程的PID,确认后再输入要修改的nice值。
  • 功能:改变进程的优先级。

优先级的计算规制:

新优先级 = 老的优先级(80) + nice值


这里将nice值设置成了100,然后查看修改后的优先级。


可以看到,这里的NI值也就是nice值是19,并不是我们设置的100,而此时的优先级是99,也就是80+19,所以说,优先级的设置是有一个范围的,此时我们知道了它的上界是19,下面来看看它的下界。


重新将nice值设置为-50,此时再查看修改后的优先级,发现NI值是-20,优先级是80-20 = 60,并不是我们所设置的值,所以nice值的下界就是-20。

说明:

  1. 优先级的本质就是一个数字,这个数字越小,表示优先级越高,也就是越先获得CPU资源。
  2. 优先级的计算方法中,老的优先级始终都是80,也就是默认值,改变后的优先级就是在80的基础上加NI值。
  3. NI值,也就是nice值是有限制的,它的范围就是[-20,19],一共40个,对应的进程优先级的范围就是[60,99]。

之所以对优先级的范围进行限制,就是为了防止调度失衡,如果一个进程的优先级可以被设置为无限高或者无限低,此时调度就不再公平,有些进程可能永远也无法获得CPU资源。

  • 竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级

在一般情况下,我们并不需要去设置一个进程的优先级。

🍟进程切换

进程切换是在多进程并发的时候必然经历的过程。

  • 并发:多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。


一个CPU中只有一套寄存器,例如eax,ebx,eip(PC指针)等等,虽然数量有很多,但是只有一套,而一套寄存器只能供一个进程使用。

当进程1正在被执行的时候,此时CPU中的寄存器中的内容都是和进程1相关的,比如运算产生的临时数据等等。

当CPU从执行进程1变为执行进程2的时候,寄存器中原本属于进程1的内容就需要被保存,因为进程2同样会用到这些寄存器,就会覆盖掉原本的内容。目前可以认为,和进程1相关的寄存器中的内容,被复制到了PCB中。这一过程被叫做,进程切换中的上下文保护

当CPU再次从执行进程2变为执行进程1的时候,和进程2相关的寄存器中的内容同样会进行保护,并且将进程1的PCB中属于上下文保护的内容再恢复到CPU的寄存器中,覆盖掉属于进程2的内容,接着之前执行到的位置继续执行下去,这一位置由eip(PC指针)寄存器从恢复的数据中读取。这一过程被叫做,进程切换中的上下文恢复

时间片:

CPU只有一个,进程有很多个,为了能够公平的执行每一个进程,CPU执行每一个进程的时间都是相同的,假设这个时间是10ms。

当进程1被执行了10ms,此时就会将进程1产生的临时数据进行上下文保护,也就是CPU寄存器中的内容都复制到对应的PCB中。再开始执行进程2,当进程2被执行了10ms后,同样会将进程2的上下文进行保护,然后将进程1的上下文进行恢复,继续执行进程1,如此往复,直到俩个进程结束。

CPU中寄存器里的内容,只是属于当前被正在执行的进程

由于CPU执行的速度很快,一个进程被执行的时间很短,所以多个进程表现出来的结果就是一起被执行下去。而一个进程被执行的时长就是时间片。

此时你是否有一个疑问,CPU为什么不执行完一个进程再去执行下一个进程呢?如果按照这种方式,当被CPU执行的这个进程是一个死循环的情况时,别的进程就永远不会被CPU执行了。

CPU之所以严格按照时间片去执行不同的进程,就是为了保证调度的均衡性,使得每个进程都能够被执行到。

进程的并发就是按照上诉过程进行的。

除了并发,还有几个和进程相关的概念:

  • 独立性:多进程运行时。需要独享各种资源,多进程运行期间互不干扰。

比如,父进程崩溃了而不再运行时,子进程并不会受到任何影响,仍然会正常的执行下去。

  • 并行:多个进程在多个CPU下分别同时进行。

并行和并发虽然只有一字之差,但它们的意义却完全不同,主要体现在CPU的个数不同。

🍟环境变量

  • 环境变量(environment variables):一般是指在操作系统中用来指定操作系统运行环境的一些参数。
  • 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。
  • 环境变量通常具有某些特殊用途,在系统当中通常具有全局特性。

环境变量在系统运行的时候就会被加载到内存中,供所有进程使用,就类似于全局变量,并且是内存级别的变量。

🥞常见的环境变量


使用指令env可以看到当前系统中的所有环境变量,如上图所示,这么本喵介绍几个常见的,来给大家解释什么是环境变量。

  1. 环境变量PATH


使用指令echo可以查看环境变量的值。如上图中,下面一串路径就是环境变量PATH的值,不同路径之间用冒号隔开。那么这个环节变量的作用是什么呢?这些路径的表示的意义是什么呢?

在执行一个可执行呈现的时候,需要使用./可执行程序名,其中./就是路径。我们自己写的可执行程序,要想执行就必须加路径才能够执行。


我们知道,指令ll的本质也是一个可执行程序,但是此时就不需要加路径,直接使用程序名就可以执行。

同样的,我们自己的可执行程序也可以不用加路径:

  1. 方法1:


将我们自己写的可执行程序,复制到/user/bin路径下,此时再执行process的时候同样不需要再写路径,直接写可执行程序名就可以执行对应的程序,如上图所示。

但是不建议这样做,因为这样会污染指令池,我们写的这个可执行程序是没有经过检测的,对于/usr/bin/路径中的其他指令文件是不安全的。

  1. 方法2:


在环境变量PATH中加上我们自己所写可执行程序所在的路径,如上图中红色框中所示,此时需要使用到指令export,加上之后再运行我们自己的可执行程序就不用加路径了,只需要输入名字即可。

这是因为,系统会去环境PATH变量中那些路径下去搜索没有带路径的可执行程序。在系统开始运行的时候,就会将PATH变量的值加载到内存中,任何一个进程都能够看到和使用环境PATH中的值。

  1. 环境变量HOME


可以看到,用户不同,环境变量HOME中的值也不同,这也是我们为什么使用指令cd~就可以回到当前目录的家目录的原因,因为会根据环境变量HOME中的值来切换路径。

  1. 其他环境变量

USER:当前用户名
PWD:当前所处路径
HOSTNAME:主机名
SHELL:当前的shell,通常它的值都是/bin/bash

在系统启动的时候,会把.bash_profile执行一次,把环境变量加载到内存中,供后面各种进程的使用。

🥞和环境变量相关的指令

  • 指令:echo $环境变量名
  • 功能:显示环境变量的值

这个指令在上面已经多次使用,本喵这里就不再演示了。

  • 指令:env
  • 功能:显示所有环境变量
  • 可采用env | grep 环境变量名 的组合方式来查看某一个环境变量


如上图,将带有HOME字眼的环境变量都显示了出来。

  • 指令:set
  • 功能:显示本地定义的shell变量和环境变量

环境变量现在大概已经了解是什么了,但是本地变量又是什么呢?


我们现在对Linux系统进行的一切操作都是在bash这个shell上进行的,而bash就是一个进程,而且我们可以在这个进程上定义变量,如上图中蓝色框中所示。此时定义的变量就像我们在C/C++代码运行过程中在堆区或者栈区创建变量一样,它是被运行的。

但是此时的变量仅是一个临时的变量,当进程结束以后就会消失,不像环境变量一样是被加载到内存中一直存在的,这种变量叫做本地变量。

可以看到,在环境变量中是无法查找到本地变量的,如上图中红色框,而set查找的不仅是环境变量,还有shell中的本地变量,所以能够查找到我们创建的本地变量,如上图中绿色框。

  • 指令:export 变量名
  • 功能:创建一个新的环境变量


使用export将原本是本地变量的my_value设置成了环境变量,并且可以在环境变量中查找到。

当然也可以设置一个全新的环境变量:


此时一个新的环境变量就被创建成功了。

  • 指令:unset 环境变量名
  • 功能:清除环境变量


此时就将我们刚刚设置的环境变量清除了,在环境变量中查找不到了。

🥞系统调用获取环境变量


这是获取环境变量的系统调用,它的需要包含的头文件是stdlib.h,环境变量存在,一字符串的形式返回环境变量的值,也就是char*类型的指针。环境变量不存在,则返回空指针。

  1. 获取USER


如上面代码,使用了系统调用getenv来获取环境变量USER。


可以看到,执行有系统调用的可执行程序的结果,和查看环境变量USER的结果是一样的,因为在程序中使用系统调用getenv获取到了环境变量USER的值。

  • 这解释了为什么在执行某些指令的时候,会显示访问被拒绝,权限不足等问题。因为是可以通过系统调用获取当前用户身份的,将结果与设定的用户进行比较,如果不相同就拒绝操作。

比如下面代码:


使用条件判断来决定是否能够操作。

  1. 模拟PWD


代码如上,使用系统调用获取环境变量PWD的值。


可以看到我们自己的模拟的pwd和系统中的pwd显示的结果是一样的。


将我们自己写的mycmd的路径加载环境变量PATH中,此时即使改变所处路径,也可以直接使用可执行程序名,实现和mypwd一样的功能,如上图中所示。

不管是我们自己模拟的mycmd还是一直使用的指令pwd,它们为什么能够知道当前所处的路径呢?


系统中有一个环境变量PWD,该变量中的值就是当前所处的路径,而且这个变量是在系统启动的时候就加载到内存中的,并且是由bash来维护的。

环境变量具有全局属性,而我们使用的指令pwd也好,还是自己写的mycmd也好,都属于bash的子进程,全局变量会被子进程继承下去,所以这些子进程都能使用父进程bash维护的环境变量。

🥞命令行参数


其实main函数也是有形参的,只是我们在平时写的时候并不写它的形参。这些形参被叫做命令行参数。

在刚接触Linux的时候,学习了很多指令,比如ls,它可以加各种选项,如上图所示。我们知道,这些指令的实质就是可执行程序,所以我们自己的可执行程序也是可以加各种选项的。


如上图所示,在可执行程序名字后面可以加各种选项。

如上图所示,可执行程序的名字和后面的选项会被系统进行处理,处理成多个字符串,可执行程序名是一个,以空格为界,每一个选项是一个字符串。

而这些字符串会被放在一个数组里,这个数组是char*类型的指针数组,如上图绿色框所示。

  • int argc表示的字符串的个数。
  • char* argv[]表示的存放各个字符串首字母地址的指针数组。

main函数是我们所写程序的入口,而此时的命令行参数是main函数的形参,既然是形参就会接收实参,而这个实参是系统的strrtup函数传递的,也就是说main函数也是被系统调用的。而这俩个命令行参数则在main函数被调用的时候进行了传参。


写入上图所示代码,根据不同的命令行参数,执行不同的语句。

选项不同,执行的结果不同,入上图所示。

环境变量的组织方式:

  1. 通过第三个命令行参数获取

而环境变量也是可以通过这种方式获取的。


main函数其实还有一个参数,如上图所示,该变量同样是一个指针数组,里面存放的值是各个环境变量。


环境变量和环境变量的值一起被当作成一个字符串,多个环境变量就有多个字符串,在指针数组env中,每一个字符串的首元素地址占据一个数组元素。

  • 该数组的最后一个元素是一个NULL指针


写如上图所示代码打印出所有环境变量。

  • for循环使用env[i]控制循环结束,因为当i的值为数组env的最后一个元素下标时,此时的env[i]的值就是0,也就是false,此时的for循环就会结束

注意:

在Makefile文件中需要加一个选项,使用c99标准来进行编译,如上图所示,否则就是会编译不过报错。


如上图所示,报错提示这种循环方式仅有c99标准会支持。


此时执行可执行程序的时候就会打印出所有环境变量。

还有一种方式可以获得环境变量,就是通过外部变量的方式。

  1. 通过外部变量获取


如上图所示的环境变量,environ是一个二级指针数组,它执行的数组中,存放着环境变量字符串首元素的地址,这个数组其实就是使用第三个命令行参数的方式中的那个数组。

如上诉代码,使用extern引用外部变量,也就是二级指针变量,如上图中的红色框中所示。


编译然后执行,可以看到,同样是将所有环境变量打印了出来。

🥞环境变量的组织方式

通过上面的分析,可以看到,环境变量是以数组的形式存储的,它的组织方式如下图:

如上图所示,每一个环境变量包括其值都被组成成一个字符串放在数组中。

每个程序在加载到内存中成为进程以后,都会收到操作系给的一张环境表,这个环节表是一个字符指针数组,每一个指针指向一个以’\\0’结尾的环境变量字符串。

这个数组中的最后一个元素是一个NULL指针。

🍟进程地址空间

这是之前在学习C/C++的时候,经常画的内存,但是真的理解它吗?物理内存中就是这样的吗?其实并不是这样的。

来看一段代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int global_value = 10;

int main()

    pid_t id = fork();
    if(id<0)
    
        printf("子进程创建失败\\n");
        return 1;
    
    else if(id == 0)
    
        int cnt = 0;
        while(1)
        
            printf("我是子进程。global_value = %d,&global_value = %p\\n",global_value,&global_value);
            cnt++;
            sleep(1);
            if(cnt==10)
            
                printf("子进程改变了global_value的值\\n");
                global_value = 100;
            
        
    
    else if(id > 0)
    
        int cnt = 0;
        while(1)
        
            printf("我是父进程。global_value = %d,&global_value = %p\\n",global_value,&global_value);
            cnt++;
            sleep(2);
        
    
    return 0;

上面代码中,创建一个子进程,父子进程同时打印一个全局变量gloabl_value,最开始全局变量是10,在子进程运行10秒钟后将这个全局变量改成了100,然后父子进程继续同时打印这个全局变量。

在子进程改变全局变量之前:

  • 父子进程打印全局变量的值都是10。
  • 父子进程打印全局变量的地址都是0x60104c。

在子进程改变全局变量之后:

  • 子进程打印全局变量的值是100,符进程打印全局变量的值仍然是10。
  • 父子进程打印全局变量的地址仍然是0x60104c。

颠覆性的认识出现了,父子进程打印的全局变量是同一个变量,而且地址也是同一个。当子进程改变这个全局变量以后,父子进程打印的全局变量值不一样了,但是打印的这个全局变量的地址还是一样的。

  1. 子进程改变了值以后,父子进程打印的值不一样可以理解,因为进程之间有独立性,各打印各的。
  2. 但是打印的地址是同一个,但是这个地址中的值在父子进程中是不一样的,一个地址出现了两个值。

这和我们的认知有很大的差距,同一个物理地址中存放的值只能是唯一的,而此时一个地址中存在了俩个值,说明此时这个地址不是物理地址,也就是不是真实的内存空间。

  • 在Linux地址下,这种地址叫做虚拟地址。
  • 我们在用C/C++语言所看到的地址,全部都是虚拟地址!
  • 物理地址,用户一概看不到,由OS统一管理。

在Linux中,虚拟地址也叫做线性地址,有时也被叫做逻辑地址,只需要知道,虚拟地址=线性地址=逻辑地址

虚拟地址必将被操作系统转化成物理地址。

而上面提到的C/C++中的内存空间,在这里我们叫做进程地址空间。

🥞进程地址空间的区域划分

以32位的系统为例,共有4GB的空间,有232个地址,一个地址代表的空间是1Byte大小。


如上图所示,在一个结构体中,有多个变量,这些变量就将这4GB的空间划分了开来。

  • 例如,stack_start中放的是栈区的起始地址,stack_start中放的栈区的结束地址。

这样一来,进程地址空间就被描述了出来,也就是被划分了出来,而且可以通过修改结构体变量中的值来调整各个区域的大小。

注意:

  1. 栈区和堆区是在进程执行的过程中才会创建空间,大小是会自动进行调整的。而数据段以及代码段这些区域是一直都存在的,而且大小是固定的,不会随着进程的进行而变化。
  2. 栈区开辟空间是从高地址向低地址开辟,堆区开辟空间是从低地址向高地址开辟,它们俩者的空间是对向增长的。
  3. 在堆区和栈区之间的那块区域称为缓冲区,当栈区和堆区的大小变化时,缓冲区的大小也会进行相应的变化。

🥞站在进程的角度看地址空间

在Linux操作系统中,每一个进程都认为自己是独占CPU资源的,而且认为4GB的空间都是属于自己的。因为CPU只有一个,只能通过时间片的方式在一个时刻指执行一个进程,此时这个进程就认为自己拥有所有的资源。

所以当一个可执行程序从硬盘加载到内存中成为一个进程以后,操作系统维护的该进程在PCB中的结构体task_struct也会有一个指向划分地址空间区域结构体的指针。


如上图所示,每个进程的task_struct中的mm指针都会指向一个mm_struct结构体,而这个结构体将4GB的空间划分成了各个区域。

当该进程的栈区以及堆区有数据创建以及释放时,对应的mm_struct中的边界值都会发生变化。

而CPU在取代码以及数据段的数据时,拿到的地址也是mm_struct中对应的地址。

所以说,区域调整的本质就是修改各个区域的end或者start值。

每一个进程的地址空间都是按照这个规则在划分,在变化,而且会认为这些空间以及CPU都是属于自己的。但事实上,进程肯定不只一个,而是有很多个,但是物理空间也就是真实的空间只有4GB,如果真的给每一个进程都分配4GB的地址空间,那么物理空间肯定是不够用的。

  • 进程地址空间就是操作系统给每一个进程画的一个大饼,让每一个进程都以为自己有4GB的空间,并且独享CPU资源。
  • 而且进程互相之间不是不知道对方的存在的。

进程认为自己有4GB的空间,如果该进程要申请4GB的空间怎么办?此时操作系统肯定不会给它4GB的物理内存空间啊,如果给了那别的进程就没空间用了,所以此时操作系统就告诉这个进程申请失败。

所以每个进程只有申请适当的空间时,操作系统才会给它分配对应大小的物理内存。

此时又有一个问题:

  • 每个进程在各自的进程地址空间中,空间的使用情况是不同的,比如代码段数据段的大小。通俗来讲就是,每个进程对操作系统画的这个大饼的完成情况是不一样的。

所以此时的操作系统需要记住个各个进程对这个大饼的完成情况。而方式就是通过维护进程PCB中task_struct中的mm指针来记住的。

此时系统中运行的所有进程的情况都被操作系统所掌握,而每个进程都不知道其他进程的存在,并且认为自己独享4GB的空间以及CPU资源。

🥞进程地址空间转化为物理空间

这是一段随便的汇编代码,可以看到,每一条汇编代码是有地址的,如上图中的红色框所示,当这个汇编代码经过汇编以后生产的二进制机器码也是有地址的。这个地址就是这些代码在代码段中的地址。

磁盘中的二进制可执行文件(.exe)是源文件经过预处理,编译,汇编,链接四个阶段后形成的,而这过程也是按照进程地址空间的规则进行的,也就是说,编译器也是按照进程地址空间的区域划分规则来编译源文件的。

  • 编译后形成可执行程序的地址被叫做逻辑地址,其本质就是虚拟地址,只是因为在磁盘上,所以叫法不同而已。

这样一来,可执行程序中代码的地址就按照进程地址空间的规则分布好了。当可执行程序从硬盘上加载到内存上以后,内存上会给这个可执行程序分配一部分物理空间用来存放代码,但是此时在物理内存中的代码的地址仍然是进程地址空间的地址,这俩个地址是不同的,一个是真实的,一个是虚拟的。

  • 加载到内存中的代码有俩个地址:
  • 物理地址是真实的地址,是用来存放加载进来的代码的。
  • 进程地址空间的地址是代码中每条语句在编译后形成的虚拟地址。

此时就有俩个地址了,而我们通过演示也知道,我们打印以及操作的地址是进程空间的地址,也就是虚拟地址,而物理空间的地址我们是操作不了的,那么它们是怎么对应起来的呢?


如上图所示,操作系统会维护一个叫做页表的东西,如上图中橘色框所示,左边是加载到内存中的的可执行程序,包括代码以及数据。

  • 在页表的左边,是进程的虚拟地址
  • 在页表的右边,是进程的物理地址

这样一来,虚拟地址和物理地址虽然不一样,但是就一一对应了起来,这个工作是由操作系统进行的。

当进程执行起来以后,堆区和栈区有数据存入的时候,同样会分配虚拟地址,并且在内存中也会有一个真实的物理地址,操作系统再通过页表将虚拟地址和物理地址对应起来。

  • 补充知识点:
  • 在32为的系统上,物理内存的大小是4GB,每4KB被叫做一页。
  • 所以说,物理内存中有4GB/4KB页

编译器在进程编译的时候,还有一种比较老的方式,就是采用相对地址偏移的方式,此时编译出来的可执行程序,只有一个基地址,之后的所有代码都是在这个基地址的基础上做加减法,而加减的值就是偏移量。

这种方式下,页表中的左边,也就是存放虚拟地址的那一列,存放的就是基地址和偏移量,而不是具体的虚拟地址了。

而现在大部分采用的方式都是第一种,就是直接使用具体的虚拟地址,而不使用偏移量。


多个进程的时候,每个进程都有一个进程地址空间,它们的划分规则都是一样的,并且它们都有各自的页表,通过各自的页表,映射到物理内存中去。

如上图中所示,子进程和父进程即使它们各自的进程地址空间完全一样,但是当映射到内存中以后,它们的物理地址是不一样的,各占一块空间。

  • CPU在取指令的时候,它读到的地址同样是虚拟地址,因为它是根据PCB中的task_struct中的进程属性找到进程在内存中对应的代码的。
  • 而PCB是由操作系统维护的,操作系也遵循着虚拟地址空间的规则,所以它维护的PC指针中的值就是虚拟地址。
  • CPU需要拿上虚拟地址通过页表才能找到指令在物理内存中的真正位置。

此时就可以回答最开始那个问题了,为什么同一个全局变量在父子进程中的值不同,但是地址是相同的?

如上图中,有俩个进程,假设它们一个是父进程,一个是子进程,就像本喵上面写的程序那样。

在父子进程的进程地址空间中,相同的地址处都有一个全局变量global_value,通过各自的页表,映射在了物理内存中。

  • 在这个全局变量每一发生改变的时候,父子进程中该值是一样的,所以在物理内存中俩个进程只需要映射到一块物理空间即可。
  • 当子进程改变了它的全局变量,此时由于俩个进程该变量的值不一样了,所以不能再映射到一块物理空间中了。
  • 所以此时操作系统会将原本在物理空间中的全局变量拷贝一份,放在另一块空间中,并且将新物理空间的物理地址更新到子进程的页表中。
  • 此时父子进程中,全局变量在各自的进程地址空间中的虚拟地址仍然是相同的,但是各自页表中对应的物理地址已经不同了。

在上诉过程中,当子进程将全局变量改变以后,操作系统将原本物理空间中的值拷贝一份放到新的空间中,这一行为叫做写时拷贝。

写是拷贝,故名思意,只有在写入的时候才会发生的拷贝行为。

🥞进程地址空间存在的意义

  1. 如果让进程直接访问物理内存,万一进程越界非法访问呢?

进程地址空间的存在保证了系统的安全。如果进程直接访问物理空间,当进程访问的地址是非常重要的空间时,此时就是非法访问。

但是有了虚拟地址空间,进程想访问哪里就访问哪里,当操作系统将进程的虚拟地址和物理地址做映射的时候,系统会发现这是一次非法的方法,就可以直接拒绝。

此时的非法请求从进程发出,还没有到达物理内存就被拒绝了,对物理内存没有任何的影响。

  1. 进程地址空间的存在,可以更方便的进行进程和进程的数据代码之间的解耦,保证了进程的独立性。

不同的进程即使操作同一块空间,因为虚拟地址空间的存在,操作系统在将不同进程的相同地址和物理空间做映射的时候,就可以在物理内存中分配不同的物理空间供不同的进程使用。

如此一来,即使进程操作的是相同的地址,但是映射到物理内存中后,操作的物理空间就不同了,并不会互相影响,保证了进程的独立性。

  1. 让进程以统一的视角来看待进程对应的代码和数据等不同的区域,也方便编译器以统一的视角规则来进行编译。

进程空间地址的存在,让进程以为自己拥有所有的空间,从而可以随意进行操作,而不用考虑是否会影响到其他的进程。

同样的,编译器在编译不同进程的时候,只需要按照进程地址空间一套规则编译即可,每个进程都一视同仁,不用考虑不同进制之间的影响。

总的来说,进程地址空间的存在,就是让各个进程只做自己的事而不用考虑其他人,编译器在编译的时候也只需要考虑一个进程。

不同进程的虚拟地址最后会由操作系统通过页表与物理内存映射起来。

🍟总结

优先级以及环境变量是一些概念性的东西,理解起来没有难度。而进程地址空间是比较难理解的,要时刻牢记我们操作的地址都是虚拟地址,这个地址是假的,可以随意操作,但是这个地址又必须在物理内存中对应存在,操作系统通过页表将虚拟地址和物理地址映射了起来。

总之,操作系统为了各个进程的正常运行以及系统的正常工作,做了很多的工作。

Linux--进程概念

前言:

        在学习操作系统的过程中,我们常常能听到进程这一概念以及相关的一些知识。例如什么是父进程,什么是子进程,如何创建子进程,如何杀死进程等。这些知识后面会一一介绍,在迈入学习进程的第一步我只需要知道在winds系统下任务管理器中的应用就是进程,而它们都是可执行文件即,所以换句话话说:进程其实就是一个可执行程序的实例

那么进程在系统中到底扮演的是什么角色呢?

进程在系统中的作用是非常重要的,程序的运行需要系统资源的支持,而操作系统中各种各样的程序运行,其资源的合理分配和各种的程序运行,其资源都是合理的分配的各种程序之间的协调都是进程的工作

在进程概念这一节,会从冯诺依曼体系结构开始说起--为了构建一个整体框架,便于后续的理解,然后我们在深入理解进程概念-PCB,创建进程,掌握僵尸进程和孤儿进程,及其形成原因和危害,了解进程调度,Linux进程优先级,理解进程竞争性与独立性,理解并行与并发。理解环境变量,熟悉常见环境变量及相关指令, 进程地址空间等等。当我们学习这些后,我们会发现进程是对正在运行程序的一个抽象,操作系统其他的所有内容都是围绕着进程的概念展开的。

目录

前言:

冯诺依曼体系结构

操作系统(Operator System)

操作系统的概念

如何理解管理

描述与组织

理解管理软硬件资源

系统调用

 计算机的软硬件体系结构图

​编辑总结

进程

操作系统对进程的管理

先描述

再组织

task_ struct内容分类

Linux中的-PCB

查看进程

查看进程

见见系统调用 

通过系统调用创建进程-fork初识 

进程状态

宏观概念

 挂起状态

Linux操作系统的的状态

进程状态查看 

两个特殊的进程

 Z(zombie)-僵尸进程

僵尸进程危害

孤儿进程

进程优先级

基本概念

查看系统进程 

用top命令更改已存在进程的nice:

其他概念

进程切换

环境变量

基本概念

常见环境变量 

和环境变量相关的命令

通过代码如何获取环境变量

程序地址空间

用段代码感受父子进程地址

感性理解程序地址空间

程序地址空间是什么

程序地址空间--区域划分

为什么会存在进程地址空间


冯诺依曼体系结构

        数学家冯·诺依曼提出了计算机制造的三个基本原则,即采用二进制逻辑、程序存储执行以及计算机由五个部分组成(运算器控制器存储器输入设备输出设备),这套理论被称为冯·诺依曼体系结构。我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。

 那么关于冯诺依曼体系,有几个问题:

1.这里的存储器指的是什么?

这里的存储器指的是内存,它的特点是掉电已失。

2.输入/输出设备是外设吗?

运算器和控制器的特点是什么?

运算器+控制器+其他=cup,运算器和控制器的计算速度是特别快

CPU的特点是什么?

CPU只能被动的接受别人的指令或数据,然后执行别人的指令,从而到达计算别人数据的目的。 

CPU如何认识别人的指令呢? 

CPU拥有自己的指令集,分别是精简指令集和复杂指令集。

我们写代码,编译的本质是什么?

CPU通过自己的指令集去识别二进制可执行文件是在做什么。换句话说就是将二进制可执行文件翻译成CPU的指令集,CPU再进行计算。

CPU的数据从哪儿来?

这里就会有一个例子,就是木桶原理:水桶装水的多少不是取决于最长的木板,而是最短的。

那么CPU也是一样的,CPU在读取或写入的时候,在数据层面上来说,CPU读取数据只和较快的内存打交道。

结论:

1.CPU不和外设直接打交道,和内存打交道

2.所有的外设,有数据需要载入,只能载入到内存中,内存写出,也一定是写到外设中

对冯诺依曼的理解,不能停留在概念上,要深入到对软件数据流理解上,请解释,从你登录上qq开始和某位朋友聊 天开始,数据的流动过程。从你打开窗口,开始给他发消息,到他的到消息之后的数据流动过程。如果是在qq上发 送文件呢? 

操作系统(Operator System)

操作系统的概念

是一个进行软硬件资源进行管理软件     

为什么要进行管理? 

通过合理的管理软硬件资源,为用户提供良好的(稳定的,高效的,安全的)执行环境

如何理解管理

在日常生活中,老板管理员工,管理的是员工的数据--员工的业绩,工资,姓名,性别等。如果老板看见员工业绩不好,想辞退该员工,那么只需要将该员工的数据从公司删除即可。

管理的本质:是对数据进行管理

这里老板是管理者,他根据数据做了决策,为了能让这个决策生效,所以就有了执行者(组长)

描述与组织

在做决策前需要描述,当描述完后再对描述的对象进行组织

描述就相当于对员工进行描述,在公司可能会制作一个表格,这里我们将当老板是一个懂计算机的,形成一个结构体,结构体里存着员工信息。形成结构体后,通过所学的数据结构将结构体形成链表,对数据进行增删改查的操作。

描述:

组织:

 在计算机中,所有管理的本质逻辑:先描述,再组织

理解管理软硬件资源

比如:老板(软件)管理一个公司,不仅仅是管理员工(软件),还要管理公司里的电脑,桌子,板凳(硬件)。

那么操作系统(软件)来说,操作系统既能管理硬件--磁盘,网卡,显卡....(硬件),又能管理软件--进程管理,文件系统,驱动管理...(软件)

系统调用

为什么会产生系统调用?

操作系统不允许用户直接操作各种硬件资源,因此用户程序只能通过系统调用的方式来请求内核为其服务,间接地使用各种资源。

总结就是:操作系统为了安全,通过操作系统接口进行对软硬件的数据的输入/输出。

 计算机的软硬件体系结构图

总结

计算机管理硬件

1. 描述起来,用struct结构体

2. 组织起来,用链表或其他高效的数据结构

系统调用和库函数概念

在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分 由操作系统提供的接口,叫做系统调用。 系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而形成库,有了库,就很有利于更上层用户或者开发者进行二次开发。

进程

上面就说过进程其实就是一个可执行程序的实例,那么程序的本质就是文件,放在磁盘上的

通过冯诺依曼结构体系,我们也大概明白了,可执行程序放在磁盘中,因为磁盘访问太慢,CPU进行数据计算的时候,操作系统会将文件放入内存中,CPU再进行处理。

操作系统对进程的管理

在内存中加载的程序很多,如果不管理,就会出现不知道那个程序先执行,那个程序后执行,如果程序挂掉,或者一直未被执行,那么这个时候是需要操作系统进行管理的。管理--先描述,再组织

先描述

当CPU处理数据前,操作系统会对每个进程的进行管理,形成一个结构体--task_struct,便于操作系统好管理,该结构体就放着进程的各个数据。

struct task_struct

                //进程的所有属性

                //该进程对应的代码和属性地址

                struct task_struct* next;

struct task_struct *pl = malloc(struct task_struct)
pl->..= XXX
p1->addr = 代码和数据的地址

再组织

所谓的对进程进行管理,就变成了进程对应的PCB进行相应的管理,PCB为结构体就就转化成对链表的增删改查。操作系统是通过对struct task_struct进行管理,而struct task_struct中相当于有许许多多的该进程对应的代码和属性地址,它通过  struct task_struct* next进行对每个进程向链接。操作系统只对PCB进行管理而不进行计算,当CPU进行计算的时候,操作系统就会把该进程放再前面让CPU进行处理。CPU就通过获取PCB的struct task_struc中的进程地址空间--一段范围,对内存中的该文件的数据进行识别,然后进行计算,进程地址空间后续会讲。

task_ struct内容分类

标示符: 描述本进程的唯一标示符,用来区别其他进程。

状态: 任务状态,退出代码,退出信号等。

优先级: 相对于其他进程的优先级。

程序计数器: 程序中即将被执行的下一条指令的地址。 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针

上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。

I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。

记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。 其他信息

Linux中的-PCB

进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合,被称之为PBC。Linux操作系统下的PCB是:task_struct--进程控制块(结构体)

Linux中task_struct用来控制管理进程,结构如下:

struct task_struct

 //说明了该进程是否可以执行,还是可中断等信息
    volatile long state; 
 //Flage 是进程号,在调用fork()时给出
 unsigned long flags; 
 //进程上是否有待处理的信号
 int sigpending;  
 //进程地址空间,区分内核进程与普通进程在内存存放的位置不同
 mm_segment_t addr_limit; //0-0xBFFFFFFF for user-thead 
      //0-0xFFFFFFFF for kernel-thread
                       
 //调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度
 volatile long need_resched;
 //锁深度
 int lock_depth; 
 //进程的基本时间片
 long nice;      

 //进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR, 分时进程:SCHED_OTHER
 unsigned long policy;
 //进程内存管理信息
 struct mm_struct *mm;
 
 int processor;
 //若进程不在任何CPU上运行, cpus_runnable 的值是0,否则是1 这个值在运行队列被锁时更新
 unsigned long cpus_runnable, cpus_allowed;
 //指向运行队列的指针
 struct list_head run_list;
 //进程的睡眠时间
 unsigned long sleep_time; 

 //用于将系统中所有的进程连成一个双向循环链表, 其根是init_task
 struct task_struct *next_task, *prev_task;
 struct mm_struct *active_mm;
 struct list_head local_pages;       //指向本地页面     
 unsigned int allocation_order, nr_local_pages;
 struct linux_binfmt *binfmt;  //进程所运行的可执行文件的格式
 int exit_code, exit_signal;
 int pdeath_signal;     //父进程终止是向子进程发送的信号
 unsigned long personality;
 //Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序
 int did_exec:1;
 pid_t pid;    //进程标识符,用来代表一个进程
 pid_t pgrp;   //进程组标识,表示进程所属的进程组
 pid_t tty_old_pgrp;  //进程控制终端所在的组标识
 pid_t session;  //进程的会话标识
 pid_t tgid;
 int leader;     //表示进程是否为会话主管
 struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr;
 struct list_head thread_group;   //线程链表
 struct task_struct *pidhash_next; //用于将进程链入HASH表
 struct task_struct **pidhash_pprev;
 wait_queue_head_t wait_chldexit;  //供wait4()使用
 struct completion *vfork_done;  //供vfork() 使用
 unsigned long rt_priority; //实时优先级,用它计算实时进程调度时的weight值
 //it_real_value,it_real_incr用于REAL定时器,单位为jiffies, 系统根据it_real_value

 //设置定时器的第一个终止时间. 在定时器到期时,向进程发送SIGALRM信号,同时根据

 //it_real_incr重置终止时间,it_prof_value,it_prof_incr用于Profile定时器,单位为jiffies。

 //当进程运行时,不管在何种状态下,每个tick都使it_prof_value值减一,当减到0时,向进程发送

 //信号SIGPROF,并根据it_prof_incr重置时间.
 //it_virt_value,it_virt_value用于Virtual定时器,单位为jiffies。当进程运行时,不管在何种

 //状态下,每个tick都使it_virt_value值减一当减到0时,向进程发送信号SIGVTALRM,根据

 //it_virt_incr重置初值。

 unsigned long it_real_value, it_prof_value, it_virt_value;
 unsigned long it_real_incr, it_prof_incr, it_virt_value;
 struct timer_list real_timer;   //指向实时定时器的指针
 struct tms times;      //记录进程消耗的时间
 unsigned long start_time;  //进程创建的时间

 //记录进程在每个CPU上所消耗的用户态时间和核心态时间
 long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];
 //内存缺页和交换信息:

 //min_flt, maj_flt累计进程的次缺页数(Copy on Write页和匿名页)和主缺页数(从映射文件或交换

 //设备读入的页面数); nswap记录进程累计换出的页面数,即写到交换设备上的页面数。
 //cmin_flt, cmaj_flt, cnswap记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。

 //在父进程回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中
 unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
 int swappable:1; //表示进程的虚拟地址空间是否允许换出
 //进程认证信息
 //uid,gid为运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid

 //euid,egid为有效uid,gid
 //fsuid,fsgid为文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件

 //系统的访问权限时使用他们。
 //suid,sgid为备份uid,gid
 uid_t uid,euid,suid,fsuid;
 gid_t gid,egid,sgid,fsgid;
 int ngroups; //记录进程在多少个用户组中
 gid_t groups[NGROUPS]; //记录进程所在的组

 //进程的权能,分别是有效位集合,继承位集合,允许位集合
 kernel_cap_t cap_effective, cap_inheritable, cap_permitted;

 int keep_capabilities:1;
 struct user_struct *user;
 struct rlimit rlim[RLIM_NLIMITS];  //与进程相关的资源限制信息
 unsigned short used_math;   //是否使用FPU
 char comm[16];   //进程正在运行的可执行文件名
 //文件系统信息
 int link_count, total_link_count;

 //NULL if no tty 进程所在的控制终端,如果不需要控制终端,则该指针为空
 struct tty_struct *tty;
 unsigned int locks;
 //进程间通信信息
 struct sem_undo *semundo;  //进程在信号灯上的所有undo操作
 struct sem_queue *semsleeping; //当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作
 //进程的CPU状态,切换时,要保存到停止进程的task_struct中
 struct thread_struct thread;
   //文件系统信息
 struct fs_struct *fs;
   //打开文件信息
 struct files_struct *files;
   //信号处理函数
 spinlock_t sigmask_lock;
 struct signal_struct *sig; //信号处理函数
 sigset_t blocked;  //进程当前要阻塞的信号,每个信号对应一位
 struct sigpending pending;  //进程上是否有待处理的信号
 unsigned long sas_ss_sp;
 size_t sas_ss_size;
 int (*notifier)(void *priv);
 void *notifier_data;
 sigset_t *notifier_mask;
 u32 parent_exec_id;
 u32 self_exec_id;

 spinlock_t alloc_lock;
 void *journal_info;
 ;

进程=内核数据结构(tast_struct)+进程对应的磁盘代码

查看进程

查看进程

myproc.c

#include <stdio.h>    
#include <unistd.h>    
    
int main()    
    
    while(1)    
        
        printf("我是一个进程!\\n");    
        sleep(1);    
        
    return 0;                                                                              
    

 Makefile

myporc:myporc.c    
    gcc -o $@ $^    
.PHONY:clean    
clean:    
    rm -f myporc      

查看进程脚本

ps ajx | head -1 && ps ajx | grep "myporc" 

  • Process ID(PID)
    Linux中标识进程的一个数字,它的值是不确定的,是由系统分配的(但是有一个例外,启动阶段,kernel运行的第一个进程是init,它的PID是1,是所有进程的最原始的父进程),每个进程都有唯一PID,当进程退出运行之后,PID就会回收,可能之后创建的进程会分配这个PID
  • Parent Process ID(PPID)
    字面意思,父进程的PID
  • Process Group ID(PGID)
    PGID就是进程所属的Group的Leader的PID,如果PGID=PID,那么该进程是Group Leader
  • Session ID(SID)
    和PGID非常相似,SID就是进程所属的Session Leader的PID,如果SID==PID,那么该进程是session leader
  • TPGID控制终端进程组ID(由控制终端修改,用于指示当前前台进程组)
  • STAT: 进程状态
  • UID:用户标识码
  • TIME:命令常用于测量一个命令的运行时间

 进程在调度运行的时候,进程具有动态属性

见见系统调用 

man getpid

测试代码:

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>                                                                     
    
    
int main()    
    
    while(1)    
        
        printf("我是一个进程!,我的ID是:%d\\n",getpid());    
        sleep(1);    
        
    return 0;    
 

每次重新运行程序pid都会改变

因为每次都需要重新加载到内存,就意味着操作系统都会重新创建task_struct,重新分配pid

 ls /proc

 “/proc/[pid]”目录,pid为进程的数字ID,是个数值,每个运行着的进                                                程都有这么一个目录。

查看父进程

#include <stdio.h>    
#include <unistd.h>    
#include <sys/types.h>    
    
    
int main()    
    
    while(1)    
        
        printf("我是一个进程!,我的ID是:%d\\n,父进程ID是:%d\\n",getpid(),getppid());                                                                                                         
        sleep(1);    
        
    return 0;    
   

 命令行上启动的进程,一般它的父进程没有特殊情况的话,都是bash

通过系统调用创建进程-fork初识 

运行 man fork 认识fork

fork有两个返回值

父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝) 

创建进程 

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
 
int main()

 int ret = fork();
 if(ret < 0)
 perror("fork");
 return 1;
 
 else if(ret == 0) //child
 printf("I am child : %d!, ret: %d\\n", getpid(), ret);
 else //father
 printf("I am father : %d!, ret: %d\\n", getpid(), ret);
 
 sleep(1);
 return 0;

进程状态

宏观概念

  总结:

1.一个CPU一个运行队列

2.让进程入队列的本质是:将该进程的struct stsk_struct结构体对象放入运行队列中

3.进程PCB在runqueue,就是R--运行状态,而不是这个进程正在运行才叫运行状态

4.进程不只会等待(占用)CPU资源,也可能随时随地要外设资源

5.所谓的进程不同的状态,本质是进程在不同的队列中,等待某种资源

 挂起状态

阻塞和挂起的区别

挂起了一定阻塞,阻塞了不一定挂起。阻塞时该进程的代码和数据都在内存中,当内存不足时,就会产生挂起状态该内存的数据和代码就会被操作系统放进磁盘中

Linux操作系统的的状态

static const char * const task_state_array[] =

"R (running)", /* 0 */

"S (sleeping)", /* 1 */

"D (disk sleep)", /* 2 */

"T (stopped)", /* 4 */

"t (tracing stop)", /* 8 */

"X (dead)", /* 16 */

"Z (zombie)", /* 32 */

;

R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。

S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。

D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的 进程通常会等待IO的结束。

T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。

X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

进程状态查看 

ps aux / ps axj 命令 

R状态--运行状态

#include <stdio.h>    
#include <unistd.h>    
    
int main()    
    
    int a=0;    
    while(1)    
        
        a=1+2;                                                      
    ;    
    
    return 0;    
    

 S状态--阻塞状态

#include <stdio.h>    
#include <unistd.h>    
    
int main()    
    
    int a=0;    
    while(1)    
        
        a=1+2;    
        printf("当前a的值是:%d\\n ",a);                                                     
    ;    
    
    return 0;    
    

为什么是S状态?

因为printf会访问显示器,显示器是外设的输入输出速度比较慢,CPU就会等待显示器就绪。99%都是在等I/O就绪,1%才是执行打印代码。所以当我们去查的时候,几乎大概率都会是S状态

t状态 --停止状态

进程处于此状态表示该进程正在被追踪,比如 gdb 调试进程: 

T状态--停止状态

测试代码  

#include <stdio.h>    
#include <unistd.h>    
    
int main()    
    
    int a=0;    
    while(1)    
        
        a=1+2;    
    ;                                                                                     
    
    return 0;    
    

kill -l

 kill -19 [pid]--停止进程 

 后台运行

  kill -18 [pid]--运行进程

后台运行--‘+’的消失

前台运行--有‘+’

 用Ctrl+c不能杀死

 就用kill -9 [pid]

总结:

先将该进程暂停后,再运行。进程状态前面的 + 号消失了,该进程变成了后台程序。但是对于后台进程来说,我们只能通过 kill 命令来杀死它。

D状态--深度睡眠

深度睡眠TASK_UNINTERRUPTIBLE:不可被信号唤醒;

浅度睡眠TASK_INTERRUPTIBLE:唤醒方式,等到需要的资源,响应信号;

深度睡眠场景:

有些场景是不能响应信号的,比如读磁盘过程是不能打断的,NFS也是;

执行程序过程中,可能需要从磁盘读入可执行代码,假如在读磁盘过程中,又有代码需要从磁盘读取,就会造成嵌套睡眠。逻辑做的太复杂,所以读磁盘过程不允许打断,即只等待IO资源可用,不响应任何信号;

应用程序无法屏蔽也无法重载SIGKILL信号,深度睡眠可以不响应SIGKILL kill-9信号;

注意:处于深度睡眠状态的进程既不能被用户杀掉,也不能被操作系统杀掉,只能通过断电,或者等待进程自己醒来。深度睡眠一般只会在高IO的情况发生下,且如果操作系统中存在多个深度睡眠状态的程序,那么说明该操作系统也即将崩溃了。

X--死亡状态

死亡状态代表着一个进程结束运行,该进程对应的PCB以及代码和数据全部被操作系统回收。

Z--僵尸状态

僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程

僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。

所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

两个特殊的进程

 Z(zombie)-僵尸进程

僵尸进程是处于僵尸状态的进程

脚本代码

while :; do ps axj | head -1 && ps axj |grep myprocess |grep -v grep; sleep 1; done

测试代码 

#include <stdio.h>                                                                         
#include <unistd.h>    
#include <stdlib.h>    
    
int main()    
    
    
        pid_t id = fork();    
        if(id == 0)    
            
            //child    
            while(1)    
                printf("I am child process, pid: %d, ppid: %d\\n", getpid(), getppid());
                sleep(5);    
                exit(1);    
                
            
        else    
            
            //parent    
            while(1)    
                
                printf("I am parent proceass, pid: %d, ppid: %d\\n", getpid(), getppid());                sleep(1);
                            
                
    return 0;

子进程:pid= 26655 

父进程:ppid=26654

当运行一段时间后的显现,26654进程因为打印变成S状态,由于26655退出了该进程变成僵尸进程。

 在上面测试代码中

 if(id == 0)    
           
            //child    
            while(1)    
                printf("I am child process, pid: %d, ppid: %d\\n", getpid(), getppid());
                sleep(5);    
                exit(1);    
               
           

子进程通过5秒后退出,结合观察得到再子进程(26655)退出后,父进程(26654)变成了僵尸状态。

26654 26655 26654  5238 pts/0    26654 Z+    1005   0:00 [myprocess] <defunct>

<defunct>  的意思失效

意味着该进程是失效的,死掉的

当我们再观察进程时,我们发现该进程不在了,其原因是关掉了父进程,该失效的子进程被系统回收了。

僵尸进程危害

进程的退出状态必须被维持下去,父进程需要一直知道子进程的状态,随时进行处理。可父进程如果一直不读取,那子进程就一直处于Z状态。

维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护。

那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费,因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!--内存泄漏

孤儿进程

测试代码

#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    
    
int main()    
    
    
        pid_t id = fork();    
        if(id == 0)    
            
            //child    
            while(1)    
                printf("I am child process, pid: %d, ppid: %d\\n", getpid(), getppid());
                sleep(1);                                                                  
                
            
        else    
            
            //parent    
            while(1)    
                
                printf("I am parent proceass, pid: %d, ppid: %d\\n", getpid(), getppid());                sleep(1);
                            
                
    return 0;
 

最开始代码跑起来时,父进程pid=8644,bash=5238;子进程的pid=8645,子进程的父进程8644;他们的运行状态都是S状态--因为在打印就会访问I/O;

当我们销毁子进程时,kill -9 8645;我们发现子打印进程不在了,但是子进程的状态还在,变成了Z状态,这个时候就只能等父进程退出,让操作系统进行回收。

 当我们杀掉父进程时,子进程被操作系统领养了,这个过程就叫做孤儿进程

 当整个进程变成孤儿了,我们发现我们用Ctrl + c 是不能退出的;我们细心就会发现最开始子进程是S+当变成孤儿进程了,它的状态就是S了。说明了该程序变成了后台程序。

那么这个时候我们就只有用kill -9 16866,杀掉子进程了

进程优先级

基本概念

cpu资源分配的先后顺序,就是指进程的优先权(priority)。

优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。

查看系统进程 

在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容: 

我们很容易注意到其中的几个重要信息,有下:

UID : 代表执行者的身份

PID : 代表这个进程的代号

PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号

PRI :代表这个进程可被执行的优先级,其值越小越早被执行

NI :代表这个进程的nice值

PRI and NI 

PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高

那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值

PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice

这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行 所以,调整进程优先级,在Linux下,就是调整进程nice值

nice其取值范围是-20至19,一共40个级别

用top命令更改已存在进程的nice:

top

1.非root用户:sudo top 进入top

2.在top中按r

3.输入进程pid

4.输入nice值

这里是输入nice值为100,我们发现最大区间是99 -19

这里是输入nice值为-100,所以我们发现最小大区间是60-20

经过上面两个例子,我们就发现其实我们只能改变范围是-20至19,一共40个级别


我们再观察一个场景,当我们把nice值改9的时候,我们发现值变成89,那么就得出结论:每次改nice值都在默认nice值的基础上进行改动,而不是修改之后。

其他概念

竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高 效完成任务,更合理竞争相关资源,便具有了优先级 独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰 并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行 并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为 并发

进程切换

测试代码

#include <stdio.h>      
      
int main()      
      
    int a=10;      
    int b=10;      
    int c=b+a;      
      
    printf("%d\\n",c);      
    a=20;      
    b=30;      
    c=b+a;      
      
   printf("%d\\n",c);                                                                                                                                                                       
    return 0;                                                                                                                   
  

我们从该代码中不难看出,a,b,c都是临时变量,在以前c语言也知道,他们的数据都是存放在寄存器里的,这里我们发现寄存器里的数据是可以发生改变的。这里我们进入vs2013查看反汇编,数据都是加载到寄存器中的。

在这里有一个概念需要知道:CPU虽然只有一套寄存器,但是寄存器的数据是属于当前进程的。这里的寄存器其实更偏向于寄存器内的数据,而不是寄存器硬件。

在这个过程中运行的时候,占有CPU进程不是一直要占有到进程结束!因为CPU虽然只有一套寄存器而且还要对其他进程进行处理,进程在运行的时候,都会有自己的时间片

操作系统会对每个进程进行设置一个时间片,让CPU去读取进程数据进行处理,就会有进入/退出--进程的切换。在这个切换的过程中寄存器数据需要被保护和恢复。

进程在切换的时候,要进行进程的上下文保护,当进程在恢复运行的时候,要进行上下文的恢复。

在任何时刻,CPU里面的寄存器里面的数据,看起来是在大家都能看到的寄存器上,但是,寄存器内的数据,只属于当前运行的进程!

寄存器被所有进程共享,寄存器内的数据,是每个进程各自私有的---上下文数据

环境变量

基本概念

环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性

常见环境变量 

PATH : 指定命令的搜索路径

我们直接输入test我们发现,编译不起

我们将我们写的可执行程序放入usr/sbin路径中,需要注意的这个文件目录只有root用户可以进去

这种是不支持的,会污染指令池;

sudo rm /usr/bin/test.o ---删除    

 一般情况下,我们是选择用

exprot PATH=$PATH:

 我们发现使用exprot PATH=$PATH:      test.o 的路径还是在11-19中

总结:

1.PATH就是系统默认的搜索路径

2.系统的指令能被找到就是因为环境变量PATH本来就默认带了系统对于的路径搜索

3.which底层实现就使用环境变量PATH来进行路径搜索

系统会默认将.bash_profile执行一次,将环境变量导到shell中,也就是说环境变量的配置也就是.bash_profile 再启动的时候加载到bash中

 vim  .bash_profile--进入.bash_profile


HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录) 

用root和普通用户,分别执行 echo $HOME ,对比差异 . 执行 cd ~; pwd ,对应 ~ 和 HOME 的关系 

SHELL : 当前Shell,它的值通常是/bin/bash。  

和环境变量相关的命令

1. echo: 显示某个环境变量值

2. export: 设置一个新的环境变量

3. env: 显示所有环境变量

4. unset: 清除环境变量

5. set: 显示本地定义的shell变量和环境变

USER

测试代码

#include<stdio.h>    
#include <string.h>    
#include <stdlib.h>    
    
#define USER "USER"                                                                                                                                                                        
    
int main()    
    
    char *who =getenv(USER);    
    if(strcmp(who,"root")==0)    
        
    
    printf("hello world\\n");    
    printf("hello world\\n");    
    printf("hello world\\n");    
    printf("hello world\\n");    
        
    else    
        printf("权限不足");    
        
    
    return 0;    
    

这里说明不同的环境变量运用的场景不同

echo- export

测试代码

#include<stdio.h>    
#include <string.h>    
#include <stdlib.h>    
    
#define USER "USER"    
#define MY_ENV "myval"    
    
int main()    
    
    
    char *myenv=getenv(MY_ENV);    
    if(NULL==myenv)    
        
        printf("%s,not found\\n",MY_ENV);    
        return 1;                                                                                                                                                                          
        
    printf("%s=%s\\n", MY_ENV, myenv);    
    
    
    return 0;    
 

我们自己定义一个变量,但是我们用echo 查看发现在当前文件能查看,但是用env去找不到。因为是这里相当于是定义的局部变量 

本地变

以上是关于Linux学习进程概念(下)的主要内容,如果未能解决你的问题,请参考以下文章

Linux学习笔记—— Linux进程概念

linux进程间通信之共享内存学习记录

linux内核学习之六 进程创建过程学习

Linux学习_线程的概念创建和终止

Linux 内核概念和学习路线

20155321 《信息安全系统设计》Linux多线程的深入学习