ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Linux] semaphore
    LinuxProgramming 2023. 11. 25. 22:58

    [semaphore]

     
    세마포어는 멀티프로그래밍 환경에서 공유자원에 대한 접근 제어를 하는 방식으로

    1개의 공유자원에 제한된 개수의 process 또는 thread만 접근할 수 있도록 만드는 교착상태에 대한 해결방법임.
     
    unix/linux 에서의 세마포어는 원자적으로 제어되는 정수변수(sem)으로,
    semaphore 값이 0이면 대기해야하고(wait), semaphore 값이 >0 이면 접근가능하다(key--)

     

    0보다 클때 : 접근함과 동시에 sem 값 - 1
    0 일때 : 접근할 수 있을때까지 wait
    종료하고 나갈때 : 나가면서 sem값 + 1 하여 다른 프로세스가 접근할 수 있도록 함

     

    이 sem값을 제어함으로써 critical section을 구현할 수 있다.

     

    만약 초기 key값이 10이고 특정 프로세스가 공유자원에 접근할수록 semop에서 -1을 해주고 접근해제할때 +1을 해준다고 치면,

    이 critical section은 10명이 동시에 실행가능하다는 것이다.

     

    그래서 만약 딱 한 프로세스만 critical section을 실행할 수 있도록 하려면 sem 초기값을 1로 잡아주면 된다.

     


    [semun]

     
    semctl할때만 쓰이는 구조체세마포어 집합을 초기화할때, 집합에 대한 정보를 얻어올때 사용된다.

     
    [1. 해당 세마포어 집합에 세마포어가 한 개밖에 없어서 한 개의 세마포어만 초기화할때]

     

    그냥 arg.val = 1; 이런 식으로 val에 하나의 정수만 지정해주고 SETVAL을 통해 처리할 수 있다.

    arg.val = 1;
    semctl(semid, 0, SETVAL, arg);

     
    [2. 세마포어 집합에 세마포어 개수가 >= 2]

     

    arg.array에 배열을 연결시켜주고 SETALL 함수를 이용함으로써 처리할 수 있다.

    ushort buf[2] = {1, 1};
    
    arg.array = buf;
    semctl(semid, 0, SETALL, arg);

     

    [3. 해당 세마포어 집합의 정보를 불러올때]

    union semun arg;
    
    semctl(semid, 0, IPC_STAT, arg);

     


    [sembuf]

     
    sembuf is used to define each individual semaphore operation.
    container for the parameters needed to perform semaphore operations using semop system call

    struct sembuf {
        unsigned short sem_num;  // 세마포어 집합 내의 특정 세마포어 번호
        short          sem_op;  // 수행할 연산 (증가, 감소, 대기 등)
        short          sem_flg; // 연산 플래그 (IPC_NOWAIT, SEM_UNDO 등)
    };


    semop에서만 쓰이는 구조체semaphore operation에 대한 정보를 나타내는 구조체이다.

    몇 번째 semaphore에 어떤 operation을 할건지 정보를 저장하는 구조체이다.

     

    얘는 union semun 처럼 따로 구조체를 정의해줄 필요가 없다.

    그냥 헤더파일에 있는 자료형이라 불러다 쓰면 된다.

     

    이때 sem_flg는 무조건 0으로 명시적으로 초기화 시켜줘야한다.

    sem_num : 연산을 적용할 대상 세마포어의 idx
    sem_op : 세마포어에서 수행할 연산
    sem_flg : 플래그 (보통 0, IPC_NOWAIT, SEM_UNDO)

     


    [sembuf 초기화 방법]

     

    1. 노가다로 초기화해준 다음 semop 한테 넘겨주기

    struct sembuf p_buf;
    
    p_buf.sem_num = 0;
    p_buf.sem_op = -1;
    p_buf.sem_flg = 0;
    
    semop(semid, &p_buf, 1);

     

     

    2. 선언과 동시에 한번에 초기화하는 방식으로 semop 한테 넘겨주기

    struct sembuf p_buf[2] = {{0, -1, 0}, {1, 1, 0}};
    
    semop(semid, &p_buf, 1);

    [sembuf 인자 설명]

     

    1. sem_num

     

    2. sem_op

    보통 0, -1, 1 을 사용하지만 다양한 정수값을 가질 수 있음

     

    0

    세마포어 값이 0이 될때까지 대기(WAIT)

    이 연산은 값이 0이 아니면 프로세스를 BLOCK 상태로 만듬

     

    양수(1)

    세마포어 값을 증가

    리소스를 반환할때 사용

    사용 가능한 세마포어 개수를 증가시킨다는 의미

     

    음수(-1)

    세마포어 값을 감소

    리소스를 사용할때 사용

    사용 가능한 세마포어 개수를 감소시킨다는 의미

     

    3. sem_flg

     

    0 (default 기본 설정)

    세마포어 연산은 BLOCKING 모드로 동작

     

    IPC_NOWAIT

    세마포어 연산이 대기 없이 바로 반환되도록 설정

    만약 sem_op = -1과 같이 감소하려는데 세마포어 값이 충분하지 않다면, (원래 대기열로 들어가는데??) 이걸 사용하면 프로세스는 블록 상태로 대기하지 않고 즉시 오류를 반환합니다.

     

    SEM_UNDO

    프로세스가 종료되면 세마포어에 대해 수행한 모든 연산을 자동으로 되돌림

    예를 들어, sem_op = -1로 세마포어 값을 감소시켰다면, 해당 프로세스가 비정상적으로 종료되더라도

    감소한 값을 원래대로 복구함

    사용목적 : 프로세스가 비정상 종료되어 동기화 상태를 깨뜨리는 문제를 방지

     


    [semget]

    1. key_t key
    key값은 한 개의 semaphore set(세마포어 집합)의 key 값이다
    key는 msgqueue에서 쓰던 ftok로 생성하면 된다.
    msgqueue와 semaphore 모두 IPC 객체에 대한 key값을 받아오는 작업과정은 똑같다
     
    2. int nsems
    한 개의 semaphore set에 몇 개의 semaphore를 생성할 것인지
    내가 어떤 문제를 semaphore를 통해 해결하려고 할때 세마포어가 단 하나를 필요하는 경우보다는 여러개의 세마포어가 필요한 경우가 월등히 많다는 소리다. 그래서 이런 기능을 제공한다.
    (참고로 하나의 세마포어 set의 각각의 세마포어는 독립적인 세마포어들이다)
     
    3. flags에서 0600은 필수다. 0400 0200은 절대 안된다
    왜냐하면 각 semaphore의 sem값을 바꾸기 위해서는 READ와 WRITE가 무조건 되어야하기 때문이다.
     
    4. O_EXCL은 필수다
    만약 O_CREAT가 2번되면 key값이 아예 초기 상태로 초기화되기 때문이다.
    따라서 critical section이 당연히 의도대로 작동하지 않는다.
    semaphore는 무조건 딱 1번만 초기화되게 해야한다.
    그래서 모든 semget에는 O_EXCL을 O_CREAT와 붙여서 flags에 넣어주어야한다.

    1. 해당 semaphore set 이미 존재
    	semget이 -1 return. 그냥 이미 있는거 갖다 쓰면 됨.
    2. 존재하지 않음
    	semget이 0 return. 생성해서 쓰면 됨.

     
    그래서 semget할때 아래와 같이 코드가 다른 것들보다 좀 길어진다.

    key = ftok("data",1);
    
    semid = semget(key, 1, 0600 | IPC_CREAT | IPC_EXCL);
    
    // 이미 존재하면 존재하고 있는거 열기
    if(semid == -1) {
    	semid = semget(key, 1, 0);
    }
    // 생성에 성공했으면 없었다는 거임
    // semaphore 초기화 해주기
    else{
    	arg.val = 1;
        	semctl(semid, 0, SETVAL, arg);
    }

     


    [semaphore set 구조]

     
    예를 들어 semget 을 통해 4개의 semaphore를 가진 semset 하나를 생성하면 
     
    아래 그림처럼 4개의 독립적인 semaphore를 갖는 semset이 생성되고 각 semaphore는
    배열에 저장되어 있어 index로 direct-access 가 가능한 구조이다.

    그리고 각 semaphore의 멤버변수들은 다음과 같다.
     


    [semctl]

     
    [arg #1 #2]
    semctl은 해당 semid 집합 내 semnum으로 지정한 한 개의 독립적인 semaphore에 command로 지정한 제어기능을 수행한다.
    함수 이름 그대로 특정 semset 내 한 개의 semaphore를 control 하는데 쓰이는 것이다.
     
    [arg #3]
    return 값은 command에 따라 달라진다.
    command의 종류로는 개별 semaphore에 적용하는 command가 있고 해당 semaphore 집합 전체에 적용할 수 있는 command로 나눠진다.

     
    [arg #4]
    4번째 인자가 가장 중요한 인자이다.
    semctl할때 해당 semaphore에서 특정 정보를 얻어올 수 도 있고 아니면 우리가 특정 정보로 해당 semaphore를 초기화해줄 수 도 있다.
    이때 해당 semaphore에게 초기화정보를 주거나 해당 semaphore의 정보를 받을때 사용하는 인자가 이 4번째 인자이다.
    즉 우리가 직접 data를 주거나 받는게 아니라 간접적으로 semctl 함수에 인자로 넘겨주는 방식으로 하는 것이다!
     


    [semop]

     
    semid인 semset 내 num_ops 개수의 독립적인 semaphore에게 semaphore 연산을 한다는 것이다.

    1이면 0번, 2면 0번 1번, 3이면 0번 1번 2번 이렇게 순차적으로 연산을 한다는 뜻

     

    즉, 두번째 인자로 sembuf 배열이 들어간다면 세마포어 연산이 연속적으로 일어날 수 있는 것이다!

    struct sembuf p_buf[2] = {{0,-1,0}, {1,-1,0}};
    semop(semid, p_buf, 2);

     

    예시로 이렇게 된다면, 0번째 세마포어를 -1 시도하고, 1번째 세마포어도 -1 시도한다는 것이다.

    즉, 한번의 semop 함수 호출로 세마포어 연산이 연속적으로 진행되는 것이다!

     

    어떤 연산을 할지 즉, semwait를 시킬지 semsignal을 시킬지는 두번째 인자로 주면됨.

    struct sembuf p_buf 를 주는데 얘한테 연산에 대한 모든 정보가 있음

    sem_num : 해당 semset 내 적용할 semaphore index
    sem_op : 적용할 연산 -1, 0, 1 , 2 등등
    sem_flag : 걍 무조건 0임

     


    [0 연산]

     

    struct sembuf p_buf = {0,0,0};
    semop(semid, &p_buf, 1);

     

    굉장히 특별한 연산임. sempahore 값이 0이 될때까지 기다리는 거임

     

    이게 signal처럼 동작할 수 있는 연산이다.

    왜냐하면 sem 값을 0으로 만듬으로써 해당 코드를 가진 process가 wait에서 벗어나서 실행되는 현상이기 때문.

     

    그럼 만약 이 코드를 가진 process가 5개고 5개 모두 waiting 상태였다가 만약 semval이 0이 되면?

    5개가 동시에 깨어나서 zqueue에서 빠져나오는거임.

     

    0으로 만든순간 zqueue에서 기다리던 한명이던 10명이던 100명이던 한번에 깨어나는거임

    일대다 동기화할때 굉장히 유용하게 사용됨

     


    사실 저 번갈아 출력작업을 한다는 말만 없으면 그냥 세마포어 하나로만 해도 되는데 번갈아가며 출력하라는 조건 때문에 세마포어 2개를 써야한다. 두 semaphore는 각 프로그램 A와 B에게 각각 적용되는 semaphore로 0번은 A를 semwait semsignal 시키는 용도 1번은 B를 semwait semsignal 시키는 용도로 사용하면 된다. 

     

    동기화를 하는 방법은 semaphore의 blocking 상태를 이용하여 signal을 보내는 느낌으로 하면 된다.
    semaphore는 semval - opval을 했을 시 0 이상일때만 작동하므로 semval을 0으로 유지하면 blocking 상태로 유지되고 0이 아닌, semval을 opval 이상인 값으로 설정해놓으면 blocking 상태에서 깨어난다.
    이 특징을 이용해서 두 프로세스 사이에 동기화 작업을 할 수 있게 만들면 된다.
     

    둘이 설정값과 semop 시 수행하는 연산 값만 다르다. 나머지 코드는 90% 동일하다.
     
    [processA.c]

     
     
    [processB.c]


    [운영체제 시간에 배운 counting sem vs unix sem]

     
    unix sem에는

    1. unix에서는 음수가 없음. 0이랑 양수만 가능함. 값이 0이 되면 누가 와서 값을 빼려고 해도 음수가 없으니까 그냥 0을 유지하면서 해당 프로세스를 그냥 대기큐에 집어넣음(nqueue/semncnt)
    2. queue가 2개임. counting에서는 1개 근데 unix에서는 2개(nqueue/zqueue)
    3. counting sem은 wait -1 signal +1. 근데 unix에서는 wait -N signal +N 까지 가능함.

    이렇듯 unix에서 훨씬 다양한 동기화를 제공해준다.
    그래서 10명이 동시에 critical section으로 들어갈 수 도 있다. 일대다 command가 가능하다.
     


    [그럼 +N(N>1)을 할 수 있다는건 뭔 의미일까]

     
    운영체제 시간에 배운 이론적 semaphore의 가장 큰 장점이 starvation 없이

    queue에 들어가서 기다리면 내 차례가 반드시 온다는 거임.
    근데 여기선 아니다.
     
    내가 -4를 하려했는데 현재 세마포어값이 2임.
    그래서 waiting queue로 다시 들어갔는데 기다리다보니 내 뒤에 -1인 놈도 대기타고 있음
    그러면 나는 건너뛰고 얘는 통과되는 거임.
     
    즉 내가 -몇을 하느냐에 따라 계속 뒤로 밀릴 수 있음.
     
    또 다른 예를 들어보자면
    만약 현재 한 semaphore의 semval이 +3임
    그리고 waiting queue에 -4 -2 -5 -1 이 있으면
    -4랑 -5는 못깨어남
    -4 건너뛰고 -2 깨워주고 -5 건너뛰고 -1 깨움.
    왜냐하면 unix는 무조건 양수아님 0이니까.
     
    즉 unix의 semaphore는 운영체제 시간에 이론상 배웠던 semaphore와 다름.
    FIFO queue가 아님.
    queue에 들어가긴 하는데 내 뒤에 있는 얘가 먼저 signal 받고 깨어날 수 있음.
    unix semaphore에서는 starvation이 발생할 수 있다는 얘기다.
     

    'LinuxProgramming' 카테고리의 다른 글

    [Linux] 과제4 : 카카오톡  (1) 2023.12.08
    [Linux] shared memory  (2) 2023.12.08
    [Linux] msgqueue  (1) 2023.11.23
    [Linux] select()  (1) 2023.11.22
    [Linux] file synchronization  (1) 2023.11.04