文章目录
前言
最近学习了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、退出
五、总结
总体来讲难度不大,基本上是一些系统调用函数的使用。多管道操作要理解其执行过程,再用代码将其表达出来。这个比较适合练手,可以加深对一些系统函数的使用与理解。