엄청 힘들게 fork, wait 까지 마무리했지만 FAIL 받은 테스트 케이스가 무려 12개..

하나씩 pass 시켜보자!!
과연 extra 과제까지 할 수 있을 것인지.. 일단 킵 꼬잉이다.
☑ rox : Read-Only eXecutable → Deny Write on Executables 처리를 했는가?
☑ bad : page fault가 발생했을 때, exit(-1)로 종료되도록 했는가?
☑ syn : synchronization 을 제대로 구현했는가? → filesys_lock 사용을 잘 했는가?
bad-*** 테스트 케이스를 통과 시켜보자! 유튜브를 참고!

쏘 이지였다.
static void
page_fault(struct intr_frame* f) {
bool not_present; /* True: not-present page, false: writing r/o page. */
bool write; /* True: access was write, false: access was read. */
bool user; /* True: access by user, false: access by kernel. */
void* fault_addr; /* Fault address. */
/* Obtain faulting address, the virtual address that was
accessed to cause the fault. It may point to code or to
data. It is not necessarily the address of the instruction
that caused the fault (that's f->rip). */
fault_addr = (void*)rcr2();
/* Turn interrupts back on (they were only off so that we could
be assured of reading CR2 before it changed). */
intr_enable();
/* Determine cause. */
not_present = (f->error_code & PF_P) == 0;
write = (f->error_code & PF_W) != 0;
user = (f->error_code & PF_U) != 0;
exit(-1); // 추가
#ifdef VM
/* For project 3 and later. */
if (vm_try_handle_fault(f, fault_addr, user, write, not_present))
return;
#endif
/* Count page faults. */
page_fault_cnt++;
/* If the fault is true fault, show info and exit. */
printf("Page fault at %p: %s error %s page in %s context.\n",
fault_addr,
not_present ? "not present" : "rights violation",
write ? "writing" : "reading",
user ? "user" : "kernel");
kill(f);
}
하라는대로 하면 패스가 된다.
근데 왜 하는 걸까?
Pintos에서는 Page fault가 발생했을 때도 일관적으로 프로세스를 종료시켜야 한다.
(일반화인가? 일단 bad-*** 테스트 케이스에 한하여는 그렇게 출력되기를 요구한다.)
테스트 케이스를 살펴보자.


위와 같이 NULL을 가리키는 포인터, 유효하지 않은 주소인 커널 주소를 가리키는 포인터를 읽어들일 때 커널을 죽이지 않고 exit(-1)로 프로세스를 종료시켜야 한다.
기존의 코드는 page fault의 정보를 print 하며 kill()을 호출한다.
/* Handler for an exception (probably) caused by a user process. */
static void
kill(struct intr_frame* f) {
/* This interrupt is one (probably) caused by a user process.
For example, the process might have tried to access unmapped
virtual memory (a page fault). For now, we simply kill the
user process. Later, we'll want to handle page faults in
the kernel. Real Unix-like operating systems pass most
exceptions back to the process via signals, but we don't
implement them. */
/* The interrupt frame's code segment value tells us where the
exception originated. */
switch (f->cs) {
case SEL_UCSEG:
/* User's code segment, so it's a user exception, as we
expected. Kill the user process. */
printf("%s: dying due to interrupt %#04llx (%s).\n",
thread_name(), f->vec_no, intr_name(f->vec_no));
intr_dump_frame(f);
thread_exit();
case SEL_KCSEG:
/* Kernel's code segment, which indicates a kernel bug.
Kernel code shouldn't throw exceptions. (Page faults
may cause kernel exceptions--but they shouldn't arrive
here.) Panic the kernel to make the point. */
intr_dump_frame(f);
PANIC("Kernel bug - unexpected interrupt in kernel");
default:
/* Some other code segment? Shouldn't happen. Panic the
kernel. */
printf("Interrupt %#04llx (%s) in unknown segment %04x\n",
f->vec_no, intr_name(f->vec_no), f->cs);
thread_exit();
}
}
user mode일 때는 thread_exit(), kernel mode일 때는 PANIC을 일으키고 있기 때문에 테스트케이스에서 원하는 대로 exit(-1)으로 종료될 수 있도록 수정하자!
rox-*** 테스트 케이스를 통과시켜보자!

