目录
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。