15

你不知道的环境变量

 4 years ago
source link: https://www.tuicool.com/articles/uU7nUz2
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

编者按:本文作者李松峰,资深技术图书译者,翻译出版过40余部技术及交互设计专著,现任360奇舞团Web前端开发资深专家,360前端技术委员会委员、W3C AC代表。

提到环境变量,我们都知道 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系统中,环境变量区分大小写。

fork,exec

在Unix中,环境变量通常在系统启动时由初始化脚本进行初始化,然后由系统中的所有其他进程继承。同样,当在一个程序中打开另一个程序时,调用程序会先复制一个与自身完全一样的进程,即子进程。子进程可以根据需要修改环境变量。最后,子进程再通过执行被调用的程序来覆盖自己。其中,复制进程对应 fork ,执行程序对应 exec

  • fork:是由操作系统内核实现的系统调用,用于创建当前进程自身的一个副本;

  • exec:同样是由操作系统内核实现的系统调用,用于在已有进程的上下文中运行一个可执行文件。

exec在实际实现中通常是一组函数的代称,比如在C语言中就有 execlexecleexeclpexecvexecveexecvp 等几个函数,它们共用 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 。下图展示了参数数组的数据结构:

aqUFN3j.png!web

如图所示,变量 argv 指向一个NULL结尾的指针数组,前面的每个元素都是一个指向参数字符串的指针。按照约定, argv[0] 是可执行文件的名称。下面是环境变量指针数组的数据结构:

BBJJfqA.png!web

两上数据结构类似。唯一的区别是,环境变量数组元素指向的字符串都是名-值对形式的,比如 "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 命令显示,但却 不是 真正的环境变量。这种变量只是命令行程序存储的,操作系统内核并不认。因此 envprintenv 命令不会显示它们,子进程也不会继承它们:

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

命令行脚本除了可以直接使用环境变量,还有一种常见的用法,就是“ 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

关于奇舞周刊

《奇舞周刊》是360公司专业前端团队「 奇舞团 」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。

fI773i6.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK