目录

一、基本概念

二、讲解命令行参数

2.1. main 函数并不是程序的入口函数

2.2. 真实的 main 函数样子 --- main(int argc,char* argv[])

2.2.1. 简单代码样式

2.2.2. 命令行参数的作用

2.2.3. 命令行参数表

三、引出环境变量

四、环境变量的相关知识

4.1. 常见的环境变量

4.2. 查看环境变量

4.2.1. 查看所有的环境变量

4.2.2. echo $NAME

4.2.3 如何直接运行可执行程序

4.3. 认识更多的环境变量

4.4. 相关命令行和系统接口

4.4.1. 相关命令行

4.4.2. 代码

​编辑4.5. 环境变量通常是具有全局属性的

4.6. 一些其他问题

4.6.1. 本地变量与环境变量的区别

4.6.2. export 的作用:将本地变量提升为环境变量

4.6.3. 关键困惑:export 是子进程,为什么能修改父进程 bash 的环境变量表?

4.6.4. 补充示例:脚本中的本地变量 vs 子进程


一、基本概念

  • 环境变量 (environment variables) 一般是指在操作系统中用来指定操作系统运行环境的一些参数

  • 如:我们在编写 C/C++ 代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。

  • 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性


二、讲解命令行参数

2.1. main 函数并不是程序的入口函数

        在 main 函数,生成完可执行文件之后,输入 ./xxx 的时候,其实是可以加入一些参数的,其中 int main() 并不是全部的,() 里面还有参数,全称应该是这样写 int main(int argc,char* argv[]),这样一看 main 函数也是一个调用的函数,在程序运行的时候,最先执行的是 start函数,然后才是 main 函数。这个可以使用 反汇编或者是 gdb 动态调试出来,我们在这使用  gdb 动态调试,反汇编的命令也写出来,大家可以看看:

objdump -S myprocess > a.txt

gdb 动态调试:

g++ -g  -o myprocess myprocess.cc
gdb ./myprocess
(gdb) break main
(gdb) run
(gdb) set backtrace past-main on
(gdb) set backtrace past-entry on
(gdb) bt

        通过上面的调试信息内容,我可能可以得出结论:main函数不是程序的入口点,ELF 文件的入口点是 _start。_start 函数负责调用 C 运行时库的 __libc_start_main ,该函数在完成初始化后,最终调用用户的 main 函数。

2.2. 真实的 main 函数样子 --- main(int argc,char* argv[])

2.2.1. 简单代码样式

#include <iostream>
#include <cstdio>
#include <unistd.h>
using namespace std;

int main(int argc, char *argv[])
{
    for (int i = 0; i < argc; i++)
    {
        cout << argv[i] << endl;
    }
    return 0;
}

我们输入这个命令,看看 打印输出的结果是什么:

./myprocess a b c

其中 a b c,bash 会自动进行切分。

此时就有疑问,需要这些命令行参数干什么?

2.2.2. 命令行参数的作用

        用于实现程序不同子功能的方法

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <cstring>
using namespace std;

int main(int argc, char *argv[])
{
    if (argc < 2)
    {
        printf("Usage: %s [-a] [-b] [-c] ...\n", argv[0]);
        return 0;
    }
    // 选项标志(可改为函数指针数组,但这里直接执行)
    for (int i = 1; i < argc; i++)
    {
        const char *arg = argv[i];
        if (strcmp(arg, "-a") == 0)
        {
            printf("执行功能1\n");
        }
        else if (strcmp(arg, "-b") == 0)
        {
            printf("执行功能2\n");
        }
        else if (strcmp(arg, "-c") == 0)
        {
            printf("执行功能3\n");
        }
        else
        {
            printf("%s: 未知选项 %s\n", argv[0], arg);
        }
    }
    return 0;
}

此时输入这个命令,看看效果:

./myprocess -a -b -d

此时我们就知道了,_start 大体上是怎么进行使用的, 下面是一个简单的伪代码:

_start
{
    int ret = 0;
    int arg_count = 0;
    arg_count = 3; //得到的参数个数
    if (arg_count == 0)
    {
    ret = main();
    }
    else if (arg_count == 2)
    {
    ret = main(argc, argv);
    }
    else
    {
    ret = main(argc, argv, env);
    }
}

伪代码中的 if/else 分支,对应了 C/C++ 标准及 GNU 扩展所支持的 main 函数的三种有效签名

分支条件 对应 main 签名 说明arg_count 
arg_count  == 0 int main(void) 最简形式,不接受任何参数(实际上在 Linux 中极少出现)。
arg_count == 2 int main(int argc,char* argv[] ) 标准形式,接收命令行参数个数和字符串数组。
else (其他情况) int main(int argc,char* argv[] ,char*envp[] ) GNU 扩展形式,额外接收环境变量数组。

        因此我们得知:启动代码会根据实际情况,自动匹配你的 main 函数签名。如果你的 main 只写 void ,它就走第一个分支;如果你写 argc / argv,就走第二个。

