WEEK[08~13] PintOS Project 2 User Programs

yeopto·2022년 6월 6일
0

SW사관학교 정글

목록 보기
11/14
post-thumbnail

OS 권영진 교수님 강의(22/06/03)


  1. problem decomposition 중요

  2. open, close, read, write, lseek → deep한 method

  3. java → shallow method

  4. DOS는 os와 커널간의 protection이 없었음

  5. privilege → 하드웨어가 os(sw)보다 privilege가 높다.

  6. virtual adrress → 임의

    physical adrress → os가 ok한 → protection을 위함

  7. MMU가 만들어진 이유 하드웨어단에서 → 소프트웨어가 하면 엄청 느리거든

  8. 어플리케이션간 메모리를 분리하기위해 프로텍션이 필요한데 그래서 운영체제에서 제공하는 것이 바로 프로세스 개념임

Project 2 : User Programs


Introduction


개요


  • User program 실행을 허용하는 시스템 부분에 대한 작업을 해야함.
  • 지금 I/O(입출력) 호환은 안됨.
  • 프로그램이 시스템 호출을 통해 OS와 상호 작용할 수 있도록 해야한다.
  • userprog 디렉토리에서 과제를 수행하게 될 것이고 pintos 거의 모든 파트를 건드리게 될 것임.

배경


  • project 1에서의 실행한 모든 코드는 운영 체제 커널의 일부였음.
  • 커널에서 돌아갔고, 완전한 시스템 엑세스 권한이 있었음.
  • OS 위에서 사용자 프로그램을 실행하기 시작하면 이것은 이제 문제가 됨
  • project2는 이 문제를 다룰거임.
  • 한번에 둘 이상의 프로세스를 실행할 수 있다. 각 프로세스는 하나의 스레드가 있음(멀티 스레드 지원 X)
  • 사용자 프로그램은 그들이 기계를 가지고 있다는 환상속에서 만들어진다.
  • 즉, 한번에 여러 프로세스를 로드하고 실행 할 때 이러한 착각을 올바르게 유지하려면 메모리, 스케줄링등등을 관리해야만 함.
  • 시작하기전에 동기화랑, 가상 주소 읽는걸 적극 추천함..
  • 시작하기전에 개요를 파악하려면 각 파트를 간단하게 검토해봐.
  • userprog 에 파일 몇개 없는데 여기서 대부분의 작업을 수행할 것임

소스파일


  • process.c , process.h : ELF 바이너리를 로드하고 프로세스를 시작함
    • ELF(Excutable and Linkable Format) : 실행파일, 목적 파일, 공유 라이브러리 그리고 코어 덤프를 위한 표준 파일 형식
    • 코어 덤프 : 컴퓨터 프로그램이 특정 시점에 작업 중이던 메모리 상태를 기록한 것. → 보통 프로그램이 비정상적으로 종료했을 때 만들어짐. 실제로는 그 외에 중요한 프로그램 상태도 같이 기록되곤 함.(PC, stack pointer 등 CPU 레지스터나, 메모리 관리 정보, 그 외 프로세서 및 운영 체제 플래그 정보 등이 포함 됨)
  • syscall.c, syscall.h
    • 이것은 시스템 콜 핸들러의 기본 구조임.
    • 현재는 단지 메시지를 출력하고 사용자 프로세스를 종료함.
    • 여기서 시스템 콜에 필요한 모든 코드를 작성해야함.
  • exception.c, exception.h
    • 사용자 프로세스가 권한 또는 금지된 작업을 수행할 때 예외 또는 장애로 커널에 트랩됨. 이러한 파일은 예외처리함. 현재 모든 예외는 단순히 메시지를 인쇄하고 프로세스를 종료한다. 이번 프로젝트의 일부 솔루션은 page_fault() 를 수정해야 함.(exception.c 108라인)
    • 트랩 : 어떤 프로세스가 특정 시스템 기능을 사용하려고 할 때 그 기능을 운영체제에게 요청하는 방법을 말함

파일 시스템 사용


  • 파일 시스템에서, 사용자 프로그램이 로드되고 구현되어야하는 많은 시스템 호출이 파일 시스템을 처리하기 때문에 이 프로젝트는 파일 시스템 코드에 대한 인터페이스가 필요함.
  • 그러나 프로젝트 초점이 파일 시스템이 아니라서 filesys 디렉토리에 단순하지만 완전한 파일 시스템이 제공 되어있음 → filesys.h , file.h 인터페이스를 통해 파일 시스템 사용 방법, 많은 제한 사항들을 이해할 수 있다.
  • 파일 시스템 코드를 고치지 마세요.. 이 프로젝트의 초점에서 벗어난거야
  • 파일 시스템 루틴을 올바르게 사용하면 파일 시스템 구현을 개선하는 프로젝트 4가 수월해질거임. 그 전까지는 이 제한 사항들을 따라야함.
    • 내부 동기화가 없어. 동시 액세스는 서로 간섭할꺼야. 동기화를 사용해서 한 번에 하나의 프로세스만 파일 시스템 코드를 실행하도록 해야함
    • 파일 크기는 생성 시 고정됨. 루트 디렉토리는 파일로 표시되므로 만들 수 있는 파일 수도 제한됨.
    • 파일 데이터는 단일 익스텐트로 할당됨. 즉, 단일 파일의 데이터는 디스크의 연속적인 섹터 범위를 차지해야함. 따라서 파일시스템 사용됨에 따라 시간이 흐르면서 external fragmentation(외부 단편화)은 심각한 문제가 될 수 있다.
    • 하위 디렉토리 없음
    • 파일이름은 14글자로 제한
    • 시스템 충돌 작업 도중 디스크가 자동으로 복구할 수 없는 방식으로 손상될 수 있음. 어쨋든 파일 시스템 복구 도구는 없다.
  • 중요 사항이 있음
    • filesys_remove() 를 위한 Unix-like semantics 가 내장되어있다. 만약 실행되고 있는 파일이 삭제된다면, 이 블록은 반환되지 않으며, 이 파일을 열려는 스레드가 모두 닫히기 전에는 접속할 수 있게 되버린다. Removing an open files를 참고하라.

유저 프로그램 동작 방법


  • 메모리가 여유로우며 작성한 시스템 콜만 사용하는 한, PintOS는 일반 C 프로그램을 수행할 것이다. 이번 프로젝트에서는 메모리 할당이 필요하지 않기 때문에 malloc()은 수행되지 않는다. 커널이 스레드를 바꿀 때, 프로세서의 floating-point를 저장하거나 복구하지 않기 때문에, pintos는 floating point operation이 있는 프로그램을 수행하지 않는다. pintos는 userprog/proces.c 안에 로더와 함께 ELF excutable을 로드할 수 있다. ELF는 Linux나 solaris 등 에서 사용되는 파일 포맷이다.

Virtual Memory Layout


  • 가상 메모리는 두 영역으로 나눠진다 : 유저 가상 메모리와 커널 가상 메모리이다. 유저 가상 메모리의 영역은 가상 주소 0부터 KERN_BASE 까지이며 (incldue/threads/vaddr.h 안에 정의되어있다. 기본값은 0x800400000) 커널 가상 메모리는 나머지를 차지하고 있다.
  • 유저 가상 메모리는 per-process이다. 커널이 프로세스를 바꿀 때, 유저 가상 주소 영역 역시 바꾸는데, processor page directory base register를 바꿈으로서 유저 가상 주소 공간도 바꾼다. 스레드는 프로세스 page table 포인터를 가지고 있다.
  • 커널 가상 메모리는 전역으로 사용된다. 유저 프로세스나 커널 스레드에 구애받지 않고 맵핑 가능하다. 핀토스에서는, 가상메모리는 물리적 메모리에 1:1 대응하고있다. 가상 주소 KERN_BASE는 물리 주소 0에 대응하고(KERN_BASE에서 시작), 가상 주소 KERN_BASE + 0x1234는 물리주소 0x1234에 대응한다.
  • 유저프로그램은 오직 유저 가상 메모리에 접근 가능하다. 커널 가상 메모리에 접근하려하면, userprog/exception.c 안의 page_fault()에 의해 page fault가 발생하고, 프로세스는 종료될 것이다. 매핑되지 않은 유저 가상 메모리에 접속하려고 하면 page fault가 일어난다.

Typical Memory Layout


  • 이번 프로젝트에서 유저 스택은 고정된 크기를 가지고 있다.
  • pintos에서 코드 영역은 유저 가상 주소 0x400000에서 시작하며 대략 128MB정도로, 주소 공간 아랫쪽에 위치한다.
  • linker는 말 그대로 메모리의 유저 프로그램의 layout을 만든다. info ld로 접속가능한 linker manul에서 script를 읽으면 linker script에 대해서 알 수 있다.

Accessing User Memory


  • 시스템 콜의 한 부분으로서, 커널은 유저 프로그램이 제공한 포인터로 메모리에 접속해야한다. 이 때, 유저가 null pointer나 가상 메모리에 맵핑되어있지 않은 포인터를 넘겨주거나, 커널 가상 주소로의 포인터를 넘겨줄 수도 있다. 이러한 포인터들은 offending process를 제거하고, process의 자원을 폐기함으로써 포인터를 제거한다.
  • 이 작업을 수행할 수 있는 두 가지 방법이 있다. 첫번째 방법은 유저가 넘긴 포인터를 검사한 뒤에 역참조 하는 것이다. 이 방법을 쓸 것이면 userprog/pagedir의 함수와 include/threads/vaddr.h를 봐라.
  • 두번째 방법은 유저가 넘긴 포인터가 KERN_BASE 이전을 가리키는지만 확인하는 것이다. 잘못된 유저 포인터였다면 page fault를 일으킬 것이고, 이는 page_fault()로 수정할 수 있다. 이 방법은 MMU방식과 같기 때문에 빨리 수행할 수 있기 때문에 Linux같은 실제 커널에서 사용된다.
  • 자원이 새지(leak)않도록 하라. 예를 들어, 시스템 콜이 malloc()으로 메모리를 할당받았거나 어떤 lock을 가졌다고 해보자. 이 다음에 잘못된 유저 포인터를 사용한다면, 반드시 lock이나 메모리를 반환해야한다. 만약 역참조하기 전에 유저 포인터를 검사했다면 잘못될 일이 없을 것이다. 만약 잘못된 유저 포인터가 page fault를 일으킨다면 에러 코드나 반환값을 받을 길이 없기 때문에 더욱 다루는 게 어려워진다.

Argument Passing


사용자 프로그램에 대한 인수 설정 process_exec()

x86-64 calling convention(함수 호출 규약)


  • cf)
    • calling convention에서 정의하는 것 중 하나가 argument passing과 stack frames다.
    • 어떻게 함수 인자를 전달하고 반환값을 받아오냐를 관리하는데, x86-64 리눅스에서는 6개의 함수 인자가 레지스터 %rdi, %rsi, %rdx, %rcx, %r8, %r9로 전달된다. 만약 7개 혹은 그 이상의 인자가 들어오면 스택으로 들어간다.
    • 그리고 반환값(return value)는 레지스터 %rax로 전달됨.
  • x86-64 calling convention에서는 user-level application에서 인자 전달을 위해 위와 같은 정수 레지스터를 사용한다.( %rdi, %rsi, %rdx, %rcx, %r8, %r9)
  • caller(호출)는 다음 instruction(return address)를 stack에 넣어주고 callee(호출받는)의 첫 인스트럭션으로 점프한다. → 그리고 callee 실행
  • callee가 반환값을 갖는 경우, %rax 레지스터에 넣음
  • callee는 return address를 stack에서 pop하면서 그 위치로 점프(x86-64 인스트럭션에서는 RET가 이를 해줌)
  • ex) f()가 3개의 정수형 인자를 받는 경우 callee가 보는 stack frame의 예시 → f(1, 2, 3)
+----------------+
stack pointer --> 0x4747fe70 | return address |
                             +----------------+
RDI: 0x0000000000000001 | RSI: 0x0000000000000002 | RDX: 0x0000000000000003

Program Startup Details


  • 사용자 프로그램 진입점은 /lib/user/entry.c_start()에서 부터이다. 이 함수는 main()을 실행하고 main이 return 하면 exit()을 호출한다.
  • 커널은 유저 프로그램 실행을 허용하기 전에 초기 함수에 대한 인자를 레지스터에 넣어줘야한다. 이때 일반적인 calling convention과 동일한 방식으로 인자가 전달된다.
  • ex) /bin/ls -l foo bar 가 전달될 때 예시
    • 커맨드를 단어로 분리함 → /bin/ls , -l , foo , bar

    • 단어들을 스택 최상단에 넣어준다. 이때 포인터로 참조되기 때문에 순서는 상관없음.

    • 각 문자열의 주소와 null pointer sentinel을 스택에서 오른쪽 → 왼쪽 순서로 넣는다.

      1. 분리한 커맨드 단어들이 argv 요소임
      2. null pointer를 넣음으로써 argv[argc]가 null pointer가 된다. → C의 표준 요구사항
      3. word로 정렬되어 있는 경우가 더 빠르기 때문에 스택 포인터를 처음 넣기 전에 stack 포인터를 8의 배수가 되도록 조정해줌.
    • %rsi 가 argv의 주소, %rdi 가 argc를 가리키도록 한다.

    • 마지막으로 가짜 return address를 넣어줌. 엔트리 함수는 반환되지 않으나 다른 스택 프레임과 동일한 구조를 만들어주기 위해 넣는다.

    • 유저 프로그램이 실행되기 직전의 stack 상태와 관련된 레지스터

Implement the argument passing


  • 유저 프로그램을 실행하기 전에, 커널은 레지스터에다가 맨 처음 functiong의 argument를 저장해야함.
  • process_exec() 은 유저가 입력한 명령어를 수행할 수 있도록 프로그램을 메모리에 적재하고 실행하는 함수다. → 현재 실행 중인 스레드의 context를 f_name에 해당하는 명령을 실행하기 위해 context switching하는 것이 process_exec()의 역할
  • 해당 프로그램은 f_name 에 문자열로 저장되어 있으나 현재 process_exec() 은 새 프로세스에 인수 전달을 지원하지 않음.
  • 이 기능을 구현하는 것이 이번 과제다!
  • process_exec() 에 코드를 추가해서 간단히 프로그램 파일 이름을 인자로 넣는것 대신에, space가 올 때마다 단어를 parsing하도록 만들어야함.
  • 이 때, 첫 번째 단어는 프로그램 이름이고 두, 세번째 단어는 각각 첫 번째, 두 번째 인자다.
  • ex) process_exec(”grep foo bar”) → process_exec() 에서 두 인자 foo, bar로 parsing되어야 함

구현


  • pintos는 프로그램과 인자를 구분하지 못하는 구조

    • ex) ls -a → ‘ls-a’를 하나의 프로그램명으로 인식
    • 프로그램 이름과 인자를 구분하여 스택에 저장, 인자를 프로그램에 전달
  • 스택 프레임(stack frame) : 함수가 호출되면 스택에는 함수의 매개변수, 호출이 끝난 뒤 돌아갈 반환 주소값, 함수에서 선언된 지역변수 등이 저장됨. 이렇게 스택영역에 차례대로 저장되는 함수의 호출정보를 스택 프레임이라고 함. 이러한 스택 프레임 덕에 함수의 호출이 모두 끝난 뒤, 해당 함수가 호출되기 이전 상태로 되돌아갈 수 있음

  • 프레임 포인터(FP) 레지스터 : 함수가 호출 되기 전의 스택메모리 주소를 저장하고 있음

  • 스택 포인터(SP) 레지스터 : 함수가 호출되고 현재 가리키고 있는 스택메모리 주소를 저장하고 있음(스택 포인터는 CPU 안에) → 스택포인터가 가리키는 곳 까지가 데이터가 채워진 영역(스택 시작점 부터 SP까지), 그 이후부터 스택 끝까지는 비어있는 영역

  • 스택은 새로운 데이터가 추가될수록 주소값이 점점 작아짐 → 스택은 커널 반대 방향으로 자라기 때문에 커널에 침범하는 일이 없게됨.

  • 인터럽트 프레임 : 인터럽트가 들어왔을 때, 이전에 레지스터에 작업하던 context를 switching하기 위해 이 정보를 담아놓은 구조체 → gp_registers R 이 스레드가 작업하고 있을때의 레지스터 값. 이걸 읽어서 do_iret을 함.

  • 구현 소스코드 - https://github.com/SWJungle4A/pintos12-team04/tree/yeopto/argument-passing

