Linux——模拟实现一个简单的shell(带重定向)
进程的相关知识是操作系统一个重要的模块。在理解进程概念同时,还需了解如何控制进程。对于进程控制,通常分成1.进程创建 (fork函数) 2.进程等待(wait系列) 3.进程替换(exec系列) 4.进程退出(exit系列,return)四个方面。在大致熟悉进程控制之后,便可基于此 ,来模拟使用一个简单的myshell,实现简单的命令解析。
在此之前,先来简单回顾进程控制一些基本方法
进程控制
(1)进程创建
进程创建一般通过fork来实现,(关于fork,前面有本人一点小小总结: 戳=> ,这里不再赘述)。
(2)进程退出
通常 进程从1. 从main返回 2. 调用exit 3. _exit 是正常终止(可以通过 echo $? 查看进程退出码) 也可能异常退出。
大部分情况下进程会return退出,return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。而关于exit和_exit函数:
1._exit
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。
2.exit
#include <unistd.h>
void exit(int status);
exit最后也会调用exit, 但在调用 exit之前,还做了其他工作:
·1. 执行用户通过 atexit或on_exit定义的清理函数。
·2. 关闭所有打开的流,所有的缓存数据均被写⼊入
·3. 调用_exit
(3)进程等待
由于子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄
漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,就连kill -9 也无能为力,因为谁也没有
办法杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们是需要知道。如,子进程运行完成,结果对还是不
对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
关于等待方法,有如下wait系列:
1.wait
#include<sys/types.h> #include<sys/wait.h> pid_t wait(int* status); 返回值: 成功返回被等待进程pid,失败返回-1。 参数: 输出型参数,获取⼦子进程退出状态,不关⼼心则可以设置成为NULL
2.waitpid
pid_ t waitpid(pid_t pid, int *status, int options); 返回值: 当正常返回的时候waitpid返回收集到的子进程的进程ID; 如果设置了选项WNOHANG,而调⽤中waitpid 若发现没有已退出的子进程可收集,则返回0; 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在; 参数: pid: Pid=-1,等待任一个子进程。与wait等效。 Pid>0.等待其进程ID与pid相等的子进程。 status: WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码) options: WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则 返回该子进程的ID。
·如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程
退出信息。
·如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
·如果不存在该子进程,则⽴立即出错返回。
总结:父进程阻塞在wait,子进程退出后继续执行
关于退出状态获取:
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)
(4)进程替换
替换原理:
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
替换函数exec系列,共6种,
char *const argv[] = {"ps", "-ef", NULL}; char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL}; execl("/bin/ps", "ps", "-ef", NULL); // 带p的,可以使⽤用环境变量PATH,⽆无需写全路径 execlp("ps", "ps", "-ef", NULL); // 带e的,需要⾃自⼰己组装环境变量 execle("ps", "ps", "-ef", NULL, envp); execv("/bin/ps", argv); // 带p的,可以使⽤用环境变量PATH,⽆无需写全路径 execvp("ps", argv); // 带e的,需要⾃自⼰己组装环境变量 execve("/bin/ps", argv, envp);
参数解释: ·这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。 ·如果调用出错则返回-1 ·所以exec函数只有出错的返回值而没有成功的返回值。
模拟实现进程创建函数process_create
基于进程控制的理解,我们可以来简单模拟实现一个进程的创建函数process_create。
process_create(pid_t* pid, void* func, void* arg) 参数: func回调函数,就是子进程执行的入口函数 arg是传递给func回调函数的参数.
该函数将fork和wait函数封装起来,然后用创建出来的子进程去回调func函数,完成func函数功能。
/************************************************************************* > File Name: pro_create.c > Author: tp > Mail: > Created Time: Wed 13 Jun 2018 10:04:21 PM CST ************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> typedef struct Stu{ char a[32]; int age; }Stu; typedef void (*FUNC_NOARG)(); typedef int (*FUNC_ARG)(void*); int print_info(void* arg) { Stu* p= (Stu *)arg; printf( "%s : %d\n", p->a, p->age); return 0; } void say_hi( ) { printf( "hello\n"); } void process_create(pid_t *pid, void *func, void *arg) { pid_t id = fork( ); if( id < 0) perror(" fork"),exit( 1); else if( id == 0) { //child if(arg != NULL) //传入参数不为NULL { FUNC_ARG callback = (FUNC_ARG)func; int ret = callback(arg); if( ret != 0) //模拟判断回调是否成功 (wait) { printf("执行回调函数有错误\n"); exit(1); } } else { FUNC_NOARG callback = (FUNC_NOARG)func; callback(); } exit(0); } else //father { *pid = wait(NULL); } } int main( ) { pid_t id; int* p = (int* )malloc(sizeof(int)); *p = 10; Stu s={"张全蛋", 30}; process_create(&id, ( void*)print_info, &s); printf("pid=%d\n", (int)id); process_create(&id, ( void*)say_hi, NULL); printf("pid=%d\n", (int)id); return 0; }
总结:通过上面程序可以感受函数与进程之间的相似性。 一个C程序有很多函数组成。一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call/return系统进行通信。exec/exit就像call/return一样。这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。系统是很鼓励将这种应用于程序之内的模式扩展到程序之间去的。
一个C程序可以fork/exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来返回值。调用它的进程可以通过wait(&ret)来获取exit的返回值。
模拟实现——简单shell
完成大致思路: shell建立一个新的进程,然后在那个进程中运行一个程序(如完成ls操作)然后等待那个进程执行结束。然后shell便可读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。所以要写一个shell,需要循环以下过程:
1. 获取命令行 2. 解析命令行 3. 建立一个子进程(fork) 4. 替换子进程(execvp) 5. 父进程等待子进程退出(wait)
①完成基本的命令
如简单的ls -l, mkdir ..等。由于是使用fork出来的一个子进程,再通过exec系列函数来单纯将进程地址空间替换来执行完成的命令,这样的方式不能直接解析完成 > 、| 和 cd 、su .., 等一些带系统权限的命令。这里我去添加了它的重定向功能,其它功能,例如 “|”管道命令操作, 可以基于管道操作pipe函数创建出一个管道来实现进程通信。若感兴趣,读者可以再自行添加。也欢迎来一起讨论!!
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <ctype.h> #include <sys/wait.h> void do_exe(char* buf, char* argv[]) //加载程序 { pid_t pid = fork(); if(pid == 0)//子进程 { execvp(buf, argv); perror("fork"); //执行到此,说明execvp未执行成功,fork失败 exit(1); } wait(NULL); //等待子进程死亡, 回收 } //对命令进行解析 void do_parse(char* buf){ char* argv[8] = {}; //将buf中的命令以‘ ’为分界存入指针数组中 int argc = 0; int status = 0; //一个新的字符串 for(int i =0; buf[i] != 0; ++i){ if(status ==0 && !isspace(buf[i])){ argv[argc++] = buf +i; status = 1; } else if(isspace(buf[i])){ status = 0; buf[i] = 0; } } argv[argc] = NULL; do_exe(buf, argv); } int main(void) { // char* argv[] = {"ls", "-lah", NULL}; // execvp("ls", argv);//替换地址空间,实则将原进程的代码段,数据段进行替换,并未创建新的进程出来。 char buf[1024] = {}; while(1) { printf("my shell#"); memset(buf, 0x00, sizeof(buf)); //[^\n]匹配除\n以外的所有字符,*用于抑制转换 //scanf成功返回输入的项数 while(scanf("%[^\n]%*c", buf) == 0) { //为0表示只输入了换行 printf("my shell#"); while(getchar() != \'\n\'); //到获得了一个‘\n\' } do_parse(buf); } return 0; }
②添加重定向功能
对于其中的重定向功能可以通过文件操作和dup函数来模拟实现。
改良版:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <ctype.h> #include <sys/wait.h> void do_exe(char* buf, char* argv[]) //加载程序 { pid_t pid = fork(); if(pid == 0)//子进程 { //寻找重定向标志 > for( int i =0; argv[i] != NULL; ++i) { if(strcmp(argv[i], ">") == 0) { if(argv[i+1] == NULL) //> 后面未带参数 perror("command \'>\'[option]?"),exit( 1); argv[i] = NULL; //根据解析命令参数,创建/打开一文件 int fd =open(argv[i+1], O_RDWR|O_CREAT|O_TRUNC, 0664); if(fd == -1)perror("open"),exit( 1); //重定向操作 dup2(fd, 1); //dup2(oldfd, newfd); close(fd); } } execvp(buf, argv); perror("fork"); //执行到此,说明execvp未执行成功,fork失败 exit(1); } wait(NULL); //等待子进程死亡, 回收 } //对命令进行解析 void do_parse(char* buf){ char* argv[8] = {}; //将buf中的命令以‘ ’为分界存入指针数组中 int argc = 0; int status = 0; //一个新的字符串 for(int i =0; buf[i] != 0; ++i){ if(status ==0 && !isspace(buf[i])){ argv[argc++] = buf +i; status = 1; } else if(isspace(buf[i])){ status = 0; buf[i] = 0; } } argv[argc] = NULL; do_exe(buf, argv); } int main(void) { // char* argv[] = {"ls", "-lah", NULL}; // execvp("ls", argv);//替换地址空间,实则将原进程的代码段,数据段进行替换,并未创建新的进程出来。 char buf[1024] = {}; while(1) { printf("my shell#"); memset(buf, 0x00, sizeof(buf)); //[^\n]匹配除\n以外的所有字符,*用于抑制转换 //scanf成功返回输入的项数 while(scanf("%[^\n]%*c", buf) == 0) { //为0表示只输入了换行 printf("my shell#"); while(getchar() != \'\n\'); //到获得了一个‘\n\' } do_parse(buf); } return 0; }
验证: