본문 바로가기

Native/C

쓰레드와 시그널

쓰레드와 시그널

윤 상배

dreamyun@yahoo.co.kr

고친 과정
고침 0.9 2004년 1월 29일 23시
시그널을 이용한 쓰레드 작동/중지 제어
고침 0.8 2003년 10월 7일 23시
최초 문서작성

1. 쓰레드에서의 시그널 사용

쓰레드에서의 시그널 사용은 시그널에 대한 기본적인 이해만 가지고 있다면 약간의 응용으로 충분히 해결할 수 있는 문제이긴 하지만 범 유닉스적으로 응용하고자 한다면(특히 리눅스가 포함된다면) 운영체제간 신경써줘야할 문제가 있다. 이번장에서는 쓰레드에서의 시그널을 이용하는 방법과 운영체제가 다름으로 인해 발생할 수 있는 문제들에 대해서 알아보도록 하겠다.


1.1. 시그널을 특정 쓰레드로 보내기

쓰레드에서 시그널은 서로 공유된다는걸 알고 있을 것이다. 문제는 공유된다는 점인데 만약 프로세스에 시그널을 보낼 경우 해당 프로세스에서 생성된 모든 쓰레드에 시그널이 전달이 되게 된다. 이것은 우리가 원하는게 아니다.

우리가 원하는 것은 특정 쓰레드에서만 시그널을 받도록 하는 것이다. 이러한 작업을 위해서 우리는 시그널 마스크를 사용한다. 시그널 마스크는 말그대로 특정 시그널에 대해서 마스크를 씌우는 것으로 해당 쓰레드에서 특정 시그널에 대해서 마스크를 씌우면 마스킹된 시그널은 해당 쓰레드로 전달되지 않는다. 이 시그널을 받기를 원하는 쓰레드에서는 이 시그널에 대한 마스크를 제거시킨다. 그러면 블럭되어 있는 시그널은 마스크가 제거된 쓰레드로 전달될 것이다. 일종의 필터기다.

그림 1. 시그널 마스크의 작동원리

위의 그림은 시그널 마스크의 작동원리를 보여준다. 메인 쓰레드에서는 SIGINT와 SIGUSR2에 대해서 시그널 마스크를 설치한다. 그리고 쓰레드 1에서는 SIGINT에 대한 마스크를 제거하고, 쓰레드 2에서는 SIGUSR2에 대한 마스크를 제거한다. 이렇게 되면 SIGINT가 메인 쓰레드에 도착했을 때 마스크 때문에 메인 쓰레드에는 도착하지 못하고 쓰레드 1로 전달될 것이다. SIGUSR2가 도착했을 경우 메인 쓰레드와 쓰레드 1에서는 마스크 때문에 전달되지 못하고 쓰레드 2로 시그널이 전달된다. 1.1.1절에서는 위의 작동원리 대로 구현된 예제 코드를 다루고 있다.

이러한 쓰레드별 시그널 마스킹을 위해서 pthread는 pthread_sigmask(3)라는 함수를 제공한다.

#include 
#include 

int pthread_sigmask(int how, const sigset_t *newmask, sigset_t *oldmask);
			
이 함수는 현재 쓰레드에 시그널newmaskhow 를 이용해서 시그널 마스크를 만든다. how는 SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK중 하나를 선택할 수 있다. SIG_BLOCK는 현재 설정된 시그널 마스크에 newmask를 추가하며 SIG_UNBLOCK는 현재 설정된 시그널 마스크에서 newmask를 제거하고 SIG_SETMASK는 newmask로 현재 시그널 마스크를 설정한다.


1.1.1. 간단 예제

그럼 pthread_mask(3)를 이용한 간다한 예제를 만들어 보도록 하겠다. 코드는 여러분이 시그널과 쓰레드에 관한 최소한의 지식을 가지고 있다는 가정하에 작성될 것이며, 설명은 주석으로 대신하도록 하겠다.

예제 : th_signal.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 

/*
 * 시그널 핸들러
 * 핸들러가 호출된 쓰레드의 ID와 시그널 번호를 출력한다.   
 */
void sig_handler(int signo)
{
    printf("SIGNAL thid %d : %d\n", pthread_self(),signo);
}

void *threadfunc2(void *arg);
void *threadfunc(void *arg);

int main()
{
    int n, i, j;
    pthread_t threadid;

    // SIGINT와 SIGUSR2 시그널을 
    // 시그널 마스크에 등록한다. 
    // 시그널 마스크는 모든 쓰레드에서 공유된다. 
    // 고로 다른 쓰레드에서도 
    // SIGINT, SIGUSR2시그널에 대해서 마스크 된다. 
    sigset_t newmask;
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGINT);
    sigaddset(&newmask, SIGUSR2);
    pthread_sigmask(SIG_BLOCK, &newmask, NULL);

    // 원하는 쓰레드로 시그널이 전달하는지 확인하기 위해서 
    // 쓰레드 ID를 확인한다. 
    if ((n = pthread_create(&threadid, NULL, threadfunc2, NULL)) != 0 )
    {
        perror("Thread create error ");
        exit(0);
    }
    printf("thread2 id %d\n", threadid);

    if ((n = pthread_create(&threadid, NULL, threadfunc, NULL)) != 0 )
    {
        perror("Thread create error ");
        exit(0);
    }
    printf("thread id %d\n", threadid);

    pthread_join(threadid, NULL);
}

void *threadfunc(void *arg)
{
    int i=0, j;
    struct sigaction act;
    sigset_t newmask;

    // 결과의 확인을 위해서 쓰레드 ID를 출력한다.  
    printf("SIGINT Thread Start %d\n", pthread_self());

    // SIGINT에 대한 시그널 핸들러를 설치하고  
    // 시그널 마스크에서 SIGINT를 제거한다.  
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGINT);
    act.sa_handler = sig_handler;
    sigaction(SIGINT, &act, NULL);
    pthread_sigmask(SIG_UNBLOCK, &newmask, NULL);

    while(1)
    {
        printf("%d\n", i);
        i++;
        sleep(1);
    }
    return ;
}

void *threadfunc2(void *arg)
{
    struct sigaction act;

    // SIGUSR2에 대한 시그널 핸들러를 설치하고 
    // 시그널 마스크에서 SIGUSR2를 제거한다.   
    sigset_t newmask;
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGUSR2);
    act.sa_handler = sig_handler;
    sigaction(SIGUSR2, &act, NULL);
    pthread_sigmask(SIG_UNBLOCK, &newmask, NULL);

    while(1)
    {
        sleep(1);
    }
}
				
위 프로그램을 실행시킨뒤 kill명령으로 SIGINT와 SIGUSR2 시그널을 PID로 보내보면 해당 쓰레드로 시그널이 전달되고 시그널 핸들러가 실행되는걸 확인할 수 있을 것이다.


1.2. 쓰레드간 시그널 전송

외부의 다른 프로세스에서 시그널을 발생시키는 것 외에도 같은 프로세스에서 작동하는 쓰레드간에 시그널을 전송해야 하는 경우도 생길 것이다.

이러한 쓰레드간 시그널 전송은 여러가지 목적으로 사용할 수 있다. 일정시간마다 특정 쓰레드에 시그널을 전송하므로써 쓰레드를 깨워서 코드를 실행시키게 한다거나 네트워크 애플리케이션에서 write, read에 타임아웃을 검사하는 용도로도 사용가능 하다.

네트워크 애플리케이션에서 스레드간 시그널 전달을 통해 타임아웃을 검사한다는 생각은 좀 생소할 수도 있을것 같다. 보통은 select나 alarm을 사용할 건데, 멀티 쓰레드 프로그램의 경우 alarm(2)의 사용은 사실상 어렵다고 볼 수 있다. 여러개의 쓰레드에서 alarm(2)을 사용할 경우 단지 하나의 alarm(마지막 alarm값)만이 등록되어서 사용할 수 있기 때문이다. 그렇다면 select를 사용해야 할 건데, select대신에 전용의 시그널을 발생하는 쓰레드를 이용해서 사용할 수 있다.

read(2)를 예로 들어서 설명해 보자 read(2)를 하기전에 특정 (전역)값을 0으로 세팅하고 read를 수행한후 1로 값을 변경하도록 한다. 그리고 타임아웃 체크를 위한 쓰레드에서는 타임아웃 시간 간격으로(sleep(2)를 이용하면 된다) 이 값을 검사한다. 만약 값이 0으로 세팅되어 있는걸 확인 했는데, 다음 시간이 돌아온 뒤에도 이 값이 0이라면 read영역에서 타임아웃이 발생했다고 판단 할 수 있을 것이다. 그러면 타임아웃이 발생한 쓰레드에 시그널을 전송하도록 한다. 쓰레드에 시그널이 전송하면 인터럽트가 발생하고 read에서 빠져나오게 된다.

