[Pintos-KAIST] Project 1 :: Priority Scheduling

2023. 4. 25. 23:46ใ†Computer Science

728x90
๋ฐ˜์‘ํ˜•

 

Priority Scheduling

๐Ÿ’ก ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’์€ ์Šค๋ ˆ๋“œ๊ฐ€ ๋จผ์ € CPU๋ฅผ ์ ์œ ํ•  ์ˆ˜ ์žˆ๋„๋ก Priority Scheduling์„ ๊ตฌํ˜„ํ•œ๋‹ค.

 

Priority Scheduling ๊ตฌํ˜„์‚ฌํ•ญ

  • ์Šค๋ ˆ๋“œ๋“ค์€ ๊ฐ ์šฐ์„ ์ˆœ์œ„์— ๋”ฐ๋ผ ready list์— ์ถ”๊ฐ€๋œ๋‹ค.
  • ํ˜„์žฌ ์‹คํ–‰ ์ค‘์ธ ์Šค๋ ˆ๋“œ์˜ ์šฐ์„ ์ˆœ์œ„๋ณด๋‹ค ๋†’์€ ์šฐ์„ ์ˆœ์œ„์˜ ์Šค๋ ˆ๋“œ๊ฐ€ ready list์— ์ถ”๊ฐ€๋˜๋ฉด, ํ˜„์žฌ ์‹คํ–‰ ์ค‘์ธ ์Šค๋ ˆ๋“œ๋Š” ๋ฐ”๋กœ CPU๋ฅผ ์–‘๋„ํ•œ๋‹ค.
  • ์Šค๋ ˆ๋“œ๋Š” ์–ธ์ œ๋“ ์ง€ ์ž์‹ ์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค.
    • ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋‚ฎ์ถ”์–ด ๋”์ด์ƒ ๊ฐ€์žฅ ๋†’์€ ์šฐ์„ ์ˆœ์œ„๊ฐ€ ์•„๋‹ˆ๊ฒŒ ๋œ๋‹ค๋ฉด, ์ฆ‰์‹œ CPU๋ฅผ ์–‘๋„ํ•œ๋‹ค.
  • lock, semaphore, condition variable์„ ์‚ฌ์šฉํ•˜์—ฌ ๋Œ€๊ธฐํ•˜๊ณ  ์žˆ๋Š” ์Šค๋ ˆ๋“œ๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ์ธ ๊ฒฝ์šฐ, ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๊ฐ€์žฅ ๋†’์€ ์Šค๋ ˆ๋“œ๊ฐ€ ๋จผ์ € ๊นจ์–ด๋‚˜์•ผ ํ•œ๋‹ค.

 

๊ตฌํ˜„ํ•˜๊ธฐ

(1) ready_list ์ •๋ ฌ

thread_yield, thread_unblock

  • ready_list์— ์Šค๋ ˆ๋“œ๋ฅผ ์‚ฝ์ž…ํ•  ๋•Œ priority๊ฐ€ ๋†’์€ ์Šค๋ ˆ๋“œ๊ฐ€ ์•ž๋ถ€๋ถ„์— ์œ„์น˜ํ•˜๋„๋ก ์ •๋ ฌํ•œ๋‹ค.
    • ๊ธฐ์กด์—๋Š” list_push_back์„ ์‚ฌ์šฉํ•ด FIFO ๋ฐฉ์‹์œผ๋กœ ์‚ฝ์ž…๋˜๊ณ  ์žˆ์—ˆ๋‹ค.
    • list_insert_ordered๋ฅผ ํ™œ์šฉํ•œ๋‹ค.
    • ์ •๋ ฌ์— ํ™œ์šฉํ•  cmp_thread_priority() ํ•จ์ˆ˜๋Š” ์•„๋ž˜์—์„œ ์ƒˆ๋กœ ์„ ์–ธํ•œ๋‹ค.
/* thread.c */

