认识环境变量和进程替换,实现一个简易的shell

news/2024/7/21 12:55:17 标签: c++, 数据结构, linux, ubuntu, centos, vim

文章目录

  • 一、环境变量
    • 1.什么是环境变量
    • 2.环境变量的分类
    • 3.查看环境变量
    • 4.设置环境变量
    • 5.获取环境变量
  • 二、进程控制
    • 1.进程终止
    • 2.进程等待
    • 3.进程替换
  • 三、实现一个简单的shell


一、环境变量

1.什么是环境变量

首先,在百度百科中,环境变量的解释是这样的:

环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数,如:临时文件夹位置和系统文件夹位置等。环境变量是在操作系统中一个具有特定名字的对象,它包含了一个或者多个应用程序所将使用到的信息。例如Windows和DOS操作系统中的path环境变量,当要求系统运行一个程序而没有告诉它程序所在的完整路径时,系统除了在当前目录下面寻找此程序外,还应到path中指定的路径去找。用户通过设置环境变量,来更好的运行进程。

指令本质上就是编译好的程序和脚本,被存储在特定的路径下(默认/user/bin/)。

比如我们执行 ls 指令,实际上就是执行这个程序或者脚本,而我们要执行一个程序就必须找到该程序,我们通常要运行一个可执行程序,是用 ./a.out 来执行的,./表示在当前目录,a.out表示一个可执行程序。所以我们要执行 ls 指令,就要找到 ls 所在的路径,而环境变量的作用就是让系统从指定的路径去找,而在PATH 中有/uer/bin 路径,所以我们就能执行所有的指令了。

在这里插入图片描述

常用的10个环境变量如下:

环境变量名称作用
HOME用户的主目录(也称家目录)
SHELL用户使用的 Shell 解释器名称
PATH定义命令行解释器搜索用户执行命令的路径
EDITOR用户默认的文本解释器
RANDOM生成一个随机数字
LANG系统语言、语系名称
HISTSIZE输出的历史命令记录条数
HISTFILESIZE保存的历史命令记录条数
PS1Bash解释器的提示符
MAIL邮件保存路径

Linux 作为一个多用户多任务的操作系统,能够为每个用户提供独立的、合适的工作运行环境,因此,一个相同的环境变量会因为用户身份的不同而具有不同的值。

2.环境变量的分类

按照变量的生存周期划分,Linux 变量可分为两类:

  • 永久的:需要修改配置文件,变量永久生效。
  • 临时的:使用 export 命令声明即可,变量在关闭 shell 时失效。

按作用的范围分,在 Linux 中的变量,可以分为环境变量和本地变量:

  • 环境变量:相当于全局变量,存在于所有的 Shell 中,具有继承性;
  • 本地变量:相当于局部变量只存在当前 Shell 中,本地变量包含环境变量,非环境变量不具有继承性。

3.查看环境变量

值得一提的是,Linux 系统中环境变量的名称一般都是大写的,这是一种约定俗成的规范。

使用 echo 命令查看单个环境变量,例如:echo $PATH;使用 env 查看当前系统定义的所有环境变量;使用 set 查看所有本地定义的环境变量。查看 PATH 环境的实例如下:

在这里插入图片描述

常用的命令如下:

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

例如我们要设置一个新的环境变量,然后再清除:

在这里插入图片描述

