[CSAPP] SHELL LAB 풀이
CS:APP LAB 자료 링크: http://csapp.cs.cmu.edu/3e/labs.html
혹시 잘못된 내용이 있다면 메일이나 댓글로 알려주시면 정말 감사하겠습니다
trace별로 해결 과정을 순차적으로 제시할까 했지만 가독성이 너무 떨어질 것 같아 완성된 답안을 토대로 구현해야 하는 함수들을 설명하는 방식으로 포스팅하겠습니다.
스켈레톤에 이미 구현되어 있고 수정할 필요가 없는 함수들은 포함하지 않겠습니다.
참고로 Waitpid
Fork
Sigemptyset
등 함수의 첫 글자가 대문자 처리되어 있는 함수들은 각각 원래 함수들의 wrapper function 으로, 원래 함수 (예를 들어, Waitpid
라면 waitpid
함수)를 실행하고 에러가 발생했는지 확인하는 역할을 합니다.
비슷하게 sio_puts
sio_putl
등 함수는 주어진 argument를 async-signal-safe 하게 출력하는 wrapper function 입니다.
모두 저자들이 제공하는 csapp.h, csapp.c 에 구현되어 있고, 이 파일들은 구글링하면 나옵니다!
eval
void eval(char *cmdline)
{
int bg;
int pid;
sigset_t mask;
char *argv[MAXARGS];
bg = parseline(cmdline, argv);
if (argv[0] == NULL) { //terminate on EOF
return;
}
if (builtin_cmd(argv)) { //if it is a built-in-command: execute it and return 1. else return 0.
return;
}
Sigemptyset(&mask);
Sigaddset(&mask, SIGCHLD);
Sigprocmask(SIG_BLOCK, &mask, NULL); //mask SIGCHLD
if ((pid = Fork()) == 0) {
// Child's behavior
setpgid(0, 0);
Sigprocmask(SIG_UNBLOCK, &mask, NULL);
if (Execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}
// Parent's behavior
if (!bg) {
addjob(jobs, pid, FG, cmdline);
Sigprocmask(SIG_UNBLOCK, &mask, NULL);
waitfg(pid); /* wait for the foreground job to finish */
} else {
addjob(jobs, pid, BG, cmdline);
Sigprocmask(SIG_UNBLOCK, &mask, NULL);
printf("[%d] (%d) %s", pid2jid(pid), (int)pid, cmdline); /* print out log and execute in background */
}
return;
}
Shell의 메인이 되는 함수입니다. 이 함수에서 built-in command를 실행하거나 child를 fork
해서 execve
로 다른 프로그램을 실행합니다.
Parent가 addjob
을 실행하기 전에 child의 작동이 끝나서 SIGCHLD
신호를 받으면 안 되기 때문에 신호를 막아주는 것과 parseline
함수의 결과에 따라 background 작업인지 foreground 작업인지 나누어 처리하는 것에 유의하면 됩니다.
builtin_cmd
int builtin_cmd(char **argv)
{
char* command = argv[0];
if (!strcmp("quit", command)) {
exit(0);
} else if (!strcmp("jobs", command)) {
listjobs(jobs);
} else if (!strcmp("bg", command) || !strcmp("fg", command)) {
do_bgfg(argv);
} else {
return 0; /* not a builtin command */
}
return 1;
}
Built-in command를 실행하는 함수입니다.
do_bgfg
void do_bgfg(char **argv)
{
struct job_t *job;
int is_bg = !strcmp("bg", argv[0]);
int is_pid;
if (argv[1] == NULL) {
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
if (argv[1][0] == '%') { /* if it's jid */
is_pid = 1;
job = getjobjid(jobs, my_atoi(&argv[1][1]));
} else { /* if it's pid */
is_pid = 0;
job = getjobpid(jobs, my_atoi(argv[1]));
}
if (errno == EINVAL) { //my_atoi error (wrong argument)
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
if (job == NULL) { //can't find job
if (is_pid) {
printf("%s: No such job\n", argv[1]);
} else {
printf("(%s): No such process\n", argv[1]);
}
} else {
Kill(-job->pid, SIGCONT); /* send SIGCONT signal to every process under process group */
if (is_bg) { // command: bg
job->state = BG;
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
} else { // command: fg
job->state = FG;
waitfg(job->pid);
}
}
return;
}
Built-in command 중 하나인 bg
와 fg
를 처리하는 함수입니다.
스펙에 있는대로 job id나 process id를 받아서 멈춰있던 함수에 SIGCONT
신호를 보내줍니다. bg
가 호출되었다면 background에서 마저 실행하고, fg
가 호출되었다면 foreground에서 실행하면 됩니다.
이때, 해당 job들의 state도 변경해줘야 합니다.
예외 처리에 my_atoi
라는 함수가 사용된 것을 보실 수 있는데, string을 int로 파싱해주기 위해 제가 만든 함수입니다. 만약 argument를 int로 바꾸는 게 불가능하다면 global variable인 errno
의 값을 EINVAL
로 바꿔주고 0을 반환합니다.
구현은 아래와 같습니다.
int my_atoi(char* start) {
if (start == NULL) {
errno = EINVAL;
return 0;
}
int result = 0;
char curr;
while ((curr = *start) != '\0') {
if ('0' <= curr && curr <= '9') {
result = result*10 + curr - '0';
++start;
} else {
errno = EINVAL;
return 0;
}
}
return result;
}
waitfg
void waitfg(pid_t pid)
{
/* according to the Hint on assignment handout, use budy loop around sleep function
* and do all reaping in the signal handler
*/
while(1) {
if (fgpid(jobs) != pid) { /* when given foreground job terminated */
break;
} else {
Sleep(1);
}
}
return;
}
주석에 적혀 있듯, CS:APP의 저자들이 제공하는 handout에 waitfg
함수의 경우 sleep
함수를 사용해 foreground 함수가 끝날때까지 기다리라고 instruction이 있습니다.
물론 좋은 구현 방법은 아닌 것 같지만.. 일단 연습하는 입장이므로 간단하게 구현하기 위해 instruction을 따르겠습니다.
sigchld_handler
void sigchld_handler(int sig)
{
pid_t pid;
int status;
char log_message_buff[1024];
struct job_t *job;
int prev_errno = errno;
/* WNOHANG: return immediately if none of the child processes in the wait set has terminated yet.
WUNTRACED: return pid of the terminated or "stopped" child */
while((pid = Waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
job = getjobpid(jobs, pid);
if (WIFSIGNALED(status)) {
/* when the process terminated */
snprintf(log_message_buff, 1024, "Job [%d] (%d) terminated by signal %d\n", job->jid, (int)pid, WTERMSIG(status));
sio_puts(log_message_buff);
deletejob(jobs, pid);
} else if (WIFSTOPPED(status)) {
/* when the process stopped */
snprintf(log_message_buff, 1024, "Job [%d] (%d) stopped by signal %d\n", job->jid, (int)pid, WSTOPSIG(status));
sio_puts(log_message_buff);
job->state = ST;
} else if (WIFEXITED(status)) {
/* delete finished job from the job list
without this, you get to send signal to wrong pid at sigint/sigtstp handler. */
deletejob(jobs, pid);
}
}
errno = prev_errno;
return;
}
SIGCHLD
신호를 받았을 때 작동할 signal handler 입니다.
현재 종료된 child 들을 모두 reap 해야 하기 때문에 while 문을 사용하고 waitpid
함수의 첫번째 parameter로 -1을 넘겨주었습니다. 또, 모든 child들이 끝나길 기다려서는 안 되고(WNOHANG
), terminate 된 상태뿐 아니라 stop된 상태의 child도 다뤄야하기 때문에(WUNTRACED
), 세번째 parameter로는 WNOHANG | WUNTRACED
를 넘겨주었습니다.
printf
는 async-signal-safe 하지 못합니다. 프로그램의 여러 곳에서 printf
를 호출해주고 있기 때문에, printf
가 호출되고 있는 상황에 신호가 들어와서 signal-handler가 호출되고 printf
가 그 안에서 호출되면 데드락 상황이 발생합니다.
따라서 printf
를 사용하는 대신 넘칠 일 없을만큼 충분히 큰 (여기서 출력하고자 하는 것은 로그 메시지이므로 길이의 상한을 이미 알고 있습니다) 버퍼 log_message_buff
를 정적으로 할당해주고, snprintf
함수와 sio_puts
함수로 출력해주었습니다.
이 함수를 구현하며 제가 애를 먹었던 부분이 if - else if 문의 마지막 분기인
else if (WIFEXITED(status)) {
deletejob(jobs, pid); //***** 중요! 이미 제대로 끝난 작업들은 job list 에서 삭제해줘야 함. 안 그러면 잘못된 pid로 signal 보내게 됨. at sigint/sigtstp handler
}
이 부분이었습니다.
자꾸 kill
에서 해당 process group을 찾을 수 없다는 에러가 나길래 뭔가 했더니.. WIFSIGNALED(status)
는 catch하지 못한 signal로 종료된 작업들만 잡아낸다는 게 문제였습니다.
정상적으로 종료된 child 들의 경우도 deletejob
을 통해 job list에서 제거해줘야 하는걸 간과했습니다.
위에 있듯 WIFEXITED(status)
를 써서 잡아낼 수 있습니다.
sigint_handler
void sigint_handler(int sig)
{
/* preserve previous errno */
int prev_errno = errno;
pid_t foreground_pid = fgpid(jobs);
if (foreground_pid != 0) {
/* if there is a foreground job, send SIGINT */
Kill(-foreground_pid, sig);
}
errno = prev_errno;
return;
}
ctrl-c가 입력되었을 때 Shell은 여전히 작동하면서 foreground의 process는 중지시키도록 해주는 signal handler입니다.
현재 foreground에 실행되고 있는 process의 process id를 fgpid
함수로 얻어오고 그 process group에 SIGINT
신호를 보냅니다. eval
함수에서 setpgid(0, 0);
식으로 child의 process group id를 설정해줬기 때문에 kill
의 첫번째 parameter로 -foreground_pid
를 제공해주면 그 process group 전체를 terminate 할 수 있게 됩니다.
앞의 my_atoi
함수에서 errno
를 활용해서 로직을 처리하기 때문에 이 signal handler가 errno
를 임의로 변경하면 안 됩니다. 따라서 함수를 시작하면서 기존 errno
를 저장해주고 함수가 끝날 때 다시 복원합니다.
sigtstp_handler
void sigtstp_handler(int sig)
{
int prev_errno = errno;
pid_t foreground_pid = fgpid(jobs);
if (foreground_pid != 0) {
/* if there is a foreground job, send SIGTSTP */
Kill(-foreground_pid, sig);
}
errno = prev_errno;
return;
}
sigint_handler
와 같습니다.
Comments