欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

LInux C编程-myshell的实现

程序员文章站 2022-03-19 13:27:43
...
  • 在Linux系统中,shell是我们每天经常使用的东西,而如何实现一个自己的shell?首先我们需要了解一些基础知识

一.进程基础知识

进程概述: CPU执行的程序,是一个动态的实体,进程是操作系统资源分配的基本单位。

进程和程序的区别在于进程是动态的,程序是静态的,进程是运行中的程序

linux 下可通过ps命令来实现查看

Linux中一个进程由三部分组成,代码段,数据段和栈堆端。代码段存放程序的全局变量,常量,静态变量。堆栈段中的堆用于存放动态分配的全局变量,堆栈中栈用于函数调用,它存放着函数的参数,函数内部定义的局部变量

Linux中的进程控制,系统提供了一些函数可以使用:

  • fork用于创建一个新进程
  • exit用于终结进程
  • wait将父进程挂起,等待子进程的终结
  • getpid 获取当前进程的PID
  • nice改变进程的优先级

进程分为几种不同的状态,运行状态,可中断状态,不可中断状态,僵尸进程,停止进程

2.还有几种特殊的进程
孤儿进程: 如果一个子进程的父进程先与子进程结束,子进程就成为一个孤儿进程

守护进程:在后台运行的,没有控制终端与之相连的进程,它独立于控制终端,周期性的执行某些任务

二.进程的内存映像

我们在linux平时见到的程序,他们是如何转化为进程的?
通常需要如下几个步骤

  • 内核将程序读入内存,为程序分配内存空间
  • 内核为该进程分配了进程标识符(PID)和其他所需资源
  • 内核为该进程保存了PID及其相应的状态信息,把程序放进队列种等待执行,这样就可以被操作系统的调度程序执行了

2.进程的内存映像
进程的内存映像是指的内核在内存中如何存放可执行程序文件,在将程序转化为进程的过程中,把硬盘复制给内存之中
,而在实现自己的shell中就使用到这一概念,可执行程序位于磁盘中,而内存映像在内存之中,内存映像随着程序的执行在动态变化中

三.实现myshell所涉及到的部分函数的用法

1.fork函数

fork函数是创建子进程的方式.
include< stdio.h>
include < unistd.h>
pid _t fork (void);
使用fork函数可以将当前进程分裂为两个进程,但是不同的是fork函数有两个返回值,一个是父进程调用fork的返回值,一个是子进程中fork函数的返回值

2.vfork函数
vfork函数与fork函数的不同是什么?
他们的基本用法是相同的,也有一些地方有一些不同,

  • vfork和fork一样都是调用一次,返回两次
  • 使用fork创建一个子进程之后,子进程会继承父进程的资源,具有良好的并发性,而vfork创建的子进程会共享父进程的地址空间,子进程对该地址空间中任何数据的修改都会被父进程所看到
  • vfork函数,一般是子进程先执行,之后父进程才进行执行

3.进程退出
LInux中进程退出分为两种,正常退出和异常退出

(1) 正常退出
在main函数中执行return
调用exit函数
调用_exit函数
而exit和_exit函数有什么区别尼,exit函数在结束时会清除缓冲区中的内容,而_exit会交给内核处理,直接关闭
(2) 异常退出
调用abort函数
进程收到某种信号,而信号会让程序停止

注:尽量减少僵尸进程的产生,应该合理的使用wait/waitpid函数,来让父进程等待子进程的结束

4.执行程序
很多小伙伴看到这里应该就很好奇,如何执行进程?
在这里给大家介绍一个函数族 exec族身为地字一号的重要人物,其作用是十分广阔的

原理:使用exec族执行一个可执行的文件来代替当前进程的内存映像
exec的族的使用并没有产生新的进程哦,而是把程序的代码换入,重新分贝数据段和栈堆段.

