cs162作业hw-shell hw-shell
在给出的架构中使用c语言实现一个shell,完成内置函数”pwd, cd”,运行已有程序,运行环境变量中的程序,输入输出重定向,管道,信号处理 的功能。
内置函数(Directory commands) 使用结构体 $fun_desc$ 封装内置函数,使用 $cmd_table$ 数组来标识每一个内置函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 typedef int cmd_fun_t (struct tokens* tokens) ;typedef struct fun_desc { cmd_fun_t * fun; char * cmd; char * doc; } fun_desc_t ; fun_desc_t cmd_table[] = { {cmd_help, "?" , "show this help menu" }, {cmd_exit, "exit" , "exit the command shell" }, {cmd_pwd, "pwd" , "print the current working directory to standard output" }, {cmd_cd, "cd" , "change the current working directory to that directory" }, };
其中 $cmd_help$ 和 $cmd_exit$ 是已经实现好的,$cmd_pwd$ 和 $cmd_cd$ 的实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 int cmd_pwd (struct tokens* tokens) { char *buffs = getcwd(NULL , 0 ); printf ("%s\n" , buffs); free (buffs); return 1 ; } int cmd_cd (struct tokens* tokens) { const char HOME_PATH[14 ] = "/home/vagrant" ; if (tokens_get_length(tokens) == 1 ) { chdir(HOME_PATH); } else if (tokens_get_length(tokens) == 2 && strcmp (tokens_get_token(tokens, 1 ), "~" ) == 0 ) { chdir(HOME_PATH); } else { int failed = chdir(tokens_get_token(tokens, 1 )); if (failed) { printf ("error\n" ); } } return 1 ; }
运行已有程序(Program Execution) 使用 $execv$ 函数进行实现。
$execv$ 函数原型
1 int execv (const char *pathname, char *const argv[]) ;
其中 $pathname$ 为需要运行的程序的相对路径或绝对路径,$argv$ 为参数列表。
$argv$ 列表的最后一项一定需要赋值为 $NULL$。
此函数会运行程序 $pathname$,使用运行程序的进程将调用该函数的进程覆盖掉。此函数只有在发生错误时才会返回 ,返回值为 $-1$。
我在此使用了一个 $configuration$ 结构体来表示运行一个程序的一些设置(参数列表,输入输出重定向,管道等信息)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef struct Configuration { char **arglist; int redirection; int read_file_fd; char *read_file_path; int write_file_fd; char *write_file_path; int **pipe; int pipe_num, cur_num; } Config;
运行程序
1 2 execv(whole_path, config->arglist);
运行环境变量中的程序(Path resolution) 使用 $getenv$ 函数获得环境变量。
$getenv$ 函数原型
1 char *getenv (const char *name) ;
The getenv() function searches the environment list to find the environment variable name, and returns a pointer to the corresponding value string.
即该函数会返回环境变量 $name$ 的指针,如果没有匹配的环境变量则返回 $NULL$ 。
注:由于该函数返回的是环境变量本身的指针,因此在使用时必须保证不会修改该指针所指的信息
使用时最好创建一个副本进行操作。
使用 $strtok_r$ 函数进行切割字符串。
$strtok_r$ 函数原型
1 char *strtok_r (char *str, const char *delim, char **saveptr) ;
其中 $str$ 为需要切割的字符串,$delim$ 为切割依据的集合,如按照 $|$ 和 $%$ 进行切割的话,$delim$ 即为 $|%$ ,$saveptr$ 为进行切割时的标记。
返回值为一个字符串的首地址(每次调用返回下一个部分的首地址),如果已经结束,则返回 $NULL$。
对于一个特定的字符串,第一次调用该函数的时候需要将 $str$ 参数设置为字符串的地址,$delim$ 即为切割依据的集合,$saveptr$ 为一个指针的地址。第一次调用之后,$saveptr$ 将会被赋予一个值,用于标记这个字符串的切割情况。之后的每次调用需要将 $str$ 置为 $NULL$,$delim$ 依然为切割依据的集合,$saveptr$ 需和之前的保持一致,用于标识这个字符串的切割情况。直到该函数返回 $NULL$ 结束。
注:该函数会修改原字符串
使用 $opendir$ , $readdir$ , $closedir$ 函数读取文件夹中的所有文件。
函数原型
1 2 3 4 5 6 7 8 9 10 11 12 DIR *opendir (const char *name) ;struct dirent *readdir (DIR *dirp) ;int closedir (DIR *dirp) ;struct dirent { ino_t d_ino; off_t d_off; unsigned short d_reclen; unsigned char d_type; char d_name[256 ]; };
$opendir$ 依据 $name$ 打开一个文件夹流,并返回一个 $DIR$ 结构体的指针。
$readdir$ 会依次读取文件夹中的每个文件,返回一个 $dirent$ 结构体指针,直到返回 $NULL$ 。
$closedir$ 即为关闭文件夹流。
从环境变量中寻找所需文件的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 char *ORI_PATH = getenv("PATH" );char *PATH = malloc (sizeof (char ) * (strlen (ORI_PATH) + 1 ));strcpy (PATH, ORI_PATH);char *flagptr, *token;for (int i = 0 ; ; ++i) { token = strtok_r(PATH, ":" , &flagptr); if (PATH != NULL ) { free (PATH); PATH = NULL ; } if (token == NULL ) { break ; } DIR *dir = opendir(token); if (dir == NULL ) { continue ; } struct dirent *file ; while (1 ) { file = readdir(dir); if (file <= 0 ) { break ; } if (strcmp (path, file->d_name) == 0 ) { found = true ; whole_path = malloc (sizeof (char ) * (strlen (token) + strlen (file->d_name) + 2 )); strcpy (whole_path, token); whole_path[strlen (token)] = '/' ; strcpy (whole_path + strlen (token) + 1 , file->d_name); break ; } } closedir(dir); if (found) break ; }
之后使用 $execv$ 运行程序即可。
输入输出重定向(Redirection) 实现类似于[process] > [file]和[process] < [file]的输入输出重定向。
即,将程序的标准输入、标准输出重定向到某个文件中。
在重定向的检测中就知识在检测<和>的位置,以及重定向符号后面的文件是否存在。
在重定向中,使用到了dup2函数
函数原型
1 int dup2 (int oldfd, int newfd) ;
使用newfd代替oldfd的文件描述符
0为stdin,1为stdout。
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int read_fd = -1 , write_fd = -1 ;if (config->redirection == 1 ) read_fd = open(config->read_file_path, O_RDONLY); else if (config->redirection == 2 ) write_fd = open(config->write_file_path, O_WRONLY | O_CREAT); else if (config->redirection == 3 ) { if (strcmp (config->read_file_path, config->write_file_path) == 0 ) { read_fd = write_fd = open(config->read_file_path, O_RDWR); } else { read_fd = open(config->read_file_path, O_RDONLY); write_fd = open(config->write_file_path, O_WRONLY | O_CREAT); } } if (read_fd != -1 ) dup2(read_fd, 0 ); if (write_fd != -1 ) dup2(write_fd, 1 );
管道(Pipes) 管道详解
需要完成类似于[process A] | [process B] | [process C]的功能
需要用到函数int pipe(int fds[2])
函数原型
1 int pipe (int pipefd[2 ]) ;
管道通信的原理
使用pipe函数创建了两个文件描述符,pipefd[0]和pipefd[1],这就是一个管道的读取通道和写入通道。
使用dup2函数将标准输入输出重定向到管道即可实现管道通信。
1 2 3 4 5 if (read_fd != -1 ) dup2(read_fd, 0 ); if (write_fd != -1 ) dup2(write_fd, 1 );
管道的内部实现是一个循环队列,写入端通过pipefd[1]将字符写道管道中,如果管道是满的,那么就会阻塞该进程,待管道不满之后,继续写入。读取端通过pipefd[0]进行读取,如果管道是空的,会阻塞该进程,直到管道非空。
注:一定要保证管道读取和写入结束之后,把文件描述符关闭掉,否则会造成进程阻塞,结束不了。
管道的创建
1 2 3 4 5 6 7 8 9 10 11 12 13 int **pipe_fd;pid_t *pid;my_token *token = (my_token*)malloc (sizeof (my_token)); token->tokens_length = 0 ; token->tokens = NULL ; int pgrp = 0 ;pipe_fd = malloc (sizeof (int *) * pipe_num); for (int i = 0 ; i < pipe_num; ++i) { pipe_fd[i] = malloc (sizeof (int ) * 2 ); pipe(pipe_fd[i]); } pid = malloc (sizeof (int ) * (pipe_num + 1 ));
在每一个子进程中需要将所有无关的管道都给关闭掉
1 2 3 4 5 6 7 8 for (int i = 0 ; i < config->pipe_num; ++i) { for (int j = 0 ; j < 2 ; ++j) { if ((config->pipe[i][j] != config->read_file_fd) && (config->pipe[i][j] != config->write_file_fd)) { close(config->pipe[i][j]); } } }
进行管道的输入输出重定向
1 2 3 4 5 if (read_fd != -1 ) dup2(read_fd, 0 ); if (write_fd != -1 ) dup2(write_fd, 1 );
同理,在父进程中,需要将所有的管道都关闭
1 2 3 4 for (int i = 0 ; i < pipe_num; ++i) for (int j = 0 ; j < 2 ; ++j) close(pipe_fd[i][j]);
之后,等待所有的子进程结束
1 2 3 for (int i = 0 ; i <= pipe_num; ++i) waitpid(pid[i], NULL , 0 );
信号处理 使用setpgid函数改变进程所属的进程组
使用tcsetpgrp(int fd, pid_t pgrp)函数改变前台进程组。
键入Ctrl + Z或Ctrl + C时,应该只对前台的进程组产生影响,而不应该对shell本身或后台的进程组产生影响。
常用信号概览
在linux中man 7 signal以查看。
CTRL-C. 默认情况下,会结束程序。
CTRL-\. 默认情况下,这个也是会结束程序,但是这个信号会试程序在退出时产生一个core文件,类似于一个程序错误信号。
没有快捷键. 这个信号会强制结束一个程序,并且,该信号不能被程序重载。
没有快捷键. 这个信号和 SIGQUIT 的行为是一样的。
CTRL-Z. 默认情况下,这会使程序暂停并回到bash中。
在bash中,当运行fg或者fg %NUMBER命令时,该信号会使一个暂停的进程重新恢复运行。
会停止后台进程从键盘读入数据
和SIGTTIN类似,这个管的是output,停止后台进程在shell的输出,并阻塞。
在C语言中,可以使用sigaction函数来改变当前进程对某些信号的行为,可以忽略、执行自定义的操作等。
函数原型
1 2 3 4 5 6 7 8 9 int sigaction (int signum, const struct sigaction *act, struct sigaction *oldact) ;struct sigaction { void (*sa_handler)(int ); void (*sa_sigaction)(int , siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void ); };
函数表示,使用新的行为act来代替对信号signum的操作,将原来的行为保存到oldact中,oldact为NULL时表示不保存原来的行为。
定义了一个专门处理信号的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 struct sigaction sa ;void signal_handle () { if (sigaction(SIGTTOU, &sa, NULL ) == -1 ) { perror("sigaction error" ); exit (EXIT_FAILURE); } if (sigaction(SIGTTIN, &sa, NULL ) == -1 ) { perror("sigaction error" ); exit (EXIT_FAILURE); } if (sigaction(SIGCONT, &sa, NULL ) == -1 ) { perror("sigaction error" ); exit (EXIT_FAILURE); } if (sigaction(SIGTSTP, &sa, NULL ) == -1 ) { perror("sigaction error" ); exit (EXIT_FAILURE); } if (sigaction(SIGTERM, &sa, NULL ) == -1 ) { perror("sigaction error" ); exit (EXIT_FAILURE); } if (sigaction(SIGINT, &sa, NULL ) == -1 ) { perror("sigaction error" ); exit (EXIT_FAILURE); } if (sigaction(SIGQUIT, &sa, NULL ) == -1 ) { perror("sigaction error" ); exit (EXIT_FAILURE); } }
main函数中的初始化
1 2 3 4 sa.sa_handler = SIG_IGN; sigemptyset(&sa.sa_mask); sa.sa_flags = 0 ; signal_handle();
在每个子进程的入口,都需要将信号改为默认行为。
1 2 3 4 5 sa.sa_handler = SIG_DFL; signal_handle(); setpgid(getpid(), getpid());