【Linux&操作系统】4. 环境变量

41 阅读7分钟

4 环境变量

4.1 环境变量的概念

  • Linux的环境变量是系统中用于存储系统配置信息的变量,它们影响系统上运行的程序和进程的行为,环境变量保存了有关系统信息,程序配置,用户配置信息等的数据,并且在用户登录时,程序执行时等上下文中发挥作用(来自Chat_GPT)

  • 说简单点,就类似于程序中的全局变量,系统某些操作的逻辑会依据这些全局变量(但是哈,环境变量是在系统层面的,这点别搞错了,而且环境变量一般是字符串)

  • 看不明白的话咱们在后面几个小节会举例子的

4.2 main()函数的参数问题

  • 了解本小节能加深我们对于环境变量的理解

  • 要知道,main()函数也是有参数的,有两个参数是int argc,char* argv[]

  • 后者是一个字符指针数组,前者是一个整形

  • 前者代表着后者数组的元素个数

  • 所以我们可以试着输出一下里面的内容

#include<stdio.h>
int main(int argc, char *argv[])
{
    int count = 0;
    for(; count != argc; count++)
    {
        printf("argv[%d]:%s\n", count, argv[count]);
    }

    return 0;
}
$ ./test
argv[0]:./test
  • 接着,我们来试一个有点神奇的内容
  • 如果我们在运行程序的指令之后再加一些内容呢?
$ ./test a b c
argv[0]:./test
argv[1]:a
argv[2]:b
argv[3]:c
  • 不难发现,这么做是不是有点像命令行指令呢?
  • 比方说
ls -a
  • 我们再把程序改一改
#include<stdio.h>
#include<string.h>

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        printf("error, you should use test like: test -a\n");
        return 0;
    }
        
    if(strcmp(argv[1], "-a") == 0)
        printf("using mode a\n");
    else if(strcmp(argv[1], "-b") == 0)
        printf("using mode b\n");
    else if(strcmp(argv[1], "-c") == 0)
        printf("using mode c\n");
    else
        printf("error, you should use test like: test -a\n");

    return 0;
}
  • 编译并运行
$ ./test -a
using mode a

$ ./test
error, you should use test like: test -a

$ ./test -b
using mode b
  • 是不是有那味儿了?

  • 我们知道Linux的指令本质上也是可执行程序

  • 但是事实是,我们没法直接像用Linux指令一样用它,

  • 我们知道,/usr/bin目录是存放系统指令的地方,我们可以把我们写的程序拷贝到这个目录下试试

  • 注意:Linux似乎不允许存在一个叫test的命令,所以我们需要改一下名字,这里我改成test_program

$ test_program -a
using mode a

$ test_program
error, you should use test like: test -a
  • 于是,这个可执行程序就真的可以像命令一样执行了
  • (事后别忘了删掉)

4.3 两张表

  • 为什么系统可以不用指明目录就可以找到这些命令的可执行程序?

  • 因为系统存在一个叫环境变量的东西,如果用户输入了一个指令,这串指令本质上就是一串字符串,于是shell会把这串字符串拿去分析

  • 字符串开头就是需要运行的程序名,后面接着的都是选项

  • 然后shell会在环境变量给出的目录中寻找对应名字的程序,然后调用系统调用接口,并传递程序的选项给系统,由系统启动相关程序

  • 意味着,这个环境变量在这里起的作用就是"默认程序启动路径"

  • argv表:其实就是选项表,我们说shell会把这串字符串拿去分析,分析后就会生成一张表,即argv表,程序会根据这张表决定应该执行哪些逻辑

  • 环境变量表:shell会在环境变量给出的目录中寻找对应名字的程序,注意:这里的环境变量不是只有一个,而是有很多个,以Key Value的形式构成一张表,这一点我们后面的小节会提到的

  • 这两张表其实本质上都是二维数组

4.4 环境变量的查询

  • 查询所有环境变量
$ env
XDG_SESSION_ID=18320
HOSTNAME=iZwz9b2bj2gor4d8h3rlx0Z
TERM=xterm
SHELL=/bin/bash
HISTSIZE=1000
SSH_CLIENT=120.227.54.111 10287 22
SSH_TTY=/dev/pts/0
USER=oldking
# ...
# 数量很多,就不全部展示了
# 这里是不是就像是一张表了呢
  • 查询某个环境变量
echo $[environment_variable_name]
  • 例如