void thread_yield(void)
{
    struct thread *curr = thread_current();
    enum intr_level old_level;
    ASSERT(!intr_context());
    old_level = intr_disable();

    if (curr != idle_thread)
        list_insert_ordered(&ready_list, &curr->elem, cmp_thread_priority, NULL);

    do_schedule(THREAD_READY);
    intr_set_level(old_level);
}
/* thread.c */

void thread_unblock(struct thread *t)
{
    enum intr_level old_level;
    ASSERT(is_thread(t));
    old_level = intr_disable();
    ASSERT(t->status == THREAD_BLOCKED);

    list_insert_ordered(&ready_list, &t->elem, cmp_thread_priority, NULL);

    t->status = THREAD_READY;
    intr_set_level(old_level);
}

cmp_thread_priority

  • ready_list์— priority๊ฐ€ ๋†’์€ ์Šค๋ ˆ๋“œ๊ฐ€ ์•ž๋ถ€๋ถ„์— ์œ„์น˜ํ•˜๋„๋ก ์ •๋ ฌํ•  ๋•Œ ์‚ฌ์šฉํ•  ์ •๋ ฌ ํ•จ์ˆ˜๋ฅผ ์ƒˆ๋กœ ์„ ์–ธํ•œ๋‹ค.
/* thread.h */

bool cmp_thread_priority(const struct list_elem *a, const struct list_elem *b, void *aux UNUSED);
/* thread.c */

bool cmp_thread_priority(const struct list_elem *a, const struct list_elem *b, void *aux UNUSED)
{
    struct thread *st_a = list_entry(a, struct thread, elem);
    struct thread *st_b = list_entry(b, struct thread, elem);
    return st_a->priority > st_b->priority;
}

 

(2) Preempt

thread_create, thread_wakeup

  • ready_list์— ์Šค๋ ˆ๋“œ๊ฐ€ ์‚ฝ์ž…๋˜๊ณ  ๋‚˜๋ฉด, ํ˜„์žฌ ์‹คํ–‰์ค‘์ธ ์Šค๋ ˆ๋“œ์™€ ready_list์— ์žˆ๋Š” ์Šค๋ ˆ๋“œ์˜ priority๋ฅผ ๋น„๊ตํ•œ๋‹ค.
  • priority๊ฐ€ ๋” ๋†’์€ ์Šค๋ ˆ๋“œ๊ฐ€ ready_list์— ์žˆ๋‹ค๋ฉด ์ฆ‰์‹œ CPU๋ฅผ ์–‘๋ณดํ•œ๋‹ค. ( preempt_priority() ํ˜ธ์ถœ )
    • ์–‘๋ณด ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๊ณ  ์–‘๋ณดํ•˜๋Š” preempt_priority() ํ•จ์ˆ˜๋Š” ์•„๋ž˜์—์„œ ์ƒˆ๋กœ ์„ ์–ธํ•œ๋‹ค.
/* thread.c */

tid_t thread_create(const char *name, int priority,
                    thread_func *function, void *aux)
{

...

    /* Add to run queue. */
    thread_unblock(t);
    preempt_priority();

    return tid;
}
/* thread.c */

void thread_wakeup(int64_t current_ticks)
{
    enum intr_level old_level;
    old_level = intr_disable(); // ์ธํ„ฐ๋ŸฝํŠธ ๋น„ํ™œ์„ฑ

    struct list_elem *curr_elem = list_begin(&sleep_list);
    while (curr_elem != list_end(&sleep_list))
    {
        struct thread *curr_thread = list_entry(curr_elem, struct thread, elem); // ํ˜„์žฌ ๊ฒ€์‚ฌ์ค‘์ธ elem์˜ ์Šค๋ ˆ๋“œ

        if (current_ticks >= curr_thread->wakeup_ticks) // ๊นฐ ์‹œ๊ฐ„์ด ๋์œผ๋ฉด
        {
            curr_elem = list_remove(curr_elem); // sleep_list์—์„œ ์ œ๊ฑฐ, curr_elem์—๋Š” ๋‹ค์Œ elem์ด ๋‹ด๊น€
            thread_unblock(curr_thread);        // ready_list๋กœ ์ด๋™
            preempt_priority();
        }
        else
            break;
    }
    intr_set_level(old_level); // ์ธํ„ฐ๋ŸฝํŠธ ์ƒํƒœ๋ฅผ ์›๋ž˜ ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ
}

 

