포스트

시스템 프로그래밍 7. Thread 프로그래밍

시스템 프로그래밍 7. Thread 프로그래밍

건국대학교 시스템 프로그래밍 진현욱 교수님의 수업을 정리한 내용입니다.

Thread

Thread란 하나의 Process에서 실행되는 독립적인 실행 흐름이다. 하나의 Process는 적어도 1개의 Thread를 반드시 가지며, 여러 Thread를 가질 수 있다.. Process의 Main이 실행되는 Thread를 Main thread라고 한다.

Process의 Virtual Address Space는 모든 Thread가 공유한다. Stack영역까지 공유하면 Stack이 깨질 수 있기 때문에, Stack 영역은 독립적으로 구분한다. 예를들어 Thread 1은 (0~100번지), Thread 2는 (101~200)번지 이와 같은 방법으로 영역을 나눈다. Stack 영역을 쓰다가 범위 이상의 Stack을 담으면 다른 영역을 침범해버릴 수도 있다. 따라서 쓰레드를 생성할 떄, 속성 값으로 Stack 영역의 메모리 영역을 적절히 설정해주는 것이 중요하다.

각 실행 Unit이 독립적인 실행 흐름을 가진다면, 각 Thread마다 Program Counter를 가지고 있어야 한다. 하지만 PC는 Process의 Context 아닌가? 그렇다면 PC가 공유되지 않을까?

  • Process Context
    • Virtual Address Space
    • File Descriptor Table
    • Signal Bit Vector
    • Signal Handler
    • Process ID
  • Thread Context
    • Program Counter
    • Stack Pointer
    • Register State
    • Signal Block Vector (Signal Mask)
    • Thread ID

[!tip]- 참고 사진{title} Pasted image 20241125102511.png

Context란 Process Context와 Thread Context 두개가 존재한다. Process Context는 Thread끼리 공유되도 괜찮은 정보를 가지고, Thread Context에는 실제 코드의 실행 흐름을 위한 정보를 갖는다. 즉 PC, SP, Register 등의 정보는 Thread 단위로 관리되지, Process가 가지고 있는 정보가 아니다.

그렇다면 Context Switch는 어떤 Context를 바꾸는건가? 당연히 Thread Context를 바꾸는 것이다. Thread가 하나의 실행 흐름이므로, 하나의 CPU 코어당 하나의 Thread가 실행될수 있다. 따라서 같은 프로세스가 병렬로 실행될 수 있는 이유가, Thread 단위로 CPU가 Context Switch를 수행하기 떄문이다.

어떤 한 CPU 코어에서, Thread 1을 실행중이라고 하자. 어느정도 실행을 하고 나면, 다른 Thread를 실행하기 위해 Context Switch를 수행한다. 이때 Switch할 Thread가 같은 Process에 있다면 빠르게 바꿀 수 있다. Process Context는 그대로 두고, Thread Context만 Switch하면 되기 때문이다. 만약 Swtcih할 Thread가 다른 Process에 있다면 위 케이스보다는 좀 느릴 것이다. Thread Context와 Process Context를 둘다 바꿔줘야 하기 떄문이다.

Thread ID

  • Process ID : pid_t, pid_t getpid()
  • Unique Thread ID at the User-Level : pthread_t, pthread_t pthread_self()
  • Unique Thread ID at the Kernel-Level : TID, pid_t gettid()

기본적으로 사용자가 사용하는 pthread_t는 Process 단위에서만 Unique한 값을 가진다. 즉 다른 Process 사이에선 두 Thread가 같은 pthread_t를 가질 수도 있다. 운영체제는 Thread 단위로 Context Switch를 하기 때문에, pthread_t가 아닌 전체적으로 쓰레드의 고유한 ID를 가지고 있어야 한다. 그 값을 TID라고 한다.

결론은, 쓰레드의 붙어있는 ID는 두개가 존재한다. 하나는 Process 레벨에서 유일한 ID와, Kernel 영역에서 유일한 ID이다.

