- 在Linux系统中,shell是我们每天经常使用的东西,而如何实现一个自己的shell?首先我们需要了解一些基础知识
一.进程基础知识
进程概述: CPU执行的程序,是一个动态的实体,进程是操作系统资源分配的基本单位。
进程和程序的区别在于进程是动态的,程序是静态的,进程是运行中的程序
linux 下可通过ps命令来实现查看
Linux中一个进程由三部分组成,代码段,数据段和栈堆端。代码段存放程序的全局变量,常量,静态变量。堆栈段中的堆用于存放动态分配的全局变量,堆栈中栈用于函数调用,它存放着函数的参数,函数内部定义的局部变量
Linux中的进程控制,系统提供了一些函数可以使用:
- fork用于创建一个新进程
- exit用于终结进程
- wait将父进程挂起,等待子进程的终结
- getpid 获取当前进程的PID
- nice改变进程的优先级
进程分为几种不同的状态,运行状态,可中断状态,不可中断状态,僵尸进程,停止进程
2.还有几种特殊的进程
孤儿进程: 如果一个子进程的父进程先与子进程结束,子进程就成为一个孤儿进程
守护进程:在后台运行的,没有控制终端与之相连的进程,它独立于控制终端,周期性的执行某些任务
二.进程的内存映像
我们在linux平时见到的程序,他们是如何转化为进程的?
通常需要如下几个步骤
- 内核将程序读入内存,为程序分配内存空间
- 内核为该进程分配了进程标识符(PID)和其他所需资源
- 内核为该进程保存了PID及其相应的状态信息,把程序放进队列种等待执行,这样就可以被操作系统的调度程序执行了
2.进程的内存映像
进程的内存映像是指的内核在内存中如何存放可执行程序文件,在将程序转化为进程的过程中,把硬盘复制给内存之中
,而在实现自己的shell中就使用到这一概念,可执行程序位于磁盘中,而内存映像在内存之中,内存映像随着程序的执行在动态变化中
三.实现myshell所涉及到的部分函数的用法
1.fork函数
fork函数是创建子进程的方式.
include< stdio.h>
include < unistd.h>
pid _t fork (void);
使用fork函数可以将当前进程分裂为两个进程,但是不同的是fork函数有两个返回值,一个是父进程调用fork的返回值,一个是子进程中fork函数的返回值
2.vfork函数
vfork函数与fork函数的不同是什么?
他们的基本用法是相同的,也有一些地方有一些不同,
- vfork和fork一样都是调用一次,返回两次
- 使用fork创建一个子进程之后,子进程会继承父进程的资源,具有良好的并发性,而vfork创建的子进程会共享父进程的地址空间,子进程对该地址空间中任何数据的修改都会被父进程所看到
- vfork函数,一般是子进程先执行,之后父进程才进行执行
3.进程退出
LInux中进程退出分为两种,正常退出和异常退出
(1) 正常退出
在main函数中执行return
调用exit函数
调用_exit函数
而exit和_exit函数有什么区别尼,exit函数在结束时会清除缓冲区中的内容,而_exit会交给内核处理,直接关闭
(2) 异常退出
调用abort函数
进程收到某种信号,而信号会让程序停止
注:尽量减少僵尸进程的产生,应该合理的使用wait/waitpid函数,来让父进程等待子进程的结束
4.执行程序
很多小伙伴看到这里应该就很好奇,如何执行进程?
在这里给大家介绍一个函数族 exec族身为地字一号的重要人物,其作用是十分广阔的
原理:使用exec族执行一个可执行的文件来代替当前进程的内存映像
exec的族的使用并没有产生新的进程哦,而是把程序的代码换入,重新分贝数据段和栈堆段.
exec族的成员如下
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...
/* (char *) NULL */);
int execlp(const char *file, const char *arg, ...
/* (char *) NULL */);
int execle(const char *path, const char *arg, ...
/*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);
我在这里使用到了execvp,在这里介绍以下它的用法吧,
它的参数中的filename,如果其中包含了"/“的话,相当于路径,不包含的话”/",函数就到环境变量中PATH来寻找
4.等待进程结束
这点是必要的,不然会造成僵尸进程的产生
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *statloc)
pid_t waitpid (pid_t pid, int * statloc , int options)
wait函数让父进程暂停执行,直到它的一个子进程结束为停止
状态信息将会被写入到statloc
waitpid 函数也用来等待子进程的结束,但是它有特定的要求pid需指定要等待的子进程的pid
5.strtok函数(我用来解析命令行参数)
该函数包含在"string.h"头文件中
函数原型:
char* strtok (char* str,constchar* delimiters );
函数功能:
切割字符串,将str切分成一个个子串
函数参数:
str:在第一次被调用的时间str是传入需要被切割字符串的首地址;在后面调用的时间传入NULL。
delimiters:表示切割字符串(字符串中每个字符都会 当作分割符)。
函数返回值:
当s中的字符查找到末尾时,返回NULL;
如果查不到delimiter所标示的字符,则返回当前strtok的字符串的指针。
注:strtok函数遇到NULL就会结束
如何实现重定向?
标准输入stdin——-需要处理的数据流
标注输出stdout——–结果数据流
标准错误输出stderr—–错误消息流
默认的三个数据流的文件描述符分别为0,1,2,默认的标准输入为终端键盘IO,标准输出和错误输出都是为终端屏幕。而IO重定向就是将三个数据流定向到别的文件描述符。
最低可用文件描述符
什么是文件描述符? 简单来说就是打开文件的一个索引号。Unix系统中,把打开文件保持在一个数组中,文件描述符即为某文件在此数组中的索引。而最低可用文件描述符的意思就是,每当系统打开一个新文件,则分配一个目前可用的最小的文件描述符用于该文件。每个Unix程序默认打开0,1,2三个文件描述符,其实它们对应的文件就是键盘设备和终端屏幕设备的设备文件。
系统调用dup函数
int dup(int oldfd);
dup函数的作用是复制oldfd文件描述符给一个最低可用文件描述符。如果我们想将标准输入重定向到新的文件描述符fd,那么我们可以先close(0)关闭标准输入文件描述符,然后调用函数dup(fd),系统则会默认使用最低可用文件描述符指向fd文件描述符对应的文件。最后再关闭fd文件描述符就完成了IO重定向,如下图所示
那具体是如何实现的? 关键就在于fork和exec函数之间,exec函数的功能只是利用了磁盘的新程序代替了当前进程的正文段,数据段,堆段和栈段,文件描述符是继承的,除非通过fcntl函数设置了执行时关闭标志.
我们可以在fork函数之后,exec函数之前进行IO重定向.
管道
管道是Unix系统进程通信的一种形式。管道有两个特点:
一般管道的数据只能在一个方向上流动
管道只能在具有公共祖先的两个进程之间使用。通常管道由一个父进程创建,调用fork函数后,这个管道就能在父进程和子进程之间使用了
管道是通过调用pipe函数创建,参数是一个大小为2的int数组,由该参数返回两个文件描述符:fd[0]为读打开,fd[1]为写打开。即fd[1]的输出是fd[0]的输入
#include< unistd.h>
#int pipe(int fd[2]);
使用fork函数创建新进程时,也会将父进程的管道复制,就像这样:
四.代码
//添加颜色使用命令别名,alias查看
//对于指针数组不能使用strcpy来进行添加 直接衡等就可
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <dirent.h>
#include <signal.h>
#include <ctype.h>
#include <string.h>
#include <pwd.h>
#include <wait.h>
#include <readline/history.h>
#include <readline/readline.h>
#define MAXARGS 20
#define ARGLEN 60
int i;
struct parameter
{
int normal; //一般命令
int out_redirect; //输出重定向 >
int in_redirect; //输入重定向 <
int have_pipe; //命令中由管道符号
int backgroud; //标识命令有没有重定向
int out_redirects; // >>
};
char oldpwd[300][300];
int o;
struct parameter param;
//cd内置命令
void shell_cd(char *path[]);
void find_command(char *path[]);
void get_ifnput(char *buf)
{
struct passwd *name; ///头文件在pwd.h之中,需要添加
name = getpwuid(getuid());
char pwd[100] = {0};
getcwd(pwd, sizeof(pwd) - 1); //保存绝对路径
int len = strlen(pwd);
char *p = pwd + len;
char temp[MAXARGS * ARGLEN];
int pathlen;
char *isstarm;
while (*p != '/' && len--) //把当前目录从后往前遍历
{
p--;
}
p++;
sprintf(temp, "[%s @myshell %s] :", name->pw_name, p);
isstarm = readline(temp);
add_history(isstarm);
write_history(NULL);
pathlen = strlen(temp);
if (pathlen == MAXARGS * ARGLEN)
{
perror("too long");
exit(-1);
}
strcpy(buf, isstarm);
//buf[pathlen]='\n';
buf[pathlen++] = '\0';
if(strcmp(buf,"")==0)
strcpy(buf,"\n");
free(isstarm);
}
//解析命令行参数
void explain(char *buf, char *list[256])
{
i = 0;
char *p = buf;
while (1)
{
if (strcmp(buf, "cd") == 0)
{
strcat(buf, " ~");
}
if((strcmp(buf,"ll"))==0)
{
strcpy(buf,"ls -l --color=auto");
}
if (p[0] == '\n')
break;
if (p[0] == ' ')
p++;
else
{
list[i] = strtok(buf, " "); //将他们的空格分开i
if(strcmp(list[i],"ls")==0)
{
list[i+1]="--color=auto";
i++;
}
i++;
while ((list[i] = strtok(NULL, " ")) != NULL && strcmp(list[i], "\n") != 0)
{ //在这里需要注意strtok的返回值为NULL
if(strcmp(list[i],"grep")==0)
{
list[i+1]="--color=auto";
i++;
}
i++;
}
}
if (list[i] == NULL)
{
break;
}
}
}
void recover_stdio() //dev/tty是终端控制台
{
int ttyfd;
ttyfd = open("/dev/tty", O_RDONLY);
//付给他标准输出和输出和报错
dup2(ttyfd, 0);
dup2(ttyfd, 1);
dup2(ttyfd, 2);
}
//标准的输入输出流分为三种
//>>函数可使用O_APPEND标识符来进行追加操作
void output_redirce(char *filename, int mode)
{
if (filename == NULL)
return;
int fd;
if (mode == 1)
fd = open(filename, O_RDWR | O_APPEND | O_CREAT, 0644);
else
fd = open(filename, O_WRONLY | O_CREAT, 0644);
if (fd < 0)
perror("open error");
dup2(fd, 1);
close(fd);
return ;
}
static void intput_redirce(char *filename)
{
int fd;
fd = open(filename, O_RDWR);
if (fd < 0)
perror("< error");
dup2(fd, 0);
close(fd);
return;
}
//因为将命令传递给execv函数需要去掉
//执行命令
void do_cmd(char *list[])
{
param.backgroud = 0;
//如果命令中有&,表示后台运行,父进程直接返回,不等子进程,
int status;
int mode = 0;
pid_t pid;
int j = 0;
int flag = 0; //标记有没有特殊字符,比如重定向或者&
int pid_flag=0;
//查看有没有后台运行程序
for (j = 0; j < i; j++)
{
if (strncmp(list[j], "&", 1) == 0)
{
if (j <= i - 1)
{
param.backgroud = 1;
break;
}
if (j > i - 1)
{
perror("Wrong command\n");
return;
}
}
if (strcmp(list[j], "|") == 0)
{
flag = 1;
pid_flag = 1;
int pipe_fd[2]; //使用pipe函数构建管道
int pipstatuts;
pid_t child, child2;
pipe(pipe_fd);
char *file[256];
*file = (char *)malloc(256);//指针数组跟二维数组不同
memset(file,0,sizeof(file));
int k=0,n=0,file_flag=0,l;
for(k=j+1;k<i;k++)
{
file[n] = list[k];
if(strcmp(file[n],">")==0)
{
file_flag = 1;
file[n] = NULL;
l=n;
n++;
continue;
}
if(strcmp(file[n],">>")==0)
{
file_flag = 2;
file[n]=NULL;
l=n;
n++;
continue;
}
if(strcmp(file[n],"<")==0)
{
file_flag = 3;
file[n]=NULL;
l=n;
n++;
continue;
}
n++;
}
if ((child = fork()) != 0) //函数的父进程
{
if ((child2 = fork()) == 0) //子进程
{
close(pipe_fd[1]); //关闭写端,管道第一个命令需要读入
close(fileno(stdin)); //关闭输入
dup2(pipe_fd[0], fileno(stdin));
close(pipe_fd[0]); //读端结束关闭,防止影响别的*/
list[j]=NULL;
file[n] = NULL;
if(file_flag == 1)
output_redirce(file[l+1], mode);
if(file_flag ==2)
output_redirce(file[l+1], 1);
if(file_flag == 3)
intput_redirce(file[l+1]);
execvp(file[0],file);
exit(0);
}
else //在这里child2的父进程迟缓
{
close(pipe_fd[0]);
close(pipe_fd[1]);
waitpid(child2, &pipstatuts, 0);
}
waitpid(child, &pipstatuts, 0);
}
else
{
close(pipe_fd[0]); //写数据
close(fileno(stdout)); //关闭读端
dup2(pipe_fd[1], fileno(stdout));
close(pipe_fd[1]);
list[j] = NULL;
execvp(list[0], list);
exit(0);
}
param.have_pipe = 3;
break;
}
if (strcmp(list[j], ">") == 0)
{
flag = 1;
pid = fork();
if (pid == 0)
{
param.out_redirect = 1;
list[j] = NULL;
output_redirce(list[j + 1], mode);
find_command(list);
exit(0);
}
break;
}
if (strcmp(list[j], "<") == 0)
{
flag = 1;
pid = fork();
if (pid == 0)
{
param.in_redirect = 1;
list[j] = NULL;
intput_redirce(list[j + 1]);
find_command(list);
exit(0);
}
break;
}
if (strcmp(list[j], ">>") == 0)
{
flag = 1;
pid = fork();
if (pid == 0)
{
param.out_redirects = 1;
list[j] = NULL;
output_redirce(list[j + 1], 1);
find_command(list);
exit(0);
}
break;
}
if (strcmp(list[j], "&") == 0)
{
param.backgroud = 1;
}
}
if (flag == 0 && param.backgroud == 0)
{
pid = fork();
if (pid == 0)
{
find_command(list);
exit(0);
}
}
if (param.backgroud == 1) //如果命令中有后台执行的程序
{
pid = fork();
printf("process id %d\n",pid);
return;
}
if(pid_flag == 0)
{
if (waitpid(pid, &status, 0) == -1)
{
printf("wait for child process error\n");
}
}
}
void showhistroy()
{
read_history(NULL);
HIST_ENTRY **history;
history = history_list();
int k = 0;
while (history[k] != NULL)
{
printf("%s\n", history[k]->line);
k++;
}
}
//查找命令中的可执行命令,在这里应该fork一个子进程让它在后台,不断工作到终止
//不然的话执行一个进程就会死掉
void find_command(char *path[])
{
int pid;
int child_info = -1;
if (*path == NULL)
return;
if (strcmp(*path, "exit") == 0 || strcmp(*path, "logout") == 0)
exit(0);
if ((pid = fork()) == -1)
perror("fork");
else if (pid == 0)
{
execvp(path[0], path);
}
else
{
if (wait(&child_info) == -1)
perror("wait");
}
return;
}
//cd内置命令
void shell_cd(char *path[])
{
struct passwd *usrname; ///头文件在pwd.h之中,需要添加
usrname = getpwuid(getuid());
if (strcmp(path[1], "~") == 0)
{
if (strcmp(usrname->pw_name, "kiosk") == 0)
{
strcpy(path[1], "/home/kiosk/");
}
if (strcmp(usrname->pw_name, "root") == 0)
{
strcpy(path[1], "/root/");
}
}
if (strcmp(path[1], "-") == 0)
{
strcpy(path[1], oldpwd[o]);
}
getcwd(oldpwd[o], sizeof(oldpwd[o]) - 1); //保存绝对路径
//printf("%s\n",oldpwd[o]);
o++;
if (chdir(path[1]) < 0) //chdir可以改变当前的工作目录,fchdir用来将当前目录改为由文件描述符所指定的目录
{
printf("%s\n",path[1]);
perror("cd");
}
}
int main(int argc, char **argv)
{
char *buf = NULL;
buf = (char *)malloc(MAXARGS * ARGLEN);
char *list[256];
*list = (char *)malloc(256);
if (buf == NULL)
{
perror("malloc failed");
exit(-1);
}
signal(SIGINT, SIG_IGN);//屏蔽掉信号,ctrl+c
signal(SIGQUIT, SIG_IGN);
signal(SIGSTOP, SIG_IGN);//ctrl+z发出信号
signal(SIGTSTP, SIG_IGN);
while (1)
{
memset(buf, 0, 1200);
memset(list, 0, sizeof(list));
get_ifnput(buf);
if (strcmp(buf, "exit") == 0 || strcmp(buf, "logout") == 0)
break;
if (strcmp(buf,"\n") == 0 )
continue; //多个回车
explain(buf, list);
if (strcmp(list[0], "cd") == 0)
{
shell_cd(list);
continue;
}
if (strcmp(list[0], "history") == 0)
{
showhistroy();
continue;
}
do_cmd(list);
}
free(buf);
for (i = 0; i < 256; i++) //将二位数组进行释放
free(list[i]);
exit(0);
}