关于父进程中的环境变量无法传递给子进程

引言

        今天在写shell脚本的时候,遇到一个棘手的问题,是关于环境变量,当时父进程的环境变量是无论如何也无法传递给子进程,重重排查,才找到问题所在,所以心血来潮,想写一下关于Linux下的环境变量具有全局属性的这个问题。

        之前写过一个shell脚本,今天以这个脚本为例,感兴趣的可以去看一下命令行解释器,另外,关于环境变量,建议也看一下Linux·下环境变量

解释一下下面我们具体在做什么:实验我们自己编写的shell脚本,是否可以设置环境变量?环境变量是否具有全局属性?

  • 首先我们自己编写了一个简单的shell脚本 --- 命令行解释器  (myshell.c)
  • 其次,现在我们想设置一个环境变量(注:环境变量具有全局属性,这意味着任何的程序都能调用)
  • 具体操作是往shell脚本中添加 export 指令,使得我们在命令行下,可以设置环境变量
  • 并且运行其他程序使用这个环境变量  (test.c)

                                                                                          myshell.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

//保存完整的命令行字符串 -- 充当缓冲区
#define NUM 1024
char cmd_line[NUM];
 
//保存切割之后的字符串
#define SIZE 32
char* g_argv[SIZE];
#define SEP " "
 
int main()
{
    //0.命令行解释器,一定是一个常驻内存的进程 --- 不退出(死循环)
    while(1)
    {
        //1打印出提示信息 -- [wxq@VM-4-9-centos shell]$ 
        printf("[dwr@VM-1-1-test shell]$ ");
        fflush(stdout);
       
        //2.获取用户的键盘输入[输入的是各种指令和选项"ls -a -l"]
        memset(cmd_line, 0, sizeof cmd_line);
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
        {
            continue;
        }
        //输入的回车键设为\0  ls -a -l \n \0
        cmd_line[strlen(cmd_line)-1]  = '\0';
 
       // printf("echo:%s\n", cmd_line);   //debug
        
        //3.命令行字符串进行解析 "ls -a -l -s"  --->  "la" "-a" "-l" "-s"
        g_argv[0] = strtok(cmd_line, SEP); //第一次调用,传入原始的字符串

        int index = 1;
        while(g_argv[index++] = strtok(NULL, SEP)); //第二次调用,如果还要分割原始字符串,传入NULL
        
        //简单配置ls的颜色
        int i = 1;
        if(strcmp(g_argv[0], "ls") == 0)
        {
            g_argv[i++] = "--color=auto";
        }
       
        //识别别名 - 主要是测试,一般是有接口的
        if(strcmp(g_argv[0], "ll") == 0)
        {
            g_argv[0] = "ls";
            g_argv[i++] = "-l";
            g_argv[i++] = "--color=auto";
        }
 
        
        //4.TODO 内置命令:让父进程(shell)自己执行的命令。我们叫做内置命令,内建命令
        //内建命令本质就是shell中的一个函数调用
        if(strcmp(g_argv[0], "cd") == 0) //not child execute; father execute;
        {
            if(g_argv[1] != NULL)
            {
                chdir(g_argv[1]); //cd  cd ..
            }
 
            continue;
        
         }
 
        //5.创建子进程进行程序替换
        pid_t id = fork();
 
        //child
        if(id == 0)
        {
           execvp(g_argv[0],g_argv);
           exit(-1);
        }
 
        //father
        int status = 0;
        pid_t ret = waitpid(id, &status, 0); //阻塞等待
        if(ret > 0)
        {
            printf("exit code:%d\n",WEXITSTATUS(status));
        }
 
    }
 
    return 0;
}

                                                                                               test.c

[wxq@VM-4-9-centos shell2]$ touch test.c
[wxq@VM-4-9-centos shell2]$ vim test.c 
[wxq@VM-4-9-centos shell2]$ cat test.c
#include <stdio.h>
#include <stdlib.h>

