기초 리눅스 API Vol.2 (2)

Erdos·2025년 9월 12일

LINUX/UNIX

목록 보기
4/8
post-thumbnail

ABOUT


☕1차적으로 읽으려는 부분

-- 스레드 위주로
[Vol.2]

1장 스레드: 소개
2장 스레드: 스레드 동기화
3장 스레드: 스레드 안전성과 스레드별 저장소
4장 스레드: 스레드 취소
5장 스레드: 기타 세부사항

-- 뮤텍스 & 세마포
[Vol.2]
6장 프로세스 간 통신 개요 -> 뮤텍스
16장 POSIX 세마포어

1권 추가 정리

5장 파일 I/O

5.1 원자성과 경쟁상태

  • 모든 시스템 호출은 아토믹하게 실행된다.
  • 이는 시스템 호출의 모든 단계가 다른 프로세스나 스레드에 의해 중단되지 않고 하나의 동작으로 완료됨을 커널이 보장한다.
  • 이것이 보장되면 경쟁 상태(race condition)을 피할 수 있다.

10장 시간

프로그램에서의 시간
1) 실제 시간: 어떤 표준 시점을 기준으로 측정(달력 시간)되거나 프로세스 동작 중 어떤 고정된 시점(프로그램의 시작)을 기준으로 측정된 시간(경과 시간). -> 파일의 타임스탬프
2) 프로세스 시간: 프로세스가 사용한 CPU 시간의 양 -> 알고리즘 성능 검사/최적화

 #include <sys/time.h>

int gettimeofday(struct timeval *tv, struct timezone *tz);
# 성공하면 0을 리턴하고, 에러가 발생하면 -1 리턴
  • tv 인자는 아래 형태의 구조체를 가리키는 포인터다
 struct timeval 
{
    time_t      tv_sec;     /* UTC 197011일 00:00:00 이래의 초 */
    suseconds_t tv_usec;    /* 추가적인 마이크로초(long int) */
};
  • tv_usec필드가 마이크로초 정밀도를 담을 수 있지만, 그 값의 정확도는 아키텍처별 구현에 따라 다르다.
    • 값을 몇자리까지 표현할 수 있는가.(마이크로초 단위까지) 단, 책에서는 정확도(accuracy)를 언급하고 있지는 않는데, 그만큼 시간을 세밀하게 측정할 수 있는가에 대해서는 보장하지 않는다.
  • 현대의 x86-32시스템에서 gettimeofday()는 실제로 마이크로초 정확도를 제공한다
    - x86-32시스템(timestamp counter register가 CPU 클록 사이클마다 증가하는 펜티엄 시스템)

여기서의 고민

현재 사용 컴퓨터에서는 얼마나 정확할까

  • 현재 리눅스는 gettimeofday() 함수가 아니라 clock_gettime() 계열의 API를 사용한다.
  • CPU의 TSC(Time Stamp Counter)가 Invariant TSC를 지원
    • 코어 간 동기화가 맞고, 주파수 변경에도 일관성 유지
    • 매우 안정적. 정밀한 고해상도 타이머 사용 가능
    • 커널 Ubuntu 22.04에서 high-res times 켜져 있음
    • 내부적으로 ns(10910^{-9}s) 단위 정밀도
    • 단 gettimeofday() 구조체는 µs(10610^{-6}s)단위까지만 제공

1장 스레드: 소개

1.2 pthreads api 의 세부 배경 지식

pthreads 데이터형

