Subject 번역으로 시작
Here are the things you need to know if you want to succeed this assignment:
• One or more philosophers sit at a round table.
There is a large bowl of spaghetti in the middle of the table.
이 과제를 성공적으로 수행하기 위해 알아야 할 사항은 다음과 같습니다.
• 한 명 이상의 철학자가 원탁에 앉습니다.
테이블 중앙에는 커다란 스파게티 그릇이 있습니다.
• The philosophers alternatively eat, think, or sleep.
While they are eating, they are not thinking nor sleeping;
while thinking, they are not eating nor sleeping;
and, of course, while sleeping, they are not eating nor thinking.
• 철학자들은 교대로 먹거나 생각하거나 잠을 잔다.
먹는 동안에는 생각하지도 않고 잠자지도 않습니다.
생각하는 동안 그들은 먹지도 자지도 않습니다.
물론 잠자는 동안에는 먹지도 생각하지도 않습니다.
• There are also forks on the table. There are as many forks as philosophers.
• 테이블에 포크도 있습니다. 철학자만큼 많은 포크가 있습니다.
• Because serving and eating spaghetti with only one fork is very inconvenient, a philosopher takes their right and their left forks to eat, one in each hand.
• 스파게티면을 포크 하나만으로 서빙하고 먹는 것은 매우 불편하기 때문에 철학자는 좌우 포크를 한 손에 하나씩 들고 먹는다.
• When a philosopher has finished eating, they put their forks back on the table and start sleeping. Once awake, they start thinking again. The simulation stops when a philosopher dies of starvation.
• 철학자는 식사를 마치면 포크를 다시 테이블 위에 놓고 잠을 잡니다. 깨어나면 다시 생각하기 시작합니다. 철학자가 굶어 죽으면 시뮬레이션이 중지됩니다.
• Every philosopher needs to eat and should never starve.
• Philosophers don’t speak with each other.
• Philosophers don’t know if another philosopher is about to die.
• No need to say that philosophers should avoid dying!
• 모든 철학자는 먹어야 하며 절대 굶어서는 안 됩니다.
• 철학자들은 서로 이야기하지 않습니다.
• 철학자는 다른 철학자가 곧 죽을지 모릅니다.
• 철학자는 죽음을 피해야 한다고 말할 필요가 없습니다!
You have to write a program for the mandatory part and another one for the bonus part (if you decide to do the bonus part). They both have to comply with the following rules:
• Global variables are forbidden!
• Your(s) program(s) should take the following arguments:
필수 부분에 대한 프로그램을 작성하고 보너스 부분에 대해 다른 프로그램을 작성해야 합니다
(보너스 부분을 수행하기로 결정한 경우). 둘 다 다음 규칙을 준수해야 합니다.
• 전역 변수는 금지되어 있습니다!
• 프로그램은 다음 인수를 취해야 합니다.
◦ number_of_philosophers: The number of philosophers and also the number of forks.
◦ number_of_philosophers: 철학자의 수와 포크의 수.
◦ time_to_die (in milliseconds): If a philosopher didn’t start eating time_to_die
milliseconds since the beginning of their last meal or the beginning of the simulation, they die.
◦ time_to_die(milliseconds): 철학자가 time_to_die를 먹기 시작하지 않은 경우
마지막 식사가 시작된 후 또는 시뮬레이션이 시작된 후 밀리초 후에 그들은 죽습니다.
◦ time_to_eat (in milliseconds): The time it takes for a philosopher to eat. During that time, they will need to hold two forks.
◦ time_to_eat(milliseconds): 철학자가 식사하는 데 걸리는 시간. 그 시간 동안 두 개의 포크를 잡아야 합니다.
◦ time_to_sleep (in milliseconds): The time a philosopher will spend sleeping.
◦ time_to_sleep(milliseconds): 철학자가 잠을 자는 시간.
◦ number_of_times_each_philosopher_must_eat (optional argument): If all philosophers have eaten at least number_of_times_each_philosopher_must_eat times, the simulation stops. If not specified, the simulation stops when a philosopher dies.
◦ number_of_times_each_philosopher_must_eat(선택 인수):
모든 철학자가 최소한 number_of_times_each_philosopher_must_eat 시간을 먹었으면
시뮬레이션이 중지됩니다. 지정하지 않으면 철학자가 죽을 때 시뮬레이션이 중지됩니다.
• Each philosopher has a number ranging from 1 to number_of_philosophers.
• Philosopher number 1 sits next to philosopher number number_of_philosophers. Any other philosopher number N sits between philosopher number N - 1 and philosopher number N + 1.
• 각 철학자는 1에서 number_of_philosophers까지의 숫자를 갖습니다.
• 철학자 번호 1은 철학자 번호 number_of_philosophers 옆에 있습니다.
다른 철학자 번호 N은 철학자 번호 N - 1과 철학자 번호 N + 1 사이에 있습니다.
About the logs of your program:
• Any state change of a philosopher must be formatted as follows:
◦ timestamp_in_ms X has taken a fork
◦ timestamp_in_ms X is eating
◦ timestamp_in_ms X is sleeping
◦ timestamp_in_ms X is thinking
◦ timestamp_in_ms X died
Replace timestamp_in_ms with the current timestamp in milliseconds and X with the philosopher number.
• A displayed state message should not be mixed up with another message.
• A message announcing a philosopher died should be displayed no more than 10 ms
after the actual death of the philosopher.
• Again, philosophers should avoid dying!
프로그램 로그 정보:
• 철학자의 상태 변경은 다음과 같이 형식화되어야 합니다.
◦ timestamp_in_ms X가 포크를 취했습니다.
◦ timestamp_in_ms X가 먹고 있음
◦ timestamp_in_ms X가 대기 중입니다.
◦ timestamp_in_ms X가 생각 중입니다.
◦ timestamp_in_ms X 사망
timestamp_in_ms를 현재 타임스탬프(밀리초)로 바꾸고 X를 철학자 번호로 바꿉니다.
• 표시된 상태 메시지는 다른 메시지와 혼동되어서는 안 됩니다.
• 철학자가 사망했음을 알리는 메시지는 철학자가 실제 사망한 후 10ms 이내에 표시되어야 합니다.
• 다시 말하지만, 철학자는 죽음을 피해야 합니다!
** Your program must not have any data races **
** 프로그램에 데이터 경합이 없어야 합니다 **
필수 부분에 대한 특정 규칙은 다음과 같습니다.
• 각 철학자는 스레드여야 합니다.
• 각 철학자 사이에는 하나의 포크가 있습니다. 따라서 여러 철학자가 있는 경우 각 철학자는 왼쪽에 포크가 있고 오른쪽에 포크가 있습니다. 철학자가 한 명뿐이라면 식탁에는 포크가 하나만 있어야 합니다.
• 철학자가 포크를 복제하는 것을 방지하려면 각 포크에 대한 뮤텍스로 포크 상태를 보호해야 합니다.
보너스 부분의 프로그램은 필수 프로그램과 동일한 인수를 사용합니다. 글로벌 규칙 장의 요구 사항을 준수해야 합니다.
보너스 부분에 대한 구체적인 규칙은 다음과 같습니다.
• 모든 포크는 테이블 중앙에 위치합니다.
• 메모리에는 상태가 없지만 사용 가능한 포크의 수는 세마포어로 표시됩니다.
• 각 철학자는 과정이어야 합니다. 그러나 주요 프로세스는 철학자가되어서는 안됩니다.
⚠︎ 필수 부분이 PERFECT인 경우에만 보너스 부분이 평가됩니다. 완벽하다는 것은 필수 부분이 완벽하게 수행되어 오작동 없이 작동한다는 의미입니다. 모든 필수 요구 사항을 통과하지 못한 경우 보너스 부분은 전혀 평가되지 않습니다. ⚠︎
Submission and peer-evaluation Turn in your assignment in your Git repository as usual. Only the work inside your repository will be evaluated during the defense. Don’t hesitate to double check the names of your files to ensure they are correct.
Mandatory part directory: philo/
Bonus part directory: philo_bonus/
평소와 같이 Git 리포지토리에서 할당을 제출합니다. 방어 중에는 저장소 내부의 작업만 평가됩니다. 주저하지 말고 파일 이름이 올바른지 다시 확인하십시오.
필수 부품 디렉토리: philo/
보너스 파트 디렉토리: philo_bonus/
(수정) 함수에 대해서만 알아서 필요한 것만 공부를 하려 했지만, 같이 공부하는 사람이 생기다보니 좀 더 공부를 해보자는 차원에서 정리를 추가적으로 하려고 한다.
서브젝트에서 사용하기에 알아야 하는 pthread(POSIX thread의 줄임말로 스레드를 편하게 만들수 있게 도와주는 API)
우선 pthread를 사용하기 전에 프로세스와 스레드에 대해서 이야기를 해보자
Process(프로세스)
프로그램을 구동하여 프로그램 자체와 프로그램의 상태가 메모리 상에서 실행되는 작업 단위
(하드디스크에 있는 프로그램을 실행하면, 실행을 위해서 메모리 할당이 이루어지고, 할당된 메모리 공간으로 바이너리 코드가 올라가게 된다. 이 순간부터 프로세스이다)
Code 영역 ㅣ 프로그램을 실행을 위한 코드 영역
Date 영역 ㅣ 초기화된 데이터 (모든 전역 및 정적 변수) / 초기화되지 않은 데이터
Heap 영역 ㅣ 동적할당을 위한 메모리 영역
Stack 영역 ㅣ 지역변수, 함수 호출시 전달되는 파라미터를 위한 영역
CPU가 하나인데, 동시에 실행되어야 할 프로세스가 여러 개라면, 기준에 의해서 순차적으로 실행된다.
프로세스의 여러 상태의 프로세스가 존재할 수 있지만, 싱글코어 CPU에서는 running 상태의 프로세스 하나만 존재한다.
프로세스 내에서 실행되는 작업의 흐름, 단위인 Thread
프로세스 내부에는 적어도 하나의 스레드가 존재, 여러 개가 존재할 때는 Multitread(멀티스레드)라 불린다.
멀티스레드에서 각 스레드는 Stack 형식으로 할당된 메모리 영역은 따로 할당받고 Code/Data/Heap 형식으로 할당된 메모리 영역을 공유한다. 일정 메모리 영역을 공유하기 때문에 동일한 프로세스 내부의 스레드 간 context switching(문맥 전환)할 때, 프로세스끼리 문맥전환할 때보다 빠른데, 상대적으로 스위칭 해야 할 메모리 영역이 적기 때문이다.
스레드 하나가 프로세스 내 자원을 망쳐버린다면 모든 프로세스가 종료될 수 있고 자원을 공유하기 때문에 필연적으로 동기화 문제가 발생할 수 있다.
이러한 것을 방지하기 위해서 Mutex
Mutual Exclusion의 약자로 상호배제라고 하며 자원에 대해 통제권을 가지고 특정 쓰레드 단독으로 들어갈 수 있게하는 동기화 기법
critical section(임계영역이라고, 공유되는 자원을 동시접근하게 되는 문제를 발생하지 않도록 독점할 수 있게 보장해줘야 하는 영역)을 보호하기 위해서 사용되는 도구로 다수의 스레드(또는 프로세스)들의 공유 리소스에 대한 접근을 리소스를 "locking" 과 "unlocking"을 통해 조율한다.
비슷한 방법의 Semaphore
옛날에는 기찻길에서 교차지점이 있고, 그 부분에서 깃발(Semaphore)을 이용하여 이용 가능 여부를 확인 할 수 있었다.
위와 설명된 거 같이 기찻길의 교차지점의 Critical section에서 공유된 데이터의 갯수를 말하는데, binary semaphore라고 공유 자원이 한 개만 있는 경우와, 공유 자원이 여러개인 counting semaphore가 있다.
signal()과 wait()로 signaling mechanism이다.
두 개의 차이점은
/* 구글 한글 번역 */
Philosophers Problem
Mutex- deadlock
- starvation
1. each fork with semaphore, philosopher acquires fork by excuting a wait() and releases fork by excuting a signal()
2. 한 개의 포크만 들고 있으면서 포크를 놓지 않아서 죽게 되는 상황
3. 먹는 철학자만 먹으면서 기아가 발생되는 상황
for the design of thread-safe concurrent
#include <sys/time.h>
int gettimeofday(struct timeval *restrict tp, void *restrict tzp);
struct timeval {
time_t tv_sec; /* seconds since Jan. 1, 1970 */
suseconds_t tv_usec; /* and microseconds */
};
struct timeval time;
gettimeofday(&time, NULL);
1 sec = 1000ms;
1 usec(microseconds) = 0.0001ms;
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg)
: 스레드 생성
- 스레드 식별자
- 스레드 특성을 지정하기 위하여 이용하는데, 보통은 NULL을 입력
- thread가 실행되었을때 시작할 스레드 함수 이름
- 스레드가 분기할 함수에 보낼 입력 파라미터
쓰레드를 pthread_create() 를 사용하여 생성하면, 쓰레드가 종료되더라도 사용했던 모든 자원이 해제되지 않는다.
int pthread_join(pthread_t thread, void **value_ptr)
: 분기 시킨 스레드들의 종료를 기다림 Causes the calling thread to wait for the termination of the specified thread.
- 스레드 식별자
- 리턴 값
종료될때까지 기다렸다가 종료시점이 되면, 자원이 반납
int pthread_detach(pthread_t thread)
: 스레드 삭제 Marks a thread for deletion.
pthread_join()을 사용하지 않더라도, 쓰레드 종료될때 모든 자원을 해제
리턴값: 성공하면 0, 실패하면 에러코드 리턴
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr)
: mutex 초기화 Initialize a mutex with specified attributes.
- 초기화시킬 mutex 객체
- 기본적으로 NULL, "fast", "recurisev", "error checking"의 3가지 종류중 하나를 선택 가능
int pthread_mutex_lock(pthread_mutex_t *mutex) : critical section 시작
int pthread_mutex_unlock(pthread_mutex_t *mutex) : critical section 종료
int pthread_mutex_destroy(pthread_mutex_t *mutex) : destroy mutex
참고하면서 보았던 사이트들
https://www.geeksforgeeks.org/mutex-lock-for-linux-thread-synchronization/
https://linuxhint.com/pthread-join-multiple-threads-example/
https://linuxhint.com/pthread_detach-function-c/
https://www.ibm.com/docs/en/zos/2.4.0?topic=functions-pthread-detach-detach-thread
https://www.ibm.com/docs/en/zos/2.4.0?topic=functions-pthread-join-wait-thread-end#ptjoin
파일을 실행할 때, 어떻게 구현이 되어야 하는지 먼저 알아보자
/philo 5 310 100 100 2 으로 실행
- 철학자는 5명
- 310ms안에 먹지않으면 죽음
- 100ms동안 스파게티 흡입
- 100ms동안 잠
- 모든 철학자가 2번 식사를 했다면 프로그램 종료
관련 정보를 읽다보면 운영체제 관련해서 너무 깊게 가는 것 같은 느낌에 졸음이 오는 14시 42분이였다. 일단 대략적으로 스레드와 mutex 역할은 알게 되었으니, 코드를 통해서 이해를 해보자
공통으로 가지고 있는 자원을 가지고 cnt를 공유하는 과정인데, 실행을 하면 어떻게 될까?
생각대로라면 thread1 - g_cnt가 0이 되었다가 다시 12까지 출력을 하고 thread2가 이것을 반복 되어야 하지만!!!
보다시피 cnt는 숫자가 늘었다가 다시 줄기도 하고 난장판이다
그럼 mutex를 이용해서 누군가 사용중인 거라면 기다리게 만들어 보고자 한다.
거기서 사용하는 것이 서브젝트에서 언급된 pthread_mutex_lock, pthread_mutex_unlock 이다.
mutex를 이용하게 되면 이러한 방법으로 순차적으로 숫자를 출력할 수 있게 된다.
함수를 써보니 어느정도 감이 잡히게 되는데, 이 글을 보는 카뎃?은 어떨지 모르겠다.
그래도 감이 안온다면 동료학습이 역시 최고가 아닐까 싶다.
멘덴토리에 나온 것과 같은 형식으로 출력이 되어야 하며, 전역변수가 없어야 한다.
공유 데이터는 항상 단일 뮤텍스를 통해 액세스해야 합니다.
mutex 범위는 모두에게 표시되어야 합니다.
글로벌 잠금 명령
- 모든 스레드는 뮤텍스 명령에 의해서 잠급니다.
항상 뮤텍스 잠금 해제
- 항상 올바른 뮤텍스를 잠금 해제하십시오.
코드를 짤 때, 크게 두 분류로 나누는 데 detach를 쓸 것인가 join을 쓸 것인가
두 가지 다 사용을 해봤을 때, 둘 다 느낌이 달랐다. 애정이 가는 join 으로 제출을 하겠지만 두 가지에 대해서 설명을 하고자 한다
어떻게 진행이 되고 어떤 방법으로 해결할 지는 해결방법을 생각해서 본인만의 생각을 넣어보기 좋은 과제인 거 같다
우선 두 개의 코드는 동일한 구조가 있다.
철학자의 루틴
- 생각하고 포크를 양손으로 들고 먹고 소화시키고 포크를 내려놓고 자고
- 종료 조건을 만족할 때까지 계속
루틴의 내용
- thinking - 특별한 거 없이 과제에서 요구한 거에 맞춰서 출력
- pickup porks - 양쪽에 있는 공유자원인 포크를 드는 행위
- eating - 밥을 먹었으니 죽을 시간을 갱신, 먹은 횟수 체크, 포크를 내려놓는 행위
- sleep - 잠
포크를 철학자의 인원에 맞춰서 mutex를 생성하여 위의 루틴을 만족을 시키면 먹는 것은 잘 먹는다.
루틴의 내용 2번에서 포크 하나하나 mutex_lock을 해줘서 다른 사람이 접근 하지 않게 해놓고
3번도 요구하는 것에 맞춰서 필요한 데이터 갱신 및 포크를 mutex_unlock을 해주면 된다.
하면서 고민을 많이 했던 부분은 죽을 때, 10ms 안에 죽었다는 메세지와 함께 종료가 되어야 하는데
누군가가 죽었다는 메세지를 띄워도 다른 스레드는 돌고 있어서 다른 행동이 표시가 되는 문제가 발생
즉, 철학자 한 명이 죽었을 때 다른 철학자는 어떠한 행동을 하지 않은 채 제대로 종료가 되어야 한다.
다른 문제의 경우는 출력하는 함수를 실행 시간 ms 와 철학자 본인의 번호 그리고 함수에서 입력받은 행동에 맞춰 출력을 두 개로 나뉘다 보니 가끔 출력하는 부분에서 중복이 되면서 밀리는 경우가 있었다.
이러한 해결을 위해서는 첫번째는 관리자로서 thread를 놓았고 두번째는 mutex를 이용하여 한 명씩 접근할 수 있도록 하였다.
그리고 detach와 join으로 구현을 해 본 결과, detach는 mutex를 이용해서 종료 되지 않게 막고 있다가 종료 시점이 되면 unlock 을 통해서 종료가 원활히 될 수 있도록 하고 join 같은 경우는 각 함수에서 종료가 되었는 걸 확인하기 위해 루틴의 함수마다 종료가 되었는지 체크하는 것을 만들 게 되었다.
detach와 join의 이해가 제대로 되지 않았었기에, 두 개의 차이를 엄청 알아봤었고 코드를 계속 고치면서 확인을 하였는데 비슷한 듯 다른 느낌이 들었다. (물론 아직 제대로 알지 못하고 오로지 과제 해결만 했을 수 있음)
detach를 사용할 때에는 main mutex를 두어서 실행과 동시에 lock을 시켜놓고 free 해주기 전에 lock을 걸어놓아서 빠져 나가지 못하게 막아놓은 상태로 각 철학자마자 내부에 스레드를 두어 종료 시점이 오면 출력 및 먹는 것을 mutex_lock으로 중지를 시키고 main mutex_unlock을 하여 종료가 될 수 있게 하는 방법
join을 사용할 때에는 관리자로서 하나의 스레드가 모든 철학자의 상황을 지켜보면서 종료 시점이 되면 flag를 1로 만들고 각 철학자들은 루틴을 접근할 때마다 종료가 되어야 하는 지 파악을 하여 반복문을 빠져 나올 수 있게 하는 방법
이렇게 두 가지로 제작을 하게 되었다.
이후에도 사실 디테일한 부분이 있는데, 그 부분은 usleep 혹은 시간을 조금만 조정하면 해결이 되는 거 같다.
(아직 제출하기 전 완성이라 생각하고 쓰는 부분)
여러 번 갈아엎은 결과, 위의 부분으로도 통과를 할 수 있을 거 같긴 하다. (이유는 detach로는 터트릴 방법이 있을 것만 같다...)
위의 방법 중에서 최대한 철학자를 살리기 위한 방법이 무엇이 있을까?
과제를 같이 진행하는 @juhur, @alee, @jim, @min-jo 그리고 @hena 등의 카뎃과 추가적으로 무엇을 더 해야 되는지 고민하게 되었는데, 그 중에서도 갈아 엎은 이유 등을 이야기 하고자 한다.
- 평가지에 철학자 200명도 넣으라고 한다는데?
- 마지막 옵션에서 0을 주면 어떻게 돼?
- 철학자가 1명이라면?
3번부터 이야기를 하면, 철학자는 포크가 1개밖에 없기 때문에,
포크를 한 개를 집었지만 시간이 지나면 죽어야 한다.
그리고 2번은 사람마다 다르긴 했는데, 마지막 옵션에서 0개의 경우는 인자의 에러로 처리했다. 사실상 0개는 먹지말고 죽으라고 하는데, 사람마다 생각하는 것이 다르기에 에러로 처리를 해도 결국 동일한 결과라고 생각했기에 인자받을 때 에러 처리를 하였다.
그리고 대망의 1번 !! 사실 200명에서 왜 실패를 하는 것인가? 스레드가 많아지면서, 시간의 딜레이가 발생한다. 그렇기 때문에 50명을 주어도 터지는 사람이 있을 것이고 서브젝트에 적혀진 최대한 살리라는 말 때문에 변명을 하게 될 것이다.
그럼에도 불구하고 이것을 해결하고자 여러 가지 시도를 하게되었는데, 조건들이 비슷하게 잡혀갔다.
홀수번째의 철학자는 잠깐 쉬어주고, 홀수번째는 왼쪽 포크부터 쥐는 것이 더 철학자를 많이 살릴 수 있다는 것을 왜 그런지 그림을 그려가면서 이유를 알게되었다.
홀수번째의 철학자만 먼저 쉬게 된다면, 홀짝홀짝 진행하게 되면서 포크가 한개씩 쥐고 있는 상황을 덜 만들게 되는 것
홀수번째는 왼쪽 포크만 주어지는 것은 상황을 그려보면 같은 포크를 드는 것보다는 좀 더 많은 철학자가 먹을 수 있는 것을 확인할 수 있게 된다. 이렇게 되면 포크를 한 개씩 쥐는 상황보다는 홀/짝 순차적으로 먹은 상황에서 옆사람과 같은 걸 경쟁하면서 포크를 한개씩만 쥐는 상황보다는 누군가가 먼저 양쪽을 먼저 잡는 경우가 많게 된다?
두 가지가 겹쳐지고, 포크를 내려놓는 것에서도 mutex_unlock을 늦게 집은 것부터 진행하면 문제가 발생하는 것을 보았다.
그리고 200명을 최대한 살리는 방법에는 크롬, vscode등 다른 프로그램을 끄고 철학자를 넣어서 실험하면 더 오래 살 수 있게 된다. 컴퓨터의 상황에 따라서 많이 달라지는 것을 볼 수 있다.
분명 죽었는데, 반복문이 돌게 되는 상황도 있어서 갈아엎고 새로 짰는데, 포크를 내려놓는 것과 시간 설정의 차이로 문제가 발생하는 경우가 있었다. usleep 또한 정확하게 재울 수 있는 함수가 아니기에 새로 psleep으로 시간을 체크해서 재울 수 있는 방법도 해주고 뭔가 다 된 듯 되지 않은 상황을 겪으면서 멘탈이 많이 나갔었다...
이러한 방법보다 철학자가 많이 들어오더라도 많이 살릴 수 있는 방법과 detach로 실행을 할 때 문제를 잡아서 활용하는 방법이 뭘까?
궁금하지만 궁금증으로 남기려고 한다.
'42 Seoul' 카테고리의 다른 글
[42Seoul/특강] 멘토 차경묵(@Hannal), 프론트엔드 개발자 김승하 (4) | 2022.05.13 |
---|---|
[42seoul] 용량 부족하니 다 밀어보자 (0) | 2022.04.22 |
[42Seoul/push_swap] 최적화는 어떻게 하는데요? (0) | 2022.04.04 |
[42Seoul/leaks 사용] leak 왜 터지는지 아직도 몰라? (0) | 2022.04.04 |
[42Seoul/born2beroot] 블랙홀 살려줘어어 ~ (1) | 2022.03.21 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!