操作系统 | Project_ShellProject

一、目的

  理解shell程序的设计方法,系统编程,实现一个基本的shell。

二、内容与设计思想

1、shell主体

  shell主体结构是一个while循环,不断接受用户键盘输入行并给出反馈。shell将输入的命令行进行解析,根据命令名称分为两类分别处理,即shell内置命令和program命令。识别为shell内置命令后,执行对应操作。接受program命令后,利用Minix自带的程序创建一个或多个新进程,并等待进程结束。如果末尾包含&参数,则为后台任务,shell不等待进程结束,直接返回。

2、shell内置命令

(1)cd +路径名,改变工作路径

  shell本身是一个程序,启动时Minix会分配一个当前工作目录,利用chdir系统调用改变shell的工作目录,并调用getcwd函数获取当前工作目录的路径。

(2)history n,显示最近执行的n条指令

  利用一个二维数组保存shell每次输入的命令行,根据指令打印出相应数量的行。

(3)exit,shell退出

  退出shell的while循环,结束shell的main函数。

(4)mytop,输出内存使用情况和CPU使用百分比

  在minix系统/proc文件夹中通过fopen/fscanf获取进程信息。

3、program命令

(1)运行程序

  利用fork调用创建子进程,利用execvp调用加载并运行,对shell中的命令进行处理,处理完之后新进程结束,shell利用wait/waitpid调用等待进程结束并回收。

(2)重定向

重定向输入(>):
  调用open得到文件描述符fd,再调用dup2(fd,1)函数,将文件描述符映射到标准输出。最后,调用execvp运行程序。
重定向输出(<):
  调用open得到文件描述符fd,再调用dup2(fd,0)函数,将文件描述符映射到标准输入。最后,调用execvp运行程序。

(3)管道

  利用pipe函数创建一个管道fd[2],再fork一个子进程,在子进程中调用dup2(fd[1],1)函数将管道写端fd[1]映射到标准输出,调用execvp运行程序,将进程的输出写入管道。在父进程中,等待子进程结束并回收,再调用dup2(fd[0],0)函数将管道读端fd[0]映射到标准输入,从管道中读入数据,并执行。

(4)后台运行

  将子进程的标准输入、输出映射到/dev/null,屏蔽键盘和控制台。调用signal(SIGCHLD, SIG_IGN),使得Minix接管此进程,shell不用等待子进程结束直接运行下一条命令。

三、使用环境

  物理机:Windows10
  虚拟机:Minix3
  虚拟机软件:Vmware

四、实验过程

1、实现的主要函数:

函数功能
void exeCommand(char *cmdline);实现内置命令、program命令和后台运行等功能。
int parseline(const char *cmdline, char **argv);解析命令行,得到参数序列,并判断是前台作业还是后台作业
void pipeline(char *process1[],char *process2[]);实现管道,完成进程间的通信。
int builtin_cmd(char **argv);实现内置命令exit、history n、cd、mytop的具体实现。
void getkinfo();在/proc/kinfo中查看进程和任务的数量
int print_memory();在/proc/meminfo中查看内存信息,计算出内存大小并打印
void get_procs();创建struct proc数组,为每个任务分配一个struct proc结构体,保存信息
void parse_dir();读取目录下每一个文件信息,并获得进程号
void parse_file(pid_t pid);在/proc/pid/psinfo中,查看进程pid的信息,并保存每个文件对应的结构体struct proc中
u64_t cputicks(struct proc *p1, struct proc *p2, int timemode);利用时间差,计算每个任务、进程的cputicks
void print_procs(struct proc *proc1,struct proc *proc2,int cputimemode);计算总体CPU使用占比并打印结果
1)int parseline(const char *cmdline, char **argv);

  利用strtok函数,根据空格对命令行进行划分,得到argv参数序列。同时判断命令行最后是否带有参数’&’,若有则为后台作业,反之,则为前台作业。

2)void exeCommand(char *cmdline);

   · 调用parseline函数解析命令行并判断前、后台作业。
   · 调用builtin_cmd函数判断是否为内置命令,若是内置命令则执行相应的内置命令操作;若不是内置命令,则返回继续执行。
   · 判断命令行中是否包含” > ”, “ < ”, “ | ”,“&”。在利用switch语句,对每种情况做出相应的处理。
(1)命令不包含管道、重定向、后台运行
   · 调用fork函数,创建一个新的子进程
   · 在子进程中,调用execvp函数运行程序
   · 在父进程中,调用waitpid函数,父进程等待子进程结束并回收

        pid = Fork();
        if (pid == 0)
        {
            execvp(argv[0], argv);
            exit(0);
        }
        if (waitpid(pid, &status, 0) == -1)
        {
            printf("wait for child process error\n");
        }