Thread System Calls

  • int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start)(void *), void *arg)
  • void pthread_exit(void *retval)
  • int pthread_cancel(pthread_t thread)
  • int pthread_join(pthread_t thread, void **value_ptr)
  • int pthread_detach(pthread_t thread)

[!tip]- pthread_attr_t attr{title}

  • pthread_attr_init(&attr) : Thread 속성 구조체를 초기화한다.
  • pthread_attr_setdetachstate(&attr, detachstate)
    • PTHREAD_CREATE_JOINABLE (default) : Thread를 Join 함수로 회수 가능하도록 한다.
    • PTHREAD_CREATE_DETACHED : 자동으로 Thread가 회수되도록 한다. Join 사용이 불가능하다.
  • pthread_attr_setscope(&attr, scope) : CPU 자원을 할당받기 위한 경쟁 범위를 설정한다.
    • PTHREAD_SCOPE_SYSTEM (default) : System 전체 영역에서 결정한다. 이 쓰레드는 하나의 독립적인 존재로 Thread 하나가 하나의 코어를 할당받을 수 있게 된다.
    • PTHREAD_SCOPE_PROCESS : 현재는 지원하지 않는다.
  • pthread_attr_setinheritsched(&attr, inheritsched) : 스케쥴링 속성 상속 여부를 설정한다.
    • PTHREAD_INHERIT_SCHED (default) : 스케쥴링 속성을 부모 쓰레드에게 상속받는다. 부모 쓰레드란 pthread_create() 함수를 사용하여 쓰레드를 생성한 쓰레드를 의미한다.
    • PTHREAD_EXPLICIT_SCHED : 상속받지 않는다.
  • pthread_attr_setschedpolicy(&attr, policy) : 쓰레드의 실시간 스케쥴링 정책 설정.
    • SCHED_FIFO : 선입 선출 방식. 우선순위가 높은 쓰레드가 끝날 때까지 다른 쓰레드의 자원을 할당하지 않는다.
    • SCHED_RR : 우선순위를 적용하고, 쓰레드들이 정해진 시간 할당량까지만 실행된다.
    • SCHED_OTHER (default) : 우선순위를 적용하지 않는다.
  • pthread_attr_setschedparam(&attr, &param) : 쓰레드 스케쥴링 우선순위 설정.
  • pthread_attr_setguardsize(&attr, guard_size) : VAS의 Stack Overflow를 감지하기 위한 Stack 사이사이의 여유 공간.
  • pthread_attr_setstacksize(&attr, stack_size) : VAS의 Stack Size 설정
1
2
3
struct sched_param {
    int sched_priority; // 스케줄링 우선순위. 숫자가 클 수록 우선순위가 높다.
};

pthread_create() 함수로 쓰레드를 생성한다. 생성에 성공하면 pthread_t와 0을 반환한다. 실패하면 -1을 반환한다. pthread_t는 Thread ID이다. pthread_attr_t 구조체를 생성해서 쓰레드를 환경설정 하거나, NULL을 넣어서 Default 값을 줄 수 있다. Thread가 main으로 실행할 함수를 반드시 제공해야 한다. 함수는 void* func_name(void* args) 형태로 고정된다. Thread의 Main 함수에 넣을 인자값을 arg에 넣을 수 있다.

pthread_exit() 함수로 현재 내 쓰레드를 종료할 수 있다. 내 쓰레드를 종료해도, 내 쓰레드에서 생성된 다른 쓰레드는 종료되지 않는다. 인자값으로 종료될 떄 반환할 값을 넘길 수 있다. 이는 메인 함수에서 return (void*) value로 리턴값을 넘기는 것과 동등하다.

pthread_cancel() 함수로 다른 쓰레드를 종료시킬 수 있다. 다른 프로세스의 쓰레드는 종료할 수 없다.

pthread_join() 함수로 쓰레드를 Reaping할 수 있다. 쓰레드가 종료되면, 쓰레드 또한 Reaping해야 할 책임이 있다. 그 책임은 보통 쓰레드를 만든 쓰레드에서 관리한다. 이 함수는 Blocking 호출이며, thread가 종료될 때까지 대기한다. Reaping 성공시 0을 반환하며, 실패시 에러 코드를 반환한다. 쓰레드가 반환하는 값이 있다면 (void**) value_ptr 인자로 담겨서 전달된다. 실패하는 케이스로 errno == EINTR인 경우 시그널을 받아 Blocking 상태가 강제로 풀린 경우를 의미한다. 이 경우 다시 join 함수를 실행해야 할 것이다.