int main()
{
    printf("hello test!\n");
    printf("myenv: %s\n", getenv("MYVAL"));

    return 0;
}
[wxq@VM-4-9-centos shell2]$ gcc -o test test.c 
[wxq@VM-4-9-centos shell2]$ ./test 
hello test!
myenv: (null)
[wxq@VM-4-9-centos shell2]$ 

目前 MYVAL 这个环境变量我们是未定义的,所以这里需要在脚本内添加指令export的逻辑。


正文开始:

此时传入的环境变量为空,现在我们切换到shell脚本中

[wxq@VM-4-9-centos shell2]$ ./myshell 
[dwr@VM-1-1-test shell]$ ./test      
hello test!
myenv: (null)
exit code:0
[dwr@VM-1-1-test shell]$ 

运行发现,这里也是空,因为我们目前还没有定义这个环境变量,现在我们增加指令export的逻辑。

逻辑:"export MAVAL=1024"  

按照我们shell脚本的逻辑,这一行代码会被进行切割 "export"  "MAVAL=1024"  这样的两个子串

同时我们设置了两个缓冲区,一个存放完整的字符串cmd_line[ ], 一个存放切割后的子串g_argv[ ]

如果g_argv[0] == export 就意味着用户在命令行输入了这个指令,此时,我们需要把后面的子串g_argv[1]设置成环境变量(需要调用一个接口 putenv:更改或添加环境变量)

实现如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

//保存完整的命令行字符串 -- 充当缓冲区
#define NUM 1024
char cmd_line[NUM];
 
//保存切割之后的字符串
#define SIZE 32
char* g_argv[SIZE];
#define SEP " "
 
int main()
{
    //0.命令行解释器,一定是一个常驻内存的进程 --- 不退出(死循环)
    while(1)
    {
        //1打印出提示信息 -- [wxq@VM-4-9-centos shell]$ 
        printf("[dwr@VM-1-1-test shell]$ ");
        fflush(stdout);
       
        //2.获取用户的键盘输入[输入的是各种指令和选项"ls -a -l"]
        memset(cmd_line, 0, sizeof cmd_line);
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
        {
            continue;
        }
        //输入的回车键设为\0  ls -a -l \n \0
        cmd_line[strlen(cmd_line)-1]  = '\0';
 
       // printf("echo:%s\n", cmd_line);   //debug
        
        //3.命令行字符串进行解析 "ls -a -l -s"  --->  "la" "-a" "-l" "-s"
        g_argv[0] = strtok(cmd_line, SEP); //第一次调用,传入原始的字符串
        
        //export MYVAL=1024
        if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
        {
            putenv(g_argv[1]);
            continue;
        }

        int index = 1;
        while(g_argv[index++] = strtok(NULL, SEP)); //第二次调用,如果还要分割原始字符串,传入NULL
        
        //简单配置ls的颜色
        int i = 1;
        if(strcmp(g_argv[0], "ls") == 0)
        {
            g_argv[i++] = "--color=auto";
        }
       
        //识别别名 - 主要是测试,一般是有接口的
        if(strcmp(g_argv[0], "ll") == 0)
        {
            g_argv[0] = "ls";
            g_argv[i++] = "-l";
            g_argv[i++] = "--color=auto";
        }
 
        
        //4.TODO 内置命令:让父进程(shell)自己执行的命令。我们叫做内置命令,内建命令
        //内建命令本质就是shell中的一个函数调用
        if(strcmp(g_argv[0], "cd") == 0) //not child execute; father execute;
        {
            if(g_argv[1] != NULL)
            {
                chdir(g_argv[1]); //cd  cd ..
            }
 
            continue;
        
         }
 
        //5.创建子进程进行程序替换
        pid_t id = fork();
 
        //child
        if(id == 0)
        {
           execvp(g_argv[0],g_argv);
           exit(-1);
        }
 
        //father
        int status = 0;
        pid_t ret = waitpid(id, &status, 0); //阻塞等待
        if(ret > 0)
        {
            printf("exit code:%d\n",WEXITSTATUS(status));
        }
 
    }
 
    return 0;
}

此时我们进入shell脚本当中,尝试设置环境变量,但是会发现结果不对劲,如下

报错1:

[wxq@VM-4-9-centos shell2]$ ./myshell 
[dwr@VM-1-1-test shell]$ export MYVAL=1024
exit code:255
[dwr@VM-1-1-test shell]$ 

分析了一下,发现出现这样的报错,是因为代码的逻辑不对:如下

        这里是没有进入if语句的,因为第二次切割子串在 if 语句之后, g_argv[1] 为空,条件不成立,未进入语句,继续执行下面的代码,创建子进程进行程序替换,但是替换时export这样的命令是不存在的(我们编写的shell脚本内),所以出现了报错 。而我们期望的是这里设置环境变量后会continue跳出while循环,进入下一次的循环。

那我们修改一下顺序,继续尝试:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
//命令行解释器
//shell 运行原理:通过让子进程执行命令,父进程等待&&解析命令

//保存完整的命令行字符串 -- 充当缓冲区
#define NUM 1024
char cmd_line[NUM];

//保存切割之后的字符串
#define SIZE 32
char* g_argv[SIZE];

#define SEP " "

int main()
{
    //0.命令行解释器,一定是一个常驻内存的进程 --- 不退出(死循环)
    while(1)
    {
        //1打印出提示信息 -- [wxq@VM-4-9-centos shell]$ 
        printf("[dwr@VM-1-1-test shell]$ ");
        fflush(stdout);
       
        //2.获取用户的键盘输入[输入的是各种指令和选项"ls -a -l"]
        memset(cmd_line, 0, sizeof cmd_line);
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
        {
            continue;
        }
        //输入的回车键设为\0  ls -a -l \n \0
        cmd_line[strlen(cmd_line)-1]  = '\0';

       // printf("echo:%s\n", cmd_line);   //debug
        
        //3.命令行字符串进行解析 "ls -a -l -s"  --->  "la" "-a" "-l" "-s"
        g_argv[0] = strtok(cmd_line, SEP); //第一次调用,传入原始的字符串

        int index = 1;
        while(g_argv[index++] = strtok(NULL, SEP)); //第二次调用,如果还要分割原始字符串,传入NULL

        //export MYVAL=1024
        if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
        {
            putenv(g_argv[1]);
            continue;
        }

        //简单配置ls的颜色
        int i = 1;
        if(strcmp(g_argv[0], "ls") == 0)
        {
            g_argv[i++] = "--color=auto";
        }
       
        //识别别名 - 主要是测试,一般是有接口的
        if(strcmp(g_argv[0], "ll") == 0)
        {
            g_argv[0] = "ls";
            g_argv[i++] = "-l";
            g_argv[i++] = "--color=auto";
        }

        
        //4.TODO 内置命令:让父进程(shell)自己执行的命令。我们叫做内置命令,内建命令
        //内建命令本质就是shell中的一个函数调用
        if(strcmp(g_argv[0], "cd") == 0) //not child execute; father execute;
        {
            if(g_argv[1] != NULL)
            {
                chdir(g_argv[1]); //cd  cd ..
            }

            continue;
        
         }

        //5.创建子进程进行程序替换
        pid_t id = fork();

        //child
        if(id == 0)
        {
           execvp(g_argv[0],g_argv);
           exit(-1);
        }

        //father
        int status = 0;
        pid_t ret = waitpid(id, &status, 0); //阻塞等待
        if(ret > 0)
        {
            printf("exit code:%d\n",WEXITSTATUS(status));
        }

    }

    return 0;
}

运行如下:

[wxq@VM-4-9-centos shell2]$ ./myshell 
[dwr@VM-1-1-test shell]$ export MYVAL=1024
[dwr@VM-1-1-test shell]$ ./test
hello test!
myenv: (null)
exit code:0
[dwr@VM-1-1-test shell]$

此时是可以设置环境变量的,但是这里test程序并没有拿到这个环境变量,那是因为什么呢?继续向下分析:

做个测试,获取putenv的返回值,看看是否真的设置了环境变量:

RETURN VALUE
       The putenv() function returns zero on success, or nonzero if an error occurs.  In the event of an error, errno is set to indicate the cause.

putenv()函数在成功时返回零,如果发生错误则返回非零