(2)命令包含重定向输出
   · 利用参数列表,得到重定向符后的文件名file
   · 调用fork函数,创建一个新的子进程
   · 在子进程中调用open函数得到file的文件描述符fd;再调用dup2(fd, 1),将file映射到标准输出;最后,调用execvp执行重定向符前的指令
   · 在父进程中,调用waitpid函数,父进程等待子进程结束并回收

		pid = Fork();
        if (pid == 0)
        {
            fd = open(file, O_RDWR | O_CREAT | O_TRUNC, 0644);
            if (fd == -1)
            {
                printf("open %s error!\n", file);
            }
            dup2(fd, 1); 
            close(fd);
            execvp(argv[0], argv);
            exit(0);
        }
        if (waitpid(pid, &status, 0) == -1)
        {
            printf("wait for child process error\n");
        }

(3)命令包含重定向输入
   · 利用参数列表,得到重定向符后的文件名file
   · 调用fork函数,创建一个新的子进程
   · 在子进程中,调用open函数得到file的文件描述符fd;再调用dup2(fd,0),将file映射到标准输入;最后,调用execvp执行重定向符前的指令
   · 在父进程中调用waitpid函数,父进程等待子进程结束并回收

		pid = Fork();
        if (pid == 0)
        {
            fd = open(file, O_RDONLY);
            dup2(fd, 0); 
            close(fd);
            execvp(argv[0], argv);
            exit(0);
        }
        if (waitpid(pid, &status, 0) == -1)
        {
            printf("wait for child process error\n");
        }

(4)命令包含管道
   · 利用参数列表,得到管道符之前的命令参数和管道符之后的命令参数
   · 调用fork函数,创建一个新的子进程
   · 在子进程中调用辅助函数pipeline实现管道
   · 在父进程中,调用waitpid函数,父进程等待子进程结束并回收

		if((pid = fork()) < 0){
            printf("fork error\n");
            return ;
        }
        if (pid == 0)
        {
            pipeline(argv,leftargv);
        }
        else
        {
            if (waitpid(pid, &status, 0) == -1)
            {
                printf("wait for child process error\n");
            }
        }

(5)后台任务
   · 调用函数fork函数,创建一个新的子进程
   · 子进程调用signal(SIGCHLD, SIG_IGN),使Minix接管此进程;在调用open函数得到”/dev/null/”的文件描述符,并映射到标准输入输出;最后execvp执行命令

        pid = Fork();
        if (pid == 0)
        {    
            signal(SIGCHLD, SIG_IGN);
            int a = open("/dev/null", O_RDONLY);
            dup2(a, 0);
            dup2(a, 1);
            execvp(argv[0], argv);
            exit(0);
        }
3)void pipeline(char *process1[],char *process2[]);

   · 调用pipe函数创建一个管道fd[2]
   · 调用fork函数创建一个子进程
   · 在子进程中,分别调用函数close(fd[0])和close(1)关闭管道读端fd[0]和文件描述符1;再调用dup(fd[1])将管道的写端映射到标准输出;然后,调用函数close(fd[1])关闭管道写端,避免堵塞;最后,调用execvp执行管道前部分指令,其结果将输出到管道中。
   · 在父进程中,分别调用函数colse(fd[1])和close(0)关闭管道写端fd[1]和文件描述符0;再调用dup(fd[0])将管道的读端映射到标准输入;然后,调用函数close(fd[0])关闭管道读端,避免堵塞;最后,调用execvp执行管道后部分指令,从管道中读入数据。

void pipeline(char *process1[],char *process2[]){
    int fd[2];
    pipe(&fd[0]);
    int status;
    pid_t pid;
    pid=Fork();
    if(pid==0){
        close(fd[0]);
        close(1);
        dup(fd[1]);
        close(fd[1]);
        execvp(process1[0],process1);
    }else{
        close(fd[1]);
        close(0);
        dup(fd[0]);
        close(fd[0]);
        execvp(process2[0],process2);
    }
}
4)int builtin_cmd(char **argv);

(1)exit
   · exit(0),退出shell的while循环,结束shell的main函数

   if (!strcmp(argv[0], "exit"))
    { 
        exit(0);
    }

(2)cd
   · 调用chdir函数,改变工作目录
   · 调用getcwd函数,获取当前所在目录

	if (!strcmp(argv[0], "cd"))
    {
        if (!argv[1])
        {
            argv[1] = ".";
        }
        int ret;
        ret = chdir(argv[1]); //改变工作目录
        if (ret < 0)
        {
            printf("No such directory!\n");
        }
        else
        {
            path = getcwd(NULL, 0); //利用getcwd取当前所在目录
        }
        return 1;
    }

(3)history n
   · 在main函数中将输入的指令保存在二维数组history中
   · 根据参数n,打印出最近输入的n条指令;若仅输入history未带参数n则打印出所有历史指令;若n大于历史指令数量则输出错误信息history error

	if (!strcmp(argv[0], "history"))
    {
        if (!argv[1])
        { //当只输入history时,打印已有的所有指令
            for (int j = 1; j <= n_his; j++)
            {
                printf("%d ", j);
                puts(history[j - 1]);
            }
        }
        else
        {
            int t = atoi(argv[1]);
            if (n_his - t < 0)
            { //如果history后未带参数或带的参数大于已有指令数
                 printf("history error\n");
            }
            else
            {
                for (int j = n_his - t; j < n_his; j++)
                {
                    printf("%d ", j + 1);
                    puts(history[j]);
                }
            }
        }
        return 1;
    }

