Linux进程控制实践——miningshell

在学习了进程控制的基础上实现一个小型的miningshell,加深对进程控制的掌握,这里实现的仅仅是一个简易版,只能完成一些基础简单的功能!


目录

显示提示符

获取用户输入

切分字符串

TODO——内建命令

cd命令

export命令

创建进程执行命令and程序替换

总结and代码实现


我们在使用shell,输入完一个命令后它会继续弹出提示符等待我们继续输入,所以这应该有一个死循环等待用户输入,所以应该先设置一个死循环输入,因为我们要执行完指令后还能继续输入,因此,执行命令的应该是创建一个子进程去执行,而不是父进程执行,否则执行完毕该程序就结束了,无法继续输入,但是有些命令是内建命令,就需要在父进程中执行,如cd,export,echo......

显示提示符

首先是显示提示符,如下:

就需要我们获取当前用户的用户名,主机号和当前目录,调用系统接口并对输出的格式进行转化即可,具体代码如下:

    //获取用户名
    uid_t uid = getuid();
    struct passwd *pw = getpwuid(uid);
    printf("[%s@", pw->pw_name);
    //获取主机号
    char hostname[256];
    if (gethostname(hostname, sizeof(hostname)) != 0) {
        perror("gethostname error");
        return 1;
    }
    printf("%s:", hostname);
    //获取当前目录
    char cwd[4096];
    if (getcwd(cwd, sizeof(cwd)) != NULL) {
        printf("%s]$ ", cwd);
    } else {
        perror("getcwd error");
        return 1;
    }
    //刷新缓冲区
    fflush(stdout);

运行该代码则会显示提示符,由于输出已设置好格式,就会像上图一样,不过会处于死循环的打印该行在没有进行死循环的输出情况如下:

(注:我们会发现Linux系统的实现对于家目录到用户目录那一块是~,但这里实现的不是,需要对获取当前目录的输出进行修改输出进行转换)

这样显示提示符就完成了 

获取用户输入

shell若要执行我们输入的命令就需要获取到我们的输入,由于我们是使用一个字符数组获取的输入,因此我们每次在读入用户输入时就需要将数组先清空,这里使用的memset函数,其次使用的是fgets函数获取用户输入,使用gets函数也是一样的,fgets函数的声明如下:

需要我们给他传存储获取到的输入,接收的数据大小,以及输入流 

    memset(command_line, '\0', sizeof command_line);
    fgets(command_line, NUM, stdin);
    command_line[strlen(command_line)-1] = '\0';//清空输入的\n

这里是第三行代码存在的意义是我们在输入完成后会敲一下回车,而这个回车就会被fgets作为\n读入到字符数组command_line中,但我们在执行完命令后本来就是会换行的,因此会出现多一个换行符,所以需要将读入的\n去掉,我们无法改变系统函数!

切分字符串

读完用户的输入的字符串就需要将读入的字符串进行切分,以"ls -a -l -i"为例,我们需要将其分开分别将"ls" "-a" "-l" "-i"切出来然后传给进程替换函数进行程序替换,切分字符串这里使用的strtok函数,其声明如下:

参数分别是需要被切割的字符串和分隔符,返回的就是被切割的字符串 

    command_args[0] = strtok(command_line, SEP);
    int index = 1;
    //给ls加上颜色
    if(strcmp(command_args[0], "ls") == 0)
      command_args[index++] = (char*)"--color=auto";
    //strtok截取成功返回字符串起始地址,截取失败返回NULL,若command_line已经截取过想继续截取该字符串则传NULL
    while(command_args[index++] = strtok(NULL, SEP));

这里使用command_args数组来存储被切割下来的字符串,第一个位置就是我们输入的命令,然后再将剩下的字符串继续切割下来往command_args数组下标为1的开始往后存储,直至全部切完后切割不了strtok返回NULL,while中的strtok传的NULL是因为我们要继续从已经被切割剩余的字符串切割,不能传command_line,否则就是一直切割第一个字符串,死循环了就

中间插入的两行代码的意义:我们在执行ls时,发现文件是有颜色的,就是因为我们使用的ls其实应该是下图这样:

alias是取别名的意思,因此其实我们使用的ls并不是原生的ls,而是带了参数的ls --color=auto,被取别名成ls,所以我们使用系统的shell时有些文件是有颜色的。

因此,我们也可以像他这样,当传入的命令是ls时,就给存储拆分字符串数组command_args中先放上一个参数--color=auto即可,就可以和系统的ls一样了

TODO——内建命令

