The truth will set you free

sunyzero.egloos.com

포토로그 마이가든



Oracle 11g on Fedora11 (or Fedora9) Com. Science

Oracle 11g R2를 설치한 과정을 자세한 그림과 함께 살펴보도록 하겠다.
* 목차
0. 설치전 확인
1. 오라클 설치 프로그램 실행
2. 데이터베이스 보안 갱신 구성
3. 데이터베이스 설치 옵션
4. 시스템 클래스 설정
    (4-A) 데스크탑 클래스를 지정한 경우
    (4-B) 서버 클래스를 지정한 경우
5. 필요 조건 검사 수행
6. 설치 옵션 요약
7. 제품 설치 시작 (or 데이터베이스 생성)
8. 오라클 엔터프라이즈 매니저
9. 오라클 환경 변수의 수정


우선 오라클을 설치한 테스트 플랫폼은 다음과 같다.

  • 호스트 1: Fedora 9
  • 호스트 2: Fedora 11
오라클 설치 프로그램은 X윈도우 기반이므로 X윈도우가 일단 설치되어있어야 한다. 그리고 GNOME환경을 추천하는데 이는 간혹 KDE나 OpenGL의 compiz에서는 메시지 창이 먹통이 되는 경우가 있기 때문이다. 설치 과정은 시스템에 따라서 다르지만 테스트한 E8400 CPU에서는 프로그램 설치에 약 50여분, DB생성에 약 40여분이 걸렸었다. 따라서 넉넉하게 2시간 걸렸다.


0. 설치전 확인

첫번째로 SELINUX 설정을 Permissive로 바꿔야 한다.(enforcing레벨에서 디렉토리, 리소스 권한 조절이 가능하면 해도 된다. 하지만 그 정도 기술이 있으면 최소한 expert 레벨이므로 이런 글을 보지 않을 것이라고 확신하기에 permissive로 바꾸는 것부터 하겠다.) SELINUX 설정의 확인은 sestatus로 확인가능하고 현재 설정을 바꾸는 것은 setenforce를 사용한다. 아래 예를 보자.

[root@localhost ~]# sestatus
SELinux status: enabled
SELinuxfs mount: /selinux
Current mode: enforcing
Mode from config file: enforcing
Policy version: 24
Policy from config file: targeted
[root@localhost ~]# setenforce permissive
[root@localhost ~]# sestatus
SELinux status: enabled
SELinuxfs mount: /selinux
Current mode: permissive
Mode from config file: enforcing
Policy version: 24
Policy from config file: targeted
"setenforce permissive"대신에 "setenforce 0"으로 명령해도 결과는 같다. 길게 permissive로 치는게 더 직관적일 것 같아서 예제는 길게 타이핑했다. 그런데 Current mode가 permissive로 바뀌었지만 여전히 Mode from config file이 enforcing이므로 재부팅하면 다시 enforcing이 된다. 따라서 설정 파일을 바꿔야 한다. 설정 파일은 /etc/sysconfig/selinux이다. vim 에디터로 열어보자.

# This file controls the state of SELinux on the system.
# SELINUX= can take one of these three values:
# enforcing - SELinux security policy is enforced.
# permissive - SELinux prints warnings instead of enforcing.
# disabled - No SELinux policy is loaded.
SELINUX=enforcing
# SELINUXTYPE= can take one of these two values:
# targeted - Targeted processes are protected,
# mls - Multi Level Security protection.
SELINUXTYPE=targeted
중간에 "SELINUX=enforcing"의 부분을 "SELINUX=permissive"로 고쳐놓으면 다음번에 재부팅할때도 permissive로 보이게 된다. 설정후 다시 sestatus를 실행하면 Current mode와 Mode from config file 부분이 둘다 permissive로 되어 있을 것이다.

두번째로 오라클 설치에 필요한 공간을 확보해야 한다. 우선 RAM 공간부터 확인하자.
  • RAM 메모리 : 2G 이상 (최소 1G이지만 2G는 되어야 한다.)
  • SWAP 메모리 : RAM 크기 or 최대 2배 (RAM이 2G이면 SWAP도 2G정도면 충분하다.)

이제는 디스크 공간을 확인해야 한다.

  • Oracle 11g 압축을 풀어놓을 임시 공간 : 약 3G
  • Oracle 11g 프로그램을 설치할 공간 : 약 7G
  • Oracle 데이터베이스를 구축할 공간 : 약 4G
  • /tmp 디렉토리 : 최소 400M 정도의 빈 공간
위의 공간은 조금 여유있게 잡은 크기이다. 설치할 공간과 DB구축의 공간을 최소로 잡으면 약 8G정도에도 가능하다.(하지만 넉넉하게 잡기를 권장한다) 오라클은 일반적으로 /u01, /u02 이런 식의 디렉토리에 설치한다. 꼭 그래야 되는 것은 아니지만 이왕이면 관습적으로 /u01 디렉토리에 10G이상의 공간을 확보해서 DB까지 같은 곳에 넣어두면 간편하긴 하다. (하지만 제대로 설치해서 사용할 요령이라면 DB공간은 프로그램 설치 공간과 다른 파티션이나 디스크로 분리하는 것이 좋다.)

세번째로 오라클 설치에 필요한 각종 패키지의 버전을 확인해야 한다. 그러나 여기서는 확인하지 않고 넘어가도록 하겠다. 왜냐하면 설치 프로그램이 자동으로 패키지의 버전을 확인해주기 때문에 그때 가서 경고가 뜨는 패키지를 골라 설치해도 늦지 않기 때문이다.

네번째로 오라클 설치에 필요한 각종 시스템 자원의 커널 파라메터를 조정해야 한다. 오라클은 세마포어 개수, 공유메모리 제한 등등 여러가지를 조정해야 하는데 위와 마찬가지로 오라클 설치 프로그램이 자동으로 확인해준다. 더군다나 확인 후 부족한 자원의 파라메터를 조정할 수 있는 쉘 스크립트 프로그램을 작성해 주므로 그때 가서 간단하게 실행해서 설정하도록 한다.(귀찮게 미리 타이핑 하는 것은 시간 낭비다.)

다섯번째로 오라클 설치 및 구동에 필요한 그룹명과 유저를 만들어야 한다. 우선 root로 로그인 한다.
# groupadd oinstall
# groupadd dba
# useradd -g oinstall -G dba oracle
# passwd oracle
설치에 필요한 그룹은 oinstall, dba이며 유저명은 oracle 이다. oracle유저의 primay group은 oinstall이고, suppliment group은 dba이다. oracle 유저의 암호는 알아서 정하자. 유저가 만들어 졌으면 이번에는 /etc/pam.d/login 파일을 손봐야 한다.
참고로 /etc/pam.d/login 파일은 유저가 로그인 할 때 각종 테스트할 기능들을 정의하는 파일이다.

이 파일에 추가할 PAM 기능은 pam_limits.so 로서 유저가 로그인할 때 /etc/security/limits.conf에 정의된 자원제한값을 적용하는 기능이다. 오라클은 유저가 사용가능한 최대프로세스 개수, 최대 파일 개수를 기본값보다 크게 잡아줘야 하기 때문에 이 기능을 설정해줘야 한다. 주의할 점은 /etc/pam.d/login파일을 잘못 건드리면 새롭게 로그인이 안되는 불상사가 발생하므로 root 유저로 로그인된 terminal을 1개 여분으로 열어두자.

이제 /etc/pam.d/login을 열어서 "session   required  pam_limits.so" 라는 행이 있는지 확인하고, 없다면 맨 아래에 추가해두자. 설정이 완료된 /etc/pam.d/login 파일은 다음과 같다. (내용은 약간 다를수 있다. 맨 끝에 pam_limits.so 기능이 들어있는 행만 있으면 된다.)

# cat /etc/pam.d/login
#%PAM-1.0
auth [user_unknown=ignore success=ok ignore=ignore default=bad] pam_securetty.so
auth include system-auth
account required pam_nologin.so
account include system-auth
password include system-auth
# pam_selinux.so close should be the first session rule
session required pam_selinux.so close
session required pam_loginuid.so
session optional pam_console.so
# pam_selinux.so open should only be followed by sessions to be executed in the user context
session required pam_selinux.so open
session required pam_namespace.so
session optional pam_keyinit.so force revoke
session include system-auth
-session optional pam_ck_connector.so
session required pam_limits.so
설정이 다 되었으면 확인을 위해서 CTRL+ALT+F2를 눌러 콘솔로 나가든지, 혹은 ssh를 통해 자신의 서버에 oracle 유저로 로그인 해보자. 만일 로그인이 실패했다면 위의 설정중에 어딘가를 잘못 타이핑한 것이다. 미리 로그인해둔 터미널에서 재확인해보자.(만일 설정을 잘못했는데 실수로 로그아웃했다면 정상적인 방법으로는 로그인이 불가능해진다. 따라서 설치용 CD/DVD로 복구 모드로 들어가서 /etc/pam.d/login 파일을 다시 수정해야 한다.)

여섯번째로 오라클을 설치할 디렉토리와 권한을 조정해둬야 한다. 여기서는 /u01에 오라클을 설치하는 것으로 진행하겠다.(여기서는 미리 /u01에 충분한 파티션을 할당해 둔 것을 전제한다.)
# mkdir /u01/app
# chown -R oracle:oinstall /u01
이제 본격적인 설치를 위해서 X 윈도우를 다시 oracle 유저로 로그인 하자. 이제부터 특별한 언급이 없으면 모든 작업은 oracle 유저로 작업하는 것이다.

일곱번째로 오라클 환경변수를 설정해야 한다. 환경변수는 관습적으로 ~/.oraenv 파일명으로 만든다. 파일 내용은 다음과 같다.
ORACLE_BASE=/u01/app/oracle
ORACLE_SID=orcl
export ORACLE_BASE ORACLE_SID

ORACLE_SID는 오라클 데이터베이스 SID 값인데, 주고 싶은 값을 사용하면 된다. 최대 30자까지 가능하다. (설치시 기본 값은 orcl을 사용한다.) 파일을 만들었으면 로그인 할 때 반영되게 해야 하므로 bash runtime configuration인 .bashrc에 넣어줘야 한다. "echo ". ~/.oraenv >> ~.bashrc"라고 명령하면 된다.


1. 오라클 설치 프로그램 실행
오라클 설치 프로그램은 여러 개의 zip 파일로 압축되어있는데 다 합쳐서 약 2G가 좀 넘는다. 압축을 풀면 database 디렉토리가 보이는데 들어가보면 runInstaller 라는 실행파일이 있다. 실행해보자.

[oracle@localhost database]$ ./runInstaller 
Oracle Universal Installer 시작 중...

임시 공간 확인 중: 80MB 이상이어야 합니다.. 실제 728MB 성공
스왑 공간 확인 중: 150MB 이상이어야 합니다.. 실제 511MB 성공
모니터 확인 중: 최소 256 색상을 표시하도록 구성되어 있어야 합니다.. 실제 16777216 성공
다음에서 Oracle Universal Installer의 시작을 준비하는 중 /tmp/OraInstall2009-10-06_03-39-30PM. 기다리십시오.
위와 같은 메시지가 나오면 좀 기다려주자. 설치 프로그램 화면이 나오기 까지는 시스템에 따라서 수 분이 걸리는 경우도 있으니 다운되었다고 착각하여 터미널을 닫아버리는 잘못을 범하지는 말자.


2. 데이터베이스 보안 갱신 구성


보안 설정에 대한 알림 메일을 받으려면 email 주소를 넣어주면 된다. Oracle Support에 유저를 등록한 경우라면 더 편리하지만 이도저도 아닌 그냥 테스트용으로 설치하는 경우는 "다음"으로 진행하면 된다.


