////
Search
🎻

과제 최종 보고서 | 20185196 추교현

개발 과정

[1] Pintos 개발 환경 설정하기

Pintos는 기본적으로 x86 아키텍처를 기반으로 설계되었기 때문에 ARM 아키텍처를 사용하는 Apple의 M시리즈 칩을 사용하는 맥북으로는 직접 실행시킬 수 없습니다.
그래서 Docker로 x86 기반의 Ubuntu를 설치하여 격리된 환경 속에서 Pintos를 실행시키면 됩니다. 구체적인 내용은 해당 링크 확인하시면 됩니다.

[2] Pintos 이해하기

부팅에서 진입점 이동까지
Pintos를 활용하기 위해서는 커널을 메모리에 로드시켜야 합니다. 컴퓨터가 부팅될 때, 맨 처음 운영체제의 커널을 메모리로 로딩하는 데 이 역할을 부트 로더 가 합니다. 부트 로더 는 커널을 메모리에 로드하고 커널의 엔트리 포인트로 점프시킵니다. 이러한 과정은 loader.S에서 진행됩니다.
부트 로더가 커널의 엔트리 포인트로 점프하면, init.c의 main 함수로 바로 이동하지 않습니다. 이 main 함수는 gcc에 의해 컴파일되면서 그 함수의 entry(진입점)가 어디에 존재할지 알기 위해서는 디어셈블리한 후에 여러 복잡한 계산을 해야 하므로 일단 loader.S의 call에 의해 호출되는 지점의 코드는 어셈블리로 한 번 더 작성하여 start.S 내부에서 main 함수를 호출하는 구조를 갖습니다.
start.S에서 main 함수를 호출합니다. main 함수는 실제 커널 초기화의 수행을 맡게 됩니다. thread_init(), console_init(), palloc_init(), malloc_init(), paging_init(), intr_init(), timer_init(), syscall_init() 등 많은 기능들이 초기화됩니다.
또한, main 함수에서 read_command_line 함수로 사용자의 입력 데이터를 argv 에 저장합니다. 그리고 argv에 따라 run_actions 함수에 매핑된 함수로 진입하여 본격적인 실행이 시작됩니다.
이번 프로젝트에서는 run_automated_warehouse라는 함수로 진입하게 됩니다.

[3] 구현 과정

전체적인 구현 과정은 다음과 같습니다:
1.
입력 인자(argv) 파싱 및 초기화(maps, message_box)
2.
N개의 robot 생성 & 스레드(robots, center) 생성
3.
block() & unblock() 함수 구현
4.
message_box 구현
5.
get_next_pos() & bfs() 구현
6.
스레드 실행 함수(cnt_thread(), robots_thread()) 구현
구체적인 구현 방법은 아래에서 말씀드리도록 하겠습니다.

구현 방법

[1] 입력 인자(argv) 파싱 및 초기화(maps, message_box)