User Memory Access


Implement user memory access


  • 시스템 콜을 구현하려면 사용자 가상메모리 공간에 접근할 수 있는 방법을 제공해야함.
  • 인자를 가져올 때는 이 기능이 필요하지 않음.
  • 시스템 콜 인자로서 제공된 포인터에서 데이터에 접근하기 위해서는 이 기능이 필요.
  • 사용자가 잘못된 커널 메모리에 대한 포인터 또는 부분적으로 해당 영역 중 하나의 블록을 제공하면 어떻게 될까? → 이런 경우를 핸들링해야함 유저 프로세스를 종료 시키면서
  • 코드
    // 주소값이 유저 영역(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);
    	}
    }

System Calls


Implement system call infrastructure


  • 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 ();
	}
}

System Call Details


  • x86 아키텍쳐에서 시스템 콜은 다른 소프트웨어 예외처리랑 같게 처리됨
  • 그러나 x86-64 제조업체는 시스템 호출에 대한 특별 명령인 syscall 을 도입했는데 이 것은 시스템 콜을 빠르게 제공함.
  • 오늘날 syscall 명령은 x86-64에서 시스템 호출을 호출하는 데 가장 일반적으로 사용되는 수단임.
  • pintos에서 사용자 프로그램 syscall 은 시스템 호출을 수행하기 위해 호출한다.
  • 두가지 사항 제외하고 일반적인 방식으로 레지스터에 저장해야함
    • %rax 가 system call number다.
    • 4번째 인자는 %rcx 가 아닌 %r10
  • 시스템 호출 핸들러가 제어권을 얻을 때 시스템 호출 번호는 rax에 있고 인자들은 %rdi, %rsi, %rdx, %r10, %r8, %r9 순으로 전달됨..
  • The caller's registers are accessible to struct intr_frame
    passed to it. (**struct intr_frame is on the kernel stack.**)

구현관련


  • pintos 시스템 콜 호출 과정
  • 유저 프로그램을 실행한다 → 유저 프로그램은 write()을 호출한다 → write() 함수는 시스템 콜로, 인자를 유저 스택에 넣고서 커널로 진입한다. 이때 스택 포인터는 인자가 들어간 영역을 가리킴 → 인터럽트 벡터 테이블에 가면 주소(이 주소는 write() 함수 안에 있어) 별로 어떤 종류의 인터럽트를 실행해야 하는지가 맵핑되어 있다.→ 주소가 0x30인 syscall_handler()를 호출함 (유저 스택의 number는 system call number 다)
  • syscall 명령은 어셈블리어다. → 32비트 x86에서는 프로그래머가 직접 함수를 구현해서 스택에 쌓인 인자를 커널로 옮겨주는 작업을 수행해야했지만, x86-64부터는 syscall이라는 어셈블리어 명령이 추가 되어서 알아서 밑단에서 스택 인자를 커널로 옮겨줌
  • x86-64 규약은 함수가 return하는 값을 rax 레지스터에 배치하는 것. 값을 반환하는 시스템 콜은 intr_frame 구조체의 rax 멤버 수정으로 가능
  • 시스템 콜 핸들러에서 유저 스택 포인터(esp) 주소와 인자가 가리키는 주소가 유저 영역인지 확인 → 핀토스는 유저영역을 벗어난 주소를 참조할 경우 페이지 폴트 발생
  • 지금 우리가 하는 일은 시스템 콜 핸들러를 구현하는 것. → 이게 왜 필요할까?유저 프로그램에서 운영체제 혹은 하드웨어에 직접 접근하지 않고 커널에다가 요청만 하면 운영체제가 시스템 콜 핸들러를 이용해 내부적으로 작업하고 결과값만 넘겨주기 위해!
  • 사용자 프로세스에서 시스템 콜을 호출하면 인터럽트 핸들러로 들어간다. 이때 커널의 스택 포인터를 찾아서 거기에 이제까지 사용자 프로세스에서 진행했던 작업을 쌓아야 하는데, 이때 커널 스택을 찾는데 드는 오버헤드를 줄이기 위한 용도로 tss를 사용한다. 즉, tss는 해당 유저 프로세스에 대응하는 커널 프로세스에 대해 해당 커널 스택 포인터 끝을 가리키고 있다는 뜻. → tss_init()을 보면 tss에 초기화해주고 현재 스레드에 대해 tss의 rsp0 레지스터에다가 스레드의 주소 + PGSIZE를 더함 → 즉, 커널 스레드 주소의 시작부분에 페이지 크기만큼 더하면 커널 페이지의 끝 부분을 가리킨다는 걸 알 수 있음. → 즉 tss 구조체 내에 스택포인터를 저장한다는 의미! → 그럼 일일이 특정 사용자 프로세스를 잡을 때 이에 대응하는 커널을 찾을 필요없이 tss에 들어가면 바로 커널 스택 끝 주소를 알아낼 수 있음!