3. 데이터베이스 설치 옵션

설치옵션에서는 3가지 설치옵션을 지정할 수 있다. 여기서는 데이터베이스 생성 및 구성으로 진행하는 것을 보이도록 하겠다.

- 데이터베이스 생성 및 구성 : 소프트웨어 설치 후 데이터베이스를 생성해준다.
- 데이터베이스 소프트웨어만 설치 : 소프트웨어만 설치하고 데이터베이스는 생성하지 않는다.
- 기존 데이터베이스 업그레이드 : 말그대로 업그레이드다. 구버전의 오라클이 설치되어있다면 업그레이드 시켜준다.



4. 시스템 클래스 설정
시스템 클래스 설정은 데이터베이스를 어떤 목적으로 구성할 것인지를 결정한다. 데스크탑용인지 서버용인지를 선택할 수 있다.

- 데스크톱 클래스 : 작은 규모용으로 설치한다.
- 서버 클래스 : 자동 백업 및 분산 복제등 여러가지 기능이 추가된다.

(4-A) 데스크톱 클래스를 지정한 경우 : 총 9개의 필드를 가진 설치 구성화면으로 넘어간다.
- Oracle Base : Oracle software가 설치될 기준 디렉토리이다. OUI(Oracle Universal Installer)가 자동으로 인식하기 위해서는 /u0[1-9]/app로 시작해야 한다.
- 소프트웨어 위치 : 오라클 DB 소프트웨어가 위치할 디렉토리로서 ORACLE_BASE/product/<버전>/dbhome_1로 자동 결정됩니다.
- 데이터베이스 파일 위치 : DB파일이 저장되는 곳
- 데이터베이스 버전 : Enterprise Edition, Standard Edition, Standard Edition One중에 하나로 설정한다.(각 버전의 특징은 Help참고)
- 문자 집합 : DB내에서 사용할 문자세트(국제화 규격에 따라 UTF-8 유니코드를 추천)
- OSDBA 그룹 : DBA 그룹
- 전역 데이터베이스 이름 : DB이름+도메인으로 구성된 전역 데이터베이스 이름
- 관리 비밀번호, 비밀번호 확인 : 비밀번호 8~30자가 되어야 하며, 대소문자와 숫자가 혼합되어야만 한다.(또한 사전상의 단어를 사용하면 안된다.)


(4-B) 서버 클래스를 지정한 경우 : 우선 "단일 인스턴스 데이터베이스"와 "Real Application Clusters" 중에 하나를 선택해야 한다.
서버 클래스 설치에서는 저장 영역 유형과 ASMSNMP 비밀번호를 지정할 수 있게 된다.
- 저장 영역 유형 : 파일시스템, 원시 블록 장치 중에 선택할 수 있다.



5. 필요 조건 검사 수행
기본 설치를 지정한 뒤에는 필요조건 검사를 행하게 된다. 여기서 커널 파라메터, 사용자 자원 제한, 패키지 버전들을 검사해준다.
커널 파라메터와 사용자 자원 제한은 "수정 및 다시 확인"버튼을 누르면 자동으로 수정가능한 스크립트를 만들어 주기 때문에 패키지 버전이나 없는 패키지만 설치해주면 된다.


"수정 및 다시 확인" 버튼을 누르면 "수정 스크립트 실행"화면이 나타난다. 위의 경로의 runfixup.sh 파일을 root 계정의 권한으로 실행하면 된다.
위와 같이 runfixup.sh를 실행하면 각종 설정을 고쳐주게 된다. 수정되는 파일은 /etc/sysctl.conf와 /etc/security/limits.conf이다. runfixup.sh가 실행한 뒤에 "다시 확인"을 누르면 재검사를 하게 된다.
위에 그림을 보면 실패 상태가 3개가 나오는데 교체크기(스왑 공간), /tmp, pdksh 패키지가 실패라고 나온다. 현재 설치하려는 시스템은 RAM이 2GB이상이므로 설치중에 굳이 스왑 공간이 사용하지 않으므로 그냥 무시해도 된다. /tmp 디렉토리도 300MB이상이면 그냥 넘어가도 된다. 페도라에서는 pdksh대신에 mksh를 사용하므로 대신 mksh를 설치하면 된다.



6. 설치 옵션 요약
여기까지 설정한 것을 요약화면으로 보여준다. 이상한 점이 있다면 뒤로 가서 수정하고 오면 된다.
이상이 없다면 "완료"버튼을 눌러 설치를 시작한다.


7. 제품 설치 시작 (or 데이터베이스 생성)


처음 설치할때 데이터베이스 구성을 선택했다면 파일 복사가 끝나고 dbca가 작동하여 데이터베이스를 구성하게 된다. 테스트한 호스트인 E8400에서는 거의 50여분을 걸렸다.


데이터베이스를 구성하다보면 DB 유저의 암호를 정하는 창이 뜬다.

SYS와 SYSTEM 암호를 설정하고, 스크롤바를 더 내리면 SCOTT 유저가 있으므로 예제나 뭐 그런 것을 수행하려면 SCOTT유저의 암호도 넣어주자.

DB구성이 끝나면 root 유저로 해야할 작업을 root.sh란 쉘 스크립트로 만들어 준다. 이를 실행하고 원래 창으로 들어와서 확인을 하면 끝난다.
[root@localhost ~]# /opt/app/oracle/product/11.2.0/dbhome_1/root.sh 
Running Oracle 11g root.sh script...

The following environment variables are set as:
ORACLE_OWNER= oracle
ORACLE_HOME= /opt/app/oracle/product/11.2.0/dbhome_1

Enter the full pathname of the local bin directory: [/usr/local/bin]:
Copying dbhome to /usr/local/bin ...
Copying oraenv to /usr/local/bin ...
Copying coraenv to /usr/local/bin ...

