cs162作业hw-shell


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
/* Built-in command functions take token array (see parse.h) and return int */
typedef int cmd_fun_t(struct tokens* tokens);

/* Built-in command struct and lookup table */
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
//Print the current working directory to standard output
int cmd_pwd(struct tokens* tokens) {
char *buffs = getcwd(NULL, 0);
printf("%s\n", buffs);
free(buffs);

return 1;
}

//Change the current working directory to that directory
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
//execution configuration
typedef struct Configuration {
//arglist
char **arglist;
//redirection
int redirection;
int read_file_fd;
char *read_file_path;
int write_file_fd;
char *write_file_path;
//pipe
int **pipe;
int pipe_num, cur_num;
} Config;

运行程序

1
2
//run
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; /* Inode number */
off_t d_off; /* Not an offset; see below */
unsigned short d_reclen; /* Length of this record */
unsigned char d_type; /* Type of file; not supported
by all filesystem types */
char d_name[256]; /* Null-terminated filename */
};

$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;

//split by ':'
for(int i = 0; ; ++i) {
token = strtok_r(PATH, ":", &flagptr);
if(PATH != NULL) {
free(PATH);//maybe not safe
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
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);

// printf(" %s\n", whole_path);//
break;
}
}
closedir(dir);

if(found)
break;
}

之后使用 $execv$ 运行程序即可。

输入输出重定向(Redirection)

实现类似于[process] > [file][process] < [file]的输入输出重定向。

即,将程序的标准输入、标准输出重定向到某个文件中。

在重定向的检测中就知识在检测<>的位置,以及重定向符号后面的文件是否存在。

在重定向中,使用到了dup2函数

函数原型

1
int dup2(int oldfd, int newfd);

使用newfd代替oldfd的文件描述符

0stdin1stdout

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//make sure the redirection
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);
}
}

//redirection and pipe
if(read_fd != -1)
dup2(read_fd, 0);//stdin
if(write_fd != -1)
dup2(write_fd, 1);//stdout

管道(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
//redirection and pipe
if(read_fd != -1)
dup2(read_fd, 0);//stdin
if(write_fd != -1)
dup2(write_fd, 1);//stdout

管道的内部实现是一个循环队列,写入端通过pipefd[1]将字符写道管道中,如果管道是满的,那么就会阻塞该进程,待管道不满之后,继续写入。读取端通过pipefd[0]进行读取,如果管道是空的,会阻塞该进程,直到管道非空。

注:一定要保证管道读取和写入结束之后,把文件描述符关闭掉,否则会造成进程阻塞,结束不了。

管道的创建

1
2
3
4
5
6
7
8
9
10
11
12
13
int **pipe_fd;//pipe_file_discreptor
pid_t *pid;//process_id
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
//close other pipe
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
//redirection and pipe
if(read_fd != -1)
dup2(read_fd, 0);//stdin
if(write_fd != -1)
dup2(write_fd, 1);//stdout

同理,在父进程中,需要将所有的管道都关闭

1
2
3
4
//close pipe
for(int i = 0; i < pipe_num; ++i)
for(int j = 0; j < 2; ++j)
close(pipe_fd[i][j]);

之后,等待所有的子进程结束

1
2
3
//wait for the childs
for(int i = 0; i <= pipe_num; ++i)
waitpid(pid[i], NULL, 0);

信号处理

使用setpgid函数改变进程所属的进程组

使用tcsetpgrp(int fd, pid_t pgrp)函数改变前台进程组。

键入Ctrl + ZCtrl + C时,应该只对前台的进程组产生影响,而不应该对shell本身或后台的进程组产生影响。

常用信号概览

linuxman 7 signal以查看。

1
SIGINT

CTRL-C. 默认情况下,会结束程序。

1
SIGQUIT

CTRL-\. 默认情况下,这个也是会结束程序,但是这个信号会试程序在退出时产生一个core文件,类似于一个程序错误信号。

1
SIGKILL

没有快捷键. 这个信号会强制结束一个程序,并且,该信号不能被程序重载。

1
SIGTERM

没有快捷键. 这个信号和 SIGQUIT 的行为是一样的。

1
SIGTSTP

CTRL-Z. 默认情况下,这会使程序暂停并回到bash中。

1
SIGCONT

bash中,当运行fg或者fg %NUMBER命令时,该信号会使一个暂停的进程重新恢复运行。

1
SIGTTIN

会停止后台进程从键盘读入数据

1
SIGTTOU

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中,oldactNULL时表示不保存原来的行为。

定义了一个专门处理信号的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
//signal control
struct sigaction sa;

//handle the singal
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
//Enable some signals
sa.sa_handler = SIG_DFL;
signal_handle();
//setpgid
setpgid(getpid(), getpid());

文章作者: Shaun
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Shaun !
评论
  目录