halt()


void halt(void) { // pintos 종료 시스템 콜
	power_off();
}

exit()


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); // 자식 잔다.
    }

wait()


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()

    • 현재 pintos는 프로세스 구조체에 부모와 자식 관계를 명시하는 코드가 없다. 즉 부모와 자식 구분이 없고, 자식 프로세스의 정보를 알지 못 하기에 자식의 시작/종료 전에 부모 프로세스가 종료되는 현상이 발생해서 프로그램이 실행되지 않는 경우가 있음 → 이를 위해 fork() 함수를 사용함.
    • 자식 프로세스를 왜 복제?
      • 프로세스를 생성하고 이와 관련된 많은 자료 구조들이 있는데 이것들을 다 새로 만드는 것보다는 기존에 생성된 프로세스의 자료구조를 복사하는게 더 효율적
      • 새로 생성된 프로세스의 엔트리 포인트를 어디로 할 것인가에 대한 것도 있음. 일반적으로 프로그램이 실행될 때 main()함수를 엔트리 포인트로 하는데, 새로 프로세스가 생성될 때마다 main() 함수부터 시작하는 것은 매우 비효율적이고 프로세스간 통신에서 문제가 발생할 수 있다. → 복제를 하면 부모 프로세스가 호출한 시점을 엔트리 포인트로 가지기 때문에 더욱 직관적이고 발생할 수 있는 문제를 줄일 수 있음
    • 부모 프로세스에서 자식 프로세스가 생성될 때, 부모 프로세스는 자식 프로세스가 끝날 때까지 wait()하고, 자식 프로세스는 생성되면서 exec()함수를 호출하게 됨.
  • fork() 함수 흐름 정리

    • cf) 첫번째 사진에서 syscall1() 호출 후 parent_thread적어 놓은 것은 돌아가고 있다는 걸 표현한거니 rax, rdi가 parent_thread의 정보가 아니라는 것!

exec()


현재 프로세스를 커맨드라인에서 지정된 인수를 전달하여 이름이 지정된 실행 파일로 변경

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;
}

create()


파일 이름과 파일 사이즈를 인자 값으로 받아 파일을 생성하는 함수. → 새 파일을 생성해도 파일이 열리지 않음. open()으로 별도작업 해줘야함. filesys_create 함수는 파일 이름과 파일 사이즈를 인자값으로 받아 파일을 생성하는 함수.

bool create(const char *file, unsigned initial_size) { // file: 생성할 파일의 이름 및 경로 정보, initial_size: 생성할 파일 크기
	check_address(file);
	return filesys_create(file, initial_size);
}

remove()


파일을 삭제하는 시스템 콜로, file 인자는 제거할 파일의 이름 및 경로 정보이다. 성공일 시 true, 실패 시 false 리턴

bool remove(const char *file) {
	check_address(file);
	return filesys_remove(file);
}

open()


파일을 열 때 사용하는 시스템 콜. 성공 시 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;
    }

filesize()


파일 크기를 알려주는 시스템 콜, 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()


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()


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()


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;
}

tell()


파일의 위치(offset)을 알려주는 함수

unsigned tell(int fd) {
	struct file *tell_file = find_file_by_fd(fd);
	if (tell_file <= 2) {
		return;
	}
	return file_tell(tell_file);
}

close()


