Linux从0到1——进程概念(下)

1. 进程优先级


1.1 基本概念


1. 优先级是什么?

  • 本质是得到某种资源的先后顺序。

2. 为什么需要确定优先级?

  • 本质上是因为计算机内的资源不足(软硬件资源)。

3. 怎么做?

  • Linux中的优先级,其实就是PCB中的一个int字段,值越小,表示优先级越高。数值范围为60~99;
  • Linux中默认进程的优先级都是80。

4. 验证优先级的存在

  • 先写一段代码:
    在这里插入图片描述

  • 编译并运行这段代码,同时执行命令ps -al
    在这里插入图片描述

  • 可以看到进程有一个PRI属性,是英文Priority的缩写,代表进程的优先级。

  • 其中UID代表执行者的身份。


1.2 Linux支持动态优先级调整


1. PRI和NI:

  • PRI就是进程的优先级,而NInice值,是进程优先级的修正数据;
  • 进程优先级的更新公式可以理解为:PRI(new) = PRI(old) + nice
  • 注意,PRI(old)默认为80,是一个固定值;
  • nice最小是-20,超过-20统一按-20处理;nice最大是19,超过19统一按19处理。

2. 为什么要把优先级限定在一定范围内?

  • OS在调度时,需要较为均衡的让每一个进程都得到调度。如果某一个进程的优先级过低,就会长时间得不到CPU资源,不能被调度,这种现象称为——进程饥饿。

3. 动态调整优先级:

  • 输入top指令,输入r,输入待调整的进程pid,最后输入nice值(下面的例子还是用的1.1中的代码)。
    在这里插入图片描述
  • 上面的例子中,我们设置该进程nice值为10,它的PRI就变成了90。
  • 注意:
    • 增大优先级(nice值为负),需要root权限;减小优先级(nice值为正),不需要root权限。
    • 一个普通用户,只有修改自己进程优先级的权限。

1.3 其他概念


1. 其他概念:

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

2. 重点理解进程切换:

  • 每一个进程,不是占有CPU就一直运行的,会每隔一段时间,自动从CPU剥离下来(这个时间间隔称为时间片);
  • Linux内核是支持进程之间进行CPU资源抢占的,称为基于时间片的轮转式抢占内核
    • 简单理解一下:假如时间片是1ms,那么每个进程,运行1ms后就会被自动剥离下来。Linux还支持进程间CPU资源抢占,如果一个进程已经运行了0.5ms, 但是来了一个优先级更高的进程,那么这个已经运行了0.5ms的进程就会被强制剥离下来,即使时间还没到,以便让这个优先级更高的进程抢占CPU资源。

3. 认识寄存器:

  • CPU中是有很多很多寄存器的。
    在这里插入图片描述
  • 上图的代码中,a是临时变量,临时变量出了作用域是会被销毁的,那么a是如何通过return返回给外部的呢?答案就是,a这个临时变量的值,会先存到eax寄存器中,然后通过这个寄存器返回给外部。
  • 进程在运行时,它怎么知道当前运行到了哪一行?如何实现函数跳转?
    • 在CPU内,有一个程序计数器eip,也叫pc指针,它会记录程序运行到了哪里。
  • 如果我们有多个进程,各个进程在CPU寄存器中形成的临时数据都应该是不一样的。这一部分数据,就称为进程的硬件上下文。
  • 寄存器 != 寄存器的内容。CPU寄存器硬件只有一套,如果此时有10个进程,那么10个进程的上下文数据就有十套。
  • 一个进程在运行时,寄存器中会保存该进程的各种临时数据。

4. 进程切换是如何做到的?

  • 实现进程切换,就必定要考虑几个问题。
    • (1)一个进程可能运行到一半被剥离了,那当它再次被调度时,CPU怎么知道当前进程运行到了哪里?
    • (2)进程在运行时,必定会产生很多的上下文数据,当这个进程再次被调度时,之前的上下文数据又在哪里?
  • 之前我们知道了,一个进程在运行时,寄存器中会保存该进程的各种临时数据。牢记一点,保存的目的,是为了恢复。一个进程在切出时,会将寄存器中保存的临时数据放进自己的PCB中,在切入时,又会将PCB中保存的数据放回寄存器(这个问题其实很复杂,我们在这里只是暂时这样简单理解)。
    • 注意:早期的内核是这样做的,但是如今的操作系统已经不这样做了,因为PCB越来越大了。
  • 将CPU内的寄存器保存到进程PCB中,本质上就是将CPU寄存器的内容,保存到内存中。

