c++简单实现shell

Ⅰ.主结构

操作系统分为shell 和 kernel,shell为上层应用软件提供接口,kernel面向计算机内部;shell可以被视为一个中介,任务就是执行用户与内核交互相关的命令,在不同的操作系统上,shell有不同的类型,比如终端形式和GUI,虽然形式不同,但其内部的逻辑都是一样的:等待用户的命令–>执行命令–>返回结果–>回到等待状态;

环境

VMWare上搭建的MINIX3操作系统

宏定义及全局变量

argc和argv[]必不可少;history[][]用于存放历史命令;buf[]存储用户键入字符串

#include <unistd.h> //提供对 POSIX 操作系统 API 的访问功能的头文件
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/stat.h>
#include <cerrno>
#include <dirent.h> 

#define buffsize 255
#define maxargs 128 //每条命令最大参数
#define maxline 128 //最大命令数

char prompt[]="myshell> ";
char buf[buffsize];
int argc;
char* argv[maxargs];
char curPath[buffsize];  
char history[maxline][buffsize];
int cmd_num = 0;  

主体逻辑

  1. Shell主体结构是一个while循环,不断地接受用户键盘输入行并给出反
    馈(getbuf函数)。
  2. 接收到用户输入后,调用parsline函数解析命令,同时构建argv数组便于之后的传参;
  3. 命令解析成功后,调用eval函数执行命令;命令分为两种,第一种是program命令,包括重定向(>),管道(|),后台运行(&),以及运行程序。第二种是内置命令,包括cd,history,mytop等;
  4. 执行eval()时,先判断是否是管道/重定向/后台命令,之后再判断是否是内置命令,如果都不是,说明是运行程序类的命令,此时fork子进程,并调用execvp执行程序。
int main()
{   
    while(1)
    {
        printf("%s",prompt);
        if(getcommand(buf))
        {   
            if(strcmp(buf,"exit")==0)
            {   
                exit(0);
                break;
            }
            else
            {
                strcpy(history[cmd_num],buf);
                cmd_num++;
                parseline(buf);
                eval(argc,argv);                
            }
        }
    }
    return 0;
}

int getcommand(char *buf)
{
    memset(buf,0,buffsize);
    int length;

    fgets(buf,buffsize,stdin);
    length = strlen(buf);
    buf[length-1] = '\0';
    return strlen(buf);
}

void parseline(char* cmd)
{
    int i,j,k;
    int len = strlen(cmd);
    int num=0;
    for(i=0;i<maxargs;i++)
    {
        argv[i] = NULL;
    }

    char tmp[buffsize];
    j=-1;
    for(i=0;i<=len;i++)
    {
        if(cmd[i]==' '||i==len)
        {
            if(i-1>j)
            {
                cmd[i]='\0';
                argv[num++] = cmd+j+1;
            }
            j = i;
        }
    }
    argc = num;
    argv[argc] = NULL;
}
//以下是伪代码
void eval(int argc,char* argv[])
{   
    pid_t pid,pid2;
    int i,j;
    int bg_flag = 0;
   /*program_command*/

    if '>' then outRedirect(argc,i,argv);
    if '<' then inRedirect(argc,i,argv);
	if '>>' then reoutRedirect(argc,i,argv); 
    if '|' then:
    	fork();
    	if pid == 0 then Pipecommand(buf);
    	else waitpid();
    if '&' then bgflag = 1;
        
    /*内置命令*/
    if(strcmp(argv[0],"cd")==0)
    {   
        int res = CD(argc);
        if(res==0) printf("cd指令错误\n");
    }
    else if(strcmp(argv[0],"history")==0)
    {   
        int res = HISTORY(argc);
        if(res ==0) printf("history指令错误\n");
    }
    else if(strcmp(argv[0],"mytop")==0)
    {
        mytop();
    }
    else
    {   
        fork();
        if pid==0 then:
        	if(bgflag==1):/*后台执行程序方法*/
        	else: execvp()
        else : waitpid();
    }
}

Ⅱ.program命令

