Linux环境变量详解
1. 生效时机
与环境变量有关的四个文件
-
/etc/profile
-
/etc/bashrc
-
~/.bash_profile
-
~/.bashrc
本篇文章实验比较多, 步骤有点复杂, 这里先说结论
执行方式 | profile | bashrc | 示例 |
---|---|---|---|
非交互式且非登录shell | 不加载 | 不加载 | crontab 中执行的bash脚本 |
非交互式登录shell | 加载 | 加载 | crontab 中通过su -l 执行的脚本 |
交互式非登录shell | 不加载 | 加载 | 如命令行中通过su 用户名 执行的命令 |
交互式登录shell | 加载 | 加载 | 普通的终端登录, ssh远程登录, 以及使用su - l 用户名 切换身份等 |
1.1 理论基础
首先认识一下登录shell与非登录shell, 交互式shell与非交互式shell.
1.1.1 登录(login)与非登录(nologin)
普通的终端登录, ssh远程登录, 或者是su -l 用户名
切换用户等, 包含的登录行为, 称打开的终端类型为登录式shell, 除了读取profile
, 还会读取bashrc
;
与之相对的, 在打开的终端中执行bash
命令得到新的shell会话, 还有使用su 用户名
切换用户的操作创建的终端, 以及执行脚本时创建的bash进程, 都是非登录式shell.
需要注意的是, 从桌面发行版图形界面登录, 或是通过VNC登录行为, 都是非登录形式. 经过实验, 觉得这句话并正确, 点击登录按钮时, 桌面程序会加载/etc/profile
文件, 见下面的实验.
1.1.2 交互式与非交互式
交互式模式: 就是shell等待用户输入,并且执行用户键入的命令. 此时shell与用户进行交互, 输入输出与终端相连. 这种模式也是大多数用户非常熟悉的: 终端登录、执行一些命令、退出. 退出后, shell也随之终止.
非交互式模式: shell不与用户进行交互,而是读取存放在文件中的命令, 并且执行. 当它读到文件的结尾,shell也就终止了. 典型的例子就是crontab
定时执行脚本, 它们的输入输出不会与任何bash进程相连接.
它们之间可以相互组合, 所以一共有4种情况.
1.2 实验验证
1.2.1 验证登录式与非登录式shell
实验如下操作
[root@localhost ~]# echo 'usual_var=123456' >> /etc/profile
[root@localhost ~]# echo $usual_var
## 切换为普通用户, 非登录shell
[root@localhost ~]# su general
[general@localhost root]$ echo $usual_var
[general@localhost root]$ exit
exit
[root@localhost ~]# echo $usual_var
[root@localhost ~]# su -l general
Last login: Sun Dec 11 20:01:46 PST 2016 on pts/1
[general@localhost ~]$ echo $usual_var
123456
[general@localhost ~]$ exit
logout
[root@localhost ~]# echo $usual_var
可以看出ssh
命令的-l
选项的作用, 就是开启一个正常的登录shell会话. 同时也验证了, profile
文件需要是登录shell才能加载的.
继续进行, 在当前终端执行bash
命令, 会打开一个新的shell, 这也是一个非登录式shell.
[root@localhost ~]# echo 'usual_var=123456' >> /etc/profile
[root@localhost ~]# echo $usual_var
[root@localhost ~]# bash
[root@localhost ~]# echo $usual_var
可见, bash
与su
都没有重新加载profile
文件.
将usual_var
变量写入到bashrc
文件, 再执行上述类似操作, 你会发现bash
, su
与su -l
三种行为都会加载bashrc
, 这里不再列出.
注意bash命令加载的是当前用户的
~/.bashrc
, 而su
操作加载的是目标用户的~/.bashrc
.
1.2.2 crontab的非交互式属性
我们都能理解, 通过crontab
执行的脚本, 输入输出不与特定终端相连, 所以被称为非交互式shell. 下面我们验证一下, 使用crontab
执行定时任务时, 任务脚本对两种文件的加载情况.
在/etc/profile
中添加环境变量env_var
.
$ echo 'export env_var=abcdef' >> /etc/profile
建立实验用的任务脚本nointeract.sh
$ cat /tmp/nointeract.sh
#!/bin/bash
echo $env_var >> /tmp/var_result
建立crontab
任务, 每10s执行1次.
$ crontab -e
* * * * * sleep 10; /bin/bash /tmp/nointeract.sh
* * * * * sleep 20; /bin/bash /tmp/nointeract.sh
* * * * * sleep 30; /bin/bash /tmp/nointeract.sh
* * * * * sleep 40; /bin/bash /tmp/nointeract.sh
* * * * * sleep 50; /bin/bash /tmp/nointeract.sh
…等待10s后, 查看/tmp/var_result
文件, 是空的, 说明任务脚本没有读取到/etc/profile
. 然后把env_var
变量写到bashrc
文件中, 10s后var_result
依然文件的输出依然为空.
说明非交互shell且非登录形式的bash进程不会加载profile
也不会加载bashrc
.
先将上面写入到bashrc
的env_var
变量删除, 只保留profile
, 然后修改crontab
任务列表, 内容如下
* * * * * sleep 10; su -l root -c '/bin/bash /tmp/nointeract.sh'
* * * * * sleep 20; su -l root -c '/bin/bash /tmp/nointeract.sh'
* * * * * sleep 30; su -l root -c '/bin/bash /tmp/nointeract.sh'
* * * * * sleep 40; su -l root -c '/bin/bash /tmp/nointeract.sh'
* * * * * sleep 50; su -l root -c '/bin/bash /tmp/nointeract.sh'
然后, /tmp/var_result
就有输出了, 的确是profile
文件中定义的env_var
的值.
保留crontab
, 将env_var
从profile
文件中移除, 添加到bashrc
中, 观察/tmp/var_result
的输出, 也能得到env_var
的值.
说明非交互式登录shell可以同时加载profile
与bashrc
文件.
1.2.3 图形界面的非登录属性
为了验证这一点, 需要有两台主机, 分别以A和B指代, A是拥有图形界面的桌面发行版.
我们在主机A的/etc/profile
文件写入以下内容. (主机A是Fedora 24的桌面发行版)
for i in {0..100};
do
ping -c 1 主机B的IP地址
sleep 1
done
重启主机A, 不做任何登录操作, 我们在主机抓取来自主机A的ICMP包tcpdump -i eth0 src 主机A的IP and dst 主机B的IP
. 没有任何反应.
然后, 在主机A的图形界面登录, 将会得到100秒的黑屏, 在这期间, 主机B上可以抓取到来自A的ping包. 说明在这段时间主机A在加载/etc/profile
的内容, 并且需要等到其执行完成才进行下一步操作.
还没完, 在主机A的图形界面黑屏期间, 我们尝试使用ssh连接到主机A, 刚完成登录, 终端界面就打印出ping操作的结果, 说明这个新创建的bash进程也尝试加载并执行了/etc/profile
文件( 不同于图形界面完全阻塞的黑屏状态, 在ssh界面我们可以使用Ctrl+C取消此ping操作的执行), 这很正常.
在ssh登录的shell中执行查询
$ ps aux | grep bash
general 7056 0.0 0.0 119904 8 tty2 S+ 22:37 0:00 -/bin/bash -c gnome-session
root 23738 0.0 0.1 120708 4376 pts/0 Ss 22:42 0:00 -bash
root 32895 0.0 0.0 117140 1032 pts/0 R+ 22:51 0:00 grep --color=auto bash
也就是说, 图形界面其实也是由bash进程启动的. 但在未登录阶段, /etc/profile
并未起作用.
等待图形界面黑屏结束, 在ssh终端中, 再次查找bash
进程, 就只剩下自己了. 停留在图形界面时, 没有开启bash进程. 但是当打开"终端"程序时, 就会自动以当前用户建立交互式登录shell.
/etc/profile
与"开机启动"这种操作没有任何关系, 不要把开机启动项写在这里, 没用的.
2. 加载顺序
非交互式非登录shell比较好说, 哪一个都不加载…
交互式非登录shell也比较好说, 也就是/etc/bashrc
-> ~/.bashrc
, 后者覆盖前者.
两种登录式shell两种文件都会加载, 一般是
/etc/bashrc
-> ~/.bashrc
,
/etc/profile
-> ~/.bash_profile
.
不过/etc/bashrc
-> ~/.bash_profile
, 总是用户级配置优先级更高. 所以最终加载顺序可以看成是
/etc/profile
-> /etc/bashrc
-> ~/.bash_profile
-> ~/.bashrc
.
Linux环境变量解析2-变量作用域及生命周期
-
abc=123
-
abc=123 && set -a abc
-
export abc=123
-
declare abc=123
-
declare -x abc=123
-
env abc=123
其中, 1和4的效果相同, 2,3和5的效果相同.
1,4只是在当前bash进程中设置变量, 无法影响到其他bash进程, 无论是子bash进程, 父bash进程还是同级的新创建的bash进程. 这里称这种变量为普通变量, 也被称为bash变量.
通过2,3,5方式创建的变量, 在同一用户, 并且属于当前bash进程的子级进程中有效. 即通过当前bash shell以这几种方法创建的变量, 可以传递给所有在当前bash, 以同一用户身份启动的进程中. 但对于父级bash进程和同级bash进程无效. 这种变量称为用户变量.
变量作用域分析
bash变量
示例1
当前bash中定义的普通变量, 无法传递到子bash进程.
$ cat test.sh
#!/bin/bash
echo 'begin'
echo $abc
echo 'end'
$ abc=123
$ echo $abc
123
$ ./test.sh
begin
end
示例2
当前bash中定义的普通变量, 也无法影响到父级bash进程.
$ cat test.sh
#!/bin/bash
def=456
echo $def
$ ./test.sh
456
$ echo $def
示例3
但是这种普通变量却可以传递到subshell进程中, 不过subshell中定义的普通变量影响到父shell.
$ abc=123
$ (echo $abc)
123
$ (abc=789; echo $abc)
789
$ echo $abc
123
这里需要注意的是, bash进程与shell进程不是一回事. 两者的区别可以通过$$
特殊变量查看, 这表示当前bash进程的进程号, 或者也可以通过$BASH_SUBSHELL
变量查看, 它表示子shell的层级数.
$ echo $$; echo $BASH_SUBSHELL
3299
0
$ (echo $$; echo $BASH_SUBSHELL)
3299
1
可以看到, 这种通过小括号创建的subshell, 与原shell同用同一个bash进程, 只是shell层级会增加. subshell是允许嵌套调用的,可以在函数或圆括号结构内再次调用圆括号结构创建subshell.
而执行脚本时, 由于第一行#!/bin/bash
这一句的存在, shell脚本都是新建bash进程然后再执行其中的命令.
$ cat test.sh
#!/bin/bash
echo $$; echo $BASH_SUBSHELL
$ ./test.sh
5990
0
这两者的区别应该用更深层的涵义, 目前先保留这个问题<???>.
用户变量分析
用户级变量与bash级变量最大的不同, 应该就是前者可以延父子关系上下传递而后者不可以.
通过2,3,5中任意一种方式可以创建用户级变量.
$ cat test.sh
#!/bin/bash
echo 'begin'
echo $abc
echo 'end'
$ export abc=789
$ ./test.sh
begin
789
end
不同级别变量操作
1. set命令
set命令作用主要是显示系统中已经存在的shell变量, 或者用于更改shell特性,符号"+“和”-"的作用分别是打开和关闭指定的模式. set命令本身不能够定义新的shell变量.
在不添加任何选项的时候, 显示当前会话中所有合法的变量,包括用户级变量.
2. env命令
不接任何参数的情况下, 显示当前的用户级别变量.
env abc=123
可以将abc
设置为用户级别变量, 当前bash进程及其子进程中有效.
env -u abc
可以从当前用户级环境变量中删除变量abc
.
3. export命令
在不接任何参数时, 显示当前导出成用户变量的bash变量, 这些变量原本是bash级别变量, 之后被提升至用户级别.
它可以接一个参数, 类似于export abc=123
或是abc=123 && export abc
, 将普通变量abc
提升到用户变量级别, 之后通过此shell启动的命令, 都可以读取到这个变量, 它的生命周期仅限于当前shell结束之前.
不过, export
的输出都很相似, 如下
$ export
declare -x CLASSPATH=".:/usr/local/jdk1.8.0_101/lib/dt.jar:/usr/local/jdk1.8.0_101/lib/tools.jar"
declare -x HISTCONTROL="ignoredups"
declare -x HISTSIZE="1000"
...
所有被提升至用户级别的变量, 实际上都是通过declare
命令为bash级变量赋予了-x
属性, 暂时不知道这两个命令之间有何内在的关联<???>.
4. declare命令
在不接任何参数时, 可以显示当前会话中所有合法变量…貌似与set的输出相同, 不仅包括普通变量, 还有用户级别变量.
这句话被注释掉了, 但也不能完全说是错误的, 在CentOS7系统中declare abc=123
可以将变量abc
声明为一个普通变量.declare abc=123
默认为附带-x
属性, 即将其声明为用户级别变量, 所以如果不带任何属性, abc
也本应该只是一个普通变量而已.