한 프로세스(A)가 어떤 실행 파일을 실행하고 있는 동안,
다른 프로세스(B)가 그 파일을 동시에 수정하면 어떤 일이 생길까?
A는 이미 그 파일 내용을 메모리에 올려놓은 상태일 수도 있고,
실행 중 일부를 다시 디스크에서 읽어오기도 한다.
그런데 그 와중에 B가 내용을 바꿔버리면?
A의 실행 흐름이 예상치 못한 코드로 바뀌거나 비정상 종료될 수 있다.
즉, 실행 파일의 무결성(integrity)을 깨뜨리게 된다.
이를 방지하기 위해서는, 파일이 이미 실행되고 있다면 다른 파일이 수정할 수 없도록 해야한다.
그러면 파일을 실행하는 시점에서 write을 금지하고,
종료되는 시점에서 write를 허용시키면 되겠다.
이 기능은 이미 구현된 함수로 제공된다.
file_deny_write(struct file *f)
→ 해당 파일에 대한 쓰기 작업을 거부한다
file_allow_write(struct file *f)
→ 다시 쓰기를 허용한다
이를 아래의 시점에서 제한해주면 된다.
load()process_exit()또한,
실행 중인 파일을 process_exit()에서 종료하기 위해서는 thread 구조체에서 역추적할 수 있어야 한다.
따라서 thread 구조체에 running_file이라는 필드를 추가하여 관리해준다.
구현해보자!
static bool
load(const char* file_name, struct intr_frame* if_) {
.
.
/* Open executable file. */
file = filesys_open(file_name);
if (file == NULL) {
printf("load: %s: open failed\n", file_name);
goto done;
}
/* 실행 파일 저장, deny write 설정 */
file_deny_write(file);
t->running_file = file;
.
.
void
process_exit(void) {
struct thread* curr = thread_current();
/* fdt 정리 */
for (int i = 0; i < 64;i++) {
curr->fd_table[i] = NULL;
}
free(curr->fd_table);
/* close running file */
if (curr->running_file != NULL) {
file_close(curr->running_file);
}
/* 프로세스 정리 */
process_cleanup();
/* sema up - parent process에게 알림 */
sema_up(&curr->wait_sema);
}
아니 근데 이렇게 해도 안 되는 것이다..
유튜브 속에 계신 교수님만 믿고 따라갔는데.. 왜.. 🤯
※ '***' 이 포함된 로그는 디버깅용이다.

※ 주석 지운 버전

0x800423c038 주소에 위치한 파일을 load하면서 deny_write를 true로 바꿔준 것을 확인할 수 있다.
하지만, 그것과는 무관하게 write 시스템 콜을 호출하였다. write 시스템 콜의 호출 시점에서는 deny_write의 상태가 false로 바뀌어있었다. 위에서 true로 설정한 것이 저장되고 있지 않았다.ㅜㅜ..
그래서 write를 수행하고, 정의된 fail 동작에 따라, FILED를 출력하며 exit(1)로 종료 되는 문제가 발생했다.
void
fail (const char *format, ...)
{
va_list args;
va_start (args, format);
vmsg (format, args, ": FAILED\n");
va_end (args);
exit (1);
}
여기에 도달하기 전에 자식 프로세스가 exit(0)으로 종료되고, 이 상태를 받은 부모 프로세스도 계층에 따라 종료되어야 한다.

테스트 케이스는 이러한 출력을 요구하고 있다.
5개의 자식 프로세스가 동일한 파일에 write를 시도하면 status 0으로 종료시킨다.
현재 내 코드를 실행시켜보면 fail로 넘어가, exit(1)로 종료된다.
이유가 뭘까!
코드를 하나씩 뜯어보는 중 이상한 점을 발견했다.
.
.
load()를 수행하고도 해당 파일은 프로세스에서 실행 중이어야 한다.
load에 실패했을 경우에만 리소스 낭비를 방지하기 위해 file_close()를 수행해야 한다.
하지만 지금 기존 코드는 load의 성공 여부와는 별개로 늘 종단에서 file_close()로 file을 닫고 있었다.

file_close를 호출하면 어떻게 될까?

file_allow_write를 호출하며, file에 대한 write를 허용하게 된다.
그러면 해당 프로세스는 여전히 file 작업을 하고있는데 다른 프로세스도 접근할 수 있게 된다는 말이다.
우리의 의도와 전혀 맞지 않다.
이를 수정해줘야 한다.
file_close는 실제로 프로세스가 해당 파일 작업이 종료되었을 떄, 한 번만 호출되게 한다.(process_exit())
static bool
load(const char* file_name, struct intr_frame* if_) {
.
.
.
done:
/* We arrive here whether the load is successful or not. */
if (!success)
file_close(file);
return success;
}
패스다. 야호!
멀티 프로세스 환경에서 파일을 다룰 때 동시 접근 문제가 발생한다.
이를 해결하기 위해 파일 시스템 관련 작업에 락을 적용해보자.
최종 목표는 syn-*** 테스트 케이스를 통과하는 것이다.

file은 모든 프로세스가 공유하는 자원이다.
여러 프로세스가 동시에 같은 파일에 접근하면, 경쟁 상태(race condition)가 발생하게 된다.
이를 방지하기 위해 lock을 사용하여 파일 시스템 관련 작업의 동시성 제어를 수행한다.
작업 전 lock_acquire(), 작업 후 lock_release()를 호출하여
Critical Section을 보호한다.
파일 시스템은 모든 프로세스에 공통된 자원이므로,
락 역시 전역 범위에서 공유되어야 한다. 따라서 syscall.h에 선언해준다.

실제 적용 대상은 open, read, write 시스템 콜이다.
이들은 파일 시스템의 주요 진입점이며, 공유 자원 접근이 빈번하므로 락 보호가 필수적이다.
int open(const char* file) {
/* open 성공시, fd를 반환하고 실패시, -1을 반환한다. */
check_address(file);
lock_acquire(&filesys_lock);
struct file* f = filesys_open(file);
lock_release(&filesys_lock);
if (f == NULL) {
return -1;
}
struct file** fdt = thread_current()->fd_table;
for (int fd = 2;fd < 64;fd++) {
if (fdt[fd] == NULL || fdt[fd] == 0) {
fdt[fd] = f;
return fd;
}
}
return -1; // fdt 전부 할당됨
}
int read(int fd, void* buffer, unsigned size) {
check_address(buffer);
// 표준 입력의 경우, 키보드의 입력을 받음
if (fd == 0) {
buffer = input_getc();
return size;
}
// 옳은 fd인지 확인
if (fd < 2 || fd >= 64) {
return -1;
}
// 접근한 file이 비어있는지 확인
struct file* read_file = thread_current()->fd_table[fd];
if (read_file == NULL) {
return -1;
}
// file_read 수행
lock_acquire(&filesys_lock);
off_t bytes = file_read(read_file, buffer, size);
lock_release(&filesys_lock);
return bytes;
}
int write(int fd, const void* buffer, unsigned size) {
check_address(buffer);
// 표준 출력: 콘솔 처리
if (fd == 1) {
lock_acquire(&filesys_lock);
putbuf(buffer, size);
lock_release(&filesys_lock);
return size;
}
// 옳은 fd인지 확인
if (fd < 2 || fd >= 64) {
return -1;
}
// 접근한 file이 비어있는지 확인
struct file* write_file = thread_current()->fd_table[fd];
if (write_file == NULL) {
return -1;
}
// file_write 수행
lock_acquire(&filesys_lock);
off_t bytes = file_write(write_file, buffer, size);
lock_release(&filesys_lock);
return bytes;
}

하나 남았다!
근데 syn-write는 왜 단독으로 돌리면 FAIL이 뜰까.. 🥲