一、 进程优先级

0x01 什么叫进程优先级

进程访问某种资源的先后顺序(方式:排队)。

0x02 为什么要有进程优先级

因为资源不足,所以优先权高的进程有优先执行权利。

0x03 查看更加详细的进程信息

 ①运行代码

#include<iostream>    
#include<unistd.h>    
using namespace std;    
    
int main()    
{    
    while(1)    
    {    
      cout << "PID:" << getpid() << " " << "PPID:" << getppid() << endl;               
      sleep(1);    
    }    
    return 0;    
} 

 ②ps -al 查看更加详细的进程信息

③提示

UID:代表执行者的身份 ;
PRI:代表这个进程可被执行的优先级,其值越小越早被执行; 
NI  :代表这个进程的nice值,是优先级的一个修正数据,取值范围是-2019,一共40个级别。

0x04 调整优先级

1)运行程序;
2)输入top命令,输入r ,输入进程号 (优先级范围 [60,99]);
3)输入nice值 (nice值范围[-20,19])。

 如果我们再进行一次调整会是什么样的呢?

 我们知道PRI(new)=PRI(old)+nice,但是为什么不是在90上再加5呢?

因为旧的优先级默认为80。

0x05 nice值为何要是一个相对较小的范围呢?

因为若不加限制,那么将自己的进程的优先级调整的非常高,别人的优先级调整的非常低。而优先级越高的进程,优先得到资源,常规的进程则很难享受到CPU资源。从而会导致进程饥饿问题。

0x06 小结

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

二、环境变量

0x01 什么叫环境变量?

环境变量是系统内置的具有特殊用途的变量,此变量的本质是系统开辟空间,给他名字和内容。

./test,为什么用"./ " ?

因为要帮系统确认对应的程序在哪里。

那为何系统的命令不用路径呢?

因为环境变量(在操作系统中用来指定操作系统运行环境的一些参数)。
PATH 环境变量 的作用是:当你输入一个命令时,系统会按照 PATH 中列出的目录顺序,在这些目录里查找对应的可执行程序。
查看环境变量:

 0x02 那么我们可以不用./,就能将程序运行起来吗?

当然可以,我们可以将路径导入到环境变量中去(只要没有改配置文件,那么重启时就会恢复默认)。

 三种方法:

1)可以将程序test放入/usr/bin目录中;
2)如果想让导入的环境变量长期有效,可以在vim ~/.bash_profile中去修改;
3)export PATH=$PATH: 路径,将路径导入环境变量(建议)。

0x03  HOME(常用环境变量)

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

0x04 SHELL (常用环境变量)

SHELL:当前Shell版本或者Shell命令,它的值通常是/bin/bash。

0x05 与环境变量相关的命令

1. echo: 显示某个环境变量值 。
2. export: 设置一个新的环境变量。 
3. env: 显示所有环境变量 。
4. unset: 清除环境变量 。
5. set: 显示本地定义的shell变量和环境变量。

0x06 环境变量的配置文件

当前修改完环境变量后,在下次登录时,环境变量有被初始化,这是为什么?

当前修改只保存在内存中,而登录时从磁盘配置文件重新初始化。要实现永久修改,必须将更改写入相应的配置文件或注册表。

修改配置文件:vim .bash_profile

重新登陆时,修改的环境变量被添加完成:

0x07 小结

1)环境变量的本质是操作系统在内存或者磁盘文件中开辟的空间,用来保存系统相关的数据;
2)环境变量 = 变量名 + 变量内容;
3)系统上还存在一种变量,是与本次登录有关的变量,只在本次登录有效(本地变量);

4)可以将本地变量导入环境变量(重启也会释放掉这个变量)。

 三、main函数参数

0x01  argc&argv

#include<stdio.h>

