Linux C编程my_shell的实现

功能

  • 实现 管道(也就是 | )
  • 实现 输入输出重定向(< > >>)
  • 实现 后台运行( &)
  • 实现 tab补全 (提示:使用readline库)
  • 实现内置命令history
  • 实现光标的移动
  • 实现 内建命令(cd )
  • 屏蔽一些信号(如ctrl + c 不能终止)
  • 通过设置环境变量 可以直接使用 (在任意地方都可以运行你的shell)
  • 界面美观
  • 输入exit退出myshell

错误处理:

  • 输入错误的命令格式报错
  • 输入不存在的命令报错

流程图

在这里插入图片描述

完整代码

github
在这里插入图片描述

命令功能讲解

1.普通命令

普通命令及常用的一些命令,如:

  • ls
  • cd 目录
  • mkdir
  • touch a.c
2.重定向符

输出重定向:

  • 符号>代表以覆盖的方式将命令的正确输出输出到指定的文件或设备当中。
  • 符号>>代表以追加方式输出。

例如:ls > text.txt

其中 ls 为命令, > 为输出重定向符, text.txt为文件。
结果为将ls的结果覆盖到text.txt文件中。
如果ls结果为:
1.c  3.c  a.out  c    e    text
2.c  a.c  b.c    c.c  e.c  text.txt
则text.txt文件中的内容为:
1.c
2.c
3.c
a.c
a.out
b.c
c
c.c
e
e.c
text
text.txt

输入重定向:

  • 命令<文件名:把文件作为命令的输入,例如wc命令时统计行,单词书和字符的。、

例如:wc < text.txt

该命令将text.txt作为命令wc的输入,wc该命令统计给定文件中的字节数、字数、行数。
输出结果为:
12 12 52
3.管道符

命令格式:命令A|命令B,即命令A的正确输出作为命令B的操作对象。

execvp函数

想要实现一个普通的命令我们用的是execvp函数,该函数原型为:

 int execvp(const char* file, const char* argv[]);
  • 第一个参数是要运行的文件,会在环境变量PATH中查找file,并执行.

  • 第二个参数,是一个参数列表,如同在shell中调用程序一样,参数列表为0,1,2,3……因此,wensen.sh 作为第0个参数,需要重复一遍.

  • argv列表最后一个必须是 NULL.

  • 失败会返回-1, 成功无返回值,但是,失败会在当前进程运行,执行成功后,直接结束当前进程,可以在子进程中运行.

例如:我们简单实现一个ls命令

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>

void main()
{
    char *arg[2];
    char *argv[2];
    argv[0] = "ls";
    argv[1] = NULL;
    arg[0] = (char *)argv[0];
    arg[1] = NULL;
    execvp(arg[0], arg);
}

看看下面代码,你猜会输出什么呢?

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>

void main()
{
    char *arg[2];
    char *argv[2];
    argv[0] = "ls";
    argv[1] = NULL;
    arg[0] = (char *)argv[0];
    arg[1] = NULL;
    execvp(arg[0], arg);

    printf("\nMy_shell\n");
}

这两个程序运行的结果都一样,因为execvp函数运行失败会返回-1, 成功无返回值,但是,失败会在当前进程运行,执行成功后,直接结束当前进程,所以当成功运行execvp后主进程被结束了,就无法执行语句 printf("\nMy_shell\n");

多进程

我们要执行输入的命令就必须调用execvp函数,但该函数运行成功后会结束它所在的进程,在流程图中我们可以看见需要有while(1)循环输入命令,直到输入exit或logout退出程序。所以execvp函数不能在主进程使用,否则将提前终止程序。这就要在子进程中使用该函数。

创建进程

创建进程需要使用fork函数或者使用vfork函数,它们的函数声明为:

#include<sys/types.h>
#include <unistd.h>
pid_t fork(void);
pid_t vfork(void);

fork函数创建进程成功后,fork函数返回两次:一次返回值为0代表子进程正在运行,另一次返回所创建的新进程的ID,如果创建进程失败,则返回-1;如下

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    pid_t pid;

    printf("进程学习\n");
    pid = fork();
    switch(pid)
    {
        //getpid函数和getppid函数分别用于获得所在进程ID和所在进程父进程的ID
        case 0:
            printf("子进程正在运行,fork返回值为:%d, 父进程ID为 %d,子进程ID为 %d\n",pid,getppid(),getpid());
            break;
        case -1:
            perror("Peocess creation failed\n");
            break;
        default:
            printf("父进程正在运行,fork返回值为:%d,父进程ID为 %d\n",pid, getpid());
            break;
    }
    exit(0);
}

fork和vfork的区别:这两个函数都可以创建新进程,但有一些区别。

  • vfork与fork一样都是调用一次,返回两次。
  • 使用fork创建进程时,子进程只是完全复制父进程的资源。这样的到的子进程独立于父进程具有良好的并发性。而使用vfork创建一个子进程时,操作系统并不将父进程的地址空间完全复制到子进程,用vfork创建的子进程共享父进程的地址空间,也就是说子进程完全运行在父进程的地址空间上。子进程对该地址空降中任何数据的修改同样为父进程所见。
  • 使用fork创建子进程时,哪个进程先运行取决于系统的调度算法。而vfork创建一个进程时,vfork保证子进程先运行,当它调用exec或exit之后,父进程才可能被调度运行。如果在调用exec或exit之前要依赖父进程的某行为,就会导致死锁。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int globVar = 5;