exec族的成员如下

 #include <unistd.h>
 extern char **environ;
 int execl(const char *path, const char *arg, ...
                       /* (char  *) NULL */);
 int execlp(const char *file, const char *arg, ...
                       /* (char  *) NULL */);
  int execle(const char *path, const char *arg, ...
                       /*, (char *) NULL, 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[],
                       char *const envp[]);

我在这里使用到了execvp,在这里介绍以下它的用法吧,

它的参数中的filename,如果其中包含了”/”的话,相当于路径,不包含的话”/”,函数就到环境变量中PATH来寻找

4.等待进程结束
这点是必要的,不然会造成僵尸进程的产生

#include<sys/types.h>
#include<sys/wait.h>
pid_t waitint *statloc)
pid_t waitpid (pid_t pid, int * statloc , int options)

wait函数让父进程暂停执行,直到它的一个子进程结束为停止
状态信息将会被写入到statloc
waitpid 函数也用来等待子进程的结束,但是它有特定的要求pid需指定要等待的子进程的pid

5.strtok函数(我用来解析命令行参数)

该函数包含在”string.h”头文件中
函数原型:

char* strtok (char* str,constchar* delimiters );

函数功能:
  切割字符串,将str切分成一个个子串
函数参数:
  str:在第一次被调用的时间str是传入需要被切割字符串的首地址;在后面调用的时间传入NULL。
  delimiters:表示切割字符串(字符串中每个字符都会 当作分割符)。
函数返回值:
  当s中的字符查找到末尾时,返回NULL;
  如果查不到delimiter所标示的字符,则返回当前strtok的字符串的指针。
注:strtok函数遇到NULL就会结束

如何实现重定向?

标准输入stdin——-需要处理的数据流
标注输出stdout——–结果数据流
标准错误输出stderr—–错误消息流

默认的三个数据流的文件描述符分别为0,1,2,默认的标准输入为终端键盘IO,标准输出和错误输出都是为终端屏幕。而IO重定向就是将三个数据流定向到别的文件描述符。
最低可用文件描述符

什么是文件描述符? 简单来说就是打开文件的一个索引号。Unix系统中,把打开文件保持在一个数组中,文件描述符即为某文件在此数组中的索引。而最低可用文件描述符的意思就是,每当系统打开一个新文件,则分配一个目前可用的最小的文件描述符用于该文件。每个Unix程序默认打开0,1,2三个文件描述符,其实它们对应的文件就是键盘设备和终端屏幕设备的设备文件。

系统调用dup函数

  int dup(int oldfd);

dup函数的作用是复制oldfd文件描述符给一个最低可用文件描述符。如果我们想将标准输入重定向到新的文件描述符fd,那么我们可以先close(0)关闭标准输入文件描述符,然后调用函数dup(fd),系统则会默认使用最低可用文件描述符指向fd文件描述符对应的文件。最后再关闭fd文件描述符就完成了IO重定向,如下图所示

LInux C编程-myshell的实现

那具体是如何实现的? 关键就在于fork和exec函数之间,exec函数的功能只是利用了磁盘的新程序代替了当前进程的正文段,数据段,堆段和栈段,文件描述符是继承的,除非通过fcntl函数设置了执行时关闭标志.
我们可以在fork函数之后,exec函数之前进行IO重定向.

管道

管道是Unix系统进程通信的一种形式。管道有两个特点:

一般管道的数据只能在一个方向上流动
管道只能在具有公共祖先的两个进程之间使用。通常管道由一个父进程创建,调用fork函数后,这个管道就能在父进程和子进程之间使用了

管道是通过调用pipe函数创建,参数是一个大小为2的int数组,由该参数返回两个文件描述符:fd[0]为读打开,fd[1]为写打开。即fd[1]的输出是fd[0]的输入
LInux C编程-myshell的实现

#include< unistd.h>
#int pipe(int fd[2]);

使用fork函数创建新进程时,也会将父进程的管道复制,就像这样:

LInux C编程-myshell的实现

四.代码

//添加颜色使用命令别名,alias查看
//对于指针数组不能使用strcpy来进行添加 直接衡等就可
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <dirent.h>
#include <signal.h>
#include <ctype.h>
#include <string.h>
#include <pwd.h>
#include <wait.h>
#include <readline/history.h>
#include <readline/readline.h>
#define MAXARGS 20
#define ARGLEN 60
int i;
struct parameter
{
    int normal;        //一般命令
    int out_redirect;  //输出重定向  >
    int in_redirect;   //输入重定向   <
    int have_pipe;     //命令中由管道符号
    int backgroud;     //标识命令有没有重定向
    int out_redirects; // >>
};
char oldpwd[300][300];
int o;
struct parameter param;
//cd内置命令
void shell_cd(char *path[]);
void find_command(char *path[]);
void get_ifnput(char *buf)
{
    struct passwd *name; ///头文件在pwd.h之中,需要添加
    name = getpwuid(getuid());
    char pwd[100] = {0};
    getcwd(pwd, sizeof(pwd) - 1); //保存绝对路径
    int len = strlen(pwd);
    char *p = pwd + len;
    char temp[MAXARGS * ARGLEN];
    int pathlen;
    char *isstarm;
    while (*p != '/' && len--) //把当前目录从后往前遍历
    {
        p--;
    }
    p++;
    sprintf(temp, "[%s @myshell %s] :", name->pw_name, p);
    isstarm = readline(temp);
    add_history(isstarm);
    write_history(NULL);
    pathlen = strlen(temp);
    if (pathlen == MAXARGS * ARGLEN)
    {
        perror("too long");
        exit(-1);
    }
    strcpy(buf, isstarm);
    //buf[pathlen]='\n';
    buf[pathlen++] = '\0';
    if(strcmp(buf,"")==0)
    strcpy(buf,"\n");
    free(isstarm);
}
//解析命令行参数
void explain(char *buf, char *list[256])
{
    i = 0;
    char *p = buf;
    while (1)
    {
        if (strcmp(buf, "cd") == 0)
        {
            strcat(buf, " ~");
        }
        if((strcmp(buf,"ll"))==0)
        {
            strcpy(buf,"ls -l --color=auto");
        }
        if (p[0] == '\n')
            break;
        if (p[0] == ' ')
            p++;
        else
        {
            list[i] = strtok(buf, " "); //将他们的空格分开i
           if(strcmp(list[i],"ls")==0)
           {
              list[i+1]="--color=auto";
               i++;
           }
            i++;
            while ((list[i] = strtok(NULL, " ")) != NULL && strcmp(list[i], "\n") != 0)
            { //在这里需要注意strtok的返回值为NULL
                if(strcmp(list[i],"grep")==0)
                {
                    list[i+1]="--color=auto";
                    i++;
                }
                i++;
            }
        }
        if (list[i] == NULL)
        {
            break;
        }
    }
}
void recover_stdio() //dev/tty是终端控制台
{
    int ttyfd;
    ttyfd = open("/dev/tty", O_RDONLY);
    //付给他标准输出和输出和报错
    dup2(ttyfd, 0);
    dup2(ttyfd, 1);
    dup2(ttyfd, 2);
}
//标准的输入输出流分为三种
//>>函数可使用O_APPEND标识符来进行追加操作
void output_redirce(char *filename, int mode)
{

    if (filename == NULL)
        return;
    int fd;
    if (mode == 1)
        fd = open(filename, O_RDWR | O_APPEND | O_CREAT, 0644);
    else
        fd = open(filename, O_WRONLY | O_CREAT, 0644);
    if (fd < 0)
        perror("open error"); 
    dup2(fd, 1);
    close(fd);
    return ;
}
static void intput_redirce(char *filename)
{
    int fd;
    fd = open(filename, O_RDWR);
    if (fd < 0)
        perror("< error");
    dup2(fd, 0);
    close(fd);
    return;
}
//因为将命令传递给execv函数需要去掉
//执行命令
void do_cmd(char *list[])
{
    param.backgroud = 0;
    //如果命令中有&,表示后台运行,父进程直接返回,不等子进程,
    int status;
    int mode = 0;
    pid_t pid;
    int j = 0;
    int flag = 0; //标记有没有特殊字符,比如重定向或者&
    int pid_flag=0;
    //查看有没有后台运行程序
    for (j = 0; j < i; j++)
    {
        if (strncmp(list[j], "&", 1) == 0)
        {
            if (j <= i - 1)
            {
                param.backgroud = 1;
                break;
            }
            if (j > i - 1)
            {
                perror("Wrong command\n");
                return;
            }
        }
        if (strcmp(list[j], "|") == 0)
        {
            flag = 1;
            pid_flag = 1;
            int pipe_fd[2]; //使用pipe函数构建管道
            int pipstatuts;
            pid_t child, child2;
            pipe(pipe_fd);
            char *file[256];
            *file = (char *)malloc(256);//指针数组跟二维数组不同
            memset(file,0,sizeof(file));
            int k=0,n=0,file_flag=0,l;
            for(k=j+1;k<i;k++)
            {
                file[n] = list[k];
                 if(strcmp(file[n],">")==0)
                {
                    file_flag = 1;
                    file[n] = NULL;
                    l=n;
                    n++;
                    continue;
                }
                if(strcmp(file[n],">>")==0)
                {
                    file_flag = 2;
                    file[n]=NULL;
                    l=n;
                    n++;
                    continue;
                }
                if(strcmp(file[n],"<")==0)
                {
                    file_flag = 3;
                    file[n]=NULL;
                    l=n;
                    n++;
                    continue;
                }
                    n++;
            }
            if ((child = fork()) != 0) //函数的父进程
            {
                if ((child2 = fork()) == 0) //子进程
                {

                    close(pipe_fd[1]);    //关闭写端,管道第一个命令需要读入
                    close(fileno(stdin)); //关闭输入
                    dup2(pipe_fd[0], fileno(stdin));
                    close(pipe_fd[0]); //读端结束关闭,防止影响别的*/
                    list[j]=NULL;
                    file[n] = NULL;
                    if(file_flag == 1)
                        output_redirce(file[l+1], mode);
                    if(file_flag ==2)  
                      output_redirce(file[l+1], 1);
                      if(file_flag == 3)
                       intput_redirce(file[l+1]);
                    execvp(file[0],file);
                    exit(0);
                }
                else //在这里child2的父进程迟缓
                {
                    close(pipe_fd[0]);
                    close(pipe_fd[1]);
                    waitpid(child2, &pipstatuts, 0);
                }
                waitpid(child, &pipstatuts, 0);
            }
            else
            {
                close(pipe_fd[0]);     //写数据
                close(fileno(stdout)); //关闭读端
                dup2(pipe_fd[1], fileno(stdout));
                close(pipe_fd[1]);
                list[j] = NULL;
                execvp(list[0], list);
                exit(0);
            }
                param.have_pipe = 3;
                break;
        }
        if (strcmp(list[j], ">") == 0)
        {
            flag = 1;
            pid = fork();
            if (pid == 0)
            {
                param.out_redirect = 1;
                list[j] = NULL;
                output_redirce(list[j + 1], mode);
                find_command(list);
                exit(0);
            }
            break;
        }
        if (strcmp(list[j], "<") == 0)
        {
            flag = 1;
            pid = fork();
            if (pid == 0)
            {
                param.in_redirect = 1;
                list[j] = NULL;
                intput_redirce(list[j + 1]);
                find_command(list);
                exit(0);
            }
            break;
        }
        if (strcmp(list[j], ">>") == 0)
        {
            flag = 1;
            pid = fork();
            if (pid == 0)
            {
                param.out_redirects = 1;
                list[j] = NULL;
                output_redirce(list[j + 1], 1);
                find_command(list);
                exit(0);
            }
            break;
        }
        if (strcmp(list[j], "&") == 0)
        {
            param.backgroud = 1;
        }
    }
    if (flag == 0 && param.backgroud == 0)
    {
        pid = fork();
        if (pid == 0)
        {
            find_command(list);
            exit(0);
        }
    }
    if (param.backgroud == 1) //如果命令中有后台执行的程序
    {
        pid = fork();
        printf("process id %d\n",pid);
        return;
    }
    if(pid_flag == 0)
    {
          if (waitpid(pid, &status, 0) == -1)
        {
          printf("wait for child process error\n");
        }
    }
}
void showhistroy()
{
    read_history(NULL); 
    HIST_ENTRY **history;
    history = history_list();
    int k = 0;
    while (history[k] != NULL)
    {
        printf("%s\n", history[k]->line);
        k++;
    }
}
//查找命令中的可执行命令,在这里应该fork一个子进程让它在后台,不断工作到终止
//不然的话执行一个进程就会死掉
void find_command(char *path[])
{
    int pid;
    int child_info = -1;
    if (*path == NULL)
        return;
    if (strcmp(*path, "exit") == 0 || strcmp(*path, "logout") == 0)
        exit(0);
    if ((pid = fork()) == -1)
        perror("fork");
    else if (pid == 0)
    {
        execvp(path[0], path);
    }
    else
    {
        if (wait(&child_info) == -1)
            perror("wait");
    }
    return;
}
//cd内置命令
void shell_cd(char *path[])
{
    struct passwd *usrname; ///头文件在pwd.h之中,需要添加
    usrname = getpwuid(getuid());
    if (strcmp(path[1], "~") == 0)
    {
        if (strcmp(usrname->pw_name, "kiosk") == 0)
        {
            strcpy(path[1], "/home/kiosk/");
        }
        if (strcmp(usrname->pw_name, "root") == 0)
        {
            strcpy(path[1], "/root/");
        }
    }
    if (strcmp(path[1], "-") == 0)
    {
        strcpy(path[1], oldpwd[o]);
    }
    getcwd(oldpwd[o], sizeof(oldpwd[o]) - 1); //保存绝对路径
    //printf("%s\n",oldpwd[o]);
    o++;
    if (chdir(path[1]) < 0) //chdir可以改变当前的工作目录,fchdir用来将当前目录改为由文件描述符所指定的目录
    {
        printf("%s\n",path[1]);
        perror("cd");
    }
}
int main(int argc, char **argv)
{
    char *buf = NULL;
    buf = (char *)malloc(MAXARGS * ARGLEN);
    char *list[256];
    *list = (char *)malloc(256);
    if (buf == NULL)
    {
        perror("malloc failed");
        exit(-1);
    }
        signal(SIGINT, SIG_IGN);//屏蔽掉信号,ctrl+c
        signal(SIGQUIT, SIG_IGN);
        signal(SIGSTOP, SIG_IGN);//ctrl+z发出信号
        signal(SIGTSTP, SIG_IGN);  
    while (1)
    {
        memset(buf, 0, 1200);
        memset(list, 0, sizeof(list));
        get_ifnput(buf);
        if (strcmp(buf, "exit") == 0 || strcmp(buf, "logout") == 0)
            break;
        if (strcmp(buf,"\n") == 0 )
            continue; //多个回车
        explain(buf, list);
        if (strcmp(list[0], "cd") == 0)
        {
            shell_cd(list);
            continue;
        }
        if (strcmp(list[0], "history") == 0)
        {
            showhistroy();
            continue;
        }
        do_cmd(list);
    }
    free(buf);
    for (i = 0; i < 256; i++) //将二位数组进行释放
        free(list[i]);
    exit(0);
}
相关标签: shell