AMD Opteron CPU by Samat Jain

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#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에 고정적으로 할당되는 것처럼 보일 수도 있으나 많은 횟수를 실행시켜보면 변한다는 것을 확인 할 수 있습니다.

 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
64
65
66
67
68
69
70
71
72
73
#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 포인터는 아래의 매크로 함수들을 사용해서 편리하게 설정 할 수 있습니다.

1
2
3
4
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 자원을 어느 정도 보장 해주는데 도움이 될 수 있다고 생각합니다.