ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • RCU (Read-Copy Update)
    개발/컴퓨터사이언스 2018. 10. 30. 21:41

    공유중인 값을 읽는 쓰레드와 업데이트하는 쓰레의 개수가 각각 하나인 경우에는 경우에는 spin_lock을 이용해도 별 어려움 없이 동기화 작업을 수행 할 수 있다. 그런데 값을 읽는 프로세스가 두개 이상이 될 때부터는 동기화 작업이 골치가 아파진다. 아래의 코드처럼 두개 이상의 쓰레드가 공유중인 val 값을 읽으려고 하고 한개의 쓰레드가 값을 업데이트 하고 있다고 해보자. 본래의 목적은 reader를 호출하는 thd[0]와 thd[1] 쓰레드가 writer를 호출하는 thd[2] 쓰레드가 업데이트 하는 값을 동일하게 읽는 것이었다.


    reader 두개를 writer보다 앞서서 생성한 덕분에 초반에는 어느 정도 동기화가 유지가 되겠지만 시간이 지나면서 쓰레드의 priority 값이 달라지게되고 어떤 기점에서 thd[2] 쓰레드가 thd[0]와 thd[1] 사이에 실행될 수도 있다. 이런 경우 thd[0]와 thd[1]은 같은 값을 읽지못하게 된다.


    pthread_spinlock_t lock;
    int val;
    
    void *reader() {
        int read;
        while (1) {
            pthread_spin_lock(&lock);
            read = val;
            printf("reader1: %d\n", val);
            pthread_spin_unlock(&lock);
        }
    }
    
    void *writer() {
        while(1) {
            pthread_spin_lock(&lock);
            val++;
            pthread_spin_unlock(&lock);
        }
    }
    
    int main() {
        pthread_t thd[3];
    
        pthread_spin_init(&lock, PTHREAD_PROCESS_SHARED);
        pthread_create(&thd[0], NULL, reader, NULL);
        pthread_create(&thd[1], NULL, reader, NULL);
        pthread_create(&thd[2], NULL, writer, NULL);
    
        pthread_join(thd[0], NULL);
        pthread_join(thd[1], NULL);
        pthread_join(thd[2], NULL);
    
        printf("DDD\n");
    
        return 1;
    }
    
    


    이런 경우를 위해 리눅스에서는 RCU(Read-Copy Update)라는 라이브러리를 제공한다. RCU를 사용하면 여러 개의 읽기 작업과 한 개의 쓰기 작업의 동기화를 구현 할 수 있다.  단, 여러 개의 쓰기 작업의 동기화는 지원이 안된다. LWN에서 소개한 예시코드로 간단히 사용 방법을 익혀보자.


     struct foo {
       struct list_head list;
       int a;
       int b;
       int c;
     };
     LIST_HEAD(head);
    
     /* . . . */
    
     p = kmalloc(sizeof(*p), GFP_KERNEL);
     p->a = 1;
     p->b = 2;
     p->c = 3;
     list_add_rcu(&p->list, &head);
    
    // Thread1
     rcu_read_lock();
     list_for_each_entry_rcu(p, head, list) {
       do_something_with(p->a, p->b, p->c);
     }
     rcu_read_unlock();
    
    // Thread2
     q = kmalloc(sizeof(*p), GFP_KERNEL);
     *q = *p;
     q->b = 2;
     q->c = 3;
     list_replace_rcu(&p->list, &q->list);
     synchronize_rcu();
     kfree(p);
    


    11~15 라인에서는 `foo`구조체 포인터로 선언된 `p`변수에 메모리 공간을 할당하고 값을 대입한 후 `list_add_rcu`라는 함수를 이용해 리스트에 넣는 작업이다. `list_head` 변수가 확장성이 좋아 RCU에서도 list를 이용해 여러 개의 변수를 관리할 수 있도록 했다.


    Thread1 은 읽는 작업을 수행하는 reader다. `p`값을 읽어오기 전에 `rcu_read_lock()` 함수를 호출해서 접근 권한을 얻고 작업을 수행한 다음에는 `rcu_read_unlock()`으로 lock을 풀어준다. 다른 쓰레드도 읽는 작업을 수행한다면 위와 동일한 코드를 탄다.


    Thread2는 rcu 리스트의 값을 업데이트하는 쓰레드다. `list_replace_rcu()` 함수는 첫번째 인자의 값을 두번째 인자의 값으로 바꾸는 함수다. 그리고 바로 아래 `synchronize_rcu()`는 다른 쓰레드에서 rcu 리스트의 값을 읽는 작업이 모두 종료될 때 까지 기다린 후 수정한 정보를 업데이트 한다. 이 함수가 호출되지 않으면 RCU 리스트 수정 함수를 호출해도 reader쪽에서 읽는 값은 동일하다. 이 함수를 이용해 읽는 작업과 쓰는 작업의 동기화를 맞춰 줄 수 있다.


    * 더 상세한 동작 메커니즘을 알고 싶으신 분은 LWN 을 참조하면 좋을 것 같다.

    '개발 > 컴퓨터사이언스' 카테고리의 다른 글

    프로스세와 스레드  (0) 2021.06.05
    스핀락, 뮤텍스, 세마포어  (0) 2018.11.07
    Cgroup (Control Group)  (0) 2018.09.15
    CPU pinning과 taskset  (0) 2018.08.27
    스핀락  (0) 2018.07.23

    댓글

Designed by Tistory.