【Linux系统化学习】自定义简易shell

news2024/9/20 6:00:49

目录

环境变量在进程替换中的继承

在当前进程中添加环境变量

putenv函数

环境变量被继承的原因

使用ecexle传递环境变量

 传递自己的环境变量表

自定义简易的shell

获取主机、使用者、工作目录

获取命令

切割分解命令

创建子进程执行命令

内建命令的特殊处理

完整代码


guan

环境变量在进程替换中的继承

上片文章我们提到可以使用exec系列函数进行进程的替换。但是上篇文章中还有一个包含环境变量参数的exec系列的函数没有介绍;首先我们先验证下进程替换后环境变量也会继承父进程。

我们使用一个C++程序打印环境变量;在另一个程序中的子进程中使用execl替换该程序。

 1 #include <iostream>
    2 #include <unistd.h>
W>  3 int main(int argc, char *argv[], char *env[])
    4 {
    5     for(int i = 0; environ[i]; i++)                                                   
    6     {
    7         std::cout << i << " : " << env[i] << std::endl;
    8     }
    9     return 0;
   10 }
  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 #include <sys/types.h>
  5 #include <sys/wait.h>
  6 
  7 extern char **environ;
  8 
  9 int main()
 10 {
 20     pid_t id = fork();
 21     if(id == 0)
 22     {
 23         printf("pid: %d, exec command begin\n", getpid());
 24         execl("./mytest", "mytest",NULL);                                               
 25         exit(1);
 26     }
 27     else{
 28         // father
 29         pid_t rid = waitpid(-1, NULL, 0);
 30         if(rid > 0)
 31         {
 32             printf("wait success, rid: %d\n", rid);
 33         }
 34     }
 35     return 0;
 36 }

现象:我们会发现替换的程序会打印出当前的环境变量。可能是继承父进程的。

在当前进程中添加环境变量

putenv函数

我们在当前的父进程中使用putenv函数添加环境变量,然后再子进程使用execl函数进行程序替换,替换的函数执行打印当前环境变量代码;观察现象;

    1 #include <stdio.h>
    2 #include <unistd.h>
    3 #include <stdlib.h>
    4 #include <sys/types.h>
    5 #include <sys/wait.h>
    6 
    7 extern char **environ;
    8 
    9 int main()
   10 {
W> 18     char *env_val = "MYVAL5=5555555555555555555555555";
   19     putenv(env_val);
   20     pid_t id = fork();
   21     if(id == 0)
   22     {                                                                                 
   23         printf("pid: %d, exec command begin\n", getpid());
   24         execl("./mytest", "mytest",NULL);
   25         exit(1);
   26     }
   27     else{
   28         // father
   29         pid_t rid = waitpid(-1, NULL, 0);
   30         if(rid > 0)
   31         {
   32             printf("wait success, rid: %d\n", rid);
   33         }
   34     }
   35     return 0;
   36 }

从这里我们可以看到,果然父进程的环境变量被继承了。

环境变量被继承的原因

这个问题也很简单,再之前学习进程的地址空间的时候。命令行参数和环境变量处在最高层;而程序替换只会替换下面的代码和数据环境变量不会被替换

因此,子进程是通过进程地址空间继承父进程的环境变量的;并且环境变量具有全局属性。

使用ecexle传递环境变量

上面的文章是替换程序直接使用父进程的环境变量;我们也可以使用execle函数给替换程序传递环境变量。

    1 #include <stdio.h>
    2 #include <unistd.h>
    3 #include <stdlib.h>
    4 #include <sys/types.h>
    5 #include <sys/wait.h>
    6 
    7 extern char **environ;
    8 
    9 int main()
   10 {
W> 18     char *env_val = "MYVAL5=5555555555555555555555555";
   19     putenv(env_val);
   20     pid_t id = fork();
   21     if(id == 0)
   22     {
   23         printf("pid: %d, exec command begin\n", getpid());
   24         execle("./mytest", "mytest","-a" , "-b", NULL ,environ);
   25         exit(1);                                                                  
   26     }                                                                             
   27     else{                                      
   28         // father                              
   29         pid_t rid = waitpid(-1, NULL, 0);      
   30         if(rid > 0)                            
   31         {                                      
   32             printf("wait success, rid: %d\n", rid);
   33         }                                      
   34     }                                          
   35     return 0;                                  
   36 } 
    1 #include <iostream>
    2 #include <unistd.h>
W>  3 int main(int argc, char *argv[], char *env[])
    4 {
    5     for(int i=0;i<argc;i++)
    6     {
    7         std:: cout<<i << " :"<<argv[i]<<std::endl;                                    
    8     }       
    9     for(int i = 0; environ[i]; i++)
   10     {       
   11         std::cout << i << " : " << env[i] << std::endl;
   12     }       
   13     return 0;
   14 } 

 传递自己的环境变量表