데이터형설명사용 목적예시
pthread_t스레드를 식별하기 위한 ID (추상적 핸들, 실제 내부 구현은 구현체에 따라 다름)pthread_create()로 생성한 스레드를 관리하거나 pthread_join()으로 대기할 때 사용c pthread_t tid; pthread_create(&tid, NULL, threadFunc, NULL);
pthread_attr_t스레드 속성 객체. 스택 크기, detach 상태 등 스레드 생성 시 옵션을 지정pthread_attr_init() 후 속성 설정, pthread_create()에 전달c pthread_attr_t attr; pthread_attr_init(&attr);
pthread_mutex_t상호 배제를 위한 뮤텍스(mutex) 객체임계 구역 보호, 경쟁 상태 방지c pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutexattr_t뮤텍스 속성 객체뮤텍스의 동작 모드(예: 재귀적 잠금 가능 여부) 설정c pthread_mutexattr_t mattr;
pthread_cond_t조건 변수(Condition Variable)스레드 간 신호 전달, 특정 조건 충족까지 대기c pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_condattr_t조건 변수 속성 객체조건 변수의 동작 특성 지정c pthread_condattr_t cattr;
pthread_rwlock_t읽기-쓰기 락(Read-Write Lock)다수의 리더, 단일 라이터 동기화 제어c pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
pthread_rwlockattr_tRW락 속성 객체읽기-쓰기 락의 속성 지정c pthread_rwlockattr_t rwattr;
pthread_barrier_t배리어(Barrier) 객체여러 스레드가 특정 지점까지 도달할 때까지 동기화c pthread_barrier_t barrier;
pthread_barrierattr_t배리어 속성 객체배리어 동작 특성 지정c pthread_barrierattr_t battr;
pthread_key_t스레드별 데이터(Thread-Specific Data, TLS) 키각 스레드 고유의 데이터를 저장하고 접근c pthread_key_t key; pthread_key_create(&key, destructor);
pthread_once_t초기화를 한 번만 수행하도록 보장하는 객체전역 자원 초기화 시 사용c pthread_once_t once = PTHREAD_ONCE_INIT;

1.3 스레드 생성

 #include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start) (void *), void *arg);
# 성공하면 0 리턴, 에러 발생하면 에러 번호(양수) 리턴
  • 프로그램이 시작될 때, 프로세스는 초기(initial) 또는 주(main) 스레드라는 하나의 스레드로 이뤄져 있다.

  • 새 스레드는 start로 지정된 함수를 인자 arg로 호출해 시작한다.(start(arg))

    • start = 스레드가 시작하자마자 실행할 함수의 이름(주소). 어떤 종류의 객체를 가리키는 포인터든지 start 함수로 넘길 수 있음.
    • arg = 이 함수에서 넘겨줄 인자
  • 예시

    
    #include <pthread.h>
    #include <stdio.h>
    
    void *worker(void *arg) 
    {
        char *msg = (char *)arg;   // 전달받은 인자 해석
        printf("스레드에서 받은 메시지: %s\n", msg);
        return NULL;
    }
    
    int main() 
    {
        pthread_t t1;
        char *text = "안녕하세요, 스레드!";
    
        // 스레드를 생성하면서 worker 함수를 실행하라고 지정
        pthread_create(&t1, NULL, worker, text);
        pthread_join(t1, NULL); // 스레드 종료 대기
        return 0;
    }
    

1.5 스레드 ID

프로세스 내의 각 스레드는 고유한 스레드 ID를 갖는다. 이 ID는 pthread_create()를 호출한 스레드에 리턴되고, 스스로의 ID는 pthread_self()를 통해 얻을 수 있다.

#include <pthread.h>

pthread_t pthread_self(void);
  • pthread_t는 (우연히) unsigned long으로 정의되어 있다. 다른 구현에는 포인터나 구조체일 수도 있다.