int main(int argc,char* argv[])    
{    
  for(int i = 0;i < argc;i++)    
  {    
    printf("argv[%d] -> %s\n",i,argv[i]);    
  }     
  return 0;
} 
[wh@bogon lesson10]$ ./test -a -b -c -d
argv[0] -> ./test
argv[1] -> -a
argv[2] -> -b
argv[3] -> -c
argv[4] -> -d

 0x02 命令行参数的作用

 同一个程序带入不同参数来呈现不同的表现形式 (同一程序输入不同的选项呈现不同的结果)。

 下面我们通过命令函参数来做一个简易的计算器。

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<string.h>
  4 #include<stdlib.h>
  5 int main(int argc,char* argv[])
  6 {
  7   if(argc != 4)
  8   {
  9     printf("Usage: %s -[add|sub|mul|div|a|b]\n",argv[0]);
 10     return 1;
 11   }
 12   int a = atoi(argv[2]);
 13   int b = atoi(argv[3]);
 14   if(strcmp(argv[1],"add") == 0 )
 15   {
 16     printf("%d + %d = %d\n",a,b,a+b);
 17   }
 18   else if(strcmp(argv[1],"sub") == 0)
 19   {
 20     printf("%d - %d = %d\n",a,b,a-b);                                                                                                                                                                          
 21   }
 22   else if(strcmp(argv[1],"mul") == 0)
 23   {
 24     printf("%d * %d = %d\n",a,b,a*b);
 25   }
 26   else if(strcmp(argv[1],"div") == 0)
 27   {
 28     printf("%d / %d = %d\n",a,b,(int)a/b);
 29   }
 30   else{
 31      printf("reinpt");
 32   }
 33 }

0x03 env[]

main函数还有第三个参数char* env[ ]

  #include<stdio.h>    
  #include<unistd.h>    
  #include<string.h>    
  int main(int argc,char* argv[],char* env[])    
  {    
      for(int i = 0;env[i]; i++)    
      {    
        printf("%d -> %s\n",i,env[i]);                                                              
      }   
   return 0; 
  }

环境变量具有全局属性的方法:

Bash(父进程)进程创建子进程时,会创建两张表(命令行参数表和环境变量表),然后向main函数中传参,子进程会通过main函数的第三个参数进行接收。因此环境变量会被子进程和孙子进程继承,所以环境变量具有全局属性。

    1   #include<stdio.h>
    2   #include<unistd.h>
    3   #include<string.h>
    4   #include<stdlib.h>
    5   #include<sys/types.h> 
    6   int main(int argc,char* argv[],char* env[])                                                                                                                                                                                                  
    7   {
    8       printf("I am a process: PID: %d, PPID: %d\n",getpid(),getppid());
    9       for(int i = 0;env[i]; i++)
   10       {
   11         printf("%d -> %s\n",i,env[i]);
   12       }
   13       pid_t id = fork();
   14       if (id == 0)
   15       {
   16         printf("I am a child process: PID: %d, PPID: %d\n",getpid(),getppid());
   17         for(int i = 0;env[i]; i++)
   18         {
   19           printf("%d -> %s\n",i,env[i]);
   20         }
   21       }
   22       sleep(1);
   23   }

当然也可以不用main函数的第三个参数的方式,查看环境变量,也可以通过第三方变量environ来获取


  #include<stdio.h>    
  #include<unistd.h>    
  #include<string.h>    
  int main(int argc,char* argv[])    
  {    
      extern char** environ;    
      for(int i = 0;environ[i]; i++)    
      {    
        printf("%d -> %s\n",i,environ[i]);                                                          
      } 
    return 0;
  }

0x04 getenv()

 用来获取环境变量。

  #include<stdio.h>    
  #include<stdlib.h>    
  #include<string.h>    
  int main(int argc,char* argv[])    
  {    
      printf("PATH: %s\n",getenv("PATH"));    
      return 0;   
  }

识别用户小游戏:
    1   #include<stdio.h>
    2   #include<unistd.h>
    3   #include<string.h>
    4   #include<stdlib.h>
    5   #include<sys/types.h>
    6   int main(int argc,char* argv[],char* env[])
    7   {
    8     const char* username = getenv("USER");
    9     if(strcmp(username,"wh") == 0)
   10     {
   11       printf("You are a user\n");                                                                                                                                                                            
   12     }
   13     else
   14     {
   15       printf("None");
   16     }
   17     return 0;
   18   }