execle函数不仅可以传递系统的环境变量表,也可以传递自己构造的环境变量表。

    1 #include <stdio.h>
    2 #include <unistd.h>
    3 #include <stdlib.h>
    4 #include <sys/types.h>
    5 #include <sys/wait.h>
    6 
    7 extern char **environ;
    8 
    9 int main()
   10 {
W> 11     char *const myenv[] ={                                                            
W> 12         "MYVAL1=11111111111111",
W> 13         "MYVAL2=11111111111111",
W> 14         "MYVAL3=11111111111111",
W> 15         "MYVAL4=11111111111111",
   16         NULL                    
   17     };                          
W> 18     char *env_val = "MYVAL5=5555555555555555555555555";
   19     putenv(env_val);            
   20     pid_t id = fork();          
   21     if(id == 0)                 
   22     {                           
   23         printf("pid: %d, exec command begin\n", getpid());
   24         execle("./mytest", "mytest","-a" , "-b", NULL ,environ);
   25         exit(1);                
   26     }                           
   27     else{                       
   28         // father               
   29         pid_t rid = waitpid(-1, NULL, 0);
   30         if(rid > 0)             
   31         {                       
   32             printf("wait success, rid: %d\n", rid);
   33         }                       
   34     }                           
   35     return 0;                   
   36 }

 事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。

下图exec函数族 一个完整的例子:

自定义简易的shell

前面的文章我们说过我们的指令是无法直接对操作系统进行操作的,需要命令行解释器;况且我们输入的指令也是一个进程;因此当我们输入一个指令的时候,命令行解释器fork一个子进程使用程序替换来执行我们的指令,再结合上篇文章我们是不是实现了一个只执行了一次的命令行解释器。根据这个原理,将这个进程循环下去是不是就是一个简易的shell?

获取主机、使用者、工作目录

当我们登录我们的云服务器时,操作系统会自动生成一份当前用户的环境变量表;这份环境变量表中包含我们需要的东西,我们可以使用系统调用getenv()来获取这些信息;

const char *getUsername()
{
    const char *name = getenv("USER");
    if(name) return name;
    else return "none";
}
const char *getHostname()
{
    const char *hostname = getenv("HOSTNAME");
    if(hostname) return hostname;
    else return "none";
}
const char *getCwd()
{
    const char *cwd = getenv("PWD");
    if(cwd) return cwd;
    else return "none";
}

获取命令

注意:获取命令肯定是获取字符串,获取字符串就避免不了我们会输入空格;因此就不可以使用scanf()函数来获取;必须使用fgets函数。

int getUserCommand(char *command, int num)
{
    printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());
    char *r = fgets(command, num, stdin);
    if(r == NULL) return -1;
    // "abcd\n" "\n"
    command[strlen(command) - 1] = '\0';
    return strlen(command);
}

这里我们获取到字符串的最后一定一定会输入一个回车,因此我们要将字符串的最后一个字符设置为0,方便下面的切割。

切割分解命令

SEP是我们定义的一个宏,实际是只有一个空格的字符串;使用strtok()库函数可以将我们输入的命令按照空格切割开来。再将切割好的每个字符串放到一个数组中。

void commandSplit(char *in, char *out[])
{
    int argc = 0;
    out[argc++] = strtok(in, SEP);
    while( out[argc++] = strtok(NULL, SEP));

#ifdef Debug
    for(int i = 0; out[i]; i++)
    {
        printf("%d:%s\n", i, out[i]);
    }
}

创建子进程执行命令

通过上面的函数我们获得了我们要执行的命令和执行该命令的附加选项,并切割成一个数组;接下来就是要进行程序替换,可是那么多程序替换函数我们到底使用哪一个呢?

使用execvp函数替换,这个函数的第一个参数是要执行的命令,刚好是我们这个数组的第一个元素;第二个参数是可执行程序命令行参数形式形成的指针数组,刚好是我们这个数组;

int execute(char *argv[])
{
    pid_t id = fork();
    if(id < 0) return -1;
    else if(id == 0) //child
    {
        // exec command
        execvp(argv[0], argv); // cd ..
        exit(1);
    }
    else // father
    {
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0){
            lastcode = WEXITSTATUS(status);
        }
    }

    return 0;
}

内建命令的特殊处理

把上面的代码放到一个while循环中,编译运行代码其实已经可以完成一些指令了;但是当我么输入执行cd、export、echo时。我们会发现根本没有用,因为这些命令是我们之前提到的内建命令,这些命令的特殊之处就是要bash自己执行。