pthread_detach() 함수를 실행하면 join하지 않고도 쓰레드의 자원을 자동으로 회수하도록 한다. 성공적으로 Detached 상태로 전환하면 0을, 실패하면 에러 코드를 반환한다. 이걸 설정하면 쓰레드에서 반환하는 값이 무시된다.

[!example]- 기본 구조, 3초 후 Print하는 Thread.{title}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

void* thread_function(void* arg) {
    sleep(3);
    printf("Pid: %d, Tid: %lu\n", getpid(), pthread_self());
    return NULL;
}

int main() {
    pthread_t thread;

    // 쓰레드 생성
    if (pthread_create(&thread, NULL, thread_function, NULL) != 0) {
        perror("Failed to create thread");
        return EXIT_FAILURE;
    }

    // 쓰레드 종료 대기
    // 두번째 인자값에 빈 포인터 변수를 넣으면, 종료된 스레드의 반환값을 채워준다. 
    // int pthread_join(pthread_t thread, void **value_ptr)
    if (pthread_join(thread, NULL) != 0) {
        perror("Failed to join thread");
        return EXIT_FAILURE;
    }

    printf("Main thread finished.\n");
    return EXIT_SUCCESS;
}

[!example]- Detached 속성을 사용한 자동 Reaping{title}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void* thread_function(void* arg) {
    sleep(3);  
    printf("Pid: %d, Tid: %lu\n", getpid(), pthread_self());
    return NULL;
}

int main() {
    pthread_t thread;
    pthread_attr_t attr;

    // 속성 초기화
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    // Detached 쓰레드 생성
    if (pthread_create(&thread, &attr, thread_function, NULL) != 0) {
        perror("Failed to create thread");
        pthread_attr_destroy(&attr);
        return EXIT_FAILURE;
    }

    pthread_attr_destroy(&attr); // 속성 객체 소멸

    printf("Main thread will not wait for detached thread.\n");
    sleep(4); // Detached 쓰레드가 종료되도록 기다림
    printf("Main thread finished.\n");

    return EXIT_SUCCESS;
}

[!example]- pthread_attr_t 사용{title}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void* thread_function(void* arg) {
    printf("Thread with custom attributes is running.\n");
    sleep(1); // 작업 시뮬레이션
    printf("Thread finished.\n");
    return NULL;
}

int main() {
    pthread_t thread;
    pthread_attr_t attr;
    struct sched_param param;

    // 속성 초기화
    pthread_attr_init(&attr);

    // 1. Detach state 설정 (자동 Reaping)
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    // 2. Scope 설정 (스레드 스케줄링 범위)
    pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM); // 시스템 단위로 경쟁

    // 3. Scheduler 상속 설정 (Inherit scheduler)
    pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED); // 스케줄러 명시적 설정

    // 4. Scheduling policy 설정
    pthread_attr_setschedpolicy(&attr, SCHED_FIFO); // 실시간 FIFO 스케줄러

    // 5. Scheduling priority 설정
    param.sched_priority = 20; // 우선순위 설정
    pthread_attr_setschedparam(&attr, &param);

    // 6. Guard Size 설정 (스택 오버플로 방지 영역)
    size_t guard_size = 8 * 1024; // 8KB
    pthread_attr_setguardsize(&attr, guard_size);

    // 7. Stack Size 설정
    size_t stack_size = 2 * 1024 * 1024; // 2MB
    pthread_attr_setstacksize(&attr, stack_size);

    // 쓰레드 생성
    if (pthread_create(&thread, &attr, thread_function, NULL) != 0) {
        perror("Failed to create thread");
        pthread_attr_destroy(&attr);
        return EXIT_FAILURE;
    }

    // Detach 상태이므로 pthread_join을 호출할 필요 없음
    printf("Main thread will not wait for detached thread.\n");

    // 속성 객체 소멸
    pthread_attr_destroy(&attr);

    // 메인 쓰레드 종료 전에 대기
    sleep(2); // Detached 쓰레드가 끝날 시간을 줌
    printf("Main thread finished.\n");

    return EXIT_SUCCESS;
}

