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,失败返回-。
参数:
输出型参数,获取⼦子进程退出状态,不关⼼心则可以设置成为NULL
2.waitpid
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调⽤中waitpid 若发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid=-,等待任一个子进程。与wait等效。
Pid>.等待其进程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);
参数解释: ·这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
·如果调用出错则返回-
·所以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,需要循环以下过程:
. 获取命令行
. 解析命令行
. 建立一个子进程(fork)
. 替换子进程(execvp)
. 父进程等待子进程退出(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;
}
验证:
Linux——模拟实现一个简单的shell(带重定向)的更多相关文章
- 如何在linux下编写一个简单的Shell脚本程序
在了解了linux终端和其搭配的基本Shell(默认为bash)的基础下,我们就可以在终端中用vi/vim编辑器编写一个shell的脚本程序了 Shell既为一种命令解释解释工具,又是一种脚本编程语言 ...
- Linux系统学习笔记之 1 一个简单的shell程序
不看笔记,长时间不用自己都忘了,还是得经常看看笔记啊. 一个简单的shell程序 shell结构 1.#!指定执行脚本的shell 2.#注释行 3.命令和控制结构 创建shell程序的步骤 第一步: ...
- 如何写一个简单的shell
如何写一个简单的shell 看完<UNIX环境高级编程>后我就一直想写一个简单的shell来作为练习,因为有事断断续续的写了好几个月,如今写了差不多来总结一下. 源代码放在了Github: ...
- 一个简单的shell脚本
一个简单的shell脚本 一个简单的shell脚本 编写 假设我想知道目前系统上有多少人登录,使用who命令可以告诉你现在系统有谁登录: 1.[KANO@kelvin ~]$ who2.KANO tt ...
- 自己模拟的一个简单的web服务器
首先我为大家推荐一本书:How Tomcat Works.这本书讲的很详细的,虽然实际开发中我们并不会自己去写一个tomcat,但是对于了解Tomcat是如何工作的还是很有必要的. Servlet容器 ...
- python定义的一个简单的shell函数的代码
把写代码过程中经常用到的一些代码段做个记录,如下代码段是关于python定义的一个简单的shell函数的代码. pipe = subprocess.Popen(cmd, stdout=subproce ...
- 实现一个简单的shell
使用已学习的各种C函数实现一个简单的交互式Shell,要求:1.给出提示符,让用户输入一行命令,识别程序名和参数并调用适当的exec函数执行程序,待执行完成后再次给出提示符.2.该程序可识别和处理以下 ...
- 在Linux下写一个简单的驱动程序
本文首先描述了一个可以实际测试运行的驱动实例,然后由此去讨论Linux下驱动模板的要素,以及Linux上应用程序到驱动的执行过程.相信这样由浅入深.由具体实例到抽象理论的描述更容易初学者入手Linux ...
- 【转】在Linux下写一个简单的驱动程序
转自:https://www.cnblogs.com/amanlikethis/p/4914510.html 本文首先描述了一个可以实际测试运行的驱动实例,然后由此去讨论Linux下驱动模板的要素,以 ...
随机推荐
- redis发布/订阅
发布订阅简介 Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息,消息之间通过channel传递. 准备工作 两台安装了redis的机器(虚拟 ...
- Tuxedo 汇总
===================================C/S / Tuxedo 架构/ B/S 架构演进===================================Tuxed ...
- Pandas时间处理的一些小方法
一.以下有两种方式可以创建一个Timestamp对象: 1. Timestamp()的构造方法 import pandas as pd from datetime import datetime as ...
- ORACLE使用CASE WHEN的方法
先写草稿. 说下我的需求,ORACLE数据库有两个字段RECEIVER_MOBILE与RECEIVER_PHONE,同为联系方式.当RECEIVER_MOBILE为空的时候,需要用到RECEIVER_ ...
- Linux中执行C++程序
参考:https://blog.csdn.net/qq_31125955/article/details/79343498 https://blog.csdn.net/weixin_35477207/ ...
- python之造测试数据-faker(转载)
在软件需求.开发.测试过程中,有时候需要使用一些测试数据,针对这种情况,我们一般要么使用已有的系统数据,要么需要手动制造一些数据. 在手动制造数据的过程中,可能需要花费大量精力和工作量,现在好了,有一 ...
- Entity Framework查询
Entity Framework是个好东西,虽然没有Hibernate功能强大,但使用更简便.今天整理一下常见SQL如何用EF来表达,Func形式和Linq形式都会列出来(本人更喜欢Func形式). ...
- DeepLearning.ai学习笔记(五)序列模型 -- week1 循环序列模型
一.为什么选择序列模型 序列模型可以用于很多领域,如语音识别,撰写文章等等.总之很多优点... 二.数学符号 为了后面方便说明,先将会用到的数学符号进行介绍. 以下图为例,假如我们需要定位一句话中人名 ...
- C语言网蓝桥杯1116 IP判断
判断IP地址的合法性, 1.不能出现除数字和点字符以外的的其他字符 2.数字必须在0-255之间,要注意边界. 题目分析: 因为一个IP是又四个数字组成,且可能存在符号和其他字符,故不能用整型数组处理 ...
- Docker入门-docker-compose使用(二)
Docker Docker容器大行其道,直接通过 docker pull + 启动参数的方式运行比较麻烦, 可以通过docker-compose插件快速创建容器 1.安装docker-compose ...