User mode vs. Kernel mode
Process
- Process Environment block (PEB)
- Process identifier (PID)
User Stack
x86_64 calling convention
Register vs. Memory
argument vector → argv
Executable Linkable Format (ELF) & loader
system call (syscall)
**!!CAUTION!! some system call’s semantic may differ from POSIX standards**
- filesys related
- open, close, create, read, write, seek, tell, ...
- process related
- halt, exit, exec, fork, wait ...
file descriptor
- file descriptor table
- dup2 syscall
만약 추가 과제의 테스트케이스를 도전하고 싶으면 userprog/Make.vars를 수정하라.
TODO가 없는 코드는 수정할 필요가 없다.
We allow more than one process to run at a time. Each process has one thread (multithreaded processes are not supported).
User programs are written under the illusion that they have the entire machine.
From now on, we will test your operating system by running user programs.
You must make sure that the user program interface meets the specifications described here, but given that constraint you are free to restructure or rewrite kernel code however you wish.
All of your codes should never located in block that enclosed by #ifdef VM
We strongly recommend you to read synchronization and virtual addrees before you start.
- 시작하기 전에 synch와 가상 메모리를 읽어라.
You will need to interface to the file system code for this project, because user programs are loaded from the file system and many of the system calls you must implement deal with the file system.
Proper use of the file system routines now will make life much easier for project 4
- 파일 시스템 루틴을 적절히 이용하면 project 4가 수월해진다.
- 다음 제한 사항을 가진다.
ㆍ내부 sync가 없음 : 동시 접속은 서로에게 영향을 줄 것이다. 한번에 한 파일 시스템 코드만 수행할 수 있도록 sync를 사용하라.
ㆍ파일 크기는 만들 때 크기로 고정된다. 루트 디렉토리가 파일 형태이기 때문에 만들어질 파일들 또한 제한된다. (무엇이?)
ㆍ파일들은 연속적인 디스크 섹터를 가진다. (분산되서 저장되지 않음) 그렇기 때문에 외부 단편화가 문제가 될 수 있다.
ㆍ서브디렉토리 없음
ㆍ파일명은 14음절로 제한된다.
ㆍ작업 중의 시스템 충돌은 디스크를 오염시킬 수 있으며, 자동으로 복구되어지지 않는다.
One important feature is included:
중요한 사항이 있음
Unix-like semantics for filesys_remove() are implemented. That is, if a file is open when it is removed, its blocks are not deallocated and it may still be accessed by any threads that have it open, until the last one closes it. See Removing an Open File for more information.
메모리가 여유로우며 작성한 시스템 콜만 사용하는 한, PintOS는 일반 C 프로그램을 수행할 것이다. 이번 프로젝트에서는 메모리 할당이 필요하지 않기 때문에 malloc()은 수행되지 않는다. 커널이 스레드를 바꿀 때, 프로세서의 floating-point를 저장하거나 복구하지 않기 때문에, pintos는 floating point operation이 있는 프로그램을 수행하지 않는다.
pintos는 userprog/proces.c 안에 로더와 함께 ELF excutable을 로드할 수 있다. ELF는 Linux나 solaris 등 에서 사용되는 파일 포맷이다.
가상 메모리는 두 영역으로 나눠진다 : 유저 가상 메모리와 커널 가상 메모리이다. 유저 가상 메모리의 영역은 가상 주소 0부터 KERN_BASE 까지이며 (incldue/threads/vaddr.h 안에 정의되어있다. 기본값은 0x800400000) 커널 가상 메모리는 나머지를 차지하고 있다.
유저 가상 메모리는 per-process이다. 커널이 프로세스를 바꿀 때, processor page directory base register를 바꿈으로서 유저 가상 주소 공간도 바꾼다. 스레드는 프로세스 page table 포인터를 가지고 있다.
커널 가상 메모리는 전역으로 사용된다. 유저 프로세스나 커널 스레드에 구애받지 않고 맵핑 가능하다. 핀토스에서는, 가상메모리는 물리적 메모리에 1:1 대응하고있다. 가상 주소 KERN_BASE는 물리 주소 0에 대응하고, 가상 주소 KERN_BASE + 0x1234는 물리주소 0x1234에 대응한다.
유저프로그램은 유저 가상 메모리에 접속가능한다. 커널 가상 메모리에 접속하려하면, userprog/exception.c 안의 page_fault()에 의해 page fault가 발생하고, 프로세스는 제거될 것이다. 배정받지 않은 유저 가상 메모리에 접속하려고 하면 page fault가 일어난다.
이번 프로젝트에서 유저 스택은 고정된 크기를 가지고 있다.
pintos에서 코드 영역은 유저 가상 주소 0x400000에서 시작하며 대략 128MB정도로, 주소 공간 아랫쪽에 위치한다.
linker는 말 그대로 메모리의 유저 프로그램의 layout을 만든다. info ld로 접속가능한 linker manul에서 script를 읽으면 linker script에 대해서 알 수 있다.
시스템 콜의 한 부분으로서, 커널은 유저 프로그램이 제공한 포인터로 메모리에 접속해야한다. 이 때, 유저가 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를 일으킨다면 에러 코드나 반환값을 받을 길이 없기 때문에 더욱 다루는 게 어려워진다. 그러므로 두번째 방법을 쓰고 싶은 사람은 아래 코드를 사용해봐라.
/* Reads a byte at user virtual address UADDR.
* UADDR must be below KERN_BASE.
* Returns the byte value if successful, -1 if a segfault
* occurred. */
static int
get_user (const uint8_t *uaddr) {
int result;
asm ("movl $1f, %0; movzbl %1, %0; 1:"
: "=&a" (result) : "m" (*uaddr));
return result;
}
/* Writes BYTE to user address UDST.
* UDST must be below KERN_BASE.
* Returns true if successful, false if a segfault occurred. */
static bool
put_user (uint8_t *udst, uint8_t byte) {
int error_code;
asm ("movl $1f, %0; movb %b2, %1; 1:"
: "=&a" (error_code), "=m" (*udst) : "q" (byte));
return error_code != -1;
}
이 함수들은 KERN_BASE아래에 유저 주소가 있다고 가정한다. 또한, 커널에서 page_fault가 일어났을 때, rax to -1 에서 일어나도록 하고, 이전 값을 %rip에 복사하도록 page_fault()를 수정했다고 가정한다.
userprog/process.c
에서 제공되는 로더.bss(Block Started by Symbol): 컴파일러와 링커에서 쓰이는 용어
컴퓨터 프로그래밍에서 .bss 또는 bss는 / 수많은 컴파일러와 링커가 처음에 0 값의 비트로 표현되는 정적으로 할당된 변수를 포함하는 데이터 세그먼트의 한 부분으로 사용한다. "bss 섹션"(bss section), "bss 세그먼트"(bss segment)라고도 부른다. 즉, 초기화되지 않은 전역 데이터를 위한 영역이다.
cc : [https://dreamlog.tistory.com/91]
일반적으로 데이터가 없는 bss 섹션의 길이만이 오브젝트 파일에 저장된다. 프로그램 로더는 프로그램을 로드할 때 bss 섹션을 위한 메모리를 할당하고 초기화한다. 운영 체제는 zero-fill-on-demand라는 기술을 사용하여 bss 세그먼트를 효율적으로 구현한다. (McKusick & Karels 1986) 임베디드 소프트웨어에서 bss 세그먼트는 main()
에 들어가기 전에 C 런타임 시스템에 의해 0으로 초기화되는 메모리로 매핑된다.
일부 컴퓨터 아키텍처에서 ABI 또한 조그마한 데이터에 대한 sbss 세그먼트를 지원한다. 일반적으로 이러한 데이터 항목들은 특정한 범위의 주소에만 접근할 수 있는 더 짧은 명령을 이용하여 접근할 수 있다.
개념적으로 각 프로세스는 자신의 사용자 가상 메모리를 자유롭게 배치할 수 있습니다. 실제로 사용자 가상 메모리는 다음과 같이 배치됩니다.
USER_STACK +----------------------------------+
| user stack |
| | |
| | |
| V |
| grows downward |
| |
| |
| |
| |
| grows upward |
| ^ |
| | |
| | |
+----------------------------------+
| uninitialized data segment (BSS) |
+----------------------------------+
| initialized data segment |
+----------------------------------+
| code segment |
0x400000 +----------------------------------+
| |
| |
| |
| |
| |
0 +----------------------------------+
code영역은 프로그램의 코드, data영역은 global 변수나 static 변수, heap 영역(gross upward)은 동적으로 할당받은 메모리 영역, stack 영역은 함수 호출 시 생성되는 지역변수나 매개변수, return 값들이 들어간다.
이 프로젝트에서는 사용자 스택의 크기가 고정되어 있지만 프로젝트 3에서는 확장이 허용됩니다. 전통적으로 초기화되지 않은 데이터 세그먼트의 크기는 시스템 호출로 조정할 수 있지만 이를 구현할 필요는 없습니다.
Pintos의 코드 세그먼트는 사용자 가상 주소 0x400000에서 시작하며 주소 공간의 맨 아래에서 약 128MB입니다. 이 값은 우분투에서 일반적인 값으로 큰 의미는 없습니다.
링커는 다양한 프로그램 세그먼트의 이름과 위치를 알려주는 "링커 스크립트"의 지시에 따라 메모리에서 사용자 프로그램의 레이아웃을 설정합니다. 를 통해 액세스할 수 있는 링커 설명서의 "스크립트" 장을 읽으면 링커 스크립트에 대해 자세히 알아볼 수 있습니다 info ld
.
특정 실행 파일의 레이아웃을 보려면 -p
옵션 과 함께 objdump를 실행하십시오 .
rdi; // 목적지(destinaion) 인덱스 레지스터. arg의 갯수를 저장
rsi; // arg[0](file name)의 char이 저장되기 시작한 주소를 저장
<interrupt.h>
cc : https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=pjt3591oo&logNo=220395406400
그래서 이번 project 2에서는 system calls를 통해 programs이 OS와 interact할 수 있도록 하는 것이다.인터랙티브란 사람들로부터 입력을 받는 것을 의미.
사용자와의 상호작용은 대개 텍스트 기반 또는 그래픽 사용자 인터페이스 둘 다에 사용된다.
1 → 2 → 3 → 4 → 5 → 4 → 3 → 6 → 3 → 2 → 1
init.c
- main()
int main(void) {
...
run_action(argv);
shutdown_power_off();
...
}
run_action()
[2→3→4→5]static void run_action (char **argv) {
static const struct action
actions[] =
{
{"run", 2, run_task},
...
};
}
run_task()
process_wait(process_excute(argv));
static void run_task(char **argv) {
...
process_wait(process_excute(argv);
...
}
process_excute()
- 프로그램 이름 파싱process_excute("echo");
→ echo 실행 load(char* str)
을 호출tid_t process_excute (const char *file_name) {
...
tid = thread_create (,start_process,);
...
return tid;
}
thread_create()
[5→4→3→6]tid_t thread_create (const char *name,
int priority, thread_func *function, void *aux)
{
struct thread *t;
struct kernel_thread_frame *kf; -> 주석처리함
...
t = palloc_get_page(PAL_ZERO);
...
kf->function = function;
...
/* Add to run queue. */
thread_unblock (t);
return tid;
}
process_wait()
[6→3→2→1] Scheduled?
- Yes
: start_process()
, No
: 종료 int process_wait (tid_t child_tid UNUSED) {
return -1;}
start_process()
: 프로그램을 메모리에 적재 load()
후 프로그램 시작인터럽트 프레임 초기화
static void start_process (void *file_name_) {
char *file_name = file_name;
struct intr_frame if_;
bool success;
...
success = load (file_name, &if_.eip, &if_.esp);
if (!success)
thread_exit();
/* Start the user process */
asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g"
(&if_) : "memory");
}
success
: 메모리 적재 성공(load() → Success) - 유저 프로그램 시작
load()
: 메모리를 할당받고 사용자 프로그램을 메모리에 적재 return success
bool load (const char *file_name, void (**eip) (void), void **esp)
{
...
struct file *file = NULL;
,,,
/* Set up stack. */
if (!setup_stack (esp))
...
success = true;
return sucess;
}
thread_exit()
: 스레드 종료void thread_exit (void) {
...
process_exit();
intr_disable();
list_remove(&thread_current()->allelem);
thread_current()->status = THREAD_DYING;
schedule();
}
command line에 한 줄 입력이 들어오면 띄어쓰기(” “)를 기준으로 parsing을 해서 file_name과 arguments를 받도록 하는 작업을 해준다.
현재는 1. file_name을 parsing하는 작업과 2. parsing한 arugments를 어딘가에 저장해놓는 코드가 구현이 안되어있다.
https://straw961030.tistory.com/262?category=957768
https://github.com/Yerimi11/pintos-kaist-team09/tree/yerim_Project_2