(4)mytop
   · 调用辅助函数getkinfo,查看进程和任务总数
   · 调用辅助函数print_memory,查看内存信息,从计算出总体内存大小、空闲内存大小、缓存大小
   · 根据进程和任务总数,分别调用两次辅助函数get_procs为每一个进程和结构体分配一个struct proc结构体,得到两个struct proc数组proc1、proc2
   · 调用辅助函数print_procs,打印出CPU使用百分比情况

	if (!strcmp(argv[0], "mytop"))
    {
        int cputimemode = 1;
        getkinfo();
        print_memory();
        //得到prev_proc
        get_procs();
        if (prev_proc == NULL)
        {
            get_procs();//得到proc
        }
        print_procs(prev_proc, proc, cputimemode);
        return 1;
    }
5)void getkinfo();

  调用fopen函数打开文件”/proc/kinfo”,读入进程和任务数量,相加得到进程和任务总数nr_total。

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

    if (fscanf(fp, "%u %u", &nr_procs, &nr_tasks) != 2)
    {
        fprintf(stderr, "reading from /proc/kinfo failed");
        exit(1);
    }
    fclose(fp);
    nr_total = (int)(nr_procs + nr_tasks);
6)int print_memory();

  调用fopen函数打开文件”/proc/meminfo”,查看内存信息,分别读入页面大小pagesize、总页数量total、空闲页数量free、最大页数量largest、缓存页数量cached。根据公式算出内存大小,同理可算出其他页内存的大小,并打印。

    if ((fp = fopen("/proc/meminfo", "r")) == NULL)
    {
        return 0;
    }
    if (fscanf(fp, "%lu %lu %lu %lu %lu", &pagesize, &total, &free, &largest, &cached) != 5)
    {
        fclose(fp);
        return 0;
    }
    fclose(fp);
    printf("main memory: %ldk total,%ldk free,%ldk contig free,%ldk cached\n", (pagesize * total) / 1024,
           (pagesize * free) / 1024, (pagesize * largest) / 1024, (pagesize * cached) / 1024);
7)void get_procs();

  根据进程和任务总数nr_total,为每个任务和进程都分配一个struct proc结构体来保存相关信息,得到一个struct proc数组proc。

		proc = malloc(nr_total * sizeof(proc[0])); //struct proc的大小
        if (proc == NULL)
        {
            fprintf(stderr, "Out of memory!\n");
            exit(0);
        }
8)void parse_dir()

  调用opendir函数打开目录”/proc”,调用readdir函数读取目录下每个文件的信息,并保存在每个文件对应的结构体struct proc中。每个文件都会得到一个struct dirent结构体的返回值储存了文件信息,结构体成员包括索引节点号、在目录文件中的偏移、文件名长、文件类型、文件名。再调用strtol函数由文件名获取进程号,最后调用parse_file函数查看进程信息。循环执行以上步骤,直至目录中的所有文件都被读取。

9)void parse_file(pid_t pid);

  调用fopen函数打开文件”/proc/pid/psinfo”,查看进程pid的信息。依次读入版本version,类型type,端点endpt,名字name,状态state,阻塞状态blocked,动态优先级priority,滴答ticks,高周期highcycle,低周期lowcycle,内存memory,有效用户ID effuid等。利用&proc[slot]获取相应的struct proc,并将信息存储在结构体中。
  若type是task则将struct proc的结构体成员p_flags倒数第二位标记为1(p->p_flags |= IS_TASK),若type是system则将p_flags倒数第三位标记为1(p->p_flags |= IS_SYSTEM)。若进程状态state不是在运行状态则将p_flags倒数第四位标记为1(p->p_flags |=BLOCKED)。
然后,调用make64(cycles_lo, cycles_hi)根据高低频计算出cputicks。最后,将p->flags最后一位标记为1,表示已访问过该进程信息(p->p_flags |=USED)。

10)u64_t cputicks(struct proc *p1, struct proc *p2, int timemode);

  (p2->p_cpucycles[i] — p1->p_cpucycles[i])计算出task i在一段时间内的cputicks

        if (p1->p_endpoint == p2->p_endpoint)
        {
            t = t + p2->p_cpucycles[i] - p1->p_cpucycles[i];
        }
        else
        { //否则t直接加上p2
            t = t + p2->p_cpucycles[i];
        }
11)void print_procs(struct proc *proc1,struct proc *proc2,int cputimemode);

  对struct proc数组中每个proc,调用函数cputicks计算他们的CPU时钟周期。并将数组中每个任务的cputicks累加得到total cputicks。在利用p_flags标记来判断是systemticks还是usertickes。最后分别除以total cputicks得到CPU使用百分比。

2、实验结果

在这里插入图片描述
完整代码与测试文件: https://github.com/RachelllYe/OSProject
欢迎探讨与指正,谢谢!

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值