[!example]- 쓰레드의 인자값으로 Custom 구조체 넘기고, 쓰레드의 반환값 받기{title}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_STR 10

// 사용자 정의 구조체
typedef struct {
    int id;
    char message[MAX_STR];
} ThreadData;

void* thread_function(void* arg) {
    ThreadData* data = (ThreadData*)arg;
    // 쓰레드가 종료되면 지역변수같이 Stack에 저장된 값은 유효하지 않게 된다. 
    // 따라서 메모리를 Heap 영역에 동적 할당해서 반환해줘야 한다.
    ThreadData* ret = (ThreadData*)malloc(sizeof(ThreadData));
    if (!ret) { 
        perror("Failed to allocate memory"); 
        pthread_exit(NULL);
    }
    
    printf("Thread ID: %d, Message: %s\n", data->id, data->message);

    ret->id = data->id + 1;
    ret->message = data->message;

    return ret;
}

int main() {
    pthread_t thread;
    ThreadData data;
    ThreadData* ret_data;

    // 구조체 초기화
    data.id = 1;
    strcpy(data.message, "Hello from thread!");

    // 쓰레드 생성
    if (pthread_create(&thread, NULL, thread_function, &data) != 0) {
        perror("Failed to create thread");
        return EXIT_FAILURE;
    }

    // 쓰레드 종료 대기
    if (pthread_join(thread, (void**) &ret_data) != 0) {
        perror("Failed to join thread");
        return EXIT_FAILURE;
    }

    printf("Main thread finished.\n");
    return EXIT_SUCCESS;
}

컴파일할 때 -lpthread 라이브러리를 추가해줘야 쓰레드를 사용할 수 있다. gcc thrd1.c –o thrd1 -lpthread

Check Limit

[!tip] ulimit command{title} 자원에 대한 제한을 확인할 수 있는 Command

[!tip] System resource limit{title} –/proc/sys/kernel/threads-max –/proc/sys/fs/file-max

Mutex

  • pthread_mutext_t mutex = PTHREAD_MUTEX_INITIALIZER or
  • int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr)
  • int pthread_mutex_lock(pthread_mutex_t *mutex)
  • int pthread_mutex_trylock(pthread_mutex_t *mutex)
  • int pthread_mutex_unlock(pthread_mutex_t *mutex)
  • int pthread_mutex_destroy(pthread_mutex_t *mutex)

여러 쓰레드에서 공유하는 자원을 사용하면 Race Condition이 발생할 수 있다. 이는 Mutex 변수를 사용해서 해결할 수 있다. 공유되는 자원을 사용해서 문제가 생길 수 있는 코드 단위를 Critical Section이라고 한다. Critical Section을 실행하기 전에 Mutex 변수의 Lock을 시도한다. Lock을 성공하면 Critical Section을 실행하고 Lock을 풀어주면 된다. Lock이 걸려있다면, 다른 누군가가 공유되는 자원을 사용하고 있다는 뜻과 같다. 따라서 Lock이 성공할 때까지 기다리거나 (Blocking), 다른 작업을 하다가 다시 Lock을 시도하는 두가지 행동을 취할 수 있다. 이런 방법을 Mutual Exclusion라고 부른다.

Mutex 변수는 각 쓰레드끼리 공유될 수 있는 메모리 공간에 생성해야 한다. 지역변수, 매개변수 등은 Stack Memory 영역에 저장되어 쓰레드끼리 공유되지 않는다. 동적 할당한 변수 또는 전역 변수, static 변수는 각각 Heap 영역, Data 영역에 저장되어 쓰레드끼리 공유될 수 있다.

Mutex 변수 생성 후, pthread_mutex_init() 함수로 초기화한다. mutex의 attribute 값을 설정해 줄 수 있지만, NULL을 넣어도 된다.