0x05 environ(通过第三方变量获取环境变量)

  1 #include<stdio.h>
  2 
  3 int main()
  4 {
  5   extern char** environ;//声明
  6   int i = 0;
  7   for(;environ[i];i++)
  8   {
  9       printf("env[%d]: %s\n",i,environ[i]);
 10   }
 11   return 0;                                                                                                                                                                                                    
 12 }

0x06 本地变量的设置与查看

因此,为什么每次登录时默认所在路径都是在家目录下?

因为系统首先要读取家目录下的环境变量的配置文件。

五、程序地址空间&进程地址空间

0x01 进程地址空间布局

  #include<stdio.h>    
  #include<stdlib.h>    
      
  int g_unval;    
  int g_val = 100;    
  int main()    
  {    
    const char* s = "hello world";    
    printf("code addr: %p\n",main);    
    printf("Character constant addr: %p\n",s);    
    printf("uninit addr: %p\n",&g_unval);                                                           
    printf("init addr: %p\n",&g_val);    
    char* heap = (char*)malloc(10);    
    printf("heap addr: %p\n",heap);    
    printf("stack addr: %p\n",&s);    
    printf("stack addr: %p\n",&heap);    
    int a = 10;    
    int b = 30;    
    printf("stack addr: %p\n",&a);    
    printf("stack addr: %p\n,%b");    
    return 0;    
  }  

 0x02 虚拟地址

此时我们会疑惑,上面打印的地址是物理地址?

下面我们可以通过这段代码来印证:

#include<stdio.h>    
#include<stdlib.h>    
#include<unistd.h>    
    
int g_val = 100;    
    
int main()                                                                                                                                                                                                         
{    
  if(fork() == 0)    
  {    
    //child     
    
    int cnt = 5;    
    while(cnt)    
    {    
      printf("I am child,times: %d,g_val = %d,&g_val = %p\n",cnt,g_val,&g_val);    
      cnt--;    
      sleep(1);    
      if(cnt == 3)    
      {    
          printf("############child change before!##############\n");    
          g_val = 200;    
          printf("############child change  after!##############\n");    
      }    
    }    
  }    
  else    
  {    
    //parent    
    while(1)    
    {    
        printf("I am parent,g_val = %d,&g_val = %p\n",g_val,&g_val);    
        sleep(1);    
    }    
  }    
  return 0;    
}

从上述结果来看,因为写时拷贝,俩个进程互不干扰,所以当子进程修改值时,父进程并没有影响,但是我们也可以发现他们的地址是完全一样的。

但是地址怎么会没有变化呢?

如果C/C++打印出来的地址是物理内存的地址,这种现象可能存在吗?
当然不可能,所以我们使用的地址,绝对不是物理地址,即不是内存地址空间,而是进程虚拟地址空间所以说,我们之前说的程序地址空间是有些不准确的,准确的应该说是进程地址空间。我们用C/C++语言所看到的的地址,全部都是虚拟地址,物理地址用户是看不到的,由操作系统统一管理,将虚拟地址转化成物理地址。

 0x03 进程虚拟地址空间

①进程&操作系统

1)操作系统 相当于 大富翁;
2)物理内存 相当于 大富翁的金库;
3)进程地址空间 相当于 大富翁画的大饼;
4)进程 相当于 大富翁的私生子。


接下来,我们就可以将其串联起来,叙述一个小故事:
背景:假如大富翁有100亿,都存在金库中,因为私生子之间互不认识,所以给私生子们都画了个相同的大饼。

大富翁:你们每个人都可以使用100亿,所以私生子们就都在规划这100亿该怎么使用,而三人之间又互不影响,都认为自己独占100亿。


这将其换成进程与操作系统就更加贴切了:
操作系统掌控着所有物理内存空间,给每一个进程都画了一个进程地址空间(独占物理内存)这样的大饼,而每个进程之间又互不影响,都认为自己独占物理内存。

 此时又出现了一个问题:

②每个进程都有一个地址空间,而系统中又存在大量的地址空间,那么操作系统应该怎样去管理这些地址空间呢?

先描述,再组织。

1. 先描述:用的是结构体mm_struct来描述地址空间(每个进程都认为mm_struct代表整个内存,都认为进程地址空间是按照4GB划分的);