测试如下:

      //export MYVAL=1024
     if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
     {
         int ret = putenv(g_argv[1]);
         if(ret == 0) printf("%s putenv success \n", g_argv[1]);                                                                                    
         continue;                                                                                                                        
     }  

运行:

[wxq@VM-4-9-centos shell2]$ ./myshell 
[dwr@VM-1-1-test shell]$ export MYVAL=1024
MYVAL=1024 putenv success 
[dwr@VM-1-1-test shell]$ ./test 
hello test!
myenv: (null)
exit code:0
[dwr@VM-1-1-test shell]$ 

这里我们发现,的确是设置成功了环境变量,但是为什么其他程序无法调用呢?继续向下分析:

再次进行测试,这里我们成功设置了环境变量后,手动去传递:声明 extern char** environ  ,然后子进程手动传入进去

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
//命令行解释器
//shell 运行原理:通过让子进程执行命令,父进程等待&&解析命令

//保存完整的命令行字符串 -- 充当缓冲区
#define NUM 1024
char cmd_line[NUM];

//保存切割之后的字符串
#define SIZE 32
char* g_argv[SIZE];

#define SEP " "

extern char** environ;

int main()
{
    //0.命令行解释器,一定是一个常驻内存的进程 --- 不退出(死循环)
    while(1)
    {
        //1打印出提示信息 -- [wxq@VM-4-9-centos shell]$ 
        printf("[dwr@VM-1-1-test shell]$ ");
        fflush(stdout);
       
        //2.获取用户的键盘输入[输入的是各种指令和选项"ls -a -l"]
        memset(cmd_line, 0, sizeof cmd_line);
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
        {
            continue;
        }
        //输入的回车键设为\0  ls -a -l \n \0
        cmd_line[strlen(cmd_line)-1]  = '\0';

       // printf("echo:%s\n", cmd_line);   //debug
        
        //3.命令行字符串进行解析 "ls -a -l -s"  --->  "la" "-a" "-l" "-s"
        g_argv[0] = strtok(cmd_line, SEP); //第一次调用,传入原始的字符串

        int index = 1;
        while(g_argv[index++] = strtok(NULL, SEP)); //第二次调用,如果还要分割原始字符串,传入NULL

        //export MYVAL=1024
        if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
        {
            int ret = putenv(g_argv[1]);
            if(ret == 0) printf("%s putenv success \n", g_argv[1]);
            continue;
        }


        //简单配置ls的颜色
        int i = 1;
        if(strcmp(g_argv[0], "ls") == 0)
        {
            g_argv[i++] = "--color=auto";
        }
       
        //识别别名 - 主要是测试,一般是有接口的
        if(strcmp(g_argv[0], "ll") == 0)
        {
            g_argv[0] = "ls";
            g_argv[i++] = "-l";
            g_argv[i++] = "--color=auto";
        }


        //debug
//        for(index = 0 ; g_argv[index]; index++)
//        {
//            printf("g_argv[%d]: %s\n", index, g_argv[index]);
//        }

        
        //4.TODO 内置命令:让父进程(shell)自己执行的命令。我们叫做内置命令,内建命令
        //内建命令本质就是shell中的一个函数调用
        if(strcmp(g_argv[0], "cd") == 0) //not child execute; father execute;
        {
            if(g_argv[1] != NULL)
            {
                chdir(g_argv[1]); //cd  cd ..
            }

            continue;
        
         }

        //5.创建子进程进行程序替换
        pid_t id = fork();

        //child
        if(id == 0)
        {
          // execvp(g_argv[0],g_argv);
          execvpe(g_argv[0], g_argv, environ);
           exit(-1);
        }

        //father
        int status = 0;
        pid_t ret = waitpid(id, &status, 0); //阻塞等待
        if(ret > 0)
        {
            printf("exit code:%d\n",WEXITSTATUS(status));
        }

    }

    return 0;
}

但是这里运行后发现,依旧不行,这里还是为空:

[wxq@VM-4-9-centos shell2]$ vim myshell.c
[wxq@VM-4-9-centos shell2]$ make
gcc -o myshell myshell.c
[wxq@VM-4-9-centos shell2]$ ./myshell 
[dwr@VM-1-1-test shell]$ export MYVAL=1024
MYVAL=1024 putenv success 
[dwr@VM-1-1-test shell]$ ./test
hello test!
myenv: (null)
exit code:0

这时候就有些丈二和尚摸不着头脑,明明设置成功了,为什么传递不行呢???
那就继续进行测试:打印一下父进程的环境变量

    //export MYVAL=1024
    if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
    {                               
       int ret = putenv(g_argv[1]);                           
      if(ret == 0) printf("%s putenv success \n", g_argv[1]);
       
           //debug
      int i = 0;
      for(i = 0; environ[i]; i++)
      {
         printf("environ[%d], %s\n", i, environ[i]);                                                                                            
      }                                                                                                                        
                                                                                                                                    
         continue;                                                                                                                
     } 

运行:

[wxq@VM-4-9-centos shell2]$ make
gcc -o myshell myshell.c
[wxq@VM-4-9-centos shell2]$ ./myshell 
[dwr@VM-1-1-test shell]$ export MYVAL=1024
MYVAL=1024 putenv success 
environ[0], XDG_SESSION_ID=459440
environ[1], HOSTNAME=VM-4-9-centos
environ[2], TERM=xterm
environ[3], SHELL=/bin/bash
environ[4], HISTSIZE=3000
environ[5], SSH_CLIENT=122.193.66.176 16210 22
environ[6], SSH_TTY=/dev/pts/0
environ[7], USER=wxq
environ[8], LD_LIBRARY_PATH=:/home/wxq/.VimForCpp/vim/bundle/YCM.so/el7.x86_64
environ[9], LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=01;05;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=01;36:*.au=01;36:*.flac=01;36:*.mid=01;36:*.midi=01;36:*.mka=01;36:*.mp3=01;36:*.mpc=01;36:*.ogg=01;36:*.ra=01;36:*.wav=01;36:*.axa=01;36:*.oga=01;36:*.spx=01;36:*.xspf=01;36:
environ[10], MAIL=/var/spool/mail/wxq
environ[11], PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/wxq/.local/bin:/home/wxq/bin
environ[12], PWD=/home/wxq/study_4/shell2
environ[13], LANG=en_US.utf8
environ[14], SHLVL=1
environ[15], HOME=/home/wxq
environ[16], LOGNAME=wxq
environ[17], SSH_CONNECTION=122.193.66.176 16210 10.0.4.9 22
environ[18], LESSOPEN=||/usr/bin/lesspipe.sh %s
environ[19], PROMPT_COMMAND=history -a; history -a; printf "\033]0;%s@%s:%s\007" "${USER}" "${HOSTNAME%%.*}" "${PWD/#$HOME/~}"
environ[20], XDG_RUNTIME_DIR=/run/user/1001
environ[21], HISTTIMEFORMAT=%F %T 
environ[22], _=./myshell
environ[23], OLDPWD=/home/wxq/study_4
environ[24], MYVAL=1024
[dwr@VM-1-1-test shell]$ 

此时我们发现,这个环境变量确实已经存在于父进程的上下文当中(最后一行),但是为什么子进程接收不到呢???

对,我们再尝试一下,看看子进程是否接收到了这个环境变量:

 //5.创建子进程进行程序替换                                                                                   
      pid_t id = fork();                                                                                           
                                                                                                                   
      //child process
      if(id == 0)
      {
          printf("child process : MYVAL : %s\n", getenv("MYVAL")); //自定义环境变量

          printf("child process : PATH : %s\n", getenv("PATH"));  //系统环境变量

       // execvp(g_argv[0],g_argv);
       execvpe(g_argv[0], g_argv, environ);
       exit(-1);
     }

运行测试:

[wxq@VM-4-9-centos shell2]$ ./myshell 
[dwr@VM-1-1-test shell]$ export MYVAL=1024
MYVAL=1024 putenv success 
[dwr@VM-1-1-test shell]$ ./test
child process : MYVAL : (null)
child process : PATH : /usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/wxq/.local/bin:/home/wxq/bin
hello test!
myenv: (null)
exit code:0
[dwr@VM-1-1-test shell]$ 

