当前位置:网站首页>进程概念(下)

进程概念(下)

2022-09-23 08:42:15鹿九丸

环境变量

引入

问:为什么我们执行自己编译的可执行程序的时候必须带路径,但是执行系统命令的时候比如lspwd等命令的时候不需要带路径?(注:ls、pwd等命令本质也是可执行程序,存储在路径/usr/bin/目录下)

答:系统中是存在相关的环境变量的,保存了程序的搜索路径的。

基本概念

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

注意:在Linux中有环境变量和普通变量两种变量,环境变量就是上面所描述的,普通变量就像下面这一种:

image-20220817150757732

上面的aaaa就是普通变量,无法通过env环境变量命令查看到。

问:我们自己如何定义环境变量?

答:可以通过export NAME=xxx来进行定义(NAME是环境变量名,xxx是我们想给环境变量定义的值)

image-20220817151148943

image-20220817151159703

问:如何取消定义的环境变量?

答:通过unset NAME命令可以进行取消我们自己定义的环境变量:

image-20220817152758382

常见环境变量

  • PATH : 指定命令的搜索路径
  • HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
  • SHELL : 当前Shell,它的值通常是/bin/bash。

查看环境变量方法

使用env命令查看环境变量:

image-20220816202007581

echo $NAME//NAME:环境变量的名称

使用举例:

image-20220816203631323

此时就可以回答了,使用lspwd等命令时,能够正常使用的原因是这些命令对应的可执行程序在PATH环境变量所标明的路径下,我们自己的可执行程序不能像这样运行的原因是因为我们自己的可执行程序并没有在PATH路径下。

问:如果我们想直接使用我们编译形成的可执行文件该如何去做?

答:

方法一:将编译形成可执行文件拷贝入到PATH路径下。(使用sudo cp NAME PATH)//NAME是可执行程序文件名,PATH是

方法二:将当前文件的路径添加到PATH路径下。

image-20220817151924073

image-20220817152239604

注意:在修改环境变量时,上面的方式是属于新增,而不是覆盖,最好不要进行覆盖,但是一旦覆盖了的话可以通过重启终端来解决,因为我们进行的修改只是在内存上进行的修改,所以并不会有任何的影响。

注意:which命令之所以能够找到我们的命令所在路径就是通过环境变量查找可执行程序的。

image-20220817152509261

image-20220817155743199

和环境变量相关的命令

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

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

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

  4. unset: 清除环境变量

  5. set: 显示本地定义的shell变量和环境变量(查看普通变量和环境变量)

    image-20220817182343567

环境变量的组织方式

image-20220817182620998

每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串

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

先了解C/C++main()函数的前两个参数:

代码:
image-20220817184103206

运行结果:

image-20220817184148855

分析argv结构:

image-20220817184624796

从上面可以知道,给main()函数传递的argc、argv[],命令行参数传递的是,命令行中输入的程序名和选项。

问:main()函数传递命令行选项的意义何在?

答:通过传入不同的参数,让同一个程序有不同的执行逻辑和执行结果,这也是为什么我们能够通过对指令进行不同的选项能够有不同表现的原因。

使用举例:简易计算器:

代码:

#include<stdio.h>
#include<string.h>
//./process -a 10 20
//10 + 20 = 30
//./process -s 10 20
//10 - 20 = -10
//./process -m 10 20
//10 * 20 = 200
//./process -d 10 20
//10 / 20 = 0
int main(int argc, char* argv[])
{
     
 if(argc != 4)
 {
     
     printf("Usage: %s [-a|-s|-m|-d] firstData secondData\n", argv[0]);
     return 0;
 }
 int x = atoi(argv[2]);//atoi()函数的作用是将char*类型的字符串转换为整数
 int y = atoi(argv[3]);
 if(strcmp("-a", argv[1]) == 0)
 {
     
     printf("%d + %d = %d\n", x, y, x + y);
 }
 else if(strcmp("-s", argv[1]) == 0)
 {
     
     printf("%d - %d = %d\n", x, y, x - y);
 }
 else if(strcmp("-m", argv[1]) == 0)
 {
     
     printf("%d * %d = %d\n", x, y, x * y);
 }
 else if(strcmp("-d", argv[1]) == 0 && y != 0)
 {
     
     printf("%d / %d = %d\n", x, y, x / y);
 }
 else
 {
     
     printf("Usage: %s [-a|-s|-m|-d] firstData secondData\n", argv[0]);
     return 0;
 }
 return 0;
}

使用详情:

image-20220817190828365

  • 命令行第三个参数
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
    
	int i = 0;
	for(; env[i]; i++)
    {
    
		printf("%s\n", env[i]);
	}
	return 0;
}
  • 通过第三方变量environ获取
#include<stdio.h>
int main()
{
    
    extern char** environ;
    int i = 0;
    for(; environ[i]; i++)
    {
    
        printf("%s\n", environ[i]);
    }
    return 0;
}

libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时 要用extern声明。

通过系统调用获取或设置环境变量

  • getenv()函数,获取环境变量

image-20220817205527578

使用举例:

#include<stdio.h>
int main()
{
    
    char* path = getenv("PATH");
    printf("PATH:%s\n", path);
    return 0;
}

环境变量通常是具有全局属性的

  • 环境变量通常具有全局属性,可以被子进程继承下去

代码:

image-20220818103721884