// automated_warehouse.c /// run_automated_warehouse 함수 n = atoi(argv[1]); const char* input = argv[2]; char** segments = parse_string(input); create_maps(); // 이동에 참고할 maps 초기화 init_message_boxes(n+1); // 모든 스레드가 참고할 message_box 초기화
C
복사
입력 인자로부터 생성할 로봇 개수(n)와 각 로봇이 들게 될 물건과 하역할 장소에 대한 정보(input)를 파싱합니다. 또한, 이동에 참고할 maps와 모든 스레드가 참고할 message_box를 초기화하는 과정을 진행합니다.
// aw_manager.c /// parse_string 함수 char** parse_string(const char* str) { int num_segments = 1; // 최소 하나의 세그먼트 존재 // ':' 기준으로 세그먼트의 개수 파악 for (int i = 0; str[i] != '\0'; i++) { if (str[i] == ':') { num_segments++; } } // 문자열 포인터 배열 동적 할당 char** result = (char**)malloc(sizeof(char*) * num_segments); if (result == NULL) { return NULL; } char* saveptr; // strtok_r 상태 저장을 위한 포인터 int index = 0; char* temp_str = my_strdup(str); char* token = strtok_r(temp_str, ":", &saveptr); while (token != NULL) { result[index++] = my_strdup(token); token = strtok_r(NULL, ":", &saveptr); } free(temp_str); // 사용한 임시 문자열 메모리 해제 return result; }
C
복사
이 함수를 예시와 함께 설명해 드리자면, 입력 인자로 "2A:4B"를 받았을 때, {"2A", "4B"}로 바꿔주는 함수입니다. str를 인자로 받아 : 을 기준으로 세그먼트의 개수를 파악합니다. result를 이중 포인터 배열로 동적 할당하고 temp_strstr을 복사한 뒤, :을 기준으로 strtok_r으로 나눕니다. 이를 result 배열에 순서대로 넣은 뒤, 리턴하면 됩니다.
// aw_manager.c /// create_maps 함수 void create_maps() { for (int i = 0; i < ROW; i++){ for (int j = 0; j < COL; j++){ if (map_draw_default[i][j] == 'X' || (map_draw_default[i][j] >= '0' && map_draw_default[i][j] <= '9')) { maps[i][j] = 1; } else { maps[i][j] = 0; } } } }
C
복사
map_draw_default를 참고하여 'X'나 숫자가 있는 곳은 1, 나머지는 0으로 maps를 만드는 함수입니다. 이 maps를 참고하여 각 로봇들이 충돌하지 않게 제어할 수 있게 됩니다.
// aw_manager.c /// init_message_boxes 함수 void init_message_boxes(int n) { boxes_from_center = malloc(sizeof(struct message_box) * (n+1)); boxes_from_robots = malloc(sizeof(struct message_box) * (n+1)); for (int i = 0; i < n; i++) { // 처음 위치는 무조건 W(5, 5) boxes_from_center[i].msg.row = ROW_W; boxes_from_center[i].msg.col = COL_W; boxes_from_center[i].dirtyBit = 1; // 1인 이유 : 처음에는 center에서 모든 로봇에게 메시지를 보내야 하기 때문 boxes_from_robots[i].msg.row = ROW_W; boxes_from_robots[i].msg.col = COL_W; boxes_from_robots[i].dirtyBit = 0; } }
C
복사
center 스레드robot 스레드들이 참고할 message_box를 동적 할당하고 각 필드의 값들을 초기화시켜 주는 함수입니다.

[2] N개의 robot 생성 & 스레드(robots, center) 생성

// automated_warehouse.c /// robot 생성 및 초기화 char name[10]; robots = malloc(sizeof(struct robot) * (n+1)); for (int i = 0; i < n; i++) { snprintf(name, sizeof(name), "R%d", i+1); NumCharPair pairs = split_string(segments[i]); // Ex) "2A" => {2, 'A'} // Ex) 2와 'A'에 해당하는 좌표 각각 구하기 Position mid_pos = findCharPosition(pairs.num + '0'); // '0' 넣은 이유 : 숫자를 문자로 바꾸기 위해 Position final_pos = findCharPosition(pairs.letter); // 초기 좌표는 W 위치인 (5, 5)로 설정 setRobot(&robots[i], name, 5, 5, 0, 0, mid_pos.row, mid_pos.col, 0, final_pos.row, final_pos.col, 0); }
C
복사
robot마다 현재 위치, 물건 좌표, 하역 장소 좌표 등을 동적 할당된 robotssetRobot으로 초기화시켜 줍니다. 이때, snprintf()를 사용해서 name 배열에 각 인덱스를 참조해서 “R1”, “R2” 등의 값을 넣은 뒤, 이를 setRobot의 name 인자에 넣습니다.
split_string 함수는 "2A"와 같은 문자열을 {2, 'A'}로 나눠주며 findCharPosition은 해당 문자가 있는 위치를 리턴하여 robot이 가야 할 좌표를 알려줍니다.
// automated_warehouse.c /// robots thread 생성 tid_t* threads = malloc(sizeof(tid_t) * (n+1)); int* idx = malloc(sizeof(int) * (n+1)); for (int i = 0; i < n; i++) { idx[i] = i; threads[i] = thread_create(robots[i].name, 0, &robots_thread, &idx[i]); } /// center thread 생성 thread_create("CNT", 0, &cnt_thread, NULL);
C
복사
thread_create()를 통해 각 robot마다 스레드를 생성하며 중앙 노드는 “CNT”라는 이름을 가진 스레드로 하나 생성하게 됩니다. 이때, 3번째 인자는 해당 스레드가 실행시킬 함수(robots_thread, cnt_thread)로서 프로그램을 실행시키는 중요한 역할을 하게 됩니다. 또한, 4번째 인자는 해당 스레드의 고유 번호(aux)라고 이해하시면 됩니다.

