본문 바로가기
CS 지식/시스템 프로그래밍

[C언어] 프로세스 Sleep개념과 관련 시스템 콜(System call)

by 코딩하는 동현 2025. 3. 2.

프로세스와 Sleep 개념

프로세스가 시간이 걸리는 특정 연산(IO, Network)을 수행해야 할 때, 프로세스는 할 일이 없으므로 무한정 대기 상태에 빠진다. 이를 방지하기 위해 운영체제는 I/O 요청을 보낸 후 해당 프로세스를 sleep 상태로 전환하고, 다른 프로세스가 CPU를 사용할 수 있도록 한다. 이후 커널이 해당 프로세스를 다시 깨우면 실행이 재개된다. 이는 운영체제의 스케줄링 기법 중 하나이다.


Low Resolution Sleeping (저해상도, 초단위 sleep)

sleep() 함수

unsigned int sleep(unsigned int seconds);

동작 방식

  • seconds 만큼 프로세스를 sleep 시킨다.
  • 성공하면 0을 반환한다.
  • 신호(Signal) 등의 이유로 sleep이 조기에 종료될 경우, 남은 시간을 반환한다.

의도한 시간만큼 정확하게 sleep을 보장하는 방법

unsigned int s = 3;
while ((s = sleep(s)))
    ;

시스템 Clock별 Sleeping 시간 비교 예제

#define BILLION 1000000000L

int main(int argc, char *argv[]) {
    struct timespec start, end;
    uint64_t diff;
    clockid_t clk_id;
    clockid_t clocks[] = {
        CLOCK_REALTIME,
        CLOCK_MONOTONIC,
        CLOCK_PROCESS_CPUTIME_ID,
        CLOCK_THREAD_CPUTIME_ID
    };

    clk_id = clocks[atoi(argv[1]) % 4];

    clock_gettime(clk_id, &start);
    sleep(3);
    clock_gettime(clk_id, &end);

    diff = BILLION * (end.tv_sec - start.tv_sec) + end.tv_nsec - start.tv_nsec;
    printf("Time taken: %llu nanoseconds\n", (unsigned long long)diff);
    return 0;
}

 

실행 결과

  • CLOCK_REALTIME과 CLOCK_MONOTONIC은 약 3초 정도 소요됨.
  • CLOCK_PROCESS_CPUTIME_ID와 CLOCK_THREAD_CPUTIME_ID는 프로세스 실행 시간만을 측정하므로 sleep 시간이 제외됨.

차례대로 real, monotonic, process, thread time으로 측정한 결과이다.
real, monotonic은 예상대로 3초에서 살짝 더 시간 걸린것을 확인할수 있다(컨텍스트 스위칭 시간)
process, thread time은 커널 코드가 아닌 프로세스의 유저 코드의 시간(실제로 동작할 시간)이므로 3초가 제외된 순수 코드 프로세싱한 나노초가 나왔다.


High Resolution Sleeping (고해상도, 나노초, 마이크로초 단위 sleep)

nanosleep() 함수

int nanosleep(const struct timespec *req, struct timespec *rem);

동작 방식

  • req에 지정된 시간 동안 프로세스를 sleep 상태로 만든다.
  • 신호 등으로 중단될 경우 rem에 남은 시간을 저장한다.
  • 성공하면 0, 실패하면 -1 반환.

clock_nanosleep() 함수 (클럭 지정 가능)

int clock_nanosleep(clockid_t clock_id, int flags, const struct timespec *req, struct timespec *rem);

주요 특징

  • clock_id를 지정하여 원하는 클럭을 사용할 수 있음.
  • flags에 TIMER_ABSTIME을 설정하면 절대 시간 기준으로 sleep 가능.

시그널을 이용하여 깨우는 예제

#define BILLION 1000000000L

void my_handler(int sig) {
    write(1, "woke up!\n", 10);
}

int main() {
    struct timespec start, end, req, rem;
    uint64_t diff;
    int rv;

    signal(SIGUSR1, my_handler);

    req.tv_sec = 5;
    req.tv_nsec = 500000000;

    clock_gettime(CLOCK_MONOTONIC, &start);
    rv = clock_nanosleep(CLOCK_MONOTONIC, 0, &req, &rem);
    clock_gettime(CLOCK_MONOTONIC, &end);

    diff = BILLION * (end.tv_sec - start.tv_sec) + end.tv_nsec - start.tv_nsec;
    printf("Time taken: %llu nanoseconds\n", (unsigned long long)diff);

    if (rv) {
        printf("remaining time = %ld s, %ld ns\n", rem.tv_sec, rem.tv_nsec);
    }
    return 0;
}

select()를 이용한 마이크로초 단위 sleep

int main() {
    struct timeval start, end, timeout;
    uint64_t diff;
    int rv;

    timeout.tv_sec = 5;
    timeout.tv_usec = 500000;

    gettimeofday(&start, NULL);
    rv = select(0, NULL, NULL, NULL, &timeout);
    gettimeofday(&end, NULL);

    diff = 1000000 * (end.tv_sec - start.tv_sec) + end.tv_usec - start.tv_usec;
    printf("Time = %llu us\n", (unsigned long long)diff);
    return 0;
}

Overruns (Sleep 시간 초과 현상)

운영체제는 요청된 시간 이상을 sleep하도록 보장한다. 즉, 요청된 시간보다 더 오래 sleep하는 경우가 발생할 수 있다.

Sleep 시간이 초과되는 주요 원인

  1. Timer Granularity (타이머 해상도)
    • 타이머의 간격보다 요청된 시간이 더 짧으면, 원하는 시간 정확도로 타이머를 작동시킬 수 없음.
  2. Delayed Scheduling (지연된 스케줄링)
    • 프로세스가 sleep에서 깨어났더라도, CPU가 다른 작업을 수행 중이면 즉시 실행되지 않을 수 있음.

결론

  • sleep()은 초 단위의 저해상도 sleep을 제공하며, 신호에 의해 조기 종료될 수 있다.
  • nanosleep()과 clock_nanosleep()은 나노초 단위의 고해상도 sleep을 제공하며, 특정 클럭을 지정할 수 있다.
  • select()를 이용하면 마이크로초 단위 sleep도 가능하다.
  • 운영체제의 타이머 해상도와 스케줄링 정책에 따라 정확한 sleep 보장이 어려울 수 있음을 고려해야 한다.

 

반응형

댓글