问题描述
实验一利用课本第9页程序1-5的框架,实现允许输入命令带参数的简单shell。原来的实现是不能够带参数的。输入命令所能带的参数个数,只受到系统键盘输入缓冲区长度(以及shell输入缓冲区长度)的限制,该缓冲区的缺省长度是4096个字节。
实现时要解决的主要问题有:
1. 正确确理解并使用系统调用fork(),execve()和waitpid(),特别是execve()函数。fork()函数创建一个新的进程。新进程就是所谓的子进程,它是执行fork()函数的进程(父进程)的“克隆”,也就是说,子进程执行的程序与父进程的完全一样。当fork()函数返回值为0时表示处于子进程中;而返回值大于0时表示处于父进程中,此时的返回值是子进程的进程id。因此,fork()的返回值可以用来划分仅仅适合父进程和子进程执行的程序段。fork()函数返回值为-1时表示出错。
如果子进程只是运行与父进程完全一样的程序,那用处是很有限的。要让子进程运行不同于父进程的程序,就必须调用execve函数,它是所有其他exec函数的基础。execve函数把调用它的进程的程序,替换成execve函数的参数所指定的程序。运行execve函数成功后,进程将开始运行新的程序,也就是execve函数的参数所指定的程序。
execve函数原型:int execve(const char *path, const char *argv[],const char *envp[]);
其中:
- path:要执行的程序路径名,比如“/bin/ls”,“cd”,“/usr/bin/gcc”等等。
- argv:参数表,比如ls命令中可带的命令行参数-l,-a等。注意,argv的第一个元素必须是要执行的程序(命令)的路径名。
- envp:环境变量表,供要执行的命令使用。实参数用NULL或系统环境变量environ均可。注意,因为environ由系统提供,属于外部变量,所以说明时必须用“extern”修饰。
例子:
char *argv[] = {“gcc”, “-g”, “-c”, “hello.c”, NULL};
char *argv1[] = {“/bin/ls”, “-l”, “-a”, NULL};
execve(“/usr/bin/gcc”, argv, environ); // 编译程序“hello.c”
execve(“/bin/ls”, argv1, NULL); // 执行命令“ls –l –a”
execve(“/usr/ls”, argv1, NULL); // 出错,因为目录/usr/下没有ls程序。
// 注意,在argv1 的第一个字符串“/bin/ls”中,只有ls是有用的。
系统调用waitpid()用于等待子进程结束、获取子进程的运行状态,详细说明在第八章。本实验仅仅用它使父进程等待子进程结束,因此维持程序1-5的用法即可。
2. 根据简单shell的输入,构造execve函数的参数。
根据程序1-5,数组buf保存用户的输入,包括命令和参数。由于shell命令的命令名和各参数之间是用空格分开,因此可以用空格作为分界符。通过一个循环可以把buf数组中的命令和各个参数依次分离开来,并赋给数组argv的各元素适当的指针值。argv数组的最后一个指针必须是NULL。接着就可以调用execve(argv[0],argv, environ)来执行用户输入的命令。
提示:argv数组中各指针所指向的字符串,可以直接利用buf的存储空间,不需要另外分配内存。
3. 正确编译程序。
由于书中例子用到了本书第一版中作者自己定义的error.c文件,因此编译时要记得把error.c文件和源代码文件一起编译,error.c文件可以到cs8的/home/luwei/common目录下复制。可以用如下命令编译:
gcc <源文件名> error.c –o<可执行文件名>
问题解决
概括来说,解决的核心问题就是如何将用户的输入 /bin/ls -l -h
以 {“/bin/ls”, “-l”, “-a”, NULL}
的形式存储到 argv1[]
中,也就是将一个字符串分成多个数组元素。
要在空格处断开,使用到的技巧是将空格变为 \0
,因为字符串是靠 \0 (等价于 0)
来确定结束位置的,如:
char buf[] = "/bin/ls -l";
char *argv;
argv = buf;
printf("%s\n", argv); /* 输出为 "/bin/ls -l" */
buf[7] = 0; /* 将空格转换为 '\0' */
printf("%s\n", argv); /* 这时输出的是 "/bin/ls" */
理解了上面这段代码,整个问题就不难理解了。代码如下:
/* Unix 带参数的简单Shell, author: 给力星 */
#include "apue.h"
int main(void) {
char buf[MAXLINE];
pid_t pid;
int status;
printf("%% ");
while(fgets(buf, MAXLINE, stdin) != NULL) {
if (buf[strlen(buf) -1] == '\n')
buf[strlen(buf) -1] = 0;
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) { /* child */
//execlp(buf, buf, (char *)0);
char *argv[100]; /* 利用buf的存储空间,分配一个字符指针数组即可 */
/* 处理用户输入的命令,简单的实现 */
int i, len, index = 0;
argv[index++] = buf;
for (i = 0, len = strlen(buf); i < len; i++) {
if (buf[i] == ' ') {
buf[i] = 0;
if (buf[i+1] != ' ') {
argv[index++] = &buf[i+1];
}
}
}
argv[index] = NULL; /* argv最后一个为NULL */
/* 处理完毕 */
execve(argv[0],argv, NULL);
err_ret("couldn't execute: %s", buf);
exit(127);
}
if ((pid = waitpid(pid, &status, 0)) < 0)
err_sys("waitpid error");
printf("%% ");
}
exit(0);
}
这个是简单的做法,能处理 "/bin/ls -l -a" 这样的输入。不过处理用户输入的命令这边的做法比较弱,不能处理有多余空格的情况。比如 " /bin/ls -l -a " 这样含有多余的空格的命令,上面的程序会出错,而在系统的shell里是可以正确运行的。
纠正这个问题的处理代码如下:
/* 处理用户输入的命令,能处理多余空格 */
int i = 0, len, index = 0;
while(buf[i] == ' ') { /* 处理开头的空格*/
i++;
}
argv[index++] = buf+i; /* 或 &buf[i],第一个非空格的位置 */
for (len = strlen(buf); i < len; i++) {
if (buf[i] == ' ') {
buf[i] = 0;
} else {
/* 需防止越界,非空格字符的前一个字符为\0,才是参数的起始位置 */
if ( (i-1 >= 0) && (buf[i-1] == 0) ) {
argv[index++] = buf+i;
}
}
}
argv[index] = NULL; /* argv最后一个为NULL */
/* 处理完毕 */
其实更简单的是利用 strtok()
函数,可以完美的实现上述拆分功能,具体用法请自行谷歌~ 也可以在下面的进阶部分看到其用法。
进阶
这样就好了吗?还可以更进一步!(注:本科生跟研究生课程的实验要求不一样,本科生的实验不要求使用PATH变量。)
书上的程序中,execlp(buf, buf, (char *)0)
是可以处理 ls -l
这样的命令的,因为 execlp()
会根据系统中的PATH路径(通过命令 echo $PATH
可以看到)来查找程序,也就是最终会查找到 /bin/ls
并执行。
但 execve()
不具备这样的功能,如果要实现类似 execlp()
的功能,则需要自己去遍历PATH路径,在每个PAT目录下尝试运行。将 execve(argv[0],argv, NULL);
修改为如下代码:
// execve(argv[0],argv, NULL);
char *pathEnv;
pathEnv = getenv("PATH"); /* 获取PATH系统变量 */
// printf("PATH =: %s\n", pathEnv);
if (NULL == pathEnv) {
err_sys("couldn't get PATH variable");
exit(0);
}
if (NULL != strstr(argv[0], "/")) { /* 命令带目录,直接执行 */
execve(argv[0], argv, NULL);
} else { /* 命令不带目录,需自行遍历 */
char *pathItem, path[100];
pathItem = strtok(pathEnv, ":"); /* 使用strtok拆分 */
while (NULL != pathItem) { /* 对每个PATH路径尝试运行 */
strcpy(path, pathItem);
strcat(path, "/");
strcat(path, argv[0]); /* 合成完整路径 */
// printf("pathItem: %s\n", pathItem);
// printf("path: %s\n", path);
execve(path, argv, NULL);
pathItem = strtok(NULL, ":");
}
}
这样子就可以直接运行 ls -l -a
这样的命令了。
另外简单shell也不是神马命令都能执行,如 cd
,这个貌似是系统命令?而不是程序。
关于 environ
关于 execve(argv[0], argv, environ)
中的 environ,这个environ是用于向子进程传输系统的环境变量的。前面说到用 NULL
就行,如果要传入 environ,首先应声明 extern char **environ;
,有兴趣的可以把这个变量的内容数出来看看你就懂了。
实际上 main()
中可以有三个参数: main(int argc, char **arg, char *envp[])
,其中的 envp
就等价于 extern char **environ;
。可以输出看下,内容是一样的。
如果我们执行 execve(argv[0], argv, NULL)
,那么子进程中的 envp
就是空的,也就是说不能通过 getenv("PATH")
获取到PATH内容。可以运行上面进阶的代码编译后的程序,如 ./shell
,然后在简单shell中再次执行 ./shell
,就会提示couldn't get PATH variable。
避免这种情况,就得传入 environ
:
int main(void) {
extern char **environ;
...
execve(path, argv, environ);
...
}
友情提示: 代码要看懂并理解,否则 copy 去应付作业是没有用的,考试时不会也白搭~