这里,系统的环境变量确实传递给了子进程,但是自定义的环境变量,父进程并没有传递给子进程,为什么???

可是为什么呢?明明这个环境变量存在于父进程的上下文当中啊。


上述问题,其实跟存储环境变量的位置有关:提示,环境变量是一个字符串!

  • 当程序把我们自定义的环境变量添加到系统环境变量当中的时候
  • 并不是把我们自定义的环境变量(字符串)拷贝到那个系统指针数组对应的空间里
  • 而是把这个字符串的地址添加到系统的指针数组当中
  • 另外虽然我们对子串进行了切割,但实际上我们的子串还是存储在cmd_line中
  • 我们所有的字符串参数的地址,都是保留在g_argv内
  • 因为所有的子字符串是写在了cmd_line里面,而每一次进入while循环,那么每一次都会进行覆盖
  • 比如说g_argv[1]它里面指向的位置其实还是cmd_line,因为是从cmd_line中提取出来的
  • 其实是把cmd_line里面的这个地址导入到了环境变量g_argv的表里面,而字符串还依旧在cmd_line内
  • 所以在下一次进入while循环的时候,cmd_line已经被清空了(字符串被覆盖了),但是此时环境变量的地址没变,也就意味着环境变量的地址为空
     

分析这么多,我们来看看到底是不是因为这个问题:

测试:我们再设置一个buffer,拷贝这个字符串

//注意 main函数外声明
char buffer[32];

if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
{
    strcpy(buffer,g_argv[1]); 
    int ret = putenv(buffer);
    if(ret == 0) printf("%s putenv success \n", g_argv[1]);
    continue;
}

接下来就是见证奇迹的时刻:

[wxq@VM-4-9-centos shell2]$ vim myshell.c
[wxq@VM-4-9-centos shell2]$ make
gcc -o myshell myshell.c
[wxq@VM-4-9-centos shell2]$ ./myshell 
[dwr@VM-1-1-test shell]$ export MYVAL=1024
MYVAL=1024 putenv success 
[dwr@VM-1-1-test shell]$ ./test
hello test!
myenv: 1024
exit code:0
[dwr@VM-1-1-test shell]$ 

        因为删除有些复杂,所以在这里并么有实现,但是可以按住键盘上的ctrl+删除,这样也能在脚本下完成删除,另外退出脚本ctrl+c就可以完成。

附上完整测试代码:

                                                                                        myshell.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
//命令行解释器
//shell 运行原理:通过让子进程执行命令,父进程等待&&解析命令

//保存完整的命令行字符串 -- 充当缓冲区
#define NUM 1024
char cmd_line[NUM];

//保存切割之后的字符串
#define SIZE 32
char* g_argv[SIZE];

#define SEP " "

extern char** environ;

char buffer[32];

