Tiny Shell Lab
在这个lab中,实现一个可以fork进程的shell终端,维护前台后台的进程。一共提供32个测试,第一部分先介绍如何实现来通过前23个测试。
Part I
优先看主函数main
初始化部分。
char cmdline[MAXLINE_TSH];
规定了一个可以写入的char数组。
init_job_list();
初始化一个链表
void init_job_list(void) {
init = true;
for (jid_t jid = 1; jid <= MAXJOBS; jid++) {
struct job_t *job = get_job(jid);
clearjob(job);
job->cmdline = NULL;
}
nextjid = 1;
}
nextjid
是一个static变量,在分配新的job时使用。
接下来通过调用 Signal()
函数为不同的信号安装处理函数
// Install the signal handlers
Signal(SIGINT, sigint_handler); // Handles Ctrl-C
Signal(SIGTSTP, sigtstp_handler); // Handles Ctrl-Z
Signal(SIGCHLD, sigchld_handler); // Handles terminated or stopped child
Signal(SIGTTIN, SIG_IGN);
Signal(SIGTTOU, SIG_IGN);
Signal(SIGQUIT, sigquit_handler);
SIGINT
通常表示键盘中断(即用户按下 Ctrl+C
),这是用来终止程序的常见方式。这里使用了自定义的 sigint_handler
函数来捕获并处理该信号。
SIGTSTP
是用户按下 Ctrl+Z
时发送的信号,通常用于将进程挂起(暂停执行)。自定义的处理函数 sigtstp_handler
将处理此信号。
SIGCHLD
信号在子进程终止或暂停时发送给父进程,常用于父进程清理已结束的子进程(如调用 wait()
或 waitpid()
处理僵尸进程)。
SIGTTIN
信号在后台进程尝试读取终端输入时发送。如果不忽略这个信号,后台进程会被暂停。
SIGTTOU
信号在后台进程尝试写入终端输出时发送。和 SIGTTIN
类似,忽略这个信号可以避免后台进程被暂停。
SIGQUIT
信号通常表示用户按下 Ctrl+\
,这个信号会导致程序生成核心转储(core dump)并终止进程。通过 sigquit_handler
改变其默认行为。
稍后介绍sigint_handler
, sigtstp_handler
, sigchld_handler
, sigquit_handler
在获取了cmdline之后,解析cmdline
eval()
parse_result = parseline(cmdline, &token);
struct cmdline_tokens {
int argc; ///< Number of arguments passed
char *argv[MAXARGS]; ///< The arguments list
char *infile; ///< The filename for input redirection, or NULL
char *outfile; ///< The filename for output redirection, or NULL
builtin_state builtin; ///< Indicates if argv[0] is a builtin command
char _buf[MAXLINE_TSH]; ///< Internal backing buffer (do not use)
};
buildin_cmd()
一共有两种commend,buildin和其他。buildin cmd 包括
quit
:如果使用quit,exit(0);
结束tsh进程
jobs
:因为访问jobs会需要获得全局jobs链表,所以先需要block信号。sigset_t mask, prev; sigemptyset(&mask); sigaddset(&mask, SIGCHLD); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGTSTP); sigprocmask(SIG_BLOCK, &mask, &prev); // block list_jobs(STDOUT_FILENO); sigprocmask(SIG_SETMASK, &prev, NULL); // unblock return 1;
后面第二部分会再次设计这部分list_jobs当前是写入到了终端输出fd,第二部分会改变这部分到输出到文件fd。
bg/fg
:将某个进程从foreground移到background或者相反操作if (!strcmp(bg_or_fg, "bg") || !strcmp(bg_or_fg, "fg")) { if(!strcmp(bg_or_fg, "bg")){ // ST -> BG convert_to_bg(&token); }else if (!strcmp(bg_or_fg, "fg")) // ST -> FG { convert_to_fg(&token); } return 1; }
其中
convert_to_bg
和convert_to_fg
会在第二部分详细解释。
其他cmd
当不是buildin_cmd
的时候,合法的指令是想要运行一个可执行程序。我们为这个指令fork一个新的进程并执行execve()
else if (!buildin_cmd(cmdline))
{
pid_t pid;
sigset_t mask, prev;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTSTP);
sigprocmask(SIG_BLOCK, &mask, &prev);
pid = fork();
if(pid == 0){ //child process
sigprocmask(SIG_SETMASK, &prev, NULL);
setpgid(pid, pid);
if(execve(token.argv[0], token.argv, environ) < 0){
perror("fialed to execve\n");
exit(-1);
}
}else{
if(parse_result == PARSELINE_FG){ // forground job
fg_pid = pid;
fg_running = 1;
add_job(pid, FG, cmdline);
while(fg_running == 1){ // while the fg job is still running
sigsuspend(&prev);
}
sigprocmask(SIG_SETMASK, &prev, NULL);
}else if (parse_result == PARSELINE_BG) // background job
{
add_job(pid, BG, cmdline);
jid_t jid = job_from_pid(pid);
printf("[%d] (%d) %s\n", jid, pid, cmdline);
sigprocmask(SIG_SETMASK, &prev, NULL);
}
}
}
如果是foreground执行(PARSELINE_FG
)那么将全局变量fg_pid
赋值为当前子进程pid
。
sigsuspend(&prev);
会临时替换进程的信号掩码为 prev
,并挂起父进程,直到接收到信号。它的作用是等待前台进程结束(如通过 SIGCHLD
接收子进程结束信号),避免父进程在前台作业结束前继续执行。fg_runing
状态改变会在接下来 sigchld_handler()
中说明。
入宫是background执行(PARSELINE_BG
)那么会简单的将分配一个新的jid
和pid
之后打印后台执行,恢复signal unblock,等待下一个prompt
sigchld_handler()
sigchld_handler
函数处理 SIGCHLD
信号,这个信号在子进程的状态发生变化时发送给父进程。此信号的处理函数会检查子进程的状态,并根据其退出状态、被终止的信号或停止状态来更新作业的状态。
void sigchld_handler(int sig) {
int olderrno = errno;
sigset_t mask, prev;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTSTP);
sigprocmask(SIG_BLOCK, &mask, &prev);
int wstate;
pid_t pid = waitpid(-1, &wstate, WUNTRACED | WNOHANG | WCONTINUED);
while(pid > 0){
if(WIFCONTINUED(wstate)){
jid_t jid = job_from_pid(pid);
job_state jstate = job_get_state(jid); // get the state of the job
if(jstate == ST){ // ST -> BG
job_set_state(jid, BG);
}
}else{
jid_t jid = job_from_pid(pid);
if(WIFEXITED(wstate)){ // normally exit
delete_job(jid);
if(pid == fg_pid){ // if a forground job
fg_running = 0; // stop running the job
}
}
if(WIFSIGNALED(wstate)){ // SIGINT
delete_job(jid);
int signum = WTERMSIG(wstate);
sio_printf("Job [%d] (%d) terminated by signal %d\n", jid, pid, signum);
if(pid == fg_pid){
fg_running = 0;
}
}
if(WIFSTOPPED(wstate)){ // SIGTSTP
fg_running = 0;
job_set_state(jid, ST);
int signum = WSTOPSIG(wstate);
sio_printf("Job [%d] (%d) stopped by signal %d\n", jid, pid, signum);
}
}
pid = waitpid(-1, &wstate, WUNTRACED | WNOHANG | WCONTINUED);
}
sigprocmask(SIG_SETMASK, &prev, NULL);
errno = olderrno;
return;
}
创建一个空的信号集 mask
。将 SIGINT
和 SIGTSTP
添加到信号集 mask
中。使用 sigprocmask
将这些信号阻塞,以防在处理过程中它们被接收,prev
保存了旧的信号掩码
使用 waitpid
等待子进程状态的变化。-1
表示等待任意子进程,
WUNTRACED
表示即使子进程被停止也要报告,
WNOHANG
表示如果没有子进程状态变化,则立即返回,
WCONTINUED
表示报告子进程的继续执行状态
在while loop中,只要有状态变化的子进程,那么使用 WIFCONTINUED
检查子进程是否恢复执行,如果是且原状态是停止(ST
),则将状态改为后台运行(BG
)。
如果不是停止
- 使用
WIFCONTINUED
检查子进程是否恢复执行,如果是且原状态是停止(ST
),则将状态改为后台运行(BG
)。
- 使用
WIFEXITED
检查子进程是否正常退出,删除作业,并如果是前台作业,更新fg_running
标志。
- 使用
WIFSIGNALED
检查子进程是否由于信号终止,删除作业,打印信息,并如果是前台作业,更新fg_running
标志。
- 使用
WIFSTOPPED
检查子进程是否被信号停止,更新作业状态为停止,并打印信号信息。
sigint_handler()
void sigint_handler(int sig) {
int _errno = errno;
if(fg_pid > 0){
killpg(fg_pid, sig);
}
errno = _errno;
}
注意,killpg并不是杀死进程,而是传递信号。killpg(fg_pid, sig)
函数用于向前台进程中的所有进程发送信号。这里的 sig
是传递给 sigint_handler
的信号类型(在这种情况下是 SIGINT
)。killpg
是一个更高层次的信号发送函数,用于向一个进程组发送信号,这比逐个发送信号更高效。不需要block信号因为只是发送一个信号。
sigtstp_handler()
void sigtstp_handler(int sig) {
int _errno = errno;
if(fg_pid > 0){
killpg(fg_pid, sig);
}
errno = _errno;
}
原理同上类似。
Part II
在实现了基本的buildin_cmd, 和其他fg, bg操作并处理好block信号维护job list之后,在第二部分,我们将实现st, fg, bg对转换,以及io变换fd。
首先,回收Part I中没有解释的convert_to_bg
, convert_to_fg
convert_to_bg()
static void convert_to_bg(struct cmdline_tokens *token){
if(token->argc == 2){ // correct number to parse
const char *id = token->argv[1];
sigset_t mask, prev;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTSTP);
sigprocmask(SIG_BLOCK, &mask, &prev); // block
pid_t pid = 0;
jid_t jid = 0;
if(id == NULL || id[0] == '\0'){ // the id is nothing...
printf("bg command requires PID or %%jobid argument\n");
}else{
if(id[0] == '%'){ // jid
jid = (jid_t)atoi(&id[1]);
if(job_exists(jid) == false){
printf("%%%d: No such job\n", jid);
sigprocmask(SIG_SETMASK, &prev, NULL); // unblock
return;
}
pid = job_get_pid(jid);
}else if ('0' <= id[0] && '9' >= id[0]) // pid
{
pid = (pid_t)atoi(id);
jid = job_from_pid(pid);
}else{ // bg xxx, not pid or %jid
printf("bg: argument must be a PID or %%jobid\n");
}
}
if(pid != 0){
const char *cmdline = job_get_cmdline(jid);
printf("[%d] (%d) %s\n", jid, pid, cmdline);
st_fgbg = 2;
killpg(pid, SIGCONT); // send comtinue
}
sigprocmask(SIG_SETMASK, &prev, NULL); // unblock
}else{ // bg input error more than 2 argv...
printf("bg command requires PID or %%jobid argument\n");
return;
}
}
st_fgbg
是全局变量。0代表bg→fg, 1代表ST→fg, 2代表ST→bg。并不存在fg→bg因为在fg执行的时候,只有Ctrl+C, Ctrl+Z这种能让fg停止或者结束。
killpg(pid, SIGCONT);
将ST的进程继续执行。
之后会触发sigchld_handler
我们需要在Part I 的基础上修改sigchld_handler
实现ST→bg的转换。
convert_to_fg()
static void convert_to_fg(struct cmdline_tokens *token){
if(token->argc != 2){
printf("fg command requires PID or %%jobid argument\n");
return;
}
const char *id = token->argv[1];
pid_t pid = 0;
pid_t jid = 0;
sigset_t mask, prev;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTSTP);
sigprocmask(SIG_BLOCK, &mask, &prev); // block
if(id == NULL || id[0] == '\0'){ // the id is nothing...
printf("fg command requires PID or %%jobid argument\n");
}else{
if(id[0] == '%'){ // jid
jid = (jid_t)atoi(&id[1]);
if(job_exists(jid) == false){
printf("%%%d: No such job\n", jid);
sigprocmask(SIG_SETMASK, &prev, NULL); // unblock
return;
}
pid = job_get_pid(jid);
}else if ('0' <= id[0] && '9' >= id[0]) // pid
{
pid = (pid_t)atoi(id);
jid = job_from_pid(pid);
}else{ // bg xxx, not pid or %jid
printf("fg: argument must be a PID or %%jobid\n");
}
}
if(pid != 0){
// different from background job things from here
job_state jstate = job_get_state(jid);
fg_pid = pid;
fg_running = 1;
if(jstate == ST){
st_fgbg = 1;
killpg(pid, SIGCONT); // send continue
}else if(jstate == BG){
st_fgbg = 0;
job_set_state(jid, FG); // BG -> FG
}
while(fg_running == 1){
sigsuspend(&prev);
}
}
sigprocmask(SIG_SETMASK, &prev, NULL); // unblock
}
和bg唯一的区别是将fg_running = 1;
之后等待fg结束。
- 当job ST的时候需要发送信号
SIGCONT
- 当job BG的时候不需要发送
SIGCONT
,因为正在执行了。
FG/BG/ST Status Convert
void sigchld_handler(int sig) {
int olderrno = errno;
sigset_t mask, prev;
...
while(pid > 0){
if(WIFCONTINUED(wstate)){
jid_t jid = job_from_pid(pid);
job_state jstate = job_get_state(jid); // get the state of the job
if(jstate == ST){ // ST -> BG
if(st_fgbg == 1){
job_set_state(jid, FG);
}else if(st_fgbg == 2){
job_set_state(jid, BG);
}
}
}else{
...
}
pid = waitpid(-1, &wstate, WUNTRACED | WNOHANG | WCONTINUED);
}
sigprocmask(SIG_SETMASK, &prev, NULL);
errno = olderrno;
return;
}
修改的地方是,如果接受到一个SIGCONT
,WIFCONTINUED(wstate)
检查子进程是否被继续执行。如果是,并且当前作业状态是停止状态(ST
),根据 st_fgbg
的值将作业状态更新为前台(FG
)或后台(BG
)。
File Redirection
eval()中还需要改动stdin
, stdout
改变函数的输入输出io。
void eval(const char *cmdline) {
parseline_return parse_result;
...
else if (!buildin_cmd(cmdline))
{
...
if(pid == 0){ //child process
sigprocmask(SIG_SETMASK, &prev, NULL);
// pid = getpid(pid, pid);
setpgid(pid, pid);
if(token.infile != NULL){ // '<' ST_INFILE
int fd = open(token.infile, O_RDONLY, 0664);
if (fd == -1) {
fprintf(stderr, "%s: No such file or directory\n", token.infile);
return;
}
if (dup2(fd, STDIN_FILENO) == -1) {
fprintf(stderr, "%s: No such file or directory\n", token.infile);
close(fd);
return;
}
}
if (token.outfile != NULL) // '>' ST_OUTFILE
{
int fd = open(token.outfile, O_WRONLY | O_CREAT | O_TRUNC, 0664);
if (fd == -1) {
fprintf(stderr, "%s: No such file or directory\n", token.outfile);
return;
}
if (dup2(fd, STDOUT_FILENO) == -1) {
fprintf(stderr, "%s: No such file or directory\n", token.outfile);
close(fd);
return;
}
}
int exe_fd = open(token.argv[0], O_RDONLY, 0664);
if(exe_fd == -1){
if (errno == ENOENT) {
fprintf(stderr, "%s: No such file or directory\n", token.argv[0]);
} else if (errno == EACCES) {
fprintf(stderr, "%s: Permission denied\n", token.argv[0]);
} else {
fprintf(stderr, "Error opening file: %s\n", strerror(errno));
}
exit(-1);
}
...
}
input redirect:
- 如果
token.infile
不为空,表示命令中指定了输入重定向。
- 使用
open
打开指定的输入文件,并检查文件是否存在。
- 使用
dup2
将输入文件描述符复制到标准输入(STDIN_FILENO
),确保后续的读取操作从这个文件中读取数据。
- 如果
open
或dup2
失败,则报告错误并返回。
output redirect:
- 如果
token.outfile
不为空,表示命令中指定了输出重定向。
- 使用
open
打开指定的输出文件,若文件不存在则创建,O_TRUNC
表示文件存在时清空文件内容。
- 使用
dup2
将输出文件描述符复制到标准输出(STDOUT_FILENO
),确保后续的写入操作将数据写入到这个文件中。
- 如果
open
或dup2
失败,则报告错误并返回。
执行文件检查:
- 尝试打开指定的可执行文件(
token.argv[0]
)。
- 检查文件是否存在(
ENOENT
),是否有权限(`EACCES),或报告其他错误。
- 如果文件无法打开,报告错误并退出子进程。
至此,tshlab结束