Creating /etc/oratab file...
Entries will be added to the /etc/oratab file as needed by
Database Configuration Assistant when a database is created
Finished running generic part of root.sh script.
Now product-specific root actions will be performed.
Finished product-specific root actions.

이제 DB 설치와 구성이 모두 끝났다. 위의 Enterprise Manager Database Control URL이 보인다. 웹 브라우저를 열어서 접속해보자.(간혹 접속이 안되는 경우는 방화벽 문제일 수 있다.)



8. 오라클 엔터프라이즈 매니저
EM에 관리목적으로 접속할 때는 sys유저의 SYSDBA권한으로 접속해야 한다.

emca화면은 웹 페이지이므로 직관적이다. 참고로 사용자 추가나 조정은 "서버">>"사용자"에서 하면 된다.

그리고 항상 주의할 점은 서버 끌때는 위의 화면에서 DB를 "작동 중지" 시킨 후에 꺼야한다. 안그러면 다음번 DB 기동시에 문제가 발생할 수 있다.


9. 오라클 환경 변수의 수정
앞에서 오라클 환경변수를 설정하기 위해서 ~/.oraenv 파일을 사용했는데, 설치후에는 이 파일을 수정해둬야 한다. 이유는 오라클의 바이너리 디렉토리를 PATH에 추가하고, Pro*C 컴파일등을 위한 라이브러리 경로를 추가해야 하기 때문이다. 따라서 앞의 3줄짜리 .oraenv를 다음과 같이 적절하게 수정한다.

ORACLE_BASE=/u01/app/oracle
ORACLE_SID=orcl
ORACLE_HOME=/u01/app/oracle/product/11.2.0/dbhome_1
export ORACLE_BASE ORACLE_HOME ORACLE_SID

if [ x`echo $PATH | grep $ORACLE_HOME/bin` = 'x' ]; then
echo "Setting oracle binary path: $ORACLE_HOME/bin"
PATH=$PATH:$ORACLE_HOME/bin
export PATH
fi

if [ x`echo $LD_LIBRARY_PATH | grep $ORACLE_HOME/lib` = 'x' ]; then
echo "Setting oracle LD_LIBRARY_PATH : $ORACLE_HOME/lib"
if [ x$LD_LIBRARY_PATH = 'x' ]; then
LD_LIBRARY_PATH=$ORACLE_HOME/lib
else
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$ORACLE_HOME/lib
fi
export LD_LIBRARY_PATH
fi


수정 후에 로그인을 다시 해보면 Setting oracle binary path: /u01/app/oracle/product/11.2.0/dbhome_1/bin 와 Setting oracle LD_LIBRARY_PATH : /u01/app/oracle/product/11.2.0/dbhome_1/lib 메시지가 보일 것이다. 이제 어느 경로든지 sqlplus를 실행할 수 있게 될 것이다.


* 그 외 오라클 관련 문서는 아래의 웹 사이트에서 볼 수 있다.
http://www.oracle.com/technology/documentation

* History
2009.10.10 "9. 오라클 환경 변수의 수정" 추가
2009.10.07 첫번째 초안 작성

C언어: OpenMP를 이용한 멀티 쓰레드 프로그래밍 HOWTO #3 책 강의 PDF,PPT

C언어: OpenMP를 이용한 멀티 쓰레드 프로그래밍 HOWTO #3


- HOWTO #1 : http://sunyzero.egloos.com/4227785
- HOWTO #2 : http://sunyzero.egloos.com/4229235



6. Single Construct

single construct로 지시된 구간은 단 한번만 실행된다. 실행되는 쓰레드는 여러 쓰레드중에 제일 먼저 진입하는 쓰레드이다.

  • single construct는 처음으로 진입한 쓰레드가 실행한다.
  • 나머지 쓰레드들은 single construct 끝에 존재하는 implicit barrier에서 대기한다.
  • single construct가 끝나고 모든 쓰레드들은 implicit barrier에서 동시에 시작한다.

그림에서 보이듯이 parallel 구간에서 쓰레드들 중에 한 개만  single construct를 실행하고 나머지는 뒤에 존재하는 implicit barrier에서 대기하는 것을 볼 수 있다. 그러면 위 소스코드를 컴파일하고 실행해보자. 실행결과는 예상대로 "1. Hello world"는 1번 출력되고, "2. Hello world"는 2번 실행된다.(테스트 호스트는 듀얼 코어이므로)
$ gcc -o omp_single -fopenmp omp_single.c
$ ./omp_single
1. Hello world
2. Hello world
2. Hello world



7. Master Construct

master construct는 single construct와 매우 비슷하다.
하지만 다른 점이 2가지 있다.

  • master construct 구간은 무조건 master thread (main thread)가 1번 실행한다.
  • master construct 구간뒤에 implicit barrier가 없다.
    즉 모든 쓰레드는 master construct 실행되는 동안에도 계속 실행한다.
실행 결과는 위의 single construct와 같지만 위 그림에서 보듯이 약간의 차이는 있다. master construct는 implicit barrier가 없다는 점이다. 중요한 차이므로 꼭 기억해야 한다. 




8. Barrier

배리어란 동기화(synchronization)을 위해서 사용되는 기능이다.

동기화는 시간적 개념이다. 풀어서 설명하기 위해 예를 들자. 스타크래프트 배틀넷은 왠만한 사람이면 다 해봤을 것이다. 최대 8명까지 게임에 참가할 수 있는데, 어떤 유저가 매우 느린 모뎀을 쓰고 있으면 게임 중간에 타임을 세는 화면이 뜨고 기다려주는 것을 볼 수 있다. 이는 빠른 네트워크/컴퓨터를 가진 유저와 느린 네트워크/컴퓨터를 가진 유저의 게임 속도를 맞추기 위해서 배리어가 작동한 것이다. 따라서 결과적으로 배리어는 느린 사람에 맞춰서 앞서 가는 사람이 대기하도록 하는 기능이다.