struct mm_struct
{
        unsigned int code_start;
        unsigned int code_start;
        .....
        unsigned int stack_start;
        unsigned int stack_end;
}

2. 再组织:通过数据结构进行链接;

3. 进程地址空间的本质:特定的数据结构对象(将多个对象通过数据结构进行链接)。        

③那么我们该如何进行区域划分呢?

地址空间上进行区域划分时,对应的线性位置虚拟地址。

而进程地址空间的划分如下:

struct mm_struct
{
        unsigned int code_start;
        unsigned int code_start;
        .....
        unsigned int stack_start;
        unsigned int stack_end;
}

作用:

1. 可以判断是否越界;

2. 可以进行扩大或者缩小范围。

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

1)通过页表可以将物理内存空间上不连续、无序的内存空间映射为有序的进程空间地址,让进程以统一的视角看待内存;

2)有了进程地址空间,进程会认为自己独占内存,从而更好的保障进程的独立性以及合理使用内存。这样能将进程管理与内存管理进行解耦合;

3)进程地址空间+页表的设计是保护内存安全的重要手段。

⑤此时我们就可以解释一下为什么父子进程中值不同,但是地址相同的原因:

 

1)因为子进程是以父进程为模板创建的,所以开始子进程物理地址也是指向父进程g_val位置,所以这也是前3秒,父进程和子进程打印的值相同的原因;

2)之后子进程的值改成了200,又因为进程是具有独立性的,父进程与子进程之间互不干扰,所以此时就发生了写时拷贝,在物理内存中重新开辟了一块空间,给子进程进行使用,然后再重新进行虚拟地址与物理地址之间的映射(写时拷贝的原因);
3)所以父进程与子进程打印的地址是一样的,是虚拟地址没有改变;
4)父子进程一般代码是共享的,此时就可以知道,都是通过映射关系,指向了同一块物理空间中的代码,所以只读的数据,一般可以只有一份,因为这是操作系统维护成本最低的。

0x04 malloc和new 开辟空间

在没学习进程地址空间时,我们会认为开辟空间,是开辟内存的空间。学习之后,我们会认为并不是如此。

1)当进程执行malloc/new操作时,因为该进程并不会立即使用内存,所以OS并不会分配物理内存,而会分配进程地址空间(虚拟地址);

2)当检测到该虚拟内存要进行修改写入操作时,会发生缺页中断,此时OS会更新页表中映射关系,申请物理内存。

提示:
缺页中断是进程访问一个合法的虚拟页时,由于该页当前未被加载到物理内存(页表项标记为无效),由硬件触发、由操作系统处理的中断。操作系统负责将所需的页从磁盘调入内存、更新页表,然后让进程重新执行被中断的访问指令。


作用:

1)充分保证内存的使用率;

2)因为实际没有在内存上开辟空间,所以提升malloc/new的速度。

六、页表及写时拷贝触发机制

页表不仅只有虚拟地址和物理地址,还有其他属性,比如权限。

问题1:我们写代码时,常量不可修改是谁决定?

答:是由操作系统在页表的权限属性上设置为只读决定的,这也是运行时保护的最后一道防线
当要对数据进行修改或写入时,首先需要将虚拟地址转化为物理地址。转化的过程中,操作系统发现该权限为只读,所以该数据不可修改不可写入。

问题2:const修饰的数据是不是也由页表决定?

答:不是。const的意义是将可能在运行阶段出现的错误提前在编译阶段发现并报错,是一种防御性编程。

问题3:操作系统如何知道何时进行写实拷贝?

答:操作系统通过页错误异常机制来感知写时拷贝的触发时机。
1. 当父进程调用fork()创建子进程后,两者共享相同的物理内存页,但操作系统会故意将对应页表项的权限设置为只读(即使进程的虚拟地址空间描述为可读可写)。
2. 当任一进程尝试写入该共享内存时,CPU的MMU会在地址转换过程中检测到写权限缺失,从而触发页错误异常。
3. 内核的页错误处理程序随后检查异常地址和页表状态,确认该页为写时拷贝共享页后,便执行写时拷贝操作:分配新的物理页、复制原页内容、更新当前进程的页表指向新页并设为可写,而其他进程仍保持只读共享原页。 

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