프로세스를 지정한 CPU에서 실행하기

AMD Opteron CPU by Samat Jain

즘 개인용 컴퓨터도 멀티코어를 장착하고 나올 정도로 멀티프로세서(Multiprocessor)가 흔해졌습니다. 덕분에 멀티프로세스(Multiprocess) 프로그래밍으로 개발한 데몬과 같은 것을 실행시켜 보면 여러 프로세서에 적당히 나뉘어 실행되는 것을 쉽게 확인 할 수 있습니다.

그런데 여기에 한가지 욕심을 더 내보자면 특정한 작업을 수행하는 프로세스를 특정한 프로세서에 할당하고 싶다는 생각이 드는 경우가 있습니다. 네트워크 데이터 및 DB 처리는 0번 CPU, 데이터 처리는 1번 CPU 식으로 말입니다.

기본적으로 OS에서 프로세스를 어떤 CPU에 할당하는지는 OS가 가진 자체적인 스케쥴링에 따르도록 되어 있습니다.

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

int main(int argc, char *argv[]) {
    unsigned int i = 0;
    pid_t pid;

    if ( (pid = fork()) == 0 ) {
        for ( i = 0; i < UINT_MAX; i++) {
        }
    }
    else {
        int status;
        waitpid(pid, &status, 0);
    }

    return EXIT_SUCCESS;
}

위 소스는 1개의 자식 프로세스를 생성하고 더하기 연산을 반복적으로 수행하여 CPU를 100% 사용하게 만드는 예제입니다.

터미널을 하나 더 열어 top을 실행시킨 후 ‘1’번 키를 눌러 CPU 별로 사용량을 지켜볼 수 있게 준비를 합니다. 그리고, 소스를 컴파일 해서 여러번 실행시켜 보면 사용량이 100%에 달하는 CPU가 고정적이지 않고 변하는 것을 확인 할 수 있습니다. 물론 OS의 스케쥴링 정책에 영향을 받기 때문에 특정한 CPU에 고정적으로 할당되는 것처럼 보일 수도 있으나 많은 횟수를 실행시켜보면 변한다는 것을 확인 할 수 있습니다.

#define _GNU_SOURCE

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <getopt.h>
#include <sched.h>

void print_help(char *cmd) {
    printf("Usage: %s -n <cpu 개수> -c < 선호CPU>\n\n", cmd);
    printf("       CPU 개수 : CPU 코어 개수\n");
    printf("       선호 CPU : CPU 코어 번호 (0 부터 시작)\n\n");
    printf("       예 : 쿼드코어 CPU에서 3번째 코어를 사용하는 경우\n");
    printf("            $ %s -n 4 -c 2\n", cmd);
}

int main(int argc, char *argv[]) {
    unsigned int i = 0;
    pid_t pid;
    int max_cpu = -1;
    int cpu = -1;
    int opt;

    while ( (opt = getopt(argc, argv, "n:c:")) != -1 ) {
        switch ( opt ) {
            case 'c' :
                cpu = atoi(optarg);
                break;
            case 'n' :
                max_cpu = atoi(optarg);
                break;
            case '?' :
            default :
                print_help(argv[0]);
                exit(EXIT_FAILURE);
                break;
        }
    }

    if ( max_cpu < 1 || cpu < 0 || cpu >= max_cpu ) {
        print_help(argv[0]);
        exit(EXIT_FAILURE);
    }

    if ( (pid = fork()) == 0 ) {
        cpu_set_t mask;

        CPU_ZERO(&mask);
        CPU_SET(cpu, &mask);
        pid = getpid();
        if ( sched_setaffinity(pid, sizeof(mask), &mask) ) {
            fprintf(stderr, "%d 번 CPU를 선호하도록 설정하지 못했습니다.\n",
                    cpu);
            exit(EXIT_FAILURE);
        }
        else {
            printf("%d 번 CPU를 선호하도록 설정했습니다.\n", cpu);
        }

        for ( i = 0; i < UINT_MAX; i++) {
        }
    }
    else {
        int status;
        waitpid(pid, &status, 0);
    }

    return EXIT_SUCCESS;
}

위 소스 코드는 sched.h에서 제공하는 sched_setaffinity 함수를 사용하여 특정한 CPU에서 프로세스가 실행되도록 한 것입니다.

sched_setaffinity 함수는 3개의 매개변수를 받는데 첫번째는 프로세스 ID(pid)입니다. pid 대신 0을 넘기면 자동으로 현재 동작중인 프로세스로 설정됩니다. 두번째는 cpusetsize 입니다. 보통은 sizeof(cpu_set_t)로 설정하면 됩니다. 세번째는 mask 포인터입니다. mask 포인터는 아래의 매크로 함수들을 사용해서 편리하게 설정 할 수 있습니다.

void CPU_CLR(int cpu, cpu_set_t *set);
int CPU_ISSET(int cpu, cpu_set_t *set);
void CPU_SET(int cpu, cpu_set_t *set);
void CPU_ZERO(cpu_set_t *set);

cpu는 CPU의 번호로 0번부터 시작합니다. 쿼드코어 CPU 라면 0~3번 사이의 값이 됩니다. mask 값을 여러개의 CPU로 지정하는 것도 가능합니다.

sched.h가 정상적으로 동작하기 위해서는 꼭 헤더파일을 인클루드 하기 전에 #define _GNU_SOURCE를 선언 해주어야 합니다. 선언하지 않으면 CPU_XXXXX 매크로 함수를 찾을 수 없다며 컴파일 오류가 발생합니다.

멀티프로세스나 멀티쓰레드를 여러개의 CPU나 코어에 적절히 배치하여 효과적으로 사용하는 것은 매우 어려운 기술입니다. sched_setaffinity 함수를 통해 수동으로 배치했다고 해서 그것이 반드시 OS의 스케쥴링에 의한 배치보다 효율적이라는 보장은 없습니다.

다만 몇가지 특징적인 프로세스들을 적절히 배치하여 CPU 자원을 어느 정도 보장 해주는데 도움이 될 수 있다고 생각합니다.

updatedupdated2021-01-142021-01-14