그러면 프로그래밍에서는 배리어를 어떻게 사용해야 하는가? 작업이 병렬적으로 이뤄진다고 하더라도 전처리, 후처리 작업들이 나눠져 있을 경우에는 전처리 작업들을 병렬처리했을때 어떤 특정 쓰레드가 빨리 처리했다고 후처리 작업을 먼저 출발하면 데이터가 꼬이거나 로직이 망가질 수 있다. 이럴 경우 중간중간에 적절한 배리어를 넣어주면 깔끔하게 해결된다.(하지만 역으로 배리어가 많으면 그 만큼 대기도 많아질 수 있다.)


8.a Implicit barrier

앞에서 implicit barrier(암묵적 배리어)에 대해서 이야기를 많이 했다. OpenMP는 각 작업의 동기화에 대한 편의성을 제공하기 위해서 implicit barrier를 잘 제공한다. 어떤 construct 에 대해서 implicit barrier가 제공되는지 정리하고 넘어가자.

  • #pragma omp parallel
  • #pragma omp for
  • #pragma omp single
위의 3가지의 경우는 블록 끝에 자동적으로 implicit barrier가 들어간다. 하지만 위의 3가지 construct 의 끝에 nowait clause를 지정하면 implicit barrier가 제거되고 대기하지 않고 이후 코드가 즉시 실행됩니다.

위의 예제에서는 single construct에 nowait를 적용하여 implicit barrier를 제거하는 것을 볼 수 있다.
(그림 아래에 있는 implicit barrier는 parallel construct에 있는 barrier입니다.)


8.b Explicit barrier

이번에는 사용자가 직접 지정할 수 있는 explicit barrier 기능에 대해서 보겠습니다.

  • #pragma omp barrier 구문을 지정하면 해당 부분에서 모든 쓰레드가 도착할 때까지 대기하게 된다.
char * get_time0(char *buf, size_t sz_buf);

int main()
{
int t_sleep; char buf[16];
#pragma omp parallel private(t_sleep, buf)
{
#pragma omp single nowait
sleep(1);
printf("[%s] phase1:sleep %ld sec.\n", get_time0(buf, sizeof(buf)), t_sleep = times(NULL)%8);
sleep(t_sleep);
#pragma omp barrier
/* explicit barrier */
printf("[%s] phase2. Hello world\n", get_time0(buf, sizeof(buf)));
}
return 0;
}

char * get_time0(char *buf, size_t sz_buf)
{
time_t t0; struct tm tm_now;
if (buf == NULL) return NULL;
if (time(&t0) == ((time_t)-1)) return NULL;
localtime_r(&t0, &tm_now);
if (strftime(buf, sz_buf, "%H:%M:%S", &tm_now) == 0) return NULL;
return buf;
}
이제 실행해보면 배리어 효과때문에 마지막 실행한 19:12:28에서 5초뒤에 phase2가 실행되는 것을 볼 수 있다.
$ ./omp_barrier
[19:12:27] phase1:sleep 1 sec.
[19:12:28] phase1:sleep 5 sec.
[19:12:33] phase2. Hello world
[19:12:33] phase2. Hello world

이번에는 여기까지 마치고, 다음에 critical section이나 atomic, OpenMP API함수등에 대해서 다뤄보도록 하겠다.

C언어: OpenMP를 이용한 멀티 쓰레드 프로그래밍 HOWTO #2 책 강의 PDF,PPT

C언어: OpenMP를 이용한 멀티 쓰레드 프로그래밍 HOWTO #2



- HOWTO #1 : http://sunyzero.egloos.com/4227785



* 연습문제 (멀티 쓰레딩과 reentrant 함수의 관계에 대한 문제)

이번에는 pi를 몬테 카를로 시뮬레이션을 이용해서 구해보도록 하겠다.
각변의 길이가 1인 정사각형이 있다. 그리고 정사각형 안에 반지름(r)이 1인 원호를 그리도록 하자.
그러면 중점으로부터 원호까지의 최단 길이는 무조건 1이 된다.

그러면 이제 정사각형 안에 임의의 점(x,y)좌표를 찍어서 중점에서 선분을 연결하고 아랫변까지 선분을 내리면 직각삼각형이 된다.
그리고 이 직각삼각형의 길이는 x, 높이는 y가 된다.
피타고라스의 정리에 의해 (x의 제곱)+(y의 제곱)=(빗변의 제곱)이 되는데, 빗변이 길이가 1보다 작으면
원호 안에 찍힌 점이 되고, 1보다 크면 원호 밖에 정사각형 내부에 찍힌 점이 된다.

원호의 넓이는 반지름 r=1일 때 pi/4이 된다. 따라서 위의 수많은 랜덤 좌표를 찍은 뒤에 (원호 내부의 점의 개수)/(전체 랜덤 수)
는 pi/4와 같아질 것이다. 그러면 이제 기본 코딩을 해보자.

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <math.h>
#define LOOP_ITERATION 200000000
int hits;

int main()
{
int i;
double x, y, rns;
time_t t_now;

printf("Loop iteration = %ld\n", (long)LOOP_ITERATION);
rns = 1.0/(double)RAND_MAX;

t_now = time(0);
srand((unsigned int)t_now);
for (i=0; i<LOOP_ITERATION; i++) {
x = (double)rand() * rns;
y = (double)rand() * rns;
if (x*x + y*y < 1) {
hits++;
}
}
printf("pi = %f\n", 4*(double)hits/LOOP_ITERATION);
return 0;
}
이제 컴파일 후 실행을 해 보겠다. 소스코드의 파일명은 pi_monte.c라고 저장했다.

$ gcc -o pi_monte pi_monte.c
$ time ./pi_monte
Loop iteration = 200000000
pi = 3.141588