if (read(..) < 0)
{
   // 만약 인터럽트로 인하여 빠져나온 거라면..
   if (errno == EINTR)
   {
      ...
   }
} 
			
시그널 발생시 인터럽트가 전달되게 하려면 약간의 부가적인 작업이 필요한데, 이것은 소켓 타임아웃을 참고하기 바란다.


1.2.1. 다른 쓰레드로 시그널 전송

이러한 쓰레드간 시그널 전송을 위해서 pthread_kill(3)이라는 함수가 제공된다.

#include 
#include 

int pthread_kill(pthread_t thread, int signo);
				
첫번째 인자thread는 시그널을 전달받을 쓰레드의 식별자이고 signo는 전달하고자 하는 시그널 번호이다. 보내는 쪽은 pthread_kill(3)을 이용해서 비교적 간단하게 구현이 가능하다.


1.2.2. 시그널 받기

시그널을 받는 쓰레드의 경우 동기와 비동기 두가지 방식을 통해서 받을 수 있다. 동기 방식으로 받을 경우는 sigwait(3)함수를 이용해서 시그널이 전달될 때까지 블럭되면서 기다린다.

#include 
#include 

int sigwait(const sigset_t *set, int *sig);
				
이 함수는 시그널 셋set에 설정된 시그널중 하나가 전달될 때까지 호출된 영역에서 대기한다. 시그널을 받았다면 리턴되고 전달 받은 시그널 번호는 sig를 통해서 넘어온다. 시그널을 기다린다는 특징을 이용해서 쓰레드간 동기화를 위한 목적으로도 유용하게 사용할 수 있을 것이다.

두번째는 비동기적인 방식으로 코드 실행중에 시그널이 전달되면 인터럽트가 걸리고 시그널 핸들러가 수행되는 방식이다. 일반적인 시그널 사용방식과 동일하다.


1.2.3. 예제

sigwait(3)를 통해서 동기적으로 기다리는 것은 구현이 간단하므로 따로 다루지 않고 시그널 핸들러를 등록해서 비동기적으로 시그널을 기다리는 코드를 구현해 보도록 하겠다. 1.1.1절의 코드를 약간 수정했다.

예제 : thtoth_sig.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 

/*
 * 시그널 핸들러
 * 핸들러가 호출된 쓰레드의 ID와 시그널 번호를 출력한다.
 */
void sig_handler(int signo)
{
    printf("SIGNAL RECV TH ID %d : %d\n", pthread_self(),signo);
}

void *threadfunc2(void *arg);
void *threadfunc(void *arg);
void *s_signal(void *arg);

// 쓰레드 ID를 저장한다.  
int sigid[2];

int main()
{
    int n, i, j;
    pthread_t threadid;

    // 원하는 쓰레드로 시그널이 전달하는지 확인하기 위해서
    // 쓰레드 ID를 확인한다.
    if ((n = pthread_create(&threadid, NULL, threadfunc2, NULL)) != 0 )
    {
        perror("Thread create error ");
        exit(0);
    }
    sigid[0] = threadid;
    printf("thread2 id %d\n", threadid);

    if ((n = pthread_create(&threadid, NULL, threadfunc, NULL)) != 0 )
    {
        perror("Thread create error ");
        exit(0);
    }
    sigid[1] = threadid;
    printf("thread id %d\n", threadid);

    if ((n = pthread_create(&threadid, NULL, s_signal, NULL)) != 0 )
    {
        perror("Thread create error ");
        exit(0);
    }

    pthread_join(threadid, NULL);
}

void *threadfunc(void *arg)
{
    int i=0, j;
    struct sigaction act;
    sigset_t newmask;

    // 결과의 확인을 위해서 쓰레드 ID를 출력한다.
    printf("SIGINT Thread Start %d\n", pthread_self());

    sigemptyset(&newmask);
    sigaddset(&newmask, SIGINT);
    act.sa_handler = sig_handler;
    sigaction(SIGINT, &act, NULL);

    while(1)
    {
        printf("%d\n", i);
        i++;
        sleep(1);
    }
    return ;
}

void *threadfunc2(void *arg)
{
    struct sigaction act;

    sigset_t newmask;
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGINT);
    act.sa_handler = sig_handler;
    sigaction(SIGINT, &ct, NULL);

    while(1)
    {
        sleep(1);
    }
}

