【Linux】进程实践项目(更新中) — 自主shell编写

简介: 前几篇文章,我们学习进程的相关知识:进程概念,进程替换,进程控制。熟悉了进程到底是个什么事情,接下来我们来做一个实践,来运用我们所学的相关知识。这个项目就是手搓一个shell模块,模拟实现Xshell中的命令行输入。

送给大家一句话:

不管前方的路有多苦,只要走的方向正确,不管多么崎岖不平,都比站在原地更接近幸福。 —— 宫崎骏《千与千寻》

自主shell命令编写

1 前言

前几篇文章,我们学习进程的相关知识:进程概念,进程替换,进程控制。熟悉了进程到底是个什么事情,接下来我们来做一个实践,来运用我们所学的相关知识。这个项目就是手搓一个shell模块,模拟实现Xshell中的命令行输入。


用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束:

然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。

所以要写一个shell,需要循环以下过程:

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork),防止打扰主程序的运行
  4. 替换子进程(execvp),来执行对应功能。
  5. 父进程等待子进程退出(wait)

根据这些思路,和我们前面的学的技术,就可以自己来实现一个shell了

2 项目实现

为了保证项目文件的优雅美观,我们按照功能来书写不同函数:

  1. 创建自己的命令行
  2. 获取命令
  3. 分割命令
  4. 创建进程执行命令

2.1 创建命令行

该模块我们需要实现类似:

获取这些信息大家应该都知道吧!通过对环境变量我们就可以获取到这些信息。使用getenv()函数就可以完成操作。

#include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/wait.h>
  4 #include<stdlib.h>
  5 #include<unistd.h>
  6 #include<string.h>
  7 //大小宏
  8 #define SIZE 256
  9 //获取用户名
 10 const char* GetUsername() 
 11 {
 12   const char* name = getenv("USER");
 13   if(name == NULL) return "NONE";
 14   return name; 
 15 }
 16 //获取机器信息
 17 const char* GetHostName()
 18 {
 19   const char* hostname = getenv("HOSTNAME");
 20   return hostname; 
 21 }
 22 //获取当前目录
 23 const char* GetCwd()
 24 {
 25   const char* cwd = getenv("PWD");
 26   if(cwd == NULL) return "NONE";
 27   return cwd;
 28 }
 29 
 30 void MakeCommandLineAndPrint()
 31 { //设置命令行字符串
 32   char line[SIZE];
 33   const char* username = GetUsername();
 34   const char* hostname = GetHostName();
 35   const char* cwd = GetCwd();
    //将获取的三个数据写入命令行中 
 36   sprintf(line,"[%s@%s %s]> ",username,hostname,cwd);                                                                                                                         
 37   printf("%s",line);
 38   fflush(stdout);//为了将命令行刷新出来
 39 }
 40 
 41 int main()
 42 {
 43   //创建我们自己的命令行
 44   MakeCommandLineAndPrint();
 45   int a = 0; scanf("%d",&a); //阻断一下方便查看
 46   return 0;
 47 }

这里使用的sprintf()函数是向流中写入格式化信息的好工具。这一段函数大家都可以看明白,就是获取三个变量,然后通过Line数组进行中转,然后打印出来。来看效果:

这时候发现,我们的所在目录全部都别打印出来了,我们可以进行一下优化:

#define SkipPath(p) do{ p += strlen(p)-1 ;while(*p != '/') p--; p++; }while(0);

通过这个宏定义就可以只保留最后的目录。

这里之所以不使用函数,是因为使用函数会涉及二级指针,会比较复杂!!!

来看效果:

这样就非常完美了!!

2.2 获取命令

这个模块可以说是非常关键的一步了,只有正确获取了对应命令,我们才好打开新进程来执行命令。

    #define ZERO '\0'
   45 int GetUserCommand(char* command,int n)
   46 {
   47   if(command == NULL) return -1;
   48   fgets(command,n,stdin);
   49   command[strlen(command) - 1] = ZERO; 
   50   return strlen(command);
   51 }

