Pintos에서 **단위(Unit)**
Test를 진행하기 위해서 아래와 같은 커맨드를 한번은 터미널창에 입력해봤을 것이다.
pintos --fs-disk=10 -p tests/userprog/args-single:args-single -- -q -f
run 'args-single onearg'
우리가 이 커맨드를 실행하면 과연 Pintos는 어떻게 이를 알아 듣고 실행을 하게되는 것일까?
우리 10팀은 이러한 부분에 근본적인 의문을 가지게 되었다.
그렇기에 크게 목차는 아래와 같이 나누어지게 된다.
목차
- 커맨드 입력
- Pintos 가상화
- PIntos의 시작점
- 프로세스 초기 생성
- 프로세스의 실행
- 사용자 모드 변경
- System Call 호출 시점
- System Call
- System Call Handler
1. 커맨드 입력
2. Pintos 가상화
**pintos.py**
에서 Command Line을 Parsing하여 우리가 입력하는 명령어에 대해 수행 및 PintOS가 OS로서의 역할을 수행할 수 있도록 가상화를 지원한다.
- 또한
**pintos.py**
의 219번 줄에서 Command Line의 Parsing이 실제로 이루어져 이를 기반으로 running(OS 환경 실행)시킨다.
if '--' in sys.argv:
pintos_arg_index = sys.argv.index('--')
util_args = sys.argv[1: pintos_arg_index]
kern_args = sys.argv[pintos_arg_index + 1:]
--
기준으로 유틸리티와 커널 실행에 필요한 인자를 구분해준다.- pintos —fs-disk=10 -p tests/userprog/args-single:args-single ( 유틸리티 )
- -q -f run ‘args-single’ ( Kernel command line → 추후 파싱 예정 )
- pintos --fs-disk=10 -p tests/userprog/args-single:args-single -- -q -f run 'args-single’
args = parser.parse_args(util_args)
Pintos(ttest=args.threads_tests, mem=args.memory, no_vga=args.no_vga,
args=kern_args, timeout=args.timeout, fs=args.fs_disk, gdb=args.gdb,
swap=args.swap_disk,
mnts=[f[0] for f in args.MNTS],
hostfns=[f[0].split(':') for f in args.HOSTFNS],
guestfns=[f[0].split(':') for f in args.GUESTFNS]).run()
2-1. init.c - int main() 실행
kernel command line: -q -f put args-single run 'args-single onearg'
- 에뮬레이터가 실행되고 곧바로
**init.c**
의**main()**
함수로 이동한다.
/* Pintos main program. */
int
main (void) {
...
/* Clear BSS and get machine's RAM size. */
bss_init ();
/* Break command line into arguments and parse options. */
argv = read_command_line ();
argv = parse_options (argv);
/* 스레드, 콘솔, 말록, 페이징, tss, gdt 등등 초기화 */
thread_init ();
console_init ();
mem_end = palloc_init ();
malloc_init ();
paging_init (mem_end);
#ifdef USERPROG
tss_init ();
gdt_init ();
#endif
/* Initialize interrupt handlers. */
intr_init ();
timer_init ();
kbd_init ();
input_init ();
#ifdef USERPROG
exception_init ();
syscall_init ();
#endif
/* Start thread scheduler and enable interrupts. */
thread_start ();
serial_init_queue ();
timer_calibrate ();
printf ("Boot complete.\n");
/* Run actions specified on kernel command line. */
run_actions (argv);
/* Finish up. */
if (power_off_when_done)
power_off ();
thread_exit ();
}
- 위에서
**-q**
부터의 option 값들을**read_command_line()**
으로 읽어들인다. - 또한 해당
**read_command_line()**
함수에서 ptov 매크로를 이용하여 물리 주소를 가상 주소로 변환하여준다. - 이후 모든 init 값들을 수행한다.
2-2. thread_init()
void thread_init(void)
{
, , ,
/* Set up a thread structure for the running thread. */
initial_thread = running_thread();
init_thread(initial_thread, "main", PRI_DEFAULT);
initial_thread->status = THREAD_RUNNING;
initial_thread->tid = allocate_tid();
}
- 해당 프로세스에서 가장 중요한 부분은
**init_thread()**
함수에 main이라는 인자값을 통해 스레드를 생성해주는 부분이다. - main thread의 메모리를 세팅해주고 해당 스레드의 메모리는 커널 스택에 가장 아래에 위치한다.
2-3. palloc_init()
Pintos booting with: ~~ / base_mem: ~~ / ext_mem: ~~
- 각종 초기화 세팅을 해주는 작업.
- 스레드부터 시작해서 콘솔, malloc 초기화를 거치면 다음에 페이지 할당 초기화 작업을 수행
- 우리가 쓸 수 있는 메모리(base memory, external memory)가 얼마나 되는지 표시해준다.
2-4. tss_init()
tss? : task-state-segment
- task는 곧 process이자 thread(pintos 기준)
- TSS는 현재 실행 중인 태스크의 상태를 저장하는데 사용되며, 태스크가 변경될 때 이전 태스크의 상태를 저장하고 새 태스크의 상태를 복원하는 데 필요한 정보를 포함
- 즉, 사용자 모드에서 커널모드로 trap이 발생할때 스택도 함께 전환시키기 위해 사용
GitBook 참조
"TSS(Task-State Segment)는 x86 아키텍처 task switching에 사용되었습니다.
그러나 x86-64에서는 task switching이 더 이상 사용되지 않습니다.
그럼에도 불구하고 TSS는 링 스위칭 동안 스택 포인터를 찾기 위해 여전히 존재합니다.
이것은 사용자 프로세스가 인터럽트 핸들러에 들어갈 때 하드웨어가 커널의 스택 포인터를 찾기 위해
tss를 참조한다는 것을 의미합니다."
/* Initializes the kernel TSS. */
void
tss_init (void) {
/* Our TSS is never used in a call gate or task gate, so only a
* few fields of it are ever referenced, and those are the only
* ones we initialize. */
tss = palloc_get_page (PAL_ASSERT | PAL_ZERO);
tss_update (thread_current ());
}
/* Returns the kernel TSS. */
struct task_state *
tss_get (void) {
ASSERT (tss != NULL);
return tss;
}
/* Sets the ring 0 stack pointer in the TSS to point to the end
* of the thread stack. */
void
tss_update (struct thread *next) {
ASSERT (tss != NULL);
tss->rsp0 = (uint64_t) next + PGSIZE;
}
- tss_init을 보면 tss에 초기화(페이지를 할당)해주고 현재 스레드(initial(main) 스레드로 부팅 중인 상황임)에 대해 rsp0 레지스터에다가 스레드의 주소 + PGSIZE를 더한다. 여기서 tss_update에 들어가는 thread는 thread_current()로 부팅 작업을 하고 있는 커널 스레드
- 이후에 timer, 하드 디스크 초기화까지 진행하고 나면 Boot complete이 뜬다.
- 즉 ,커널 스레드 주소의 시작 부분에 페이지 크기만큼 더하면 커널 페이지의 끝 부분
2-5. fsutil_put()
Putting 'args-single' into the file system...
- 하드 디스크 내 파일 시스템에 명령어를 복사해서 넣어주는 역할
3. Pintos의 시작점
**main()
함수에서run_action()
**이 호출되게 된다.- 그리고
**run_action()
에선run_task()
** 함수를 호출하게 된다.
run_actions
static void
run_actions (char **argv) {
/* An action. */
struct action {
char *name; /* Action name. */
int argc; /* # of args, including action name. */
void (*function) (char **argv); /* Function to execute action. */
};
/* Table of supported actions. */
static const struct action actions[] = {
{"run", 2, run_task},
#ifdef FILESYS
{"ls", 1, fsutil_ls},
{"cat", 2, fsutil_cat},
{"rm", 2, fsutil_rm},
{"put", 2, fsutil_put},
{"get", 2, fsutil_get},
, , ,
run_task()
static void
run_task (char **argv) {
const char *task = argv[1];
printf ("Executing '%s':\n", task);
#ifdef USERPROG
if (thread_tests){
run_test (task);
} else {
process_wait (process_create_initd (task));
}
#else
run_test (task);
#endif
printf ("Execution of '%s' complete.\n", task);
run_task() 함수 실행 프로세스
- process_create_initd()를 실행해 우리가 입력해준 'args-single onearg'에 대한 프로세스를 생성
- process_wait()으로 반환값이 인자로 들어가며 대기
- 이때 무한 루프를 수행하며, timer_init()으로 타이머 인터럽트를 초기화해줬기에 정해진 시간 동안 기다리다가 타이머가 작동하면 switch가 일어남
4. Process 초기 생성
- process의 초기 생성은 반드시
**process_create_initd()
로만 이루어진다.**- 이후 프로세스들은
**fork()**
를 통해 생성
- 이후 프로세스들은
process_create_initd
tid_t
process_create_initd (const char *file_name) {
char *fn_copy;
tid_t tid;
/* Make a copy of FILE_NAME.
* Otherwise there's a race between the caller and load(). */
fn_copy = palloc_get_page (0);
if (fn_copy == NULL)
return TID_ERROR;
strlcpy (fn_copy, file_name, PGSIZE);
/* Create a new thread to execute FILE_NAME. */
tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy);
if (tid == TID_ERROR)
palloc_free_page (fn_copy);
return tid;
}
initd()
static void
initd (void *f_name) {
#ifdef VM
supplemental_page_table_init (&thread_current ()->spt);
#endif
process_init ();
if (process_exec (f_name) < 0)
PANIC("Fail to launch initd\n");
NOT_REACHED ();
}
5. Process의 실행
int
process_exec (void *f_name) { // 유저가 입력한 명령어를 수행하도록 프로그램을 메모리에 적재하고 실행하는 함수. 여기에 파일 네임 인자로 받아서 저장(문자열) => 근데 실행 프로그램 파일과 옵션이 분리되지 않은 상황.
...(인자 파싱 작업)...
/* We cannot use the intr_frame in the thread structure.
* This is because when current thread rescheduled,
* it stores the execution information to the member. */
struct intr_frame _if; // intr_frame 내 구조체 멤버에 필요한 정보를 담는다.
_if.ds = _if.es = _if.ss = SEL_UDSEG; // user data selector
_if.cs = SEL_UCSEG; // user code selector
_if.eflags = FLAG_IF | FLAG_MBS;
/* And then load the binary */
success = load (file_name_copy, &_if); // file_name, _if를 현재 프로세스에 load.
(...)
hex_dump(_if.rsp, _if.rsp, USER_STACK - _if.rsp, true);
(...)
/* Start switched process. */
do_iret (&_if);
NOT_REACHED ();
}
process_exec()
함수에서 argument parsing 및 유저 커널 스택에 정보를 올리는 작업(load)을 수행한다.- load()를 통해 사용자 프로세스 작업을 수행하기 위한 인터럽트 프레임 구조체 내 정보를 유저 커널 스택에 쌓는다(
**argument_stack**
) - 이후 if(interrupt frame)을 인자로
**do_iret()**
을 실행
6. 사용자 모드 변경
do_iret()
void
do_iret (struct intr_frame *tf) {
__asm __volatile(
"movq %0, %%rsp\n"
"movq 0(%%rsp),%%r15\n"
"movq 8(%%rsp),%%r14\n"
"movq 16(%%rsp),%%r13\n"
"movq 24(%%rsp),%%r12\n"
"movq 32(%%rsp),%%r11\n"
"movq 40(%%rsp),%%r10\n"
"movq 48(%%rsp),%%r9\n"
"movq 56(%%rsp),%%r8\n"
"movq 64(%%rsp),%%rsi\n"
"movq 72(%%rsp),%%rdi\n"
"movq 80(%%rsp),%%rbp\n"
"movq 88(%%rsp),%%rdx\n"
"movq 96(%%rsp),%%rcx\n"
"movq 104(%%rsp),%%rbx\n"
"movq 112(%%rsp),%%rax\n"
"addq $120,%%rsp\n"
"movw 8(%%rsp),%%ds\n"
"movw (%%rsp),%%es\n"
"addq $32, %%rsp\n"
"iretq"
: : "g" ((uint64_t) tf) : "memory");
}
- addq 명령어로 스택 포인터를 조절하고,
iretq
실행 시 스택에 있던 반환 주소로 점프하여 실행을 계속반복 iretq
에 의해 복원된 프로그램 카운터가 args.c의 주소값을 가르키기에 args.c로 이동
7. System Call 호출 시점
args.c - main()
- 우리는 args라는 값을 실행하라고 어셈블리어 명령을 통해 레지스터에 넣어줬고 따라서 arg.c를 실행하게 된다.
int
main (int argc, char *argv[])
{
int i;
test_name = "args";
if (((unsigned long long) argv & 7) != 0)
msg ("argv and stack must be word-aligned, actually %p", argv);
msg ("begin");
msg ("argc = %d", argc);
for (i = 0; i <= argc; i++)
if (argv[i] != NULL)
msg ("argv[%d] = '%s'", i, argv[i]);
else
msg ("argv[%d] = null", i);
msg ("end");
return 0;
}
msg
void
msg (const char *format, ...)
{
va_list args;
if (quiet)
return;
va_start (args, format);
vmsg (format, args, "\n");
va_end (args);
}
vsmg
static void
vmsg (const char *format, va_list args, const char *suffix)
{
/* We go to some trouble to stuff the entire message into a
single buffer and output it in a single system call, because
that'll (typically) ensure that it gets sent to the console
atomically. Otherwise kernel messages like "foo: exit(0)"
can end up being interleaved if we're unlucky. */
static char buf[1024];
snprintf (buf, sizeof buf, "(%s) ", test_name);
vsnprintf (buf + strlen (buf), sizeof buf - strlen (buf), format, args);
strlcpy (buf + strlen (buf), suffix, sizeof buf - strlen (buf));
write (STDOUT_FILENO, buf, strlen (buf));
}
8. System Call
@/lib/user/syscall.c
int
write (int fd, const void *buffer, unsigned size) {
return syscall3 (SYS_WRITE, fd, buffer, size);
}
#define syscall3(NUMBER, ARG0, ARG1, ARG2) ( \
syscall(((uint64_t) NUMBER), \
((uint64_t) ARG0), \
((uint64_t) ARG1), \
((uint64_t) ARG2), 0, 0, 0))
__attribute__((always_inline))
static __inline int64_t syscall (uint64_t num_, uint64_t a1_, uint64_t a2_,
uint64_t a3_, uint64_t a4_, uint64_t a5_, uint64_t a6_) {
int64_t ret;
register uint64_t *num asm ("rax") = (uint64_t *) num_;
register uint64_t *a1 asm ("rdi") = (uint64_t *) a1_;
register uint64_t *a2 asm ("rsi") = (uint64_t *) a2_;
register uint64_t *a3 asm ("rdx") = (uint64_t *) a3_;
register uint64_t *a4 asm ("r10") = (uint64_t *) a4_;
register uint64_t *a5 asm ("r8") = (uint64_t *) a5_;
register uint64_t *a6 asm ("r9") = (uint64_t *) a6_;
__asm __volatile(
"mov %1, %%rax\n"
"mov %2, %%rdi\n"
"mov %3, %%rsi\n"
"mov %4, %%rdx\n"
"mov %5, %%r10\n"
"mov %6, %%r8\n"
"mov %7, %%r9\n"
"syscall\n"
: "=a" (ret)
: "g" (num), "g" (a1), "g" (a2), "g" (a3), "g" (a4), "g" (a5), "g" (a6)
: "cc", "memory");
return ret;
}
9. System Call Handler
syscall_entry:
movq %rbx, temp1(%rip)
movq %r12, temp2(%rip) /* callee saved registers */
movq %rsp, %rbx /* Store userland rsp */
movabs $tss, %r12
movq (%r12), %r12
movq 4(%r12), %rsp /* Read ring0 rsp from the tss */
/* Now we are in the kernel stack */
push $(SEL_UDSEG) /* if->ss */
push %rbx /* if->rsp */
push %r11 /* if->eflags */
push $(SEL_UCSEG) /* if->cs */
push %rcx /* if->rip */
subq $16, %rsp /* skip error_code, vec_no */
push $(SEL_UDSEG) /* if->ds */
push $(SEL_UDSEG) /* if->es */
, , ,
check_intr:
btsq $9, %r11 /* Check whether we recover the interrupt */
jnb no_sti
sti /* restore interrupt */
no_sti:
movabs $syscall_handler, %r12 // <- syscall Handler 직접 호출
call *%r12
popq %r15
popq %r14
'PintOS' 카테고리의 다른 글
[PintOS] Victim Policy - 교체 알고리즘 (0) | 2024.05.27 |
---|---|
[Pintos] Project3 - Page Fault (0) | 2024.05.21 |
[PintOS] Project1 - Monitor System Deep Dive (0) | 2024.05.03 |