real 0m8.726s
user 0m8.702s
sys 0m0.023s
CPU를 한 개만 사용했기 때문에 real과 user+sys 시간이 같게 나온다.
이제 여기에 OpenMP를 적용해보도록 하자. 중복되는 코드는 모두 생략하고 중간에 loop부분만 적도록 하겠다.
...생략...
#pragma omp parallel for private(x,y) reduction(+:hits)
for (i=0; i<LOOP_ITERATION; i++) {
x = (double)rand() * rns;
y = (double)rand() * rns;
if (x*x + y*y < 1) {
hits++;
}
}
...생략...
자 이제 수정된 소스코드를 컴파일하고 다시 실행해보겠다.
$ gcc -o pi_monte_omp -fopenmp pi_monte.c
$ time ./pi_monte_omp
Loop iteration = 200000000
pi = 3.141513

real 0m54.909s
user 0m54.173s
sys 0m53.115s
OpenMP버전의 실행 시간이 엄청나게 증가한 것을 볼 수 있다.
이는 뭔가 문제가 발생한 것이다. profiler가 있으면 cache miss가 많아진 것을 추적할 수 있다. 그러면 왜 cache miss가 많아졌을까?

이는 멀티 쓰레드 안전(thread-safe, MT-safe)한 함수를 사용하지 않았기 때문이다.
단도직입으로 원인은 rand()함수이다. rand()함수는 내부에 static 변수(BSS영역)를 사용하기 때문에 lock 없이 사용하면
오염된 공간을 쓸 수 있다. 그렇다고 lock으로 보호하는 것은 성능상으로 좋지 못하다. 따라서 MT-safe한 랜덤생성 함수로
대체해야 한다. 마침 rand()의 reentrant 버전인 rand_r()이 있으므로 이를 사용하도록 바꾸면 된다.

* reentrant에 대한 이해가 필요하다면...http://sunyzero.egloos.com/4188321 글을 읽도록 한다.

그러면 이제 수정된 버전을 보도록 하겠다.

#define _REENTRANT
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <math.h>
#ifdef LOOP_ITERATION
#define LOOP_ITERATION 200000000
#endif
int hits;

int main()
{
unsigned int state;
int i;
double x, y, rns;
printf("Loop iteration = %ld\n", (long)LOOP_ITERATION);
rns = 1.0/(double)RAND_MAX;
state = (unsigned int)time(0);
#pragma omp parallel for firstprivate(state) private(x, y) reduction(+:hits)
for (i=0; i<LOOP_ITERATION; i++) {
x = (double)rand_r(&state) * rns;
y = (double)rand_r(&state) * rns;
#if LOOP_ITERATION < 100
printf("THR[%d:%d] x,y/state = %f,%f/%u\n", omp_get_thread_num(), i, x, y, state);
#endif
if (x*x + y*y < 1) {
hits++;
}
}
printf("pi = %f (hits = %d)\n", (double)hits/LOOP_ITERATION * 4, hits);
return 0;
}
중요한 변화를 확인하기 위해 LOOP_ITERATION이 100 이하인 경우에는 루프를 돌때마다 rand_r()로 얻어진 x, y, state의 값을 출력하도록 디버깅 코드를 심어놨다. 그러면 LOOP_ITERATION을 20으로 줄여서 컴파일&실행으로 디버깅 해보자.

$ gcc -o pi_monte_omp_logging -DLOOP_ITERATION=20 -fopenmp pi_monte.c
$ ./pi_monte_omp_logging
Loop iteration = 20
THR[0:0] x,y/state = 0.739258,0.373189/1534713604
THR[0:1] x,y/state = 0.741886,0.152093/434332526
THR[0:2] x,y/state = 0.399359,0.675170/1402811528
THR[0:3] x,y/state = 0.140991,0.510551/3217124562
THR[0:4] x,y/state = 0.117010,0.018486/2612351692
THR[0:5] x,y/state = 0.151627,0.286311/535820534
THR[0:6] x,y/state = 0.412198,0.450266/361487824
THR[0:7] x,y/state = 0.126866,0.324974/1313573850
THR[0:8] x,y/state = 0.923697,0.862684/1937324436
THR[0:9] x,y/state = 0.828232,0.810037/3167316350
THR[1:10] x,y/state = 0.739258,0.373189/1534713604
THR[1:11] x,y/state = 0.741886,0.152093/434332526
THR[1:12] x,y/state = 0.399359,0.675170/1402811528
THR[1:13] x,y/state = 0.140991,0.510551/3217124562
THR[1:14] x,y/state = 0.117010,0.018486/2612351692
THR[1:15] x,y/state = 0.151627,0.286311/535820534
THR[1:16] x,y/state = 0.412198,0.450266/361487824
THR[1:17] x,y/state = 0.126866,0.324974/1313573850
THR[1:18] x,y/state = 0.923697,0.862684/1937324436
THR[1:19] x,y/state = 0.828232,0.810037/3167316350
pi = 3.200000 (hits = 16)
iteration이 20번이므로 pi의 정확도는 일단 포기하자. 지금은 pi의 결과를 보려는 것이 아니라, 각 쓰레드의 x, y값을 확인하기 위함이다. 0번째 쓰레드의 첫번째 데이터와 1번째 쓰레드의 첫번째 데이터를 비교하니 뭔가 이상하다. 이해를 돕기 위해 두개만 따로 떼어서 보도록 하자.

THR[0:0] x,y/state = 0.739258,0.373189/1534713604
THR[1:10] x,y/state = 0.739258,0.373189/1534713604

둘이 놀랍도록 일치하지 않은가? 왜 이런 문제가 발생할까? 이는 rand_r()함수에 사용하는 seed값 변수인 state의 초기값이 모든 쓰레드들에 대해서 같기 때문에 발생하는 문제다. 그렇다면 각 쓰레드가 seed를 다르게 가져가도록 소스코드를 수정해야 한다.

#define _REENTRANT
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <math.h>
#ifdef LOOP_ITERATION
#define LOOP_ITERATION 200000000
#endif
int hits;