[3] block() & unblock() 함수 구현

intr_disable ()을 호출하여 현재 인터럽트 레벨을 비활성화하고, 이전 인터럽트 레벨을 old_level에 저장합니다. 이는 함수의 작업 중 인터럽트가 발생하지 않도록 보장하기 위함입니다.
함수의 작업이 끝나면, intr_set_level (old_level)을 호출하여 원래의 인터럽트 레벨을 복원합니다.
위의 두 과정은 스레드 간 경쟁 상태가 되지 않도록 만들기에 중요하며 block() & unblock() 두 함수 덕분에 center 스레드와 robots 스레드 간의 핑퐁(혹은 소통)이 가능해지게 됩니다.
// aw_thread.c /// 현재 스레드를 block시키고 blocked_threads 리스트에 넣는 함수 void block_thread(){ enum intr_level old_level; old_level = intr_disable (); list_push_back(&blocked_threads, &thread_current ()->elem); thread_block (); intr_set_level (old_level); }
C
복사
현재 스레드를 block 시키고 blocked_threads 리스트에 넣는 함수입니다.
// aw_thread.c /// blocked_threads 리스트 안에 있는 모든 스레드들을 꺼내고 unblock 시키는 함수 void unblock_threads(){ enum intr_level old_level; old_level = intr_disable (); while (!list_empty(&blocked_threads)){ struct thread *t = list_entry(list_pop_front(&blocked_threads), struct thread, elem); thread_unblock(t); } intr_set_level (old_level); }
C
복사
blocked_threads 리스트 안에 있는 모든 스레드들을 꺼내고 unblock 시키는 함수입니다.

[4] message_box 구현

center 스레드와 robots 스레드 간의 소통을 위해 동적 할당한 message_box를 활용하여 소통하도록 코드를 구현했습니다.
// aw_message.c /// message_box : robot -> center void message_from_robots(int idx, struct message_box* msg_box, int robot_row, int robot_col) { msg_box->msg.row = robot_row; msg_box->msg.col = robot_col; msg_box->dirtyBit = 1; }
C
복사
robot 스레드center 스레드에 보내는 message_box를 업데이트하는 함수입니다.
// aw_message.c /// message_box : center -> robot void message_from_center(int idx, struct message_box* msg_box, int n_r, int n_c) { msg_box->msg.row = n_r; msg_box->msg.col = n_c; msg_box->dirtyBit = 1; }
C
복사
center 스레드robot 스레드에 보내는 message_box를 업데이트하는 함수입니다.

[5] get_next_pos() & bfs() 구현

