Linux进程控制:Shell的功能模拟实现

目录

一、前言

二、shell模拟实现

框架编写

2.1打印命令行提示符,获取用户输入命令字符串

2.2对命令行字符串进行切割

2.3执行命令

2.4处理内建命令


一、前言

[root@hecs-215580 cpp]# ls
1,17.cpp
[root@hecs-215580 cpp]# ps
  PID TTY          TIME CMD
24190 pts/0    00:00:00 bash
24493 pts/0    00:00:00 ps

上面是在Xshell上的经典基本互动和指令操作。

下图的时间轴来表示事件的发生次序。其中时间从左向右。 shell 由标识为 sh 的方块代表,它随着时间的流逝从左向右移动。shell 从用户读入字符串 "ls" shell 建立一个新的进程,然后在那个进程中运行 ls 程序并等待那个进程结束。
Linux进程控制:Shell的功能模拟实现_第1张图片
shell 读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。
所以要写一个 shell ,需要循环以下过程:
1. 获取命令行
2. 解析命令行
3. 建立一个子进程(fork)
4. 替换子进程(execvp)
5. 父进程等待子进程退出(wait)

二、shell模拟实现

框架编写

#include 
#include 
#include 
#include 
#include 
#include 


#define SIZE 1024
#define MAX_ARGC 64
#define SEP " "

char *argv[MAX_ARGC];//方便代码运行时父子进程都可以看到
char pwd[SIZE];
char env[SIZE]; // for test
int lastcode = 0;

int main()
{
    while(1)
    {
        char commandline[SIZE];
        // 1. 打印命令行提示符,获取用户输入的命令字符串
        int n = Interactive(commandline, SIZE);
        if(n == 0) continue;
        // 2. 对命令行字符串进行切割
        Split(commandline);
        // 3. 处理内建命令
        n = BuildinCmd();
        if(n) continue;
        // 4. 执行这个命令
        Execute();
    }
   // for(int i=0; argv[i]; i++)
   // {
   //     printf("argv[%d]: %s\n", i, argv[i]);
   // }
    return 0;
}

首先包含所需要用到的头文件,创建需要用到的全局变量,并创建主函数main,将shell所具有的功能进行函数封装,然后来逐步实现。

2.1打印命令行提示符,获取用户输入命令字符串

const char* HostName()
{
    char *hostname = getenv("HOSTNAME");
    if(hostname) return hostname;
    else return "None";
}

const char* UserName()
{
    char *hostname = getenv("USER");
    if(hostname) return hostname;
    else return "None";
}

const char *CurrentWorkDir()
{
    char *hostname = getenv("PWD");
    if(hostname) return hostname;
    else return "None";
}

int Interactive(char out[], int size)
{
    // 输出提示符并获取用户输入的命令字符串"ls -a -l"
    printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
    fgets(out, size, stdin);
    out[strlen(out)-1] = 0; //'\0', commandline是空串的情况?
    return strlen(out);
}

通过getenv获取到当前用户的信息并打印。

而在获取用户命令行指令时,一条指令里往往都包含有空格,所以用scanf就会导致缓冲区被刷新导致拿不到完整的命令字符串,所以此处用fgets更好一些,使用fgets获取到字符串后,编译器默认会自动在字符串最后添加\0。

因为在输入完成后需要回车来换行结束,所以回车也会被fgets获取,所以最后一个字符就是\n,从而导致在打印时多打印出一个空行,所以在获取字符串后通过strlen计算长度,并把\n改为0,就避免了多打印空行的问题。

Linux进程控制:Shell的功能模拟实现_第2张图片

2.2对命令行字符串进行切割

void Split(char in[])
{
    int i = 0;
    argv[i++] = strtok(in, SEP); // "ls -a -l" SEP为空格
    while(argv[i++] = strtok(NULL, SEP)); 
// 故意将== 写成 = 切割完成后将argv再往后一个成员赋值为null
//注:为方便满足使用其他函数接口的要求,所以将argv最后一个成员赋值为null
//最后while判断argv条件不满足推出循环
    if(strcmp(argv[0], "ls") ==0)
    {
        argv[i-1] = (char*)"--color";
        argv[i] = NULL;
    }
}

Linux进程控制:Shell的功能模拟实现_第3张图片

char *strtok(char s[], const char *delim);

这里用strtok函数分解字符串。s为要分解的目标字符传,delim为分隔符字符(如果传入字符串,则传入的字符串中每个字符均为分割符)。首次调用时,s指向要分解的字符串,之后再次调用要把s设成NULL。在头文件#include中。


 当strtok()在s字符串中找到参数delim中所包含的分割字符时,则会将该字符改为\0 字符。在第一次调用时,strtok()必需给予参数s字符串,往后的调用继续分割历史字符串则需要将参数s设置成NULL。继续往后分割。 

从s开头开始的一个个被分割的串。当s中的字符查找到末尾时,返回NULL。如果查找不到delim中的字符时,返回当前strtok的字符串的指针。所有delim中包含的字符都会被滤掉,并将被滤掉的地方设为一处分割的节点。

将用户输入的指令行以空格作为分割进行分割后,将分割出的命令的指针依次存入argv指针数组中。

2.3执行命令

void Execute()
{
    pid_t id = fork();
    if(id == 0)//通过id判断是否为子进程
    {
        // 让子进程执行命令
        execvp(argv[0], argv);
        exit(1);
    }

    //父进程
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);//等待子进程结束
    if(rid == id) lastcode = WEXITSTATUS(status); //获取退出码

}

因为命令行shell的运行是持续性的,所以不能让shell自己去进行程序替换,所以需要fork创建子进程来不断进行单次命令的执行。

2.4处理内建命令

像cd,echo,export这种内建命令单纯让子进程直接去执行是行不通的,只会改变子进程的路径,而子进程在执行完毕后就会进行销毁,父进程无法直接打印出切换后的用户路径,需要bash自己去切换。

int BuildinCmd()
{
    int ret = 0;
    // 1. 检测是否是内建命令, 是 1, 否 0
    if(strcmp("cd", argv[0]) == 0)
    {
        // 2. 执行
        ret = 1;
        char *target = argv[1]; //cd XXX or cd
        if(!target) target = Home();
        chdir(target);
        char temp[1024];
        getcwd(temp, 1024);//获取当前路径
        snprintf(pwd, SIZE, "PWD=%s", temp);//用绝对路径更新环境变量
        //snprintf:按照指定格式,把指定内容格式化写入到指定长度的字符串中
        putenv(pwd);//导入到环境变量中
    }
    else if(strcmp("export", argv[0]) == 0)
    {
        ret = 1;
        if(argv[1])
        {
            strcpy(env, argv[1]);
            putenv(env);
        }
    }
    else if(strcmp("echo", argv[0]) == 0)
    {
        ret = 1;
        if(argv[1] == NULL) {
            printf("\n");
        }
        else{
            if(argv[1][0] == '$')
            {
                if(argv[1][1] == '?')
                {
                    printf("%d\n", lastcode);//打印退出码
                    lastcode = 0;
                }
                else{
                    char *e = getenv(argv[1]+1);
                    if(e) printf("%s\n", e);
                }
            }
            else{
                printf("%s\n", argv[1]);
            }
        }
    }
    return ret;
}

你可能感兴趣的:(Linux,linux,服务器,c语言,c++)