运行程序

执行程序是shell最基本的功能,我们可以使用exec系函数来完成,用fork创建新进程,用exec初始执行新的程序,exit和wait处理终止和等待终止,这是最基本的进程控制原语;exec有七个不同的函数供我们使用,由于被执行的是内置命令脚本,所以直接调用execvp();

int execvp(const char *filename,char *const argv[])
  • 如果filename中包含/,则将其视为路径名
  • 否则按path环境变量,在目录中寻找可执行文件,如果该文件不是由连接编辑器产生的机器可执行文件,则将其视为脚本文件,并尝试/bin/sh

exec是不返回的,当命令不是后台命令时,父进程应该等待子进程结束,再接收下一条命令,这一要求可以由waitpid()实现,(后台命令的情况在后面讨论)。而为了防止其成为僵尸子进程,父进程需要接收其识别码,便于报错;

if((pid=fork())==-1)
        {
            printf("子进程创建失败\n");
            return ;
        }
        else if(pid == 0)
        {   
            if(bg_flag)
            {
				/*后台命令处理方法*/
            }
            execvp(argv[0], argv);
            printf("%s: 命令输入错误\n", argv[0]);
            exit(1);

        }
        else
        {
            if(bg_flag)
            {
                /*后台命令处理方法*/
            }
            int status;
            waitpid(pid, &status, 0);       // 等待子进程返回
            int err = WEXITSTATUS(status);  // 读取子进程的返回码
            if (err) { 
                printf("Error %d: %s\n", errno,strerror(err));
            }
        }

重定向

重定向命令可以分为三种:

  1. ’>’ 覆盖写
  2. ’<’ 文件输入
  3. ’>>’ 追加写

由于三个操作的函数逻辑是一样的,所以这里以覆盖写操作为例。
首先在eval函数中要识别命令是覆盖写命令,只需要遍历argv即可,如果匹配到了’>’,则调用处理覆盖写的函数outRedirect;

dup2函数:
谈到重定向,就不得不谈到dup2函数。

int dup2(int fd,int fd2)

dup2允许将一个未使用的文件描述符定向到一个已打开的文件.若参数fd2已经被程序使用,则系统就会将fd2所指的文件关闭,若fd2等于fd,则返回fd2,而不关闭fd2所指的文件。dup2所复制的文件描述符与原来的文件描述符共享各种文件状态。共享所有的锁定,读写位置和各项权限或flags等。
所以当fd为目标文件,fd2为标准输出时,系统会先关闭标准输出,然后令标准输出指向目标文件。

open函数:
想要重定向输出,还需要知道目标文件的信息,而目标文件的路径可以以字符串的形式从argv中读出。所以调用open函数获取文件的文件描述符:

int open(const char* path,int oflag,.../*mode_t mode*/)

path参数是文件的路径;oflag可用来说明此函数的多个选项,用下列一个或多个常量进行运算构成oflag参数;

  • O_RDONLY 只读打开
  • O_WRONLY 只写打开
  • O_RDWR 读写打开
  • O_EXEC 只执行打开
  • O_SEARCH 只搜索打开

以上5个参数只能选择一个,以下常量则可多个搭配

  • O_APPEND 每次写都追加到尾端
  • O_CREAT 若不存在则创建
  • O_TRUNC 每次打开后长度截断为0

易知覆盖写的oflag为 O_WRONLY | O_TRUNC | O_CREAT

当oflag中存在O_CREAT时,函数还要有第三个参数:mode:mode指定了新文件的访问权限位,共有11位,其中有9位的文件访问权限位。每3位为一组,所以mode为八进制码,例如0777换算为2进制则是0111111111,即用户、同组用户、其他用户均可以对其进行读写执行操作;