thread_set_priority

  • ์‹คํ–‰์ค‘์ธ thread์˜ priority๋ฅผ ๋ณ€๊ฒฝ๋˜๋ฏ€๋กœ ready_list์— ์žˆ๋Š” ์Šค๋ ˆ๋“œ๋ณด๋‹ค priority์™€ ๋น„๊ตํ•˜์—ฌ ํ˜„์žฌ ๋ณ€๊ฒฝ๋œ priority๊ฐ€ ๋” ๋‚ฎ๋‹ค๋ฉด, ์ฆ‰์‹œ CPU๋ฅผ ์–‘๋ณดํ•œ๋‹ค. ( preempt_priority() ํ˜ธ์ถœ )
/* thread.c */

void thread_set_priority(int new_priority)
{
    thread_current()->init_priority = new_priority;
    preempt_priority();
}

 

preempt_priority

  • ready_list์— ์žˆ๋Š” ์Šค๋ ˆ๋“œ์˜ priority๊ฐ€ ํ˜„์žฌ ์‹คํ–‰์ค‘์ธ ์Šค๋ ˆ๋“œ์˜ priority๋ณด๋‹ค ๋†’์œผ๋ฉด ์–‘๋ณดํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ์ƒˆ๋กœ ์„ ์–ธํ•œ๋‹ค.
    • ready_list๋Š” priority๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ๋˜๋ฏ€๋กœ, ๊ฐ€์žฅ ์•ž์— ์žˆ๋Š” ์Šค๋ ˆ๋“œ๋งŒ ๊ฒ€์‚ฌํ•˜๋ฉด ๋œ๋‹ค.
/* thread.h */

void preempt_priority(void);
/* thread.c */

void preempt_priority(void)
{
    if (thread_current() == idle_thread)
        return;
    if (list_empty(&ready_list))
        return;
    struct thread *curr = thread_current();
    struct thread *ready = list_entry(list_front(&ready_list), struct thread, elem);
    if (curr->priority < ready->priority) // ready_list์— ํ˜„์žฌ ์‹คํ–‰์ค‘์ธ ์Šค๋ ˆ๋“œ๋ณด๋‹ค ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’์€ ์Šค๋ ˆ๋“œ๊ฐ€ ์žˆ์œผ๋ฉด
        thread_yield();
}

 

Test - Priority 1

/* Test: /threads์—์„œ ์ž…๋ ฅ */

make check

๐Ÿ‘‡๐Ÿป ๊ตฌํ˜„ ์ „

๐Ÿ‘‡๐Ÿป ๊ตฌํ˜„ ํ›„

priority ๊ด€๋ จ ํ…Œ์ŠคํŠธ 4๊ฐœ๋ฅผ ๋” ํ†ต๊ณผํ–ˆ๋‹ค!

 