由于是miningshell,因此这里只实现了cd和export命令。

    //内建命令cd,因为cd要改变父进程而不是子进程
    if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL )
    {
      chdir(command_args[1]);
      continue;
    }
    //内建命令export导入环境变量,将环境变量变成全局的
    if(strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
    {
      strcpy(env_array, command_args[1]);
      putenv(env_array);
      continue;
    }

cd命令

由于改变路径我们想要的是改变父进程的路径而不是子进程路径,子进程执行完命令就die了,所以cd要被作为内建命令实现,当我们输入cd路径时,需要判断输入是否合法,合法则调用系统接口chdir函数来改变我们父进程的路径,chdir函数的声明如下:

将新的路径传入即可,即我们切割字符串存储数组command_args的第二个位置就是路径即command_args[1],由于是内建命令,因此执行完毕后就continue开始下一轮命令的读入 

export命令

在学习环境变量时我们知道环境变量是全局属性的,从当前进程往下的每一个进程都会继承当前进程的环境变量,而我们知道export就可以将我们自建的环境变量导入到系统的环境变量中(shell层面),所有导入的环境变量最后最会变成shell的内容,即变成shell中的环境变量。

所以这里有个问题,如果是在子进程中导入我们自建的环境变量则无法影响到shell,只能影响到从子进程往下的进程,此时在shell中就无法查询到该自建的环境变量。

因此我们需要将export作为自建命令来实现,不能交由子进程来执行该命令。

使用putenv函数将自建环境变量导入到系统环境变量,声明如下:

即将command_args数组第二个位置的字符串路径传入给putenv函数。

但是这里还有个问题,由于我们执行完内建内建命令后会进行下一轮更新等待用户输入,因此command_args数组会被覆盖,那之前存储的环境变量就不复存在了,再打印该环境变量就会变成null即不存在该环境变量,所有我们需要一个全局变量来存储我们的环境变量,因为环境变量需要具有全局性,能够被子进程继承,所以这里为了测试设置了一个字符串数组env_array(正确做法应该是开辟一块空间给它,不能随意被释放)用来存储环境变量,将需要导入的环境变量拷贝到env_array中存储下来即可

以上的两个问题可以创建一个程序打印自建的环境变量,在我们自己实现miningshell中使用export导入自建的环境变量,然后执行打印自建的环境变量查看输出结果来验证

创建进程执行命令and程序替换

不是内建命令交给子进程去执行,让子进程执行完毕即可die,让子进程进行程序替换,执行命令

    pid_t id = fork();
    if(id == 0)
    {
      //6. 程序替换
      execvp(command_args[0], command_args);//选择该函数进行替换,因为用户输入的是系统命令在PATH中
      exit(1);//替换失败
    }
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if(ret > 0)
      printf("wait successfully! singlecode : %d, exitcode : %d\n", status&0x7F, (status>>8)&0xFF);

这里创建进程后设置的是阻塞状态下等待子进程结束,也可以设置成非阻塞状态等待,这里的程序替换函数选择的是execvp函数,因为我们替换时,是将存储被切割成多个字符串的command_args数组传给替换函数,因此选择execvp函数更方便

总结and代码实现

至此,一个简单的miningshell就完成了,实现miningshell是为了加深对进程控制这块的理解,以下是代码实现:

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

#define NUM 1024
#define SIZE 128
#define SEP " "

char command_line[NUM];//存储输入的字符串
char *command_args[SIZE];//存储切割完的字符串
char *env_array[NUM];//用来存储全局的环境变量

int main()
{
  while(1)
  {
    //1. 显示提示符
    //获取用户名
    uid_t uid = getuid();
    struct passwd *pw = getpwuid(uid);
    printf("[%s@", pw->pw_name);
    //获取主机号
    char hostname[256];
    if (gethostname(hostname, sizeof(hostname)) != 0) {
        perror("gethostname error");
        return 1;
    }
    printf("%s:", hostname);
    //获取当前目录
    char cwd[4096];
    if (getcwd(cwd, sizeof(cwd)) != NULL) {
        printf("%s]$ ", cwd);
    } else {
        perror("getcwd error");
        return 1;
    }
    //刷新缓冲区
    fflush(stdout);
    //2. 获取用户输入
    memset(command_line, '\0', sizeof command_line);
    fgets(command_line, NUM, stdin);
    command_line[strlen(command_line)-1] = '\0';//清空输入的\n
    //3. 切分字符串
    command_args[0] = strtok(command_line, SEP);
    int index = 1;
    //给ls加上颜色
    if(strcmp(command_args[0], "ls") == 0)
      command_args[index++] = (char*)"--color=auto";
    //strtok截取成功返回字符串起始地址,截取失败返回NULL,若command_line已经截取过想继续截取该字符串则传NULL
    while(command_args[index++] = strtok(NULL, SEP));
    //4. TODO
    //内建命令cd,因为cd要改变父进程而不是子进程
    if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL )
    {
      chdir(command_args[1]);
      continue;
    }
    //内建命令export导入环境变量,将环境变量变成全局的
    if(strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
    {
      strcpy(env_array, command_args[1]);
      putenv(env_array);
      continue;
    }
    //5. 创建进程执行命令
    pid_t id = fork();
    if(id == 0)
    {
      //6. 程序替换
      execvp(command_args[0], command_args);//选择该函数进行替换,因为用户输入的是系统命令在PATH中
      exit(1);//替换失败
    }
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if(ret > 0)
      printf("wait successfully! singlecode : %d, exitcode : %d\n", status&0x7F, (status>>8)&0xFF);

  }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hiland.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值