int outRedirect(int argc,int inter,char* argv[])
{
    char outFile[buffsize];
    memset(outFile, 0x00, buffsize);
    int i,j;
    int redi_num = 0;
    for(i=0;i<argc;i++)
    {   
        if(strcmp(argv[i],">")==0)
        {
            redi_num += 1 ;
        }
    }
    if(redi_num != 1)
    {
        printf("wrong Redirect command\n");
        return 0;
    }
    if(inter==argc-1) 
    {
        printf("lack of output-file name\n");
        return 0;
    }
    if(argc-inter>2) 
    {
        printf("to many file\n");
        return 0;
    }
    strcpy(outFile,argv[inter+1]);
    for(j=inter;j<argc;j++)
    {
        argv[inter] =NULL;
    }

    pid_t pid;
    
    if((pid=fork())==-1)
    {
        printf("子进程创建失败\n");
        return 0;
    }
    else if(pid == 0)
    {
        int fd;
        int oldfd;
        fd = open(outFile, O_WRONLY|O_CREAT|O_TRUNC, 777);
        // 文件打开失败
        if (fd < 0) {
            printf("open error\n");
            exit(1);
        }
        if(dup2(fd, STDOUT_FILENO) == -1)
        {
            printf("dup2 error\n");
            exit(1);
        } 
        execvp(argv[0], argv);
        if (fd != STDOUT_FILENO) {
            close(fd);
        }      
        printf("%s: 命令输入错误\n", argv[0]);
        exit(1);
    }
    int status;
    waitpid(pid, &status, 0);       // 等待子进程返回
    int err = WEXITSTATUS(status);  // 读取子进程的返回码
    if (err) { 
        printf("Error %d: %s\n", errno,strerror(err));
    }
    
    return 0;
}

管道

管道命令的实现与重定向类似,主要用到pipe()和dup()函数;

int pipe(int fd[2])//成功返回0,出错返回-1

管道只能在具有共同祖先的进程下使用,所以需要fork出父子两个进程,分别完场前后两个命令,然后用管道将前一条命令进程的输出作为下一条命令进程的输入。
pipe函数经由参数fd返回两个文件描述符:fd[0]为读而打开,fd[1]为写而打开,fd[1]的输入是fd[0]的输出;

int Pipecommand(char* buf)
{
    int fd[2];
    int i;
    int pos;
    pid_t pid;
    
    for(i=0;i<strlen(buf);i++)
    {
        if(buf[i]=='|'&&buf[i+1]==' ')
        {
            pos = i;
            break;
        }
    }
    char readpipe[buffsize];
    memset(readpipe,0,buffsize);
    char writepipe[buffsize];
    memset(writepipe,0,buffsize);
    for(i=0;i<strlen(buf);i++)
    {
        if(i<pos)
        {
            writepipe[i] = buf[i];
        }
        else if(i>pos)
        {
            readpipe[i-pos-1] = buf[i];
        }
    }
    writepipe[pos]='\0';
    readpipe[strlen(buf)-pos-1] = '\0';


    if (pipe(fd) < 0) {
        printf("pipe failed\n");
        exit(1);
    }

    pid = fork();
    if (pid < 0) {
        printf("创建子进程失败\n");
        exit(1);
    }
    if(pid==0){
        close(fd[0]); //关闭写功能
        close(1);	//关闭标准输出
        dup(fd[1]);//fd[1]管道写入端,映射到标准输出1
        close(fd[1]);	//用不到fd[1]
        parseline(writepipe);
        execvp(argv[0], argv);
        return 0;
    }else{
        int status;
        waitpid(pid, &status, 0);       // 等待子进程返回
        int err = WEXITSTATUS(status);  // 读取子进程的返回码
        if (err) { 
            printf("Error: %s\n", strerror(err));
        }

        close(fd[1]);
        close(0);
        dup(fd[0]);//fd[0]管道读入端,映射到标准输入0
        close(fd[0]);
        parseline(readpipe);
        execvp(argv[0], argv);
        return 0;
    }

}

后台运行

之前介绍执行程序时,提到先fork子进程,在子进程中exec,然后父进程等待子进程结束后再返回。

