본문 바로가기

Core BSP 분석/리눅스 커널 핵심 분석

[리눅스] 프로세스를 지정한 CPU에서 실행: sched_setaffinity() 함수

소형 임베디드 장비를 제외하고는 대부분 시스템은 멀티 CPU 코어 환경에서 개발됩니다. 
멀티 프로세스(Multiprocess) 기반으로 작성된 데몬을 실행하면 여러 CPU 코어에 적당히 나뉘어 실행되는 것을 쉽게 볼 수 있습니다. CPU0에서 실행됐다가 CPU2에서 실행되는 것이죠.
 
그런데 가끔은 특정 작업을 수행하는 프로세스가 특정 CPU 코어에서 실행되도록 설정해야 할 때가 있습니다. 여러가지 이유가 있지만 요약하면 다음과 같습니다.
 
    * 실행하는 프로세스와 관련된 인터럽트가 특정 CPU 코어에서만 트리거됨(IRQ Affinity라고 하죠.)
    * 프로세스가 여러 CPU 코어에 옮겨 다니면서 실행할 때 요구되는 오버헤드(마이그레이션)을 최소화하자
 
참고로 커널이 프로세스를 어떤 CPU 코어에 할당하는지는, 커널 내부의 자체적인 스케쥴링 메카니즘에 따라 결정됩니다. 
 
간단한 예제 코드로 실습하기
 
이어서 다음 코드를 보면서 설명을 이어나겠습니다.
 
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/wait.h>
 
int main(void) 
{
unsigned int i = 0;
pid_t pid;
 
if ((pid = fork()) == 0 ) {
for ( i = 0; i < UINT_MAX; i++) {
printf("I am running\n");
}
}
else {
int status;
waitpid(pid, &status, 0);
}
 
return EXIT_SUCCESS;
}
 
위 소스는 1개의 자식 프로세스를 생성하고 더하기 연산을 반복적으로 수행하여 CPU를 100% 사용하게 만드는 예제입니다. 
 
위 코드를 작성한 후 raspbian_test.c 이란 이름으로 저장합니다. 이어서 다음 구문을 입력해 Makefile을 생성합니다.
 
#Makefile
raspbian_proc: raspbian_test.c
gcc -o raspbian_proc raspbian_test.c
 
Makefile을 만든 다음에 make를 입력하면 raspbian_test.c을 손쉽게 컴파일할 수 있습니다.
아래는 make 명령어를 입력한 후의 터미널 출력 결과입니다.
 
root@raspberrypi:/home/pi/work/test_affinity# make
gcc -o raspbian_proc raspbian_test.c
 
이어서 ./raspbian_proc 명령어를 입력해 작성한 코드를 실행합니다.
 
root@raspberrypi:/home/pi/work/test_affinity# ./raspbian_proc 
I am running
I am running
I am running
 
예제 코드 실행 결과 분석하기
 
터미널을 하나 더 열어 top을 실행시킨 후 '1'번 키를 눌러 CPU 별로 사용량을 지켜볼 수 있게 준비를 합니다. 
 
root@raspberrypi:/home/pi# top
top - 10:27:42 up 51 min,  5 users,  load average: 2.20, 0.72, 0.26
Tasks: 146 total,   6 running, 140 sleeping,   0 stopped,   0 zombie
%Cpu0  : 12.9 us, 49.4 sy,  0.0 ni, 33.2 id,  0.0 wa,  0.0 hi,  4.4 si,  0.0 st
%Cpu1  : 16.4 us, 55.5 sy,  0.0 ni, 26.7 id,  0.0 wa,  0.0 hi,  1.4 si,  0.0 st
%Cpu2  :  7.1 us, 59.1 sy,  0.0 ni, 33.8 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  :  4.5 us, 36.0 sy,  0.0 ni, 57.4 id,  0.0 wa,  0.0 hi,  2.1 si,  0.0 st
MiB Mem :   1939.5 total,   1494.6 free,     93.7 used,    351.1 buff/cache
MiB Swap:    100.0 total,    100.0 free,      0.0 used.   1662.1 avail Mem
 
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
  132 root      20   0   45984  17828  16852 S   99.3   0.9   0:33.90 raspbian_proc
 
그리고, 소스를 컴파일 해서 여러번 실행시켜 보면 사용량이 100%에 달하는 CPU가 고정적이지 않고 변하는 것을 확인 할 수 있습니다. 
 
이번에는 ftrace 메시지를 통해 위 코드가 실행될 때 CPU 코어의 번호를 확인해 보겠습니다. 아래 ftrace 메시지를 볼까요? 
 
raspbian_proc-1106  [002] d...  4086.493934: sched_switch: prev_comm=raspbian_proc prev_pid=1 106 prev_prio=120 prev_state=R+ ==> next_comm=kworker/u8:2 next_pid=1019 next_prio=120
...
raspbian_proc-1106  [001] d...  4086.583273: sched_switch: prev_comm=raspbian_proc prev_pid=1 106 prev_prio=120 prev_state=R+ ==> next_comm=kworker/u8:0 next_pid=1084 next_prio=120
 
4086.493934 초에는 2번째 CPU 코어에서 실행하다가, 4086.583273초에는 1번째 CPU 코어에서 해당 코드가 실행 중이란 사실을 알 수 있습니다. 
 
sched_setaffinity() 함수를 사용해 CPU 코어를 지정하는 예시 코드
 
이번에는 해당 프로세스를 특정 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);
printf("cpu: %d \n", cpu);
break;
 
case 'n' :
max_cpu = atoi(optarg);
printf("max_cpu: %d \n", max_cpu);
break;
 
case '?' :
default :
print_help(argv[0]);
exit(EXIT_FAILURE);
break;
}
}
 
printf("@: %d \n", __LINE__);
 
if ( max_cpu < 1 || cpu < 0 || cpu >= max_cpu ) {
print_help(argv[0]);
printf("@: %d \n", __LINE__);
exit(EXIT_FAILURE);
}
 
if ( (pid = fork()) == 0 ) {
cpu_set_t mask;
printf("@: %d \n", __LINE__);
 
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 포인터는 아래의 매크로 함수들을 사용해서 편리하게 설정 할 수 있습니다.
 
위 코드를 컴파일한 다음에 다음 명령어를 입력해 실행해 볼까요?
 
root@raspberrypi:/home/pi/work/test_affinity# ./raspbian_proc -n 4 -c 0
max_cpu: 4
cpu: 0
@: 49
@: 59
0 번 CPU를 선호하도록 설정했습니다.
 
전체 CPU 코어의 갯수는 4, 지정하려는 CPU 번호는 0으로 설정해 실행했습니다.
 
로그 분석
 
아래는 위 코드를 실행한 후 확인한 ftrace 메시지입니다.
 
raspbian_proc-1177  [000] d...  4556.349390: sched_switch: prev_comm=raspbian_proc prev_pid=1177 prev_prio=120 prev_state=R ==> next_comm=irq/36-mmc1 next_pid=83 next_prio=49
raspbian_proc-1177  [000] d...  4556.454209: sched_switch: prev_comm=raspbian_proc prev_pid=1177 prev_prio=120 prev_state=R ==> next_comm=kworker/0:0 next_pid=1142 next_prio=120
kworker/0:0-1142  [000] d...  4556.454225: sched_switch: prev_comm=kworker/0:0 prev_pid=1142 prev_prio=120 prev_state=D ==> next_comm=raspbian_proc next_pid=1177 next_prio=120
 
보다시피, 0번째 CPU에서 위 코드가 실행했다는 사실을 알 수 있습니다.
 
cpu는 CPU의 번호로 0번부터 시작합니다. 쿼드코어 CPU를 탑재한 라즈베리 파이에서는 0~3번 사이의 값이 됩니다. 또한 mask 값을 여러개의 CPU로 지정하는 것도 가능합니다.
 
참고로 아래는 CPU 코어를 설정하는데 필요한 함수 목록입니다.
 
 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나 코어에 적절히 배치하여 효과적으로 사용하는 것은 매우 어려운 기술입니다. sched_setaffinity 함수를 통해 수동으로 배치했다고 해서 그것이 반드시 커널의 스케줄러에 의해 선택되는 동작보다 효율적이라는 보장은 없습니다.
 
다만 몇가지 특징적인 프로세스들을 적절히 배치하여 CPU 자원을 어느 정도 보장 해주는데 도움이 될 수 있습니다.
 
Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자
 
 
 
 
 
다음 포스트에서는 sched_setaffinity() 함수를 유저 프로세스에서 실행하면 커널에서 어떤 방식으로 처리되는지 소개합니다.