这样我们就可以获取命令行输入的字符串了。

2.3 分割命令

获取命令之后,我们还需要对输入的一串命令来进行分割,来保证我们可以正常执行命令

   
   
   12 #define SEP " "
   ...
   14 //全局命令 方便操作
   15 char* gArgv[NUM];
   ...
   58 void SplitCommand(char command[] , size_t n)
   59 {
   60   (void)n;
   61   gArgv[0] = strtok(command,SEP);
   62   int index = 1;
   63   // done, 故意写成=,表示先赋值,在判断. 分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL, 并且while判断结束
   64   while((gArgv[index++] = strtok(NULL,SEP)));
   65 }    

我们使用来strtok()函数:

char *strtok(char *str, const char *delim)
  • str—要被分解的字符串
  • delim—用作分隔符的字符(可以是一个,也可以是集合)在这里我们使用宏定义SEP( 代表 “ ” )
  1. 第一次调用strtok(),传入的参数str是要被分割的字符串{aaa - bbb -ccc},而成功后返回的是第一个子字符串{aaa};
  2. 第二次调用strtok的时候,传入的参数应该为NULL,这样使该函数默认使用上一次未分割完的字符串继续分割 ,就从上一次分割的位置作为本次分割的起始位置,直到分割结束。(strtok内部会记录相应信息)

这样就成功分割命令,来看效果:

我们的准备工作做完了,接下来就可以进行最终的操作:创建新进程来执行命令!

2.4 运行命令

运行命令就要使用:

  • 创建子进程
  • 进程替换

这两个加在一起就有了非常牛批的力量,究极POWER!。

   68 //执行命令
   69 void ExecuteCommand()
   70 {//创建子进程
   71   pid_t id = fork();                                                                                                                                                        
   72   if(id == 0)                                                                                                                 
   73   { //进程替换                                                                                                                          
   74     execvp(gArgv[0],gArgv);                                                                                               
   75     exit(errno);                                                                                                              
   76   }                                                                                                                           
   77   else                                                                                                                        
   78   {                                                                                                                           
   79     int status = 0;                                                                                                           
   80     pid_t rid = waitpid(id,&status,0);//进程等待                                                                                        
   81     if(rid > 0)                                                                                                               
   82     { //如果错误打印错误信息                                                                                                                        
   83       int lastcode = WEXITSTATUS(status);                                                                                     
   84       if(lastcode != 0) printf("%s:%s:%d\n",gArgv[0],strerror(lastcode),lastcode);                                            
   85     }                                                                                                                              
   86   }                                                                                                                                
   87 }   

前面已经做好大部分工作了,执行命令这一步就很简单了。来看效果:

这样就完成了绝大部分的代码编写。我们在加上一个while循环,让命令行一直运行试试:

这样就实现了shell的大部分功能,但是还是有一些功能没有做到:比如我们运行cd等内建命令时,会无法运行,所以我要加上特殊情况来保证内建命令可以执行!!!

90 char* GetHome()
 91 {
 92   char* home = getenv("HOME");
 93   return home;
 94 }
 95 
 96 char cwd[SIZE];
 97 
 98 void cd()
 99 {
100   const char* path = gArgv[1];
101   if(path == NULL) path = GetHome();
102   chdir(path);
103 
104   char temp[SIZE];
105   getcwd(temp,sizeof(temp));                                                                                                                                                  
106   snprintf(cwd,sizeof(cwd),"PWD=%s",temp);
107   putenv(cwd);
108 }
109 
110 //检查是否为内建命令 并单独执行
111 bool CheckBuildin()
112 {
113   bool yes = false;
114   //if语句判断即可,内建命令是有限的
115   if(strcmp(gArgv[0],"cd") == 0)
116   {
117     cd();
118     yes = true;
119   }
120   return yes;
121 }
123 int main()
124 {
125   int quit = 0;                                                                                                                                                               
126 
127   while(!quit)
128   {
129 
130     //创建我们自己的命令行
131     MakeCommandLineAndPrint();
132     
133     //获取命令行信息
134     char usercommand[SIZE];
135     int n = GetUserCommand(usercommand,sizeof(usercommand));
136     if(n <= 0) return 1;
137     
138     //分割命令行信息
139     SplitCommand(usercommand, sizeof(usercommand));
140     
141     bool judge = CheckBuildin();
142     if(judge) continue;
143 
144     //执行命令
145     ExecuteCommand();
146   }
147 
148 
149   return 0;
150 }
       

