本文是南科大科学与工程计算中心 Linux 系列教程的第二篇。我们将学习环境变量的概念和使用方法,并了解那些影响程序编译和运行的常用环境变量。
01
什么是环境变量?为什么要关心它?
在操作系统中,程序会以进程的形式执行,包括我们平常使用的 shell(如 Bash, Zsh, Bourne)。要执行一行命令时,shell 会相应地创建一个新的子进程。对于一个进程而言,环境变量是一系列具有特定名字的值(即一系列键值对);作为进程运行环境的一部分,这些值往往是一些字符串,它们可以被动态设置,并在不改变程序代码的条件下影响程序的行为。
环境变量常用于配置用途,比如指定搜索目录、密码、端口等。父进程可以把自己的环境变量传递给子进程,所以人们常常先在 shell 会话(父进程)中设置环境变量以指定配置,再启动待运行的程序(作为子进程)。子进程在运行过程中通过编程接口读取环境变量(例如,C 程序可以通过 getenv(“NAME”) 的方式读取名为 NAME 的环境变量),并决定自己的行为。因此,环境变量也可以看作程序的一种输入数据。
因为 shell、编译器等通用的程序所读取的环境变量名称比较固定(例如PATH, LD_LIBRARY_PATH, CFLAGS 等,后文会详细介绍),所以我们可以通过设置环境变量,实现修改可执行文件查找目录、指定编译参数等目的。
02
查看环境变量
运行 printenv 命令,可以列出当前 shell 会话中的所有环境变量,如:
[user@host ~]$ printenv
LANG=en_US.UTF-8
USER=user
LOGNAME=User
HOME=/work/user
PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin
MAIL=/var/spool/mail/user
...
若要查看某个特定的环境变量,可以使用 echo 命令,如:
[user@host ~]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin
在这个例子中,PATH 是我们想要查看的环境变量的名称;在它前面加上$符号后,shell 会将 PATH 视作一个变量,读取它的值,得到/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin 这一字符串;然后,这个值被 echo 打印输出。
注:这里的 $PATH 也可写作 ${PATH}。
PATH 是一个特殊的环境变量。在 Linux 系统中,它的值由若干个目录路径拼接而成,并以冒号分隔。当我们在 shell 中输入一行(non built-in)命令时,shell 会按照从左到右的顺序到这些路径中查找与该命令同名的文件,直到找到为止。
03
设置环境变量
方法 1:临时定义
在要运行的命令前面(同一行内),直接加上 key=value 指定环境变量,如:
CC=icc CFLAGS="-O3 -Wall" ./configure
注:这行命令使用 CC 环境变量指定了 icc (Intel C Compiler) 作为 C 编译器,用 CFLAGS 环境变量指定了编译优化和警告参数,并运行 ./configure 进行软件安装前的配置。
使用这种方式指定的环境变量仅对该行命令有效,一般用于一次性的操作。
方法 2:保持有效直到当前 shell 会话结束
在交互式 shell 中,可以使用 export key=value 命令设置名为 key、值为 value 的环境变量,如:
[user ~]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin
[userexport PATH="/opt/app/bin:$PATH" ~]$
[user ~]$ echo $PATH
/opt/app/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin
还记得我们前面提到环境变量的值是字符串吗?我们可以在 shell 中直接拼接字符串:/opt/app/bin:$PATH 这一表达式中,环境变量 PATH 的旧值被取出,与 /opt/app/bin: 拼接起来,得到了新的值;而该值被设置成 PATH 的新值,这样便将 /opt/app/bin 这一目录插入到了 PATH 的最前处。
注:在 shell 中,用双引号括起来的字符串内部的变量引用依然会被替换成相应的值。这里使用双引号,是为了让 shell 将该字符串视作整体,避免路径中可能存在的空格截断字符串。
使用本方法设置的环境变量,在执行 exit 退出(或关闭终端窗口)后便会失效。
方法 3:长期有效
如果能让 shell 每次启动时都先执行方法 2 中的命令,便能使更新过的环境变量长期保持有效了。幸运的是,Linux 中各种 shell 在启动时,都会遵循一定的规则,去运行一些文件里的命令:
用于所有用户:/etc/profile
此文件会影响所有用户。Linux 系统中常用的 shell 都会在启动时读取和执行它。也有针对特定的 shell 的全局配置文件,如用于 Bash 的 /etc/bash.bashrc。
用于当前用户:~/.bashrc、~/.zshrc 等
将方法 2 中介绍的 export 语句写到 HOME 目录下相应的 shell 配置文件(例如,对于 Bash 为 .bashrc,对于 Zsh 为 .zshrc)的最后。
注:Bash 读取 ~/.bashrc 的方式有些特殊。从命令行登录系统并进入 Bash 时(例如通过 ssh),Bash 并不执行 ~/.bashrc,而是去执行 ~/.bash_profile;此后,启动新的 Bash 进程时,~/.bashrc 才会被执行。为了保持统一,我们一般只维护一份 ~/.bashrc,并在 ~/.bash_profile 中执行以下命令来加载 ~/.bashrc 的内容:
[[ -f ~/.bashrc ]] && . ~/.bashrc
方法 4:在脚本中指定环境变量
在 shell 脚本,也可以像方法 2 那样用 export 来指定环境变量。脚本中指定的环境变量在脚本运行期间有效。
我们可能还希望将脚本中指定的环境变量暴露到当前环境中。直接运行该脚本是无效的,因为通过 bash script.sh 或 ./script.sh 等方式运行的脚本运行在新的子进程中,脚本退出后,子进程中指定的环境变量便消失了。解决方案是使用 shell 内建命令 source 在当前 shell 进程中执行脚本,如 source script.sh。source 也可以简写成句点,如 . script.sh。
在超算上,不同的作业管理系统对于环境变量的处理方式有所不同。LSF 默认会继承提交环境下的环境变量,而 PBS 则不同。为了保险起见,建议在作业提交脚本中明确地指定环境变量。
陷阱:反复 source 同一脚本
在修改 shell 配置文件后,为使之生效,可以退出当前的 shell 会话,重新开启新的 shell。
也有人通过执行 source .bashrc 实现更新环境变量的目的。笔者并不推荐这种做法,因为在环境变量配置比较复杂的情况下,可能需要反复修改 .bashrc 等文件来更正错误。而每次运行 source 命令,都是在基于当前现有环境进行更改。若当前环境变量中存在错误,则该错误可能会因为字符串拼接而被一直保留,导致配置结果始终不理想。比如,假设当前 PATH 变量的值为 /err:/usr/bin:/usr/sbin,其中 /err 是被错误配置的内容,而 .bashrc 中设置 PATH 的方式为 export PATH=”$PATH:/correct”。执行一次 source 后,PATH 更新为 /err:/usr/bin:/usr/sbin:/correct;执行两次后变成/err:/usr/bin:/usr/sbin:/correct:/correct;依此类推。不难发现,在这个例子中,无论连续执行多少次 source 命令,错误都无法得到更正。
04
常用的环境变量
基本路径
PATH
如前文所介绍,shell 会到 PATH 环境变量所指定的目录中查找待执行的命令。如果多个目录中存在名称相同的可执行程序,则 shell 会选择最左出现的那个。
对于某个给定名称的命令,如果想知道 shell 会选择哪个文件,可以使用 which 命令,如:
[user@host ~]$ which python
/usr/bin/python
[user@host ~]$ source ./venv/bin/activate
(venv) [user@host ~]$ which python
/work/user/venv/bin/python
HOME
环境变量 HOME 的值表示当前用户的主目录路径。对于非 root 用户,这一路经通常为 /home/,但也可以配置成别的路径。例如,在太乙集群上,名为 user 的用户的主目录便是 /work/user:
[user@host ~]$ echo $HOME
/work/user
正因为 HOME 目录的路径是可配置的,我们在编写脚本的时候,应当使用 $HOME 来准确获取当前用户的 HOME 目录,而非用 /home/$(whoami) 的方式进行拼接。
注:在 shell 中,~ 也常被展开为 HOME 目录的路径,但它不是环境变量。
PWD
环境变量 PWD 的值表示当前目录的绝对路径。
注:小写的 pwd 是一个 shell 内建命令,运行它可以输出当前目录的绝对路径;而大写的 PWD 则是一个环境变量。
编译运行一条龙
编译是我们在开发静态语言(如 C/C++, Fortran)程序的过程中必不可少的一步,也是我们在 Linux 系统上安装软件的常用方式之一。在超算环境下,由于普通用户往往没有使用包管理器的权限,编译安装软件便成了家常便饭。
对于一个典型的 C/C++ 程序而言,从源码到可执行程序,需要经过预处理、编译和汇编、连接这几个步骤。我们先来了解一下这些步骤中涉及的常用环境变量,然后通过一个简单的例子说明这些环境变量的使用方式。
预处理阶段
C/C++ 程序源代码中往往有许多头文件(.h,.hpp)。在预处理阶段,预处理器(cpp, the C preprocessor)会展开代码中 #include 指令,替换成头文件的内容。预处理器会默认到 /usr/include 等目录中寻找头文件;而配置环境变量,可以让预处理到我们指定的目录中去查找头文件。
多数情况下,我们只需要指定 CPATH 环境变量,加入我们想要引入的库的头文件所在的目录。可以指定多个查找目录,只需要像在 PATH 环境变量中所做的那样,用冒号进行分隔即可。
也有人喜欢用 -I/path/to/include 参数指定头文件查找路径,这亦不失为一种可行之策,不过笔者更喜欢指定环境变量这种方式的简便。
编译阶段
这些环境变量可以被 CMake、(Autoconf 生成的)configure 等程序识别,用于生成明确指定编译器及其参数的 Makefile 文件。
连接阶段
在连接阶段,连接器 ld 会到 /usr/lib64 等目录下查找静态链接库(.a文件)和动态链接库(.so文件,扩展名中可能还带有版本号),与当前程序的多个二进制目标文件(.o)整合到一起,生成可执行程序文件。影响连接阶段的环境变量包括:
运行阶段
注:为什么动态链接库的查找路径需要在连接和运行阶段分别指定两次呢?请考虑一台机器上编译的程序被复制到另一台机器上运行的情形。不同机器上安装有的动态链接库的位置、版本可能有所不同,故连接阶段不宜将动态链接库的路径写死在可执行程序的内容中。
利用 ldd 命令,我们可以查询一个程序在当前环境下会加载的动态链接库的路径,以检查加载的是否为我们所期待的版本。如:
[user@host ~]$ ldd ./demo
linux-vdso.so.1 (0x00007ffd889f3000)
libm.so.6 => /usr/lib/libm.so.6 (0x00007fabee67f000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007fabee4b9000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fabee80d000)
此外,当二进制接口兼容时,我们还可以利用 LD_LIBRARY_PATH 所提供的灵活性,在不改动代码的条件下(考虑一些闭源程序),给同一个可执行程序指定不同的动态链接库(如换成优化过的版本)。
05
实例:让 HDF5 使用指定的 zlib
在多人共享的超算环境下编译安装软件的过程中,可能会遇到这样的情景:想要安装软件 A,A 依赖于软件 B 的某个特定版本。集群上可能没有安装 B,或者已安装的版本无法满足要求,而自己又没有权限用包管理器安装或升级 B 。一种解决方案是找管理员代为安装。那么,有没有“自己动手丰衣足食”的解决方案呢?如果 B 也是开源的,那么答案是肯定的——我们可以通过自行编译安装 B 解决这一问题。
HDF5 是高性能计算中用于组织和存储大量数据的一种较常用的格式;驱动这一格式的软件包(亦称作 HDF5)依赖于 zlib,后者常用于实现数据压缩。接下来,我们将以 HDF5 为 A、以 zlib 为 B 举例,让编译安装的 HDF5 不去使用系统目录下已有的 zlib,而是使用我们自行指定的 zlib。由于本文主要关注环境变量,关于编译选项设置等方面的内容会有所精简。
首先需要到 zlib.net 和 hdfgroup.org 分别下载 zlib 和 HDF5 的源代码,得到 zlib-1.2.11.tar.gz 和 hdf5-1.12.0.tar.bz2。
我们先编译安装 zlib,将其安装到 ~/local 目录中:
tar xvzf zlib-1.2.11.tar.gz # 解压
cd zlib-1.2.11/ # 进入解压得到的目录
CC=gcc ./configure --prefix="$HOME/local" # 配置
make -j$(nproc) # 多进程编译
make install # 将编译得到的文件复制到 ~/local 中
在上述的 ./configure 命令中,我们引用了 HOME 环境变量并拼接出安装目录,用 CC 环境变量指定 gcc 作为编译器。除此以外,其实也可以同时设置 CFLAGS 等环境变量,调整优化参数。在运行 ./configure 前,还可以通过 ./configure –help 查看其支持的配置选项。
执行完上述命令后,zlib 这个库的头文件、静态和动态链接库等都被安装到了 ~/local 目录中:
/work/user/local
├── include
│ ├── zconf.h
│ └── zlib.h
├── lib
│ ├── libz.a
│ ├── libz.so -> libz.so.1.2.11
│ ├── libz.so.1 -> libz.so.1.2.11
│ ├── libz.so.1.2.11
│ └── pkgconfig
│ └── zlib.pc
└── share
└── man
└── man3
└── zlib.3
6 directories, 8 files
为了在编译 HDF5 时到正确的目录中寻找 zlib 的头文件和库文件,我们需要先设置一下环境变量:
export CPATH="$HOME/local/include"
export LIBRARY_PATH="$HOME/local/lib"
然后,我们将 HDF5 也安装到 ~/local 目录下:
./configure --prefix=$HOME/local --enable-build-mode=production --enable-tests=no
make -j$(nproc)
make install
最后,我们编辑 .bashrc,更新 PATH 和 LD_LIBRARY_PATH 的值:
export PATH="$HOME/local/bin:$PATH"
if [[ -z "$LD_LIBRARY_PATH" ]] ; then
export LD_LIBRARY_PATH="$HOME/local/lib"
else
export LD_LIBRARY_PATH="$HOME/local/lib:$LD_LIBRARY_PATH"
fi
重启 shell 后,我们可以用 ldd 进行检查:
[user@host ~]$ ldd ~/local/lib/libhdf5.so
linux-vdso.so.1 (0x00007ffd027f6000)
libz.so.1 => /work/user/local/lib/libz.so.1 (0x00007f702d59e000)
libdl.so.2 => /usr/lib/libdl.so.2 (0x00007f702d558000)
libm.so.6 => /usr/lib/libm.so.6 (0x00007f702d412000)
libc.so.6 => /usr/lib/libc.so.6 (0x00007f702d24c000)
/usr/lib64/ld-linux-x86-64.so.2 (0x00007f702d9f1000)
可以看到,libz.so.1 已经正确指向了我们自行编译的 zlib。
06
总结
本文讲解了环境变量的含义和配置方式,介绍了一些常用的环境变量。
理解环境变量的作用方式,有助于我们读懂程序编译和运行时所报的一类错误的产生原因,并相应解决之。
限时特惠:本站每日持续更新海量各大内部网赚创业教程,会员可以下载全站资源点击查看详情
站长微信:11082411