因此我们要在执行命令前判断是不是内建命令;这里只实现极个别内建命令。

char *homepath()
{
    char *home = getenv("HOME");
    if(home) return home;
    else return (char*)".";
}
void cd(const char *path)
{
    chdir(path);
    char tmp[1024];
    getcwd(tmp, sizeof(tmp));
    sprintf(cwd, "PWD=%s", tmp); // bug
    putenv(cwd);
}

// 什么叫做内键命令: 内建命令就是bash自己执行的,类似于自己内部的一个函数!
// 1->yes, 0->no, -1->err
int doBuildin(char *argv[])
{
    if(strcmp(argv[0], "cd") == 0)
    {
        char *path = NULL;
        if(argv[1] == NULL) path=homepath();
        else path = argv[1];
        cd(path);
        return 1;
    }
    else if(strcmp(argv[0], "export") == 0)
    {
        if(argv[1] == NULL) return 1;
        strcpy(enval, argv[1]);
        putenv(enval); // ???
        return 1;
    }
    else if(strcmp(argv[0], "echo") == 0)
    {
        if(argv[1] == NULL){
            printf("\n");
            return 1;
        }
        if(*(argv[1]) == '$' && strlen(argv[1]) > 1){ 
            char *val = argv[1]+1; // $PATH $?
            if(strcmp(val, "?") == 0)
            {
                printf("%d\n", lastcode);
                lastcode = 0;
            }
            else{
                const char *enval = getenv(val);
                if(enval) printf("%s\n", enval);
                else printf("\n");
            }
            return 1;
        }
        else {
            printf("%s\n", argv[1]);
            return 1;
        }
    }
    else if(0){}

    return 0;
}

Linux中还有很多内建命令,就不一一实现了;大家有兴趣的话可以自己尝试的添加下。到此我们简易的shell就编写完成了,下面是完整代码。

完整代码

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

#define NUM 1024
#define SIZE 64
#define SEP " "
//#define Debug 1

char cwd[1024];
char enval[1024]; // for test
int lastcode = 0;

char *homepath()
{
    char *home = getenv("HOME");
    if(home) return home;
    else return (char*)".";
}

const char *getUsername()
{
    const char *name = getenv("USER");
    if(name) return name;
    else return "none";
}
const char *getHostname()
{
    const char *hostname = getenv("HOSTNAME");
    if(hostname) return hostname;
    else return "none";
}
const char *getCwd()
{
    const char *cwd = getenv("PWD");
    if(cwd) return cwd;
    else return "none";
}
int getUserCommand(char *command, int num)
{
    printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());
    char *r = fgets(command, num, stdin); // 最终你还是会输入\n
    if(r == NULL) return -1;
    // "abcd\n" "\n"
    command[strlen(command) - 1] = '\0'; // 有没有可能越界?不会
    return strlen(command);
}

void commandSplit(char *in, char *out[])
{
    int argc = 0;
    out[argc++] = strtok(in, SEP);
    while( out[argc++] = strtok(NULL, SEP));

#ifdef Debug
    for(int i = 0; out[i]; i++)
    {
        printf("%d:%s\n", i, out[i]);
    }
#endif
}

int execute(char *argv[])
{
    pid_t id = fork();
    if(id < 0) return -1;
    else if(id == 0) //child
    {
        // exec command
        execvp(argv[0], argv); // cd ..
        exit(1);
    }
    else // father
    {
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0){
            lastcode = WEXITSTATUS(status);
        }
    }

    return 0;
}

void cd(const char *path)
{
    chdir(path);
    char tmp[1024];
    getcwd(tmp, sizeof(tmp));
    sprintf(cwd, "PWD=%s", tmp); // bug
    putenv(cwd);
}

// 什么叫做内键命令: 内建命令就是bash自己执行的,类似于自己内部的一个函数!
// 1->yes, 0->no, -1->err
int doBuildin(char *argv[])
{
    if(strcmp(argv[0], "cd") == 0)
    {
        char *path = NULL;
        if(argv[1] == NULL) path=homepath();
        else path = argv[1];
        cd(path);
        return 1;
    }
    else if(strcmp(argv[0], "export") == 0)
    {
        if(argv[1] == NULL) return 1;
        strcpy(enval, argv[1]);
        putenv(enval); // ???
        return 1;
    }
    else if(strcmp(argv[0], "echo") == 0)
    {
        if(argv[1] == NULL){
            printf("\n");
            return 1;
        }
        if(*(argv[1]) == '$' && strlen(argv[1]) > 1){ 
            char *val = argv[1]+1; // $PATH $?
            if(strcmp(val, "?") == 0)
            {
                printf("%d\n", lastcode);
                lastcode = 0;
            }
            else{
                const char *enval = getenv(val);
                if(enval) printf("%s\n", enval);
                else printf("\n");
            }
            return 1;
        }
        else {
            printf("%s\n", argv[1]);
            return 1;
        }
    }
    else if(0){}

    return 0;
}