1.6 종료된 스레드와 조인하기

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);
# 성공하면 0 리턴, 에러 발생하면 에러 번호(양수) 리턴
  • waitpid()가 프로세스에 대해 수행하는 작업과 비슷
  • 매개변수
    • thread: 기다릴 스레드의 ID
    • retval: 기다린 스레드가 pthread_exit()으로 준 값을 받을 포인터
  • 차이점
    - 스레드는 서로 동등하다. 프로세스는 fork로 자식을 만들고 wait()를 통해 기다릴 수 있는 것은 부모 프로세스 뿐이다(계층적). 반면, 스레드는 프로세스 내에서는 자유롭게 조인이 가능하다.
  • 궁금한 점(pthread_exit() vs pthread_join())
    • pthread_exit: 스레드가 자기 자신을 끝낼 때
    • pthread_join: 다른 스레드의 종료를 기다릴 때. 어떤 스레드가 다른 스레드가 끝날 때까지 기다리고, 그 스레드의 종료 상태/결과를 받아오는 함수

2장 스레드: 스레드 동기화

2.1 공유 변수 접근 보호: 뮤텍스

  • critical section(임계 영역)은 공유 자원에 접근하는 코드의 영역. 이 영역은 atomic해야 한다. 즉, 임계 영역의 실행을 같은 공유 자원에 동시에 접근하는 다른 스레드가 중단시켜서는 안 된다.
  • 한 번에 하나의 스레드만 변수에 접근할 수 있도록 보장해야 한다.
  • 잠금(lock)과 풀림(unlock)/ 획득, 해제라고도 함
    - 뮤텍스의 소유자: 뮤텍스를 잠근 스레드. 푸는 것도 소유자만 풀 수 있다.
    • 최대 하나의 스레드만 뮤텍스를 잠가둘 수 있다.

2.1.1 정적으로 할당한 뮤텍스

  • 정적 변수로 할당될 수도 있고 실행 시에 동적으로 생성될 수도 있음
  • pthread_mutex_t 형의 변수. 사용하기 전에 반드시 초기화
  • 정적으로 할당되어 있다면
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

2.1.2 뮤텍스 잠금과 풀림

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
# 성공하면 0을 리턴하고, 에러가 발생하면 에러 번호(양수) 리턴
  • lock: 뮤텍스가 현재 풀려 있으면 잠그고 즉시 리턴. 만일, 뮤텍스가 다른 스레드에 의해 잠겨 있으면 해당 뮤텍스가 풀릴 때까지 블록했다가 뮤텍스가 풀리는대로 그 스레드를 잠근다.
  • unlock: 푼 뮤텍스를 획득하려고 둘 이상의 스레드가 기다리고 있으면, 어느 스레드가 획득에 성공할지는 알 수 없다.
 int pthread_mutex_lock(pthread_mutex_t *mutex);
 # 성공하면 0 리턴, 에러 발생하면 에러 번호(양수) 리턴

2.1.3 뮤텍스의 성능

흥미로운 질문
책에서 pthread_mutex, POSIX semaphores, fnctl(파일 레벨 락킹) 3개의 성능을 초로 비교하고 있다.
아래는 책 내용과 gpt를 동료삼아 정리한 내용이다.

1) 뮤텍스(pthread_mutex)

  • 스레드 동기화 전용 잠금 도구
  • 같은 프로세스 안의 여러 스레드가 공유 자원에 접근할 때 빠르게 잠금/해제를 할 수 있음
  • 커널 개입이 최소한이라 속도가 빠름
  • 아토믹한 기계어 동작으로 구현되어 있고(?) 시스템 호출은 lock contention의 경우만 필요함

2) 세마포어(POSIX semaphores)

  • 카운터 기반 동기화 도구(sem_wait, sem_post)
  • 뮤텍스보다 범용적이다(여러 프로세스 간에서 사용 가능하다)
  • 하지만 커널을 더 자주 거쳐서 뮤텍스보다는 느리다
  • 잠그고 푸는 동작에 시스템 호출 요구

3) fcntl(파일 레벨 락킹)

  • 파일 디스크립터를 이용해 파일 단위로 잠금을 거는 방식(오우)
  • 파일을 기반으로 한 프로세스 간 동기화가 가능하지만, 매번 커널을 거쳐야 해서 가장 느리다
  • 잠그고 푸는 동작에 시스템 호출 요구

