2023. 5. 16. 22:48ㆍComputer Science
Pintos Project 2 관련 포스팅 목록
- Argument Passing (User 프로그램 인자 설정하기)
- System Calls - 1 (System Calls이 호출되는 과정)
- System Calls - 2 (exec, wait, fork) & Deny Write on Executables
- System Calls - 3 (File System) & User Memory
User Memory Access
- system call의 일환으로, 커널은 종종 사용자 프로그램이 제공한 포인터를 통해 메모리에 접근해야 합니다.
- 그러나 사용자는
1) null 포인터,
2) 매핑되지 않은 가상 메모리를 가리키는 포인터 또는
3) 커널 가상 주소 공간(KERN_BASE 이상)을 전달할 수 있으므로, 커널은 이를 매우 신중하게 처리해야 합니다. - 이러한 유형의 잘못된 포인터는 거부되어야 합니다.
문제를 일으킨 프로세스를 종료하고 그 자원을 해제해야 합니다.
check_address 구현하기
- user 프로그램이 잘못된 포인터를 전달하면 exit을 호출한다.
- 이 check_address 함수를 포인터가 전달된 system call이 호출된 경우 주소 검증에 사용한다.
/* userprog/syscall.c */
void check_address(void *addr); // 선언
void check_address(void *addr)
{
if (addr == NULL)
exit(-1);
if (!is_user_vaddr(addr))
exit(-1);
if (pml4_get_page(thread_current()->pml4, addr) == NULL)
exit(-1);
}
File System
System Calls 중에는 File System과 관련된 호출이 많이 있다.
Pintos는 File System을 구현하는 것이 목적인 프로젝트가 아니기 때문에 이미 구현되어 있는 File System을 제공해주고 있다.
따라서 System Calls을 구현하면서 file 관련된 함수는 직접 구현하는 것이 아닌,
filesys.h 혹은 file.h에서 적절한 함수를 찾아서 사용하면 된다.
동기화
🚨 구현 시 주의할 점으로,
File System 코드를 실행하는 동안에는 한 번에 하나의 프로세스만 실행되도록 동기화를 사용해야 한다.
즉 file 관련된 코드가 실행되기 전에 syscall.c에서 새로 만든 filesys_lock을 acquire한 후에 접근하도록 해야 한다.
처음에는 read와 write에만 lock을 얻은 이후에 접근할 수 있도록 구현하였으나,
이후 Project 3의 Stack Growth를 구현한 이후에 syn-read 테스트가 간헐적으로 (30~40번에 한 번꼴) 실패하는 상황이 발생했다. 😿😿🙀
Gitbook의 Project 2 - Introduction에서,
파일 시스템 코드가 실행되는 동안에는 한 번에 하나의 프로세스만 실행되도록 동기화를 사용해야 한다는 내용을 확인했고
File System 관련 코드에 lock을 얻은 이후에 접근할 수 있도록 하여 동시 접근을 방지했다.
( + process.c의 process_exec() 함수 안에서 load()를 호출하는 경우에도 File System에 접근하므로, 동일하게 filesys_lock을 획득한 후 실행되어야 한다.)
👇🏻Gitbook 내용
- syscall.h에서 filesys_lock을 전역으로 새로 선언하고 syscall_init에서 초기화한다.
/* userprog/syscall.h */
#ifndef USERPROG_SYSCALL_H
#define USERPROG_SYSCALL_H
#include "threads/synch.h"
void syscall_init(void);
struct lock filesys_lock; // 추가
#endif /* userprog/syscall.h */
/* userprog/syscall.c */
void syscall_init(void)
{
...
lock_init(&filesys_lock);
}
System Calls 요구사항 및 구현사항
create
- 파일의 초기 크기가
initial_size
바이트인 새 파일을 생성합니다. - 성공적으로 생성되면 true를 반환하고, 그렇지 않으면 false를 반환합니다.
remove
file
이라는 이름의 파일을 삭제합니다.- 성공적으로 삭제되면 true를 반환하고, 그렇지 않으면 false를 반환합니다.
- 열려 있는 파일이 제거될 경우
- Unix 표준 시맨틱스를 구현해야 한다. 👇🏻
- 기존의 파일 디스크립터는 여전히 파일에 접근할 수 있습니다:
- 해당 파일에 대한 파일 디스크립터를 가진 프로세스는 여전히 해당 디스크립터를 사용하여 파일을 읽거나 쓸 수 있습니다.
- 다른 프로세스는 해당 파일을 열 수 없으며, 파일은 이름을 가지지 않게 됩니다.
- 그러나 모든 파일 디스크립터가 닫히거나 기계가 종료될 때까지 파일은 존재하게 됩니다.
- Unix 표준 시맨틱스를 구현해야 한다. 👇🏻
open
- "file"이라는 이름의 파일을 엽니다. "파일 디스크립터"라고 불리는 음수가 아닌 정수 핸들(fd)을 반환하며, 파일을 열 수 없는 경우 -1을 반환합니다.
- 파일 디스크립터 0과 1은 콘솔에 예약되어 있습니다: fd 0 (STDIN_FILENO)은 표준 입력(standard input)이며, fd 1 (STDOUT_FILENO)은 표준 출력(standard output)입니다. open 시스템 호출은 이러한 파일 디스크립터 중 하나를 반환하지 않으며, 이들은 명시적으로 아래에서 설명된 시스템 호출 인수로만 유효합니다.
- 각 프로세스는 독립적인 파일 디스크립터 세트를 가지고 있으며, 파일 디스크립터는 자식 프로세스에게 상속됩니다.
- 하나의 파일이 하나 이상의 방법으로 열린 경우, 단일 프로세스나 다른 프로세스에 의해 각각의 열기는 새로운 파일 디스크립터를 반환합니다.
- (같은 파일이 여러 번 열리면, 동일한 파일에 대해 각각 다른 파일 디스크립터가 반환됩니다.)
- 하나의 파일에 대한 다른 파일 디스크립터는 독립적으로 닫히며 파일 위치를 공유하지 않습니다.
- 추가 작업을 수행하기 위해, 0부터 시작하는 정수를 반환하는 Linux 체계를 따라야 합니다.
1) File Descriptor
파일 입출력을 위해서는 파일 디스크립터를 구현해야 한다.
🤔 File Descriptor Table이란?
- 각 프로세스가 가지고 있는 파일 객체와 연결된 File Descriptor(fd)의 배열이다.
- File Descriptor 등록 시 fd는 2부터 순차적으로 1씩 증가한다.
1-1) fdt (File Descriptor Table) 구현하기
struct thread
- 스레드 구조체에 파일 디스크립터 테이블
fdt
와 새로 할당할 fd의 인덱스를 나타낼next_fd
를 추가한다.
/* threads/thread.h */
struct thread
{
...
int init_priority;
struct lock *wait_on_lock;
struct list donations;
struct list_elem donation_elem;
struct file **fdt; // 추가
int next_fd; // 추가
...
}
thread_create
- 새로 만든 fdt를 초기화해준다.
- FDT_PAGES의 크기가 1이어도 충분할 것 같은데 2 미만으로 지정하면 multi-oom 테스트 케이스에서 간헐적으로 FAIL이 뜬다.. 이유를 잘 모르겠다ㅠㅠ 아시는 분이 이 글을 보신다면 가르침을 주세요,,🙏
/* threads/thread.h */
#define FDT_PAGES 2
#define FDT_COUNT_LIMIT 128
/* threads/thread.c */
tid_t thread_create(const char *name, int priority, thread_func *function, void *aux)
{
...
t->fdt = palloc_get_multiple(PAL_ZERO, FDT_PAGES); // 추가
if (t->fdt == NULL) // 추가
return TID_ERROR; // 추가
/* Add to run queue. */
thread_unblock(t);
preempt_priority();
return tid;
}
init_thread
- 새로 만든 next_fd를 초기화해준다.
/* threads/thread.c */
static void
init_thread(struct thread *t, const char *name, int priority)
{
...
t->next_fd = 2; // 추가
}
1-2) fdt에 파일을 추가하는 함수
process_add_file
- 현재 스레드의 fdt에 현재 파일을 추가한다.
- 스레드에 저장되어 있는 next_fd부터 탐색을 시작해 LIMIT 전까지 탐색해서 빈 자리에 할당한다.
- 할당을 성공했으면 fd를, 실패했으면 -1을 리턴한다.
/* userprog/process.h */
int process_add_file(struct file *f);
/* userprog/process.c */
// 파일 객체에 대한 파일 디스크립터를 생성하는 함수
int process_add_file(struct file *f)
{
struct thread *curr = thread_current();
struct file **fdt = curr->fdt;
// limit을 넘지 않는 범위 안에서 빈 자리 탐색
while (curr->next_fd < FDT_COUNT_LIMIT && fdt[curr->next_fd])
curr->next_fd++;
if (curr->next_fd >= FDT_COUNT_LIMIT)
return -1;
fdt[curr->next_fd] = f;
return curr->next_fd;
}
2) open 구현사항
- file_name 포인터 주소를 검증한다.
- filesys_open를 호출해서 파일을 open한다.
- 이 함수는 주어진 파일 이름
name
에 해당하는 파일을 찾아서 열기 위해 사용된다. - 파일이 성공적으로 열리면 해당 파일에 대한 구조체 포인터인
struct file *
을 반환하고, 파일을 찾지 못하거나 내부 메모리 할당에 실패한 경우에는NULL
포인터를 반환한다.
- 이 함수는 주어진 파일 이름
- 파일을 열 수 없는 경우 (file == NULL인 경우)에는 -1을 리턴한다.
- process_add_file 함수를 호출해서 파일 디스크립터 테이블 (fdt)에 파일을 추가하고 fd를 반환받는다.
- fdt에 추가할 수 없는 경우 (fd == -1인 경우)에는 파일을 닫고 -1을 리턴한다.
- 오픈한 파일의 fd를 반환한다.
filesize
fd
로 열려있는 파일의 사이즈를 byte 단위로 리턴한다.
1) filesize 구현사항
- process_get_file를 호출해 fd에 해당하는 파일 객체를 찾는다.
- process_get_file 함수는 새로 선언한다.
- 찾은 파일 객체를 인자로 file_length 함수를 호출한다.
- file_length 함수는 파일의 크기를 bytes 단위로 반환한다.
2) process_get_file
- 스레드가 가진 파일 디스크립터 테이블 fdt에서 fd에 해당하는 파일을 리턴하는 함수
/* userprog/process.h */
int process_add_file(struct file *f);
/* userprog/process.c */
// 파일 객체를 검색하는 함수
struct file *process_get_file(int fd)
{
struct thread *curr = thread_current();
struct file **fdt = curr->fdt;
/* 파일 디스크립터에 해당하는 파일 객체를 리턴 */
/* 없을 시 NULL 리턴 */
if (fd < 2 || fd >= FDT_COUNT_LIMIT)
return NULL;
return fdt[fd];
}
seek
seek
함수는 파일 내에서 다음에 읽거나 쓸 바이트의 위치를 변경합니다. 이 위치는 파일의 시작으로부터 바이트 단위로 표현됩니다.- 파일의 현재 끝을 넘어서서
seek
를 수행하는 것은 오류를 발생시키지 않습니다. 즉, 파일 끝을 넘어서seek
를 수행해도 문제가 되지 않습니다. - 이후에 수행되는 읽기 작업에서 0바이트를 얻게 되면 파일의 끝을 의미합니다. 즉, 파일의 끝에 도달했을 때 읽기 작업을 수행하면 더 이상 읽을 내용이 없다는 것을 알 수 있습니다.
- 이후에 수행되는 쓰기 작업은 파일을 확장하며, 쓰기되지 않은 공간은 0으로 채워집니다. 따라서 파일의 크기가 쓰기 작업에 의해 자동으로 늘어나며, 쓰기되지 않은 공간은 0으로 채워집니다.
- Pintos 시스템에서는 프로젝트 4가 완료될 때까지 파일의 길이가 고정되어 있으므로, 파일 끝을 넘어서 쓰기 작업을 수행하면 오류가 발생합니다. 따라서 프로젝트 4 이전에는 파일의 끝을 넘어서 쓰기 작업을 수행할 수 없습니다.
- 이러한 동작은 파일 시스템에서 구현되어 있으며, 시스템 호출 구현에는 특별한 노력이 필요하지 않습니다.
1) seek 구현사항
- process_get_file를 호출해 fd에 해당하는 파일 객체를 찾는다.
- 찾은 파일 객체를 인자로 file_seek 함수를 호출한다.
- file_seek 함수는 파일의 현재 위치를 position으로 이동시킨다.
tell
- 파일 내에서 다음에 읽거나 쓸 위치를 반환합니다. 이 위치는 파일의 시작으로부터 바이트 단위로 표현됩니다.
1) tell 구현사항
- process_get_file를 호출해 fd에 해당하는 파일 객체를 찾는다.
- 찾은 파일 객체를 인자로 file_tell 함수를 호출한다.
- file_tell 함수는 파일의 위치(offset)를 반환한다.
close
- 파일 디스크립터
fd
를 닫습니다. - 프로세스가 종료되거나 중단될 때, 해당 프로세스의 모든 열린 파일 디스크립터가 암묵적으로 닫힙니다. 마치 이 함수를 각각의 파일 디스크립터에 대해 호출한 것과 같은 효과가 있습니다.
1) close 구현사항
- process_get_file를 호출해 fd에 해당하는 파일 객체를 찾는다.
- 찾은 파일 객체를 인자로 file_close 함수를 호출한다.
- file_close 함수는 파일닫는다.
- 파일 디스크립터 테이블에서 fd에 해당하는 파일을 제거하는 함수 process_close_file을 호출한다.
- process_close_file 함수는 새로 선언한다.
2) process_close_file
- 파일 디스크립터 테이블에서 fd에 해당하는 index에 NULL을 할당해서 파일과의 연결을 끊어준다.
// 파일 디스크립터 테이블에서 파일 객체를 제거하는 함수
void process_close_file(int fd)
{
struct thread *curr = thread_current();
struct file **fdt = curr->fdt;
if (fd < 2 || fd >= FDT_COUNT_LIMIT)
return NULL;
fdt[fd] = NULL;
}
read
read
함수는 파일 디스크립터fd
로 열린 파일에서size
바이트의 데이터를 읽어와서buffer
에 저장합니다.read
함수는 실제로 읽어온 바이트 수를 반환합니다. 파일의 끝에 도달하면 0을 반환합니다. 즉, 더 이상 읽을 내용이 없을 때 0을 반환합니다.- 파일을 읽을 수 없는 경우 (파일의 끝이 아닌 다른 조건으로 인해), -1을 반환합니다. 예를 들어 파일이 존재하지 않거나 읽기 권한이 없는 경우 등이 해당될 수 있습니다.
- 파일 디스크립터
fd
가 0인 경우,read
함수는 키보드로부터 데이터를 읽어옵니다. 이때input_getc()
함수를 사용하여 키보드 입력을 받습니다.
1) read 구현사항
- buffer 포인터 주소를 검증한다.
- fd가 0인 경우, 키보드로부터 입력을 받아오는 input_getc 함수를 호출해 size만큼 값을 읽는다.
- fd가 2보다 작은 경우(위에서 0인 경우를 걸렀으므로 1인 경우) standard output 작업에 해당하므로 -1을 리턴한다.
- 이외의 경우에는 process_get_file를 호출해 fd에 해당하는 파일 객체를 찾는다.
- 찾은 파일 객체를 인자로 file_read 함수를 호출한다.
- file_read 함수는 파일에서 현재 위치부터 "SIZE" 바이트 수만큼 데이터를 읽어서 "BUFFER"에 저장한다.
write
buffer
에서size
바이트 수만큼 데이터를 읽어서fd
에 연결된 파일에 씁니다.- 파일의 끝을 초과하는 위치에 쓰려고 해도 파일이 확장되지 않으며, 가능한 한 많은 바이트 수만큼 파일 끝까지 쓰기를 시도합니다.
- 쓰여진 실제 바이트 수가 반환되며, 모든 바이트가 쓰여지지 않은 경우에는 쓰여진 바이트 수가
size
보다 작게 반환됩니다. fd 1
은 콘솔(console)에 쓰기 작업을 수행합니다.- 콘솔(console)에 쓰기 작업을 수행할 때
putbuf()
함수를 한 번 호출하여 가능한 모든buffer
데이터를 한 번에 쓰는 것이 좋습니다. size
가 큰 경우에는 적절한 크기로 분할하여 작성해야 합니다.
- 콘솔(console)에 쓰기 작업을 수행할 때
1) write 구현사항
- buffer 포인터 주소를 검증한다.
- fd가 1인 경우, 버퍼에 저장된 데이터를 화면에 출력하는 putbuf 함수를 호출한다.
- fd가 2보다 작은 경우(위에서 1인 경우를 걸렀으므로 0인 경우) standard input 작업에 해당하므로 -1을 리턴한다.
- 이외의 경우에는 process_get_file를 호출해 fd에 해당하는 파일 객체를 찾는다.
- 찾은 파일 객체를 인자로 file_write 함수를 호출한다.
- file_write 함수는 파일에서 현재 위치부터 "SIZE" 바이트 수만큼 "BUFFER"에 있는 데이터를 쓴다.
테스트
File System 관련 함수들을 먼저 구현하고 나서 Process 관련 함수들을 구현하는 순서로 진행했다.
exec, wait, fork를 구현하지 않은 상태에서 테스트한 결과는 아래와 같았다.
pass tests/userprog/args-none
pass tests/userprog/args-single
pass tests/userprog/args-multiple
pass tests/userprog/args-many
pass tests/userprog/args-dbl-space
pass tests/userprog/halt
pass tests/userprog/exit
pass tests/userprog/create-normal
pass tests/userprog/create-empty
pass tests/userprog/create-null
pass tests/userprog/create-bad-ptr
pass tests/userprog/create-long
pass tests/userprog/create-exists
pass tests/userprog/create-bound
pass tests/userprog/open-normal
pass tests/userprog/open-missing
pass tests/userprog/open-boundary
pass tests/userprog/open-empty
pass tests/userprog/open-null
pass tests/userprog/open-bad-ptr
pass tests/userprog/open-twice
pass tests/userprog/close-normal
pass tests/userprog/close-twice
pass tests/userprog/close-bad-fd
pass tests/userprog/read-normal
pass tests/userprog/read-bad-ptr
pass tests/userprog/read-boundary
pass tests/userprog/read-zero
pass tests/userprog/read-stdout
pass tests/userprog/read-bad-fd
pass tests/userprog/write-normal
pass tests/userprog/write-bad-ptr
pass tests/userprog/write-boundary
pass tests/userprog/write-zero
pass tests/userprog/write-stdin
pass tests/userprog/write-bad-fd
FAIL tests/userprog/fork-once
FAIL tests/userprog/fork-multiple
FAIL tests/userprog/fork-recursive
FAIL tests/userprog/fork-read
FAIL tests/userprog/fork-close
FAIL tests/userprog/fork-boundary
FAIL tests/userprog/exec-once
FAIL tests/userprog/exec-arg
FAIL tests/userprog/exec-boundary
FAIL tests/userprog/exec-missing
pass tests/userprog/exec-bad-ptr
FAIL tests/userprog/exec-read
FAIL tests/userprog/wait-simple
FAIL tests/userprog/wait-twice
FAIL tests/userprog/wait-killed
pass tests/userprog/wait-bad-pid
FAIL tests/userprog/multi-recurse
FAIL tests/userprog/multi-child-fd
FAIL tests/userprog/rox-simple
FAIL tests/userprog/rox-child
FAIL tests/userprog/rox-multichild
pass tests/userprog/bad-read
pass tests/userprog/bad-write
pass tests/userprog/bad-read2
pass tests/userprog/bad-write2
pass tests/userprog/bad-jump
pass tests/userprog/bad-jump2
pass tests/filesys/base/lg-create
pass tests/filesys/base/lg-full
pass tests/filesys/base/lg-random
pass tests/filesys/base/lg-seq-block
pass tests/filesys/base/lg-seq-random
pass tests/filesys/base/sm-create
pass tests/filesys/base/sm-full
pass tests/filesys/base/sm-random
pass tests/filesys/base/sm-seq-block
pass tests/filesys/base/sm-seq-random
FAIL tests/filesys/base/syn-read
pass tests/filesys/base/syn-remove
FAIL tests/filesys/base/syn-write
FAIL tests/userprog/no-vm/multi-oom
pass tests/threads/alarm-single
pass tests/threads/alarm-multiple
pass tests/threads/alarm-simultaneous
pass tests/threads/alarm-priority
pass tests/threads/alarm-zero
pass tests/threads/alarm-negative
pass tests/threads/priority-change
pass tests/threads/priority-donate-one
pass tests/threads/priority-donate-multiple
pass tests/threads/priority-donate-multiple2
pass tests/threads/priority-donate-nest
pass tests/threads/priority-donate-sema
pass tests/threads/priority-donate-lower
pass tests/threads/priority-fifo
pass tests/threads/priority-preempt
pass tests/threads/priority-sema
pass tests/threads/priority-condvar
pass tests/threads/priority-donate-chain
22 of 95 tests failed.
'Computer Science' 카테고리의 다른 글
[Pintos-KAIST] Project 3 :: Memory Management (1) | 2023.05.16 |
---|---|
[Pintos-KAIST] Project 2 :: Argument Passing (User 프로그램 인자 설정하기) (0) | 2023.05.16 |
[Pintos-KAIST] Project 3 :: Stack Growth (Pintos의 스택 확장 메커니즘) (0) | 2023.05.16 |