进程切换时,寄存器中的内容不用清空,而是直接被覆盖。


2. Linux2.6内核进程调度队列(选学)


1. 实时操作系统、分时操作系统:

  • 实时操作系统:严格遵循优先级高的先调度,所有优先级低的进程统统给优先级高的进程让路。
    • 现在有一个车载系统,刹车这个进程的优先级肯定是最高的。如果此时采用分时操作系统,公平调度,车载系统说现在你还在放音乐呢,等这个进程结束了,再刹车,这就扯淡了。 所以车载系统肯定是采用实时操作系统,所有进程给刹车进程让路。
  • 分时操作系统:不严格遵循优先级高的先调度。按时间片的方式分配资源。

Linux操作系统是实时兼分时的操作系统。

2. Linux2.6中的调度原理:

在这里插入图片描述

  • Linux中的普通优先级只有[66, 99]。我们可以发现,Linux的调度队列有140个优先级,我们是做网络开发的,前[0, 99]个优先级用不上,这里就不再介绍了。主要用到的就是后[100, 139]优先级,其中queue[100]就对应60优先级,queue[139]就对应99优先级。
  • 所谓的调度队列,其实就是task_struct的指针数组。从上向下遍历数组,只要出现不为空的位置,就执行对应位置的进程(可能有多个)。进程是以队列的方式,链入到调度队列对应优先级的位置中的。来一个进程,就在后面链接一个。
    在这里插入图片描述
  • 考虑一个问题,上图中,如果链入了一个优先级为99的进程,还没有被执行。此时再不断地向queue中链入优先级为80的进程,那么每次遍历queue时,queue[120]位置的值都不为空,优先执行优先级为80的进程。这样就会造成进程饥饿,即优先级为99的进程永远不会被调度,这就不符合公平的原则了。
  • 为了解决这个问题,Linux2.6内核中,设计了两个queue,一个是活动队列active,一个是过期队列expired。看下图,此时活动队列中已有的进程如下,60优先级的有三个,80优先级的有1个,99优先级的也有一个。此时又想在queue中链入新进程(一个60,两个80),就不会直接链入到活动队列中了,会先链入到过期队列中,等活动队列中的进程都调度完了(这里只是简单这样理解),再交换activeexpired指针,调度刚才过期队列中的进程,体现了局部的优先级特性,也体现了公平性。

在这里插入图片描述

  • 注意:交换发生时,活动队列中的进程,不一定是被彻底执行完了,但是active队列一定为空。比如此时有一个80优先级的进程进入了阻塞状态,它并没有执行完,但是需要从活动队列中剥离,放入阻塞队列中,等资源就绪时,再将这个进程放入过期队列,等待下一次调度。
  • 确定优先级需要从上到下遍历数组,还是太麻烦了,Linux中采用位图bitmap来记录queue的各个位置为不为空,为空是0,不为空是1,这样就可以8位8位或16位16位的检测了,大大提升了检测效率。
  • 判断活动/过期队列是否为空,是根据nr_active来判断的。
  • 这就是Linux2.6的大O(1)调度算法!

3. 命令行参数


3.1 认识命令行参数


1. main函数是可以传参的:

  • 写一段代码。
    在这里插入图片描述
    • 其中argcargv这两个参数叫命令行参数。
  • 试着编译并运行一下上述代码:
    在这里插入图片描述
    • 可以看到,main函数接受了我们传进去的-q-a,和-w
    • 这个工作是由shell做的,它将我们输入的一串字符,以空格为分割符,进行分割。能分割出几个字符串,argc就是几,argv数组就存储这些分割好的子串。

2. 画图理解:

在这里插入图片描述

3. 命令行参数的意义:

  • 命令行参数,可以支持各种指令级别的命令行选项的设置!

3.2 手动实现一个简单加/减/乘/除计算器


1. 代码:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>

