problem decomposition 중요
open, close, read, write, lseek → deep한 method
java → shallow method
DOS는 os와 커널간의 protection이 없었음
privilege → 하드웨어가 os(sw)보다 privilege가 높다.
virtual adrress → 임의
physical adrress → os가 ok한 → protection을 위함
MMU가 만들어진 이유 하드웨어단에서 → 소프트웨어가 하면 엄청 느리거든
어플리케이션간 메모리를 분리하기위해 프로텍션이 필요한데 그래서 운영체제에서 제공하는 것이 바로 프로세스 개념임
userprog
디렉토리에서 과제를 수행하게 될 것이고 pintos 거의 모든 파트를 건드리게 될 것임.시작하기전에 동기화랑, 가상 주소 읽는걸 적극 추천함..
userprog
에 파일 몇개 없는데 여기서 대부분의 작업을 수행할 것임process.c
, process.h
: ELF 바이너리를 로드하고 프로세스를 시작함syscall.c
, syscall.h
exception.c
, exception.h
page_fault()
를 수정해야 함.(exception.c
108라인)filesys.h
, file.h
인터페이스를 통해 파일 시스템 사용 방법, 많은 제한 사항들을 이해할 수 있다.filesys_remove()
를 위한 Unix-like semantics 가 내장되어있다. 만약 실행되고 있는 파일이 삭제된다면, 이 블록은 반환되지 않으며, 이 파일을 열려는 스레드가 모두 닫히기 전에는 접속할 수 있게 되버린다. Removing an open files
를 참고하라.userprog/proces.c
안에 로더와 함께 ELF excutable을 로드할 수 있다. ELF는 Linux나 solaris 등 에서 사용되는 파일 포맷이다.incldue/threads/vaddr.h
안에 정의되어있다. 기본값은 0x800400000) 커널 가상 메모리는 나머지를 차지하고 있다.userprog/exception.c
안의 page_fault()
에 의해 page fault가 발생하고, 프로세스는 종료될 것이다. 매핑되지 않은 유저 가상 메모리에 접속하려고 하면 page fault가 일어난다.userprog/pagedir
의 함수와 include/threads/vaddr.h
를 봐라.KERN_BASE 이전
을 가리키는지만 확인하는 것이다. 잘못된 유저 포인터였다면 page fault를 일으킬 것이고, 이는 page_fault()
로 수정할 수 있다. 이 방법은 MMU방식과 같기 때문에 빨리 수행할 수 있기 때문에 Linux같은 실제 커널에서 사용된다.사용자 프로그램에 대한 인수 설정
process_exec()
%rdi
, %rsi
, %rdx
, %rcx
, %r8
, %r9
로 전달된다. 만약 7개 혹은 그 이상의 인자가 들어오면 스택으로 들어간다.%rax
로 전달됨.%rdi
, %rsi
, %rdx
, %rcx
, %r8
, %r9
)+----------------+
stack pointer --> 0x4747fe70 | return address |
+----------------+
RDI: 0x0000000000000001 | RSI: 0x0000000000000002 | RDX: 0x0000000000000003
/lib/user/entry.c
의 _start()
에서 부터이다. 이 함수는 main()을 실행하고 main이 return 하면 exit()을 호출한다./bin/ls -l foo bar
가 전달될 때 예시커맨드를 단어로 분리함 → /bin/ls
, -l
, foo
, bar
단어들을 스택 최상단에 넣어준다. 이때 포인터로 참조되기 때문에 순서는 상관없음.
각 문자열의 주소와 null pointer sentinel을 스택에서 오른쪽 → 왼쪽 순서로 넣는다.
%rsi
가 argv의 주소, %rdi
가 argc를 가리키도록 한다.
마지막으로 가짜 return address를 넣어줌. 엔트리 함수는 반환되지 않으나 다른 스택 프레임과 동일한 구조를 만들어주기 위해 넣는다.
유저 프로그램이 실행되기 직전의 stack 상태와 관련된 레지스터
process_exec()
은 유저가 입력한 명령어를 수행할 수 있도록 프로그램을 메모리에 적재하고 실행하는 함수다. → 현재 실행 중인 스레드의 context를 f_name에 해당하는 명령을 실행하기 위해 context switching하는 것이 process_exec()의 역할f_name
에 문자열로 저장되어 있으나 현재 process_exec()
은 새 프로세스에 인수 전달을 지원하지 않음.process_exec()
에 코드를 추가해서 간단히 프로그램 파일 이름을 인자로 넣는것 대신에, space가 올 때마다 단어를 parsing하도록 만들어야함.process_exec()
에서 두 인자 foo, bar로 parsing되어야 함pintos는 프로그램과 인자를 구분하지 못하는 구조
스택 프레임(stack frame) : 함수가 호출되면 스택에는 함수의 매개변수, 호출이 끝난 뒤 돌아갈 반환 주소값, 함수에서 선언된 지역변수 등이 저장됨. 이렇게 스택영역에 차례대로 저장되는 함수의 호출정보를 스택 프레임이라고 함. 이러한 스택 프레임 덕에 함수의 호출이 모두 끝난 뒤, 해당 함수가 호출되기 이전 상태로 되돌아갈 수 있음
프레임 포인터(FP) 레지스터 : 함수가 호출 되기 전의 스택메모리 주소를 저장하고 있음
스택 포인터(SP) 레지스터 : 함수가 호출되고 현재 가리키고 있는 스택메모리 주소를 저장하고 있음(스택 포인터는 CPU 안에) → 스택포인터가 가리키는 곳 까지가 데이터가 채워진 영역(스택 시작점 부터 SP까지), 그 이후부터 스택 끝까지는 비어있는 영역
스택은 새로운 데이터가 추가될수록 주소값이 점점 작아짐 → 스택은 커널 반대 방향으로 자라기 때문에 커널에 침범하는 일이 없게됨.
인터럽트 프레임 : 인터럽트가 들어왔을 때, 이전에 레지스터에 작업하던 context를 switching하기 위해 이 정보를 담아놓은 구조체 → gp_registers R
이 스레드가 작업하고 있을때의 레지스터 값. 이걸 읽어서 do_iret을 함.
구현 소스코드 - https://github.com/SWJungle4A/pintos12-team04/tree/yeopto/argument-passing
// 주소값이 유저 영역(0x8048000~0xc0000000)에서 사용하는 주소값인지 확인하는 함수
void check_address(const uint64_t *uaddr)
{
struct thread *cur = thread_current();
if (uaddr == NULL || !(is_user_vaddr(uaddr)) || pml4_get_page(cur->pml4, uaddr) == NULL)
{
exit(-1);
}
}
userprog/syscall.c
에서 구현void
syscall_handler (struct intr_frame *f UNUSED) { // 어셈에서 이거 실행
// TODO: Your implementation goes here.
// printf ("system call!\n");
char *fn_copy;
/*
x86-64 규약은 함수가 리턴하는 값을 rax 레지스터에 배치하는 것
값을 반환하는 시스템 콜은 intr_frame 구조체의 rax 멤버 수정으로 가능
*/
switch (f->R.rax) { // rax is the system call number
case SYS_HALT:
halt(); // pintos를 종료시키는 시스템 콜
break;
case SYS_EXIT:
exit(f->R.rdi); // 현재 프로세스를 종료시키는 시스템 콜
break;
case SYS_FORK:
f->R.rax = fork(f->R.rdi, f); // 자식 스레드 이름으로 인자가 들어감, 인터럽트 프레임이랑 R.rax에 리턴값을 담는거지!!
break;
case SYS_EXEC:
if (exec(f->R.rdi) == -1) {
exit(-1);
}
break;
case SYS_WAIT:
f->R.rax = wait(f->R.rdi);
break;
case SYS_CREATE:
f->R.rax = create(f->R.rdi, f->R.rsi);
break;
case SYS_REMOVE:
f->R.rax = remove(f->R.rdi);
break;
case SYS_OPEN:
f->R.rax = open(f->R.rdi);
break;
case SYS_FILESIZE:
f->R.rax = filesize(f->R.rdi);
break;
case SYS_READ:
f->R.rax = read(f->R.rdi, f->R.rsi, f->R.rdx);
break;
case SYS_WRITE:
f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
break;
case SYS_SEEK:
seek(f->R.rdi, f->R.rsi);
break;
case SYS_TELL:
f->R.rax = tell(f->R.rdi);
break;
case SYS_CLOSE:
close(f->R.rdi);
break;
default:
exit(-1);
break;
// thread_exit ();
}
}
syscall
을 도입했는데 이 것은 시스템 콜을 빠르게 제공함.syscall
명령은 x86-64에서 시스템 호출을 호출하는 데 가장 일반적으로 사용되는 수단임.syscall
은 시스템 호출을 수행하기 위해 호출한다.%rax
가 system call number다.%rcx
가 아닌 %r10
rax
에 있고 인자들은 %rdi
, %rsi
, %rdx
, %r10
, %r8
, %r9
순으로 전달됨..struct intr_frame
**struct intr_frame
is on the kernel stack.**)write()
을 호출한다 → write()
함수는 시스템 콜로, 인자를 유저 스택에 넣고서 커널로 진입한다. 이때 스택 포인터는 인자가 들어간 영역을 가리킴 → 인터럽트 벡터 테이블에 가면 주소(이 주소는 write()
함수 안에 있어) 별로 어떤 종류의 인터럽트를 실행해야 하는지가 맵핑되어 있다.→ 주소가 0x30인 syscall_handler()
를 호출함 (유저 스택의 number는 system call number 다)syscall
명령은 어셈블리어다. → 32비트 x86에서는 프로그래머가 직접 함수를 구현해서 스택에 쌓인 인자를 커널로 옮겨주는 작업을 수행해야했지만, x86-64부터는 syscall이라는 어셈블리어 명령이 추가 되어서 알아서 밑단에서 스택 인자를 커널로 옮겨줌return
하는 값을 rax
레지스터에 배치하는 것. 값을 반환하는 시스템 콜은 intr_frame
구조체의 rax
멤버 수정으로 가능void halt(void) { // pintos 종료 시스템 콜
power_off();
}
void exit(int status) { // 프로세스를 종료시키는 시스템 콜
struct thread *cur = thread_current();
cur->exit_status = status; // 종료시 상태를 확인, 정상 종료면 state = 0
printf("%s: exit(%d)\n", thread_name(), status); // plj2 - process termination messages
thread_exit(); // thread 종료 -> process_exit() 실행
}
현재 유저 프로그램을 종료하고, 커널로 status를 반환함.
프로세스의 상위 항목이 대기하는경우엔, status가 반환됨.
일반적으로 status가 0이면 성공이고 0이 아닌 값은 오류를 나타냄.
exit()
흐름
process_exit()
/* Exit the process. This function is called by thread_exit (). */
void
process_exit (void) {
struct thread *curr = thread_current ();
/* TODO: Your code goes here.
* TODO: Implement process termination message (see
* TODO: project2/process_termination.html).
* TODO: We recommend you to implement process resource cleanup here. */
// P2-4 Close all opened files // 열린 파일 모두 닫기
for (int i = 0; i < FDCOUNT_LIMIT; i++) {
close(i);
}
palloc_free_multiple(curr->fd_table, FDT_PAGES); // multi-oom
file_close(curr->running); //for rox // 닫을때 다시 쓸 수 있게 활성화 시켜줌
process_cleanup ();
// Wake up blocked parent
sema_up(&curr->wait_sema); // 부모 깨워줘
// Postpone child termination until parents receives its exit status with 'wait'
// 부모 프로세스가 sema_up(free_sema)할 때까지 기다림(block 상태 진입)
sema_down(&curr->free_sema); // 자식 잔다.
}
process_wait() 함수는 자식 프로세스가 모두 종료될 때까지 대기하고, 자식 프로세스가 올바르게 종료되었는지 확인 하는 기능
int
process_wait (tid_t child_tid) {
/* XXX: Hint) The pintos exit if process_wait (initd), we recommend you
* XXX: to add infinite loop here before
* XXX: implementing the process_wait. */
struct thread *child = get_child_with_pid(child_tid);
// [Fail] Not my child
if (child == NULL)
return -1;
// Parent waits until child signals (sema_up) after its execution
sema_down(&child->wait_sema); // 자식이 부모 재워
/*
자식 수행 중
*/
/*
exit()에서 자식이 종료할때 자기 부모를 깨울거야 sema_up(curr->wait_sema)
부모를 깨우고 자식은 sema_down(curr->free_sema) 함 -> 그럼 자식의 상태는 blocked가 될거임
*/
int exit_status = child->exit_status;
// Keep child page so parent can get exit_status
list_remove(&child->child_elem); // 자식은 끝났으니까 다 지워줘
sema_up(&child->free_sema); // wake-up child in process_exit - proceed with thread_exit // 자식 재웠으니 자식 깨워줘야지
return exit_status;
}
fork()
fork()
함수를 사용함.main()
함수를 엔트리 포인트로 하는데, 새로 프로세스가 생성될 때마다 main()
함수부터 시작하는 것은 매우 비효율적이고 프로세스간 통신에서 문제가 발생할 수 있다. → 복제를 하면 부모 프로세스가 호출한 시점을 엔트리 포인트로 가지기 때문에 더욱 직관적이고 발생할 수 있는 문제를 줄일 수 있음wait()
하고, 자식 프로세스는 생성되면서 exec()
함수를 호출하게 됨.fork()
함수 흐름 정리
현재 프로세스를 커맨드라인에서 지정된 인수를 전달하여 이름이 지정된 실행 파일로 변경
int exec(char *file_name) { // 현재 프로세스를 커맨드라인에서 지정된 인수를 전달하여 이름이 지정된 실행 파일로 변경
check_address(file_name);
int file_size = strlen(file_name) + 1; // NULL 까지 + 1
char *fn_copy = palloc_get_page(PAL_ZERO);
if (fn_copy == NULL) {
exit(-1);
}
strlcpy(fn_copy, file_name, file_size);
if (process_exec(fn_copy) == -1) {
return -1;
}
NOT_REACHED();
return 0;
}
파일 이름과 파일 사이즈를 인자 값으로 받아 파일을 생성하는 함수. → 새 파일을 생성해도 파일이 열리지 않음.
open()
으로 별도작업 해줘야함.filesys_create
함수는 파일 이름과 파일 사이즈를 인자값으로 받아 파일을 생성하는 함수.
bool create(const char *file, unsigned initial_size) { // file: 생성할 파일의 이름 및 경로 정보, initial_size: 생성할 파일 크기
check_address(file);
return filesys_create(file, initial_size);
}
파일을 삭제하는 시스템 콜로, file 인자는 제거할 파일의 이름 및 경로 정보이다. 성공일 시 true, 실패 시 false 리턴
bool remove(const char *file) {
check_address(file);
return filesys_remove(file);
}
파일을 열 때 사용하는 시스템 콜. 성공 시 fd를 생성하고 반환, 실패 시 -1 반환
int open(const char *file) {
check_address(file); // 사용자 영역인지 확인해보자
struct file *open_file = filesys_open(file); // filesys_open으로 file을 진짜 오픈해줘서 오픈된 파일을 리턴해줌
// open_null 테스트 패스 위해
if (file == NULL) {
return -1;
}
if (open_file == NULL) {
return -1;
}
int fd = add_file_to_fdt(open_file);
if (fd == -1) {
file_close(open_file);
}
return fd;
}
add_file_to_fdt()
int add_file_to_fdt(struct file *file) {
struct thread *cur = thread_current();
struct file **fdt = cur->fd_table;
// fd의 위치가 제한 범위를 넘지 않고, fdtable의 인덱스 위치와 일치한다면
while(cur->fd_idx < FDCOUNT_LIMIT && fdt[cur->fd_idx]) {
cur->fd_idx++; // open-twice -> 같은파일을 두번 열었는데 둘다 2를 리턴하면안돼 그래서 인덱스를 ++를 해줘서 다르게
}
if (cur->fd_idx >= FDCOUNT_LIMIT)
return -1;
fdt[cur->fd_idx] = file;
return cur->fd_idx;
}
파일 크기를 알려주는 시스템 콜, fd인자를 받아 파일 크기 리턴
int filesize(int fd) {
struct file *open_file = find_file_by_fd(fd);
if (open_file == NULL) {
return -1;
}
return file_length(open_file);
}
find_file_by_fd()
static struct file *find_file_by_fd(int fd) {
struct thread *cur = thread_current();
if (fd < 0 || fd >= FDCOUNT_LIMIT) {
return NULL;
}
return cur->fd_table[fd];
}
read()는 열린 파일의 데이터를 읽는 시스템 콜
int read(int fd, void *buffer, unsigned size) { // buffer는 읽은 데이터를 저장할 버퍼의 주소값, size는 읽을 데이터의 크기
check_address(buffer);
off_t read_byte;
uint8_t *read_buffer = buffer;
if (fd == 0) { // fd 값이 0일 때는 표준입력이기 때문에 input_getc() 함수를 이용하여 키보드의 데이터를 읽어 버퍼에 저장함.
char key;
for (read_byte = 0; read_byte < size; read_byte++) {
key = input_getc();
*read_buffer++ = key;
if (key == '\0') {
break;
}
}
}
else if (fd == 1) { // tests/userprog/read-stdout
return -1;
}
else {
struct file *read_file = find_file_by_fd(fd);
if (read_file == NULL) {
return -1;
} // race-condition을 피하기 위해 읽을 동안 lock
lock_acquire(&filesys_lock);
read_byte = file_read(read_file, buffer, size);
lock_release(&filesys_lock);
}
return read_byte;
}
write() 함수는 열린 파일의 데이터를 기록하는 시스템 콜
// buffer로부터 사이즈 쓰기
int write(int fd, const void *buffer, unsigned size)
{
check_address(buffer);
int write_result;
if (fd == 0) // stdin
{
return 0;
}
else if (fd == 1) // stdout
{
putbuf(buffer, size);
return size;
}
else
{
struct file *write_file = find_file_by_fd(fd);
if (write_file == NULL)
{
return 0;
}
lock_acquire(&filesys_lock);
off_t write_result = file_write(write_file, buffer, size);
lock_release(&filesys_lock);
return write_result;
}
}
seek()은 열려있는 파일 fd에 쓰거나 읽을 바이트 위치를 인자로 넣어줄 position 위치로 변경하는 함수, 파일 위치(offset)로 이동하는 함수, 우리가 입력해줄 position 위치부터 읽을 수 있도록 해당 position을 찾는 함수
void seek(int fd, unsigned position) {
struct file *seek_file = find_file_by_fd(fd);
if (seek_file <= 2) { // 초기값 2로 설정. 0: 표준 입력, 1: 표준 출력
return;
}
seek_file->pos = position;
}
파일의 위치(offset)을 알려주는 함수
unsigned tell(int fd) {
struct file *tell_file = find_file_by_fd(fd);
if (tell_file <= 2) {
return;
}
return file_tell(tell_file);
}
열린 파일을 닫는 시스템 콜. 파일을 닫고 fd제거
void close(int fd) {
struct file *fileobj = find_file_by_fd(fd);
if (fileobj == NULL) {
return;
}
remove_file_from_fdt(fd);
}
remove_file_from_fdt()
void remove_file_from_fdt(int fd) {
struct thread *cur = thread_current();
//error -invalid fd
if (fd < 0 || fd >= FDCOUNT_LIMIT)
return;
cur->fd_table[fd] = NULL;
}
프로세스 종료 메세지 출력
exit
호출 되거나 다른 이유로 인해 사용자 프로세스가 종료 될때 마다 프로세스 이름과 종료 코드를 프린트 해줘야함printf ("%s: exit(%d)\n", ...);
fork()
에 전달된 전체 이름이어야 함.void exit(int status) { // 프로세스를 종료시키는 시스템 콜
struct thread *cur = thread_current();
cur->exit_status = status;
printf("%s: exit(%d)\n", thread_name(), status); // plj2 - process termination messages
thread_exit(); // thread 종료
}
실행 파일에 쓰기를 거부함.
file_deny_write()
를 사용해서 열린 파일에 쓰기를 방지할 수 있음. 파일에서 file_allow_write()
를 호출하면 해당 파일이 다른 오프너에 의해 쓰기가 거부되지 않는 한 다시 활성화됨.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;
}
// project 2-5 deny writes to running exec
t->running = file; //
file_deny_write(file); // 열린 파일에 쓰기 방지함.
.
.
.
User Mode → Kernel Mode 요청
Kernel Mode → User Mode 반환
open() 호출 → 커널 모드 진입 → open에 대한 입력값을 커널로 전달 → 해당 일 완료하고 커널에서 return 하면서 유저모드로 돌아감.
구조
프로세스 메모리 구조
커널 영역 구조(physical memory 에서)
project1때는 개념부터 잡고 한다고 너무 느렸어서 이번엔 코드보면서 구현부터 해보자는 생각에 여러 코드들을 보며 만들었는데 안돌아갔다.. 방대한 양을 따라쳤는데 안 돌아가서 왜 안돌아가지 하면서 시간을 오래 투자했는데, 그 투자한 시간이 공부한다는 느낌보단 틀린그림찾기 하는데 시간을 투자하는 느낌이었다. 굉장히 허탈했다. 난 지금 뭐하고 있는거지 싶었다. 꾸역꾸역 돌아가게 만들어놓고 방대한 과제의 흐름을 잡기 시작했다. 물론 따라치면서 아무 이해없이 따라친건 아니었기에 흐름을 잡고 코드를 다시 보니 이해가 더 잘 됐다. 난 아무래도 개념부터 하고 코드에 대해 공부하는게 시간이 더 걸리더라도 나에게 효율적인것 같았다. 남는 것도 있는거 같고. 진짜 악명 높은 pintos.. 어렵다.. 그래도 이 과제를 하면서 운영체제에 대해 2프로정도는 알게된거같다. project4가 끝나면 10퍼정도 알았으면 좋겠다. 이제 조가 바뀐다. 셋 다 공부 방식이 조금씩 다르지만 서로의 방식을 존중하면서 서로 부족한거 채워주는게 굉장히 좋았다. 하지만 다른 몇몇 조처럼 좀 더 똘똘 뭉쳐야했나란 아쉬움도 있었다.(끝날때가 되니까 드는 생각) pintos과제도 어느정도 적응이 되었으니 다음 과제부턴 협업부분을 조금 더 고민해봐야겠다.
잘봤습니다 👍