int main()
{
   pid_t pid;
   int var = 1;

   printf("fork与vfork \n");

   //pid = fork();
   pid = vfork();
   switch(pid)
   {
       case 0:
           printf("子进程运行中\n");
           globVar = globVar +5;
           var = var + 5;
           printf("Child's globVar = %d,var = %d\n",globVar,var);
           exit(0);
       case -1:
           perror("创建失败\n");
           exit(0);
       default :
           printf("父进程运行中\n");
           globVar = globVar + 5;
           var = var + 5;
           printf("Parent's globVar = %d ,var = %d\n",globVar ,var);
           exit(0);
   }
}

命令实现

常用普通命令如ls已经在execvp函数时讲过了,我们说一说其他的命令实现吧!

输出重定向符

命令: ls > text.txt (单个输出重定向符)
将ls的结果覆盖到文件text.txt上
代码:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<dirent.h>

int main()
{
	char *A[5];
	char *arg[3];
	arg[0] = A[0] = "ls";
	arg[1] = NULL;
	int fp = open("text.txt",O_RDWR|O_CREAT|O_TRUNC, 0644);     //打开文件
	dup2(fp,1);                                             //文件描述符改为1(输出)
	execvp(arg[0], arg);                                    //注意在shell中必须在子进程中执行
	return 0;
}

arg并不像普通命令一样把命令全存入,

ls -l 时arg[0] = “ls”arg[1] = “-l”arg[2] = NULL
ls > text 时arg[0] = “ls”arg[1] = NULL

ls 后的部分由open函数和dup2函数完成。

命令:ls -l >> text.txt (两个输出重定向符)
将ls -l 的输出以追加的形式加入text.txt文件中。
代码:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<dirent.h>

int main()
{
    char *A[5];
	char *arg[3];
	arg[0] = A[0] = "ls";
    arg[1] = "-l";
	arg[2] = NULL;
	int fp = open("text.txt",O_RDWR|O_CREAT|O_APPEND);     //打开文件
	dup2(fp,1);                                             //文件描述符改为1(输出)
	execvp(arg[0], arg);                                    //注意在shell中必须在子进程中执行
	return 0;
}

在这里插入图片描述
在这里插入图片描述

这里主要是运用了open函数的参数所赋予权限的不同来实现的:

输入重定向符

命令: wc < text
该命令将文件text作为wc命令的对象
代码:

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<dirent.h>

int main()
{
	char *A[5];
	char *arg[3];
	arg[0] = A[0] = "wc";
	arg[1] = NULL;
	int fp = open("text",O_RDONLY);
	dup2(fp,0);
	execvp(arg[0], arg);           //注意在shell中必须在子进程中执行
	return 0;
}

同上arg并不像普通命令一样把命令全存入,

ls -l 时arg[0] = “ls”arg[1] = “-l”arg[2] = NULL
wc < text 时arg[0] = “wc”arg[1] = NULL

wc 后的部分由open函数和dup2函数完成。

管道符

命令:ls | wc
该命令将ls的输入做为wc的输出,该命令相当与在shell中执行了两个命令,所以要调用两次execvp函数,但调用一次该函数,它所在的进程都会结束,所以要创建两个新进程,每一个进程中调用一次,而且调用第一次是将输出结果保存到文件text中,以做为第二次调用时的输入。
代码:

        //输入的命令中含有管道符|
        if(pid == 0)                        //在主进程(A)的子进程(B)中
        {
            int pid2;
            int status2;
            int fd2;

            if( (pid2 = fork()) < 0 )      //在进程(B)中创建子进程(C)
            {
                printf("fork2 error\n");
                return ;
            }
            else if(pid2 == 0)
            {
                if( !(find_command(arg[0])) )
                {
                    printf("%s : command not found\n", arg[0]);
                    exit(0);
                }
                fd2 = open("text", O_WRONLY|O_CREAT|O_TRUNC, 0644);
                dup2(fd2, 1);
                execvp(arg[0], arg);                 //第一次调用
                exit(0);
            }
            if(waitpid(pid2, &status2, 0) == -1)
            {
                printf("wait for child process error\n");
            }
            if( !(find_command(argnext[0])) )
            {
                printf("%s : command not found\n", argnext[0]);
                exit(0);
            }
            fd2 = open("text", O_RDONLY);
            dup2(fd2, 0);
            execvp (argnext[0], argnext);         //第二次调用
            if( remove("text"))
            {
                printf("remove error\n");
            }
            exit(0);
        }
        break;

        default:
        break;

屏蔽ctrl + c

在main函数中加入下面代码就可以屏蔽ctrl + c杀死程序.

signal(SIGINT, SIG_IGN);

错误输入

在这里我们使用find_command函数来判断输入命令是否正确

//查找命令中的可执行程序
int find_command(char *command)
{
    DIR *dp;
    struct dirent *dirp;
    char *path[] = {"./", "/bin", "/usr/bin", NULL};

    //使当前目录下的程序可以运行,如命令“./fork”可以被正确解释和执行
    if( strncmp(command, "./", 2) == 0 )
    {
        command = command + 2;
    }

    //分别在当前目录,/bin和/usr/bin目录查找要执行的程序
    int i = 0;
    while(path[i] != NULL)
    {
        if( (dp= opendir(path[i])) ==NULL )
        {
            printf("can not open /bin \n");
        }
        while( (dirp = readdir(dp)) != NULL )
        {
            if(strcmp(dirp->d_name, command) == 0)
            {
                closedir(dp);
                return 1;
            }
        }
        closedir(dp);
        i++;
    }
    return 0;
}

readline

光标移动tab补全历史命令都可以用readline库中的函数解决,在我的另一篇博客有详细讲解
myshell中readline的使用

环境变量

为了在任何地方都可以运行你的shell需要设置一下环境变量。
首先进入.bashrc文件(终端操作)

vim .bashrc

进入后在里面加入下面内容(里面的/home/ycl/code是我my_shell.c文件的位置)

export PATH=/home/ycl/code:$PATH    
  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值