Linux系统网络编程|Linux系统(进程理解)

进程理解 4. 环境变量 4.1 环境变量的概念
环境变量指操作系统中用来确定系统运行环境的一些参数,环境变量是系统级的变量,属于全局变量。比如 C/C++ 代码中链接的动静态库的地址在安装环境的时候就被放在了环境变量里。11
执行用户编写的代码时需要在文件名前加./,以辅助操作系统确定执行文件的位置。系统级的指令同样也是文件,但使用指令时却直接输入文件名。
这是因为系统级指令的所在目录都被放入环境变量PATH中,每个目录以:分隔。

[yyx@VM-4-16-centos lesson3]$ echo $PATH /usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/yyx/.local/bin:/home/yyx/bin

系统在查找指令的时候,就是按顺序从头到尾到每个目录中挨个查找,找到则停止查找并允许当前路径下的可执行程序,找不到则报错。
#1. [yyx@VM-4-16-centos ~]$ export PATH=/home/yyx/4DefinitionOfProcess/lesson3 # 覆盖PATH [yyx@VM-4-16-centos ~]$ echo $PATH /home/yyx/.local/bin:/home/yyx/bin #2. [yyx@VM-4-16-centos ~]$ export PATH=$PATH:/home/yyx/4DefinitionOfProcess/lesson3 # 添加PATH [yyx@VM-4-16-centos ~]$ echo $PATH /usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/yyx/.local/bin:/home/yyx/bin

export修改PATH的方式,都是内存级的修改。也就是说,仅本次登录有效。
环境变量是系统级别的变量,不建议用户过多修改。需要修改只改本次就行,系统环境变量被放在~/.bash_profile的文件内。
4.2 环境变量的查看
  1. PATH:系统级指令的搜索路径;
  2. HOME:当前用户的工作目录(家目录);
  3. SHELL:系统命令行解释器的具体名称,通常是 bin/bash 等;
  1. 使用env命令可直接查看系统内所有环境变量。
环境变量也是变量,本质就是操作系统在内存磁盘中开辟的空间,用来保存系统相关的数据。简单来说,环境变量就是系统维护的一个全局变量表,用于辅助系统维护一些程序的运行。
4.3 环境变量的组织形式
命令行参数 在曾经C语言的学习过程中,了解到main函数的参数声明可以是如下方式:
int main (int argc, char* argv[]);

argc,argv分别是 参数个数 argument counter 和参数值 argument value 的简称。参数指的是命令行参数,也就是输入指令时所带的选项。
命令行参数是指令名称和其所带的选项的统称。指令和选项都被看作字符串,所以argv是个字符串指针数组。
$ ls -a -b -c -d # argc = 6, argv = {"ls", "-a", "-b", "-c", "-d"}

  • argc是命令行参数的个数
  • argv是命令行各个参数的字符串组成的字符串数组。首元素是指令名称,故之后的元素下标正好是参数的位数。
数组传参,最好将元素个数也一并传入。为防止越界访问,可以在数组结尾的下一个位置存入NULL以标识边界。
既然命令行参数会以参数的形式传入主函数,则可以根据参数的不同完成同一个代码的不同子逻辑。
int main(int argc, char* argv[], char* env[]);

从上述主函数的声明可以看出环境变量env也是字符串数组,也可以做主函数的参数。和命令行参数一致,环境变量也是依靠字符串数组组织起来的。
int main(int argc, char** argv, char** env) { for (int i = 0; env[i]; i++) { printf("env[%d]->%s\n", i, env[i]); } }

