大三的时候,那时候因为课程需要,我们调试过my_shell.c程序。当时只是对整个程序的结构了解,在涉及linux系统调用函数的时候就不是很清楚”为什么会这样做?”。my_shell.c程序核心函数便是do_cmd函数,此函数是整个程序的核心,负责对用户输入的命令进行执行。
本文便是分析do_cmd函数,以达到对my_shell.c程序有深入的理解。此函数会涉及文件操作中的相关系统调用函数,如果你和我一样调试并分析过前面所说的my_ls.c程序,那么接下来所述的内容对你来说并不困难。
我们按照如下方式定义do_cmd函数:
void do_cmd(int argcount, char arglist[100][256]);
其中,argcount是统计用户输入命令中选项的个数,arglist数组存储每个选项。比如输入:ls -l /那么此时argcount=3,而arglist[0],arglist[1],arglist[2]中分别存储的是:ls,-l,/。
首先此函数会检查用户输入的命令行参数中是否存在后台运行符。利用for循环,对arglist数组中的每个参数与”&”进行比较。由于arglist中的元素是字符串,那么应该选用strcmp函数,而不是利用==来判断。此外即便找到了后台运行符,还应该检查是否”&”位于参数末尾。如果i==argcount-1为假,那么便出错。实现代码如下:
//check if the command include character of running in the background
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\n");
return;
}
}
}//for
接下来,检测命令行参数是否含有>,<,|符号,检测方法与上出代码原理类似。由于本程序要求用户输入的命令行参数只能包含上述符号其中之一,所以当flag大于一时,错误。参数中符号合法,接下来提取文件名,存于字符串file中。程序中分别有三个if语句依次针对出现>,<,|时的情况。我们只要分析出现管道符号的情况。
在分析之前首先弄起出管道符号的作用。比如下面命令:
edsionte@edsionte-laptop:~$ ls -l / | wc -c
1513
管道符号前后分别是两个shell命令,前命令的输出作为后命令的输入。
如果检测出命令行中含有管道符号,即how=have_pipe。然后将命令行中两个shell命令分离出来,前命令依然存于arg数组中,而后命令存于argnext数组中。实现命令如下:
if(how==have_pipe)
{
for(i=0;arg[i]!=NULL;i++)
{
if(strncmp(arg[i],"|",1)==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;
}
}
}
接下来就要用到进程控制的内容了。我们fork一个新的进程,让它去执行命令行参数中的命令。当命令行中不包含管道符号时,只需fork一个进程,比如ls -l / > a;我们只需fork一个新进程,让其(if(pid==0))执行ls命令即可;否则,我们必须fork两个信进程,前一个进程执行|前的命令,另外一个进程执行|后的命令。至于另个进程如何”通信”,如何让前一个命令的输出称为后者的输入?不着急,后面会详细说明。
1.如果所输命令中不包含<,>,|,how==normal(0);那么调用exe族函数即可让子进程去执行所输的命令。如下:
if(pid==0)
{
if(!(find_command(arg[0])))
{
printf("%s: command not found\n",arg[0]);
exit(0);
}
execvp(arg[0],arg);
exit(0);
}
find_command函数是在指定目录下去查找命令arg[0]对应的可执行程序。
2.如果命令行中包括输出重定向符号>,how==out_redirect(1);那么需在上述代码中exe函数前添加下面的代码:
fd=open(file,O_RDWR|O_CREAT|O_TRUNC,0644);
dup2(fd,1);//make the file as a standard output stream
execvp(arg[0],arg);//execute the command
前面我们已经提到file中存储的是输出重定向(或输入重定向)的文件名,因此首先应以可读可写方式打开一个文件,并且档要打开的文件存在时,会清空源文件数据,这些我们在前面的博文中都有解释。我们知道标准输出输入的文件描述符为:1,0,利用dup2函数就可以将fd,1同时指向file文件,实现将标准输出重定向到我们刚已打开的文件。
3.如果命令行中包含输入重定向符号<,how==out_redirect(2);那么添加如下代码:
fd=open(file,O_RDONLY);
dup2(fd,0);//make the file as a standard output stream
execvp(arg[0],arg);//execute the command
这里只涉及到读取文件,因此以只读方式打开文件。
4.如果命令行中包含管道符号,how==have_pipe(3);如同我们在上面所述的,子进程还需再fork一个进程(暂且叫它子子进程,child_child_process)。首先让子子进程去执行管道符号前面的shell命令,并将输出结果暂存于tempfile文件中;让子进程去执行管道符号后面的shell命令,并读取tempfile文件,将其作为第二个命令的输入。这样,子进程和子子进程实现了管道”通信”。具体代码实现其实是上述2和3的结合,具体如下:
if(pid==0)//child_process
{
int pid2;
int status2;
int fd2;
if((pid2=fork())<\0)
{
printf("fork2 error\n");
return;
}
else if(pid2==0)//child_child_process
{
if(!(find_command(arg[0])))
{
printf("%s: command not found\n",arg[0]);
exit(0);
}
fd2=open("/tmp/tempfile",O_WRONLY|O_CREAT|O_TRUNC,0644);
dup2(fd2,1);
execvp(arg[0],arg);
exit(0);
}
if(waitpid(pid2,&status2,0)==-1)//waiting for child_child_process
{
printf("wait for child_child process error\n");
}
if(!(find_command(argnext[0])))
{
printf("%s:command not found\n",argnext[0]);
exit(0);
}
fd2=open("/tmp/tempfile",O_RDONLY);
dup2(fd2,0);
execvp(argnext[0],argnext);
if(remove("/tmp/tempfile"))
{
printf("remove error\n");
}
exit(0);
}
上出1~4处代码需要一个switch语句来组织,根据how参数来选择相应代码,进行执行。另外,由于上述代码中设计两次fork进程,因此要弄清楚父子进程的关系,其次还需了解代码4中waitpid函数的作用。由于子子进程和子进程同组,因此子进程等待子子进程结束。
同样,父进程需要等待子进程执行完毕。但是如果命令行中包含后台运行父&,那么附近成可直接返回,不用等待子进程。这些功能的实现代码如下:
if(background==1)
{
printf("process id %d\n",pid);
}
if(waitpid(pid,&status,0)==-1)
{
printf("wait for child process error\n");
}
至此,我们分析完了do_cmd函数的功能,完整代码可以参考这里。