int main(int argc, char *argv[])
{
    if (argc != 4)
    {
        printf("Use error: %s op[-add|sub|mul|div] d1 d2\n", argv[0]);
        return 1;
    }
    
    // 将字符串转成int
    int x = atoi(argv[2]);
    int y = atoi(argv[3]);

    int result = 0;
    // 你的程序一定有4个命令行参数,第一个参数是程序名
    if (strcmp(argv[1], "-add") == 0)
    {
        result = x + y;
        printf("%d+%d=%d\n", x, y, result);
    }
    else if (strcmp(argv[1], "-sub") == 0)
    {
        result = x - y;
        printf("%d-%d=%d\n", x, y, result);
    }
    else if (strcmp(argv[1], "-mul") == 0)
    {
        result = x * y;
        printf("%d*%d=%d\n", x, y, result);
    }
    else if (strcmp(argv[1], "-div") == 0)
    {
        if (0 == y)
        {
            printf("error, div zero\n");
            return 1;
        }
        result = x / y;
        printf("%d/%d=%d\n", x, y, result);
    }
    else
    {
        printf("User error, you should use right op\n");
        return 1;
    }
    
    return 0;
}

2. 使用示例:

在这里插入图片描述


3.3 模拟实现touch


1. 代码:

#include<stdio.h>

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        printf("touch: missing file operator\n");
        return 1;
    }

    FILE *fp = fopen(argv[1], "w");
    if (fp != NULL) fclose(fp);

    return 0;
}

2. 使用实例:

在这里插入图片描述


4. 环境变量


4.1 基本概念


1. 问题引入:

在这里插入图片描述

  • 为什么我们在执行系统指令时,不用加路径,直接就能运行,而刚刚我们自己实现的mytouch却需要跟上路径?
  • 执行一个指令的前提,是必须找到这个可执行程序的位置。所以我们在执行mytouch时,需要跟上./,告诉系统这个可执行程序在哪。那么系统指令可以不跟路径的原因,只能是它有自己默认的搜索路径。

2. 环境变量PATH:

  • Linux中有很多环境变量,上面我们提到的默认搜索路径,就是环境变量PATH,我们可以通过指令echo $PATH来打印这个环境的内容。
    在这里插入图片描述
    • :为分割符,有多个默认搜索路径。

3. 小实验:将mytouch添加进环境变量PATH

在这里插入图片描述

  • 注意:不要直接写PATH=/home/LHY/test/test_2024/test_6_24,这样会将之前默认的路径全覆盖掉。
  • 默认更改环境变量,只限于本次登录,重新登录,环境变量自动被恢复(为什么?之后再说)。

4. 概念:

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

4.2 常见的环境变量


1. 常见环境变量:

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

2. 查看这些环境变量:

在这里插入图片描述

3. 思考:为什么用户在登录的时候,默认就是在自己的家目录下?

  • 登录时,默认会进行如下操作。
  • 输入用户名&&密码;
  • 认证;
  • 形成环境变量(不止一个,有PATHPWDHOME等);
  • 根据用户名,初始化HOME
  • 执行指令,cd $HOME

4. 使用指令env,查看所有环境变量:

在这里插入图片描述

  • 其中有一个环境变量USER要注意,它记录的是你登录这个系统时,使用的身份。如果你是以某一个普通用户登录的,那么USER就一直是这个普通用户名,就算你之后切换为root身份,也只是权限变了,USER不会改变。
  • 使用su -可以切换登入身份为root;使用su 普通用户名可以切换登入身份为普通用户;但使用su/su root只能提权,无法切换登入身份为root

4.3 通过代码获取环境变量


1. 通过getenv()获取环境变量:

  • 查看手册。
    在这里插入图片描述
    • 参数是环境变量名,返回值是环境变量的值。
  • 使用实例:设计一个只有登入用户是root才能使用的功能:
    在这里插入图片描述

2. 通过main函数的第三个参数,获取环境变量:

  • 系统在启动程序时,可以选择给进程(main)提供两张表:
    • 命令行参数表;
    • 环境变量表。
#include<stdio.h>

int main(int argc, char *argv[], char *env[])
{
    int i = 0;
    for (; env[i]; i++)
    {
        printf("%d: %s\n", i, env[i]);
    }
    return 0;
}
  • 编译并运行:
[LHY@localhost test_6_27]$ ./mypro 
0: XDG_SESSION_ID=3
1: HOSTNAME=localhost.localdomain
2: TERM=xterm
3: SHELL=/bin/bash
4: HISTSIZE=1000
5: OLDPWD=/home/LHY/test/test_2024
6: USER=LHY
7: JRE_HOME=/usr/local/soft/java/jdk1.8.0_40/jre
8: LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=01;05;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=01;36:*.au=01;36:*.flac=01;36:*.mid=01;36:*.midi=01;36:*.mka=01;36:*.mp3=01;36:*.mpc=01;36:*.ogg=01;36:*.ra=01;36:*.wav=01;36:*.axa=01;36:*.oga=01;36:*.spx=01;36:*.xspf=01;36:
9: LD_LIBRARY_PATH=:/root/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/LHY/.VimForCpp/vim/bundle/YCM.so/el7.x86_64
10: PATH=/usr/local/soft/java/jdk1.8.0_40/bin:/usr/local/soft/java/jdk1.8.0_40/jre/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin
11: MAIL=/var/spool/mail/root
12: PWD=/home/LHY/test/test_2024/test_6_27
13: JAVA_HOME=/usr/local/soft/java/jdk1.8.0_40
14: LANG=en_US.UTF-8
15: HISTCONTROL=ignoredups
16: HOME=/home/LHY
17: SHLVL=2
18: LOGNAME=LHY
19: CLASSPATH=/usr/local/soft/java/jdk1.8.0_40/lib:/usr/local/soft/java/jdk1.8.0_40/jre/lib:
20: LESSOPEN=||/usr/bin/lesspipe.sh %s
21: XDG_RUNTIME_DIR=/run/user/0
22: DISPLAY=localhost:10.0
23: _=./mypro
  • 不难发现,这种方式得到的环境变量,和直接使用env指令得到的环境变量基本相同;
  • env[]数组的最后一个位置值为NULL

在这里插入图片描述

3. 通过environ指针获取环境变量:

  • 系统中默认给我们提供了一个全局变量char **environ,它指向上面提过的环境变量表。
    在这里插入图片描述

在这里插入图片描述


4.4 环境变量的全局性


1. 为什么每次用户登入时,环境变量会恢复默认值?

  • 命令行启动的进程都是shell/bash的子进程,子进程的命令行参数和环境变量,是父进程bash给我们传递的。之前讲过,直接更改PATH是更改内存中环境变量的信息,实际上我们直接更改的就是bash进程内部的环境变量信息(进程在被调度时要加载进内存)。每一次重新登入,都会给我们形成新的bash解释器,并且新的bash解释器会自动从配置文件中读取信息,进而形成自己的环境变量表信息。
  • 这个配置文件,在用户家目录下,文件名为.bash_profile
    在这里插入图片描述
  • 看一眼这个文件中的内容:
    在这里插入图片描述
  • 文件中的内容是一种脚本语言,我们暂时不关注。我们关心的结论就是,每一次用户登入时,bash进程都会读取用户家目录下.bash_profile配置文件中的内容,为我们bash进程形成一张环境变量表信息。

2. 验证上面的结论:

  • 先导入两个环境变量,问:再次登入时,这两个环境变量还在不在?
    在这里插入图片描述
  • 不在,因为这个导入操作实际上是在内存中,也就是bash进程内部进行的,进程退出,变量就丢失了。
  • 要想让自己设置的环境变量永久存在,可以直接修改.bash_profile配置文件。
    在这里插入图片描述
  • 再次登入时,MYENV自动导入环境变量。
    在这里插入图片描述

3. 环境变量的全局性:

  • 所有在命令行上启动的进程都是bash的子进程,子进程大多都是通过fork创建的,和父进程共享代码和数据。之前说过,全局变量表可以通过environ指针来获取,这个变量是bash进程中定义的一个全局变量。所以所有bash的子进程都可以直接拿到这个environ指针。
  • 同理,bash子进程,fork出来的子进程,和bash的子进程共享代码和数据,它也可以直接拿到environ指针,由此可见,环境变量是具有全局性的。

4.5 本地变量VS环境变量


1. 本地变量和环境变量:

  • 本地进程只在bash进程内部有效,不会被子进程继承下去;
  • 环境变量通过让所有子进程继承的方式,实现自身的全局性。

2. Linux命令的分类:

在这里插入图片描述

  • 我们将PATH环境变量置空,ls\touch这些命令都跑不起来了,这是正常现象,可是为什么echo这个命令还可以跑?
    • 常规命令:shell通过fork创建子进程,然后让子进程执行;
    • 内建命令:shell命令行中的一个函数。
  • ls\touch就是常规命令,现在找不到目标可执行程序了,shell就无法fork子进程,自然就无法执行;echo是内建命令,当然可以直接读取shell内部定义的本地变量。

4.6 和环境变量相关的指令


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

5. 程序地址空间


5.1 程序地址空间回顾


1. 回顾C语言中讲过的程序地址空间分布:

在这里插入图片描述

  • 代码验证:
#include<stdio.h>
#include<stdlib.h>

