0
点赞
收藏
分享

微信扫一扫

零基础Linux_9(进程)环境变量+进程地址空间+进程创建fork

目录

1. 环境变量

1.1 环境变量基本概念

1.2 环境变量PATH 

1.3 环境变量HOME和SHELL

1.4 获取环境变量(main函数参数)

1.4.1 main函数第三个参数

1.4.2 设置普通变量和环境变量

1.4.3 main函数前两个参数

2. 进程地址空间

2.1 验证进程地址空间的分步

2.2 进程地址空间的引入

2.3 进程地址空间是什么

2.4 为什么要有进程地址空间

3. 进程创建fork

3.1 fork函数概念和用法

3.2 写时拷贝

本篇完。


1. 环境变量

1.1 环境变量基本概念

继续用上一篇Linux_写的代码:

当我们 ls 显示当前路径文件时,和运行自己写的代码时,比如 ./process,有没有想过:

为什么我们的代码运行要带路径,而系统的指令不用带路径?

如果我们直接输入我们的可执行程序,会显示 bash: process: command not found 

我们说过,执行系统的指令实际上也是程序,系统的指令你也是可以带上路径的:

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

为什么我们的代码运行要带路径,而系统的指令不用带?其本质是由环境变量 PATH引起的。

1.2 环境变量PATH 

输入 env 显示所有环境变量

这些变量每一个都有它特殊的用途,系统中搜索可执行程序的环境变量叫做 PATH。

我们可以通过 grep 去抓一下:输入 env | grep PATH

 如何查看环境变量的内容?我们可以使用 echo 去显示:输入 echo $PATH

($用来区别直接打出PATH这个字符串)

环境变量 PATH中会承载多种路径,中间用冒号 ( : ) 作为分隔符。

我们在执行某一个程序时,比如执行 ls 时,我们的系统识别到 ls 的输入时,会在上面路径中逐个搜索,只要在特定的路径下找到了 ls,就会执行特定路径下的 ls 并停止搜索。

换言之,PATH就提供了环境变量,可执行程序搜索的路径。

我们的 ls 在  usr/bin  路径下,这说明当前的 ls 在 PATH中是可以被找到的,

所以执行 ls 的时侯自然可以不带路径,所以我们自己的  process 不带路径自然就不能执行。

因为当前的  process 所在的路径并没有这里的环境变量,程序在搜索的时侯找了路径也没有找到你这个可执行程序,搜索完找不到,自然就报 "command not found" 了。
 

那我现在就想让我的可执行程序 process 不带路径直接执行起来,可以吗?

可以!我们先讲述一种简单粗暴的方式,直接把我们的可执行程序 cp 拷贝到系统的路径中:

转到root用户,然后输入:cp process /usr/bin/

更好的方式是将 process 所处的路径也添加到环境变量中。
 

pwd查看当前路径,把当前路径添加到环境变量中:export PATH=$PATH:路径

添加完成后我们的程序就能不用输入路径直接运行了:

 这里只要重新登录,环境变量又变成原本的了。

1.3 环境变量HOME和SHELL

其他环境变量:

USER:当前用户名
PWD:当前所处路径
HOSTNAME:主机名

(环境变量实在多,全部讲完不太现实,所以就先讲到这)

1.4 获取环境变量(main函数参数)

环境变量是以数组的形式存储的,它的组织方式如下图:

其实main函数有三个参数:

1.4.1 main函数第三个参数

下面我们按照上面,先讲讲第三个参数:环境变量参数。

据图,看一段在Linux环境下运行的代码:

 (根据图,env最后放的是空指针,退出for循环)编译运行:

 这是在代码里获取环境变量的第一种方式。

 根据这个图,我们还可以用environ获取环境变量,问一下man怎么用: 

这是一个全局的第三方变量: 

编译运行:

这是代码获取环境变量的第二种方式,但是一般不用前两种方式,用第三种:

这是一个函数接口,用来获取一种环境变量,这里获取上面讲的PATH:

 编译运行:

现在我们学了这么多获取环境变量的方式,那么这个环境变量是谁设置的?main函数的第三个参数是谁传入的?

我们这里用gerenv顺便获取一个不存在的环境变量: 

编译运行:

发生了段错误,因为根本没有这个环境变量。

1.4.2 设置普通变量和环境变量