pthread_mutex_lock() 함수로 lock을 시도한다. lock 실패 시, Blocking State 상태로 전환된다. lock이 성공할 때까지 무한정 대기하며, 성공하면 0을 반환하고, 실패하면 에러 코드를 반환한다.

pthread_mutex_trylock() 함수로 lock을 찍먹할 수 있다. lock 실패시 Blocking State로 전환되지 않으며, EBUSY를 반환한다. 성공하면 0을 반환하고, 다른 이유로 실패하면 그에 맞는 에러 코드를 반환한다.

mutex를 걸어 잠그고, Critical Section 실행이 끝나면 pthread_mutex_unlock() 함수를 통해 Lock을 해제해줘야 한다.

Mutex 사용이 끝났다면 pthread_mutex_destroy() 함수로 Mutex 자원을 정리한다.

[!example]- Mutex Lock Example{title}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

pthread_mutex_t mutex; // Mutex 선언
int shared_data = 0;   // 공유 자원

void* thread_function(void* arg) {
    pthread_mutex_lock(&mutex);
    shared_data++;
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t thread1, thread2;

    // Mutex 초기화
    pthread_mutex_init(&mutex, NULL);

    // 두 개의 쓰레드 생성
    pthread_create(&thread1, NULL, thread_function, NULL);
    pthread_create(&thread2, NULL, thread_function, NULL);

    // 쓰레드 종료 대기
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // Mutex 소멸
    pthread_mutex_destroy(&mutex);

    printf("Final shared data: %d\n", shared_data);
    return 0;
}

[!example]- Mutex TryLock Example{title}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

pthread_mutex_t mutex; // Mutex 선언
int shared_data = 0;   // 공유 자원

void* thread_function(void* arg) {
    // TryLock는 Mutex Lock에 실패시 Wait 상태로 바뀌는 것이 아니라,
    // 바로 false 코드를 반환한다.
    if (pthread_mutex_trylock(&mutex) == 0) {
        shared_data++;
        pthread_mutex_unlock(&mutex)
    } else {
        printf("Thread %ld could not acquire the mutex.\n", pthread_self());
    }
    return NULL;
}

int main() {
    pthread_t thread1, thread2;

    // Mutex 초기화
    pthread_mutex_init(&mutex, NULL);

    // 두 개의 쓰레드 생성
    pthread_create(&thread1, NULL, thread_function, NULL);
    pthread_create(&thread2, NULL, thread_function, NULL);

    // 쓰레드 종료 대기
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // Mutex 소멸
    pthread_mutex_destroy(&mutex);

    printf("Final shared data: %d\n", shared_data);
    return 0;
}

[!tip]- Mutex Tip{title}

  1. 코드를 앞에 선언해도 누가 먼저 Lock에 진입할지 알 수 없다. Context Switch 때문이다.
  2. Mutex와 Binary Semaphore는 비슷하면서도 똑같은건 아니다. Mutex는 완전히 Race Condition을 해결하기 위해 설계되었고, Semaphore는 그 이상으로 다양한 상황에서 활용할 수 있도록 설계되었다.
  3. Lock을 거는 동작은 어셈블리 코드 기준으로 딱 한번의 Instruction만으로 가능하여 레이스 컨디션이 절대로 발생하지 않는다. 따라서 두 CPU 코어에서 동시에 Lock을 거는 것처럼 보여도, 미묘한 차이로 인하여 한쪽에서 먼저 Lock을 진입하게 된다.

Dining-Philosophers Problem

여러개의 공유 Resource를 여러개의 Mutex를 사용해서 관리하는 경우 생길 수 있는 순환 Wait 문제다. 먹고 생각만 하는 5명의 철학자가 원형 테이블에 존재하고, 철학자의 양 옆에 젓가락이 한쪽씩만 주어진다. 철학자가 음식을 먹기 위해 본인의 양쪽에 있는 젓가락을 동시에 집어서 음식을 먹어야 한다. 공유되어있는 Resource는 젓가락이다. 철학자는 음식을 먹기 위해서 다음과 같은 프로세스를 거친다.

  1. 왼쪽 젓가락을 집는다. (Mutex Lock)
  2. 오른쪽 젓가락을 집는다. (Mutex Lock)
  3. 음식을 먹는다.
  4. 두 젓가락을 내려놓는다.