2.2.3. 命令行参数表


三、引出环境变量

问题  - 为什么系统的命令 ls /pwd / mkdir 不用加 ./ 而我们自己写的 code 需要 ./code

我们需要知道对应 命令的 位置

输入以下命令:

which ls
which mkdir

        我们会发现,他们都存储在 /usr/bin 路径下,如果我们把 自己写的可执行文件也放入到 该路径下,是不是就可以不加入 ./ 前缀了,我们试一下:

sudo cp myprocess /usr/bin/
myprocess -a -b -d 

此时我们才得到今天的主题:这个就是 环境变量!


四、环境变量的相关知识

4.1. 常见的环境变量

  • PATH:指定命令的搜索路径
  • HOME:指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
  • SHELL:当前Shell,它的值通常是/bin/bash。

4.2. 查看环境变量

4.2.1. 查看所有的环境变量

env

里面的内容蛮多的。

4.2.2. echo $NAME

echo $NAME //NAME:你的环境变量名称

例如:echo $SHELL

测试HOME

1. 用root和普通用户,分别执行 echo $HOME,对比差异

2. 执行 cd ~; pwd,对应 ~ 和 HOME 的关系

我们会发现,其实 $HOME  就是 ~ ,$HOME  和 ~  只是同一个路径的两种表达方式(一个变量,一个快捷键)

4.2.3 如何直接运行可执行程序

有两种方法:

  • 把可执行程序放到到 /usr/bin/ (不推荐)
  • 把可执行程序所在的路径,放入到 PATH 中

我们现在使用第二个方法进行:

输入以下命令:

echo $PATH
PATH=/home/xxhh/review/linux
myprocess -a -b -c

但是此时我们执行下面的命令,就会发现一些问题以及问题的根源

ls
pwd
echo $PATH

  1. ls 命令找不到了
  2. pwd 可以找到,因为他是个 内联函数,以后再说
  3. 原因是:PAHT 被覆盖了

这个改变是内存级的,不用担心,把终端关闭,再打开一个新的,就恢复到原来的样子了。

  • 环境变量是“内存级”的:你在终端里用 export  或 PATH = xxx 修的改,只改当前 Shell 进程的内存。重启电脑或打开新终端,改动就没了(恢复成系统默认)。

  • 变量从配置文件来:每次登录时,Bash 进程会去硬盘上读取固定的配置文件(比如 ~/.profile 、 ~/.bashrc),用文件里的内容重新构建内存里的环境变量。这也是“重启后自动恢复默认”的原因。

  • 执行顺序:登录时 先执行 ~/.profile,在执行 ~/.profile 的过程中,又调用了 ~/.bashrc。

 Linux 系统默认的用户配置文件 ~/.profile

if [ -n "$BASH_VERSION" ]; then
    # include .bashrc if it exists
    if [ -f "$HOME/.bashrc" ]; then
        . "$HOME/.bashrc"
    fi
fi

# set PATH so it includes user's private bin if it exists
if [ -d "$HOME/bin" ] ; then
    PATH="$HOME/bin:$PATH"
fi

# set PATH so it includes user's private bin if it exists
if [ -d "$HOME/.local/bin" ] ; then
    PATH="$HOME/.local/bin:$PATH"
fi

如何解决这个问题呢,输入以下的命令即可:

echo $PATH
PATH=$PATH:/home/xxhh/review/linux
echo $PATH
ls
pwd
myprocess -a -b -c

        登录的时候,bash进程就会被创建,就会读取 .bashrc 和 .profile 里面的内容,构建出环境变量  PATH的本质是,系统内搜索指定命令的一个执行命令环境变量值 。bash 会有两个表,一个是环境变量表,一个是命令行参数表,内部都是字符串。

4.3. 认识更多的环境变量

变量 值示例 解释
SHELL /bin/bash shell 脚本的位置(当前使用的 Shell 解释器路径)
HISTSIZE 1000 最多可以存放历史命令的最大个数
PWD /home/xxhh/review/linux 当前工作目录(pwd 命令显示的内容)
LOGNAME xxhh 用户登录名
HOME /home/xxhh 家目录
USER xxhh 用户名(通常与 LOGNAME 相同)
PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:...... 命令搜索路径(系统查找可执行程序的目录列表)
OLDPWD /home/xxhh 上一个路径的地址(执行 cd 之前所在的目录)

4.4. 相关命令行和系统接口

4.4.1. 相关命令行

  • echo: 显示某个环境变量值  
  • export: 设置一个新的环境变量  
  • env: 显示所有环境变量  
  • unset: 清除环境变量  
  • set: 显示本地定义的shell变量和环境变量

输入以下命令:

export myEnv=1122
echo $myEnv

4.4.2. 代码

1. 利用参数列表,来获取环境变量

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <cstring>
using namespace std;