命令行上直接写,变量名等于值:

 你所定义的这个变量 abcdef,就是 普通变量

用系统查看环境变量的命令 env 去查看一下这个本地变量,会发现根本找不到,

试试编译运行上面这个代码:

还是段错误,因为它不以环境变量的形式存在,不具有全局性,但是它是存在的!

如果你想让一个变量变成环境变量,你可以通过 export 导出一个在系统中可以查看的环境变量

 env可以看到,代码也不用编译再运行也能打印。

父进程只要有用父进程那也是继承的,直到头,一般是bash。

1.4.3 main函数前两个参数

前面讲了第三个参数,现在讲前两个参数,第二个参数也是指针数组,第一个应该是第二个参数指向的个数,先打印一下:

编译运行:

 通过一顿乱敲,机智的童鞋已经发现什么了: 这就是Linux下命令设置的方式

所以,第二个参数的数组存放的指针指向的就是命令行参数。argc是含第一个参数的参数个数。

Linux下命令设置的方式再规范一丢丢就是这样的:

 编译运行:

 

再加上前面把process路径设置成不用绝对路径的形式,和 ls 命令差不多了。

至此,main函数的三个参数全部讲完。(先跑步去,脑子累死......洗完澡复活复活)

2. 进程地址空间

进程地址空间可以叫作程序地址空间(不准确),也叫作内存地址空间,也叫作虚拟地址空间

我们先基于是什么,为什么,怎么办,问三个问题:

① 什么是进程地址空间?(是什么)2.2

② 为什么要有进程地址空间?(为什么)2.3

③ 进程地址空间是如何设计的?(怎么办)2.1

2.1 验证进程地址空间的分步

进程地址空间是如何设计的?(怎么办)

之前在学习C和C++的时候,经常画过类似的空间布局图(其实就是进程地址空间的分步):

我们先验证下:

但是真的理解它吗?物理内存中就是这样的吗?其实并不是这样的。来看一段代码(这里返回rtx2目录创建linux_9目录),

写一个新的Makefile,以前写的是这样的:

 如果有很多文件要这样写:

  一直有就一直要写两遍,所以就有一个符号可以这样写:

 

 以后输入只有一个文件,我们也这样写了:(这里应该没配置过所以加上-std=c99)

 验证最开始的那张内存图,写test.c:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
 
int un_g_val;
int g_val = 100;
 
int main(int argc, char* argv[], char* env[])
{
    printf("code addr            : %p\n", main);
    printf("init global addr     : %p\n", &g_val);
    printf("uninit global addr   : %p\n", &un_g_val);
 
    char* m1 = (char*)malloc(100);
 
    printf("heap addr            : %p\n", m1);
    printf("stack addr           : %p\n", &m1);
 
    for (int i = 0; i < argc; i++) 
    {
        printf("argv addr        : %p\n", argv[i]);   
    }
 
    for (int i = 0; env[i]; i++) 
    {
        printf("env addr         : %p\n", env[i]);
    }
    return 0;
}

 编译运行:

验证成功,整体向高地址增长的,再验证一下堆像高地址增长,栈向低地址增长(

  编译运行: 

所以:堆像高地址增长,栈向低地址增长(堆,栈,相对而生)

我们再来理解一下 static 变量,如何理解 static 变量?

我们可以加入一个 static 变量进刚才的代码中,我们来观察观察:

int a = 77;
static int s_a = 777;

printf("%p\n",&a);
printf("%p\n",&s_a);

  编译运行:

至此,成功验证进程地址空间的分步。

2.2 进程地址空间的引入

还是看一段代码:

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

int g_val = 100;
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        while (1)
        {
            printf("子进程: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
            sleep(1);
        }
    }
    else if (id > 0)
    {
        while (1)
        {
            printf("父进程: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
            sleep(1);
        }
    }
    else
    {
        printf("创建子进程失败\n");
    }
    return 0;
}

编译运行:

当父子进程没有人修改全局数据的时候,父子是共享该数据的。

如果此时尝试写入,比如我们让子进程有一个修改的操作。

我们让子进程执行五次之后给它改值:

编译运行:

父子进程打出来的地址是一样的,值却不一样!?

所以我们在 C/C++ 中使用的地址,绝对不是真实物理内存的地址。

如果是物理地址,上面出现的那种现象是不可能产生的!

不是物理地址,那是什么呢?就是下面进程地址空间的虚拟地址。

2.3 进程地址空间是什么

内核中的地址空间,本质也是一种数据结构,所以就要有区域的划分:

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

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

操作系统为了管理一个进程,给该进程维护一个 task_struct 叫做进程控制块。

虚拟地址空间是怎么和内存联系起来的?现代计算机,提出了下面的机制:

物理内存本身是可以被顺便读写的,非常的不安全,所以就有了上面虚拟地址空间的机制

 所以进程地址空间是内存吗?:不是,进程地址空间不是内存。

页表在讲线程的时候才会更好讲解,这里弱化一下,虚拟地址空间和页表每个进程都有一份,所以保证了进程之间的独立性,所以就有了这样的图:

2.4 为什么要有进程地址空间

为什么要有进程地址空间,三大原因:(进程地址空间的意义)

所以为什么要有进程地址空间?:三个关键:1保护物理内存,2解耦合,3有序地保证独立性。

3. 进程创建fork

3.1 fork函数概念和用法

在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。前面《零基础Linux_7(进程)冯诺依曼结构+操作系统原理+进程的概念和基本操作》中已经讲了一部分fork的内容。

fork之前父进程独立执行,fork之后,父子两个执行流分别执行。

注意,fork之后,谁先执行完全由调度器决定。

进程调用 fork,当控制转移到内核中的fork代码后,操作系统会做什么? 

那么fork之后,是否只有fork之后的代码是被父子进程共享的?

我们再来重新思考一下fork之后操作系统会做什么?

所以:fork之后创建一批结构,代码以共享的方式,数据以写时拷贝的方式,两个进程保证 "独立性",做到互不影响。在这种共享机制下子进程或父进程任何一方挂掉,不会影响另一个进程。

3.2 写时拷贝

程序被编译出来,没有被加载的时候,程序内部有地址吗?有!有没有区域?也有!

所以刚才我们代码测试,打印看到的虚拟地址值是一样的,并且内容也是一样的。在没有人写入的时候,虚拟地址到物理地址之间映射的页表是一样的,所以指向的代码和数据都是一样的。

 因为进程具有独立性,比如如果此时子进程把变量改了(写入),就会导致父进程识别的问题就出现了父进程和子进程不一的情况,因为进程是具有独立性的,所以我们就要做到互不影响。我们的子进程要进行修改了,影响到父进程怎么办?当我们识别到子进程要修改时,操作系统会重新给子进程开辟一段空间,并且把 100 拷贝下来,重新给进程建立映射关系,所以子进程的页表就不再指向父进程所对应的 100 了,而直接指向新的 100。你在做修改时又把它的值从 100 改成 777时,我们就出现了 "改的时候永远改的是页表的右侧,左侧不变" 的情况,所以最后你看到了父子进程的虚拟地址一样,但是经过页表映射到了不同的物理内存,所以了你看到了一个是 100 一个是 777,父子进程的数据不同的结果。

我们在前面:零基础Linux_7(进程)冯诺依曼结构+操作系统原理+进程的概念和基本操作

就提出过一个问题,关于 fork 为什么有两个返回值的问题。

当时我们还提出了两个问题,局限于当时还没有讲到进程地址空间,所以没有办法深入讲解,

我直接穿越回去截个图:

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

现在我们就可以理解了,因为当它 return 的时候,pid_t id 是属于父进程的栈空间中定义的。

fork 内部 return 会被执行两次,return 的本质就是通过寄存器将返回值写入到接收返回值的变量中。当我们的 id = fork() 时,谁先返回,谁就要发生 写时拷贝。所以,同一个变量会有不同的返回值,本质是因为大家的虚拟地址是一样的,但大家的物理地址是不一样的。

还有如何理解,父子进程让if和else if同时执行?

fork之后的代码,父子进程是共享的。

也就是说,在fork之后的代码,父子进程是共同执行的,并且父子进程使用的是同一块物理空间中的代码。但是各自的id值是不同的,所以会父子进程会进入不同的条件判断中,并且执行不同的代码。

本篇完。

下一篇:零基础Linux_10(进程)进程终止(main函数的返回值)+进程等待。

再下一篇:进程程序替换+实现简单的shell。

举报

相关推荐

0 条评论