Linux系统学习——简单的Shell终端实现

前言

最近学习了fork、pipe、exec族等的linux系统调用函数,现在将这几天所学的结合起来,做一个简单地shell终端。

一、程序所用到的函数
1、fork函数

定义:pid_t fork(void)
功能:创建一个子进程
使用例程

//包含的头文件
#include <sys/types.h>
#include <unistd.h>

pid_t pid;

pid =  fork(void);

if(pid<0){}//表示创建失败
else if(pid==0){}//表示子进程
else if(pid>0){}//表示父进程 pid为子进程的PID
2、pipe函数

定义:int pipe(int pipefd[2]);
功能:创建一个管道 pipefd[0]为读端 pipefd[1]为写端,读端写端实质是两个文件描述符
使用例程

//包含头文件
#include <unistd.h>

int fd[2],ret;

ret = pipe(fd);
if(ret==-1){}//表示创建失败
3、execvp函数

定义:int execvp(const char *file, char *const argv[]);
功能:会根据当前系统环境变量,找到file执行,传入参数为 argv[] 指针数组 执行之后,当前进程的所有内容会被替换
使用例程

//包含头文件
#include <unistd.h>

char *argv[] = {"ls", "-al", NULL};//最后一个参数要为NULL,表示传入的参数到这里就已经结束
execvp(argv[0], argv);//执行ls命令  
4、dup2函数

定义:int dup2(int oldfd, int newfd);
功能:将newfd文件描述符,指向oldfd所指向的文件
使用dup2之前
在这里插入图片描述
使用dup2之后
在这里插入图片描述
参考文章:dup2的理解
使用例程

//使用头文件 
#include <unistd.h>
#include <fcntl.h>

int fd = open("file", O_RDWR);
dup2(fd, STDOUT_FILENO);//将标准输出指向文件file 所有的输出都会输出到file中
5、waitpid函数

定义:pid_t waitpid(pid_t pid, int *wstatus, int options)
功能:等待执行PID的子进程结束,结束状态会存放在wstatus中(结合一些特定的宏一起使用),options为0时表示阻塞等待,options为WNOHANG(宏定义)时,表示非阻塞等待
使用例程

//包含的头文件
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

pid_t pid = fork();

if(pid==0)
{
}//子进程
else if(pid>0)
{
	waitpid(pid, NULL, 0);//阻塞等待子进程结束
}//父进程
6、getcwd函数和chdir函数

定义:char *getcwd(char *buf, size_t size)
int chdir(char *path);
功能:获取当前进程的目录
改变当前进程的目录
使用例程

//所用到的头文件
#include <unistd.h>

char buf[1024];
getcwd(buf, sizeof(buf));
chdir("..");

//假设当前目录为 /test/chdir/getcwd 
//执行程序之后
//当前目录为 /test/chdir
7、strtok函数

定义:char *strtok(char *str, const char *delim);
功能:对字符串str,根据delim进行分割
使用例程

//包含头文件
#include <string.h>
#include <stdio.h>

char str[] = "I am SJ,hello world";
char *cstr;

cstr = strtok(str, " ");//以空格进行分割,str只需传入一次 后面传入NULL
while(cstr!=NULL)//当分割到末尾时 返回NULL
{
	printf("%s\n", cstr);
	cstr = strtok(NULL, " ");
} 
/*  运行结果
 *   I
 *   am
 *   SJ,hello
 *   world
 */
7、fgets函数

定义:char *fgets(char *s, int size, FILE *stream)
功能:从stream中获取size大小的一行输入,换行符(\n)表示输入结束,将其存放在s中
使用例程

//使用的头文件
#include <stdio.h>

char s[1024];
fgets(s, sizeof(s), stdin);//从标准输入中读取size个字符 遇换行符则结束 
// 输入: I am SJ(回车)
// s = "I am SJ\n"
二、程序所实现的功能

1、基本的命令的使用,如:cd、ls、cat、echo、ps 等的一些基本命令,操作与在linux终端下的没有区别
2、重定向输入和输出,即 cmd > file 和 cmd < file
3、多管道操作 | 如:cat file | cat | cat | cat
基本来讲linux终端下可执行的命令,都可以在这个shell终端下执行

三、程序功能的实现

1、获取输入的命令,并使用strtok将其分割

/*
 *   命令获取并处理函数
 */
void argv_Handle()
{
	int len;
	length = 0;
	
	//获取一行字符串,将其存放于cmd字符数组中
	if((fgets(cmd, sizeof(cmd), stdin))==NULL)
	{
		sys_error("fges fail");
	}
	
	//将输入cmd命令用strtok函数根据空格进行分割,存放于arg指针数组中
	arg[length]=strtok(cmd, " ");
	while(arg[length]!=NULL)
	{
		length++;
		arg[length]=strtok(NULL, " ");
	}
	
	//fgets函数获取一行字符串时会将末尾的 \n 一起获取出来 所以这里要将其处理掉
	if(arg[length-1][0]=='\n')
	{
		/*
		 *  判断最后一个被分割出的命令是否只包含一个换行符(\n)
		 *  将其位置设置为NULL(作为一个哨兵) 并将命令总长度减一
		 */
		arg[length-1]=NULL;
		length = length - 1;
	}
	else
	{
		/*
		 *  设置哨兵
		 *  并将最后一个命令的换行符删除
		 */
		arg[length]=NULL;
		len = strlen(arg[length-1]);
		arg[length-1][len-1]='\0';
	}
}

2、命令执行函数
支持重定向、管道和普通命令的执行
重定向和管道操作的实质是对进程的标准输入(STDIN_FILENO)和标准输出(STDOUT_FILENO)进行重定向,指向另外一个文件或者其他进程 使用dup2函数即可实现该功能