但是对于后台命令,则不希望shell一直等待子进程结束,而是忽略他,让他在后台慢慢运行: 为了屏蔽键盘和控制台,子进程的标准输入、输出映射成
/dev/null。子进程调用signal(SIGCHLD,SIG_IGN),作用是令父进程忽略sigchild的信号,从而使minix接管此进程。因此Shell可以避免调用wait/waitpid直接运行下一条命令。

if((pid=fork())==-1)
        {
            printf("子进程创建失败\n");
            return ;
        }
        else if(pid == 0)
        {   
            if(bg_flag)
            {
                int fd1 = open("/dev/null", O_RDONLY);
                dup2(fd1, 0);
                dup2(fd1, 1);
                dup2(fd1, 2);
                signal(SIGCHLD, SIG_IGN);
            }
            execvp(argv[0], argv);
            printf("%s: 命令输入错误\n", argv[0]);
            exit(1);

        }
        else
        {
            if(bg_flag)
            {
                printf("[process id %d]\n", pid); 
                return ;       //若为后台程序,则输出进程号
            }
            int status;
            waitpid(pid, &status, 0);       // 等待子进程返回
            int err = WEXITSTATUS(status);  // 读取子进程的返回码
            if (err) { 
                printf("Error %d: %s\n", errno,strerror(err));
            }
        }

Ⅲ.内置命令

cd

int CD(int argc)
{
    int res ;
    if(argc == 2) 
    {
        if(!chdir(argv[1])) res = 1;
        else res = 0;
    }
    else{
        res = 0;
    }

    if(getcwd(curPath, buffsize)==NULL)
    {
        printf("getcwd failed\n");
        res =0;
    }          
    else printf("%s\n",curPath);
    return res;    
}

exit

int main()
{   
    while(1)
    {
        printf("%s",prompt);
        if(getcommand(buf))
        {   
            if(strcmp(buf,"exit")==0)
            {   
                exit(0);	//输入exit则退出
                break;
            }
            else
            {
                strcpy(history[cmd_num],buf);
                cmd_num++;
                parseline(buf);
                eval(argc,argv);                
            }
        }
    }
    return 0;
}

history

int HISTORY(int argc)
{   
    if(argc!=2) 
    {
        printf("history failed\n");
        return 0;
    }
    int res = 1;
    int n = atoi(argv[argc-1]);
    int i;
    for (i = n; i > 0; i--) {
        if( cmd_num - i < 0)
        {   
            continue;
        }
        printf("%s\n", history[cmd_num - i]);
    }
    return res;
}

mytop

mytop命令输出:

  1. 总体内存大小,空闲内存大小,缓存大小。
  2. 总体CPU使用占比。计算方法:得到进程和任务总数量total_proc,对每一个proc的ticks累加得到总体ticks,再计算空闲的ticks,最终可得到CPU使用百分比。

/proc/meminfo中, 查看内存信息,每个参数对应含义依次是页面大小pagesize,总页数量total , 空闲页数量free ,最大页数量largest ,缓存页数量cached 。可计算内存大小: (pagesize * total)/1024,同理算出其他页内存大小。

/proc/kinfo中,查看进程和任务数量

/proc/pid/psinfo中,例如 /proc/107/psinfo文件中,查看pid为107的进程信息。每个参数对应含义依次是:版本version,类型type,端点endpt,名字name,状态state,阻塞状态blocked,动态优先级priority,滴答ticks,高周期highcycle,低周期lowcycle,内存memory,有效用户ID effuid,静态优先级nice等。其中会用到的参数有:类型,状态,滴答。进程时间time=ticks/ (u32_t)60。

void mytop() {
    FILE *fp = NULL;                   
    char buff[255];

    fp = fopen("/proc/meminfo", "r");   // 以只读方式打开meminfo文件
    fgets(buff, 255, (FILE*)fp);        // 读取meminfo文件内容进buff
    fclose(fp);

    // 获取 pagesize
    int i = 0, pagesize = 0;
    while (buff[i] != ' ') {
        pagesize = 10 * pagesize + buff[i] - 48;
        i++;
    }

    // 获取 页总数 total
    i++;
    int total = 0;
    while (buff[i] != ' ') {
        total = 10 * total + buff[i] - 48;
        i++;
    }

    // 获取空闲页数 free
    i++;
    int free = 0;
    while (buff[i] != ' ') {
        free = 10 * free + buff[i] - 48;
        i++;
    }

    // 获取最大页数量largest
    i++;
    int largest = 0;
    while (buff[i] != ' ') {
        largest = 10 * largest + buff[i] - 48;
        i++;
    }

    // 获取缓存页数量 cached
    i++;
    int cached = 0;
    while (buff[i] >= '0' && buff[i] <= '9') {
        cached = 10 * cached + buff[i] - 48;
        i++;
    }



    // 总体内存大小 = (pagesize * total) / 1024 单位 KB
    int totalMemory  = pagesize / 1024 * total;
    // 空闲内存大小 = pagesize * free) / 1024 单位 KB
    int freeMemory   = pagesize / 1024 * free;
    // 缓存大小    = (pagesize * cached) / 1024 单位 KB
    int cachedMemory = pagesize / 1024 * cached;

    printf("totalMemory  is %d KB\n", totalMemory);
    printf("freeMemory   is %d KB\n", freeMemory);
    printf("cachedMemory is %d KB\n", cachedMemory);


    unsigned int procsnum, tasksnum;
    int total_n;
    if ((fp = fopen("/proc/kinfo", "r")) == NULL)
    {
        fprintf(stderr, "opening /proc/kinfo failed\n");
        exit(1);
    }

    if (fscanf(fp, "%u %u", &procsnum, &tasksnum) != 2)
    {
        fprintf(stderr, "reading from /proc/kinfo failed");
        exit(1);
    }

    fclose(fp);
    

    total_n = (int)(procsnum + tasksnum);

   
    DIR *d;
    struct dirent *dir;
    d = opendir("/proc");
    int totalTicks = 0, freeTicks = 0;
    if (d) {
        while ((dir = readdir(d)) != NULL) {                   // 遍历proc文件夹
            if (strcmp(dir->d_name, ".") != 0 && 
                strcmp(dir->d_name, "..") != 0) {
                    char path[255];
                    memset(path, 0x00, 255);
                    strcpy(path, "/proc/");
                    strcat(path, dir->d_name);          // 连接成为完成路径名
                    struct stat s;
                    if (stat (path, &s) == 0) {
                        if (S_ISDIR(s.st_mode)) {       // 判断为目录
                            strcat(path, "/psinfo");

                            FILE* fp = fopen(path, "r");
                            char buf[255];
                            memset(buf, 0x00, 255);
                            fgets(buf, 255, (FILE*)fp);
                            fclose(fp);

                            // 获取ticks和进程状态
                            int j = 0;
                            for (i = 0; i < 4;) {
                                for (j = 0; j < 255; j++) {
                                    if (i >= 4) break;
                                    if (buf[j] == ' ') i++;
                                }
                            }
                            // 循环结束, buf[j]为进程的状态, 共有S, W, R三种状态.
                            int k = j + 1;
                            for (i = 0; i < 3;) {               // 循环结束后k指向ticks位置
                                for (k = j + 1; k < 255; k++) {
                                    if (i >= 3) break;
                                    if (buf[k] == ' ') i++;
                                }
                            }
                            int processTick = 0;
                            while (buf[k] != ' ') {
                                processTick = 10 * processTick + buff[k] - 48;
                                k++;
                            }
                            totalTicks += processTick;
                            if (buf[j] != 'R') {
                                freeTicks += processTick;
                            }
                        }else continue;
                    }else continue;
                }
        }
    }
    printf("CPU states: %.2lf%% used,\t%.2lf%% idle\n",
           (double)((totalTicks - freeTicks) * 100) / (double)totalTicks,
           (double)(freeTicks * 100) / (double)totalTicks);
    return;
}

Ⅳ. 结果测试

在这里插入图片描述在这里插入图片描述在这里插入图片描述
完整代码:TianQingyuan/2022/OS/code

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值