이번 프로젝트에서 핵심이라고 할 수 있는 get_next_pos() & bfs() 두 함수에 대해서 설명드리겠습니다.
우선, 저는 매 턴마다 로봇을 하나씩만 제어하는 것이 비효율적이고 오히려 복잡도가 늘어날 것이라고 생각했습니다. 그래서 처음부터 모든 로봇들을 동시에 제어하는 방향으로 설계했습니다.
각 로봇(Ex. 1번)은 물건(Ex. 2)이 있는 곳을 갔다가 하역 장소(Ex. A)로 들어가면, 임무 완료이며 모든 로봇이 임무를 완료해야 시뮬레이션이 종료됩니다. 그렇기 때문에 저는 각 로봇이 상하좌우 한 칸씩 이동(nr, nc)하면서 장애물(’X’)과 로봇들을 피해 물건(혹은 하역 장소)으로 갈 수 있는 방향을 먼저 고르게 했습니다. 그리고 만약 갈 수 있는 방향이 없다면, 4를 리턴하여 dr=0, dy=0으로 제자리에서 대기하도록 구현했습니다.
// aw_manager.c int get_next_pos(int c_r, int c_c, int t_r, int t_c) { int min_distance = 1000; int next_pos = 4; // 4인 이유 : 움직일 수 없을 때, 제자리 유지를 위해서 for (int i = 0; i < 4; i++) { int nr = c_r + dr[i]; int nc = c_c + dc[i]; // 만약 다음 지점이 목표 지점이라면, 그 방향으로 이동 if (nr == t_r && nc == t_c) { return i; } if (isValid(nr, nc) && maps[nr][nc] == 0) { /// 목표 지점까지 가장 짧은 거리에 해당하는 방향으로 이동 int distance = bfs(nr, nc, t_r, t_c); if (distance < min_distance) { min_distance = distance; next_pos = i; } } } return next_pos; }
C
복사
그런데 목표 지점까지 갈 수 있는 방향이 여러 개일 수도 있게 됩니다. 그렇다면, 여러 방향 중 목표 지점과 가장 가까워지는 방향으로 이동하면 되는데 이를 찾아주는 bfs() 함수가 필요하게 됩니다.
// aw_manager.c int bfs(int c_r, int c_c, int t_r, int t_c) { int visited[ROW][COL] = {0}; // 방문 배열 초기화 int distance[ROW][COL] = {0}; // 거리 저장 배열 // 큐 초기화 Point queue[ROW * COL]; int front = 0, rear = 0; // 시작점을 큐에 삽입 후, 방문 표시 queue[rear++] = (Point){c_r, c_c}; visited[c_r][c_c] = 1; while (front < rear) { // 큐의 맨 앞에 있는 요소 꺼내기 Point cur = queue[front++]; // 만약 목표 지점에 도달했다면, 거리 반환 if (cur.row == t_r && cur.col == t_c) { return distance[t_r][t_c]; } for (int i = 0; i < 4; i++) { int nr = cur.row + dr[i]; int nc = cur.col + dc[i]; // 만약 다음 지점이 목표 지점이라면, 거리 반환 (이게 없으면, 진입 불가) if (nr == t_r && nc == t_c) { return distance[cur.row][cur.col] + 1; } if (isValid(nr, nc) && maps[nr][nc] != 1 && !visited[nr][nc]) { visited[nr][nc] = 1; distance[nr][nc] = distance[cur.row][cur.col] + 1; queue[rear++] = (Point){nr, nc}; } } } return 1000; }
C
복사
bfs() 함수는 큐를 이용해서 가장 가까운 노드(여기서는 map의 위치)들을 탐색하면서 이동한 거리를 distance에 저장하게 됩니다. 그러다가 목표 지점에 도달하게 되면 해당 위치의 distance 값을 리턴하게 됩니다. 그리고 그 값 중에서 가장 작은 값에 해당하는 방향이 로봇이 가야 할 방향이 됩니다.
이렇게 두 함수를 통해 로봇이 다음 이동 위치를 정하게 됩니다. 다음 이동할 위치를 maps에 업데이트하며, 다른 로봇들이 갈 수 있도록 원래 있었던 위치를 0으로 바꿔줍니다.
또한, 각 로봇마다 다음 이동 위치를 정하기 전에 물건을 들고 있는지(is_loaded)와 하역 장소까지 도착했는지(is_finish)에 대한 여부를 robot 구조체의 is_loaded, is_finish 에 저장하여 목표 지점을 판단하는 로직을 추가했습니다.
// automated_warehouse.c /// cnt_thread() 함수 int target_row, target_col; if (robots[i].is_loaded) { // 하역 장소로 이동 target_row = robots[i].f_row; target_col = robots[i].f_col; } else { // 적재 장소로 이동 target_row = robots[i].l_row; target_col = robots[i].l_col; }
C
복사

[6] 스레드 실행 함수(cnt_thread(), robots_thread()) 구현