int main()
{
    //0.命令行解释器,一定是一个常驻内存的进程 --- 不退出(死循环)
    while(1)
    {
        //1打印出提示信息 -- [wxq@VM-4-9-centos shell]$ 
        printf("[dwr@VM-1-1-test shell]$ ");
        fflush(stdout);
       
        //2.获取用户的键盘输入[输入的是各种指令和选项"ls -a -l"]
        memset(cmd_line, 0, sizeof cmd_line);
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
        {
            continue;
        }
        //输入的回车键设为\0  ls -a -l \n \0
        cmd_line[strlen(cmd_line)-1]  = '\0';

       // printf("echo:%s\n", cmd_line);   //debug
        
        //3.命令行字符串进行解析 "ls -a -l -s"  --->  "la" "-a" "-l" "-s"
        g_argv[0] = strtok(cmd_line, SEP); //第一次调用,传入原始的字符串

        int index = 1;
        while(g_argv[index++] = strtok(NULL, SEP)); //第二次调用,如果还要分割原始字符串,传入NULL

        //export MYVAL=1024
        if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
        {
            strcpy(buffer,g_argv[1]);
            int ret = putenv(buffer);
            if(ret == 0) printf("%s putenv success \n", g_argv[1]);
         

            //debug
         //   int i = 0;
         //   for(i = 0; environ[i]; i++)
         //   {
         //       printf("environ[%d], %s\n", i, environ[i]);
         //   }

            continue;
        }


        //简单配置ls的颜色
        int i = 1;
        if(strcmp(g_argv[0], "ls") == 0)
        {
            g_argv[i++] = "--color=auto";
        }
       
        //识别别名 - 主要是测试,一般是有接口的
        if(strcmp(g_argv[0], "ll") == 0)
        {
            g_argv[0] = "ls";
            g_argv[i++] = "-l";
            g_argv[i++] = "--color=auto";
        }


        //debug
//        for(index = 0 ; g_argv[index]; index++)
//        {
//            printf("g_argv[%d]: %s\n", index, g_argv[index]);
//        }

        
        //4.TODO 内置命令:让父进程(shell)自己执行的命令。我们叫做内置命令,内建命令
        //内建命令本质就是shell中的一个函数调用
        if(strcmp(g_argv[0], "cd") == 0) //not child execute; father execute;
        {
            if(g_argv[1] != NULL)
            {
                chdir(g_argv[1]); //cd  cd ..
            }

            continue;
        
         }

        //5.创建子进程进行程序替换
        pid_t id = fork();

        //child process
        if(id == 0)
        {
          //  printf("child process : MYVAL : %s\n", getenv("MYVAL")); //自定义环境变量

          //  printf("child process : PATH : %s\n", getenv("PATH"));  //系统环境变量

          // execvp(g_argv[0],g_argv);
          execvpe(g_argv[0], g_argv, environ);
           exit(-1);
        }

        //father
        int status = 0;
        pid_t ret = waitpid(id, &status, 0); //阻塞等待
        if(ret > 0)
        {
            printf("exit code:%d\n",WEXITSTATUS(status));
        }

    }

    return 0;
}


  • 所以排查这个问题,需要明白
  • 导入环境变量实际上是在指针数组中添加指针的
  • 而我们在命令行添加环境变量的本质,实际上是
  • 调用putenv添加到了当前的父进程(shell的上下文)当中,我们不能手动添加到数组里,必须要使用这样的接口
  • 添加了之后,(其实是调用接口添加到了虚拟地址空间) --- 栈区再往上的空间就是环境变量空间
  • 接下来创建的子进程都会默认的去添加这个环境变量,因为子进程继承了父进程的地址空间,当然也就继承了环境变量
  • 所以环境变量具有全局属性
     

但是这里存在一个问题,是关于:创建子进程,进行程序替换的时候,代码和数据都存在写时拷贝吗???

参考:程序地址空间

进程创建/写实拷贝

这里需要捋顺一个问题:

首先:

父进程创建子进程,如果父进程或者子进程对数据进行写入,那么操作系统就会发生写时拷贝。

此时代码不会进行写时拷贝,代码是不可更改的,因为代码存储在常量区,父进程或者子进程只有读取的权限

(代码共享,数据写时拷贝。父子进程共用虚拟地址空间)

其次:

如果子进程发生替换程序,意味着新程序会从磁盘往内存中加载,这不就是写入吗?

此时,代码会不会发生写实拷贝,答案是肯定,因为要保证进程的独立性,此时,父子的代码必须要分离

(代码,数据写时拷贝,重新给子进程建立虚拟地址空间)

再次:

那么,回到上面的问题,环境变量具有全局属性,但是程序替换不是会替换掉所有的代码和数据吗?

为什么子进程还可以使用父进程的环境变量???

因为环境变量是一个特例,可以理解为特殊情况,在进行程序替换的时候,不会被替换掉

或者可以理解为,在给子进程创建虚拟地址空间的时候,会拷贝一份给子进程。


那就再多说一句吧,为什么定义环境变量在其他的程序中也能使用?

        因为export定义环境变量是会定义给bash,而bash就是shell外壳,也是所有程序(进程)的父进程,所以定义的环境变量就具有了全局属性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值