这样把内建命令单独进行运行就可以了,我这里只写了一个cd命令。来看效果:

这样就完成了我们的自主shell编写!!!

3 加入文件操作(4.19 更新)

学习过文件操作的重定向之后,我们也可以向我们的shell外壳中添加上文件操作的功能。

3.1 识别判断重定向

和上面的内建命令一样,我们需要进行一下判断,看看是否使用了重定向> < >>

      //文件打开模式
   18 #define None_Redir 0
   19 #define In_Redir   1 // 输入
   20 #define Out_Redir  2 // 输出
   21 #define App_Redir  3 // 追加
   22 #define Skip_Space(cmd ,pos) do{ while(1) if(isspace(cmd[pos])) pos++; else break;}while(0)
   23 int redir_type = None_Redir;
   24 char *filename = NULL;
  .....
  134 void CheckRedir(char* cmd)
  135 {
  136   // ls -a -l > myfile.txt
  137   int pos = 0 ;
  138   int end = strlen(cmd);
  139   //左闭右开
  140   while(pos != end)
  141   {
  142     if(cmd[pos] == '>')
  143     {
  144       if(cmd[pos+1] == '>')
  145       {
  146         cmd[pos++] = 0;
  147         pos++;
  148         redir_type = App_Redir;
  149         Skip_Space(cmd,pos);
  150         filename = cmd + pos;
  151       }
  152       else
  153       {
  154         cmd[pos++] = 0;
  155         redir_type = Out_Redir;
  156         Skip_Space(cmd,pos);
  157         filename = cmd + pos;
  158 
  159       }
  160     }
  161     else if(cmd[pos] == '>')
  162     {
  163       cmd[pos++] = 0 ;                                                                                                                                                      
  164       redir_type = In_Redir;
  165       Skip_Space(cmd,pos);
  166       filename = cmd + pos;
  167     }
  168     else
  169     {
  170       pos++;
  171     }
  172   }
  173 }

逻辑是很简单的:

  1. 遍历一遍命令行数组,检查里面是否有> < >>
  2. 在pos位置设置为‘\0’,截断命令。
  3. 跳过重定向符号与文件名之间的空格
  4. 文件名即为此时pos后的字符串。

重定向符号的对应数字就可以我们可以提前对重定向模式进行宏定义,方便使用。

3.2 执行重定向

进行重定向的判断之后,我们可以在执行命令的模块函数中进行一个判断,按照对应的重定向模式进行重定向。子进程中使用dup2函数,不会影响到父进程的文件指向,就可以完成任务。

   86 void ExecuteCommand()
   87 {
   88   pid_t id = fork();
   89   if(id == 0)
   90   {
   91     if(filename != NULL)
   92     {
   93 
   94        if(redir_type == In_Redir)
   95        {
   96          int fd = open(filename,O_RDONLY);
   97          dup2(fd , 0);
   98        }
   99        else if(redir_type == Out_Redir)
  100        {
  101          int fd = open(filename,O_WRONLY | O_TRUNC | O_CREAT, 0666 );
  102          dup2(fd , 1);
  103        }
  104        else if(redir_type == App_Redir)
  105        {
  106          int fd = open(filename,O_WRONLY | O_CREAT | O_TRUNC, 0666 );
  107          dup2(fd , 1);
  108 
  109        }
  110        else
  111        {
  112 
  113        }
  114     }                                                                                                                                                                       
  115     execvp(gArgv[0],gArgv);
  116     exit(errno);
  117   }
  118   else
  119   {
  120     int status = 0;
  121     pid_t rid = waitpid(id,&status,0);
  122     if(rid > 0)
  123     {
  124       lastcode = WEXITSTATUS(status);
  125       if(lastcode != 0) printf("%s:%s:%d\n",gArgv[0],strerror(lastcode),lastcode);
  126     }
  127   } 
  128 }