int main()
{
unsigned int state1, state2;
int i;
double x, y, rns;

printf("Loop iteration = %ld\n", (long)LOOP_ITERATION);
rns = 1.0/(double)RAND_MAX;
state1 = (unsigned int)times(NULL);
#pragma omp parallel private(x, y, state2) reduction(+:hits) shared(state1)
{
#pragma omp critical
state2 = rand_r(&state1);
printf("THR[%d] state1/state2 = %u/%u\n", omp_get_thread_num(), state1, state2);
#pragma omp for
for (i=0; i<LOOP_ITERATION; i++) {
x = (double)rand_r(&state2) * rns;
y = (double)rand_r(&state2) * rns;
#if LOOP_ITERATION < 100
printf("THR[%d:%d] x,y/state,hits = %f,%f/%u,%d\n", omp_get_thread_num(), i, x, y, state2,hits);
#endif
if (x*x + y*y < 1) {
hits++;
}
}
}
printf("hits(%d), pi = %f\n", hits, (double)hits/LOOP_ITERATION * 4);
return 0;
}
#pragma omp critical 지시어는 아래의 블럭공간을 락으로 보호해준다. 즉 state1으로부터 state2라는 seed값을 생성하는 코드 부분은 각 쓰레드들이 직렬적으로 실행하게 되므로 각각 다른 seed (=state2)를 가질 수 있게 해준다.

그러면 이제 수정된 버전의 확인을 위해 LOOP_ITERATION을 20번으로 지정하고 컴파일하여 다시 실행해보자.
$ gcc -o pi_monte_omp_logging -DLOOP_ITERATION=20 -fopenmp pi_monte.c
$ ./pi_monte_omp_logging
Loop iteration = 20
THR[0] state1/state2 = 2209777173/920556694
THR[0:0] x,y/state,hits = 0.007502,0.800638/402955376,0
THR[0:1] x,y/state,hits = 0.031584,0.366846/1879397754,1
THR[0:2] x,y/state,hits = 0.084310,0.126795/2318644788,2
THR[0:3] x,y/state,hits = 0.219188,0.054924/839319838,3
THR[0:4] x,y/state,hits = 0.315477,0.640078/2081973432,4
THR[0:5] x,y/state,hits = 0.423693,0.287051/4130577282,5
THR[0:6] x,y/state,hits = 0.738140,0.376765/2380434428,6
THR[0:7] x,y/state,hits = 0.127375,0.723816/3248220326,7
THR[0:8] x,y/state,hits = 0.832496,0.256793/3481063424,8
THR[0:9] x,y/state,hits = 0.939841,0.108509/3174395018,9
THR[1] state1/state2 = 2209777173/754906038
THR[1:10] x,y/state,hits = 0.446803,0.061819/2639594128,0
THR[1:11] x,y/state,hits = 0.919237,0.585474/2439875226,1
THR[1:12] x,y/state,hits = 0.122634,0.254468/2048737876,1
THR[1:13] x,y/state,hits = 0.204702,0.526382/3058641982,2
THR[1:14] x,y/state,hits = 0.642845,0.756832/477247192,3
THR[1:15] x,y/state,hits = 0.550340,0.531024/1736039586,4
THR[1:16] x,y/state,hits = 0.926036,0.217729/4074357788,5
THR[1:17] x,y/state,hits = 0.266219,0.975137/2510577606,6
THR[1:18] x,y/state,hits = 0.663719,0.557898/2880736800,6
THR[1:19] x,y/state,hits = 0.491536,0.580296/2760522154,7
hits(18), pi = 3.600000
실행 결과를 보면 이제 x, y값이 각 쓰레드마다 달라지는 것을 볼 수 있다.

그러면 이제 기능상의 문제는 해결되었으니 원래대로 200,000,000번의 시행 횟수로 실행시간을 비교해보자.
$ gcc -o pi_monte_omp -fopenmp pi_monte.c
$ time ./pi_monte_omp
Loop iteration = 200000000
THR[0] state1/state2 = 319398670/265139008
THR[1] state1/state2 = 319398670/1387545353
hits(157082826), pi = 3.141657

real 0m2.414s
user 0m4.737s
sys 0m0.013s
싱글 쓰레드 버전이 8.6초 걸린 것에 비해 OpenMP로 돌린 버전은 2.4초로 확 줄어들었다. 원래대로라면 듀얼코어니 4.3초정도가 나와야 하겠지만, rand_r()자체가 rand()보다 가볍기 때문에 그로 인해서 속도가 더 빨라졌다. 즉 싱글 쓰레드 버전이라고 해도 rand()보다는 rand_r()을 사용하면 더 빠르다라는 교훈도 덤으로 알려드리는 문제였다.



5. Section construct
section construct는 task level parallelism에 사용하며, 각각의 작업들이 서로 관련이 없는 경우에 사용한다.
(task 레벨의 병렬화이므로 divide and conquer 형태의 문제해결에도 적용가능하다.)

그림에서 볼 수 있듯이 오렌지색의 섹션과 블루색의 섹션이 각각 독립적으로 작동하도록 구성할 수 있다.
단 각각의 섹션은 누가 먼저 종료하든지 #pragram omp sections 블록의 끝에는 implicit barrier가 있으므로 대기하게 된다.

참고로 병렬구간내에 섹션구간만 존재하는 경우라면 #pragma omp parallel sections로 구문을 합칠 수 있다.

그러면 이번에는 앞에서 loop construct때 연습했던 2가지 pi 구하는 방법(numerical integration, monte carlo simulation)을
sections를 이용해서 동시에 작동시키도록 바꿔보자.


* 연습문제 (섹션별로 task를 할당하는 방법)


앞에서 Single threaded 버전으로 만들었던 소스코드들을 연달아 붙여두면 된다.
우선 Numerical Integration 방법의 소스코드를 다시 보자.

#include <stdio.h>
#include <stdlib.h>
int num_steps=1000000000; /* 10억번 : 너무 많으면 조금 줄이시길... */