최종적으로 위의 함수들을 활용해서 각 스레드가 실행하고 block/unblock 처리하는 스레드 실행 함수를 구현했습니다.
// automated_warehouse.c void cnt_thread(){ while(1) { // robots thread가 모두 block되면, 실행 int is_full = is_blocked_thread_full(n); if (is_full) { print_map(robots, n); int final_cnt = 0; for (int i = 0; i < n; i++) { if(boxes_from_robots[i].dirtyBit == 1) { // 각 robot이 보낸 메시지를 바탕으로 명령 내리기 ... // 각 robot에게 보낼 message_box 업데이트 message_from_center(i, &boxes_from_center[i], n_row, n_col); boxes_from_robots[i].dirtyBit = 0; } } // 모든 robot이 작업을 완료 if (final_cnt == n) { printf("\n--------- 🎉🎉🎉🎉🎉 Mission Complete 🎉🎉🎉🎉🎉 ---------\n"); break; } increase_step(); unblock_threads(); } } }
C
복사
center 스레드는 모든 robot 스레드가 block 처리가 되면 실행합니다. print_map을 호출하고 각 robot이 보낸 메시지를 참고하여 get_next_pos() & bfs() 함수를 통해 각 robot이 이동할 좌표를 구하며 이를 message_box로 다음 명령을 하달합니다.
모든 robot이 작업이 끝나면, 시뮬레이션은 종료되며 그렇지 않으면, step을 올리고(increase_step()) 모든 스레드를 unblock(unblock_threads()) 시킵니다.
// automated_warehouse.c void robots_thread(void* aux){ int idx = *((int *)aux); int test = 0; while(1) { if (boxes_from_center[idx].dirtyBit == 1) { // center가 보낸 메시지를 각 robot 구조체에 업데이트 ... // center에게 보낼 message_box를 업데이트 message_from_robots(idx, &boxes_from_robots[idx], robots[idx].row, robots[idx].col); boxes_from_center[idx].dirtyBit = 0; } // message_box를 보냈다면, 스스로 block 되기 block_thread(); } }
C
복사
robot 스레드는 각자 고유한 번호(aux)가 있으며 이 번호를 통해 message_box에 접근하여 center 스레드 가 보낸 명령을 확인합니다. 이 명령을 참고하여 자신의 robot 구조체의 값들을 업데이트하고 업데이트한 값들을 center 스레드에게 보낼 message_box에 넣습니다. 이 과정이 끝나면, block 처리를 하여 blocked_threads 리스트에 들어갑니다.

트러블 슈팅 과정

[1] robot을 생성할 때, 메모리 공간을 하나만 사용하여 마지막 이름만 찍힘

