目录
分析和计算(evaluate)用户输入,执行内部命令或可执行文件
建立argv数组,返回命令属性(foreground / background)
头文件
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <setjmp.h>
#include <signal.h>
#include <dirent.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <errno.h>
#include <math.h>
#include <pthread.h>
#include <semaphore.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXARGS 128
#define MAXLINE 8192 /* Max text line length */
void eval(char* cmdline);
int builtin_command(char** argv);
int parseline(char* buf, char** argv);
void print_working_directory();
void list(char* filename);
void concatenate(char* filename);
void remove_file(char* filename);
void touch(char* filename);
void directly_systemcall_exit();
实验步骤
主函数main
确定shell的运行模式
- 使用signal系统调用设置SIGCHLD信号的处理函数(回收僵尸进程)
- 打印出提示符
- 从键盘(标准输入)获取一条命令
- 分析和运行该命令
代码
int main() {
char cmdline[MAXLINE];
if (signal(SIGCHLD, sigchld_handler) == SIG_ERR)
printf("signal error\n");
while (1) {
printf("> ");
fgets(cmdline, MAXLINE, stdin);
if (feof(stdin)) exit(0);
eval(cmdline);
}
}
核心函数eval
分析和计算(evaluate)用户输入,执行内部命令或可执行文件
- 首先对输入的命令进行语法分析(parse),并建立argv数组
- 然后检查argv数组的第一项argv[0],检查其是否为内部命令(builtin command)
- 如果是内部命令,那么执行该命令
- 如果不是内部命令,那么将其视作可执行文件,调用fork系统调用创建一个子进程,然后在子进程中调用execve系统调用来为该可执行文件创建进程上下文
- 对于为可执行文件生成的进程,检查其为前台进程(foreground)还是后台进程(background),如果是前台进程,那么在父进程中调用waitpid系统调用来等待其终止;如果是后台进程,输出其进程号和当前命令行,回到主函数
代码
void eval(char* cmdline)
{
char* argv[MAXARGS]; //argument list execve()
char buf[MAXLINE]; //holds modified command line
int bg; //should the job run in bg or fg?
pid_t pid; //process id
strcpy(buf, cmdline);
bg = parseline(buf, argv);
if (argv[0] == NULL) return; //ignore empty lines
if (!builtin_command(argv)) {
if ((pid = fork()) == 0) { //child runs user job
if (execve(argv[0], argv, NULL) < 0) {
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}
/* parent waits for foreground job to terminate */
if (!bg) {
int status;
if (waitpid(pid, &status, 0) < 0)
printf("waitfg: waitpid error\n");
}
else printf("%d %s", pid, cmdline);
}
return;
}
命令行解析函数parseline
建立argv数组,返回命令属性(foreground / background)
- 用空格作为分隔符,来划分用户输入的字符串,每个子串首地址作为argv数组的一项,并将argv数组尾元素的下一个位置设为NULL
- 检查argv数组的最后一个字符串是否为“&”,如果是,那么该命令生成一个后台进程(bg=1);否则该命令生成一个前台进程(bg=0)
代码
/* parseline - Parse the command line and build the argv array */
int parseline(char* buf, char** argv)
{
char* delim; //points to first space delimiter
int argc; //number of args
int bg; //background job?
buf[strlen(buf) - 1] = ' '; //replace trailing '\n' with space
while (*buf && (*buf == ' ')) ++buf; //ignore leading spaces
/* build the argv list */
argc = 0;
while ((delim = strchr(buf, ' '))) {
argv[argc++] = buf;
*delim = '\0';
buf = delim + 1;
while (*buf && (*buf == ' ')) ++buf; //ignore spaces
}
argv[argc] = NULL;
if (argc == 0) return 1; //ignore blank line
/* should the job run in the background? */
if ((bg = (*argv[argc - 1] == '&')) != 0)
argv[--argc] = NULL;
return bg;
}
内部命令处理函数builtin_command
判断命令属性,运行内部命令
- 如果输入参数是内部命令,那么运行该命令,并返回1(true)
- 否则返回0(false)
代码
/* if first arg is a builtin command, run it and return true */
int builtin_command(char** argv)
{
if (!strcmp(argv[0], "pwd")) {
print_working_directory();
}
else if (!strcmp(argv[0], "ls")) {
list(argv[1]);
}
else if (!strcmp(argv[0], "cat")) {
concatenate(argv[1]);
}
else if (!strcmp(argv[0], "rm")) {
remove_file(argv[1]);
}
else if (!strcmp(argv[0], "touch")) {
touch(argv[1]);
}
else if (!strcmp(argv[0], "exit")) {
directly_systemcall_exit();
}
else if (!strcmp(argv[0], "&")); // ignore singleton &
else return 0;
return 1;
}
信号处理函数sigchld_handler
保持errno不变,回收所有子进程
- 保存errno变量
- 调用waitpid系统调用,第一个参数设置为-1,则等待集合为当前进程的所有子进程;第二个参数设置为NULL,忽略子进程的状态信息;第三个参数设置为0,表示默认状态,即挂起当前进程的执行,直到等待集合中一个子进程的终止
- 恢复errno变量
代码
void sigchld_handler(int sig) {
int olderrno = errno;
while (waitpid(-1, NULL, 0) > 0);
if (errno != ECHILD) {
printf("waitpid error\n");
exit(1);
}
errno = olderrno;
}
实现内部命令
pwd:显示当前目录
void print_working_directory() {
char dirname[MAXLINE];
getcwd(dirname, MAXLINE);
printf("%s\n", dirname);
}
ls:显示指定目录下的所有文件
void list(char* filename) {
DIR* dir = opendir(filename);
struct dirent* ptr;
int i;
while (ptr = readdir(dir)) {
printf("%s\n", ptr->d_name);
}
closedir(dir);
}
cat:显示文件内容
void concatenate(char* filename) {
int ch;
FILE* fp;
if ((fp = fopen(filename, "r")) == NULL) {
printf("Can't open %s\n", filename);
exit(1);
}
while ((ch = getc(fp)) != EOF) {
putc(ch, stdout);
}
fclose(fp);
}
rm:删除文件
void remove_file(char* filename) {
int ret = remove(filename);
if (ret) {
printf("Remove %s failed.\n", filename);
}
else {
printf("Remove %s sucessed.\n", filename);
}
}
touch:建立空文件
void touch(char* filename) {
FILE* fp = fopen(filename, "w");
fclose(fp);
}
exit:终止当前进程(直接调用了_exit系统调用)
void directly_systemcall_exit() {
_exit(0);
}
创建专属命令——myshell
(本地用户的家目录为/home/kku19099/)
可以看到PATH环境变量的最后一项为/home/kku19099/bin,因此编写一个名为myshell的shell脚本,放入家目录中的bin目录中,即可通过直接在命令行输入myshell来运行我们的命令行解释程序。
编写myshell脚本
- 用一个变量记录下当前目录
- 跳转到我们命令行解释程序所在的目录
- 使用make清理之前的残留目标文件,生成一个新的可执行目标文件main
- 执行main
- 返回原目录
更改脚本权限
增加用户可执行权限:
然后回到根目录,输入myshell就可以运行我们自己的shell了
技术难点和解决方案
如何正确的加载和运行一个可执行文件
解决
- 使用fork系统调用生成一个子进程
- 在子进程中调用execve系统调用来为可执行文件创建进程上下文
如何为execve系统调用生成合适的参数
解决:
execve函数接受三个参数,分别为可执行文件名,参数列表和环境变量列表
可执行文件名是我们已有的,环境变量列表可设置为NULL,那么主要工作就是准备合适的参数列表,这需要分析用户的输入来创建argv数组,将argv数组作为execve函数的第二个参数。
如何一次性完成对数十个C程序的编译和链接
解决:
使用make进行宏编译,这需要了解make指令的用法,make指令的行为,以及最重要的——makefile文件的编写。
makefile文件的编写
解决:
变量OBJS
因目标文件众多,所以使用一个变量OBJS来记录所有的目标文件的名称,之后可由OBJS来替换所有目标文件的出现
目标main
- 编译所有源文件,生成对应的目标文件
- 链接所有目标文件,生成可执行目标文件main
- 列出所有目标文件和可执行目标文件main
目标clean
- 清除掉所有目标文件和可执行目标文件main
- 列出当前目录下所有文件
具体内容
测试
myshell命令
前台进程
/home/kku19099/bin/ans_yn.sh是一个和用户交互的可执行文件
后台进程
/home/kku19099/bin/cal_1_100.sh计算1+2+…+100
内部命令
pwd
ls
cat
touch
rm
exit