$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/oldking/.local/bin:/home/oldking/bin
  • 注意:echo PATH是错误指令,PATH前必须要加上$,类似于Makefile的语法

  • 可以看到,这个环境变量里的东西挺多的,同时也不难看出,这串字符串中包含多个路径,每个路径由:分隔,

  • shell会依次查找这些路径中的可执行程序,直到找到一个对应上的

  • 其他常见的环境变量:

    1. $HOME:当前用户的主目录路径。
    • 示例: /home/username
    • 例如: 执行cd ~时,实际上就是切换到 $HOME 变量指定的目录。
    1. $USER:当前登录用户的用户名。
    • 示例: username
    1. $SHELL:当前用户使用的shell类型。
    • 示例: /bin/bash,/bin/zsh
    1. $PWD:当前工作目录的绝对路径。
    • 示例: /home/username/projects
    1. $LANG:系统语言和区域设置。
    • 示例: en_US.UTF-8
    1. $EDITOR:系统默认的文本编辑器。
    • 示例: vim,nano
    1. $TERM:当前终端的类型。
    • 示例: xterm,screen
    1. $PS1:命令提示符的格式。
    • 示例: \u@\h:\w\$(比方说:[oldking@oldkingnana_s_pc testdir]$)
    1. $LD_LIBRARY_PATH:用于指定共享库搜索路径。
    • 示例: /usr/local/lib:/usr/lib:/lib
    1. HOSTNAME:主机名
    • 示例: oldkingnana_s_pc
    1. HISTSIZE:历史指令的存储量(比方说上下键最多可以存储的指令数量)
    • 示例: 1000
    1. SSH_TTY:终端设备的路径
    • 示例: /dev/pts/0(这个路径在"一切皆文件"提到过)
    1. LOGNAME:登录用户
    • 示例: oldkingnana
    1. OLDPWD:上一次进入的目录
    • 示例: /home(如果没有访问上一个目录则为空,可以用cd -切换到上一个目录)

4.5 环境变量的修改

  • 临时修改环境变量
$ PATH=/home/oldking
$ echo $PATH
/home/oldking
$ ls
-bash: ls: command not found
  • 好消息:修改成功了

  • 坏消息:它把以前的也给覆盖了,导致原本的默认路径都用不了了

  • 怎么解决?

  • 重新登陆shell就行

  • 为什么重新登陆shell就可以?

  • 首先我们得知道,环境变量其实存储在内存中,并且,每个用户的环境变量还可以不一样

  • 我们用户在登录shell的时候,系统会启动一个叫bash的进程,然后bash被启动的时候,就会将已经配置好的环境变量加载进内存中

  • 我们更该的环境变量仅仅只是内存级的更改,下次启动又会将已经配置好的环境变量重新加载

  • 不难猜到,这个"配置好的环境变量"一定是在磁盘中的某个文件

  • 在每个用户的/home/username的目录下,都会有两个隐藏的配置文件跟环境变量有关

-rw-r--r--   1 oldking oldking  193 Nov 25  2021 .bash_profile
-rw-r--r--   1 oldking oldking  355 Oct 19 21:44 .bashrc
  • 首先我们可以看一下这两个文件的内容
$ cat .bashrc
# .bashrc

# Source global definitions
if [ -f /etc/bashrc ]; then
	. /etc/bashrc
fi

# Uncomment the following line if you don't like systemctl's auto-paging feature:
# export SYSTEMD_PAGER=

# User specific aliases and functions
alias vim='/home/oldking/.VimForCpp/nvim'
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:~/.VimForCpp/vim/bundle/YCM.so/el7.x86_64


$ cat .bash_profile 
# .bash_profile

# Get the aliases and functions
if [ -f ~/.bashrc ]; then
	. ~/.bashrc
fi

# User specific environment and startup programs

PATH=$PATH:$HOME/.local/bin:$HOME/bin

export PATH
  • 这两个配置文件中都是shell脚本,我们不需要完全看懂,不过就看文件里那几个目录咱就明白,.bashrc肯定调用了/etc/bashrc的这个文件,.bash_profile肯定调用了.bashrc这个文件

  • 解释一下这两个文件:

    1. .bashrc:这个文件会调用/etc/bashrc,当用户使用非登录的shell时(非登录shell其实就类似于从某个带有图形化界面的Linux的桌面的终端图标启动终端,反正我们已经登陆过了,不必再登陆了,所以就使用.bashrc),bash进程会读取.bashrc,然后.bashrc的内容又是读取/etc/bashrc,然后将读取到的内容拷贝到内存中bash的环境变量表中的PATH
    2. .bash_profile:.bash_profile则是在使用登录shell的时候,进程bash会调用.bash_profile,然后.bash_profile又会调用.bashrc,最后又调到/etc/bashrc
  • 这俩文件可以用于

    • 配置环境变量
    • 为命令取别名(例如ll)
    • ...
  • (具体可以查一查)

  • 我们使用的是shell登录,所以我们可以修改.bash_profile