int main()
{
    while(1){
        char usercommand[NUM];
        char *argv[SIZE];
        // 1. 打印提示符&&获取用户命令字符串获取成功
        int n = getUserCommand(usercommand, sizeof(usercommand));
        if(n <= 0) continue;
        // 2. 分割字符串
        // "ls -a -l" -> "ls" "-a" "-l"
        commandSplit(usercommand, argv);
        // 3. check build-in command
        n = doBuildin(argv);
        if(n) continue;
        // 4. 执行对应的命令
        execute(argv);
    }
}

这篇文章到此我们就翻过Linux的第一座大山——进程控制;从冯诺依曼体系结构到现在,有关Linux中进程的相关介绍已经完结,大家可以看看前面的文章温习温习。从下篇文章开始我们将开始介绍Linux的第二座大山——文件系统。


今天对Linux下自定义简易shell的分享到这就结束了,希望大家读完后有很大的收获,也可以在评论区点评文章中的内容和分享自己的看法;个人主页还有很多精彩的内容。您三连的支持就是我前进的动力,感谢大家的支持!!! 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1437739.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

CSS之盒子模型

盒子模型 01-选择器 结构伪类选择器 基本使用 作用&#xff1a;根据元素的结构关系查找元素。 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IE…

根据MySql建表语句创建Java实体类工具

点击下载《根据MySql建表语句创建Java实体类工具》 1. 前言 在软件开发领域&#xff0c;特别是在构建企业级应用时&#xff0c;数据模型与代码模型之间的映射是至关重要的。该软件是一款基于C#开发的高效工具&#xff0c;它将这一繁琐且容易出错的过程变得简洁且快速。此工具…

5.0 ZooKeeper 数据模型 znode 结构详解

数据模型 在 zookeeper 中&#xff0c;可以说 zookeeper 中的所有存储的数据是由 znode 组成的&#xff0c;节点也称为 znode&#xff0c;并以 key/value 形式存储数据。 整体结构类似于 linux 文件系统的模式以树形结构存储。其中根路径以 / 开头。 进入 zookeeper 安装的 …

【wu-lazy-cloud-network】Java自动化内网穿透

项目介绍 wu-lazy-cloud-network 是一款基于&#xff08;wu-framework-parent&#xff09;孵化出的项目&#xff0c;内部使用Lazy ORM操作数据库&#xff0c;主要功能是网络穿透&#xff0c;对于没有公网IP的服务进行公网IP映射 使用环境JDK17 Spring Boot 3.0.2 功能 1.内网…

MRAM存内计算:现状及挑战

1 传统冯诺依曼瓶颈 随着人工智能、物联网、智能传感等应用的快速兴起&#xff0c;数据以爆发式的速度增长。海量数据的高效存储、迁移与处理成为当前信息领域的重大挑战。受限于经典的冯诺依曼计算架构存储与处理分离的特性&#xff0c;在面向大数据分析等应用场景中&#xff…

计算机设计大赛 深度学习+python+opencv实现动物识别 - 图像识别

文章目录 0 前言1 课题背景2 实现效果3 卷积神经网络3.1卷积层3.2 池化层3.3 激活函数&#xff1a;3.4 全连接层3.5 使用tensorflow中keras模块实现卷积神经网络 4 inception_v3网络5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; *…

MybatisPlus快速入门及常见设置

目录 一、快速入门 1.1 准备数据 1.2 创建SpringBoot工程 1.3 使用MP 1.4 获取Mapper进行测试 二、常用设置 2.1 设置表映射规则 2.1.1 单独设置 2.1.2 全局设置 2.2 设置主键生成策略 2.2.1 为什么会有雪花算法&#xff1f; 2.2.2 垂直分表 2.2.3 水平分表 2.…

用云手机打造tiktok账号需要注意些什么?

随着tiktok平台的火热&#xff0c;越来越多的商家开始尝试更高效的tiktok运营方法。其中&#xff0c;tiktok云手机作为一种新科技引起了很多人的注意&#xff0c;那么用云手机运营tiktok需要注意些什么&#xff1f;下文将对此进行详细解析。 1. 不是所有的云手机都适合做tiktok…

AI-数学-高中-23-三角函数的平移与伸缩

