본문 바로가기
개발공부/SW정글

PINTOS 2-3주차: USER PROGRAM

by realbro.noh 2021. 10. 13.

User Programs

 

프로젝트 2를 하며 느낀점:

이번 프로젝트는 코드 분석이 전부였다. 다행히 프로젝트2를 통과한 분의 코드를 구할 수 있었다. 먼저 공식 문서를 보며 어떤 것이 필요하고 무엇을 구현해야 하는지 파악했다. 그리고 그 분의 코드를 분석하며 어떻게 요구사항을 맞추었는지 정리했다. 저번과는 달리 코드의 깊이가 상당했다. 함수 하나를 보면 타고 타고 가서 처음에는 압도됐다. 한번 보는 것으로는 이해할 수 없었다. 괜찮다. 2번보고 3번보고 계속 보면 된다. 볼수록 저번에 이해할 수 없었던 부분들이 보이기 시작하고 전체적인 흐름을 알 수 있었다.

실제 구현 위주로 이번 프로젝트를 한 분들도 많다. 그런 분들을 보면서 나도 구현 위주로 했어야 하나 하느 생각도 든다. 하지만 이번 핀토스를 하면서 어떤 길을 택하든 모든 길에 의미가 있다고 생각한다. 우리에게 주어진 시간이 많지 않다. 그리고 기반 지식도 부족하다. 코드 구현을 중점으로 하면 코드 구현 능력이 향상될 것이고, 코드 분석을 위주로 하면 몇천줄의 복잡한 코드를 빠르게 이해하는 능력을 기를 수 있을 것이다. 심지어 그냥 OS 공부를 하는 것도 의미가 있다고 생각한다. os 지식을 바탕으로 핀토스 코드를 다시 봤을 때, 실제로 어떻게 적용이 되었는지 깨달을 수 있다.

이번 프로젝트를 통해 어떤 복잡한 코드가 주어져도 70% 정도는 이해할 수 있다는 느낌이 들었다. 결국 함수를 타고 가면서 역할과 의미를 하나하나 따져가면 될 것이다(시간은 많이 걸릴 지라도 점점 단축될 것이라 생각한다). 다음 프로젝트는 소스가 없을 것으로 생각한다. 하지만 코드 분석을 중점적으로 생각한다면 굳이 새 핀토스 버전이 아니어도 괜찮고 테스트 통과에 대한 생각도 잠시 내려놓아도 괜찮다고 생각한다. 구 버전의 소스(32 bit  PINTOS)를 분석하며 OS 지식이 어떻게 구현되어있는지 파악하고, 복잡한 함수를 해석하는 능력을 기르면 그 또한 의미가 있을 것이라고 생각한다.

이번 주차에 어떤 것을 했는지 밑에 적어놓았다.

 

1 ARGUMENT PASSING

- 목표

명령어와 함께 들어온 인자들을 user program의 user stack에 정해진 순서에 따라 넣어준다.

- 예시

  • 'bin/ls -l foo bar'이 커맨드라인에 들어오면 argv[0] = 'bin/ls\0', 'argv[1] = -l\0', argv[2] = 'foo\0', argv[3] = 'bar\0'으로 parsing(문자열 분리)한 뒤에 아래와 같이 스택에 넣어준다.
  • | argv[3] | ... | argv[0] | padding for 8 배수 | NULL pointer for argv[argc=4] | pointer for argv[3] | ... | pointer for argv[0] | return address = NULL |
  •  

- 코드

1 file_name을 parsing하는 코드

char *argv[30];
int argc = 0;

char *token, *save_ptr;  // argc = N+1; argv = {filename, arg1, ... argN}로 parsing
token = strtok_r(file_name, " ", &save_ptr);
while (token != NULL)
{
    argv[argc] = token;
    token = strtok_r(NULL, " ", &save_ptr);    // 두번째 parsing부터는 첫번째 인자를 NULL로 한다
    argc++;
}

2 strtok_r 함수: 문자열 파싱하는 함수

