Linux实现简易版Shell的代码详解

本篇文章,我们将一起踏上一段有趣的旅程,仿照CentOS – Bash的工作流程,实现一个功能虽然简单,但足以让你深刻理解 Shell 工作原理的迷你 Shell,g感兴趣的小伙伴跟着小伙伴一起来看看吧

目录
  • 一、程序流程分析
  • 二、代码实现
    • 1. 打印命令行提示符
    • 2. 获取用户输入的命令行
    • 3. 命令行解析
    • 4. 执行命令
      • 内建命令的处理
      • cd的简易实现
  • 三、测试
    • 四、程序全部代码
      • 总结

        一、程序流程分析

        我们日常使用Bash时,通过输入命令执行相应的操作,比如:

        那么,Bash是如何进行工作的呢?观察一下,就会发现,首先Bash会打印命令行提示符,包括当前用户、主机名以及路径。之后会等待我们输入相关命令,然后根据命令执行相应程序。程序执行结束后,就会再次打印命令行提示符,等待我们再次输入指令…很明显是一个死循环。

        总结一下,Bash的大体工作流程是:

        1.打印命令行提示符(包括当前用户名、主机名、路径)
        2.获取用户输入的命令行
        3.解析命令行
        4.执行命令
        5.继续打印命令行提示符…

        注意:当Bash执行非内建命令时,会创建一个子进程,由子进程完成相应的工作,Bash自己等待子进程工作结束。而对于内建命令(如cd,echo),需要Bash自己执行任务。

        二、代码实现

        接下来,我们开始按照上述工作流程,一步步实现我们的简易Shell。

        1. 打印命令行提示符

        CentOS的命令行提示符主要包含三个内容:当前用户名、主机名和当前所在路径。 之前学习Linux环境变量时,我们了解到环境变量(USER、HOSTNAME、PWD)中存储着这些内容。所以我们使用getenv函数获取环境变量相应的值。

        代码实现:

        #include <iostream>
        #include <cstdio>
        #include <cstring>
        #include <cstdlib>
        #include <unistd.h>
        #include <sys/types.h>
        #include <sys/wait.h>
        
        //获取当前用户名
        const char* GetUserName()
        {
            const char* name = getenv("USER");
            return name == nullptr ? "none" : name;
        }
        
        //获取当前主机名
        const char* GetHostName()
        {
            const char* name = getenv("HOSTNAME");
            return name == nullptr ? "none" : name;
        }
        
        //获取当前工作路径
        const char* GetPwd()
        {
            const char* pwd = getenv("PWD");
            return pwd == nullptr ? "none" : pwd;
        }
        

        接下来,调用这些函数,形成一串命令行提示符并打印:

        //创建并打印命令行提示符
        void PrintCommandPrompt()
        {
            char CommandPrompt[1024];
            //这里为了区分Bash,用大括号
            snprintf(CommandPrompt, sizeof CommandPrompt,"{%s@%s %s}@ " , GetUserName(), GetHostName(), GetPwd());
            std::cout << CommandPrompt << std::flush; // 打印并刷新缓冲区
        }
        

        注意这里打印结束后,由于没有换行,所以缓冲区可能不会刷新,导致命令行提示符没有出现在屏幕上。因此这里我们需要主动刷新缓冲区。

        进行测试:

        int main()
        {
            PrintCommandPrompt();
            return 0;
        }
        

        运行结果:

        可以看到,用户名和主机名都正常打印,但在我们写的shell中,当前所在路径是绝对路径,太过冗长,所以可以对生成的路径进行一些处理:

        //创建并打印命令行提示符
        void PrintCommandPrompt()
        {
            char CommandPrompt[1024];
            
            //处理当前工作路径
            std::string pwd = GetPwd();
            if(pwd != "/") // 如果是根目录,则直接输出
                pwd = pwd.substr(pwd.rfind('/') + 1); // 查找最后一个"/",并从下一个位置开始分割
        
            //这里为了区分Bash,用大括号
            snprintf(CommandPrompt, sizeof CommandPrompt,"{%s@%s %s}@ " , GetUserName(), GetHostName(), pwd.c_str());
            std::cout << CommandPrompt << std::flush; // 打印并刷新缓冲区
        }
        

        运行结果:

        2. 获取用户输入的命令行

        在Bash输入命令时,往往会带上一些选项,并用空格隔开。因此,使用scanf或cin读入时会以空格作为分隔符,达不到想要的效果。这里我们选择用fgets进行读取。

        另外,主函数当中,命令的全部处理应该放在一个死循环当中,这样才能完成用户多次派发的任务。

        //获取用户输入的命令行
        bool GetCommandLine(char* command, int size)
        {
            //从键盘读取命令
            if(fgets(command, size, stdin) == nullptr)
            {
                return false;
            }
        
            //注意清理末尾的'\n'
            if(strlen(command) == 0) return false;
            command[strlen(command) - 1] = '\0';
            return true;
        }
        
        int main()
        {
            while(true)
            {
                //打印命令行提示符
                PrintCommandPrompt();
        
                //读入命令
                char command[1024];
                if(!GetCommandLine(command, sizeof command)) // 若读取错误,就continue重新读取
                {
                    std::cout << "读取错误" << std::endl;
                    continue;
                }
        
                //打印输入的命令行
                std::cout << command << std::endl;
            }
            return 0;
        }
        
        

        注意:使用fgets从键盘读取字符串时会附带末尾’\n’,需要进行处理。

        运行测试:

        可以看到,程序成功读入了我们的命令(包括空格),并且将命令回显出来。

        3. 命令行解析

        命令行解析的过程中,需要将用户读入的命令行进行分割,提取出要执行的程序名以及选项。这里我们创建两个全局变量g_argc和g_argv,分别存储解析到的命令行参数以及参数个数,方便后续指令的执行。

        注:这里的命令行分割操作由strtok函数完成。

        代码实现:

        //全局变量存储命令行参数及其个数
        int g_argc = 0;
        char* g_argv[128];
        
        //命令行解析
        bool CommandParse(char* command)
        {
            g_argc = 0;
            for(char* p = strtok(command, " "); p != nullptr; p = strtok(nullptr, " "))
            {
                g_argv[g_argc++] = p;
            }
            return g_argc == 0 ? false : true;
        }
        
        int main()
        {
            while(true)
            {
                //打印命令行提示符
                PrintCommandPrompt();
        
                //读入命令
                char command[1024];
                if(!GetCommandLine(command, sizeof command)) // 若读取错误,就continue重新读取
                {
                    std::cout << "输入错误" << std::endl;
                    continue;
                }
                // //打印输入的命令行
                // std::cout << command << std::endl;
        
                //命令行解析
                if(!CommandParse(command))
                {
                    std::cout << "命令行解析失败" << std::endl;
                    continue;
                }
                //打印解析结果
                for(int i = 0; i < g_argc; i++)
                {
                    std::cout << g_argv[i] << std::endl;
                }
            }
            return 0;
        }
        

        测试结果:

        程序成功地按照空格将我们输入的命令行参数提取了出来。接下来,根据提取到的参数,就可以执行相关指令了。

        4. 执行命令

        对于非内建命令,Bash会创建子进程,并让子进程执行;而对于内建命令,则是由Bash自己执行。因此,执行命令之前,需要先判断该命令是否是内建命令,然后进行相应的操作。

        为什么会有内建命令?

        • 效率: 执行内建命令通常比执行外部命令更快,因为避免了创建新进程的开销。
        • Shell 功能: 许多内建命令直接操作 Shell 的内部状态,例如改变当前工作目录 (cd)、设置环境变量 (export)、控制 Shell 行为等,这些功能如果作为外部命令实现会更加复杂或不可能。
        • 基本操作: 一些非常基础和常用的操作需要作为内建命令提供,以确保 Shell 的基本功能可用

        内建命令的处理

        在Bash当中,可以使用type命令判断一个命令是否是内建命令,例如:

        当然,除了cd和echo命令,还有printf、help等内建命令。本次实现中,为了能够让大家深刻理解shell运行原理,同时降低实现难度,博主就只针对cd和echo这两个内建命令进行简易实现。

        内建命令的检查和处理:

        //内建命令处理
        bool CheckAndExecBuiltin()
        {
            //取出命令行参数表的首元素,判断是否为内建命令,如果是,则直接执行
            std::string str = g_argv[0];
            if(str == "cd")
            {
                Cd();
                return true;
            }
            else if(str == "echo")
            {
                Echo();
                return true;
            }
            //else if(...)
            return false; // 不是内建命令,直接返回
        }
        
        int main()
        {
            while(true)
            {
                //打印命令行提示符
                PrintCommandPrompt();
        
                //读入命令
                char command[1024];
                if(!GetCommandLine(command, sizeof command)) // 若读取错误,就continue重新读取
                {
                    std::cout << "输入错误" << std::endl;
                    continue;
                }
                // //打印输入的命令行
                // std::cout << command << std::endl;
        
                //命令行解析
                if(!CommandParse(command))
                {
                    std::cout << "命令行解析失败" << std::endl;
                    continue;
                }
                // //打印解析结果
                // for(int i = 0; i < g_argc; i++)
                // {
                //     std::cout << g_argv[i] << std::endl;
                // }
        
                //内建命令的处理
                if(CheckAndExecBuiltin())
                {
                    continue; // 是内建命令,执行完毕就回去重新打印提示符
                }
        
                //不是内建命令,由子进程处理
            }
            return 0;
        }
        

        cd的简易实现

        cd的功能是改变当前工作路径。如果创建子进程,其只能修改它自己的工作路径,而无法修改Bash的工作路径。因此cd操作需要Bash亲自完成。 我们获取到命令行参数后,可以通过调用chdir函数实现:

        void Cd()
        {
            std::string dst;
            if(g_argc == 1 || g_argv[1] == std::string("~")) // 处理进入家目录的情况
            {
                dst = GetHome();
                if(dst == "") return;
            }
            else
            {
                dst = g_argv[1];
            }
            chdir(dst.c_str());
        }
        

        测试结果:

        可以看到,使用cd后,当前路径貌似并没有发生改变。为什么呢?实际上chdir确实起到了效果,但是我们的命令行提示符中,当前工作路径是从环境变量中获取的,环境变量中的PWD并没有发生改变。 因此,修改当前工作路径之后,要顺带着修改环境变量PWD的值。其次,当前工作路径改变后,修改环境变量之前,要获取到当前工作路径,就需要使用getcwd函数。进行键值处理后,使用putenv修改环境变量。

        另外要注意:许多putenv函数的实现(特别是 POSIX 标准的实现)不会复制我们传递给它的字符串,而会直接使用传入的指针来表示新的环境变量,因此要创建一个全局变量存储PWD键值对,确保其不会被销毁。

        代码实现:

        //全局的当前工作路径
        char cwd[256];
        
        void Cd()
        {
            std::string dst;
            if(g_argc == 1 || g_argv[1] == "~") // 处理进入家目录的情况
            {
                dst = GetHome();
                if(dst == "") return;
            }
            else
            {
                dst = g_argv[1];
            }
            chdir(dst.c_str());
        
            //同时修改环境变量中的当前工作路径
            char tmp[128];
            if(getcwd(tmp, sizeof tmp))
            {
                snprintf(cwd, sizeof cwd, "PWD=%s", tmp); // 键值处理
                putenv(cwd);
            }
        }
        

        测试结果:

        echo的简易实现

        echo不仅可以向屏幕打印字符串,还可以打印上一个执行的程序的退出码、以及环境变量等信息。为了确保效率以及执行的可靠性,该命令也是一个内建命令,由Bash亲自执行。

        //全局变量记录上一个程序的退出码
        int exit_code;
        
        void Echo()
        {
            std::string op = g_argv[1];
            if(op == "$?") // 打印上一个程序的退出码
            {
                std::cout << exit_code << std::endl;
                exit_code = 0;
            }
            else if(op[0] == '$') // 打印环境变量的值
            {
                //从"$"后的第一个字符开始,获取环境变量名
                std::string name = op.substr(1);
        
                //用getenv获取环境变量值
                const char* value = getenv(name.c_str());
                if(value)
                    std::cout << value << std::endl;
            }
            else // 打印字符串
            {
                std::cout << op << std::endl;
            }
        }
        

        测试结果:

        非内建命令的处理

        针对非内建命令,需要创建子进程,然后由子进程找到系统的对应指令程序的位置,直接进行进程程序替换,完成相关任务。这样我们就无需一个个实现相关指令操作了。

        为了让子进程自动查找对应程序的位置,减少我们的工作量,且使程序替换的参数与我们创建的全局命令行参数表一一对应,这里我们直接选用execvp进行程序替换。

        #include <unistd.h>
          
        int execvp(const char *file, char *const argv[])
        

        代码实现:

        //执行非内建命令
        void Execute()
        {
            //创建子进程
            pid_t id = fork();
            if(id < 0)
            {
                //创建子进程失败,直接退出
                exit(1);
            }
            else if(id == 0)
            {
                //子进程 -- 通过程序替换执行指令
                execvp(g_argv[0], g_argv);
            }
            else
            {
                //父进程 -- 等待子进程,并获取子进程的退出信息
                int status = 0;
                pid_t rid = waitpid(id, &status, 0);
                if(rid > 0) // 等待成功
                {
                    //将退出信息存入全局exit_code
                    exit_code = WEXITSTATUS(status);
                }
            }
        }
        
        int main()
        {
            while(true)
            {
                //打印命令行提示符
                PrintCommandPrompt();
        
                //读入命令
                char command[1024];
                if(!GetCommandLine(command, sizeof command)) // 若读取错误,就continue重新读取
                {
                    std::cout << "输入错误" << std::endl;
                    continue;
                }
                // //打印输入的命令行
                // std::cout << command << std::endl;
        
                //命令行解析
                if(!CommandParse(command))
                {
                    std::cout << "命令行解析失败" << std::endl;
                    continue;
                }
                // //打印解析结果
                // for(int i = 0; i < g_argc; i++)
                // {
                //     std::cout << g_argv[i] << std::endl;
                // }
        
                //内建命令的处理
                if(CheckAndExecBuiltin())
                {
                    continue; // 是内建命令,执行完毕就回去重新打印提示符
                }
        
                //不是内建命令,由子进程处理
                Execute();
            }
            return 0;
        }
        

        三、测试

        接下来,我们输入一些指令进行测试,看看我们实现的shell能否达到预期效果:

        perfect!

        四、程序全部代码

        自实现shell的全部代码如下:

        #include <iostream>
        #include <cstdio>
        #include <cstdlib>
        #include <cstring>
        #include <unistd.h>
        #include <sys/types.h>
        #include <sys/wait.h>
        
        //全局变量存储命令行参数及其个数
        int g_argc = 0;
        char* g_argv[128];
        
        //全局的当前工作路径
        char cwd[256];
        
        //全局变量记录上一个程序的退出码
        int exit_code;
        
        //获取当前用户名
        const char* GetUserName()
        {
            const char* name = getenv("USER");
            return name == nullptr ? "none" : name;
        }
        
        //获取当前主机名
        const char* GetHostName()
        {
            const char* name = getenv("HOSTNAME");
            return name == nullptr ? "none" : name;
        }
        
        //获取当前工作路径
        const char* GetPwd()
        {
            const char* pwd = getenv("PWD");
            return pwd == nullptr ? "none" : pwd;
        }
        
        //获取用户的家目录
        const char* GetHome()
        {
            const char* home = getenv("HOME");
            return home == nullptr ? "" : home;
        }
        
        //创建并打印命令行提示符
        void PrintCommandPrompt()
        {
            char CommandPrompt[1024];
            
            //处理当前工作路径
            std::string pwd = GetPwd();
            if(pwd != "/") // 如果是根目录,则直接输出
                pwd = pwd.substr(pwd.rfind('/') + 1); // 查找最后一个"/",并从下一个位置开始分割
        
            //这里为了区分Bash,用大括号
            snprintf(CommandPrompt, sizeof CommandPrompt,"{%s@%s %s}@ " , GetUserName(), GetHostName(), pwd.c_str());
            std::cout << CommandPrompt << std::flush; // 打印并刷新缓冲区
        }
        
        //获取用户输入的命令行
        bool GetCommandLine(char* command, int size)
        {
            //从键盘读取命令
            if(fgets(command, size, stdin) == nullptr)
            {
                return false;
            }
        
            //注意清理末尾的'\n'
            if(strlen(command) == 0) return false;
            command[strlen(command) - 1] = '\0';
            return true;
        }
        
        //命令行解析
        bool CommandParse(char* command)
        {
            g_argc = 0;
            for(char* p = strtok(command, " "); p != nullptr; p = strtok(nullptr, " "))
            {
                g_argv[g_argc++] = p;
            }
            return g_argc == 0 ? false : true;
        }
        
        void Cd()
        {
            std::string dst;
            if(g_argc == 1 || g_argv[1] == std::string("~")) // 处理进入家目录的情况
            {
                dst = GetHome();
                if(dst == "") return;
            }
            else
            {
                dst = g_argv[1];
            }
            chdir(dst.c_str());
        
            //同时修改环境变量中的当前工作路径
            char tmp[128];
            if(getcwd(tmp, sizeof tmp))
            {
                snprintf(cwd, sizeof cwd, "PWD=%s", tmp);
                putenv(cwd);
            }
        }
        
        void Echo()
        {
            std::string op = g_argv[1];
            if(op == "$?") // 打印上一个程序的退出码
            {
                std::cout << exit_code << std::endl;
                exit_code = 0;
            }
            else if(op[0] == '$') // 打印环境变量的值
            {
                //从"$"后的第一个字符开始,获取环境变量名
                std::string name = op.substr(1);
        
                //用getenv获取环境变量值
                const char* value = getenv(name.c_str());
                if(value)
                    std::cout << value << std::endl;
            }
            else // 打印字符串
            {
                std::cout << op << std::endl;
            }
        }
        
        //内建命令处理
        bool CheckAndExecBuiltin()
        {
            //取出命令行参数表的首元素,判断是否为内建命令,如果是,则直接执行
            std::string str = g_argv[0];
            if(str == "cd")
            {
                Cd();
                return true;
            }
            else if(str == "echo")
            {
                Echo();
                return true;
            }
            //else if(...)
            return false; // 不是内建命令,直接返回
        }
        
        //执行非内建命令
        void Execute()
        {
            //创建子进程
            pid_t id = fork();
            if(id < 0)
            {
                //创建子进程失败,直接退出
                exit(1);
            }
            else if(id == 0)
            {
                //子进程 -- 通过程序替换执行指令
                execvp(g_argv[0], g_argv);
            }
            else
            {
                //父进程 -- 等待子进程,并获取子进程的退出信息
                int status = 0;
                pid_t rid = waitpid(id, &status, 0);
                if(rid > 0) // 等待成功
                {
                    //将退出信息存入全局exit_code
                    exit_code = WEXITSTATUS(status);
                }
            }
        }
        
        int main()
        {
            while(true)
            {
                //打印命令行提示符
                PrintCommandPrompt();
        
                //读入命令
                char command[1024];
                if(!GetCommandLine(command, sizeof command)) // 若读取错误,就continue重新读取
                {
                    std::cout << "输入错误" << std::endl;
                    continue;
                }
                // //打印输入的命令行
                // std::cout << command << std::endl;
        
                //命令行解析
                if(!CommandParse(command))
                {
                    std::cout << "命令行解析失败" << std::endl;
                    continue;
                }
                // //打印解析结果
                // for(int i = 0; i < g_argc; i++)
                // {
                //     std::cout << g_argv[i] << std::endl;
                // }
        
                //内建命令的处理
                if(CheckAndExecBuiltin())
                {
                    continue; // 是内建命令,执行完毕就回去重新打印提示符
                }
        
                //不是内建命令,由子进程处理
                Execute();
            }
            return 0;
        }
        

        总结

        本篇文章,我们基于之前学习的Linux进程相关概念以及进程控制接口,实现了一个简易版的shell。本次实现让我们对Shell的运行原理有了更深刻的理解。

        以上就是Linux实现简易版Shell的代码详解的详细内容,更多关于Linux简易版Shell的资料请关注骃骐网其它相关文章!

        您可能感兴趣的文章:

        • 分享个简易版Linux服务器初始化Shell脚本
        • Linux实现自主Shell命令行解释器
        • Linux下Shell编程之sed命令的用法详解
        • Linux上读取或设置环境和shell变量的教程

        相关推荐:

        如何优化Python代码的字节码加载_通过PYTHONOPTIMIZE环境变量

        PYTHONOPTIMIZE是Python启动时控制字节码优化级别的环境变量,值为0/1/2分别对应无优化、移除assert、额外移除docstring,影响__debug__、.pyc路径及内容,需确保字节码缓存与运行时级别严格匹配。 PYTHONOPTIMIZE 环境变量是 Python 启动时控制字节码优化级别的底层开关,它直接决定 __debug__ 的值、是否移除 assert 语句、是...

        如何计算Python中代码块的执行时间_使用timeit模块或time.perf_counter

        该用 time.perf_counter() 时:只想测单次代码实际耗时,如调试算法分支、验证 API 延迟或粗略比对逻辑耗时;它返回纳秒级单调递增时间,不受系统时钟影响,是官方推荐的经过时间测量首选。 直接用 time.perf_counter() 测单次代码块最准,timeit 适合测小函数的重复执行均值——别拿 timeit 去包带 I/O 或状态变更的代码,结果会失真。 什么时候该用 ti...

        如何在Python 3.12中利用Type Alias优化测试代码_提升pytest用例可读性

        优先选 type 关键字。它更安全、IDE 跳转更准、mypy 报错更早,提升 pytest 中参数和 fixture 类型声明可读性,尤其适合复杂嵌套结构,但不支持 isinstance 直接使用。 直接用 type 关键字定义类型别名,比字符串前向引用或旧式 TypeAlias 赋值更安全、IDE 跳转更准、mypy 报错更早——但必须注意它不支持 isinstance 直接使用。 测试中类型...

        如何在Jenkins中集成Python代码质量检查_配置Pylint与Flake8插件

        Jenkins不原生支持Pylint/Flake8,需用Shell脚本调用并配合Warnings Next Generation插件等方案;Violations插件仅兼容Pylint特定格式,且新版Jenkins存在路径、环境、编码等多重兼容问题。 直接说结论:Jenkins 本身不原生支持 Pylint/Flake8,必须靠 Shell 脚本 + Violations 插件(仅对 Pylint ...

        Python数据标准化用哪个方案好_StandardScaler与MinMaxScaler详解

        StandardScaler不适用于明显偏态或含大量异常值的数据,因其依赖的均值和标准差易被离群点污染,导致正常数据被压缩到极窄范围,影响模型性能;必须训练集fit_transform、测试集transform,避免数据泄露。 StandardScaler 什么时候不能用 当你的数据明显偏离正态分布,或者存在大量异常值时,StandardScaler 的效果会变差。它依赖均值和标准差,而这两个统计...

        Python怎么减少协程切换的开销_异步代码中Await的合理摆放

        协程切换开销主要来自栈帧保存/恢复、内存分配(如__await__返回的迭代器)及调度延迟;高频小任务中尤为明显,但await已完成Task或Future时走快速路径,不实际挂起。 协程切换开销主要来自哪里 Python 的 await 不是免费的——每次遇到 await,事件循环就要保存当前协程的栈帧、调度下一个就绪协程、之后再恢复。这个过程本身不耗 CPU,但会带来内存分配(如 __await...

        CSS代码怎么编_CSS代码编写规范与高效开发技巧教程

        答案:CSS编写需掌握语法、命名规范、预处理器和响应式布局。使用BEM命名可避免冲突,Sass提升可维护性,Flexbox与Grid实现高效响应式设计,结合性能优化策略构建高质量网页样式系统。 CSS代码编写,说白了,就是给网页的骨架(HTML)穿上漂亮的衣服,让它看起来更美观、更具交互性。这其中,不仅要掌握基本的语法,更重要的是形成一套高效且易于维护的编码习惯,这直接关系到项目后续的扩展性和团队...

        怎么理解CSS浮动_CSS浮动原理与清除浮动方法详解教程

        浮动通过脱离文档流实现元素左/右对齐,导致父元素高度塌陷;常用清除方法包括clear属性、overflow设置和伪元素clearfix;现代布局中,float仍适用于文本环绕图片等简单场景,而复杂布局推荐使用Flexbox或Grid。 CSS浮动,说白了,就是让一个元素脱离常规的文档流,像浮在水面上一样,沿着其父容器的左侧或右侧移动,直到遇到父容器的边缘或者另一个浮动元素。它最早是为了实现文本环绕...

        CSS怎么注释内容_CSS代码注释方法与规范教程

        CSS注释使用/ /包裹,用于解释代码意图、禁用样式或标记待办事项,提升代码可读性与维护性,是团队协作和自我回顾的重要工具。 CSS中注释内容非常直接,你只需要使用 /* ... */ 这种多行注释格式。它允许你在代码中插入解释性文本,这些文本会被浏览器完全忽略,不会影响页面的渲染或性能。 解决方案 在CSS中,无论你想注释单行还是多行,都统一使用 /* 和 */ 来包裹你的注释内容。这种方式非常...