int main(int argc, char *argv[], char *env[])
{
    (void)argc;
    (void)argv;
    for (int i = 0; env[i]; i++)
    {
        cout << "env[" << i << "]-> " << env[i] << endl;
    }
    return 0;
}

其中,我们自己写的代码,编译成可执行程序,执行后,他的父进程是 bash,有一个命令行参数表和环境变量表。bash 为了给子进程做命令行解析,会把命令行解析传递给我们,同时在登陆的时候,就已经有了环境变量表,也会把环境变量表传递给子进程。

2. getenv

DESCRIPTION
       The  getenv() function searches the environment list to find the environment variable name, and returns a pointer to the  corresponding value string.

描述
        getenv() 函数在环境列表中搜索名为 name 的环境变量,并返回指向对应值字符串的指针。

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
using namespace std;

int main(int argc, char *argv[], char *env[])
{
    (void)argc;
    (void)argv;
    (void)env;
    char *val = getenv("PATH");
    if (val == nullptr)
    {
        return -1;
    }
    else
    {
        cout << "PATH = " << val << endl;
    }
    return 0;
}

写一个程序,满足某个功能只能让特定的人使用,其他用户不行。

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
using namespace std;

int main(int argc, char *argv[], char *env[])
{
    (void)argc;
    (void)argv;
    (void)env;
    const char *who = getenv("USER");
    if (who == NULL)
        return 1;
    if (strcmp(who, "xxhh") == 0)
    {
        printf("用户名: %s, 该功能只能xxhh用户使用\n", who);
    }
    else
    {
        printf("该用户%s不能使用这个功能\n", who);
    }
    return 0;
}

3. environ

  • environ 本身是一个二级指针,它指向一个以 NULL 结尾的字符指针数组

  • 数组中的每个元素都是格式为 "变量名 = 值" 的字符串

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
using namespace std;

int main(int argc, char *argv[], char *env[])
{
    (void)argc;
    (void)argv;
    (void)env;
    // 注意:不需要接收 env 参数
    for (int i = 0; environ[i] != NULL; i++)
    {
        printf("env[%d]: %s\n", i, environ[i]);
    }
    return 0;
}

4.5. 环境变量通常是具有全局属性的

        通常是可以被子进程进行继承的

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
using namespace std;

extern char **environ; // 必须加入声明
int main(int argc, char *argv[])
{
    if (fork() == 0)
    {
        printf("子进程开始打印 env\n");
        for (int i = 0; environ[i]; i++)
        {
            printf("environment[%d] ->%s\n", i, environ[i]);
        }
    }
    printf("父进程睡眠3s\n");
    sleep(3);
    return 0;
}

4.6. 一些其他问题

4.6.1. 本地变量与环境变量的区别

做下面的一组实验:

输入以下命令:

i=11
echo $i
set | grep i=
env | grep i=

  • 本地变量(Local Variable):只在当前 Shell 进程中有效,不会被 fork() 创建的子进程继承。
    示例中 i=11 就是本地变量,用 env | grep i= 查不到,因为它不在环境变量表里。

  • 环境变量(Environment Variable):会被子进程继承,存在于环境变量表中(可通过 env 查看)。

4.6.2. export 的作用:将本地变量提升为环境变量

如果想将本地变量提升为环境变量,需要输入以下命令:

export i
env | grep i=

  • export i 把本地变量 i 导入到当前 Shell 的环境变量表中,之后子进程就能看到它了。
    验证:env | grep i= 此时能输出 i=11。

4.6.3. 关键困惑:export 是子进程,为什么能修改父进程 bash 的环境变量表?

核心答案:export 是 Shell 的内建命令,不会创建子进程,由 bash 自己执行。

  • 像 ls、myprocess 这种外部命令,会通过 fork() + exec() 创建子进程来运行,父子进程内存独立,子进程无法修改父进程的环境变量表。

  • 但 export、cd、echo 等是 bash 自己内部实现的,由 bash 进程自身直接调用函数或系统调用,不产生子进程,所以它可以修改 bash 自己的环境变量表。

4.6.4. 补充示例:脚本中的本地变量 vs 子进程

现在有个问题:

当我们输入 export i 的时候,这个命令是子进程,父进程是 bash,bash 的环境变量表 子进程可以使用,但是 子进程的输入 export i 这个 i 变量可以给 bash,并放到环境变量表中,进程之间不是具有独立性?

  • 脚本 test.sh 内容:

    #!/bin/bash
    touch file
    mv file myfile
    i=100
    echo $i
  • 执行 bash test.sh 时,会创建一个子进程(新的 bash 解释器)来运行脚本。

  • 脚本内定义的 i=100 是该子进程的本地变量,脚本执行完毕后子进程退出,不影响父 bash 的环境变量表。

  • 所以执行完脚本后,ls 看到文件操作(touch、mv)生效了,但变量 i 不会留在父 bash 中。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