int un_gval;    // 未初始化的全局变量
int init_gval = 100;    // 初始化的全局变量

int main()
{
    printf("code addr: %p\n", main);    // 代码区
    const char *str = "hello Linux";
    printf("read only char addr: %p\n", str);   // 字符常量区
    printf("init global value addr: %p\n", &init_gval);
    printf("uninit global value addr: %p\n", &un_gval);

    char *heap1 = (char*)malloc(100);
    char *heap2 = (char*)malloc(100);
    char *heap3 = (char*)malloc(100);
    char *heap4 = (char*)malloc(100);
    printf("heap1 addr: %p\n", heap1);    // 堆区
    printf("heap2 addr: %p\n", heap2);    // 堆区
    printf("heap3 addr: %p\n", heap3);    // 堆区
    printf("heap4 addr: %p\n", heap4);    // 堆区
    printf("stack1 addr: %p\n", &str);   // 栈区
    printf("stack2 addr: %p\n", &heap1);   // 栈区
    printf("stack3 addr: %p\n", &heap2);   // 栈区
    printf("stack4 addr: %p\n", &heap3);   // 栈区
    printf("stack5 addr: %p\n", &heap4);   // 栈区
    
    return 0;
}

在这里插入图片描述

2. 补充:存储命令行参数表和环境变量表的区域,在栈区之上:

在这里插入图片描述

  • 代码验证:
#include<stdio.h>
#include<stdlib.h>

int un_gval;    // 未初始化的全局变量
int init_gval = 100;    // 初始化的全局变量

int main(int argc, char *argv[], char *env[])
{
    printf("code addr: %p\n", main);    // 代码区
    const char *str = "hello Linux";
    printf("read only char addr: %p\n", str);   // 字符常量区
    printf("init global value addr: %p\n", &init_gval);
    printf("uninit global value addr: %p\n", &un_gval);

    char *heap1 = (char*)malloc(100);
    char *heap2 = (char*)malloc(100);
    char *heap3 = (char*)malloc(100);
    char *heap4 = (char*)malloc(100);
    printf("heap1 addr: %p\n", heap1);    // 堆区
    printf("heap2 addr: %p\n", heap2);    // 堆区
    printf("heap3 addr: %p\n", heap3);    // 堆区
    printf("heap4 addr: %p\n", heap4);    // 堆区
    printf("stack1 addr: %p\n", &str);   // 栈区
    printf("stack2 addr: %p\n", &heap1);   // 栈区
    printf("stack3 addr: %p\n", &heap2);   // 栈区
    printf("stack4 addr: %p\n", &heap3);   // 栈区
    printf("stack5 addr: %p\n", &heap4);   // 栈区

    int i = 0;
    for(; argv[i]; i++)
    {
        printf("argv[%d]: %p\n", i, argv[i]);
    }
    for(i = 0; env[i]; i++)
    {
        printf("env[%d]: %p\n", i, env[i]);
    }

    return 0;
}

在这里插入图片描述


5.2 页表&虚拟地址空间


1. 问题引入:

  • 写一段代码,子进程在五秒后改变全局变量g_val
#include<stdio.h>
#include<unistd.h>

int g_val = 100;

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // child
        int cnt = 5;
        while(1)
        {
            printf("child, Pid: %d, Ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
            sleep(1);
            if (cnt == 0)
            {
                g_val = 200;
                printf("child change g_val: 100->200\n");
            }
            cnt--;
        }
    }
    else
    {
        // father
        while(1)
        {
            printf("father, Pid: %d, Ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
            sleep(1);
        }
    }

    return 0;
}

在这里插入图片描述

  • 怎么可能对同一个地址进行读取,却读出了不同的值?由此我们可以得出一个推论,我们在C/C++中看到的地址,一定不是物理地址。
  • 上面我们所学的程序地址空间,也叫进程地址空间,它不是真实的物理地址,而是虚拟地址。

2. 虚拟地址&物理地址

  • 每一个进程运行之后,都会有一个自己的进程地址空间。我们上面看到的0x60105c是一个虚拟地址,它通过页表映射到真实的物理地址上。
  • fork之后,创建了子进程,它也有自己的进程地址空间,和自己的页表,并且都是拷贝父进程的。

在这里插入图片描述

  • 当子进程或父进程对g_val进行修改时,会发生写时拷贝。这个写时拷贝是发生在物理内存中的,并且页表右侧物理地址改变,即改变映射关系,但左侧虚拟地址不变。