4.4 环境变量的获取
系统变量environ
int main(int argc, char** argv) { extern char** environ; //声明环境变量指针 for (int i = 0; environ[i]; i++) { printf("environ[%d]->%s\n", i, environ[i]); } }

环境变量指针environ是系统的一个全局变量,也是字符串数组。和main函数的参数char* env[]一个道理。
系统调用putenv/getenv 主函数
大部分情况都不会使用参数env和系统变量environ两种获取环境变量的方式,一般都是用系统调用接口 putenvgetenv
$ man putenv && man getenv # putenv NAME putenv - change or add an environment variable SYNOPSIS #include int putenv(char *string); DESCRIPTION Theputenv()functionaddsor changes the value of environment variables.The argumentstring is of the form name=value. If name does not already exist in the environment,then stringisaddedtotheenvironment. If name does exist, then the value of name in the environment is changed to value.The string pointed to by string becomes part of the environment, so altering the string changes the environment. RETURN VALUE The putenv() function returns zero on success, or nonzero if an error occurs.In the eventof an error, errno is set to indicate the cause. # getenv NAME getenv, secure_getenv - get an environment variable SYNOPSIS #include char *getenv(const char *name); DESCRIPTION Thegetenv() function searches the environment list to find the environment variable name, and returns a pointer to the corresponding value string.

printf("PATH->%s\n", getenv("PATH")); printf("HOME->%s\n", getenv("HOME")); printf("SHELL->%s\n", getenv("SHELL"));

即便如此,环境变量在开发中也很少使用到,学习环境变量主要是为了更好的理解进程。
4.5 环境变量的全局属性
每一个主动创建的进程都是被操作系统加载到内存中的,它们的父进程都是bashbash创建之初就会从磁盘上导入环境变量,所以我们的进程的环境变量就是从父进程bash中导入的。
将本地变量导入环境变量后,再运行进程就会从bash导入环境变量,即可调用getenv成功打印。
也就是说,环境变量是可以被继承的。命令行运行的进程从命令行解释器处继承环境变量,而子进程也会从父进程处继承环境变量。这样,环境变量就影响了整个用户系统。

5. 进程地址空间 5.1 虚拟地址空间的定义
在C/C++动态内存管理章节处,有个程序地址空间的概念。但从语言的角度,无法深入理解这个概念,因为它是操作系统中的概念。
多打印几个变量可以看到堆栈相向而生的现象,栈区由高到低,堆区由低到高。
如下代码,子进程修改了父进程的数据,应当发生写时拷贝,也就是会拷贝一份g_val单独给子进程使用,此时父子进程同时打印g_val的地址。
int g_val = 0; int main() { if (fork() == 0) { //child int cnt = 5; while (cnt--) { printf(" I am child proc, g_val=%d, &g_val=%p, times:%d\n", g_val, &g_val, cnt); sleep(1); if (cnt == 3) { printf("################ child change g_val ###################\n"); g_val = 200; printf("################ g_val changed done ###################\n"); } } exit(0); } //parent while (1) { printf("I am parent proc, g_val=%d, &g_val=%p\n",g_val, &g_val); sleep(1); } return 0; }

结果是修改前后父子进程的g_val的地址是一样的。也就是说,C/C++打印出的地址并不真实的物理地址,而是虚拟的逻辑地址。
一般C/C++打印的地址都是虚拟地址,再由操作系统链接到真实地址。但不排除一些简单系统不使用虚拟地址。
程序地址空间,在操作系统的角度,准确的来说叫做进程虚拟地址空间。每个进程都拥有一个进程地址空间,每一个进程都认为自己独占即拥有物理内存。
设计出进程地址空间,是为了更好地管理进程的内存操作。仍然是先描述再组织的方式,系统为每一个进程分配一个结构体mm_struct,它就是具体的进程地址空间变量,用来描述进程地址空间。
故地址空间本质是内核中的一种数据类型mm_struct,每个进程都有独有一份地址空间。操作系统会将地址空间映射到物理地址上。
struct mm_struct { size_t stack_begin; size_t stack_end; size_t heap_begin; size_t heap_end; //... };

可能内核中的mm_struct里面就是这样的一堆变量,虚拟出的堆区栈区等等内存区域的下标。所以说,堆区栈区这样区域本质就是两个下标进行划分。
struct mm_struct { size_t stack_begin; size_t stack_end; size_t heap_begin; size_t heap_end; size_t malloc_begin = 0x778010, malloc_end = 0x778014; // 在内存上开辟了4字节的空间,就是创建了这样的下标变量 //... };

语言上的开辟内存空间就是在结构体mm_struct中创建两个整型变量(相当于下标,指明一段区间长度),并不一定是真实地将内存上一段空间划分给了进程。
虚拟地址就像一把刻度尺,起划分区域的作用,单纯看这些虚拟地址就可以确定变量的位置。物理内存中是没有什么栈区堆区这样的区域概念的,就是单纯的存放数据。
进程并不需要关心真实地址,它就认为自己的mm_struct代表整个内存,都属于当前进程的,自己有权限使用所有的内存空间。由操作系统充当中间人,将真实地址和虚拟地址关联映射起来。
操作系统通过页表和 MMU,将虚拟地址和物理地址关联映射起来。
  1. MMU 也叫做内存管理单元,集成在 CPU 中,它的作用是查页表。
  2. 页表是用操作系统提供的一张映射散列表,如图所示,其左侧一列存放虚拟地址,右侧一列存放虚拟地址所映射的真实地址。
5.2 进程地址空间的意义
  1. 通过中间层,有效地实现对进程的内存操作进行风险管理 (权限管理),本质就是更好的保护物理内存和进程安全。
    • 通过页表对访问的地址进行管控,防止进程越界访问非法空间,导致程序出错或威胁进程安全。
    • 页表也会针对虚拟地址所在区域,给进程在访问对应的物理地址的时候设置权限。比如进程访问字符常量区数据时,不允许其写入对应的真实地址。
  2. 通过内存管理算法,将应用和内存管理进行解耦,进程开辟内存的操作和操作系统的内存管理实现软件层面上上的分离。
    • 内存申请和使用的概念在时间上区分开来。申请的空间不会立即在内存上分配出去,能够做到实际使用空间再将其划分给进程,或者用了多少给多少。更加人性化高效率地使用内存,避免了空间浪费。
    申请的时候只是在虚拟地址空间上划出了一段区域,可以说是假的空间,等到使用空间时再将虚拟地址映射到物理内存上。这就是基于缺页中断进行物理内存申请。
  3. 从操作系统和应用层的角度,进程能够避免差异化,以统一的方式看待内存,且虚拟地址的划分是相对确定的,CPU 访问地址空间的方式也相对统一。
    每个进程都有独立的地址空间,其程序的入口主函数的地址是一致的,这样 CPU 在调度进程时就可以更加快捷。差异化会造成更多特殊情况,更加容易出错。
  4. 通过页表的映射,进程的相关代码和数据可以随意的存储在物理内存中,大大节省了空间,且不会影响 CPU 读取效率
    CPU执行进程直接访问的是进程相关数据结构,也就是虚拟地址,再通过页表映射到真实地址。
进程PCBtask_struct,进程地址空间mm_struct以及页表都是进程相关的数据结构,都是在进程创建之初形成的。
子进程的创建是以父进程为模版的,继承了父进程的代码数据还有进程数据结构。所以全局变量g_val的虚拟地址也是一样的, 但当子进程修改时,操作系统重新开辟一段空间给子进程,这就是写时拷贝。
写时拷贝是在物理内存上重开空间,将进程页表中虚拟地址重新映射到新位置。这个过程完全是在系统底层实现的,并没有涉及到虚拟地址,虚拟地址没有被修改,打印结果自然一样。
【Linux系统网络编程|Linux系统(进程理解)】父子进程之间共享程序代码和数据也是通过虚拟地址映射到同一段物理地址上的,这就是共享带代码数据的原理。

    推荐阅读