(3) Lock Waiters ์ •๋ ฌ

  • lock, semaphore, condition variable์„ ๊ธฐ๋‹ค๋ฆฌ๋ฉฐ ๋Œ€๊ธฐํ•˜๊ณ  ์žˆ๋Š” ์Šค๋ ˆ๋“œ๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ์ธ ๊ฒฝ์šฐ, lock์„ ํš๋“ํ•  ์ˆ˜ ์žˆ์–ด์กŒ์„ ๋•Œ priority๊ฐ€ ๊ฐ€์žฅ ๋†’์€ ์Šค๋ ˆ๋“œ๊ฐ€ ๋จผ์ € ๊นจ์–ด๋‚˜์•ผ ํ•œ๋‹ค.
    • lock์˜ ๋Œ€๊ธฐ์ž ๋ชฉ๋ก์ธ waiters๋„ priority ๊ธฐ์ค€์œผ๋กœ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค.
  • ๐Ÿ’ก semaphore
    • ์„ธ๋งˆํฌ์–ด๋Š” ์–‘์˜ ์ •์ˆ˜๊ฐ’๊ณผ ๋‘ ๊ฐœ์˜ ์—ฐ์‚ฐ์ž(P์™€ V)๋กœ ๊ตฌ์„ฑ๋œ ๋™๊ธฐํ™” ๊ธฐ๋ฒ•์ด๋‹ค.
    • "Down" ๋˜๋Š” "P"
      • ์„ธ๋งˆํฌ์–ด๋ฅผ ํš๋“ํ•˜๊ธฐ ์œ„ํ•ด(๊ณต์œ  ์ž์›์— ์ ‘๊ทผํ•˜๋ ค ํ•  ๋•Œ) ํ˜ธ์ถœํ•œ๋‹ค.
      • ํš๋“ํ•˜๋ฉด ์„ธ๋งˆํฌ์–ด๋ฅผ ํš๋“ํ–ˆ๋‹ค๋Š” ์˜๋ฏธ๋กœ value๋ฅผ 1 ๊ฐ์†Œ์‹œํ‚จ๋‹ค.
      • ์„ธ๋งˆํฌ์–ด๋ฅผ ํš๋“ํ•  ๋•Œ๊นŒ์ง€(value๊ฐ€ ์–‘์ˆ˜๊ฐ€ ๋  ๋•Œ๊นŒ์ง€) ๊ธฐ๋‹ค๋ฆฐ๋‹ค.(๋ธ”๋ก๋œ๋‹ค.)
      • ์„ธ๋งˆํฌ์–ด๋ฅผ ๋ฐ”๋กœ ํš๋“ํ•  ์ˆ˜ ์—†์„ ๋•Œ๋Š” ์Šค๋ ˆ๋“œ๊ฐ€ ๊ธฐ๋‹ค๋ฆฌ๊ฒŒ ๋˜๋Š”๋ฐ(๋ธ”๋ก๋จ),
        ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋™์•ˆ(๋ธ”๋ก๋˜์–ด ์žˆ๋Š” ๋™์•ˆ)์—๋Š” ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ๊ฐ€ ์„ธ๋งˆํฌ์–ด๋ฅผ ๋จผ์ € ํ•ด์ œ(UP)ํ•˜๊ณ  ๋‚˜์„œ, ์ด๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์Šค๋ ˆ๋“œ๊ฐ€ ์‹คํ–‰๋˜๊ฒŒ ๋œ๋‹ค.
    • "UP" ๋˜๋Š” "V"
      • ์„ธ๋งˆํฌ์–ด๋ฅผ ๋ฐ˜ํ™˜ํ•  ๋•Œ(๊ณต์œ  ์ž์› ์‚ฌ์šฉ์„ ์™„๋ฃŒํ–ˆ์„ ๋•Œ) ํ˜ธ์ถœํ•œ๋‹ค.
      • ๋Œ€๊ธฐ ์ค‘์ธ ์Šค๋ ˆ๋“œ ์ค‘ ํ•˜๋‚˜๋ฅผ ๊นจ์›Œ์ฃผ๊ณ , ์„ธ๋งˆํฌ์–ด์˜ value๋ฅผ ๋ฐ˜ํ™˜ํ–ˆ๋‹ค๋Š” ์˜๋ฏธ๋กœ 1 ์ฆ๊ฐ€์‹œํ‚จ๋‹ค.

 

