当前位置 : 主页 > 编程语言 > 其它开发 >

Shell实现:基本功能

来源:互联网 收集:自由互联 发布时间:2022-05-30
本文介绍如何在Linux系统下使用C语言编写一个能运行和终止程序的Shell 独立博客阅读地址:https://panqiincs.me/2017/02/26/write-a-shell-basic-functionality/ Shell的功能 Shell是操作系统中管理进程和运
本文介绍如何在Linux系统下使用C语言编写一个能运行和终止程序的Shell

独立博客阅读地址:https://panqiincs.me/2017/02/26/write-a-shell-basic-functionality/

随机一图

Shell的功能

Shell是操作系统中管理进程和运行程序的程序。所有常用的shell都有三个主要功能:

  • 运行程序:grep, date, ls等都是一些普通的程序,shell将它们载入内存并运行它们
  • 管理输入和输出:使用<,>和 | 符号可以将输入、输出重定向。这样就可以告诉shell将进程的输入和输出连接到一个文件或是其他的进程
  • 可编程:shell同时也是带有变量和流程控制的编程语言

本文将介绍如何编写包含最基本功能,即能运行和终止程序的shell。

Shell的主流程

Shell打印提示符、输入命令、运行命令,如此反复。因此,主流程由下面的循环组成:

for (;;) {
    print_prompt(prompt_str);
    handle_input(arg_buf, arg_list);
    run_cmd(arg_list);
}
打印提示符

下面是一个提示符的示例:

[psh]krist@linux-szhw:/home/krist/Workspace$

加上前缀[psh]以和系统自带的shell区别开来。后面依次是用户名(用getlogin_r函数获取)、主机名(用gethostname函数获取)、当前目录(用getcwd函数获取)和命令提示符。如果当前登录的用户是root,命令提示符为#,普通用户则打印美元符号$,因此需要用geteuid函数获取当前的Effective User ID。

处理用户输入

一般需要限制用户单次输入的最多字符数MAX_ARG_LEN以及参数的最大个数MAX_ARG_NUM。用户输入命令,按下回车键即表示当前的命令输入完毕。用getline函数读取一行用户输入,然后将一整条命令拆分。例如命令ls -al /home拆分之后变成ls-al/home三个字符串,第一个字符串是命令的名称,后面依次是命令参数。拆分字符使用strsep函数,分割符(delimeter)为空格(Space)。下面是拆分过程的代码:

stringp = input_buf;
arg_num = 0;       // number of arguments
while (((token = strsep(&stringp, " ")) != NULL)
        && (arg_num < MAX_ARG_NUM)) 
{
    // Token is terminated by overwriting the delimiter with a null 
    // byte('\0'), so continuous space will result in a token with 
    // only a null byte, skip it.
    if (strcmp(token, "") != 0) {
        arg_vec[arg_num] = token;
        arg_num++;
    }
} 
arg_vec[arg_num] = NULL;

其中input_buf是一个字符数组,存放用户输入的单条命令字符串,arg_vec是字符串数组,存放拆分后的多个字符串。

运行命令 fork和exec

想必大家都熟悉shell运行命令的流程:用户输入一条命令后,父进程会fork一个子进程,在子进程中使用exec函数运行用户输入的命令,父进程等待子进程退出后,等待用户输入下一条命令,如此反复。代码如下:

void exec_cmd(char **arg_vec)
{
    int   status;
    pid_t pid;

    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) { // child process, run the command
        // execvp() returns only if an error occurs
        execvp(arg_vec[0], arg_vec);
        perror("execvp");
        exit(EXIT_FAILURE);
    } else {               // parent process, wait for children to exit
        while (wait(&status) != pid)
            ;
    }
}

exec函数毫无疑问选择最方便的execvp,主进程使用wait函数等待子进程退出。

内建命令

这个时候一个可以跑的shell就完成了。但是却无法运行cd命令,提示错误如下:

execvp: No such file or directory

运行whereis cd命令,并没有显示可执行文件的路径。原来cd命令是内建命令(build in command),需要在shell中自己实现,而且它的执行不需要建立子进程,需要额外判断和处理。

我写了一个函数判断是否是内建命令,如果不是内建命令则返回-1,否则按照内建命令的方式运行。目前只实现了cdexit两个内建命令。代码如下:

int builtin_cmd(char **arg_vec)
{
    if (strcmp(arg_vec[0], "cd") == 0) { // cd
        if ((arg_vec[1] != NULL) && (arg_vec[2] == NULL)) {
            if (chdir(arg_vec[1]) == -1) {
                perror("cd"); 
            }
        } else {
            printf("Usage: cd [directory]\n");
        }
        return 0;
    } else if (strcmp(arg_vec[0], "exit") == 0) { // exit
        exit(EXIT_SUCCESS); 
    }

    return -1;
}

此时,shell主循环中的run_cmd函数演变为:

void run_cmd(char **arg_vec)
{
    if (arg_vec[0] == NULL) {
        return;
    }
    if (builtin_cmd(arg_vec) == -1) { // run build-in commands
        exec_cmd(arg_vec); // run normal commands
    }
}
处理信号

我们期望shell能够处理信号。在shell的命令行中敲命令运行程序,在程序还在运行时按下Ctrl-CCtrl-\键(分别产生SIGINTSIGQUIT信号),运行的程序会退出,即中断运行子进程;在shell的命令行中不运行命令,直接按下这两个键组合,不能退出shell程序,即父进程不会中断运行,退出shell程序只能通过Ctrl-D按键。实现上述功能需要在父进程中忽略信号SIGINTSIGQUIT,但是在子进程中恢复对信号SIGINTSIGQUIT的默认操作。代码很简单,在这里不列出。

这样,一个具备基本功能的shell就完成了,完整的代码请参考这里。请继续看下一篇:Shell实现:重定向和管道。

参考
  1. The Linux Programming Interface
  2. Unix/Linux编程实践教程
网友评论