열린 파일을 닫는 시스템 콜. 파일을 닫고 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;
    }

Process Termination Message


프로세스 종료 메세지 출력

  • 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 종료
    }

Deny Write on Executables


실행 파일에 쓰기를 거부함.

  • 실행 파일로 사용 중인 파일에 대한 쓰기를 거부하는 코드를 추가해라.
  • 많은 OS는 프로세스가 디스크에서 변경 중인 코드를 실행하려고 할 때 예측할 수 없는 결과로 인해 이러한 작업을 수행함.
  • 이것은 프로젝트 3에서 가상 메모리가 구현되면 특히 중요하지만, 지금은 힘들게 하지 않을거임.
  • 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); // 열린 파일에 쓰기 방지함.
    	.
    	.
    	.

Keyword


User mode vs Kernel mode


  • I/O 장치를 보호하기 위해서 CPU에 두가지 모드가 있다.
  • 예를 들어 컴퓨터 하드디스크에 있는 내용을 싹 지워버리는 악의적인 프로그램을 만들었다 해보자. → 이런 상황을 방지하기 위해 어플리케이션 프로그램들은 직접 I/O 장치에 접근할 수 없게 만들고 운영체제 통해서만 I/O 장치를 사용할 수 있게 하는 것 → 즉, I/O protection을 수행하는 것

  • User Mode → Kernel Mode 요청

    • 프로세스가 유저모드에서 실행되다가 특별한 요청이 필요할 때 system call을 이용해서 커널에 요청함.
  • Kernel Mode → User Mode 반환

    • system call의 요청을 받은 커널이 그 요청에 대한 일을 하고 결과값을 system call의 리턴 값으로 전해줌.
  • open() 호출 → 커널 모드 진입 → open에 대한 입력값을 커널로 전달 → 해당 일 완료하고 커널에서 return 하면서 유저모드로 돌아감.

  • 구조

  • 프로세스 메모리 구조

  • 커널 영역 구조(physical memory 에서)

User stack


system call


  • process 관련 syscall (fork(), wait(), exec(), exit() 등)

inode table


  • 리눅스에서는 하나의 파일이 파일데이터와 그 데이터에 관련된 inode(속성정보)로 구성됨
  • inode 테이블 → 현재 시스템 상에 존재하는 프로세스들이 열어서 사용하고 있는 파일들의 inode정보를 갖는 테이블
    • 프로세스가 파일을 연다 → 커널은 inode 테이블에 빈 엔트리를 할당 → 디스크로부터 파일 속성을 가져와 저장
    • 하나의 파일에 대해서 하나의 inode 테이블 엔트리만 존재할 수 있음 → 하나의 파일을 두번 이상 동시에 열때는 이미 할당된 inode 엔트리를 공유.
    • inode 정보
      • 장치 번호 - 이 파일이 있는 디스크의 장치번호
      • inode번호 - inode를 식별하기 위한 번호로 한 디스크내의 모든 inode들의 번호는 고유하다.
      • 모드 - 파일종류(파일,디렉토리등)과 접근권한(rwx)
      • uid, gid - 소유자와 그룹의 id
      • 실제 장치 번호 - 파일 종류가 장치파일인 경우 실제 장치 번호
      • 파일 크기 - 파일에 저장된 데이터의 바이트
      • 파일 접근 시간- 최종접근, 데이터변경, 속성변경시간
      • i_count(참조 카운터) - 이 inode를 가리키는 파일 테이블 엔트리의 수.

file table


  • 현재 열려있는 파일의 읽기/쓰기 동작을 위한 자료구조.
  • open(), creat() 시스템 콜에 의해 파일이 열릴 때마다 하나씩 할당되고 close() 시스템 콜에 의해 닫을 때 해제됨. → 따라서 하나의 파일에 대해 동시의 여러개의 파일테이블 엔트리가 존재할 수 있음.
  • 파일 테이블 엔트리 정보
    • 열기모드 - 읽기전용(O_RDONLY), 읽기/쓰기(O_RDWR), 쓰기전용(O_WRONLY)등의 모드
    • 플래그 - 부가적 특성(O_ASYNC, O_NONBLOCK 등)
    • 읽기/쓰기 위치(f_pos) - 현재 읽기/쓰기 위치
    • f_count(참조 카운터) - 이 엔트리를 가리키는 파일 디스크립터 테이블 엔트리의 수