sema_down , cond_wait

  • waiters์— ์Šค๋ ˆ๋“œ๋ฅผ ์‚ฝ์ž…ํ•  ๋•Œ priority๊ฐ€ ๋†’์€ ์Šค๋ ˆ๋“œ๊ฐ€ ์•ž๋ถ€๋ถ„์— ์œ„์น˜ํ•˜๋„๋ก ์ •๋ ฌํ•œ๋‹ค.
    • list_insert_ordered๋ฅผ ํ™œ์šฉํ•œ๋‹ค.
    • sema→waiters๋ฅผ ์ •๋ ฌํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” cmp_thread_priority ํ•จ์ˆ˜๋Š” ready_list๋ฅผ ์ •๋ ฌํ•  ๋•Œ ์‚ฌ์šฉํ•œ ํ•จ์ˆ˜๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•œ๋‹ค.
    • cond→waiters๋ฅผ ์ •๋ ฌํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” cmp_sema_priority ํ•จ์ˆ˜๋Š” ์•„๋ž˜์—์„œ ์ƒˆ๋กœ ์„ ์–ธํ•œ๋‹ค.
/* synch.c */

void sema_down(struct semaphore *sema)
{
    enum intr_level old_level;

    ASSERT(sema != NULL);
    ASSERT(!intr_context());

    old_level = intr_disable();
    while (sema->value == 0) // ์„ธ๋งˆํฌ์–ด ๊ฐ’์ด 0์ธ ๊ฒฝ์šฐ, ์„ธ๋งˆํฌ์–ด ๊ฐ’์ด ์–‘์ˆ˜๊ฐ€ ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ
    {
        list_insert_ordered(&sema->waiters, &thread_current()->elem, cmp_thread_priority, NULL);
        thread_block(); // ์Šค๋ ˆ๋“œ๋Š” ๋Œ€๊ธฐ ์ƒํƒœ์— ๋“ค์–ด๊ฐ
    }
    sema->value--; // ์„ธ๋งˆํฌ์–ด ๊ฐ’์ด ์–‘์ˆ˜๊ฐ€ ๋˜๋ฉด, ์„ธ๋งˆํฌ์–ด ๊ฐ’์„ 1 ๊ฐ์†Œ
    intr_set_level(old_level);
}
/* synch.c */

void cond_wait(struct condition *cond, struct lock *lock)
{
    struct semaphore_elem waiter;

    ASSERT(cond != NULL);
    ASSERT(lock != NULL);
    ASSERT(!intr_context());
    ASSERT(lock_held_by_current_thread(lock));

    sema_init(&waiter.semaphore, 0);
    list_insert_ordered(&cond->waiters, &waiter.elem, cmp_sema_priority, NULL);
    lock_release(lock);
    sema_down(&waiter.semaphore);
    lock_acquire(lock);
}

 

cmp_sema_priority

  • cond→waiters๋ฅผ ์ •๋ ฌํ•  ๋•Œ ์‚ฌ์šฉํ•  ํ•จ์ˆ˜๋ฅผ ์ƒˆ๋กœ ์„ ์–ธํ•œ๋‹ค.
    • ์ธ์ž๋กœ ์ „๋‹ฌ๋˜๋Š” elem์œผ๋กœ ๋ฐ”๋กœ ์Šค๋ ˆ๋“œ์— ์ ‘๊ทผํ•  ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์—, ์ด์ „์˜ cmp_thread_priority๋ฅผ ์“ธ ์ˆ˜ ์—†์–ด์„œ ์ƒˆ๋กœ ์„ ์–ธํ•ด์•ผ ํ•œ๋‹ค!
  • ๋‘ semahore ์•ˆ์˜ 'waiters ์ค‘ ์ œ์ผ ๋†’์€ priority'๋ฅผ ๋น„๊ตํ•ด์„œ ๋†’์œผ๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜
/* thread.h */

bool cmp_sema_priority(const struct list_elem *a, const struct list_elem *b, void *aux);
/* synch.c */