在这里插入图片描述

注意:

  • 写时拷贝在物理内存中完成;
  • 写时拷贝的操作由操作系统完成;
  • 该操作不影响上层语言。

3. 页表(可以先看5.3的1、2)

在这里插入图片描述

  • 页表中比较重要的字段有四个,前两个是虚拟地址和物理地址。后两个是访问权限字段,和表示是否给虚拟地址分配空间、虚拟地址是否有内容的字段。

    • 访问权限字段:如果代码中,出现修改常量字符串的操作,会在页表处就进行拦截。

    • 标记是否分配空间&是否有内容的字段:模拟一次内存分配,假如现在执行代码int a = 1;,编译器会先访问到a的虚拟地址,但是此时a还未定义,也就是该字段为00(假设00表示未分配,没有内容),操作系统就会先暂停对该虚拟地址的访问,随后进行物理内存的分配,初始化,然后再修改页表,将映射的物理地址和该标记字段修改一下——这个过程也叫缺页中断

#include<stdio.h>

int main()
{
    char *str = "hello Linux";
    *str = 'H'; // 字符常量区不可被修改,为什么?

    return 0;
}
  • 磁盘中的可执行程序加载到内存中后,物理地址是无序的,数据放在内存中的什么位置都可以。因为大家的虚拟地址是统一的,而虚拟地址是有序的,虚拟地址到物理地址的映射是唯一的。
  • 一个进程要想执行必须加载到内存中,CPU中有一个专门的寄存器CR3用来存储该进程对应的页表地址(这个地址是物理地址),task_struct中保存该寄存器的信息。
  • 有些可执行程序可能非常大,而32位机器的实际物理内存只有4G,比较小,可执行程序无法一次性加载进内存。所以可执行程序可以分成一小段一小段,用局部加载的方式加载进内存。

5.3 深入理解地址空间


1. 地址空间的大小即概念:

在这里插入图片描述

  • 计算机内各个设备之间,要通过线来连接。连接外设和内存的线,叫IO总线;连接CPU和内存的线,叫系统总线。
  • 系统总线又分为地址总线和控制总线。在32位机器中,地址总线就有32根,一共可以表示232个地址,每个地址存储一个字节,所以32位地址总线可寻址的地址空间大小为4GB。

在这里插入图片描述

  • 每一个进程都有自己对应的虚拟地址空间,他们对外宣称的大小都是4GB。但是每个进程所能利用的真实地址空间,大小肯定不是4GB,因为真实的物理地址空间一共就这么大(虚拟地址就是操作系统给进程画的一张饼)。

2. 如何管理地址空间?

  • 每一个进程都有地址空间,系统中,一定要对地址空间做管理。
  • 先描述,再组织。地址空间在操作系统中,就是一个内核数据结构对象,一个内核结构体。
  • 在Linux中,这个结构体是(下面这个只是一个非常简单的示例,真实的mm_struct更加复杂):
struct mm_struct
{
	...
	long code_start;	// 代码区起始位置
	long code_end;		// 代码区结束位置
	long data_start;	
	long data_end;
	long heap_start;
	long heap_end;
	long stack_start;
	long stack_end;
	...
}
  • PCB中会有一个struct mm_struct *mm指针,指向这个结构体对象。

3. 为什么要有地址空间?(先学习页表)

  • 让进程以统一的视角看待内存。任意一个进程,可以通过地址空间+页表的方式,将乱序的内存数据变成有序,分门别类的规划好;
  • 可以有效的进行进程访问内存的安全性检查;
  • 将进程管理和内存管理进行解耦;
  • 通过页表,让进程映射到不同的物理内存处,从而实现进程的独立性。

在这里插入图片描述

4. 子区域划分:

在这里插入图片描述

  • 4G的虚拟空间中,用户能使用的只有3G,还有1G是内核空间,是留给操作系统的(之后细讲)。

在这里插入图片描述

  • 在3G的用户空间中,可能会存在一些零碎的空间难以利用。比如在堆区开辟了一块区域,紧接着又开辟了一块,随后将最先开辟的那一块空间释放了,那么这一块空间就会零散的空出来。
  • 为了更加有效的利用这些空间,mm_struct结构体中还定义了一个链表结构mmap,其中每一个节点是一个vm_area_struct结构体,描述一小块空间。
  • 真实的进程地址空间是由内存描述符mm_struct,和线性空间vm_area_struct组成的。

  • 9
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-指短琴长-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值