# .bash_profile

# Get the aliases and functions
if [ -f ~/.bashrc ]; then
	. ~/.bashrc
fi

# User specific environment and startup programs

PATH=$PATH:$HOME/.local/bin:$HOME/bin:/home/oldking/testdir

export PATH
  • 于是,我们的PATH就变成了这样
$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/oldking/.local/bin:/home/oldking/bin:/home/oldking/testdir
  • 现在我们也就可以直接用/home/oldking/testdir的命令了
$ test_program
error, you should use test like: test -a
  • 当然,这只是对当前用户的环境变量的修改,每个不同用户启动的bash所加载的环境变量的配置文件都不同,当然,还有系统的统一配置文件/etc/bashrc
  • 即,用户可以以新增环境变量(或其值)的形式自定义自己的环境变量,但同时又必须保留系统统一的环境变量配置

4.6 环境变量的其他相关命令和代码中获取方式

4.6.1 命令
# 新增环境变量
export [env_name]=[content]

# 移除环境变量
unset [env_name]
4.6.2 代码中获取方式
4.6.2.1 通过main()参数获取
  • main()函数还有第三个参数char *env[]
#include<stdio.h>
#include<string.h>

int main(int argc, char *argv[], char *env[])
{
    (void) argc;
    (void) argv;

    int i = 0;
    for(i = 0; env[i]; i++)
    {
        printf("env[%d]:%s\n", i, env[i]);
    }
    return 0;
}
  • 执行后输出内容为
$ ./test
env[0]:XDG_SESSION_ID=18330
env[1]:HOSTNAME=iZwz9b2bj2gor4d8h3rlx0Z
env[2]:TERM=xterm
env[3]:SHELL=/bin/bash
env[4]:HISTSIZE=1000
# ...
4.6.2.2 通过接口获取
  • getevn()
  • 这个接口会搜索当前进程的的环境变量表(环境变量在进程中存在继承动作,意味着父进程会将环境变量继承给子进程,这也是为什么我们什么都不用输入就可以让main()函数的第三个参数拥有内容的原因),然后寻找和对应参数符合的环境变量,有的话就返回char*,没有就返回NULL
SYNOPSIS
       #include <stdlib.h>
       char *getenv(const char *name);
RETURN VALUE
       The getenv() function returns a pointer to the value  in  the  environ‐
       ment, or NULL if there is no match.
  • 使用案例
$ cat test.c
#include<stdio.h>
#include<string.h>
#include<stdlib.h>

int main()
{
    char* env = getenv("PATH");    
    printf("%s\n", env);

    return 0;
}
$ ./test
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/oldking/.local/bin:/home/oldking/bin
4.6.2.3 通过全局变量获取
  • environ是一个全局变量,类型是char ** [],包含在头文件<unistd.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>

extern char** environ;
int main()
{
    int i = 0;
    for(i = 0; environ[i]; i++)
    {
        printf("environ[%d]:%s\n", i, environ[i]);
    }
    return 0;
}
$ ./test
environ[0]:XDG_SESSION_ID=18330
environ[1]:HOSTNAME=iZwz9b2bj2gor4d8h3rlx0Z
environ[2]:TERM=xterm
environ[3]:SHELL=/bin/bash
# ...

4.7 本地变量

  • 本地变量和环境变量没啥直接联系,但还是得提一嘴

  • 本地变量有点像是.c文件中的变量,一样可以声明,给值进去

$ i=10
  • 可以用set查询(set会把环境变量一并查出来)
$ set

# ...
XDG_SESSION_ID=18330
_=
colors=/home/oldking/.dircolors
i=10
  • 为什么需要本地变量?

  • 有一个东西叫shell脚本,类似于一门简单的编程语言,只不过它是帮助我们批量化干重复的活的,所以需要本地变量

  • 本地变量一样存储在bash进程中,一样在内存中

  • 但本地变量不能被子进程继承,只能存在于bash

  • unset命令可以删除本地变量,用法和环境变量一致,就不多提了

4.8 内建命令

  • 我们知道,环境变量可以由父进程继承给子进程,一个进程很难修改(或者说不能修改)另一个进程的环境变量,因为两个任意进程之间有独立性

  • 问题就来了,export这个进程怎么修改它的父进程bash的环境变量的?

  • 事实是,export根本就不是一个进程,而是一个"内建命令",意味着执行操作的不是export这个进程,而是bash亲自执行,bash分析命令确认你需要用export之后,他会执行自己的代码,意味着bash的代码本身就包含export的所有逻辑


  • 如有问题或者有想分享的见解欢迎留言讨论