bool cmp_sema_priority(const struct list_elem *a, const struct list_elem *b, void *aux UNUSED)
{
    struct semaphore_elem *sema_a = list_entry(a, struct semaphore_elem, elem);
    struct semaphore_elem *sema_b = list_entry(b, struct semaphore_elem, elem);

    struct list *waiters_a = &(sema_a->semaphore.waiters);
    struct list *waiters_b = &(sema_b->semaphore.waiters);

    struct thread *root_a = list_entry(list_begin(waiters_a), struct thread, elem);
    struct thread *root_b = list_entry(list_begin(waiters_b), struct thread, elem);

    return root_a->priority > root_b->priority;
}

 

sema_up , cond_signal

  • waiters์—์„œ ์Šค๋ ˆ๋“œ๋ฅผ ๊นจ์šฐ๊ธฐ ์ „์— waiters ๋ชฉ๋ก์„ ๋‹ค์‹œ ํ•œ ๋ฒˆ ์ •๋ ฌํ•œ๋‹ค.
    • waiters์— ๋“ค์–ด์žˆ๋Š” ์Šค๋ ˆ๋“œ๊ฐ€ donate๋ฅผ ๋ฐ›์•„ ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋‹ฌ๋ผ์กŒ์„ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
  • sema_up์—์„œ๋Š” unblock() ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋˜๋ฉด์„œ ready_list์— ์Šค๋ ˆ๋“œ๊ฐ€ ์‚ฝ์ž…๋˜๋ฏ€๋กœ, priority๊ฐ€ ๋” ๋†’์€ ์Šค๋ ˆ๋“œ๊ฐ€ ready_list์— ์žˆ๋‹ค๋ฉด ์ฆ‰์‹œ CPU๋ฅผ ์–‘๋ณดํ•œ๋‹ค. ( preempt_priority() ํ˜ธ์ถœ )
/* synch.c */

void sema_up(struct semaphore *sema)
{
    enum intr_level old_level;

    ASSERT(sema != NULL);

    old_level = intr_disable();
    if (!list_empty(&sema->waiters)) // ๋Œ€๊ธฐ ์ค‘์ธ ์Šค๋ ˆ๋“œ๋ฅผ ๊นจ์›€
    {
        // waiters์— ๋“ค์–ด์žˆ๋Š” ์Šค๋ ˆ๋“œ๊ฐ€ donate๋ฅผ ๋ฐ›์•„ ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋‹ฌ๋ผ์กŒ์„ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์žฌ์ •๋ ฌ
        list_sort(&sema->waiters, cmp_thread_priority, NULL);
        thread_unblock(list_entry(list_pop_front(&sema->waiters), struct thread, elem));
    }
    sema->value++;
    preempt_priority(); // unblock์ด ํ˜ธ์ถœ๋˜๋ฉฐ ready_list๊ฐ€ ์ˆ˜์ •๋˜์—ˆ์œผ๋ฏ€๋กœ ์„ ์  ์—ฌ๋ถ€ ํ™•์ธ
    intr_set_level(old_level);
}
/* synch.c */

void cond_signal(struct condition *cond, struct lock *lock UNUSED)
{
    ASSERT(cond != NULL);
    ASSERT(lock != NULL);
    ASSERT(!intr_context());
    ASSERT(lock_held_by_current_thread(lock));

    if (!list_empty(&cond->waiters))
    {
        list_sort(&cond->waiters, cmp_sema_priority, NULL);
        sema_up(&list_entry(list_pop_front(&cond->waiters),
                            struct semaphore_elem, elem)
                        ->semaphore);
    }
}

 

Test - Priority 2

/* Test: /threads์—์„œ ์ž…๋ ฅ */

make check

๐Ÿ‘‡๐Ÿป ๊ตฌํ˜„ ์ „

๐Ÿ‘‡๐Ÿป ๊ตฌํ˜„ ํ›„

priority - sema, condvar ํ…Œ์ŠคํŠธ 2๊ฐœ๋ฅผ ๋” ํ†ต๊ณผํ–ˆ๋‹ค!

728x90
๋ฐ˜์‘ํ˜•