file descriptor table


  • 이 테이블은 프로세스마다 하나씩 가지는 것으로 프로세스가 사용중인 파일을 관리하기 위한 테이블 → 파일테이블에 대한 포인터를 저장하는 배열임
  • 시스템 호출을 할때마다 하나의 빈 엔트리가 할당되고 해당하는 파일테이블에 대한 포인터가 들어감.
  • open(), creat(), dup()에 의해 반환되는 파일 디스크립터는 이 배열의 인덱스.
  • close()에 의해 엔트리가 제거됨.
  • 예를 들어 "file1"이라는 파일을 열기위한 fd1=open("file1", O_RDONLY) 시스템 콜은 커널 내에서 다음과 같은 일을 한다.
    1. 디스크에서 파일(file1)을 찾아 그 inode 정보를 가져와 inode 테이블의 빈 엔트리를 채우고 참조 카운터(i_count)를 1로 한다. 그 파일의 inode 정보가 이미 테이블에 있으면 새로운 엔트리를 할당하지 않고 참조 카운터만 증가시킨다.
    2. inode에 있는 접근 권한이 열기 모드를 허용하는지 조사한다.
    3. 파일테이블 엔트리를 할당하고 읽기/쓰기위치(f_pos)를 0으로 한다. (만약 열기모드에 O_APPEND가 있으면 f_pos를 파일 크기와 같게 한다.) 그리고 참조 카운터(f_count)를 1로 한다.
    4. fd 테이블을 처음부터 탐색해서 사용되지 않는 영역에 파일 테이블 엔트리의 포인터를 기록하고, 그 인덱스를 반환한다.
  • 파일 테이블 엔트리는 파일을 열 때마다 새로 할당되고 파일 디스크립터 테이블의 엔트리는 파일 디스크립터가 새로 생성될 때마다 새로 할당된다. 한편 파일 닫기 동작 close()는 open()의 반대 과정을 수행한다. 먼저 디스크립터 테이블에서 해당 포인터를 지우고 파일테이블의 f_count를 감소시킨다. f_count가 0이 되면 이 엔트리를 해제하고 다시 inode 테이블의 i_count를 감소시킨다. inode테이블의 i_count가 0이되면 이 엔트리를 해제한다

project1때는 개념부터 잡고 한다고 너무 느렸어서 이번엔 코드보면서 구현부터 해보자는 생각에 여러 코드들을 보며 만들었는데 안돌아갔다.. 방대한 양을 따라쳤는데 안 돌아가서 왜 안돌아가지 하면서 시간을 오래 투자했는데, 그 투자한 시간이 공부한다는 느낌보단 틀린그림찾기 하는데 시간을 투자하는 느낌이었다. 굉장히 허탈했다. 난 지금 뭐하고 있는거지 싶었다. 꾸역꾸역 돌아가게 만들어놓고 방대한 과제의 흐름을 잡기 시작했다. 물론 따라치면서 아무 이해없이 따라친건 아니었기에 흐름을 잡고 코드를 다시 보니 이해가 더 잘 됐다. 난 아무래도 개념부터 하고 코드에 대해 공부하는게 시간이 더 걸리더라도 나에게 효율적인것 같았다. 남는 것도 있는거 같고. 진짜 악명 높은 pintos.. 어렵다.. 그래도 이 과제를 하면서 운영체제에 대해 2프로정도는 알게된거같다. project4가 끝나면 10퍼정도 알았으면 좋겠다. 이제 조가 바뀐다. 셋 다 공부 방식이 조금씩 다르지만 서로의 방식을 존중하면서 서로 부족한거 채워주는게 굉장히 좋았다. 하지만 다른 몇몇 조처럼 좀 더 똘똘 뭉쳐야했나란 아쉬움도 있었다.(끝날때가 되니까 드는 생각) pintos과제도 어느정도 적응이 되었으니 다음 과제부턴 협업부분을 조금 더 고민해봐야겠다.

profile
https://yeopto.github.io로 이동했습니다.

1개의 댓글

잘봤습니다 👍

답글 달기