시스템 호출 인프라를 구현
userprog/syscall.c에 시스템 호출 핸들러를 구현. 제공된 골격 구현은 프로세스를 종료하여 시스템 호출을 "처리”함. 시스템 호출 번호와 인수를 검색하고 적절한 조치를 수행해야함.
시스템 호출 세부 정보
첫 번째 프로젝트에서는 운영 체제가 사용자 프로그램으로부터 제어권을 되찾는 한 가지 방법인 타이머 및 I/O 장치로부터의 인터럽트를 다뤘습니다. 이는 CPU 외부의 엔티티에 의해 발생하므로 "외부" 인터럽트입니다.
운영 체제는 프로그램 코드에서 발생하는 이벤트인 소프트웨어 예외도 처리합니다. 여기에는 페이지 폴트 또는 0으로 나누기와 같은 오류가 포함될 수 있습니다. 예외는 또한 사용자 프로그램이 운영 체제에 서비스("시스템 호출")를 요청하는 수단이기도 합니다.
전통적인 x86 아키텍처에서 시스템 호출은 다른 소프트웨어 예외와 동일하게 처리되었습니다. 그러나 x86-64에서는 제조업체가 시스템 호출을 위한 특수 명령인 syscall을 도입했습니다. 이는 시스템 호출 핸들러를 호출하는 빠른 방법을 제공합니다.
오늘날 syscall 명령어는 x86-64에서 시스템 호출을 호출하는 가장 일반적으로 사용되는 수단입니다. Pintos에서 사용자 프로그램은 syscall을 호출하여 시스템 호출을 수행합니다. 시스템 호출 번호 및 추가 인수는 두 가지 점을 제외하고 syscall 명령어를 호출하기 전에 일반적인 방식으로 레지스터에 설정되어야 합니다.
•
%rax는 시스템 호출 번호입니다.
•
네 번째 인수는 %rcx가 아닌 %r10입니다.
따라서 시스템 호출 핸들러인 syscall_handler()가 제어권을 얻으면 시스템 호출 번호는 rax에 있고, 인수는 %rdi, %rsi, %rdx, %r10, %r8, %r9 순서로 전달됩니다.
호출자의 레지스터는 struct intr_frame을 통해 접근할 수 있습니다. (struct intr_frame은 커널 스택에 있습니다.)
함수 반환 값에 대한 x86-64 규칙은 반환 값을 RAX 레지스터에 배치하는 것입니다. 값을 반환하는 시스템 호출은 struct intr_frame의 rax 멤버를 수정하여 이를 수행할 수 있습니다.
다음 시스템 호출을 구현하세요.
나열된 프로토타입은 include/lib/user/syscall.h를 포함하는 사용자 프로그램이 볼 수 있는 프로토타입입니다. (이 헤더와 include/lib/user의 다른 모든 헤더는 사용자 프로그램 전용입니다.) 각 시스템 호출에 대한 시스템 호출 번호는 include/lib/syscall-nr.h에 정의되어 있습니다.
시스템 호출 구현 목록
void halt (void);
C
복사
•
power_off()를 호출하여 Pintos를 종료합니다. (src/include/threads/init.h에 선언됨) 교착 상태 등에 대한 일부 정보를 잃을 수 있으므로 거의 사용해서는 안 됩니다.
void exit (int status);
C
복사
•
현재 사용자 프로그램을 종료하고 커널에 status를 반환합니다. 프로세스의 부모가 이를 기다리면 (아래 참조) 이 상태가 반환됩니다. 일반적으로 status 0은 성공을, 0이 아닌 값은 오류를 나타냅니다.
pid_t fork (const char *thread_name);
C
복사
•
THREAD_NAME이라는 이름으로 현재 프로세스의 복제본인 새 프로세스를 만듭니다. RBX, RSP, RBP, R12에서 R15까지의 callee-saved 레지스터를 제외하고는 레지스터의 값을 복제할 필요가 없습니다. 자식 프로세스의 pid를 반환해야 하며, 그렇지 않으면 유효한 pid가 아니어야 합니다. 자식 프로세스에서 반환 값은 0이어야 합니다. 자식은 파일 디스크립터 및 가상 메모리 공간을 포함한 복제된 리소스를 가져야 합니다. 부모 프로세스는 자식 프로세스가 성공적으로 복제되었는지 알기 전까지는 fork()에서 반환해서는 안 됩니다. 즉, 자식 프로세스가 리소스를 복제하는 데 실패하면 부모의 fork() 호출은 TID_ERROR를 반환해야 합니다.
•
템플릿은 threads/mmu.c의 pml4_for_each()를 사용하여 해당 페이지 테이블 구조를 포함한 전체 사용자 메모리 공간을 복사하지만, 전달된 pte_for_each_func의 누락된 부분을 채워야 합니다. (가상 주소 참고)
int exec (const char *cmd_line);
C
복사
•
cmd_line에 지정된 이름을 가진 실행 파일로 현재 프로세스를 변경하고, 제공된 인수를 전달합니다. 성공하면 이 함수는 절대 반환되지 않습니다. 그렇지 않으면 프로그램이 어떤 이유로든 로드되거나 실행될 수 없으면 프로세스는 종료 상태 -1로 종료됩니다. 이 함수는 exec을 호출한 스레드의 이름을 변경하지 않습니다. 파일 디스크립터는 exec 호출 시에도 열린 상태로 유지됩니다.
int wait (pid_t pid);
C
복사
•
자식 프로세스 pid를 기다리고 자식의 종료 상태를 검색합니다. pid가 아직 살아 있으면 종료될 때까지 기다립니다. 그런 다음 pid가 exit에 전달한 상태를 반환합니다. pid가 exit()를 호출하지 않고 커널에 의해 종료된 경우 (예: 예외로 인해 종료된 경우) wait(pid)는 -1을 반환해야 합니다. 부모가 wait를 호출할 때 이미 종료된 자식 프로세스를 기다리는 것은 완벽하게 합법적이지만, 커널은 부모가 자식의 종료 상태를 검색하거나 자식이 커널에 의해 종료되었음을 알 수 있도록 허용해야 합니다.
•
다음 조건 중 하나라도 참이면 wait는 즉시 실패하고 -1을 반환해야 합니다.
◦
pid가 호출 프로세스의 직접적인 자식을 참조하지 않는 경우. pid는 호출 프로세스가 fork에 대한 성공적인 호출의 반환 값으로 pid를 받은 경우에만 호출 프로세스의 직접적인 자식입니다. 자식은 상속되지 않는다는 점에 유의하세요. 예를 들어, A가 자식 B를 생성하고 B가 자식 프로세스 C를 생성하는 경우, A는 B가 죽더라도 C를 기다릴 수 없습니다. 프로세스 A의 wait(C) 호출은 실패해야 합니다. 마찬가지로, 고아 프로세스는 부모 프로세스가 종료되기 전에 종료되면 새로운 부모에게 할당되지 않습니다.
◦
wait를 호출하는 프로세스가 이미 pid에 대해 wait를 호출한 경우. 즉, 프로세스는 주어진 자식을 한 번만 기다릴 수 있습니다.
•
프로세스는 원하는 수의 자식을 생성하고, 원하는 순서로 기다릴 수 있으며, 일부 또는 모든 자식을 기다리지 않고 종료할 수도 있습니다. 여러분의 설계는 wait가 발생할 수 있는 모든 방법을 고려해야 합니다. 프로세스의 struct thread를 포함한 모든 리소스는 부모가 기다리든 기다리지 않든, 그리고 자식이 부모보다 먼저 종료되든 나중에 종료되든 관계없이 해제되어야 합니다.
•
초기 프로세스가 종료될 때까지 Pintos가 종료되지 않도록 해야 합니다. 제공된 Pintos 코드는 main()(threads/init.c에 있음)에서 process_wait()(userprog/process.c에 있음)를 호출하여 이를 시도합니다. 함수 맨 위에 있는 주석에 따라 process_wait()를 구현한 다음 process_wait()를 기반으로 wait 시스템 호출을 구현하는 것이 좋습니다.
bool create (const char *file, unsigned initial_size);
C
복사
•
file이라는 이름의 새 파일을 initial_size 바이트 크기로 만듭니다. 성공하면 true, 그렇지 않으면 false를 반환합니다. 새 파일을 생성한다고 해서 파일을 여는 것은 아닙니다. 새 파일을 여는 것은 별도의 작업이며 open 시스템 호출이 필요합니다.
bool remove (const char *file);
C
복사
•
file이라는 이름의 파일을 삭제합니다. 성공하면 true, 그렇지 않으면 false를 반환합니다. 파일은 열려 있든 닫혀 있든 관계없이 제거될 수 있으며, 열린 파일을 제거해도 파일이 닫히지 않습니다. 자세한 내용은 FAQ의 "열려 있는 파일 제거"를 참조하세요.
int open (const char *file);
C
복사
•
file이라는 이름의 파일을 엽니다. "파일 디스크립터(fd)"라고 하는 음수가 아닌 정수 핸들을 반환하거나, 파일을 열 수 없는 경우 -1을 반환합니다. 파일 디스크립터 0과 1은 콘솔을 위해 예약되어 있습니다. fd 0 (STDIN_FILENO)은 표준 입력이고, fd 1 (STDOUT_FILENO)은 표준 출력입니다. open 시스템 호출은 이 두 파일 디스크립터를 절대 반환하지 않으며, 이들은 아래에 명시적으로 설명된 경우에만 시스템 호출 인수로 유효합니다. 각 프로세스는 독립적인 파일 디스크립터 세트를 가집니다. 파일 디스크립터는 자식 프로세스에 의해 상속됩니다. 단일 파일이 한 번 이상 열리면, 단일 프로세스에 의해서든 다른 프로세스에 의해서든, 각 열기는 새 파일 디스크립터를 반환합니다. 단일 파일에 대한 다른 파일 디스크립터는 close에 대한 별도의 호출에서 독립적으로 닫히고 파일 위치를 공유하지 않습니다. 추가 작업을 위해 0부터 시작하는 정수를 반환하는 Linux 체계를 따라야 합니다.
int filesize (int fd);
C
복사
•
fd로 열린 파일의 크기를 바이트 단위로 반환합니다.
int read (int fd, void *buffer, unsigned size);
C
복사
•
fd로 열린 파일에서 size 바이트를 buffer로 읽습니다. 실제로 읽은 바이트 수를 반환하거나 (파일 끝에서는 0), 파일을 읽을 수 없는 경우 (파일 끝 이외의 조건으로 인해) -1을 반환합니다. fd 0은 input_getc()를 사용하여 키보드에서 읽습니다.
int write (int fd, const void *buffer, unsigned size);
C
복사
•
buffer에서 열린 파일 fd로 size 바이트를 씁니다. 실제로 쓰인 바이트 수를 반환하며, 일부 바이트를 쓸 수 없는 경우 size보다 작을 수 있습니다. 파일 끝을 지나서 쓰는 것은 일반적으로 파일을 확장하지만, 기본 파일 시스템에서는 파일 성장이 구현되지 않습니다. 예상되는 동작은 파일 끝까지 가능한 한 많은 바이트를 쓰고 실제로 쓰인 바이트 수를 반환하거나, 바이트를 전혀 쓸 수 없으면 0을 반환하는 것입니다. fd 1은 콘솔에 씁니다. 콘솔에 쓰는 코드는 putbuf()에 대한 한 번의 호출에서 전체 buffer를 써야 합니다. 적어도 size가 수백 바이트보다 크지 않은 한은요. (더 큰 버퍼를 분할하는 것은 합리적입니다.) 그렇지 않으면 다른 프로세스에서 출력된 텍스트 줄이 콘솔에서 뒤섞여 인간 독자와 우리의 채점 스크립트를 모두 혼란스럽게 할 수 있습니다.
void seek (int fd, unsigned position);
C
복사
•
열린 파일 fd에서 읽거나 쓸 다음 바이트를 파일 시작 부분에서 바이트로 표현된 position으로 변경합니다. (따라서 position이 0이면 파일의 시작입니다.) 파일의 현재 끝을 지나서 seek하는 것은 오류가 아닙니다. 나중에 읽기는 0바이트를 얻어 파일의 끝을 나타냅니다. 나중에 쓰기는 파일을 확장하고, 쓰지 않은 간격을 0으로 채웁니다. (그러나 Pintos 파일은 프로젝트 4가 완료될 때까지 고정된 길이를 가지므로, 파일 끝을 지나서 쓰는 것은 오류를 반환합니다.) 이러한 의미는 파일 시스템에 구현되어 있으며 시스템 호출 구현에서 특별한 노력이 필요하지 않습니다.
unsigned tell (int fd);
C
복사
•
열린 파일 fd에서 읽거나 쓸 다음 바이트의 위치를 파일 시작 부분에서 바이트로 표현하여 반환합니다.
void close (int fd);
C
복사
•
파일 디스크립터 fd를 닫습니다. 프로세스를 종료하거나 중단하면 마치 각 파일 디스크립터에 대해 이 함수를 호출하는 것처럼 모든 열린 파일 디스크립터가 암시적으로 닫힙니다.
기타 지침
파일은 다른 시스템 호출을 정의합니다. 당분간은 무시하세요. 프로젝트 3에서 일부를, 프로젝트 4에서 나머지를 구현할 예정이므로 시스템을 확장성을 염두에 두고 설계해야 합니다.
여러 사용자 프로세스가 한 번에 시스템 호출을 할 수 있도록 시스템 호출을 동기화해야 합니다. 특히, filesys 디렉터리에 제공된 파일 시스템 코드를 여러 스레드에서 동시에 호출하는 것은 안전하지 않습니다. 시스템 호출 구현은 파일 시스템 코드를 임계 영역으로 취급해야 합니다. process_exec()도 파일에 접근한다는 점을 잊지 마세요. 당분간은 filesys 디렉터리의 코드를 수정하지 않는 것이 좋습니다.
lib/user/syscall.c에 각 시스템 호출에 대한 사용자 수준 함수를 제공했습니다. 이들은 사용자 프로세스가 C 프로그램에서 각 시스템 호출을 호출하는 방법을 제공합니다. 각각은 약간의 인라인 어셈블리 코드를 사용하여 시스템 호출을 호출하고 (적절한 경우) 시스템 호출의 반환 값을 반환합니다.
이 부분을 완료하면 Pintos는 방탄(bulletproof)이 되어야 합니다. 사용자 프로그램이 할 수 있는 어떤 것도 OS를 충돌시키거나, 패닉시키거나, 어설션에 실패하게 하거나, 다른 방식으로 오작동하게 해서는 안 됩니다. 이 점을 강조하는 것이 중요합니다. 우리의 테스트는 수많은 방법으로 시스템 호출을 깨뜨리려고 시도할 것입니다. 모든 예외적인 경우(corner cases)를 생각하고 처리해야 합니다. 사용자 프로그램이 OS를 중단시킬 수 있는 유일한 방법은 halt 시스템 호출을 호출하는 것입니다.
시스템 호출에 잘못된 인수가 전달되면, 허용되는 옵션에는 오류 값 반환 (값을 반환하는 호출의 경우), 정의되지 않은 값 반환, 또는 프로세스 종료가 포함됩니다.