原作者视频&#xff1a;三角函数】11三角函数的平移伸缩&#xff08;易&#xff09;_哔哩哔哩_bilibili 左加右减&#xff1a;针对函数中的x变化&#xff0c;上加下减&#xff1a;针对函数f(x)变化。 示例1&#xff1a; 示例2&#xff1a; 示例3

解锁售前新效能:AI助手使用的三点建议

1.售前工作概述 自从阴差阳错从技术实施转做售前到现在也有10多年时间&#xff0c;与技术实施仅负责设备安装调试、用户使用培训以及售后维护等被动工作不同。售前更多的是针对用户的主动性工作&#xff0c;包括需求调研与分析、技术沟通与咨询、方案设计与制定、方案演示与讲…

C#在既有数组中插入另一个数组:Array.Copy方法 vs 自定义插入方法

目录 一、使用的方法 1.使用Array.Copy方法 2.Copy(Array, Int32, Array, Int32, Int32) 3. 使用自定义的方法 二、实例 1.示例1&#xff1a;使用Array.Copy方法 2.示例2&#xff1a;使用自定义的方法 一、使用的方法 1.使用Array.Copy方法 首先定义了一个名为InsertAr…

Docker 有哪些常用的命令和操作?

Docker是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的容器中&#xff0c;然后发布到任何流行的Linux机器或Windows机器上&#xff0c;也可以实现虚拟化。以下是Docker的一些常用命令和操作&#xff1a; 安装和启动Docker 要使用Do…

npm修改镜像源

背景&#xff1a;切换npm镜像源是经常遇到的事&#xff0c;下面记录下具体操作命令 1. 打开终端运行"npm config get registry"命令来查看当前配置的镜像源 npm config get registry2. 修改成淘宝镜像源"https://registry.npmjs.org/" npm config set re…

【MySQL进阶之路】MySQL部署后一定记得先压测!

欢迎关注公众号&#xff08;通过文章导读关注&#xff1a;【11来了】&#xff09;&#xff0c;及时收到 AI 前沿项目工具及新技术的推送&#xff01; 在我后台回复 「资料」 可领取编程高频电子书&#xff01; 在我后台回复「面试」可领取硬核面试笔记&#xff01; 文章导读地址…

DAY5.

握手&#xff1a; 第一次握手&#xff1a;客户端发送SYN包给服务器&#xff0c;并进入SYN_SENT状态&#xff0c;等待服务器返回确认包。 第二次握手&#xff1a;服务器接收到SYN包&#xff0c;确认客户端的SYN&#xff0c;发送ACK包&#xff0c;同时发送一个SYN包&#xff0c;…

(十三)springboot实战——springboot前后端分离方式项目集成spring securtity安全框架

前言 Spring Security 是一款强大且高度可定制的认证和访问控制框架&#xff0c;它是为了保护基于Spring的应用程序提供安全性支持。Spring Security提供了全面的安全服务&#xff0c;主要针对企业级应用程序的需求。其核心组件主要包含&#xff1a;Authentication&#xff08…

如何进行嵌入式系统的产品化和量产准备

嵌入式系统是当今技术发展中不可或缺的一部分。从智能家居设备到汽车电子&#xff0c;嵌入式系统的应用广泛存在。而将嵌入式系统从概念变为产品并进行量产准备是一个复杂的过程。本文将介绍嵌入式系统产品化和量产准备的关键步骤&#xff0c;为您提供一些有用的指导。 一、需求…

【Kubernetes】kubectl top pod 异常?

目录 前言一、表象二、解决方法1、导入镜像包2、编辑yaml文件3、解决问题 三、优化改造1.修改配置文件2.检查api-server服务是否正常3.测试验证 总结 前言 各位老铁大家好&#xff0c;好久不见&#xff0c;卑微涛目前从事kubernetes相关容器工作&#xff0c;感兴趣的小伙伴相互…

docker部署showdoc

目录 安装 1.拉取镜像 2.创建容器 使用 1.选择语言 2.默认账户/密码:showdoc/123456​编辑 3.登陆 4.首页 安装 1.拉取镜像 docker pull star7th/showdoc 2.创建容器 mkdir -p /opt/showdoc/html docker run -d --name showdoc --userroot --privilegedtrue -p 1005…

Java+微信小程序实现智慧家政系统 JAVA+Vue+SpringBoot+MySQL

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、系统展示四、核心代码4.1 查询家政服务4.2 新增单条服务订单4.3 新增留言反馈4.4 小程序登录4.5 小程序数据展示 五、免责说明 一、摘要 1.1 项目介绍 基于微信小程序JAVAVueSpringBootMySQL的智慧家政系统&#xff0…