环境变量
环境变量是系统的全局配置手册,里面存放了在当前操作系统下的一些参数。其一般是全局属性,具体的用途比如在链接C/C++代码的时候,系统会自动链接到动静态库中,然后生成可执行文件。
我们常见的环境变量有三个:
**• PATH :**指定命令的搜索路径
• HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
**• SHELL :**当前Shell,它的值通常是/bin/bash
命令行参数
我们先来了解一下,我们在敲击命令的时候,bash是如何将其切分成一个个短字符串,最后将其组装成argv数组的。比如,当我们在命令行敲下:
ls -a -b
具体是怎么工作的呢?首先,他先去bin目录下找ls执行文件,找到以后在确定参数数量,如图有两个,那么ls的主函数的agrc(参数数量)就是2,agrv(参数数组)就存放三个数据(argv[0]是ls命令本身,argv[1]是-a,argv是-b),argv存放的数据会被main函数的逻辑进行处理调用。
真正的大流程是这样的在我们输入命令的时候,bash已经在内存中维护好一份描述,它会申请一份内存用于存储刚刚说到的参数("ls","-a","-b"),然后会创建一个指针数组指向这片空间,让argv[0]指向"ls"。fork会复制一份父进程的代码数据出来,在复制以后,子进程的main函数开始调用之前,bash会执行execve,其系统原型如下(了解一下即可)。
int execve(const char *filename, char *const argv[], char *const envp[]);
bash并不会直接把数据发给子进程,子进程现在的代码还没加载,此时会把argv和envp两个数组的首地址写在execve函数的参数列表,内核会得到这两个地址,然后他会把fork出来的子进程原本的数据,代码,堆栈全部抹除。内核会在子进程的新虚拟地址空间 中,开辟一段最高的区域(也就是你板书最底下那张图显示的 Argument strings 区域)。内核把 Bash 内存里的那些字符串("./code", "a"...)深拷贝 到这个新区域。内核在新进程的栈底(高地址)重新组织好 argv 和 envp 指针数组,让这些指针指向刚刚拷贝过来的字符串。最后,内核修改 CPU 的寄存器,让它跳到新程序的 main 函数入口,并把 argc 和 argv 的地址传给它。
PATH环境变量
接下来,我们通过PATH来了解明白系统是如何通过环境变量来"认路"和"认人"的。
我们如果在当前目录下编译了一个名字叫"ls"的二进制执行文件,那么我们可以用./ls来执行这个文件,那么,通过./ls执行的和ls直接执行的有什么差别呢?其主要差别是,./ls是执行当前目录下的,直接ls是执行PATH目录里的。如果我们想要要把例如./code变成可以直接code执行的命令,我们只要把当前目录加入PATH路径下即可。怎么加进去呢?下面提供两个方法:
1.export法 :通过export,$PATH 代表原来的路径清单,: 是分隔符,. 代表当前目录。这个的意思是在当前PATH路径下,再加个.。这个方法是临时的,因为环境变量随着进程继承,当把这个终端关了,这个也就没了。
export PATH=$PATH:.
**2.修改文件法:**通过修改文件法,可以使得该修改永久生效。我们首先打开bashrc文件,然后在文件末尾加上我们上面的临时法,最后重新启动终端或者source ~/.bashrc就行。
vim ~/.bashrc
export PATH=$PATH:.
得到/设置环境变量
我们有三种常用的方法可以得到环境变量:
方法A: getenv()这几乎是最好用的方法了,只需传入变量名,返回对应的字符串。简单而高效
方法B: main 函数参数 char *env[],通过 main(int argc, char *argv[], char *env[]) 直接拿到。一般用于在程序启动的一刻快速遍历环境。
方法C: extern char **environ,这是一个全局变量,定义在 libc 中,它指向整个环境表的头部。如果你想做一个类似 env 命令的程序(列出所有变量),用它最方便。
修改当前进程的环境变量 ,我们可以使用setenv("NAME", "VAL", 1),只能修改当前进程及其后续创建的子进程。它无法修改父进程(Bash)的环境变量。
我们前面说过,环境变量具有全局性,我们分析一下下面两个命令,看看有什么不同
i=10
echo $i
export j=10
echo $j
在这个命令中,i是我们设置的本地变量,他不会被子进程继承,只会在当前bash内部使用。j是我们设置的环境变量,他会被继承,但是当我们杀死全部终端以后,重新开机,这个变量也会消失。
虚拟内存
在过去,没有虚拟内存的时代,或者是在单片机裸机上进行开发的时候,程序是会直接访问物理内存的。比如有个进程A,占用了1~2GB的位置,进程B占用了2~3GB的位置。此时,若是进程A发生BUG,比如出现了野指针,往2.5G的地方写入了数据,进程A就会覆盖进程B,导致进程B莫名其妙的挂掉。当时就是,进程之间没有隔离,互相裸奔,安全感为零;而且编写编译器极其困难(因为每次运行,程序被加载到物理内存的哪个位置是不确定的)
接下来,我们再看一个例子,如果我们运行以下代码,会发生什么呢?
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
} e
lse if(id == 0){ //child,⼦进程肯定先跑完,也就是⼦进程先修改,完成之后,⽗进程再读取
g_val=100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else{ //parent
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
} s
leep(1);
return 0;
}
我们发现,父子进程,输出地址是⼀致的,但是变量内容不⼀样!因为变量内容不一样,因此我们可以认定,父子进程输出的变量不是同一个变量。然后呢,因为地址一样,因此我们可以认为,这不是一个真实的地址!在linux环境下,这种地址被称为虚拟地址。
当OS创建子进程的时候,他会所给有子进程都给一份虚拟空间地址,这样会使得子进程认为自己拥有了整个空间的地址。那么,这份虚拟空间地址到底是什么呢?我们先来看一下这个虚拟地址空间都有些什么。首先,最底层是C++代码(只读) ,然后再往上,存放全局变量 ,在中区,存放堆 ,这是由malloc/new申请的空间他会继续向上生长。在高区,是栈区 ,这里存放局部变量,函数调用,它是向下生长的。最高处是环境变量和命令行参数。假设这份空间有4GB,OS只会把3GB给进程进行折腾,最后1GB是内核空间,存放着操作系统的核心代码和数据结构,不可逾越。
既然这个内存不是真实的,那么他到底是什么呢?其实,他只是,内核里的一个数据结构mm_struct!这个结构体用一个个的参数指定了各个部分的起始结束处。根据什么进行对应呢?就是根据页表进行对应。页表可以理解成是一个对应表,每一行都是一个虚拟地址对应一个真实地址。比如当CPU想要访问一个0x111111地址,他会去查这个进程的页表,然后找到相对应的真实物理地址。我们就继续以上文那个g_val进行讨论:
父进程运行中 :父进程有一个变量 g_val=100。它的虚拟地址是 0x111111。页表记录:
0x111111 -> 物理内存 A区。
此时,进行了fork ,产生了子进程,由于此时还没有对父或子进程中任意一个进程发生修改数据,因此此时他只会复制页表,因此,子进程也是0x111111 -> 物理内存 A区。
子进程尝试修改:
-
触发"报错":CPU 发现子进程想往只读的"A区"写数据,于是停下来,向内核报告(缺页异常)。
-
写时拷贝(COW) :内核发现这是子进程在改数据,于是悄悄地在物理内存开辟了一块新的"B区",把 A 的内容拷过去并改成 200。
-
更新映射 :内核只修改子进程的页表,让它的 0x111111 -> 物理内存 B区。
就此,就发生了我们之前见到的画面。
struct mm_struct
在task_struct中,有一个指针struct mm_struct *mm;他指向了一个结构体,他直接指向了一系列的重要地址,包含各个数据段的起始终止处。这些起始终止处,刚好就是vm_area_struct的vm_start和vm_end。vm_struct和mm_struct的区别在于,vm_struct中还记载了这块区域是只读 的、它是从哪个磁盘文件(你的二进制程序)映射过来的、如果发生缺页该怎么办。
struct mm_struct
{
/*...*/
struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */
struct rb_root mm_rb; /* red_black树 */
unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*/
/*...*/
// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
/*...*/
}
现在,我们来对下面的代码进行验证一下。当我们运行下面代码,百分百报错,因为我们在对常量字符串区域进行更改。
char *str = "hello world";
str[0] = 'H'; // 试图把小写 h 改成大写 H