출처
CSAPP Lab 과제 페이지 https://csapp.cs.cmu.edu/3e/labs.html
Proxy Lab 과제 안내서(2003) https://csapp.cs.cmu.edu/2e/shlab.pdf
소개
이 과제의 목적은 프로세스 컨트롤과 시그널링의 개념과 친숙해지는 것입니다. job control을 지원하는 간단한 Unix shell 프로그램을 만들어보세요.
제출 지침
- shlab-handout.tar 다운받기
- tar xvf shlab-handout.tar 입력
- make 입력 (몇몇 테스트 루틴을 컴파일하고 링크하기)
- tsh.c 상위에 팀 멤버 이름 적기
tsh.c (tiny shell) 파일에는 이미 간단한 Unix shell의 기능적인 골격이 제공되어 있습니다. 여러분에 주어진 과제는 아래 목록에 있는 남아 있는 빈 함수들을 완성하는 것입니다. 여러분이 참고할 수 있도록 레퍼런스 해결책 (주석이 많음)에 있는 함수들의 대략적인 코드 라인의 수를 적어놓았습니다.
- eval: 커맨드 라인을 파싱하고 해석하는 메인 루틴 [70줄]
- builtin-cmd: 내장 명령어 quit, fg, bg, jobs 를 인식하고 해석 [25줄]
- do_bgfg: 내장 명령어 bg와 fg를 구현 [50줄]
- waitfg: foreground job이 끝나기를 기다리기 [20줄]
- sigchld_handler: SIGCHILD 시그널을 catch하기 [80줄]
- sigint_handler: SIGINT (ctrl-c) 시그널을 catch하기 [15줄]
- sigtstp_handler: SIGTSTP (ctrl-z) 시그널을 catch하기 [15줄]
당신의 tsh.c 파일을 수정할 때마다 make를 타이핑하여 파일을 재컴파일하세요. 당신의 shell을 실행시키려면 커맨드 라인에 tsh를 입력하세요.
unix > ./tsh
tsh> [type commands to your shell here]
Unix Shell에 대한 전반적인 개요
shell은 사용자를 위해 프로그램을 실행시켜주는 상호작용적인 커맨드 라인 해석기입니다. shell은 반복적으로 프롬프트를 출력하고, stdin에서 커맨드 라인을 기다리고, 커맨드 라인에 따라 어떤 액션을 수행합니다.
커맨드 라인은 공백에 의해 구분되는 아스키 텍스트의 시퀀스입니다. 커맨드 라인의 첫 번째 단어는 내장 명령어의 이름이거나 실행 가능한 파일의 경로 이름입니다. 나머지 단어들은 커맨드 라인 인자들입니다. 만약 첫 번째 단어가 내장 명령어라면 shell은 현재 프로세스에서 즉시 커맨드를 실행시킵니다. 그렇지 않다면 첫 번째 단어는 실행 가능한 프로그램의 경로명으로 간주됩니다. 이 경우에 shell은 자식 프로세스를 fork하고, 자식 프로세스의 컨텍스트에 프로그램을 로드하고 실행시킵니다. 커맨드 라인을 해석한 결과로 생성된 자식 프로세스들은 job이라고 부릅니다. 일반적으로 job은 Unix pipe로 연결된 여러 개의 자식 프로세스로 이루어져 있습니다.
만약 커맨드 라인이 "&"로 끝난다면 job은 백그라운드에서 실행됩니다. 이는 shell이 프롬프트를 출력하고 다음 커맨드 라인을 기다리기 전까지 job이 종료되기를 기다리지 않는다는 것을 의미합니다. 그렇지 않다면 job은 foreground에서 실행됩니다. 이는 shell이 다음 커맨드 라인을 기다리기 전에 job이 종료되기를 기다린다는 의미입니다. 따라서 어느 시점에나 최대 한 개의 job만 foreground에서 실행될 수 있습니다. 하지만 백그라운드에는 여러 개의 job이 실행될 수 있습니다.
예를 들어, 다음과 같이 커맨드 라인을 타이핑하면 shell은 내장 명령어인 jobs를 실행시킵니다.
tsh> jobs
다음과 같은 커맨드 라인을 타이핑하면 ls 프로그램은 foreground에서 실행됩니다.
tsh> /bin/ls -l -d
관습적으로 shell은 프로그램이 main 루틴을 실행시킬 때 argc와 argv가 다음과 같은 값을 가지도록 보장합니다.
- argc == 3
- argv[0] == "/bin/ls"
- argv[1] == "-l"
- argv[2] == "-d"
다음과 같은 커맨드 라인을 입력하면 ls 프로그램은 백그라운드에서 실행됩니다.
tsh> /bin/ls -l -d &
Unix shell은 job control이라는 개념을 지원합니다. 이는 사용자가 job을 background와 foreground 사이에서 왔다갔다 움직일 수 있게 해주고 job에 있는 프로세스의 프로세스 상태(running, stopped, terminated)를 변경할 수 있게 해줍니다. ctrl-c를 입력하면 foreground job에 있는 모든 프로세스에게 SIGINT 시그널이 전달됩니다. ctrl-z를 입력하면 foreground에 있는 모든 프로세스에 SIGTSTP 시그널이 전달됩니다. SIGTSTP의 기본 액션은 프로세스를 stopped state로 만드는 것입니다. 프로세스가 stopped state가 되면 SIGCONT 시그널을 받아 깨어날 때까지 stopped state에서 머뭅니다. Unix shell은 job control을 지원하는 다양한 내장 함수를 지원합니다.
- jobs: 실행 중이거나 멈춰 있는 background job의 목록을 보여줍니다.
- bg <job>: 멈춰 있는 background job을 실행 중인 background job으로 변경합니다.
- fg <job>: 멈춰 있는 foreground job을 실행 중인 foreground job으로 변경합니다.
- kill <job>: job을 종료시킵니다.
tsh 명세 사항
tsh shell은 다음과 같은 기능을 가져야 합니다.
- 프롬프트는 문자열 "tsh> "로 시작합니다.
- 사용자가 타이핑하는 커맨드 라인은 이름과 0개 또는 그 이상의 인자를 가집니다. 이들은 한 개 이상의 공백으로 구분됩니다. 만약 이름이 내장 명령어라면 tsh는 즉시 이를 처리하고 다음 커맨드 라인을 기다립니다. 그렇지 않다면 tsh는 name을 어떤 실행 가능한 파일의 이름으로 간주하고 초기 자식 프로세스의 컨텍스트에 이를 로드하고 실행시킵니다. (여기서 job은 초기 자식 프로세스를 의미합니다.)
- tsh는 파이프 ( | )나 I/O 리다이렉션 (< 과 >)을 지원하지 않아도 됩니다.
- ctrl-c(ctrl-z)는 현재의 foreground job (그리고 그 job의 자손들)에 SIGINT (SIGTSTP) 시그널이 전송되게 만듭니다. foreground job이 없다면 시그널은 아무 영향도 가지지 않게 됩니다.
- 커맨드 라인이 &로 끝난다면 tsh는 그 job을 백그라운드에서 실행시켜야 합니다. 그렇지 않다면 job을 foreground에서 실행시켜야 합니다.
- 모든 job은 프로세스 아이디 (PID)나 job 아이디 (JID)로 식별될 수 있습니다. 이들은 tsh에 의해 할당되는 양수 정수입니다. JID는 커맨드 라인에서 prefix "%"로 표시되어야 합니다. 예를 들어, "%5"는 JID 5를 나타내고, "5"는 PID 5를 나타냅니다. (job list를 다루는 데 필요한 모든 루틴은 이미 제공되어 있습니다.)
- tsh는 다음과 같은 내장 명령어를 지원해야 합니다.
- quit 명령어는 shell을 종료시킵니다.
- jobs 명령어는 모든 백그라운드 job을 목록으로 보여줍니다.
- bg <job> 명령어는 <job>에게 SIGCONT 시그널을 보내 <job>을 재시작시키고 백그라운드에서 실행시킵니다. <job>의 인자는 PID일 수도 있고 JID일 수도 있습니다.
- fg 명령어는 에게 SIGCONT 시그널을 보내 <job>을 재시작시키고 foreground에서 실행시킵니다. <job>의 인자는 PID일 수도 있고 JID일 수도 있습니다. - tsh는 모든 좀비 자식 프로세스를 거두어야 합니다. 만약 job이 catch하지 못한 시그널 때문에 종료되면 tsh는 이 이벤트를 인식하고 job의 PID와 offending signal의 설명을 출력해야 합니다.
검토하기
당신의 작업을 검토할 수 있는 도구들을 몇 가지 제공합니다.
- 레퍼런스
Linux 실행 가능 파일 tshref는 레퍼런스 shell입니다. 이 프로그램을 실행시켜서 당신의 shell이 어떻게 동작해야 하는지 확인하세요. 당신의 shell은 이 레퍼런스와 동일한 ouput을 만들어야 합니다.
- shell driver
shdriver.pl 프로그램은 shell을 자식 프로세스로 실행시키고, trace file에 따라 shell에 커맨드와 시그널을 보내고, output을 보여줍니다.
shdriver.pl의 사용 방법을 알고 싶다면 -h 인자를 이용하세요.
unix> ./sdriver.pl -h
Usage: sdriver.pl [-hv] -t -s -a
Options:
-h Print this message
-v Be more verbose
-t Trace file
-s Shell program to test
-a Shell arguments
-g Generate output for autograder
우리는 당신의 shell의 정확성을 테스트해볼 수 있는 16개의 trace file (trace{01-16}.txt)을 같이 제공합니다. 낮은 숫자의 trace file은 간단한 테스트를 진행하고 높은 숫자의 테스트는 더 복잡한 테스트를 진행합니다.
trace0.1txt을 이용하여 shell driver를 당신의 shell에 대해 돌리고 싶다면 다음과 같이 타이핑하세요.
unix> ./sdriver.pl -t trace01.txt -s ./tsh -a "-p"
(-a "-p" 인자는 당신의 shell이 프롬프트를 출력하지 않도록 명령합니다.)
또는 다음과 같이 타이핑할 수도 있습니다.
unix> make test01
레퍼런스 shell에 대해 shell driver를 돌리고 싶다면 다음과 같이 타이핑하세요.
unix> ./sdriver.pl -t trace01.txt -s ./tshref -a "-p"
또는 다음과 같이 타이핑할 수도 있습니다.
unix> make rtest01
tshref.out를 통해서도 레퍼런스 shell의 결과를 확인할 수 있습니다.
(./sdriver.pl 실행 결과 예시 생략)
힌트
- CSAPP 8장 전체를 읽으세요.
- 당신의 shell의 발전을 가이드하기 위해 trace file을 이용하세요. trace01.txt를 이용하여 당신의 shell이 레퍼런스 shell과 동일한 output을 생산하도록 만드세요.
- waitpid, kill, fork, execve, setpgid, sigprocmask 함수가 유용할 것입니다. waitpid의 WUNTRACED와 WNOHANG 옵션도 매우 유용할 것입니다.
- 당신의 시그널 핸들러를 구현할 때, kill 함수에게 "pid" 대신에 "-pid" 인자를 이용하여 모든 foreground 프로세스 그룹에게 SIGINT와 SIGTSTP 시그널을 보내세요. sdriver.pl 프로그램이 이 에러를 테스트합니다.
- 이 과제의 까다로운 부분 중 하나는 waitfg와 sigchld_handler 함수에게 작업을 어떻게 할당할지를 결정하는 것입니다. 우리는 다음과 같은 접근을 추천합니다.
- waitfg에서는 busy loop을 이용하세요. (around the sleep function)
- sigchld_handler에서는 waitpid 하나만 호출하세요.
다른 방법들도 가능합니다. 예를 들어, wiatfg와 sigchld_handler 모두에서 waitpid를 호출할 수 있습니다. 하지만 이런 방식은 혼란을 유발합니다. 거두는 행위는 핸들러에서 모두 수행하는 방법이 훨씬 더 간단합니다. - eval에서 부모 프로세스는 자식 프로세스를 fork하기 전에 SIGCHLD 시그널을 차단하기 위해 sigprocmask를 이용해야 하고, 그다음에 addjob을 호출하여 job list에 자식 프로세스를 추가하고 나서 다시 sigprocmask를 사용하여 시그널 차단을 풀어야 합니다. 자식 프로세스가 부모 프로세스의 blocked vectors를 물려받기 때문에 자식 프로세스는 새로운 프로그램을 실행하기 전에 SIGCHLD 시그널 차단을 해제해야 합니다.
부모 프로세스가 addjob을 호출하기 전에 sigchld_handler가 자식 프로세스를 거두는, 그래서 job list에서 자식 프로세스가 제거되는 race condition을 피하기 위해서 부모 프로세스는 SIGCHLD를 차단해야 합니다. - more, less, vi, emacs와 같은 프로그램은 터미널 세팅과 관련하여 이상한 일들을 합니다. 당신의 shell에서는 이러한 프로그램을 실행시키지 마세요. /bin/ls, /bin/ps, /bin/echo와 같은 간단한 텍스트 기반 프로그램만 이용하세요.
- standard Unix Shell에서 당신의 shell을 실행시키면 당신의 shell은 foreground process group에서 실행됩니다. 당신의 shell이 자식 프로세스를 생성하면 기본적으로 그 자식 프로세스 또한 foreground process group에 속하게 될 것입니다. ctrl-c를 타이핑하면 foreground에 있는 모든 프로세스에게 SIGINT를 전송하기 때문에, 당신의 shell과 당신의 shell이 생성한 모든 프로세스에 SIGINT 시그널이 전송될 것입니다. 이는 올바르지 않은 동작입니다.
이렇게 해야 합니다. fork를 하고 나서 그리고 execve를 하기 전에 자식 프로세스는 setpgid(0,0)을 호출해야 합니다. 이는 자식 프로세스를 그룹 아이디가 자식 프로세스의 PID와 동일한 새로운 프로세스 그룹에 넣습니다. 이는 해당 foreground process group에 오직 하나의 프로세스, 당신의 shell만 있도록 보장합니다. ctrl-c를 타이핑하면, shell은 SIGINT를 catch하고 이를 적절한 foreground job에게 (더 정확히 말하면, foreground job을 가진 프로세스 그룹에게) 포워딩해야 합니다.
평가
점수는 최대 90점입니다.
- 80점: 정확성. 16개의 trace file은 각각 5점입니다.
- 10점: 스타일 점수. 좋은 주석에 5점, 그리고 모든 시스템 콜의 리턴 값을 확인하여 5점을 부여합니다.
당신의 shell은 리눅스 머신에서, lab directory에 있는 shell driver와 trace file을 이용하여 테스트될 것입니다. 당신의 shell은 레퍼런스 shell과 동일한 output을 만들어내야 합니다. 이때 예외는 단 두가지입니다.
- PID는 다를 수 있습니다.
- trace11.txt, trace12.txt, trace13.txt에 있는 /bin/ps 명령어의 ouptput은 실행할 때마다 달라질 수 있습니다. 하지만 /bin/ps 명령어의 output에서 mysplit 프로세스의 running state는 항상 동일해야 합니다.
제출 지침
tsh.c 파일을 제출하려면 다음과 같이 타이핑하세요.
make handin TEAM=teamname
제출 후에 수정을 하고 싶다면 다음과 같이 타이핑하세요.
make handin TEAM=teamname VERSION=2
(팀명 관련 지침은 생략)
행운을 빕니다!
'Computer System > CMU-LAB' 카테고리의 다른 글
[CMU-LAB] Unix Shell Lab 리뷰 (0) | 2025.02.04 |
---|---|
[CMU-LAB] Caching Web Proxy Lab 과제 안내서 (번역) (0) | 2025.01.22 |