[에러 상황]
robots 를 생성하는 과정에서 snprintf(name, sizeof(name), "R%d", i+1) 을 통해 name 배열에 “R1”, “R2”, “R3” 등의 이름을 순차적으로 저장했습니다. 그리고 setRobot 함수를 통해 각 robot의 name 필드에 이 name 배열의 주소를 저장하게 됩니다. 그런데 setRobot은 아래와 같이 동일한 name 배열을 가리키게 되므로 name 배열의 내용이 바뀌면, 모든 로봇의 이름이 마지막에 저장된 값으로 표시될 수밖에 없습니다.
void setRobot(struct robot* _robot, const char* name, int row, ...){ _robot->name = name; _robot->row = row; ... }
C
복사
[에러 해결]
각 로봇이 고유한 name을 유지하려면, 각 로봇의 name을 별도의 메모리 공간에 저장해야 됩니다. 이를 위해, strdup 함수를 사용하여 각 이름을 복제하고 로봇의 name 필드에 저장하면 됩니다. 하지만, 현재 개발 환경 버전에는 strdup 함수가 없어서 직접 구현했습니다.
char* my_strdup(const char* s) { if (s == NULL) return NULL; size_t size = strlen(s) + 1; char* new_str = malloc(size); if (new_str == NULL) return NULL; strlcpy(new_str, s, size); // strcpy가 아닌 strlcpy를 사용하여 버퍼 오버플로 방지하면서 문자열 복사 return new_str; }
C
복사
이 함수를 통해 각 로봇에게 고유한 name을 부여할 수 있었습니다.

[2] 스레드 생성 시, aux 값을 이상하게 부여해서 스레드 관리가 어려워짐

[에러 상황]
// robots thread 생성 tid_t* threads = malloc(sizeof(tid_t) * (n+1)); for (int i = 0; i < n; i++) { threads[i] = thread_create(robots[i].name, 0, &robots_thread, i); }
C
복사
스레드를 생성할 때, thread_create 함수에서 네 번째 인자는 thread를 인덱싱할 수 있는 번호입니다. 그래서 저는 robot의 인덱스와 똑같이 하기 위해서 i를 넣어서 스레드를 생성했습니다.
그런데 에러가 떠서 thread_create 함수 내부를 보니 인자가 포인터였습니다. 포인터 라는 부분만 보고 깊이 생각을 안 해서 바보처럼 i&i로 바꿨습니다.
그래서 robots_thread() 실행 함수에서 각 스레드의 index를 가져오는데 값이 엄청 이상한 값이 나왔습니다.
[에러 해결]
위의 에러를 해결하고자 아래와 같이 idx라는 포인터 배열을 사용했습니다. 이 포인터 배열을 동적 할당하고 각 요소는 해당 인덱스값으로 채운 뒤, 인덱스에 맞는 요소의 주솟값을 4번째 인자에 넣었습니다.
// robots thread 생성 tid_t* threads = malloc(sizeof(tid_t) * (n+1)); int* idx = malloc(sizeof(int) * (n+1)); for (int i = 0; i < n; i++) { idx[i] = i; threads[i] = thread_create(robots[i].name, 0, &robots_thread, &idx[i]); }
C
복사
스레드 실행 함수에서 int idx = *((int *)aux) 이런 식으로 값을 불러와 인덱스로 활용했습니다.

[3] block() & unblock() 함수 구현 중 cnt_thread 에서 무한 루프가 돌며 robots_thread로 넘어가질 않음

[에러 상황]
robot 스레드center 스레드 를 생성시킨 뒤, cnt_thread 에서 무한 루프가 돌면서 robots_thread로 넘어가질 않았습니다. 그래서 저는 처음에 block() & unblock() 두 함수를 잘못 구현한 줄 알고 이리저리 헤매다가 아래와 같이 심플하게 구현했습니다.
// aw_thread.c void block_thread(){ enum intr_level old_level; old_level = intr_disable (); list_push_back(&blocked_threads, &thread_current ()->elem); thread_block (); intr_set_level (old_level); }
C
복사
// aw_thread.c void unblock_threads(){ enum intr_level old_level; old_level = intr_disable (); while (!list_empty(&blocked_threads)){ struct thread *t = list_entry(list_pop_front(&blocked_threads), struct thread, elem); thread_unblock(t); } intr_set_level (old_level); }
C
복사
무한 루프가 돈 이유는 cnt_thread 함수가 실행된 이후, 이 함수의 실행을 막는 조건이 없었기 때문이었습니다.
[에러 해결]
처음에는 block 된 스레드들을 모은 리스트(blocked_threads)가 비었는지 알려주는 is_blocked_thread_empty()를 통해 blocked_threads가 비어있지 않으면 함수를 실행하도록 구현했습니다.
그런데 robots_thread 중에서 하나라도 block 처리가 되면 blocked_threads가 비어있지 않게 되므로 모든 robot 스레드가 다 실행되지 않았음에도 center 스레드로 실행이 넘어가 버려 순서가 꼬여버리는 에러가 발생했습니다.
그래서 모든 robot 스레드가 block이 되어야 center 스레드가 실행되도록 모든 스레드가 block이 되었는지를 알려주는 is_blocked_thread_full(n)을 만들어서 실행 조건문으로 활용해서 해결했습니다.
// aw_thread.c /// 모든 thread가 block되었는지 알려주는 함수 int is_blocked_thread_full(int n) { int size = list_size(&blocked_threads); return size == n; }
C
복사

결과

각 Robot들을 하나씩 제어하지 않고 동시에 제어(이동 및 대기)하면서 물건을 적재한 뒤, 하역 장소까지 완벽하게 아주 잘 도착합니다. 중간 과정의 이미지(왼쪽)와 결과 이미지(오른쪽)를 첨부했습니다.

Test Case #1. 5 2A:4C:2B:2C:3A

Test Case #2. 3 2A:4C:3B

Test Case #3. 4 6B:6B:6B:6B

배운 점

역대 프로젝트 중에서 가장 난이도가 있었지만 그만큼 비례해서 가장 재밌고 많이 배운 프로젝트였습니다.
Pintos 개발 환경을 설치하기 위해서 Linux 명령어, 도커 등과 친해졌으며 이론으로만 배웠던 스레드를 직접 생성하고 block을 처리한 뒤, 특정 작업을 수행시키고 다시 unblock을 처리하는 등의 과정을 통해 더 깊게 이해하게 되었습니다.
또한, C언어로 제대로 개발하는 것이 처음이었는데 확실히 메모리 공간을 직접 할당하고 해제하며 포인터를 당연하게 사용하는 개발 방식이 어렵게 느껴졌습니다. 생각해 보면, 디버깅의 80% 이상이 포인터와 동적 할당 관련된 것이었던 것 같습니다.
감사합니다.