执行结果:

image-20220818103659442

注意:创建的myproc进程中的环境变量也是从bash进程继承到的

注意:所谓的本地变量,本质就是在bash内部定义的变量,不会被子进程继承下去。

问:既然本地变量不会被子进程继承,那么我们echo $NAME是如何将本地变量进行打印的?

答:Linux下的大部分命令都是通过子进程的方式进行执行的,但是还有一部分命令不通过子进程的方式进行执行,而是由bash自己执行(直接调用自己的对应的函数来完成特定的功能),我们把这类命令叫作内建命令。

程序地址空间

image-20220818105830721

测试代码:

image-20220818115340693

运行结果:

image-20220818115318290

注意:静态局部变量和全局变量存储的位置是一样的,即全局数据区

代码感受:

image-20220818161605052

执行结果:

image-20220818161636827

子进程和父进程的g_val的值和地址是一样的。

将代码进行如下修改:

image-20220818162027599

image-20220818161948228

通过上述代码发现:子进程和父进程读取的同一个地址的同一个变量,但是g_val在子进程中修改后,g_val的值只在子进程中发生了改变,即变成了20,但是父进程中g_val的值依旧是10。

结论:我们在C/C++中使用的地址不是物理地址,因为如果是物理地址,就不会出现上面的情况,同一个物理地址中存储的必然是同一个值。

在上面中的地址是虚拟地址!操作系统负责将虚拟地址转化为物理地址

进程地址空间

每一个进程在启动的时候,都会让操作系统给它创建一个地址空间,该地址空间就是进程地址空间。

注意:所谓的进程地址空间,其实是内核的一个数据结构,和task_struct类似,类型叫struct mm_struct。

struct mm_struct
{
    
    //代码区
    long code_start;
    long code_end;
    //初始化全局数据区
    long init_start;
    long init _end;
    ······
}

问:程序被编译出来,没有被加载到内存的时候,程序内部有地址吗?程序内部有区域吗?

答:可执行程序在编译后,本身就是有自己的一套地址,并且本身也是有区域的,这些都在磁盘上已经划分好了。

image-20220818185833608

编译程序的时候,就认为程序是按照0000~FFFF进行编址的。

对上面g_val现象进行解释:

修改前:

image-20220818192640546

修改后(写时拷贝):

image-20220818192947381

因为打印的始终都是虚拟地址,所以即使修改后,&g_val的值仍然没有发生改变。

问:fork有两个返回值,pid_t id,同一个变量,为什么会有不同的值?

答:pid_t id是父进程栈空间中定义的变量,fork内部,return会被执行两次,return的本质,就是通过寄存器将返回值写入到接收返回值的变量中!当id = fork()的时候,谁后返回,谁就要先发生写时拷贝,所以同一个变量,会有不同的值,本质是因为大家的虚拟地址是一样的,但是大家对应的物理地址是不一样的。

问:页表是一开始就形成的,还是在运行的时候动态生成的?

答:页表是一开始就形成的,但是有一些是动态生成的,比如堆空间。

问:为什么要有虚拟地址空间?

答:1. 虚拟地址空间为访问内存添加了一层软硬件层,可以堆转化过程进程审核,非法的访问可以直接进行拦截,即保护内存的作用。2. 当我们扩大申请的内存空间时,扩大的只是地址空间,起到了节约内存的作用,并且通过地址空间在进程管理和Linux内存管理两个功能模块之间进行结耦。3. 让进程或者程序可以以一种统一的视角看待内存,即每个进程都有一个自己的进程地址空间,方便以统一的方式来编译和加载所有的可执行程序,进而简化进程本身的设计与实现。

Linux2.6内核进程调度队列

image-20220819092431404

上图是Linux2.6内核中进程队列的数据结构,

一个CPU拥有一个runqueue

  • 如果有多个CPU就要考虑进程个数的负载均衡问题

优先级

  • 普通优先级:60~99

活动队列

  • 时间片还没有结束的所有进程都按照优先级放在该队列

  • nr_active: 总共有多少个运行状态的进程

  • queue[140]: 一个元素就是一个进程队列,相同优先级的进程按照FIFO规则进行排队调度,所以,数组下 标就是优先级!

  • 从该结构中,选择一个最合适的进程,过程是怎么的呢?

    1. 从0下表开始遍历queue[140]
    2. 找到第一个非空队列,该队列必定为优先级最高的队列
    3. 拿到选中队列的第一个进程,开始运行,调度完成!
    4. 遍历queue[140]时间复杂度是常数!但还是太低效了!
  • bitmap[5]:一共140个优先级,一共140个进程队列,为了提高查找非空队列的效率,就可以用5*32个 比特位表示队列是否为空,这样,便可以大大提高查找效率!

过期队列

  • 过期队列和活动队列结构一模一样
  • 过期队列上放置的进程,都是时间片耗尽的进程
  • 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算

active指针和expired指针

  • active指针永远指向活动队列
  • expired指针永远指向过期队列
  • 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在 的。
  • 在合适的时候,只要能够交换active指针和expired指针的内容,就相当于有具有了一批新的活动进程!

总结

  • 在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增 加,我们称之为进程调度O(1)算法!
原网站

版权声明
本文为[鹿九丸]所创,转载请带上原文链接,感谢
https://blog.csdn.net/m0_57304511/article/details/126995384

随机推荐