提到环境变量,我们都知道PATH
中包含着常用可执行文件的路径,有了它在命令行程序中直接输入文件名就可以运行程序。在Node.js环境中,我们也经常使用process.env.NODE_ENV
来区分开发和线上环境,比如在开发环境下可以打印日志、不压缩资源,以方便调试。
但是,除了这些,你对环境变量还知道多少?本文将带领大家全面了解一下环境变量。
我们现在使用的环境变量是在1979年Version 7 Unix中成型的,此后所有Unix系统包括Linux和macOS都实现了同样的特性。1982年,从PC DOS 2.0起,所有Windows操作系统也包含了环境变量,只是语法、用法和标准变量名有所差异。
环境变量是与进程紧密相关的一个特性。进程是什么?通俗地理解,进程就是“进行中的程序”,书面说法则是“运行中程序的一个实例”。意思其实都一样。之所以要抽象出进程这个概念,部分原因是为了隐藏使用CPU和内存资源的复杂度。有了进程的概念,一个程序在运行时就好像可以占有全部CPU和内存一样,不必考虑其他同时运行的程序。而每个程序都是在一定的上下文中运行,这个上下文中包含了程序正确运行所需的状态。这里所说的状态包括程序的代码、数据、栈、通用寄存器的内容、程序计数器、环境变量及打开的文件描述符,等等。
环境变量可以在脚本中使用,也可以在命令行中使用。通常需要在变量名前面或两侧添加特殊符号来引用某个环境变量。比如,要显示用户的主目录,在大多数脚本环境中必须使用:
echo $HOME
在DOS/Windows命令行解释器(如cmd.exe
)中,要这样写:
ECHO %HOME%
在Windows PowerShell中,则要这样写:
Write-Output $env:HOMEPATH
命令行程序有3个内置命令,可以列出环境变量及它们的值:
env
set
printenv
在Unix和类Unix系统中,环境变量区分大小写。
在Unix中,环境变量通常在系统启动时由初始化脚本进行初始化,然后由系统中的所有其他进程继承。同样,当在一个程序中打开另一个程序时,调用程序会先复制一个与自身完全一样的进程,即子进程。子进程可以根据需要修改环境变量。最后,子进程再通过执行被调用的程序来覆盖自己。其中,复制进程对应fork
,执行程序对应exec
:
- fork:是由操作系统内核实现的系统调用,用于创建当前进程自身的一个副本;
- exec:同样是由操作系统内核实现的系统调用,用于在已有进程的上下文中运行一个可执行文件。
exec在实际实现中通常是一组函数的代称,比如在C语言中就有execl、execle、execlp、execv、 execve和execvp等几个函数,它们共用exec
这个名字,只是分别在末尾追加了一两个字母,表示自己接受的参数不同(比如,e表示接收环境变量的指针数组,l表示一个一个地接收命令行参数,即参数列表,p表示使用PATH
环境变量查找文件,v表示接收命令行参数的指针数组或者叫向量)。Linux内核只有一个叫execve
的实现,前面所有其他的函数都是在用户空间中对这个系统调用的封装。
下面我们通过分析execve
来理解父进程在创建子进程时如何传递环境变量。先看一看execve
的接口:
#include <unistd.h>
int execve(const char *pathname, char *const argv[],
char *const envp[]);
(http://man7.org/linux/man-pages/man2/execve.2.html)
execve
函数接收3个参数,第一个是可执行文件的路径pathname
,第二个是参数的指针数组argv
,第三个是环境变量的指针数组envp
。下图展示了参数数组的数据结构:
如图所示,变量argv
指向一个NULL结尾的指针数组,前面的每个元素都是一个指向参数字符串的指针。按照约定,argv[0]
是可执行文件的名称。下面是环境变量指针数组的数据结构:
两上数据结构类似。唯一的区别是,环境变量数组元素指向的字符串都是名-值对形式的,比如"PWD=/usr/droh"
。
在找到pathname
对应的可执行文件后,execve
会调用操作系统永驻内存的loader
代码,把可执行文件的代码和数据从磁盘复制到内存。然后,跳到其第一个指令或“入口点”开始执行该程序。这个过程叫加载。
加载之后,就是通过系统启动函数来运行用户的main
函数:
int main(int argc, char *argv[], char *envp[]);
同样,main
函数也接收3个参数,其中最后一个参数envp
就是新进程或子进程继承的环境变量。
命令行程序是我们最常用的启用其他程序的工具,因此会频繁地使用上述的fork
/exec
过程。比如:
node ./index.js
就会启用一个新的Node.js进程,运行index.js。与此同时,父进程的环境变量也会传递给这个新的子进程。
在Unix中,脚本或编译的程序修改的环境变量只会影响当前进程及其子进程。父进程及其他不相关的进程不受影响。
在命令行程序中,内置的export
命令用来在当前进程中创建环境变量(自然也会被子进程继承):
export API_URL=http://example.com/api
不使用export
关键字也可以创建变量,虽然以这种方式定义的变量可以通过set
命令显示,但却不是真正的环境变量。这种变量只是命令行程序存储的,操作系统内核并不认。因此env
和printenv
命令不会显示它们,子进程也不会继承它们:
API_URL=http://example.com/api
然而,在命令行中调用其他程序时,在前面添加类似上面的变量赋值,则会将该变量添加到子进程的环境变量中:
API_URL=http://example.com/api node ./index.js
这样,index.js就可以通过process.env.API_URL
取得传入的API地址了。
用户可以在自己所用的命令行工具的配置脚本(profile script)中添加或修改环境变量。
在DOS和Windows命令行解释器中,使用SET
命令创建环境变量并赋值:
SET VAR_NAME=VALUE
只有SET
命令会显示所有环境变量及它们的值。
命令行脚本除了可以直接使用环境变量,还有一种常见的用法,就是“hashbang”。比如,下面这个脚本使用Node.js作为解释器:
#!/usr/bin/env node
console.log('Hello, You Got It!')
第一行中的#!/usr/bin/env
是命令行程序内置env
命令的完整路径,后面跟着运行当前脚本的程序名node
。意思是在env
的环境变量PATH
中去查找解释程序node
。
当然,不使用env
而直接给出node
的路径也是可以的:
#!/usr/local/bin/node
console.log('Hello, You Got It!')
但这样是不是就不灵活了?毕竟不同环境下node
的位置可能不一样。使用env
就可以做到在运行时再定位解释器的位置,从而让脚本的兼容性更好。
当然,使用env
也有缺点:不同机器上env
的位置同样可能不一样!
[完]
- https://en.wikipedia.org/wiki/Environment_variables
- https://en.wikipedia.org/wiki/Env
- Computer Systems A Programmer’s Perspective Third edition