下面是我根据书上的my_shell实现的my_shell.涉及到一下关于进程的知识,下面我将介绍一下.
代码部分
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<dirent.h>
#include<string.h>
#define normal 0 /*一般的命令*/
#define out_redirent 1 /*输出重定向*/
#define in_redirent 2 /*输入重定向*/
#define have_pipe 3 /*命令中有管道*/
//打印shell的提示符
void print_prompt();
//得到输入的命令
void get_input(char *);
//对输入命令进行解析
void explanin_input(char *,int *,char a[][256]);
//执行命令
void do_cmd(int ,char [][256]);
//查找命令中的可执行程序
int find_command(char *);
void cd(char*);
int main()
{
signal(SIGINT,SIG_IGN);
int argcount = 0;
char arglist[100][256];
char **arg = NULL;
char *buf = NULL;
buf = (char *)malloc(256);
if(buf == NULL)
{
perror("malloc failed");
exit(-1);
}
while(1)
{
memset(buf,0,256);
print_prompt();
get_input(buf); // buf是以\n结尾的
//若输出的命令为exit就推出程序
if(0==strcmp(buf,"exit\n"))
break;
argcount=0;
explanin_input(buf,&argcount,arglist);
if(strcmp("cd",arglist[0])!=0)
do_cmd(argcount,arglist);
else if(strcmp("cd",arglist[0])==0)
cd(arglist[1]);
}
if(buf !=NULL)
{
free(buf);
buf=NULL;
}
exit(0);
}
void cd(char *path)
{
chdir(path);
}
//打印出欢迎的界面
void print_prompt()
{
printf("my_shell$$:\n");
}
//获取用户的输出
void get_input(char *buf)
{
int len = 0;
int ch;
ch = getchar();
while(len<256 && ch != '\n')
{
buf[len++]=ch;
ch = getchar();
}
if(256 == len)
{
printf("command is too long \n");
exit(-1);
}
buf[len] ='\n';
len++;
buf[len] = '\0';
}
void explanin_input(char *buf,int *argcount,char arglist[][256])
{
char *p=buf;
char *q=buf;
int number = 0;
while(1)
{
if(p[0]=='\n')
break;
if(p[0]==' ')
p++;
else
{
q=p;
number = 0;
while(q[0]!=' '&&(q[0]!='\n'))
{
number++;
q++;
}
strncpy(arglist[*argcount],p,number+1);
arglist[*argcount][number]='\0';
*argcount = *argcount + 1;
p = q;
}
}
}
void do_cmd(int argcount,char arglist[][256])
{
//how 用于标识命令中是否含有> < |
// background 标识命令中是否有后台运行标识符 &
int flag=0,how=0,background=0,status,i,fd;
char *arg[argcount+1];
char *argnext[argcount+1];
char *file;
pid_t pid;
//取出命令行
for(i=0;i<argcount;i++)
arg[i]=arglist[i];
arg[i]=NULL;
//查看命令行是否有后台运行
for(i=0;i<argcount;i++)
{
if(strncmp(arg[i],"&",1)==0)
{
if(i==argcount-1)
{
background = 1;
arg[argcount -1] = NULL;
break;
}
else
{
printf("wrong command %d\n",__LINE__);
return;
}
}
}
for(i = 0;arg[i]!=NULL;i++)
{
if(strcmp(arg[i],">")==0)
{
flag++;
how = out_redirent;
if(arg[i+1]==NULL)
flag++;
}
else if(strcmp(arg[i],"<")==0)
{
flag++;
how = in_redirent;
if(i==0)
flag++;
}
else if(strcmp(arg[i],"|")==0)
{
flag++;
how = have_pipe;
if(arg[i+1]==NULL)
flag++;
if(i==0)
flag++;
}
}
//flag 大于 1 ,说明命令中含有多个>,<,|符号,本程序不支持这样的命令
//或者格式不对。如 ls -l /temp >
if(flag > 1)
{
printf("wrong command %d\n",__LINE__);
return ;
}
if(how==out_redirent)
{
for(i = 0;arg[i]!=NULL;i++)
{
if(strcmp(arg[i],">")==0)
{
file = arg[i+1];
arg[i]=NULL;
}
}
}
if(how == in_redirent)
{
for(i=0;arg[i]!=NULL;i++)
{
if(strcmp(arg[i],"<")==0)
{
file =arg[i+1];
arg[i]=NULL;
}
}
}
if(how == have_pipe)
{
for(i=0;arg[i]!=NULL;i++)
{
if(strcmp(arg[i],"|")==0)
{
arg[i] = NULL;
int j;
for(j = i+1;arg[j]!=NULL;j++)
argnext[j-i-1] =arg[j];
argnext[j-i-1] =arg[j];
break;
}
}
}
if((pid = fork())<0)
{
printf("%d fork error\n",__LINE__);
return;
}
if(pid==0)
{
//子进程设置信号
signal(SIGINT,SIG_IGN);
}
switch(how)
{
case 0:
if(pid==0) //子进程
{
if(!find_command(arg[0]))
{
printf("%d %s:command not found\n",__LINE__,arg[0]);
exit(0);
}
execvp(arg[0],arg);
exit(0);
}
break;
case 1:
// >
if(pid==0)
{
if(!(find_command(arg[0])))
{
printf("%d %s :command not found\n",__LINE__,arg[0]);
exit(0);
}
fd = open(file,O_RDWR|O_CREAT|O_TRUNC,0664);
dup2(fd,1);
execvp(arg[0],arg);
exit(0);
}
break;
case 2:
//输出的命令中含有重定向符号 <
if(pid == 0)
{
if(!(find_command(arg[0])))
{
printf("%d %s :command not found\n",__LINE__,arg[0]);
exit(0);
}
fd = open(file,O_RDONLY);
dup2(fd,0);
execvp(arg[0],arg);
exit(0);
}
break;
case 3:
if(pid == 0) //子进程
{
int pid2,status2,fd2;
if((pid2 = fork())<0)
{
printf("%d fork2 error\n",__LINE__);
return;
}
if(pid2 ==0)
{
if(!(find_command(arg[0])))
{
printf("%s: command not found %d \n",arg[0],__LINE__);
exit(0);
}
fd2 =open("/tmp/youdonotknowfile",O_WRONLY|O_CREAT|O_TRUNC,0664);
dup2(fd2,1); //子进程负责输出
execvp(arg[0],arg);
exit(0); //字进程退出
}
if(waitpid(pid2,&status2,0)==-1)
{
printf("wait for child process error %d\n",__LINE__);
}
if(!(find_command(argnext[0])))
{
printf("%s :command not found %d\n",argnext[0],__LINE__);
}
fd2 = open("/tmp/youdonotknowfile",O_RDONLY);
dup2(fd2,0); //父进程负责输入
if(remove("/tmp/youdonotknowfile")) printf("remove error %d\n",__LINE__);
execvp(argnext[0],argnext);
exit(0);
}
break;
default:
break;
}
//若命令中有&,表示后台执行,父进程直接推出,不等待子进程结束
if(background == 1)
{
printf("process id %d\n",pid);
return;
}
//父进程等待子进程结束
if(waitpid(pid,&status,0)==-1)
{
printf("wait for child porcess error %d\n",__LINE__);
}
}
int find_command(char *command)
{
DIR* dp;
struct dirent* dirp;
char *path[]={
"./","/bin","/usr/bin",NULL};
if(strncmp(command,"./",2)==0)
command = command +2;
int i = 0;
while(path[i]!=NULL)
{
if((dp = opendir(path[i]))==NULL)
printf("can not open /bin %d\n",__LINE__);
while((dirp = readdir(dp))!=NULL)
{
if(strcmp(dirp->d_name,command)==0)
{
closedir(dp);
return 1;
}
}
closedir(dp);
i++;
}
return 0;
}
核心的思想
核心的思想就是通过exec函数族来实现,当进程调用一种e x e c函数时,该进程完全由新程序代换,而新程序则从其 main函数开始执行。
因为调用exec并不创建新进程,所以前后的进程 I D并未改变。 exec只是用另一个新程序替换了
当前进程的正文、数据、堆和栈段,进程还是原来的那个进程
另外,每次我们执行输入的命令的时候,必须要fork一个进程,用子进程去执行我们输入的命令,另外,命令其实就是一个可执行的程序而已.这样的话我们的父进程就不会死掉,也就一直循环输入命令了,也就是可以一直执行可执行程序了。
exec函数族
下面是函数原型
int execl(const char * pathname, const char *arg 0, ... /* (char *) 0 */);
int execv(const char * pathname, char *const a rgv [] );
int execle(const char * pathname, const char *arg 0, ...
/* (char *)0, char *const e n v p [] */);
int execve(const char * pathname, char *const argv [], char *const envp [] );
int execlp(const char * filename, const char *arg 0, ... /* (char *) 0 */);
int execvp(const char * filename, char *const argv []
这里我们使用的是execvp函数。所以do_cond之前的部分都是在为了解析出execvp的参数,构建好参数让父进程的子进程来执行。
cd命令必须要我们自己构建
也就是会所cd命令是一个内置的命令
我们在终端下面用type判断一个命令是不是内置的命令
❯ type cd
cd is a shell builtin
可以看出来cd命令就是一个内置的命令,也就是说cd不是一个已经存在的可执行程序。
原因
如果我们要切换目录的话,要调用的函数是chdir。
因为当前工作目录是一个进程的属性,所以它只影响调用 c h d i r的进程本身,而不影响其他
进程。也就是说如果我们自己写一个cd程序出来,那么在我们cd的时候,是执行的是cd本身,程序本身的工作目录是没有改变的。
环境变量
环境变量是至关重要的东西,因为环境变量觉得了我们exec函数搜索的路径,也就是说我们的exec函数需要在环境目录里面的目录里面去找可执行程序。
另一方面,我们输入的命令其实就是程序,默认就是在环境变量的目录里面去寻找可执行的程序,换句话说,如果我们把可执行程序放到了环境变量里面,我们运行我们的可执行程序的时候就可以直接输入程序的名字了。
echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/bin:/var/lib/flatpak/exports/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl
fork函数
fork函数就是要生成一个子进程,子进程的返回值为0,父进程的返回值是子进程的pid。如果生成的子进程失败,返回-1。
父进程和子进程共享文件表项,也就是说我们对文件读写的时候是可以同步,是不会出错的,还有缓冲区也是会继承的。
子进程和父进程继续执行 fork之后的指令。子进程是父进程的复制品。例如,子进程获得
父进程数据空间、堆和栈的复制品。注意,这是子进程所拥有的拷贝。父、子进程并不共享这些存储空间部分。如果正文段是只读的,则父、子进程共享正文段。
子进程继承的其他属性
• 实际用户I D、实际组I D、有效用户I D、有效组I D。
• 添加组I D。
• 进程组I D。
• 对话期I D。
• 控制终端。
• 设置-用户- I D标志和设置-组- I D标志。
• 当前工作目录。
• 根目录。
• 文件方式创建屏蔽字。
• 信号屏蔽和排列。
• 对任一打开文件描述符的在执行时关闭标志。
• 环境。
• 连接的共享存储段。
• 资源限制。
父、子进程之间的区别是:
• fork的返回值。
• 进程I D。
• 不同的父进程I D。
• 子进程的t m s _ u t i m e , t m s _ s t i m e , t m s _ c u t i m e以及t m s _ u s t i m e设置为0。
• 父进程设置的锁,子进程不继承。
• 子进程的未决告警被清除。
• 子进程的未决信号集设置为空集
signal函数
函数原型
#include <signal.h>
void (*signal (int signo, void (*func)(int))) (int);
signo是信号名,也就是产生的信号的名字,fanc也就是产生这个信号的时候我们的操作。signal的返回值是指向之前的信号处理程序的指针。
可以要求系统在某个信号出现时按照下列三种方式中的一种进行操作。
(1) 忽略此信号。这里的 func就是SIG_IGN
(2) 捕捉信号。func 我们自己写的函数
(3) 执行系统默认动作 。SIG_DFL 默认操作
dup和dup2函数
可以复制一个现存的文件描述符。
函数原型
#include <unistd.h>
int dup(int filedes) ;
int dup2(int filedes, int filedes2) ;
两函数的返回:若成功为新的文件描述符,若出错为- 1
由d u p返回的新文件描述符一定是当前可用文件描述符中的最小数值。用 dup2则可以用filedes 参数指定新描述符的数值。如果filede s 2已经打开,则先将其关闭。如若filedes等于filedes 2,则dup2返回filedes2,而不关闭它。这些函数返回的新文件描述符与参数filede s共享同一个文件表项。
也就是说当我们dup(fd,1)的时候,fd和1这个文件描述符都指向了一个文件表项。
我们就是利用这一点来实现了管道这个命令,子进程在fork一个子进程,fork之后的子进程写东西到文件里面,然后父进程读取子进程写的东西。也就是说当我们使用管道的时候,输入的左边的命令所运行的程序在这里相当于是子进程,右边的进程是父进程。