来看效果

这样就实现了shell里的文件操作

4 源代码

#include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/wait.h>
  4 #include<stdlib.h>
  5 #include<unistd.h>
  6 #include<string.h>
  7 #include<errno.h>
  8 #include<stdbool.h> 
  9 
 10 #define SIZE 256
 11 #define SkipPath(p) do{ p += strlen(p)-1 ;while(*p != '/') p--; }while(0); 
 12 #define ZERO '\0'
 13 #define NUM 32
 14 #define SEP " "
 15 
 16 //命令
 17 char* gArgv[NUM];
 18 int lastcode = 0;
 19 char cwd[SIZE];
 20 
 21 const char* GetUsername()
 22 {
 23   const char* name = getenv("USER");
 24   if(name == NULL) return "NONE";
 25   return name; 
 26 }
 27 
 28 const char* GetHostName()
 29 {
 30   const char* hostname = getenv("HOSTNAME");
 31   return hostname; 
 32 }
 33 
 34 const char* GetCwd()
 35 {
 36   const char* cwd = getenv("PWD");
 37   if(cwd == NULL) return "NONE";
 38   return cwd;
 39 }
 40                                                                                                                                                                               
 41 void MakeCommandLineAndPrint()
 42 {
 43   char line[SIZE];
 44   const char* username = GetUsername();  
 45   const char* hostname = GetHostName();
 46   const char* cwd = GetCwd();
 47   SkipPath(cwd);
 48   sprintf(line,"[%s@%s %s]> ",username,hostname,strlen(cwd) == 1?"/":cwd + 1);
 49   printf("%s",line);
 50   fflush(stdout);
 51 }                                                                                                                                                                             
 52 
 53 int GetUserCommand(char command[] ,size_t n)
 54 {
 55   char* s = fgets(command,n,stdin);
 56   if(s == NULL) return -1;
 57   command[strlen(command) - 1] = ZERO; 
 58   return strlen(command);
 59 }
 60 
 61 void SplitCommand(char command[] , size_t n)
 62 {
 63   (void)n;
 64   gArgv[0] = strtok(command,SEP);
 65   int index = 1;
 66   // done, 故意写成=,表示先赋值,在判断. 分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL, 并且while判断结束
 67   while((gArgv[index++] = strtok(NULL,SEP)));
 68 } 
 69 
 70 //执行命令
 71 void ExecuteCommand()
 72 {
 73   pid_t id = fork();
 74   if(id == 0)
 75   {
 76     execvp(gArgv[0],gArgv);
 77     exit(errno);
 78   }
 79   else
 80   {
 81     int status = 0;
 82     pid_t rid = waitpid(id,&status,0);
 83     if(rid > 0)
 84     {
 85       lastcode = WEXITSTATUS(status);
 86       if(lastcode != 0) printf("%s:%s:%d\n",gArgv[0],strerror(lastcode),lastcode);
 87     }
 88   } 
 89 }
 90 
 91 char* GetHome()
 92 {
 93   char* home = getenv("HOME");
 94   return home;
 95 }
 96                                                                                                                                                                               
 97 
 98 void cd()
 99 {
100   const char* path = gArgv[1];
101   if(path == NULL) path = GetHome();
102   chdir(path);
103 
104   char temp[SIZE];
105   getcwd(temp,sizeof(temp));
106   snprintf(cwd,sizeof(cwd),"PWD=%s",temp);
107   putenv(cwd);
108 }
109 
110 //检查是否为内建命令 并单独执行
111 bool CheckBuildin()
112 {
113   bool yes = false;
114   //if语句判断即可,内建命令是有限的
115   if(strcmp(gArgv[0],"cd") == 0)
116   {
117     cd();
118     yes = true;
119   }
120   return yes;
121 }
122 
123 int main()
124 {
125   int quit = 0;
126 
127   while(!quit)
128   {
129 
130     //创建我们自己的命令行
131     MakeCommandLineAndPrint();
132     
133     //获取命令行信息
134     char usercommand[SIZE];
135     int n = GetUserCommand(usercommand,sizeof(usercommand));
136     if(n <= 0) return 1;
137     
138     //分割命令行信息
139     SplitCommand(usercommand, sizeof(usercommand));
140     
141     bool judge = CheckBuildin();
142     if(judge) continue;
143 
144     //执行命令
145     ExecuteCommand();
146   }
147 
148 
149   return 0;
150 }

Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!

相关文章
|
8月前
|
存储 安全 Unix
七、Linux Shell 与脚本基础
别再一遍遍地敲重复的命令了,把它们写进Shell脚本,就能一键搞定。脚本本质上就是个存着一堆命令的文本文件,但要让它“活”起来,有几个关键点:文件开头最好用#!/usr/bin/env bash来指定解释器,并用chmod +x给它执行权限。执行时也有讲究:./script.sh是在一个新“房间”(子Shell)里跑,不影响你;而source script.sh是在当前“房间”里跑,适合用来加载环境变量和配置文件。
768 9
|
8月前
|
算法 Linux Shell
Linux实用技能:打包压缩、热键、Shell与权限管理
本文详解Linux打包压缩技巧、常用命令与原理,涵盖.zip与.tgz格式操作、跨系统传文件方法、Shell运行机制及权限管理,助你高效使用Linux系统。
Linux实用技能:打包压缩、热键、Shell与权限管理
|
8月前
|
存储 Shell Linux
八、Linux Shell 脚本:变量与字符串
Shell脚本里的变量就像一个个贴着标签的“箱子”。装东西(赋值)时,=两边千万不能有空格。用单引号''装进去的东西会原封不动,用双引号""则会让里面的$变量先“变身”再装箱。默认箱子只能在当前“房间”(Shell进程)用,想让隔壁房间(子进程)也能看到,就得给箱子盖个export的“出口”戳。此外,Shell还自带了$?(上条命令的成绩单)和$1(别人递进来的第一个包裹)等许多特殊箱子,非常有用。
778 2
|
10月前
|
Web App开发 缓存 安全
Linux一键清理系统垃圾:释放30GB空间的Shell脚本实战​
这篇博客介绍了一个实用的Linux系统盘清理脚本,主要功能包括: 安全权限检查和旧内核清理,保留当前使用内核 7天以上日志文件清理和系统日志压缩 浏览器缓存(Chrome/Firefox)、APT缓存、临时文件清理 智能清理Snap旧版本和Docker无用数据 提供磁盘空间使用前后对比和大文件查找功能 脚本采用交互式设计确保安全性,适合定期维护开发环境、服务器和个人电脑。文章详细解析了脚本的关键功能代码,并给出了使用建议。完整脚本已开源,用户可根据需求自定义调整清理策略。
1178 1
|
Linux Shell
Centos或Linux编写一键式Shell脚本删除用户、组指导手册
Centos或Linux编写一键式Shell脚本删除用户、组指导手册
369 4
|
12月前
|
Linux Shell 数据安全/隐私保护
Centos或Linux编写一键式Shell脚本创建用户、组、目录分配权限指导手册
Centos或Linux编写一键式Shell脚本创建用户、组、目录分配权限指导手册
626 3
|
11月前
|
监控 Shell Linux
Linux进程控制(详细讲解)
进程等待是系统通过调用特定的接口(如waitwaitpid)来实现的。来进行对子进程状态检测与回收的功能。
265 0
|
11月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
332 0
|
11月前
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
216 0
|
11月前
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
273 0