从头实现Linux指令(一)实现自己的shell

本文介绍了如何从零开始实现一个简单的Linux shell,包括读取用户输入、解析命令、内部命令(如exit和cd)、外部命令执行、读取脚本文件、重定向及并发执行。项目涵盖了REPL循环、系统调用如fork()和execv()、文件操作及进程间通信。此外,还提供了测试与调试方法,并分享了作者对学习Linux开发的感悟。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文从零实现Linux指令系列文章,其他系列文章:

从头实现Linux指令(零)序言

从头实现Linux指令(一)实现自己的shell

从头实现Linux指令(一续)实现自己的shell代码解析

从头实现Linux指令(二)word counting

shell概述

熟悉linux的人应该都对shell有一个感性的认识,大概知道它是什么~我们还可以用一些更加精确的语言给它一个定义:命令行解释器( CLI) 或shell是代表其用户运行其他程序的程序。shell 反复打印提示例如$,等待用户输入,然后执行用户的操作。不用shell的linux是没有灵魂的
用户输入的命令行是由空格分隔的 ASCII 单词序列。命令行中的第一个单词要么是内置命令的名称,要么是可执行文件的名称。剩下的词是命令行参数。
linux发行版例如ubuntu自带的shell一般是bash,除了bash以外,还有一些很好用的shell工具,这里强推zsh。

本项目希望能够实现一个迷你的shell,功能也许不强大,但应该具有基本的shell功能,例如ls, cd, pwd, path, 并发执行以及重定向等功能。本文首先介绍各模块的实现,最后说明如何运行并调试本项目。

完全掌握整个项目代码的时间:30h

开始使用 Shell 项目

如果有厉害的大手子想直接审查一下笔者写的代码,可以直接访问github:https://github.com/SFUMECJF/linux-command

0 练手任务

斐波那契数列大家都知道是什么,不过我们这里使用c语言中的fork() / wait()以递归方式编写。每个子进程将其结果返回给父进程,父进程一直等到子进程完成。那么通过这个练习要掌握父子进程通信的方式。
如果不限制必须使用多进程 的方式的话,实现是很简单的:

int helper(int n) {
    if (n == 0)
       return 0;
    else if (n == 1)
        return 1;
    else {
        return helper(n - 1) + helper(n - 2);
    }
}

那么用进程怎么办呢?
实现思路如下:

  1. 每次递归调用函数的时候,都开一个新进程调用递归函数。
  2. 父进程给子进程可以直接传参,子进程给父进程的返回值这里利用exit()函数,也即如果子进程调用 exit(0)结束,则父进程可以通过wait(&return_value)等待子进程结束,然后再利用sum += WEXITSTATUS(return_value);将相应子进程的返回值收集到我们想要的结果上。

实现结果:

make fib #编译
unix> fib 3
2
unix> fib 10
55

1 shell 骨架

本节练习系统调用,strtok、strcmp和execv,完成的目标是解析输入的指令以及实现内部指令。

1.1 REPL

任何 shell 的核心都是 REPL,即read-evaluate-print loop。这是一个重复执行以下三个动作的循环:

  1. 从用户(或从用户指定的脚本)读取输入。
  2. 评估输入,弄清楚用户想要做什么并去做。
  3. 打印相关的任何输出。

本项目在行首循环打印utcsh>,然后使用getline读取用户的输入到c语言的字符串数组。读取数组之后,使用strtok对数组按照空格拆分,就可以获取每个单独的单词了~也就是命令以及参数。

1.2 内部命令

  1. exit:该指令退出shell程序。使用系统调用exit(0)完成

  2. cd:cd总是只接受一个参数。使用系统调用chdir()完成。

  3. path:path命令接受零个或多个参数,每个参数由空格分隔。典型的用法可能如下所示:utcsh> path /bin /usr/bin需要在代码内部维护一个二维字符串数组,每次更新的路径放到数组中。路径是为了在执行外部命令时能够找到相应的可执行程序。

1.3 外部命令

如果给出的命令不是三个内置命令之一,则应将其视为外部可执行程序。

对于这些外部命令,使用 fork-and-exec 方法执行程序。每一个外部指令,都利用系统调用execv()调用一个子进程来执行。这里execv如何能够找到外部可执行程序呢,要靠我们上面维护的路径二维数组。

对于父进程:父进程应该使用wait()或waitpid()等待子进程。

1.4 读入shell脚本文件

除了手动输入命令之外,还需要能够处理文件中的大量脚本。这里练习的文件操作,打开文件,读取一行,注意这里不是从标准输入读取,而是从文件中读取。

1.5 重定向

很多时候,shell 用户更喜欢将程序的输出发送到文件而不是屏幕。shell支持使用 > 以及 < 进行输入和输出的重定向。这里使用open()以及dup1和dup2完成重定向到文件的功能。

1.6 并发执行命令

utcsh> cmd1 & cmd2 & cmd3 args1
输入以上命令时, shell 应该并发执行cmd1,cmd2和cmd3(无论传递了什么参数) ,然后等待它们中的所有命令完成。
然后,一旦所有子进程都已启动,父进程必须使用wait()或waitpid() 确保所有进程都已完成,然后再继续。

1.7 测试与运行

运行:make
调试:make debug之后使用gdb或者vscode配置好环境用ide调试
测试代码功能是否正确,总共有32个测试。make check搞定所有测试,make testcase id=3单独测试第3条。

1.8 运行截图

在这里插入图片描述

1.9 收获与想法

很久之前我做robomaster比赛,学习了CMake以及c++调用Open CV库的基本操作。但当我去字节实习面试的时候,只记得面试官很诧异地问我:啊,你的代码里都没有系统调用吗?(那时我只会打开文件,而且并不知道文件操作也属于系统调用的一部分)~
希望大家Linux开发的知识都多多的~

互相交流

读者你好!如果你对本文内容感兴趣,我十分希望能够和你互相学习,可以扫码和我联系!一起进步

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值