요즘 개인용 컴퓨터도 멀티코어를 장착하고 나올 정도로 멀티프로세서(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 자원을 어느 정도 보장 해주는데 도움이 될 수 있다고 생각합니다.