사이트 내 전체검색
[linux] 리눅스 서버 최적화 테크닉 키우기 1
로빈아빠
https://cmd.kr/server/174 URL이 복사되었습니다.

본문

리눅스 서버 최적화 테크닉 키우기 1

리눅스 커널의 이해와 최적화를 위한 컴파일러 설정

오픈소스의 매력을 만끽하자. 커널이 공개된 리눅스 운영체제를 사용하면서도 ‘커널최적화’라는 말은 어딘지 낯설다. 그만큼 리눅스는 일부 전문가만이 그 매력을 느낄수 있는 운영체제였다는 말이다. 파워 리눅스 유저로 가려면 ‘커널’이라는 높은 산을 넘어야 한다. 그 길로 가는 첫 걸음으로 리눅스 커널의 특징과 최적화를 위한 컴파일러 설정 방법에 대해 알아본다.

이상호 성균관대학교 산업용 네트워크 연구실

연재 순서
1회(2000. 07): 리눅스 커널 이해와 최적화를 위한 컴파일러 설정
2회: 커널 설정시 선택 사항과 애플리케이션 최적화
3회: 네트워크 서버와 라우터로서 리눅스 최적화 방법


--------------------------------------------------------------------------------

리눅스 시스템 운영자라면 누구나 서버 또는 워크스테이션 시스템을 최적화해 최대의 효율을 얻으려는 마음을 갖고 있을 것이다. 최적화의 의미는 같은 조건에서 보다 빠르게 또는 보다 작게라는 의미도 있지만 시스템의 안정성과 관리의 효율성의 의미도 함께 갖고 있다.

시스템 최적화는 커널 영역(Kernel Area)에서의 최적화와 애플리케이션 영역에서의 최적화 두 가지로 나눌 수 있다. 커널 영역에서의 최적화는 올바른 커널 구성을 통한 시스템 안정화와 시스템 관리 방법의 선택으로 생각할 수 있다. 애플리케이션 영역에서의 최적화는 httpd, ftpd 등의 서버 애플리케이션 또는 데몬(Daemon)의 설정과 선택, 애플리케이션 코드의 최적화와 관련된 것으로 생각할 수 있다.

커널 최적화는 전체 시스템에 폭 넓게 영향을 미치므로 일반적인 커널 구성과 최적화에 대해 생각해 보자.

프로그램 실행파일 및 커널 이미지 생성 운영체제는 여러 프로그램의 집합이다. 그 중 가장 핵심적인 부분인 커널 또한 프로그램이며 주기억장치에서 실행되는 것도 여느 프로그램과 크게 다르지 않다.
앞으로의 설명에 대해 이해가 쉽도록 먼저 기본적인 프로그램 생성 과정을 살펴보자.

일반적으로 프로그램 생성은 프로그래머가 작성한 원시 코드를 실행파일로 바꾸는 과정이다. 프로그래머가 작성하는 원시 코드에는 미리 만들어진 라이브러리와 프로그래머가 직접 작성한 라이브러리 또는 함수들이 사용된다. 이들 또한 각기 다른 파일로 이뤄져 있는 경우가 대부분이다. 라이브러리는 함수들이 미리 컴파일된 목적 코드(object code)의 집합이다.

목적 코드는 기계어로만 알고 있는 사람들이 많은데 다른 목적 코드와 서로 연결될 수 있는 정보를 포함하고 있다. 프로그래머가 직접 작성한 원시 코드 또한 컴파일 과정을 거치면 목적 코드가 되며, 다른 목적 코드나 라이브러리와 합쳐져야 올바른 실행파일이 생성된다. 각각의 목적 코드를 연결해 하나의 실행파일을 구성하는 과정을 링크라고 한다. 링크 과정을 거쳐 만들어진 실행파일은 실제 기계어 외에도 실행되기 위해 메모리에 상주(Load)되기 위한 정보를 함께 갖고 있다. 운영체제에서 로더(Loader)는 실행파일의 내용을 분석해 메모리에 로드시키는 역할을 한다.

그럼 커널 실행파일에 대해 알아보자. 일반적으로 우리가 사용하는 워드프로세서나 게임 프로그램의 파일 형태를 실행파일이라고 부르면서 커널을 커널 실행파일이라고 하지 않고 커널 이미지라고 부르는 이유는 무엇일까.

보통 애플리케이션 프로그램의 실행은 주기억장치에 상주한 후에 프로세스가 돼 처리된다. 보통 다른 프로세스들이 사용하지 않은 부분에 상주되기 때문에 실행파일 내의 실제 실행코드는 미리 주기억장치의 절대적인 어드레스를 가질 수 없다. 대신 상대적 또는 재배치 가능한 어드레스(relocatable address)를 갖고 있어 로더는 이를 바탕으로 프로그램을 상주시킨다.

그런데 커널의 실행파일은 일반 프로그램의 실행파일과 같은 상대적 어드레스 또는 재배치 가능한 어드레스를 가질 필요가 없다. 이유는 단순하다. 커널은 시스템에서 가장 먼저 프로세스화, 즉 가장 먼저 주기억장치에 상주되기 때문에 항상 같은 주기억장치의 번지에 상주된다. 커널을 파일 형태로 만들기 위해서는 메모리에 상주된 상태 그대로 파일 형태로 만들어야 하므로 실행파일이라고 하지 않고 이미지라고 한다. 커널을 주기억장치에 상주시키는 일은 lilo와 같은 boot-loader의 몫이다.
리눅스의 커널 이미지는 ‘/boot’에 vmlinuz라는 이름으로 돼 있을 것이다.

커널 구성 형태별 분류
커널의 구성 방식은 크게 자원 관리에 대한 모든 기능을 하나로 만든 마너리식(monolithic)과, 자주 쓰이는 기능만 기본 커널로 제공하고 나머지 커널은 선택적으로 사용될 수 있도록 분리한 방식으로 나눌 수 있다. 리눅스는 둘 중 어디에도 속하지 않은 중간 형태의 커널을 갖고 있다고 볼 수 있다.

마너리식 커널
마너리식 커널의 대표적인 예는 유닉스를 들을 수 있다. 마너리식 커널은 커널 이미지가 하나로 되어 있고 하드웨어 장치들이 초기화된 후 바로 메모리에 로드(load)돼 실행된다. 커널이 하나의 실행 이미지로 되어 있기 때문에 하드웨어 구성이 바뀌거나 커널 설정을 바꾸기 위해서는 커널 소스를 다시 컴파일해 커널 이미지를 새로 생성해야 한다. 구식 유닉스 시스템은 시스템 설정을 바꾸기 위한 과정이 오래 걸렸는데 이는 복잡한 커널이 다시 컴파일되는 과정을 거쳤기 때문이다.
오랫동안 리눅스를 사용해본 사람들은 한번쯤은 커널 컴파일 과정을 거쳐 시스템 설정을 바꾼 경험이 있을 것이다.

마너리식 커널은 하나의 실행 모듈로 이루어졌기 때문에 빠르게 실행되며 부팅 시간이 짧은 이점이 있지만 PC와 같이 수많은 종류의 장치가 있는 경우 디바이스 드라이버를 지원하는 데 어려움이 있고 사용하지 않은 기능까지 포함하는 경우가 많기 때문에 무거운 느낌을 준다.

μ 커널
μ 커널 구조는 시그널 또는 메시지 전달과 같은 기본적인 기능만을 기본 μ 커널 이미지로 만들어 놓고 사용자에게 필요한 기능들이 부팅시 또는 해당 기능이 필요할때 동적으로 커널에 링크하는 형태로 운영된다. PC 부품 가운데 비디오 카드만 보더라도 많은 종류가 있는데 커널에서 이들을 모두 제어하는 디바이스 드라이버를 갖기는 어렵다. 그래서 하드웨어 업체는 커널에 동적으로 링크될 수 있는 형태의 디바이스 드라이버를 제공한다. 리눅스에서는 동적으로 링크되는 방법으로 모듈을 사용하고 있다.

커널 구조의 시스템 부팅은 먼저 최소의 커널이 메모리에 로드된 후 다른 동적 라이브러리가 이미 로드된 커널에 붙어서 동작한다. 리눅스는 모듈을 사용함으로써 유연한 동작이 가능해졌지만 마너리식 커널 구조에 더 가깝다.

리눅스 커널의 구조
리눅스는 유닉스 계열 운영체제와 마찬가지로 고정적인 마너리식 커널에서 출발했지만 모듈이라는 동적 링크 개념을 수용하면서 좀더 유연한 형태의 커널로 발전했다.
리눅스에서 사용되는 사운드 장치나 네트워크 장치 등의 디바이스 드라이버는 모듈 형태로 만들어진 것이다.

리눅스에서 프로세스 매니지먼트나 메모리 매니지먼트, 파일 시스템을 사용자별로 서로 다른 방법을 선택해 사용하는 경우는 드물 것이다. 사운드 카드의 경우 사용자마다 다른 것을 사용하는 경우가 많은데 커널에 수십 가지의 사운드 카드 제어 기능을 넣기는 어려울 것이고, 사용자들이 사운드카드의 설정을 위해 커널을 재컴파일하는 것 또한 그리 좋아하지는 않을 것이다. 자신에게 맞는 사운드 카드 제어 프로그램을 커널에 동적으로 링크시켜 커널이 사운드 카드를 제어할 수 있게 한 것이 현재의 모듈이다. 윈도우나 기타 운영체제에서는 이 방법이 훨씬 더 일찍부터 보편화됐다는 것을 독자들은 잘 알고 있을 것이다.

모듈은 장치 관리에만 사용되는 것이 아니라 커널 구성 요소의 대부분을 모듈로 구성할 수 있다.

리눅스 커널 설정과 업그레이드
일반적으로 리눅스를 설치한 다음 커널을 별도로 재설정하지 않고 사용하지만 조금이라도 시스템 최적화 의지가 있다면 컴파일을 통한 커널 설정 변경을 그냥 지나칠 수 없을 것이다. 커널 설정 이전에 미리 생각할 점은 다음과 같다.

1 커널에 포함할 기능과 모듈의 사용범위: 서버의 활용 범위를 규정한다. 용도에 따라 필요한 기능의 선택과 선택된 각 기능을 코어 커널에 포함시킬지 아니면 모듈로 사용할지를 결정한다.

2 컴파일러 설정: 최적화된 커널 이미지를 만들기 위해서는 하드웨어의 특성을 고려해 컴파일할 수 있도록 컴파일러 옵션을 설정한다.

커널을 컴파일하기 전에 컴파일러의 옵션에 관한 설정이 우선돼야 하기 때문에 이를 먼저 살펴보자.

컴파일러 옵션
컴파일러의 선택 사항에 따라 실행코드 생성이 어떻게 달라지는지 간단한 예를 통해 살펴보자. 다음과 같은 간단한 프로그램 코드를 컴파일해 선택 사항에 따라 생성되는 실행코드가 다르다는 것을 알 수 있다.

#include <stdio.h>
main()
{ double df;
  float ff;
  int i;
  i=0;
  df=12345.0; ff=1.3;
  for(i=0; i<10; i++){ df=df/2; ff=ff*2; }
  printf(“ %f , %f ”, df, ff);
}

gcc의 -S 옵션을 사용하면 어셈블리어로 번역돼 결과가 출력된다. 예제 프로그램 파일의 이름이 opt_test.c라고 가정하고 -S 옵션만을 사용해 예제 프로그램을 컴파일해 보자.

gcc -S opt_test.c

그러면 opt_test.s라는 어셈블리 코드로 이뤄진 파일이 생성된다. 다음과 같은 에디터로 생성된 어셈블리 코드를 살펴볼 수 있다.

.file “opt_test.c”
.version “01.01”
gcc2_compiled.:
.section .rodata
.LC1:
  .string “ %f , %f ”
  .align 8
.LC0:
  .long 0x0,0x40000000
.text
  .align 4
.globl main
  .type main,@function
main:
  pushl %ebp
  movl %esp,%ebp
  subl $20,%esp
  movl $0,-16(%ebp)
  movl $0,-8(%ebp)
  movl $1086856320,-4(%ebp)
  movl $1067869798,-12(%ebp)
  movl $0,-16(%ebp)
.p2align 4,,7
.L2:
  cmpl $9,-16(%ebp)
  jle .L5
  jmp .L3
  .p2align 4,,7
.L5:
  fldl -8(%ebp)
  fldl .LC0
  fstp %st(0)
  fldl .LC0
  fdivrp %st,%st(1)
  fstpl -8(%ebp)
  flds -12(%ebp)
  fstps -20(%ebp)
  flds -20(%ebp)
  fadds -20(%ebp)
  fstps -12(%ebp)
.L4:
  incl -16(%ebp)
  jmp .L2
  .p2align 4,,7
.L3:
  flds -12(%ebp)
  subl $8,%esp
  fstpl (%esp)
  fldl -8(%ebp)
  subl $8,%esp
  fstpl (%esp)
  pushl $.LC1
  call printf
  addl $20,%esp
.L1:
  leave
  ret
.Lfe1:
  .size main,.Lfe1-main
  .ident “GCC: (GNU) egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)”

이어서 최적화 옵션(Optimize option)을 사용해 생성되는 코드와 비교해 보자. -O 옵션 바로 뒤에 최적화 정도를 입력할 수 있는데 9가 가장 높은 수준의 최적화이다.

gcc -S -O9 opt_test.c

생성된 어셈블리 코드를 이전에 생성된 코드와 비교해 보자.

    .file “opt_test.c”
  .version “01.01”
gcc2_compiled.:
.section .rodata
.LC3:
  .string “ %f , %f ”
  .align 8
.LC4:
  .long 0x0,0x40c81c80
  .align 4
.LC5:
  .long 0x3fa66666
  .align 8
.LC6:
  .long 0x0,0x3fe00000
.text
    .align 4
.globl main
    .type main,@function
main:
  pushl %ebp
  movl %esp,%ebp
  movl $9,%eax
  fldl .LC4
  flds .LC5
  fldl .LC6
  jmp .L2
.L22:
  fxch %st(1)
  .p2align 4,,7
.L20:
  fmul %st,%st(2)
  fxch %st(1)
  fadd %st(0),%st
  decl %eax
  jns .L22
  fstp %st(1)
  subl $8,%esp
  fstpl (%esp)
  subl $8,%esp
  fstpl (%esp)
  pushl $.LC3
  call printf
  leave
  ret
.Lfe1:
  .size main,.Lfe1-main
  .ident “GCC: (GNU) egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)”

마지막으로 최적화에 관련된 모든 옵션을 사용해 보자. 몇 가지 옵션은 CPU에 따라 최적의 실행코드를 생성해주는 옵션이다. CPU가 펜티엄 프로, 펜티엄 II, 펜티엄 III인 경우 다음과 같이 여러 옵션을 사용해 컴파일한다.

gcc -S -O9 -funroll-loops -ffast-math -malign-double -mcpu=pentiumpro -march=pentiumpro -fomit-frame-pointer -fno-exceptions opt_test.c

생성된 어셈블리 코드를 보면 엄청나게 짧게 변했음을 알 수 있다.

    .file “opt_test.c”
  .version “01.01”
gcc2_compiled.:
.section .rodata
.LC3:
  .string “ %f , %f ”
.text
  .align 4
  .globl main
.type main,@function
main:
  pushl $1083493580
  pushl $-1073741824
  pushl $1076370560
  pushl $0
  pushl $.LC3
  call printf
  addl $20,%esp
  ret
.Lfe1:
  .size main,.Lfe1-main
  .ident “GCC: (GNU) egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)”

보통 소스 코드 형식으로 배포되는 프로그램들은 make를 이용해 전체 프로그램을 컴파일한다. make의 입력파일 Makefile을 살펴보면 CFLAGS라는 변수를 통해 gcc에 옵션을 주기 때문에 CFLAGS를 미리 시스템의 전역 변수로 설정하면 된다. 보통 GNU 형식의 배포본은 configure를 이용해 Makefile을 생성하는데, 전역 변수로 설정하면 configure는 자동으로 이를 인식해 Makefile에 적용시킨다. 커널의 경우 configure를 사용해 Makefile을 정의하지 않기 때문에 직접 Makefile의 설정을 수동으로 할 필요가 있다. 기본적인 시스템 설정은 /etc/profile에 정의하는데 CFLAGS를 /etc/profile에 정의하자. 다음 예와 같이 펜티엄 프로 이상의 CPU를 사용하는 것을 가정하면 /etc/profile에 다음과 같이 CFLAGS 정의를 추가한다.

CFLAGS= -O9 -funroll-loops -ffast-math -malign-double -mcpu=pentiumpro -march=pentiumpro -fomit-frame-pointer -fno-exceptions

/etc/profile을 잘 살펴보면 export가 있을 것이다. export란의 끝에 CFLAGS LANG LESSCHARSET을 추가한다.

export PATH PS1 USER LOGNAME MAIL HOSTNAME HISTSIZE HISTFILESIZE INPUTRC CFLAGS LANG LESSCHARSET

이제 다시 로그인하면 새롭게 /etc/profile의 내용이 적용된다.

커널 컴파일하기
gcc의 옵션에 따른 실행코드 최적화에 대해 살펴보았으므로 실제 커널 컴파일을 해보자.

리눅스의 소스 코드가 위치한 /usr/src/linux로 이동, 에디터를 이용해 Makefile을 편집해야 한다. 먼저 CFLAGS와 값 이외의 HOSTFLAGS, 컴파일러 선택에 대한 값을 바꿔주자. CFLAGS는 펜티엄 프로 이상에서는 다음과 같이 바꾼다.

CFLAGS = -Wall -Wstrict-prototypes -O9 -funroll-loops -ffast-math -malign-double -mcpu=pentiumpro -march=pentiumpro -fomit-frame-pointer -fno-eceptions

HOSTCFLAGS도 CFLAGS와 같은 값을 갖는다.

HOSTCFLAGS = -Wall -Wstrict-prototypes -O9 -funroll-loops -ffast-math -malign-double -mcpu=pentiumpro -march=pentiumpro -fomit-frame-pointer -fno-eceptions

컴파일할 때 gcc가 아닌 egcs를 사용하기 위해서 다음 두 가지의 값을 찾아서 바꾼다.

HOSTCC=egcs
CC=$(CROSS_COMPILE)egcs -D_KERNEL_ -I$(HPATH)

이제 /usr/src/linux에서 make config를 입력해 컴파일할 커널의 설정을 시작한다.
이후 작업은 각 선택된 기능들 간의 의존성 체크를 하는 make dep, 새로운 목적 코드를 만들기 위해 이전의 목적 코드를 삭제하는 make clean06 과정을 거쳐 마지막으로 make bzImage를 입력해 컴파일을 마친다. 새로 만들어진 커널은 /usr/src/linux/arch/i386/bzImage이다. 아직 모듈 컴파일이 되지 않았기 때문에 가능하면 새로 생성된 커널 이미지 사용은 보류하자. /boot/vmlinuz는 보통 /boot/vmlinuz-version으로 이름이 붙여진 커널에 링크돼 있다.

다음호에서는 make config 과정에서의 주의 사항과 최적화된 커널의 분석, 그리고 데몬 프로세스의 최적화에 대해 살펴볼 계획이다.

필자 연락처 : turtle@ece.skku.ac.kr
정리 : 박세영 andrea@sbmedia.co.kr

댓글목록

등록된 댓글이 없습니다.

1,139 (15/23P)

Search

Copyright © Cmd 명령어 3.21.246.53