/*
 *	命令执行函数
 *  start : 表示在命令的起始位置
 *  fd_in : 输入重定向文件描述符 -1表示此次执行的命令无重定向输入
 *  fd_out: 输出重定向文件描述符 -1表示此次执行的命令无重定向输出
 *  flag  : 管道标志位 1表示此次命令的输出需要使用管道进行重定向
 *  返回值: 若flag为true 则返回创建的管道的读端 作为下一条命令的重定向输入
 */
int Pipe_handle(int start, int fd_in, int fd_out, int flag)
{
	int fd[2],ret;
	pid_t pid;
	
	if(flag)
	{
		//flag为true时 创建管道
		ret = pipe(fd);
		if(ret==-1)
		{
			sys_error("pipe error");
		}
	}
	
	//创建一个子进程 用于执行命令
	pid = fork();
	if(pid<0)//fork出错
	{
		sys_error("fork fail");
	}
	else if(pid==0)//子进程
	{
		if(flag)
		{
			//使用创建的管道进行输出重定向
			close(fd[0]);
			dup2(fd[1], STDOUT_FILENO);
		}
		
		if(fd_out!=-1)
		{
			//使用传入的输出重定向文件描述符进行重定向
			dup2(fd_out, STDOUT_FILENO);
		}
		
		if(fd_in!=-1)
		{
			//使用传入的输入重定向文件描述符进行重定向
			dup2(fd_in, STDIN_FILENO);
		}
		//执行命令 使用固定参数个数并使用path的函数
		execvp(arg[start], arg+start);
	}
	else //父进程 返回值pid为子进程的PID
	{
		//阻塞等待子进程结束
		waitpid(pid, NULL, 0);
	}
	
	if(fd_in!=-1)
	{
		close(fd_in);
	}
	
	if(fd_out!=-1)
	{
		close(fd_out);
	}
	
	if(flag)
	{
		close(fd[1]);
	}
	
	return flag?fd[0]:0;
}

3、命令解析函数
功能:遍历全部的命令字符串(fgets获取到一行命令之后使用strtok函数分割出来的命令),对 < 、>、| 和 普通命令进行不同的操作,实现其功能

/*
 *  命令识别函数
 *  识别命令中的 管道(|) 重定向符(< >)
 */
void argv_execl()
{
	int i,j,start=0,fd=-1;
	/*
	 *  处理方法详解:
	 *  1、>:重定向输出  格式: cmd > file
	 *     将file打开 获得文件描述符fd
	 *     执行Pipe_handle(start, -1, fd, 0) 表示无重定向输入 有重定向输出 无管道操作
	 *  2、<:重定向输入  格式:cmd < file
	 *     将file打开 获得文件描述符fd
	 *     执行Pipe_handle(start, -1, fd, 0) 表示无重定向输入 有重定向输出 无管道操作
	 *  3、|:管道操作符  格式:cmd | cmd | cmd | .....
	 *     将 | 命令的位置设置为NULL 作为execvp函数执行时的哨兵 
	 *     start位置移动到下一条命令的起始位置 即当前 管道符(|)的位置加一
	 *     执行 fd = Pipe_handle(start, fd, -1, 1); 返回值fd为所创建管道的读端 作为下一条命令的重定向输入
	 *     fd的初始值为 -1 所以首次执行管道命令时,输入不会重定向
	 */
	for(i=0; i<length; ++i)
	{
		if(strcmp(arg[i], ">")==0)
		{
			arg[i]=NULL;
			fd = open(arg[i+1], O_RDWR|O_CREAT, 0664);
			if(fd<0)
			{
				sys_error("> open file fail");
			}
			
			Pipe_handle(start, -1, fd, 0);
			i=i+1;
			fd = 0;
		}
		else if(strcmp(arg[i], "<")==0)
		{
			arg[i]=NULL;
			fd = open(arg[i+1], O_RDONLY);
			if(fd<0)
			{
				sys_error("< open file fail");
			}
			
			Pipe_handle(start, fd, -1, 0);
			i=i+1;
			fd = 0;
		}
		else if(strcmp(arg[i], "|")==0)
		{
			arg[i]=NULL;
			fd = Pipe_handle(start, fd, -1, 1);
			start = i+1;
		}
		else if(i==length-1)
		{
			if(fd==-1)//表示一条普通命令 不具备三种操作符 
			{
				Pipe_handle(start, -1, -1, 0);
			}
			else if(fd > 0)// 表示执行到管道的最后一条命令 
			{
				//获取到的管道的读端作为程序的输入
				Pipe_handle(start, fd, -1, 0);
			}
		}
	}
}

在这里插入图片描述
4、回车和cd改变目录
如果直接执行cd命令,改变的是execvp该子进程的路径,不是主进程的路径。所以要使用chdir改变当前进程的目录

//cd表示改变目录 要使用chdir改变进程的目录 arg[1]代表路径 cd path,cd 是 arg[0]
chdir(arg[1]);

回车操作,即只输入一个换行符,经过命令解析函数解析后,arg[0]=NULL
当该条件成立时,跳出此次循环,开始下一次循环。

if(arg[0]==NULL)
{
	//回车
	continue;
}
四、执行效果

1、启动
在这里插入图片描述
2、基本命令,cd、ls 、 pwd 、ps和文件目录操作

在这里插入图片描述
在这里插入图片描述
3、重定向操作
在这里插入图片描述
在这里插入图片描述
4、退出
在这里插入图片描述

五、总结

总体来讲难度不大,基本上是一些系统调用函数的使用。多管道操作要理解其执行过程,再用代码将其表达出来。这个比较适合练手,可以加深对一些系统函数的使用与理解。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值