int main()
{
int i;
double x, step, sum = 0.0;
step = 1.0/(double) num_steps;
for (i=0; i<num_steps; i++) {
x = (i+0.5) * step;
sum += 4.0/(1.0 + x*x);
}
printf("PI = %.8f (sum = %.8f)\n", step*sum, sum);
return EXIT_SUCCESS;
}
이번에는 Monte Carlo simulation방법을 다시 보겠다.

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <math.h>
#define LOOP_ITERATION 200000000
int hits;

int main()
{
int i;
double x, y, rns;
time_t t_now;

printf("Loop iteration = %ld\n", (long)LOOP_ITERATION);
rns = 1.0/(double)RAND_MAX;

t_now = time(0);
srand((unsigned int)t_now);
for (i=0; i<LOOP_ITERATION; i++) {
x = (double)rand() * rns;
y = (double)rand() * rns;
if (x*x + y*y < 1) {
hits++;
}
}
printf("pi = %f\n", 4*(double)hits/LOOP_ITERATION);
return 0;
}
이제 이 2개의 소스코드를 합쳐야 한다. 주의할 점은 두번째 Monte Carlo simulation에서는 rand()함수 대신에 rand_r()을 사용하는 것을 잊지 말아야 한다. 항상 Non-multi-threaded model을 Multi-threaded model로 바꿀때는 MT-safe function이나 reentrant function으로 골라 써야 한다는 점이다.

그리고 false-sharing문제를 피하기 위해서 동일 캐시 라인에 올라갈 수 있는 전역변수나 힙 메모리를 사용하면 안된다는 점이다. 왠만하면 쓰레드 로컬 변수에서 대부분 해결하도록 하자.

그러면 둘을 #pragma omp sections로 합친 코드를 보겠다. 보기 좋게 출력부분의 수정을 했으나 기본 코드는 같다.

#define _XOPEN_SOURCE   600
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <time.h>
#include <sys/times.h>
#include <omp.h>
#ifndef LOOP_ITERATION
#define LOOP_ITERATION 200000000
#endif

double pi[2];
int main()
{
clock_t start_m, end_m;
clock_t sc_clk_tck = sysconf(_SC_CLK_TCK);
start_m = times(NULL);
printf("LOOP ITERATION = %ld\n",(long)LOOP_ITERATION);
#ifdef _OPENMP
omp_set_num_threads(2);
#endif
#pragma omp parallel
{
#pragma omp sections
{
#pragma omp section
{
int i;
double x, step, hits;
float t_elapsed;
clock_t start, end;
hits = 0.0;
#ifdef _OPENMP
printf("[SECTION] integration method by thread(%d)\n", omp_get_thread_num());
#else
printf("[SECTION] start integration method\n");
#endif
start = times(NULL); /* get clock tick */
step = 1.0/(double) LOOP_ITERATION;
for (i=0; i<LOOP_ITERATION; i++) {
x = (i+0.5) * step;
hits += 1.0/(1.0 + x*x);
}
pi[0] = 4 * hits * step;
end = times(NULL);
t_elapsed = (float) (end - start)/sc_clk_tck;
#ifdef _OPENMP
printf("[SECTION] end integration method by thread(%d):elapsed time(%.02f sec)\n",
omp_get_thread_num(), t_elapsed);
#else
printf("[SECTION] end integration method: elapsed time(%.02f sec)\n",
t_elapsed);
#endif
}
#pragma omp section
{
int i, state, hits;
double x, y, rns;
float t_elapsed;
clock_t start, end;
#ifdef _OPENMP
printf("[SECTION] monte carlo method by thread(%d)\n", omp_get_thread_num());
#else
printf("[SECTION] start monte carlo method\n");
#endif
start = times(NULL); /* get clock tick */
state = time(0);
rns = 1.0/(double)RAND_MAX;
hits = 0;
for (i=0; i<LOOP_ITERATION; i++) {
x = (double)rand_r((unsigned int *)&state) * rns;
y = (double)rand_r((unsigned int *)&state) * rns;
if (x*x + y*y < 1) {
hits++;
}
}
pi[1] = (hits / LOOP_ITERATION) * 4;
end = times(NULL);
t_elapsed = (float) (end - start)/sc_clk_tck;
#ifdef _OPENMP
printf("[SECTION] end monte carlo method by thread(%d): elapsed time(%.02f sec)\n",
omp_get_thread_num(), t_elapsed);
#else
printf("[SECTION] end monte carlo method: elapsed time(%.02f sec)\n",
t_elapsed);
#endif
}
}
}
end_m = times(NULL);
printf("integration PI = %.8f\n", pi[0]);
printf("monte carlo PI = %.8f\n", pi[1]);
printf("* Total elapsed time(%.02f sec)\n", (double)(end_m - start_m)/sc_clk_tck);
return EXIT_SUCCESS;
}
이제 실행을 해보자.
$ gcc -o pi_sections_omp -fopenmp pi_sections.c
$ time ./pi_sections_omp
LOOP ITERATION = 200000000
[SECTION] integration method by thread(0)
[SECTION] monte carlo method by thread(1)
[SECTION] end integration method by thread(0):elapsed time(1.79 sec)
[SECTION] end monte carlo method by thread(1): elapsed time(5.05 sec)
integration PI = 3.14159265
monte carlo PI = 3.14161276
* Total elapsed time(5.05 sec)

real 0m5.052s
user 0m6.858s
sys 0m0.016s
당연히 Numerical Integration이 더 빠르기 때문에 먼저 끝난다. 하지만 implicit barrier가 있기 때문에 대기하게 된다. Monte Carlo simulation은 더 오래 걸리기 때문에 전체 수행 시간은 Monte Carlo simulation이 끝나는 시간에 종료한다.

* 오늘은 여기까지... 나머지는 다음 장에. (이것도 정말 힘들군요... 본문의 반말은 이해해주세요.)

1 2 3 4 5 6 7 8 9 10 다음