/*
 * SIGINT를 두개의 쓰레드로 서로다른 시간간격으로 
 * 전달한다. 
 */
void *s_signal(void *arg)
{
    int i = 1;
    while(1)
    {
        sleep(1);
        i++;
        if((i % 7) == 0)
        {
            printf("Send SIGINT %d\n", sigid[0]);
            pthread_kill(sigid[0], SIGINT);
        }
        if((i % 11) == 0)
        {
            printf("Send SIGINT %d\n", sigid[1]);
            pthread_kill(sigid[1], SIGINT);
        }
    }
}
				
위의 코드의 경우 시그널을 받을 쓰레드를 명시해줄 수 있으므로 시그널 마스크등을 설치할 필요가 없다. SIGINT가 원하는 쓰레드로 정확하게 전달되는걸 확인할 수 있을 것이다.


1.2.4. 시그널을 이용한 쓰레드 작동 제어

쓰레드 프로그래밍을 하다보면 비동기 적으로 특정 쓰레드를 중단 시켜야 되는 경우가 발생한다. 물론 임의의 시점에서 중단된 쓰레드를 다시 작동하도록 만들어 주어야 할것이다.

다른 우회적인 몇가지 구현 방법이 있겠지만 비동기적인 처리를 위해서는 역시 시그널만한게 없는 것 같다.

#include 
#include 
#include 
#include 
#include 

pthread_t thread_t[3];

void sig_handler(int signo)
{
    printf("Thread Stop %d:\n", (int)pthread_self());
    sleep(100);
}

void null(int signo)
{
    printf("Thread Start\n");
}

void *test(void *data)
{
    sigset_t newmask;
    struct sigaction act, act2;
    int i = 0;

    sigemptyset(&newmask);
    sigaddset(&newmask, SIGUSR1);
    sigaddset(&newmask, SIGCONT);
    act.sa_handler = sig_handler;
    act2.sa_handler = null;

    sigaction(SIGUSR1, &act, NULL);
    sigaction(SIGCONT, &act2, NULL);

    pthread_sigmask(SIG_UNBLOCK, &newmask, NULL);

    while(1)
    {
        printf("Im child Thread %d %d\n", (int)pthread_self(),i);
        i++;
        sleep(1);
    }
}

void *worker(void *data)
{

    sigset_t newmask;
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGUSR1);
    sigaddset(&newmask, SIGCONT);

    pthread_sigmask(SIG_BLOCK, &newmask, NULL);
    while(1)
    {
        sleep(2);
        pthread_kill(thread_t[0], SIGUSR1);
        sleep(3);
        pthread_kill(thread_t[0], SIGCONT);
    }
}

int main()
{
    pthread_create(&thread_t[0], NULL, test, NULL);
    pthread_create(&thread_t[1], NULL, test, NULL);
    pthread_create(&thread_t[2], NULL, worker, NULL);

    pthread_join(thread_t[0], NULL);
    pthread_join(thread_t[1], NULL);
    return 1;
}
				


1.3. 운영체제별 차이점

쓰레드의 작동방식은 운영체제별로 많은 차이를 보여줄 수 있으며, 차이점에 유의해서 프로그램을 작성해야 한다. 여기에서는 솔라리스와 리눅스를 비교해서 설명하도록 하겠다.

지금까지의 쓰레드와 시그널에 대해서 다루었던 내용은 솔라리스와 같이 하나의 프로세스에서 다중의 쓰레드를 관리하는 경우를 기준으로 했다. 그러나 리눅스의 경우 clone(2)를 통한 다중 프로세스형태로 쓰레드가 생성된다. 때문에 ps를 이용해서 확인할 경우 다중 쓰레드 프로세스임에도 불구하고 각각의 PID를 가지는 프로세스로 쓰레드가 생성되는걸 확인 할 수 있다.

이런 특징 때문에 리눅스 시스템에서 외부 프로세스에서 시그널을 특정 쓰레드로 보낼 경우에는 메인 쓰레드가 아닌 해당 쓰레드의 PID를 명시해 주어야 한다.

'Native > C' 카테고리의 다른 글

Video Capture에 대하여  (0) 2013.10.02
Packet Capture using libpcap  (0) 2013.10.02
간단한 c 프로그램과 cron으로 mysql 백업을 편하게...  (0) 2013.10.02
인자로서의 함수  (0) 2013.10.02
레기드 배열  (0) 2013.10.02