char *
strtok_r (char *s, const char *delimiters, char **save_ptr) {
    char *token;

    ASSERT (delimiters != NULL);
    ASSERT (save_ptr != NULL);

    /* If S is nonnull, start from it.
       If S is null, start from saved position. */
    if (s == NULL)
        s = *save_ptr;
    ASSERT (s != NULL);

    /* Skip any DELIMITERS at our current position. */
    while (strchr (delimiters, *s) != NULL) {
        /* strchr() will always return nonnull if we're searching
           for a null byte, because every string contains a null
           byte (at the end). */
        if (*s == '\0') {
            *save_ptr = s;
            return NULL;
        }

        s++;
    }

    /* Skip any non-DELIMITERS up to the end of the string. */
    token = s;
    while (strchr (delimiters, *s) == NULL)
        s++;
    if (*s != '\0') {
        *s = '\0';
        *save_ptr = s + 1;
    } else
        *save_ptr = s;
    return token;
}

2 parsed arguments를 user stack에 넣어주는 함수

void load_userStack(char **argv, int argc, void **rspp)
{
    // 1. Save argument strings (character by character)
    for (int i = argc - 1; i >= 0; i--)
    {
        int N = strlen(argv[i]);
        for (int j = N; j >= 0; j--)
        {
            char individual_character = argv[i][j];    // argv - i번째 스트링의 j번째 character; '\0'부터 거꾸로 시작
            (*rspp)--;        // (*rspp) = _if.rsp (unsigned long int: 8 byte 정수로 주소값 표현?) --> _if.rsp 1 감소 (1 byte 전 주소값으로 이동?)
            **(char **)rspp = individual_character; // rspp를 (char **)로 캐스팅하면, **(char **)rspp = *(char *)rsp = (char)(메모리)
        }
        // *(char **)rspp 는 (char *)형 포인터!
        argv[i] = *(char **)rspp; // (char *) 포인터..push this address too; 3번에 사용할 것
    }

    // 2. Word-align padding
    int pad = (int)*rspp % 8;        // (*rspp): _if.rsp 주소값 --> 8로 나눈 나머지
    for (int k = 0; k < pad; k++)    // pad만큼 주소값--; 주소 번지를 8의 배수 맞추기 위해
    {
        (*rspp)--;                    // _if.rsp 주소값 -1 이동
        **(uint8_t **)rspp = (uint8_t)0; // _if.rsp가 가리키는 내용물으 0으로 채움(1 byte)
    }

    // 3. Pointers to the argument strings
    size_t PTR_SIZE = sizeof(char *);

    (*rspp) -= PTR_SIZE;            // _if.rsp를 8byte 내림
    **(char ***)rspp = (char *)0;    // NULL포인터로 채움 argv[argc]

    for (int i = argc - 1; i >= 0; i--)
    {
        (*rspp) -= PTR_SIZE;        // 8byte 이동하면서 각 argv[i]가 저장된 스택의 주소값(char *) 기록(위-위 for문 마지막에 저장한 것)
        // 하고싶은 것: _if.rsp가 가리키는 곳에 argv[i] (char * 자료형) 을 기입하고 싶음 --> *(char **)_if.rsp = argv[i] 를 해주고 싶은 것! <-- 캐스팅하는 이유는 (char *)를 기록해야 하므로
        **(char ***)rspp = argv[i];    // argv[i]는 (char *)이고, *rspp가 _if.rsp --> (char ***)rspp 로 캐스팅하면
                                    //                            rspp는 (char ***), *rsp == _if.rsp는 (char **)형으로 캐스팅된 것..!: 원하는 결과
                                    // 즉, *(char **)_if.rsp = argv[i]   <==>  **(char ***)rspp = argv[i]
    }

    // 4. Return address
    (*rspp) -= PTR_SIZE;
    **(void ***)rspp = (void *)0;    // 마지막 return add도 void형 포인터 NULL 기입
}

2 SYSTEM CALLS

각 system call 함수의 흐름을 정리했다

- halt()

halt() --> power_off()

- exit()