`

4.设置环境变量

在 Linux 中设置环境变量有三种方法:

1.所有用户永久添加环境变量: vi /etc/profile,在 /etc/profile 文件中添加变量。

2.当前用户永久添加环境变量: vi ~/.bashrc,在用户目录下的 ~/.bashrc 文件中添加变量。

3.临时添加环境变量 : 可通过 export 命令,如运行命令export HELLO=100

5.获取环境变量

  • 命令行第三个参数
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
	 int i = 0;
	 for(; env[i]; i++){
	 printf("%s\n", env[i]);
	 }
	 return 0;
}
  • 通过第三方变量environ 获取
#include <stdio.h>
int main(int argc, char *argv[])
{
	 extern char **environ;
	 int i = 0;
	 for(; environ[i]; i++){
	 printf("%s\n", environ[i]);
	 }
	 return 0;
}

lib.c中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在使用时要用extern声明。

  • 通过系统调用获取
#include <stdio.h>
#include <stdlib.h>
int main()
{
 	printf("%s\n", getenv("PATH"));
 	return 0;
}

二、进程控制

1.进程终止

正常退出:

  • 从 main 函数返回
  • 调用exit
  • 调用_exit

异常退出:

  • crtl + c,信号终止

_exit():

 #include <unistd.h>
 void _exit(int status);

DESCRIPTION:

The function _exit() terminates the calling process “immediately”. Any open file descriptors belonging to the process are closed; any children of the process are inherited byprocess 1, init, and the process’s parent is sent a SIGCHLD signal.

_eixt()函数立即终止进程,关闭所有属于该进程的文件描述符,其子进程被1号init 进程领养,然后向父进程发送SIGCHLD 信号。

The value status is returned to the parent process as the process’s exit status, and can be collected using one of the wait(2) family of calls.

值状态作为进程的退出状态返回到父进程,并且可以使用 wait(2) 系列调用之一来收集。

exit():

 #include <unistd.h>
 void exit(int status);
  1. 执行用户通过 atexit或on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入
  3. 调用_exit

return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。

2.进程等待

进程等待的必要性:

  • 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  • 进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

wait():用来等待任何一个子进程退出,由父进程调用。

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);

返回值:
 成功返回被等待进程pid,失败返回-1。
参数:
 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
wait方式:
 阻塞式等待,等待的子进程不退出时,父进程一直不退出;

waitpid():

pid_ t waitpid(pid_t pid, int *status, int options);

参数:
pid:
 Pid=-1,等待任一个子进程。与wait等效。
 Pid>0.等待其进程ID与pid相等的子进程。
 
status:
 WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
 WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
 
options:
	当options参数为0时,与wait功能相同,仍是阻塞式等待,不提供额外功能
	如果为下列常量按位或则提供更多功能:
 WCONTINUED:若实现支持作业控制,那么由pid指定的任一子进程在暂停后已经继续,但状态尚未报告,则返回状态
 WNOHANG:若由pid指定的子进程并不是立即结束,则waitpid不阻塞,即此时以非阻塞方式(轮询式访问的必要条件)等待子进程,并且返回0。若正常结束,则返回该⼦进程的ID。
 WUNTRACED:若实现支持作业控制,而pid指定的任一子进程已经暂停,且其状态尚未报告,则返回其状态。

返回值:
当正常返回的时候,waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD;

获取子进程status:

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
    否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下(只研究status低16比特位):
    在这里插入图片描述

我们可以通过status & 0x7f来判断异常信号是否为0;若为0,则正常退出,然后可以通过(status >> 8) & 0xff来获取子进程返回值。sys/wait.h中提供了一些宏来简化这些操作:

if (WIFEXITED(status)) {
    // 正常退出:((status) & 0x7f) == 0
    // 打印退出码:(status >> 8) & 0xff
    printf("child return: %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
    // 异常退出:((signed char) (((status) & 0x7f) + 1) >> 1) > 0
    // 打印异常信号值:(status) & 0x7f
    printf("child signal: %d\n", WTERMSIG(status));
}

我们用这段代码来读取子进程的status,以获取它的退出码和异常信号值,以及父进程收到的SIGCHLD信号:

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


void catchsig(int sig)
{
    printf("catch a sig : %d , pid->%d\n",sig,getpid());
}

int main()
{
    signal(SIGCHLD,catchsig);
    pid_t id = fork();

    if(id<0)
    {
        printf("fork error!\n");
        exit(-1);
    }
    else if(id==0)
    {
        int cnt = 3;
        while(cnt)
        {
            printf("this is a child process! id-->%d   pid-->%d  ppid--> %d\n",cnt--,getpid(),getppid());
        }
        //exit(-1);
       int i =  10/0;
    }

    sleep(2);
    int status = 0;
    pid_t ret = waitpid(id,&status,0);
    if(id>0)
        //0-7:终止信号 15-8:退出状态
        printf("wait success:%d , sign number:%d , child exit code:%d\n",ret,(status & 0x7F),(status>>8 & 0xFF));

    return 0;
}

运行结果如下:

在这里插入图片描述
 其实,⼦进程在终⽌时会给⽗进程发SIGCHLD信号,该信号的默认处理动作是忽略,⽗进程可以⾃定义SIGCHLD信号的处理函数,这样⽗进程只需专⼼处理⾃⼰的⼯作,不必关⼼⼦进程了,⼦进程终⽌时会通知⽗进程,⽗进程在信号处理函数中调⽤wait清理⼦进程即可。一般情况下父进程收到这个信号的默认处理是忽略这个信号,即就是不做任何处理。

3.进程替换

替换原理:

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

替换函数:

#include <unistd.h>

       extern char **environ;

       int execl(const char *path, const char *arg, ...);
       int execlp(const char *file, const char *arg, ...);
       int execle(const char *path, const char *arg, ... , char * const envp[]);
       int execv(const char *path, char *const argv[]);
       int execvp(const char *file, char *const argv[]);
       int execvpe(const char *file, char *const argv[], *const envp[]);
       
	这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
	如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。

命名理解:

l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量

int execl(const char *path, const char *arg, ...);

其中*path表示的是路径,arg表示的是要执行的程序,“…”表示的就是可变参数列表,即命令行上怎么执行这里就写入什么参数。必须以NULL作为参数列表的结束。

#include<unistd.h>    
#include<stdio.h>    
#include<sys/wait.h>    
#include<stdlib.h>    
int main()    
{    
  if(fork()==0)    
  {    
    printf("command begin\n");    
    execl("/usr/bin/ls","ls","-a","-l",NULL);                                                                                                            
    printf("command fail\n");    
    exit(1);    
  }    
  waitpid(-1,NULL,0);    
  printf("wait child success\n");    
  return 0;    
}    

当子进程执行完打印command begin的语句的时候,进行进程的替换。其中替换的是/usr/bin/ls,在命令行要输入的是ls -a -l,将程序运行起来:

在这里插入图片描述

下面是这些函数的用法:

#include <unistd.h>
int main()
{
 	char *const argv[] = {"ps", "-ef", NULL};
 	char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
 	
 	execl("/bin/ps", "ps", "-ef", NULL);
 	
 // 带p的,可以使用环境变量PATH,无需写全路径
 	execlp("ps", "ps", "-ef", NULL);
 	
 // 带e的,需要自己组装环境变量
 	execle("ps", "ps", "-ef", NULL, envp);
 	
 	execv("/bin/ps", argv);
 
 // 带p的,可以使用环境变量PATH,无需写全路径
 	execvp("ps", argv);
 	
 // 带e的,需要自己组装环境变量
 	execve("/bin/ps", argv, envp);
 	
 	exit(0);
}

操作系统实际上只提供了一个接口那就是:execve,其他的函数都是对该接口封装而成的库函数。它们的底层都是使用execve来进行实现的。

三、实现一个简单的shell

以下代码利用程序替换实现了shell 的基本功能,包括重定向,退出码等功能,后续还可以再补充。

#include<stdio.h>
#include<ctype.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<assert.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<string.h>
#define NUM 1024//缓冲区大小
#define COM_NUM 64//存放指令字符串的最大值
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUPUT_REDIR 2
#define APPEND_REDIR  4
char lineCommand[NUM];//输入缓冲区
char *myargv[COM_NUM];//存放一个个的指令字符串
int exit_code=0;//退出码
int exit_sign=0;//退出信号值
int redirType = NONE_REDIR;//读方式
char* myFile = NULL;

//跳过空格
#define trimSpace(start) do{while(isspace(*start)) ++start;}while(0)


void commandCheck(char* commands)
{
    assert(commands);
    char *start = commands;
    char *end = commands+strlen(commands);

    while(start<end)
    {
        if(*start == '>')
        {
            *start = '\0';
            start++;
            if(*start == '>') // >> 表示追加重定向
            {
                start++;
                redirType = APPEND_REDIR;
            }
            else // > 表示输出重定向 
            {
                
                redirType = OUPUT_REDIR;
            }

            trimSpace(start);
            myFile = start;
            break;
        }
        else if(*start == '<') // < 表示输入重定向
        {
            *start = '\0';
            start++;
            redirType = INPUT_REDIR;
            trimSpace(start);
            myFile = start;
            break;
        }
        else 
            start++;
    }
}

//最新添加重定向功能!!!!!
int main()
{
    while(1)
    {
        redirType = NONE_REDIR;
        myFile = NULL;

        printf("[wml @ my_bash path#]");
        fflush(stdout);
        
        //获取输入行内容
        char *s = fgets(lineCommand,sizeof(lineCommand)-1,stdin);
        assert(s != NULL);
        (void)s;

        lineCommand[strlen(lineCommand)-1] = 0;

        //重定向:
        //"ls -a -l > "test.txt""  ---->  "ls -a -l"  >  "test.txt"
        //"ls -a -l >> "test.txt""  ---->  "ls -a -l"  >>  "test.txt"
        //"cat < "test.txt""  ---->  "cat"  <  "test.txt"
    
        commandCheck(lineCommand);

        //切割字符串
        myargv[0] = strtok(lineCommand," ");
        int i = 1;

        //添加颜色选项
        if(myargv[0]!= NULL && strcmp(myargv[0],"ls") == 0  )
        {
            myargv[i++]  = (char*) "--color=auto";
        }

        //读取每个选项
        while(myargv[i++] = strtok(NULL," "));
        
        //解决工作路径无法改变的问题
        if(myargv[0]!=NULL && strcmp(myargv[0],"cd") == 0)
        {
            if(myargv[1]!=NULL) 
                chdir(myargv[1]);
            continue;
        }

        //设置退出码
        if(myargv[0]!=NULL && myargv[1]!=NULL && strcmp(myargv[0],"echo")==0 )
        {
            if(strcmp(myargv[1],"$?")==0)
                printf("exit_code-->%d | exit_sign-->%d\n",exit_code,exit_sign);
            else 
                printf("%s\n",myargv[1]);
            continue;
        }

#ifdef DEBUG 
        for(int i=0;myargv[i];i++)
            printf("myargv[%d]:%s\n",i,myargv[i]);
#endif

        pid_t id = fork();
        assert(id!=-1);

        if(id==0)
        {
            switch(redirType)
            {
                case NONE_REDIR:
                    break;
                case INPUT_REDIR:
                {
                    int fd  = open(myFile,O_RDONLY);
                    if(fd<0) {perror("open");return 1;}
                    dup2(fd,0);
                }
                     break;
                case OUPUT_REDIR:
                case APPEND_REDIR:
                {
                    umask(0);
                    int flag = O_WRONLY | O_CREAT;
                    if(redirType == APPEND_REDIR) flag |= O_APPEND;
                    else flag |= O_TRUNC;

                    int fd  = open(myFile,flag,0666);
                    if(fd<0){perror("open");return 2;};
                    dup2(fd,1);
                }
                    break;
                default:
                    printf("bug!\n");
                    break;

            }
            execvp(myargv[0],myargv);
           _exit(1);
        }

        int status=0;
        pid_t ret = waitpid(id,&status,0);
        assert(ret!=-1);
        (void)ret;

        exit_sign = (status & 0x7f);
        exit_code = (status>>8) & 0xff;


    }
    return 0;
}


http://www.niftyadmin.cn/n/5119186.html

相关文章

分库分表-ShardingSphere 4.x(1)

❤️作者简介&#xff1a;2022新星计划第三季云原生与云计算赛道Top5&#x1f3c5;、华为云享专家&#x1f3c5;、云原生领域潜力新星&#x1f3c5; &#x1f49b;博客首页&#xff1a;C站个人主页&#x1f31e; &#x1f497;作者目的&#xff1a;如有错误请指正&#xff0c;将…

【Docker】Docker-Compose内置DNS负载均衡失效问题

Docker Compose实现负载均衡 还是对前面的例子docker-compose.yml稍微修改&#xff1a; version: "3.8"services:flask-demo:build:context: .dockerfile: Dockerfileimage: flask-demo:latestenvironment:- REDIS_HOSTredis-server- REDIS_PASS${REDIS_PASS}healt…

[数据分析与可视化] 基于Python绘制简单动图

动画是一种高效的可视化工具&#xff0c;能够提升用户的吸引力和视觉体验&#xff0c;有助于以富有意义的方式呈现数据可视化。本文的主要介绍在Python中两种简单制作动图的方法。其中一种方法是使用matplotlib的Animations模块绘制动图&#xff0c;另一种方法是基于Pillow生成…

Plooks大型视频在线一起看网站源码

在前段时间&#xff0c;因为想和异地的朋友一起看电影&#xff0c;但是发现有电影的地方没有一起看功能&#xff0c;有一起看功能的视频网站没有电影&#xff0c;所以就想自己做一个一起看网站&#xff0c;于是就有了Plooks。 Plooks是一个完整的视频网站&#xff0c;其中包括…

03142《互联⽹及其应⽤》各章简答题解答(课后习题)

03142《互联⽹及其应⽤》各章简答题解答&#xff08;课后习题&#xff09; *第* *1* *章* *名词解释* 互联网网络&#xff08;Internet&#xff09; 互联网是建立在一组共同协议之上的网络设备和线路的物理集合&#xff0c;是一组可共享的资源集。&#xff08;1 分&#xf…

xcode The document “...“ could not be saved

Today when I tried to save a file on my project I get an error message saying: The document “nameOfFile.m” could not be saved. I tried reinstalling xcode but no luck. The file can be edited with other editors and I see the same behavior on all my project…

如何在oracle中查询所有用户表的表名、主键名称、索引、外键等

1、查找表的所有索引&#xff08;包括索引名&#xff0c;类型&#xff0c;构成列&#xff09;&#xff1a; select t.*,i.index_type from user_ind_columns t,user_indexes i where t.index_name i.index_name and t.table_name i.table_name and t.table_name 要查询的表…

Typecho 添加 Emoji 表情报错「解决方案」

Typecho 添加 Emoji 表情报错 文章目录 Typecho 添加 Emoji 表情报错前言Emoji 表情utf8mb4 与 UTF8 解决方案[1] 数据库编码更改[2] 数据库配置文件更改 前言 Typecho 添加 Emoji 表情不支持&#xff0c;报错 Database Query Error Emoji 表情 Emoji 就是表情符号&#xff0c…