속도(빠름 ←→ 느림)

뮤텍스 |█████████████████████████████
세마포어 |█████████
fcntl |██

4) 참고
리눅스에서 뮤텍스는 퓨텍스(futex, fast user space mutex)로 구현되어 있다.

2.1.4 뮤텍스 데드락

deadlock: 영원히 기다림 상태에 빠지는 현상

  • 잠금을 걸고 풀지 않거나, 스레드들이 서로의 잠금을 기다리면 데드락 발생
  • 피하기 쉬운 가장 쉬운 방법
  • 뮤텍스 서열(mutex hierarchy)을 정의: 항상 작은 ID의 뮤텍스를 먼저, 큰 ID의 뮤텍스를 나중에 잡도록 규칙을 정한다.
  • 시도한 다음 물러서기

철학자 문제에서는

여기 부분은 하면서 계속 수정될 수 있음.

1) 뮤텍스 서열 정의

  • 포크 항상 작은 번호 -> 큰 번호 순서로만 뮤텍스를 잠글 수 있게 만든다.
  • 포크에 서열(번호)를 부여하고 낮은 번호부터 잠금 규칙을 지키면 원형 대기가 깨짐(데드락 방지)
  • 여기서 문제는 데드락이 아니라 기아 상태 -> 특정 철학자만 계속 못 먹는 상황

2) 시도한 다음 물러나기

3) 공정성 문제를 보통 어떻게 해결하나?

  • 자원 사용 시간 제한
  • 우선 순위 조정(aging 기법): 운영체제 스케줄러의 aging과 비슷
  • 웨이터 (중앙 조정자) 알고리즘: 웨이터에게 허락. round-robin
  • 공정한 자원할당(FIFO 큐): 세마포어 + 큐 or 조건 변수(pthread_cond_wait/signal)
    문제는 mandatory의 mutex함수 범위에서는 이 공정성 문제를 적용할 수 없다고 한다.

2.1.5 뮤텍스를 동적으로 초기화하기

int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
						const pthread_mutexattr_t *restrict attr);

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

PTHREAD_MUTEX_INITIALIZER는 정적으로 할당된 뮤텍스를 기본 속성으로 초기화 할 때만 쓸 수 있다. 다른 모든 경우에는 pthread_mutex_init()를 써서 뮤텍스를 동적으로 초기화해야 한다.

2.1.7 뮤텍스 종류

뮤텍스의 동작
1) 하나의 스레드는 같은 뮤텍스를 두 번 잠그면 안 된다.
2) 스레드는 자신이 소유하지 않은 뮤텍스를 풀면 안 된다.
3) 스레드는 현재 잠겨 있지 않은 뮤텍스를 풀면 안 된다

2.2 상태 변화 알리기: 조건 변수(condition variable)

  • 조건 변수는 언제나 뮤텍스와 함께 사용
  • 공유 변수 접근에 대한 상호 배제를 제공하는 한편, 조건 변수는 상태 변화를 알리는 신호를 보낸다.(리눅스 signal과는 개념이 다름)

2.2.2 조건 변수를 이용한 대기와 시그널

  • 조건 변수의 주요 동작은 signal과 wait다.
  • signal: 하나 이상의 대기 스레드에게 공유 변수의 상태가 바뀌었음을 통보
  • wait: 이 통보를 받을 때까지 블록하는 것
  • 조건의 성립을 기다리는 도구다.
#include <pthread.h>

int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
       
// 성공하면 0을 리턴, 에러가 발생하면 에러 번호(양수) 리턴

3장 스레드: 스레드 안전성과 스레드별 저장소

3.1 스레드 안정성(그리고 재진입성)

  • thread-safe: 동시에 여러 스레드에서 안전하게 부를 수 있는 함수
profile
수학을 사랑하는 애독자📚 Stop dreaming. Start living. - 'The Secret Life of Walter Mitty'

0개의 댓글