exit() --> thread_exit() --> process_exit(): 

                                        --> 열린 파일들 닫기
                                        --> palloc_free_multiple(): free fdt
                                        --> file_close(): 현재 실행중인 executable file 닫기
                                        --> process_cleanup(): free current process's resources
                                        --> sema_up(wait_sema): process_wait()에 sema_down(wait_sema)에 막혀 잠든 부모 스레드 깨운다
                                        --> sema_down(free_sema): 부모 프로세스가 sema_up(free_sema)할때까지 잠듦

                         --> do_schedule(THREAD_DYING

- fork()

(부모 스레드)
process_fork(thread_name, intr_frame)

 -> memcpy(&cur->parent_if, if_,): to pass this intr_frame down to child
--> thread_create(, __do_fork, cur): [자식 스레드]
                                    --> do_fork(cur):
                                                     --> 1. 부모 스레드의 cpu context 복사(intr_frame)
                                                     --> 2. page table 복사
                                                     --> sema_up(&current->fork_sema): wake up parrent process
                                                     --> do_iret(): switch to newly created process
                                                     --> 종료

--> sema_sown(&child->fork_se ma): child thread가 실행되어 __do_fork에서 sema_up()하기까지 기다림
--> return tid

- exec()

fn_copy = palloc_get_page(): 파일 이름 동적할당하여 복사할 공간 확보
strlcpy(): 복사 후 넘겨주기(process_cleanup()에서 다 날라가나?)

process_exec(fn_copy):
                      --> intr_frame 생성
                      --> process_cleanup(): kill the current context
                      --> argument parsing(게시글 맨 위 argument passgin 부분 참고)
                      --> load(file_name, &_if): 실행파일을 메모리에 로딩
                      --> load_userStack(): parsed arguments를 userstack에 올림
                      --> do_iret(): switch process

- process_wait()

get_child_with_pid()
sema_down(&child->wait_sema): [부모 스레드 잠듬] wait_sema가 0으로 초기화되어 있어 
                              자식 스레드가 process_exit()에서 sema_up(wait_sema)호출할때까지 기다림

                            --> [자식 스레드] process_exit()
                            --> ... process_cleanup()
                            --> sema_up(&cur->wait_sema): process_wait()으로 잠든 부모 스레드 깨움
                            --> sema_down(&cur->free_sema): [자식 스레드 잠듬] 부모 스레드가 자식 리스트 지울 때까지 기다림

                            <-- [부모 스레드]
exit_status = child->exit_status : 자식 스레드의 종료 status 기록(child->exit_status는 자식 스레드가 syscall.c/exit()호출할때 변경)
list_remove(): 자식 스레드 리스트에서 제거
sema_up(&child->free_sema): 자고 있는 자식 스레드 깨움

                            --> [자식 스레드] process_exit() 종료

- create()

filesys_create(file, initial_size)

 -> 1. dir_open_root(): 루트 디렉토리 inode를 메모리로 로딩

                        --> inode_opedn(ROOT_DIR_SECTOR): 루트 섹터에서 inode를 읽어들임

                                        --> 이미 열려있는 inode인지 확인 후 reopen(): open_inodes 리스트 순회
                                        --> 아닌 경우, inode 메모리 동적할당
                                        --> inode 초기화: open_inodes 리스트에 추가, 기본 멤버 초기화
                                        --> disk_read(): 디스크 SECTOR에 저장된 정보를 inode->data에 저장
                                        --> return inode

                        --> dir_open(inode): dir 자료구조 메모리 할당 및 inode 연결

--> 2. free_map_allocate(1, &inode_sector): 디스크에서 1 sector 할당(생성할 파일 inode 기록하기 위한 공간?)
--> 3. inode_create(inode_sector, initial_size): file의 inode를 작성하고(메모리에서) 디스크(inode_sector)에 기록

                        --> disk_inode 동적할당(디스크에 쓸 자료구조)
                        --> disk_inode멤버 초기화: 파일 크기 크기, 등
                        --> free_map_allocate(): 파일 크기에 해당하는 sector 수만큼 디스크 할당
                        --> disk_write(): 디스크에 disk_inode 기록
                        --> disk_write(): 디스크에 파일 기록
                        --> disk_inode 동적 해제

--> 4. dir_add(): 열려 있는 루트 디렉토리에 새로 만든 파일 아이노드 엔트리 추가하고 디스크에 기록

                        --> lookup(): DIR 내 동일 이름 파일 있는지 검사
                        --> dir->inode에서 비어있는 엔트리 찾는다: 해당 ofs(오프셋)값을 찾는다 
                        --> dir->inode 해당 엔트리에 생성된 파일 inode정보(멤버) 기록: in_use, name, sector 등
                        --> inode_write_at(): 디스크에 저장

--> 5. dir_close(): 루트 디렉토리 inode메모리 해제

                    --> inode_close(dir->inode): inode.open_cnt를 하나 내리고 0이 되면 비로소 메모리에서 지운다

                                            --> list_remove(): open_inodes 리스트에서 지운다
                                            --> 디스크에서 해당 섹터를 할당 해제한다(inode->sector: inode자체, inode->data: inode에 해당하는 파일)
                                            --> free(inode) 

                    --> free(dir)

remove()

filesys_remove()

 -> dir_open_root(): root 디렉토리 연다
--> dir_remove(): 디렉토리에서 해당 엔트를 사용안함처리하고, 아이노드 removed=true 처리

                --> lookup(): 디렉토리에서 파일 이름을 가진 엔트리 찾는다(offset)
                --> inode_open(): 엔트리에 해당하는 아이노드를 연다
                --> e.in_use=true: 해당 엔트리 사용안함처리
                --> inode_write_at(): 디렉토리의 해당 엔트리 갱신(사용안함처리했으므로)
                --> inode_remove(inode): 해당 아이노드를 deleted 처리

                                    --> inode->removed=true

--> inode_close(inode): inode->opencnt를 하나 내리고 0이 되면 메모리에서 내린다
--> dir_close(): 루트 디렉토리를 닫는다

open()

filesys_open(): 파일을 연다

            --> dir_open_root(): 루트 디렉토리 연다
            --> dir_lookup(): 디렉토리 엔트리 검색 -> 찾은 파일의 inode를 open_inodes리스트에 추가
            --> dir_close(): 루트 디렉토리 닫는다
            --> file_open(inode): 메모리에 file 자료구조 할당 후 해당 파일의 inode 포인팅 및 초기화

                                --> file 구조체 동적 할당
                                --> 파일 멤버 초기화: inode와 연결

add_file_to_fdt(): 현재 실행 스레드의 FDT(file descriptor table)의 비어있는 인덱스에 파일 할당

filesize()

find_file_by_fd(): 현재 실행 스레드의 fdt에서 fd번째 파일 찾는다
file_length(): 해당 파일의 길이(파일 -> inode -> inode->data.length)

             --> inode_length(file->inode) --> inode->data.length

seek()

find_file_by_fd(): 현재 실행 스레드의 fdt에서 fd번째 파일 찾는다
fileobj->pos = position: 다음에 읽을 곳(pos)을 변경한다

tell()

find_file_by_fd(): 현재 실행 스레드의 fdt에서 fd번째 파일 찾는다
file_tell(): 현재 파일에서 다음에 읽을 곳 반환

        --> file->pos

read()

find_file_by_fd(): 현재 실행 스레드의 fdt에서 fd번째 파일 찾는다
CASE1: STDOUT --> X
CASE2: STDIN  --> input_getc()로 한글자씩 버퍼에 복사
ELSE : 
        lock_acquire(rw_lock): rw 접근 하나의 스레드만?
        file_read() -> inode_read_at(): read SIZE byes from INODE into BUFFER; 어렵다
        lock_release(rw_lock)

write()

find_file_by_fd(): 현재 실행 스레드의 fdt에서 fd번째 파일 찾는다
CASE1: STDIN  --> X
CASE2: STDOUT --> putbuf()로 출력?
ELSE : 
        lock_acquire(rw_lock): rw 접근 하나의 스레드만?
        file_write() -> inode_write_at(): write SIZE bytes from BUFFER into INODE; 어렵다
        lock_release(rw_lock)

close()

find_file_by_fd(): 현재 실행 스레드의 fdt에서 fd번째 파일 찾는다
CASE1: STDIN  --> cur->stdin_count--;
CASE2: STDOUT --> cur->stdout_count--;
remove_file_from_fdt(): 해당파일 fdt에서 제거

IF CASE1 OR CASE2: return
IF file0>dupCount != 0: file->dupCount --;
ELSE:
    file_close(): 
                --> file_allow_write(file): 쓰기 허용
                --> inode_close(file->inode): inode를 메모리에서 내린다

                                --> if (--inode->open_cnt == 0): open_cnt를 하나 내리고 0이 되면
                                --> list_remove(): open_inodes리스트에서 해당 아이노드 삭제
                                --> 그리고 inode->removed=true(아이노드 삭제 처리되었을때)
                                --> 디스크에서 아이노드 부분과 아이노드가 가리키는 파일 부분의 블록 할당 해제 
                                --> free(inode)

                --> free(file)

'개발공부 > SW정글' 카테고리의 다른 글

PINTOS 6주차 file system  (0) 2021.11.02
PINTOS 4-5주차 VM  (1) 2021.10.28
PINTOS 1주차: priority scheduling  (0) 2021.10.04
정글일기 20210825  (1) 2021.08.25
WEEK01 돌아보는 시간  (4) 2021.08.08