만약 모든 철학자가 동시에 왼쪽 젓가락을 집고 나면, 모든 철학자가 Wait 상태에 빠진다. 이를 Deadlock 상태라고 한다. 자원도 널널하고, 문제 없어보이는데 소프트웨어가 멈춰있다면 대부분의 경우 Deadlock이라고 보면 됨. 이를 해결하기 위한 방법이 여러가지가 있음

  1. 한명을 빼버리면 교착상태에 일어나지 않는다. 좋은 해결법은 아닌듯.
  2. 2개 젓가락을 확실히 집을 수 있는 상태인지 확인하고 집는다. 이를 위해 Mutex를 하나 더만들고, Lock하는 과정을 Mutual exclusion한다.
  3. 젓가락 집는 순서를 뒤섞는다. 임의로 짝수 ID를 가진다면 왼쪽부터, 홀수 ID를 가진다면 오른쪽부터 집도록 코드를 짤 수 있다.

보통 DeadLock 문제는 하나의 Mutex만 사용하고, Mutex가 반드시 Unlock됨이 보장되면 발생하지 않는다. 두개 이상의 Mutex를 사용할 때 발생할 수 있다.

Starvation Problem

위 철학자 문제 상황을 가져오자. 만약 A 철학자가 다 먹을때까지 B 철학자가 기다리고 있다. 하지만 A 철학자가 젓가락을 내려놓아도, B 철학자가 먹게 된다는 보장이 없다. 운이 나쁘면 B는 계속 자원을 할당받지 못하는 상태, 즉 Starvation(기아) 상태가 되고, 이는 Fairness(공정성)에 대한 문제가 있다. 이를 해결하기 위해 Condition Variable를 도입하는데, 이는 운영체제 시간에 자세히 다룰 예정이다

Thread-Signal

  • int pthread_kill(pthread_t thread, int sig)

운영체제에서 멀티쓰레딩 환경에서 시그널을 어떻게 처리할까? Signal Bit Vector, Signal Handler는 Process Context 단위로 관리하고, Signal Mask Vector는 Thread Context에서 관리한다. 이는 다음과 같다.

  1. Signal Handler를 어떤 쓰레드에서 변경하면, 같은 쓰레드에 있는 모든 쓰레드가 영향을 받는다.
  2. 프로세스에 Signal이 전달되면, Signal Handler를 실행할 수 있는 쓰레드 중 하나를 골라서 그 쓰레드의 Stack 영역에 기생하여 실행된다.

pthread_kill() 함수를 사용하면 특정 쓰레드가 시그널을 처리하도록 할 수 있다. 다른 프로세스의 쓰레드에게는 시그널을 보낼 수 없다. 만약 그 쓰레드가 시그널을 블록하고있으면, 다른 쓰레드에서 그 시그널을 처리하지 않고 보류(pending)한다.

Kernel은 시그널이 그냥 프로세스에게 전달된건지, 아니면 특정 쓰레드에게 전달된건지 기억한다. 만약 그냥 프로세스에게 전달된 것이라면, 시그널이 Fatal인지 아닌지에 따라 동작 구조가 다르다.

  • Fatal(긴급) Signal : Thread의 Signal Mask에 상관 없이 이 시그널을 처리할 수 있는 가장 빠른 Thread에서 Signal Handler를 실행한다.
  • Non-fatal Signal : Thread의 Signal Mask를 보고, Signal을 실행할 수 있는 쓰레드 중 하나를 선택하여 Signal Handler를 실행한다.

만약 특정 쓰레드에게 전달된 쓰레드라면 그 쓰레드의 Signal Mask를 보고 Signal Handler 실행 여부를 결정한다. 만약 Block하고 있다면, Block이 해제 될때까지 Signal을 처리하지 않고 Pending 상태가 된다.