'Computer_language'에 해당되는 글 82건
- 2009.01.12 [Tip] win32k.sys 덤프가 뜰 경우..(메모리 테스트 프로그램들) [출처] [Tip] win32k.sys 덤프가 뜰 경우..(메모리 테스트 프로그램들)
- 2009.01.12 프로그램 덤프 뜨기...
- 2009.01.12 GDB 잘 쓰기 2: User Defined Commands
- 2009.01.12 [디버깅] core dump file을 분석해 보자
- 2009.01.12 NS2 - 무선환경 시뮬레이션
- 2009.01.12 [EJB] 객체지향과 컴포넌트 프로그래밍 [출처] [EJB] 객체지향과 컴포넌트 프로그래밍|작성자 젠센쭈
- 2009.01.12 oop 객체 지향 프로그래밍(Object Oriented Programming, OOP)
- 2009.01.12 A* 알고리즘에 대한 간단한 설명
- 2009.01.12 리눅스 커널 스터디 참고자료
- 2009.01.12 초보자를 위한 리눅스 커널의 메모리 관리
[Tip] win32k.sys 덤프가 뜰 경우..(메모리 테스트 프로그램들) [출처] [Tip] win32k.sys 덤프가 뜰 경우..(메모리 테스트 프로그램들)
| Computer_language/Comfile 2009. 1. 12. 02:28하지만, 저 메시지가 뜬다면 win32k.sys(0X00000050) 한번쯤 메모리 테스트 해보는 수도 있습니다.
테스트 프로그램은
1. windiag(http://oca.microsoft.com/ko/windiag.asp#top) -> 자료실 15번
2. memtest86 3.4a(http://memtest86.com/ -> Free Download)
3. memtest86 2.1(http://memtest.org -> Download(Pre-built & ISOs))
4. memtest 3.7(http://hcidesign.com/memtest/download.html) -> 자료실 57번(신기센터장님)
(버전 업이 되어서 3.7까지 올라갔네요..)
몇군데는 usb용도 존재하네요..
입맛에 맞게 사용하세요..
오늘 며칠전에 출고한 컴인데, 고객이 자꾸 파란화면이 뜬다고 해서 오늘 입고처리했습니다.
(그땐 이상없이 동작해서 윈도우도 깔고, 가져다 준건데....)
메모리 테스트를 해보았는데, 오류 메시지를 듬뿍~!! 쏟아내더군요..
메모리 교체후 몇시간 테스트해보고 있지만, 현재까진 이상이 없네요..
참고하세요..
[출처] [Tip] win32k.sys 덤프가 뜰 경우..(메모리 테스트 프로그램들)|작성자 제우스신
'Computer_language > Comfile' 카테고리의 다른 글
프로그램 덤프 뜨기... (0) | 2009.01.12 |
---|---|
[디버깅] core dump file을 분석해 보자 (0) | 2009.01.12 |
NS2 - 무선환경 시뮬레이션 (0) | 2009.01.12 |
[EJB] 객체지향과 컴포넌트 프로그래밍 [출처] [EJB] 객체지향과 컴포넌트 프로그래밍|작성자 젠센쭈 (0) | 2009.01.12 |
열심히 배울려고 자료를 찾고 있다..
음음..
디버깅 관련 문서를 보고 있는데..
http://msdn2.microsoft.com/en-us/library/ms954590.aspx
요놈을 보다가..
adplus 에 대해서 알게 되었다..
아무튼..
프로세스 상에 떠있는 놈을 덤프를 뜰수 있다..
옵션은 대충 hang, crash 의 옵션이 있는데..
hang 을 통하여 덤프를 뜨게 되면..
메모리의 대용을 그대로( hex ) 화일로 덤프를 뜨게 된다..
음.. 머 저걸로 분석을 한다는건 다른 문제지만..
그래도 일단은 통으로 덤프를 뜰수 있다는 정도..
아.. 그리고 한가지..
덤프를 뜨면서.. 프로그램과 함깨 로드된 모듈의 정보도 같이 나타나게 된다..
음음..
조금더 자세한 사항들은 더 연구를 해봐야.. 흐흐..
자아.. 이건 adplus 에 대한 문서..
http://support.microsoft.com/default.aspx?scid=kb;en-us;q286350
http://support.microsoft.com/default.aspx?scid=kb;en-us;q286350
덤프를 뜨는 예제..
adplus -hang -pn process.exe -o c:\dir\newdir
요래하면 된다..
[출처] 프로그램 덤프 뜨기...|작성자 베타
'Computer_language > Comfile' 카테고리의 다른 글
[Tip] win32k.sys 덤프가 뜰 경우..(메모리 테스트 프로그램들) [출처] [Tip] win32k.sys 덤프가 뜰 경우..(메모리 테스트 프로그램들) (0) | 2009.01.12 |
---|---|
[디버깅] core dump file을 분석해 보자 (0) | 2009.01.12 |
NS2 - 무선환경 시뮬레이션 (0) | 2009.01.12 |
[EJB] 객체지향과 컴포넌트 프로그래밍 [출처] [EJB] 객체지향과 컴포넌트 프로그래밍|작성자 젠센쭈 (0) | 2009.01.12 |
디버깅 작업 또는 프로그램의 안전성을 검사할 때 디버거를 잘 쓰면 꽤 많은 시간을 절약할 수 있습니다. 대부분 개발자들이 GDB를 써서 디버깅을 하고 있지만, GDB가 가지고 있는 강력한 기능들을 거의 쓰지 못하고 있기 때문에, 이 글에서는 자주 쓰이지는 않을 지언정, 알면 매우 도움이 되는 기능들을 위주로 살펴보겠습니다.
먼저, 이 글을 읽는 분들이 GDB의 기본적인 사용 방법 (특히 break, run, continue, file, backtrace, print 등)을 알고 있다고 가정하겠습니다. 기본적인 사용 방법을 모르신다면 Emacs/GDB/etags/cscope나 기타 GDB manual을 참고하기 바랍니다.
Breakpoints
break 명령은 대개 다음과 같이 쓸 수 있다는 것은 이미 알고 계실 것입니다:
(gdb) break # 현재 줄에 breakpoint 설정
(gdb) break 31 # 현재 파일 31번째 줄에 breakpoint 설정
(gdb) break foo # 함수 foo에 breakpoint 설정
(gdb) break list::next # list 클래스 next 멤버 함수에 설정
(gdb) break hello.c:main # hello.c 파일의 main 함수에 설정
(gdb) break util.c:300 # util.c 파일의 300번째 줄에 설정
특히 C++의 경우, 한 클래스의 모든 멤버 함수에 breakpoint를 설정하고 검사할 필요가 있는데, 이 경우, 정규 표현식(regular expression)으로 breakpoint를 설정하는 rbreak 명령을 쓰면 편리합니다. 예를 들어 보면:
(gdb) rbreak f*o # "f*o"를 만족하는 심볼 전체에 대해 설정
(gdb) rbreak list:: # "list::.*"를 만족하는 심볼 전체에 대해 설정
특히 위 두번째 예제를 보시면 ".*"이 항상 default로 따라 온다는 것을 알 수 있습니다. 사실 rbreak 명령에 "foo"를 준 경우 사용되는 정규 표현식은, 정확히 말하면 ".*foo.*"가 됩니다. 따라서 "foo"로 시작하는 함수 전체에 대해 breakpoint를 설정하고 싶다면, 다음처럼 쓰면 됩니다:
(gdb) rbreak ^foo
breakpoint를 설정하면, 해당 breakpoint마다 번호(BNUM)가 주어지고, 이 번호를 써서 다양한 작업을 수행할 수 있습니다. 예를 들어, 전체 breakpoint 목록을 보고 싶다면:
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x08066b44 in eventShow() at menubar.cpp:1017
breakpoint already hit 3 time
2 breakpoint keep y 0x080b06f4 in Play() at thumbview.cpp:416
3 breakpoint keep y 0x08066e7e in ActPlay() at menubar.cpp:1085
4 breakpoint keep y 0x08059cd3 in Play_SS(int, int) at widgets.cpp:2183
(gdb)
첫번째 컬럼(Num)은 각 breakpoint에 대한 고유번호(BNUM)를 나타냅니다. 그리고 두번째 컬럼(Type)은 breakpoint인지 watchpoint인지 catchpoint인지를 나타냅니다. (watchpoint와 catchpoint는 다음에 설명..) 그리고 세번째 컬럼(Disp)은 이 breakpoint의 특징을 나타냅니다. (다음에 설명). 네번째 컬럼(Enb)는 현재 이 breakpoint가 활성화되어 있는지를 나타냅니다. 비활성화(n)로 표시된 breakpoint는 동작하지 않습니다. 활성화/비활성화는 'enable br [BNUM]' 또는 'disable br [BNUM]'으로 변경할 수 있습니다. 예를 들어 1번 breakpoint를 비활성화하고 싶다면:
(gdb) disable br 1
전체 breakpoint를 활성화하고 싶다면:
(gdb) enable br
2번, 4번 breakpoint를 비활성화하고 싶다면:
(gdb) disable br 2 4
2번부터 5번까지 breakpoint를 활성화 하고 싶다면:
(gdb) enable br 2-5
등으로 할 수 있습니다.
때때로, 딱 한 번만 쓸 breakpoint가 필요한 경우가 있습니다. 이 경우 쓸 수 있는 명령은 enable br once [BNUM] 또는 enable br delete [BNUM]을 쓸 수 있습니다. 예를 들어 아래 명령은 1번, 3번 breakpoint를 활성화하고, 사용된 경우 바로 비활성화시킵니다:
(gdb) enable br once 1 3
아래 명령은 4번 breakpoint를 활성화하고, 사용된 경우, 이 breakpoint를 삭제합니다:
(gdb) enable br delete 4
쓸모있는 기능 중 하나가 바로 breakpoint에 조건을 지정하고, 해당 조건을 만족할 경우에 멈추도록 하는 것입니다. 예를 들어 다음과 같은 코드가 있다고 가정해 봅시다:
int i = 0;
/* do something #1 */
for (i = 0; i < 1000; i++) {
/* do something #2 */
/* do something #3 */
}
이상하게도 i가 456일때 반복문 안에서 프로그램이 이상하게 동작한다고 가정해 봅시다. 이 때 "do something #2" 부분에 breakpoint를 걸었다면 (이 breakpoint의 번호는 8번이라고 가정합시다), 반복할 때마다 계속 프로그램 실행이 멈출 겁니다. 정확히 1000번 멈추겠죠. 456번까지 진행한다는 것은 매우 귀찮은 일입니다. 이 경우, 다음과 같이 조건을 지정할 수 있습니다:
(gdb) cond 8 i == 456
즉, 8번 breakpoint는 i == 456을 만족할 때에만 멈추도록 지정합니다. 조건식에는 단순한 상수 비교 이외에, 복잡한 함수 호출도 가능합니다. 예를 들면 다음과 같습니다:
(gdb) cond 8 foo(i) > bar(rand())
앞에서 예로 든 코드는 단순 반복문이기 때문에, 처음 456 - 1번에 발생하는 breakpoint는 무시하라고 지정할 수도 있습니다. 처음 N번 발생하는 breakpoint를 무시하라는 명령은 다음과 같습니다:
(gdb) ignore 8 455
즉, 8번 breakpoint는 455번 동안 무시됩니다.
또, 다음과 같은 코드를 가정해 봅시다:
int i = 0;
int j, k;
long l;
while (1) {
j = rand();
k = some_funtion(j, time());
/* do something #1 */
l = j & 0xFF00 + (int)(log(k) * 3.2108) - ...;
if (some_condition)
break;
}
위 코드는 j와 k가 실행할 때마다 값이 변합니다. 그리고 이상하게도 j < k 일때 변수 l이 이상한 결과를 가지는 것 같지만, 확실하지는 않습니다. 우리가 확신할 수 있는 것은 j < k일 경우, l은 항상 양수이어야 한다는 것입니다. 그래서 l의 값이 전체 반복을 끝낼 동안 어떤 값을 가지고 있는지 검사해보고 싶습니다. 이 경우 해당 breakpoint에서 멈출 때, 특정 명령을 수행하도록 하는 GDB 명령인 commands를 쓰면 됩니다.
일단 "l = j & 0xFF00..." 부분에 breakpoint를 걸고 (9번 breakpoint라고 가정), 다음 명령을 내립니다:
(gdb) commands 9
Type commands for when breakpoint 9 is hit, one per line.
End with a line saying just "end".
>silent
>if j < k
>printf "l is %d\n", l
>end
>cont
>end
대충 눈치가 빠른 분은 아시겠지만 'commands [BNUM] ... end'는, BNUM breakpoint에서 멈췄을 때, "..."에 지정한 GDB 명령들을 수행합니다. 일단 silent 명령으로 명령 자체가 출력되지 않도록 한 다음, GDB printf 명령으로 변수 l 값을 출력합니다. 그리고 continue 명령으로 계속 프로그램을 진행하도록 합니다. 그 결과, 프로그램을 실행할 경우, breakpoint에서 멈추고 l 값을 출력한 다음 프로그램을 자동으로 진행합니다. 이 과정은 반복문이 끝날 때까지 계속되기 때문에, 다음과 같은 비슷한 출력을 얻을 수 있습니다.
(gdb) continue
l is 3
l is -2
l is 2
l is 1
l is -3
앞에서 j < k일 때, l은 항상 양수여야 한다고 말했습니다. 위 결과를 보고 우리는 l 값이 때때로 잘못된다는 것을 쉽게 알 수 있습니다.
commands에 쓸 수 있는 GDB 명령어 형태는 다음 기회에...
가끔 next나 step으로 실행 과정을 따라 가다가 반복문을 만날 경우, 반복문 끝난 부분으로 바로 건너뛰거나, 현재 함수의 실행을 정상적으로 끝내고 상위 함수로 돌아가야할 경우가 있습니다. 예를 들어:
for (i = 0; i < 1000; i++) {
/* do something #1 */
/* do something #2 */
}
/* do something #3 */
현재 "/* do something #2 */" 부분까지 실행했고, 이 반복문에 이상이 없다고 판단되면, 반복문 다음까지 빠르게 진행하고 싶을 겁니다. 이 경우, until 명령이나 advance 명령을 쓰면 편리합니다.
until 명령을 쓰면, 반복문이 아닌 경우에는 next 명령과 똑같이 동작합니다.
(gdb) until
반복문일 경우, 현재 스택 프레임 (즉, 현재 함수) 안에서, 현재 줄 다음 줄에 올 때까지 프로그램을 실행합니다. 쉬운 말로, 루프를 진행하고 빠져 나오는 순간까지 실행한 다음 "(gdb)" 프롬프트를 보여줍니다.
advance 명령은 continue 명령과 마찬가지로 프로그램을 주욱 실행시키는 대신, 지정한 곳에 코드 흐름이 오면 바로 멈춥니다. 예를 들어 위 코드의 "/* do something #3 */" 부분의 줄 번호가 34였다면, until 명령 대신 다음과 같이 실행할 수도 있습니다:
(gdb) advance 34
advance 명령은 스택 프레임에 대한 제한이 없기 때문에, 현재 함수가 아닌, 아무 곳이나 설정할 수 있으며, 위치 지정은 줄 번호 뿐만 아니라, break 명령에 쓰는 모든 형식을 다 지원합니다.
네트워크로 서비스 요청 데이터를 전송받아 분석하고, 적절한 기능을 수행하고, 그 결과를 돌려주는 서버 프로그램을 생각해 봅시다. 그리고 다음과 같은 꼴로 되어 있다고 가정해 봅시다:
#define PACKET_MAX 10
int
fetch(void)
{
int packet_received = 0;
int received[PACKET_MAX];
while (1) {
if (!packet_received) {
if (recv_data(received, PACKET_MAX) == 0)
packet_received = 1;
}
/* do work here */
process_packet(received, PACKET_MAX);
}
return 0;
}
이 프로그램은 평소에는 정상적으로 잘 동작하지만, 특정 패킷을 받으면 이상하게 동작한다고 가정합시다. 그리고 이 패킷은 아주 가끔 들어온다고 가정해 봅시다. 원하는 대로 패킷을 보내주는 프로그램을 따로 작성해 두지 않았다면, 이 프로그램을 디버깅하기 위해서, 문제를 일으키는 패킷이 올 때까지 하염없이 기다려야할 지도 모릅니다. 실제 코드는 다음과 같습니다:
만약 원하는 패킷이 recv_data()를 통해 들어왔다고 가정합시다. 이 때 packet_received는 1이 되고, 그에 따라 처리 작업이 이상하게 동작할 것입니다. 이 때, received의 내용을 저장하기 위해, 다음 명령을 쓸 수 있습니다:
(gdb) dump binary value buggy.dat received
위 명령을 수행하면 배열 received의 내용을 파일 buggy.dat에 저장합니다. 만약 시작 주소와 끝 주소를 알고 있다면 다음 명령을 쓸 수 있습니다:
dump binary data buggy.dat START-ADDR END-ADDR
이 때, START-ADDR는 시작 주소를, END-ADDR는 끝 주소를 나타냅니다. 즉, 앞 received 배열의 경우, 다음과 같이 쓸 수 있습니다.
(gdb) dump binary memory buggy.dat received received+10
어느 방법을 썼든지, 현재 디렉토리에는 buggy.dat이라는 파일로, 배열 received의 내용이 저장될 것입니다. 이는 메모리 내용을 그대로 dump시킨 것이므로 od(1)와 같은 툴을 써서 그 내용을 직접 볼 수 있습니다. received 배열은 int 배열이므로 다음과 같이 확인 가능합니다:
$ od -td buggy.dat
0000000 163 151 162 85
0000020 83 190 241 252
0000040 249 121
0000050
$ _
만약, 바로 디버깅을 성공적으로 끝냈다면, 사실 위와 같은 기능은 큰 역할을 발휘하지 못합니다. 하지만, 계속해서 디버거를 실행해서 여러번 디버깅을 해야 한다면 꽤 쓸모있다는 것을 알 수 있습니다.
일단, 새로 GDB를 띄워 디버깅을 시작했다고 합시다.
if (!packet_received) {
위 코드를 실행할 때, 강제로 packet_received를 1로 만들어, 패킷을 받는 부분을 건너뜁니다. 변수의 값 변경은 print 명령으로 쉽게 할 수 있습니다:
(gdb) p packet_received = 1
그리고 나서, received 배열을 아까 저장해 두었던 buggy.dat에서 다음과 같이
불러올 수 있습니다:
(gdb) restore buggy.dat binary received
Restoring binary file buggy.dat into memory (0xbfeda890 to 0xbfeda8b8)
이 외에도, GDB는 타 디버거에 비해 강력한 기능들을 많이 제공합니다. 다음 기회에 좀 더 알아보겠습니다.
'Computer_language > Debug' 카테고리의 다른 글
Pedro Vale Estrela - NS2 Debugging Page (0) | 2009.01.12 |
---|---|
[From NS-User] (0) | 2009.01.12 |
NS2 Programming (0) | 2009.01.12 |
ns2 gdb debug 관련 파일 (0) | 2009.01.12 |
Pedro Vale Estrela - NS2 Debug / BugFix Tutorial (OTCL + C++) (0) | 2009.01.12 |
core dump file이 발생하였을 경우 이것을 가지고 디버깅 하는 방법을 소개하고자 한다.
간단한 소스 코드를 보자.
#include <stdio.h>
int main(int argc, char* argv[]) {
char buffer[16];
buffer[80000] = 3;
return;
}
gcc -g -o main main.c
ps: -g 옵션은 디버그 정보를 넣어서 컴파일을 해 주는데, 이게 없으면 core 파일을 분석 할 수 없다.
frank@tightrope:~/tmp/gdbtest$ ./main
Segmentation fault
core dump file이 생성되지 않을 경우는 막아 놓았기 때문인데, ulimit 를 사용해서
이것을 풀어줄 수 있다.
ulimit -c 1024 (core file 용량을 정해준다.)
frank@tightrope:~/tmp/gdbtest$ ./main
Segmentation fault (core dumped)
frank@tightrope:~/tmp/gdbtest$ ls -als core
72 -rw------- 1 frank frank 147456 2006-01-13 21:19 core
분석을 해 보도록 하겠다.
frank@tightrope:~/tmp/gdbtest$ gdb ./main ./core
GNU gdb 6.3-debian
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i486-linux-gnu"...
Using host libthread_db library "/lib/tls/i686/cmov/libthread_db.so.1".
Core was generated by `./main'.
Program terminated with signal 11, Segmentation fault.
warning: current_sos: Can't read pathname for load map: Input/output error
Reading symbols from /lib/tls/i686/cmov/libc.so.6...done.
Loaded symbols for /lib/tls/i686/cmov/libc.so.6
Reading symbols from /lib/ld-linux.so.2...done.
Loaded symbols for /lib/ld-linux.so.2
#0 main (argc=1, argv=0xbfca9944) at main.c:6
6 buffer[80000] = 3;
(gdb)
이걸 보고 우리는 main 함수에서 Segmentation fault로 종료되었다는 것을 알 수 있다.
함수 이름과 매개변수 값, 몇 번째 줄에서 segmentation fault가 났는지 보여준다.
이제는 조금 더 복잡한 소스를 디버깅해 보도록 해 보자.
#include <stdio.h>
int main(int argc, char* argv[]) {
// notice the erroneous "=", the coder meant "=="
if(argv = 0) return;
printString(argv);
return;
}
컴파일 하고 실행해 보자.
frank@tightrope:~/tmp/gdbtest$ gcc -g -o main main.c
frank@tightrope:~/tmp/gdbtest$ ./main
Segmentation fault (core dumped)
frank@tightrope:~/tmp/gdbtest$ ls -als core
68 -rw------- 1 frank frank 147456 2006-01-13 21:53 core
이제 디버깅을 해 보도록 하자.
frank@tightrope:~/tmp/gdbtest$ gdb ./main ./core
GNU gdb 6.3-debian
Copyright 2004 Free Software Foundation, Inc.
...
#0 0x08048396 in printString (string=0x0) at main.c:14
14 sprintf(string, "This is a test.\n");
#0 줄을 보면 string 이 널 포인터라는게 분명하다. 그런데 우리는 이게 어디서 왔는지
모른다. 단서는 파일 main.c에 14번 째 줄이라는 것과 main 에서 에러가 발생하지
않았다는 것이다. 그래서 우리는 backtrace를 할 것이다.
(gdb) backtrace
#0 0x08048396 in printString (string=0x0) at main.c:14
#1 0x0804837c in main (argc=1, argv=0x0) at main.c:8
'backtrace' 명령어는 함수를 연대기순으로 나열해 준다. 맨 위에 줄이 segmentation fault
가 발생한 소스이다. 저걸 보면 main.c:8 에서 main.c:14를 호출한 것을 알 수 있다.
더 많은 정보를 얻기 위해 'frame' 명령을 사용하도록 하자. 'frame' 명령은 'backtrace'
명령 실행으로 나온 항목들에 대해 더 많은 정보를 보여준다.
(gdb) frame 0
#0 0x08048396 in printString (string=0x0) at main.c:14
14 sprintf(string, "This is a test.\n");
(gdb) frame 1
#1 0x0804837c in main (argc=1, argv=0x0) at main.c:8
8 printString(argv);
이것을 보면 이미 main.c:8 에서 argv가 0으로 값이 넘어간 것을 알 수 있다.
*잡다한 tip
스레드를 사용해서 프로그래밍을 할 때 threadsafe 함수를 사용해야 한다. 예를 들어서 localtime
함수는 언젠가 프로그램과 충동할 것이다. 그 대신에 localtime_r 함수를 쓰는게 좋다. localtime_r
함수는 threadsafe 함수이다.
'Computer_language > Comfile' 카테고리의 다른 글
[Tip] win32k.sys 덤프가 뜰 경우..(메모리 테스트 프로그램들) [출처] [Tip] win32k.sys 덤프가 뜰 경우..(메모리 테스트 프로그램들) (0) | 2009.01.12 |
---|---|
프로그램 덤프 뜨기... (0) | 2009.01.12 |
NS2 - 무선환경 시뮬레이션 (0) | 2009.01.12 |
[EJB] 객체지향과 컴포넌트 프로그래밍 [출처] [EJB] 객체지향과 컴포넌트 프로그래밍|작성자 젠센쭈 (0) | 2009.01.12 |
유선과 무선 환경의 차이
- 노드 사이의 명시적인 링크가 존재하지 않음
- 라디오의 전송 범위 내에 노드들이 위치하면 통신 가능
- 무선상에서는 MAC 기술이 아주 중요
- NS2 유선 시뮬레이션 환경은 MAX이 존재하지 않음
- 무선상에서 노드는 이동성이 존재
- 라우팅
- 유선상에서는 특별히 라우팅을 설정하지 않아도 링크 설정관계를 통해서 최단 경로가 선택
- 무선에는 반드시 라우팅이 존재해야 함
- 주소
- 유선에서는 flat address
- 무선만 존재하면 flat address
- 유무선 연동인 경우는 hierarchical address
NS2에서의 유선과 무선의 차이
- C++ 상에서의
- 유선 : class Node
- 무선 : class MobileNode
- 노드 생성은 유무선 모두 $ns node로 하지만 무선에서는 node-config라는 추가 작업이 필요
- Trace
- $ns/common 에서
- 유선 : trace.{h, cc}
- 무선 : cmu-trace.{h, cc}
- $ns/common 에서
- 이동성
- 무선에서는 이동성을 지원
- 위치 정보가 중요
- 유선에서는 연결 구조가 중요, 노드의 배치는 중요하지 않음
- 무선에서는 위치에 따라서 전송 범위 내에 있는지 여부가 결정
- 그 외에는 유무선 모두 같은 네트워크 컴포넌트를 사용
node-config
* $ns가 앞으로 만들어질 노드에 대한 설정 정보들을 저장, 이후 만들어지는 노드들은 그 설정 정보들로부터 생성됨
* -adhocRouting 설정 정보로부터 유선 노드와 무선 노드 구별
* node-config를 이용해서 노드 설정을 바뀌었다면 바뀐 설정은 새로 만들어지는 노드부터 적용
Topography
* 지형도
* 영역을 만듦 ->무선 환경에서는 위치가 중요 정보
* set topo [new Topography]
* $topo load_flatgrid <x축 크기> <y축 크기>
God 객체
* Simulater와 마찬가지로 단 하나의 인스턴스만 존재, 없다면 에러
* 노드간 연결(라우팅 가능한지) 정보 제공
* 예) set god_ [create-god <모바일 노드 개수>]
* God 인스턴스 이름은 god_로 해야함 (일부 $god_로 사용)
- -adhocRouting : 라우팅 프로토콜 설정
- DSDV, AODV, TORA,, FLOODING,...
- -llType : 링크 레이어 설정
- LL
- -macType : 맥프로토콜 설정
- Mac/802_11, Mac/SMAC
- -propType : 경로손실타입
- Propagation/twoRayGround, Propagation/FreeSpace, Propagation/Shadowing,...
- -ifqType : 큐타입
- Queue/DropTail/PriQueue,...
- 애드혹라우팅 프로토콜이 DSR인 경우 반드시 CMUPriQueue로 해야 함
- -ifqLen : 큐길이
- 패킷 개수
- -phyType : 피지컬레이어타입
- Phy/WirelessPhy,...
- -antType : 안테나 타입
- Antenna/OmniAntenna
- -channel : 현재는 없어짐
- Channel/WirelssChannel
- -topoInstance : 전체 영역
- $topo
- -agentTrace : 트레이스 설정
- ON or OFF
- -routerTrace : 트레이스 설정
- ON or OFF
- -macTrace : 트레이스 설정
- ON or OFF
- -movementTrace : 트레이스 설정
- ON or OFF
NS2 무선환경 시뮬레이션 스크립터
$ns trace-all $trfd
$ns namtrace-all-wireless $namfd 1000 1000
$topo load_flatgrid 1000 1000
-llType LL \
-macType Mac/802_11 \
-propType Propagation/TwoRayGround \
-ifqType Queue/DropTail/PriQueue \
-ifqLen 50 \
-phyType Phy/WirelessPhy \
-antType Antenna/OmniAntenna \
-channel [new Channel/WirelessChannel] \
-topoInstance $topo \
-agentTrace ON \
-routerTrace ON \
-macTrace ON \
-movementTrace ON
set node_($i) [$ns node]
$node_($i) set Y_ [expr $i / 5 * 200]
$node_($i) set Z_ 0
}
$ns attach-agent $node_(0) $udp
$cbr set packetSize_ 500
$cbr set interval_ 0.1
$cbr attach-agent $udp
$ns attach-agent $node_(24) $null
$ns attach-agent $node_(20) $tcp
$ftp attach-agent $tcp
$ns attach-agent $node_(4) $tcpSink
$ns at 5.0 "$cbr stop"
$ns at 4.0 "$ftp stop"
$ns run
Max/802_11 Bandwidth
- basic rate, data rate 두가지 존재
- basic reate : RTS, CTS, ACK 을 전송하는 속도
- data rate : 실제 데이터를 전송하는 속도
- 설정하는 방법
- Mac/802_11 set basicRate_ 1Mb
- Mac/802_11 set dataRate_ 1Mb
- 기본값은 둘다 1Mbps
범위 설정
- 전송 범위 : 데이터가 성공적으로 수신되는 범위
- 캐리어 감지 범위 : 데이터를 성공적으로 수신하지는 못하지만 신호만 감지하는 범위 -> CSMA/CA 프로토콜의 CA
- 전송 범위와 캐리어 감지 범위는 수신 노드에서 받는 전파세기와 쓰레쉬홀드에 의해서 결정
- $ns/tcl/lib/ns-default.tcl
Phy/WirelessPhy set CSThresh_ 1.559e-11 # 수신한 전파의 세기가 이 값 이상이면 감지
Phy/WirelessPhy set RXThresh_ 3.652e-10 # 수신한 전파의 세기가 이 값 이상이면 성공적인 수신
Phy/WirelessPhy set bandwidth_ 2e6
Phy/WirelessPhy set Pt_ 0.28183815
Phy/WirelessPhy set freq_ 914e+6
Phy/WirelessPhy set L_ 1.0
- 다른 설정은 변경하지 않고 위 두개의 threshold만 변화시켜 범위 조절 가능
- $ns/indep-utils/propagation/threshold.cc
- 컴파일 후 threshold -m <경로손실모델> <범위> ->threshold
'Computer_language > Comfile' 카테고리의 다른 글
[Tip] win32k.sys 덤프가 뜰 경우..(메모리 테스트 프로그램들) [출처] [Tip] win32k.sys 덤프가 뜰 경우..(메모리 테스트 프로그램들) (0) | 2009.01.12 |
---|---|
프로그램 덤프 뜨기... (0) | 2009.01.12 |
[디버깅] core dump file을 분석해 보자 (0) | 2009.01.12 |
[EJB] 객체지향과 컴포넌트 프로그래밍 [출처] [EJB] 객체지향과 컴포넌트 프로그래밍|작성자 젠센쭈 (0) | 2009.01.12 |
[EJB] 객체지향과 컴포넌트 프로그래밍 [출처] [EJB] 객체지향과 컴포넌트 프로그래밍|작성자 젠센쭈
| Computer_language/Comfile 2009. 1. 12. 02:26우리나라에서도 컴포넌트에 대한 관심이 많이 높아지고 있습니다. 이것은 객체지향과 더불어 하나의 새로운 패러다임이라고도 합니다. 컴포넌트는 소프트웨어를 이제까지처럼 계획, 설계, 분석, 구현, 테스트하는 순차적인 개발 과정으로 보는 것이 아니라 레고 블럭이나 자동차 부품처럼 조립하는 개념으로 간주하기 때문에 현재 IT 산업을 이루고 있는 기존 틀과 구조를 가내 수공업 형태에서 ‘표준화’하고 ‘산업화’하는 데 그 의미가 있다고 하겠습니다.
이러한 컴포넌트는 인터넷이라는 환경의 부상으로 더욱 강력한 위치를 차지하게 됐습니다. 그것은 개발자에게도 새로운 개발 환경을 이해하도록 요구하고 있습니다. 컴포넌트 프로그래밍에서는 특정한 환경의 국소적인 이해보다는 시스템에 대해 전체적으로 이해할 수 있어야만 좋은 프로그램을 제작할 수 있습니다. 하지만 컴포넌트를 쉽게 이해할 수 있는 방법이 마땅히 있지 않습니다. 이번 연재에서는 객체지향과 컴포넌트를 연계해서 이해하고 간단하게 컴포넌트 환경을 살펴보겠습니다.
컴포넌트의 등장
컴포넌트라 함은 말 그대로 서로의 인터페이스를 연결하면 하나의 시스템으로 동작할 수 있는 환경의 구성 요소를 의미합니다. 몇 가지 컴포넌트의 결합으로 새로운 소프트웨어를 만들 수 있다는 생각은 그리 새로운 것은 아닙니다. 실제 우리 생활에서도 뮤직 컴포넌트처럼 서로 공통의 인터페이스로 연결해 새로운 시스템을 만드는 경우가 많습니다. 어릴 적 즐겨 가지고 놀았던 합체 로봇도 컴포넌트 시스템입니다. 그리고 비디오, 오디오, TV로 만드는 AV 시스템 역시 훌륭한 컴포넌트 시스템의 예입니다. 서로 다른 기능을 하는 두 요소를 결합해 새로운 기능을 만든다면 그것을 컴포넌트 시스템이라고 말할 수 있습니다.
우리가 사용하는 컴퓨터 시스템도 또 다른 컴포넌트 시스템의 예입니다. 컴퓨터 시스템은 컴퓨터 본체, 모니터, 키보드, 마우스, 스피커로 구성되고 사전에 규정된 모니터 케이블, PS/2 마우스, 키보드 포트, 오디오 잭 등을 통해 컴포넌트의 결합을 이룹니다. 하드웨어적인 컴포넌트는 표준화를 통해 산업화하는 하나의 전형적인 메커니즘입니다.
소프트웨어에서도 컴포넌트라고 하면 마찬가지 개념으로 생각해 볼 수 있습니다. 소프트웨어에 있어서 컴포넌트라는 개념은 RAD 툴의 등장과 더불어 탄생했다고 할 수 있습니다. RAD 툴의 등장 자체가 프로그래밍의 산업화 과정이라고 할 수 있습니다. 물론 소프트웨어 컴포넌트라는 개념이 갑자기 만들어진 것은 아닙니다. 이미 컴포넌트 이전에 라이브러리가 그러한 기능을 하고 있었습니다. 사전에 필요한 함수들을 만들어 놓고 프로그래밍에서 활용할 수 있도록 만들어 두는 것이 라이브러리인데, 컴포넌트를 이해하기에 앞서 우선 라이브러리를 이해하는 것이 좋습니다.
소프트웨어 라이브러리
소프트웨어 라이브러리는 매우 중요한 프로그래밍 자원으로 프로그래밍에 자주 쓰이는 함수나 클래스를 함께 묶어놓은 것입니다. 라이브러리 안에 있는 코드를 사용하기 위해서는 필요한 함수나 클래스의 사용법을 미리 숙지하고, 정의된 인터페이스를 통해 프로그래밍 모듈을 작성하고, 컴파일 과정에서 라이브러리와 작성한 모듈을 링크하는 작업이 필요합니다.
라이브러리를 많이 가지고 있는 친구는 빠르게 프로그램을 완성할 수 있는 반면에 라이브러리를 가지고 있지 않거나 기본적으로 언어 개발 툴에서 제공하는 라이브러리를 잘 이해하지 못하는 경우에는 이미 라이브러리에 있는, 그러나 본인은 알지 못하는, 함수를 직접 구현하기 위해 많은 시간을 소비하면서도 프로그램을 완성하지 못하기도 합니다.
기껏 만들고 있으면, 다른 친구가 “야, 이건 이미 기본 라이브러리에 있는 기능이야”라고 말해 당사자를 실망하게 만듭니다. 상황이 이렇다 보니, 한때는 소프트웨어 개발자들이 가지고 있는 소프트웨어 라이브러리의 양이 좋은 개발자와 나쁜 개발자로 나누는 기준이 되기도 했습니다.
일부 개발자 중에서는 편집광적으로 소프트웨어 라이브러리를 모으는 경우도 쉽게 찾아볼 수 있었습니다. 하지만, 개발 툴이 커지고 대규모가 되면서 대부분의 필요한 라이브러리들은 개발 툴에 포함되어 제공됐습니다. 최근에는 개발 툴이 제공하지 않는 추가적인 라이브러리를 이용하는 비중은 틈새시장 정도로 무척이나 작아졌습니다. 그만큼 소프트웨어 라이브러리가 정형화됐다고 할 수 있습니다. 하지만, 여전히 라이브러리에 대한 충분한 사전 지식 없이 소프트웨어 라이브러리를 쉽게 사용한다는 것은 어려운 일입니다.
컴포넌트와 라이브러리
인터넷의 보급과 더불어 누구나 쉽게 라이브러리를 인터넷에서 찾아 사용할 수 있게 됐고 부담 없는 수준에서 라이브러리를 구입할 수 있습니다. 그리고 오픈 소스 정책으로 인해 많은 라이브러리 소스가 공개되면서 라이브러리의 가치는 지속적으로 떨어졌습니다. 하지만 이것은 다른 한편으로 게으른 개발자를 양산했습니다. 그냥 모든 것은 인터넷에 있다고 생각하는 나머지 라이브러리 사용법조차 알지 못하는 개발자가 많아진 것입니다.
이러한 문제점을 새롭게 해결하고자 나온 것이 바로 컴포넌트입니다. 컴포넌트는 컴포넌트 스스로가 어떤 인터페이스를 가지고 있는지 개발자에게 알려줄 수 있습니다. 그리고 속성을 변경해 자신의 초기값을 조정할 수 있기 때문에 커스터마이징이 쉽습니다. 이러한 사용자 환경이 만들어낸 것이 애플리케이션을 빠르게 제작할 수 있는 RAD 툴입니다. RAD 툴 안에서는 미리 작성되어 있는 컴포넌트들을 통해 애플리케이션을 쉽게 작성할 수 있습니다.
컴포넌트의 기본적인 개념은 소프트웨어 라이브러리와 유사합니다. 라이브러리가 사전에 필요한 코드를 작성해놓고 나중에 컴파일 과정에서 연결해 사용하는 것이라면, 컴포넌트는 사전에 정의된 컴포넌트 인터페이스를 만족하도록 작성되고, 이를 통해 프로그래밍 과정에서 연결해 사용할 수 있게 되어 있습니다. 따라서 컴포넌트의 동작 상황을 개발 당시에 확인할 수 있다는 장점이 있습니다. 그리고 대부분 비주얼한 컴포넌트를 제공하기 때문에 GUI까지 한꺼번에 해결해주기도 합니다.
최근에 나온 개발 툴은 대부분 자신들의 컴포넌트를 제공합니다. 하지만 컴포넌트가 단지 최신 라이브러리라는 의미는 아닙니다. 그것은 현재 시스템이 점점 복잡해지면서 시스템을 구축할 때 모든 기능을 새롭게 구축하면 비용이 많이 들기 때문에 필수적이고 복잡한 환경은 사전에 만들어 놓고 적용하려는 시스템에 따라 변화되는 부분만을 새롭게 작성하여 동작할 수 있도록 하는 데 사용하는 것입니다. 아마도 이것은 뮤직 컴포넌트 개념보다는 DVD 플레이어와 DVD의 관계라고 말할 수 있을 것입니다.
컴포넌트의 구성
그렇다면 소프트웨어 컴포넌트는 무엇으로 구성될까요? 컴포넌트는 우선 서로 다른 컴포넌트를 연결하는 입출력 인터페이스가 필요합니다. 그리고 기본적인 상태를 정의할 수 있는 프로퍼티가 있습니다. 컴포넌트는 잘 정의된 인터페이스를 통해 사전에 구현한 소프트웨어 모듈입니다. 예전에는 특정한 툴 환경에서만 의미 있는 컴포넌트들이 있었는데, 지금은 특정한 언어와 상관없이 사용할 수 있는 컴포넌트 구조들도 있습니다.
대표적으로 COM(Common Object Model)은 윈도우 환경에서 빼놓을 수 없는 핵심적인 컴포넌트 구조를 제공합니다. COM은 윈도우의 핵심 커널을 제외하고는 대부분의 환경에서 사용되는 모델입니다.
그리고 EJB(Enterprise Java Beans)는 자바 프로그래밍 언어를 통해 미들웨어를 구현할 수 있도록 제공하는 모델입니다. 트랜잭션, 보안, 데이터베이스 접속 등을 쉽게 이용할 수 있도록 하는 환경을 제공하고, 이를 컴포넌트화해 소프트웨어 모듈을 개발할 수 있게 하는 것입니다.
객체지향과 컴포넌트
객체지향은 소프트웨어를 행동과 상태 기반의 객체로 정의해 재사용성을 높이는 것이 목표입니다. 컴포넌트는 사전에 정의된 인터페이스를 통해 즉각 실행할 수 있는 컴포넌트로 생산하여 재사용성을 높이는 것입니다. 재새용성을 높인다는 것에 대해서는 그 목적이 동일하지만 접근 방법의 차이가 있다고 할 수 있습니다. 객체지향에서는 새로운 데이터 형을 작성하는 것부터 출발하는데, 컴포넌트는 잘 짜여진 인터페이스를 사전에 정의하고, 이를 통해 필요한 동작을 구현하는 것부터 출발합니다.
◆ 클래스 = 데이터 + 메쏘드
◆ 컴포넌트 = 특성 + 인터페이스 + 로직 + 이벤트
이와 같이 컴포넌트는 서로의 기능을 쉽게 연결할 수 있는 인터페이스가 가장 중요한 위치를 차지합니다. 객체지향과 컴포넌트가 만나는 위치는 인터페이스입니다. 컴포넌트의 인터페이스는 객체지향의 메쏘드와 유사합니다. 물론 객체지향 언어에서 메쏘드와 인터페이스는 엄밀한 의미에서 구별되지만 실제로는 그 기능이 비슷하다고 할 수 있습니다.
차이점은 인터페이스가 아니라 접근 방법에 있습니다. 컴포넌트는 컨테이너라는 컴포넌트가 동작할 수 있는 환경을 제공하고, 이 구조에 맞는 다른 컴포넌트들이 서로 연결되어 동작할 수 있게 해주는 개발 환경이라면, 객체지향의 기본 개념은 메쏘드 재정의나 클래스 확장을 통해 기존 클래스를 재사용할 수 있도록 하는 것입니다.
컴포넌트 소프트웨어 환경
컴포넌트는 크게 비주얼 컴포넌트와 미들웨어 컴포넌트로 나눌 수 있습니다. 비주얼 컴포넌트는 대부분의 RAD 툴이 제공하는 각종 UI와 이를 이용한 기능성 컴포넌트입니다. 반면에 미들웨어 컴포넌트는 미들웨어 위에서 동작하는 컴포넌트로 특별한 UI없이 동작할 수 있습니다. 최근 웹 환경 개발이 강화되면서 중요한 컴포넌트로 자리를 잡았습니다. 여기서 중요한 것은 비주얼 컴포넌트와 미들웨어 컴포넌트는 개념상 비슷하지만 그 내용은 크게 다르다는 것입니다.
비주얼 컴포넌트
비주얼 컴포넌트는 말 그대로 시각적인 컴포넌트입니다. 비주얼 컴포넌트 소프트웨어로는 델파이, 비주얼 베이직, 비주얼 에이지 등이 대표적입니다. 이들은 대부분 비슷한 GUI를 가지고 컴포넌트를 마우스를 통해 소프트웨어 폼에 쉽게 부착하고, 프로퍼티를 조절하고, 다른 컴포넌트와 연결해 새로운 소프트웨어를 만들 수 있습니다. 비주얼 컴포넌트는 대부분 사용자 인터페이스와 관련이 있습니다. 여기서는 이에 대해 구체적으로 다루는 것은 생략하겠습니다.
미들웨어 컴포넌트
미들웨어 컴포넌트는 시각적인 인터페이스를 제공하지 않는 컴포넌트입니다. 미들웨어 컴포넌트는 컴포넌트 환경이 제공하는 다양한 기능을 이용해 실제 데이터의 동작에 대한 로직만을 제공해 소프트웨어가 완성될 수 있게 합니다. 미들웨어 컴포넌트는 대부분 웹 인터페이스를 통해 사용자와 인터페이스하기 때문에 사용자는 웹 브라우저로 웹 서버에 접속하면 내부적인 미들웨어 컴포넌트에 의해 동작되는 결과들을 확인할 수 있습니다. 아마도 최근에 비주얼 컴포넌트보다 미들웨어 컴포넌트가 더 강화되는 이유가 이 때문이 아닐까 생각합니다. 미들웨어 컴포넌트의 대표적인 두 가지가 COM+와 EJB입니다.
COM+
COM+는 마이크로소프트의 MTS(Microsoft Transaction Server)의 최신 버전입니다. 1999년에 발표됐으므로 약 3년 정도의 시간이 지났습니다. COM+는 닷넷 기반의 애플리케이션과 COM을 하나로 묶는 것으로 관리 기능, JITA(Just-In-Time Activation), 객체 풀링, 트랜잭션 처리, 동기화, 보안, 컴포넌트 큐 관리, 이벤트 처리 등 다양한 기능을 제공합니다. COM+에는 다음과 같은 구성 요소가 있습니다.
◆ COM+ 대리 실행 모듈(dllhost.exe)
◆ COM+ 클래스 로딩 및 리모트 프레임워크(ole32.dll)
◆ 컨텍스트 오브젝트와 랩퍼 프록시
◆ COM+ 서버 컴포넌트
◆ COM+ 클라이언트
◆ COM+ 런타임, 서비스 컨트롤 관리자(SCM), 분산 트랜잭션 관리자(MS-DTC), 메시지큐(MSMQ), 트랜잭션 통합 관리자(COM-TI)
COM+는 조금 복잡한 동작을 요구합니다. 기본적으로 COM+ 서버 컴포넌트는 OLE32.DLL 및 DLLHOST.EXE로 구성된 컨테이너에 의해 호출되고, 다시 컨텍스트 객체에 의해 처리되도록 고안됐습니다. 이것은 COM+가 기존 MTS 및 다른 환경도 함께 수용할 수 있는 환경을 만들다 보니 더 복잡한 환경을 제공하는 것으로 보이지만 실제로는 기존 COM 환경에서 그리 많은 변화가 있던 것은 아닙니다.
COM+의 특징 중 하나는 스스로 자신을 설명할 수 있는 컴포넌트라는 점입니다. 이것은 프로퍼티와 달리 애트리뷰트를 제공합니다. 애트리뷰트는 컴포넌트에 런타임 특성을 설정할 수 있는 것을 의미합니다. 이것은 특정한 기능이 작동하도록 컴포넌트를 조절할 수 있는 기능입니다. 예를 들어, 데이터베이스 연결에 사용하는 문자열을 런타임 환경에서는 관리자에 의해 변화할 수 있는 값이 되는데 이것은 자바 환경에서 상태 저장·복원과 관련이 있는 것입니다.
COM+는 또한 큐 컴포넌트 기능을 제공합니다. 큐 컴포넌트는 컴포넌트와 애플리케이션의 상호작용이 실시간에 이루어지지 않더라도 큐를 이용해 컴포넌트가 다시 기동했을 때 상호작용이 다시 일어날 수 있게 하는 것입니다. 이벤트 메커니즘은 자바의 이벤트 리스너 모델처럼 다양한 이벤트 채널을 통해 이벤트를 전달할 수 있게 되어 있습니다. 이 밖의 다른 기능은 COM+의 참고자료를 이용하기 바랍니다.
EJB
EJB는 자바 표준 미들웨어 컴포넌트 구조입니다. EJB는 다음과 같은 구성요소로 이루어져 있습니다.
◆ EJB 서버
◆ EJB 컨테이너(EJB 서버 위에서 동작)
◆ EJB(EJB 컨테이너 위에서 동작)
◆ EJB 클라이언트
◆ 기타 구성 요소 : JNDI(Java Naming and Directory Inter face), JTS(Java Transaction Service)
이를 그림으로 표현하면 <그림 4>와 같습니다. EJB 서버는 CORBA의 ORB(Object Request Broker)와 비슷한 기능을 제공합니다. 시스템 실행환경을 제공하고, 멀티프로세싱, 부하분산, 장치제어, 네이밍 및 트랜잭션 처리 등을 제공하며, EJB 컨테이너를 올릴 수 있게 해줍니다. EJB 컨테이너는 EJB와 외부 세계의 인터페이스를 제공합니다. EJB 클라이언트는 결코 직접 EJB와 연결되지 않습니다. EJB를 연결할 수 있는 메쏘드를 EJB 클라이언트에 제공하는 것이 바로 EJB 컨테이너입니다.
EJB는 크게 두 종류가 있습니다. EJB의 상태가 지속적으로 유지되는 경우와 매번 초기화되는 경우가 있습니다. EJB의 상태가 유지되려면 가장 최근의 EJB 속성을 저장하거나 복구하는 기능을 제공해야 합니다. 이러한 기능은 EJB 컨테이너 인터페이스를 통해 EJB 서버가 제공합니다. EJB 클라이언트는 컴포넌트를 이용하는 주체이며, EJB는 컴포넌트 자체입니다. 이러한 구조는 대부분의 컴포넌트 구조의 특징입니다.
EJB는 EJB 클라이언트의 요청에 의해 그때마다 생성되는 세션 빈과 상태를 공유하는 엔티티 빈으로 나눌 수 있습니다. 엔티티 빈은 항상 상태를 가지고 있으며, 시스템이 동작하는 한 계속 살아있는 상태가 됩니다. 세션 빈은 상태를 가지고 있을 수도 있고, 상태가 없을 수도 있습니다. 이러한 상태 저장 및 복구는 그 주체가 누구이냐에 따라 컨테이너 관리형 상태 유지와 빈 관리형 상태 유지로 나눌 수 있습니다. 쉽게 이야기하면 저장 및 복구를 컨테이너가 하면 컨테이너 관리형 상태 유지이고, 저장 및 복구가 빈에 의해 수행되면 빈 관리형 상태 유지입니다. EJB는 *.ser 파일에 의해 동작 가능하도록 배치(deploy)할 수 있습니다. 이 파일은 다음과 같은 엔트리로 지정되어 동작합니다.
Name: ~bean/CompanyAccssRight.ser
Enterprise-Bean: True
컴포넌트의 미래
정보통신부는 컴포넌트 산업을 육성하기 위해 향후 3년간 공용 컴포넌트 개발은 물론 공용 컴포넌트 뱅크 마련, 컴포넌트 유통 시스템 구축, 컴포넌트 품질 평가 및 인증제도 확립, 컴포넌트 가격 산정 및 라이선스 규정 수립, 관련 인력 양성 등에 박차를 가할 계획이라고 합니다.
현재 국내 시장에 나와 있는 컴포넌트는 약 150종에 불과하지만 올해 말이면 1000개 가량의 컴포넌트가 개발될 것이라고 예견하기도 합니다. 현재 국내 신생 업체들을 중심으로 자바 컴포넌트 기술을 이용해 상용 애플리케이션을 개발하는 사례가 늘고 있으며 특히 전사적자원관리 시스템부터 개발 툴, XML 솔루션, 검색엔진, 통신용 소프트웨어, 인터넷 구축 툴, 언어처리 모듈, 지식관리, 고객관리 등의 분야에 이르기까지 다양한 분야에서 개발이 진행되고 있다고 합니다.
하지만 컴포넌트가 일반적인 개발 환경이 되기 위해서는 넘어야 할 과정이 많이 있습니다. 아직 교육 환경이 컴포넌트 개발 환경에 맞게 빠르게 제공되지 못하는 실정입니다. 대부분의 개발자는 전체 컴포넌트 환경을 이해할 수 있는 수준이 되지 못하고 있습니다. 그것은 소프트웨어 시장의 요구를 교육 환경이 미처 뒷받침해줄 수 없을 정도로 빠르게 변화하고 있다는 것을 의미합니다. 필자의 생각으로는 컴포넌트가 일반적인 개발 환경에 되려면 좀더 많은 시간이 필요할 것입니다. 객체지향이 20년이 지나서야 일반적인 개발환경이 되는 것을 보면 새로운 개념이 쉽게 보편화되지는 않기 때문입니다. 하지만 컴포넌트가 일반화되는 데 20년이 걸릴 것이라는 것은 아닙니다. 그보다 훨씬 빠른 시간에 도달할 것입니다. 하지만 이 자리에서 그 시기가 언제라고 언급하는 것은 별 의미가 없다고 생각합니다.
컴포넌트에 대한 깊은 이해와 적용 필요
컴포넌트와 객체지향은 밀접한 관계가 있다고 말할 수 없지만, 그 목적이 비슷하기 때문에 객체지향과 컴포넌트가 연결되는 다양한 시도가 있었습니다. 대부분의 개발 툴이 객체지향 언어를 지원하고 컴포넌트 구조를 제공함으로써 더욱 편리하게 프로그래밍이 가능하도록 하고 있습니다. 비주얼 컴포넌트는 대부분의 개발 툴이 제공하는 요소가 됐습니다. 하지만 이것은 특정한 개발 환경에서만 사용할 수 있는 경우가 많고, 대부분 웹 인터페이스를 사용하기 때문에 미들웨어 컴포넌트의 중요성이 더 커지고 있습니다. 미들웨어 컴포넌트는 윈도우 기반의 COM 계열, 자바 기반의 EJB 계열, OMG의 CORBA 계열이 있습니다.
이들을 간략하게 살펴보았는데, 실제 체계적인 학습 없이 빠른 시간에 컴포넌트 환경을 이해한다는 것은 매우 어려운 일입니다. 개발자들이 컴포넌트 환경을 이해하고 업무에 활용하기 위해서는 앞으로 많은 교육 기회가 주어져야 할 것으로 보입니다. 그런 의미에서 서로 다른 컴포넌트 환경은 경쟁자라기보다는 컴포넌트라는 하나의 거대한 목표를 향해 달리는 협력 주자라고 할 수 있습니다.
정리 : 송우일 wooil@sbmedia.co.kr
출처 : http://www.imaso.co.kr/
'Computer_language > Comfile' 카테고리의 다른 글
[Tip] win32k.sys 덤프가 뜰 경우..(메모리 테스트 프로그램들) [출처] [Tip] win32k.sys 덤프가 뜰 경우..(메모리 테스트 프로그램들) (0) | 2009.01.12 |
---|---|
프로그램 덤프 뜨기... (0) | 2009.01.12 |
[디버깅] core dump file을 분석해 보자 (0) | 2009.01.12 |
NS2 - 무선환경 시뮬레이션 (0) | 2009.01.12 |
Teodor Zlatanov
프로그래머, Gold Software Systems
2002년 1월
보다 나은 펄(Perl) 프로그래밍을 위한 완벽한 가이드를 제공하고 있다. 이번에는 객체 지향 프로그래밍(OOP, Object Oriented Programming)에 대해 설명한다. 객체 지향 프로그래밍이 무엇이고, 언제 쓰이며, 펄에서는 어떻게 작용하는지를 설명할 것이다.
객체 지향 프로그래밍(Object Oriented Programming, OOP)
OOP는 프로그래밍 방식 또는 문제를 해결에 쓰이는 일반적인 접근방식이다. 절차적 프로그래밍과 함수 프로그래밍 방식과는 연관성이 적고, 그들과 잘 맞지 않는다.
이 글에서는 펄에서 OOP와 함수적/절차적 프로그래밍의 기초를 다루겠다. 그리고 펄 프로그램과 모듈에서 OOP를 사용하는 방법을 설명하겠다. 이 글은 요약에 그칠것이라는 것을 명심하라. 펄 OOP의 모든 부분을 설명하는 것은 무리이다. 그와 같은 것을 다루는 책이 많이 나와있으며 이미 여러차례 다루어졌다.(참고자료).
OOP가 정확히 무엇인가?
OOP는 객체를 사용하여 문제를 해결하는 기술이다. 프로그래밍 용어에서 객체(object)란 속성(property)과 동작(behavior)이 문제 해결에 주요하게 작용하는 엔터티(entity)이다. 정의를 좀 더 자세하게 내려야 하겠지만, 오늘날의 컴퓨터 산업에서 OOP 접근방식의 엄청난 다양성 때문에 불가능하다.
펄 프로그래밍에서, OOP는 필수적인 것은 아니다. Perl version 5 이상 버전은 OOP를 장려하지만 필요한 것은 아니다. 모든 펄 라이브러리는 모듈이다. 적어도 OOP의 기초를 사용한다는 것을 의미한다. 게다가 대부분의 펄 라이브러리는 객체로서 구현된다. 모듈을 사용하는 사람은 특정 작동과 속성을 지닌 OOP 엔터티로서 그들을 사용해야 한다.
기본적인 OO 프로그래밍 언어 특징
일반적으로, OO 프로그래밍 언어에 있어서 기본적인 세 가지의 특징이 있다. 상속(inheritance), 다형성(polymorphism), 캡슐화(encapsulation)가 바로 그것이다.
펄은 상속을 지원한다. 상속(Inheritance)은 하나의 객체(child)가 시작 포인트(parent)로서 다른 것을 사용하고 그런다음 필요할 경우에 속성과 동작을 변경할 때 적용된다. 이러한 자식-부모 관계는 OOP에 있어서 필수적이다. 재사용(reuse)은 OOP의 이점 중 하나이며 프로그래머들도 이러한 특징에 만족한다.
상속에는 두 가지 유형이 있다. 단일 상속(Single inheritance)은 자식이 단 하나의 부모를 가져야 한다. 반면 다중 상속(multiple inheritance) 은 좀 더 자유롭다. 실제로 둘 이상의 부모를 보게 될 경우는 드물지만 펄은 다중 상속을 지원한다.
다형성(Polymorphism)은 한 객체를 다른 것처럼 보이도록 만드는 기술이다. 펄에서, 다형성은 완벽히 지원된다. 펄 프로그래머들은 상속된 동작을 변경하는 것보다는 객체 속성을 가진 일반적인 작동을 변경하는 것을 더욱 선호하기 때문에 빈번하게 사용되는 것은 아니다. 234 포트의 UDP 패킷 리셉션과 트랜스미션에, 80 포트의 TCP 패킷 리셉션에, 1024 포트의 TCP 패킷 트랜스미션을 위해 세 개의 IO::Socket::INET
객체를 만드는 코드를 기대한다는 것을 의미한다. 첫 번째 경우에는 IO::Socket::INET::UDPTransceiver
를, 두 번째의 경우 IO::Socket::INET::TCPReceiver
를, 세 번째의 경우 IO::Socket::TCPTransmitter
를 사용하는 코드를 기대하기 보다는 말이다.
OOP 순수주의자들은 모든것이 적절하게 구별되어 한다고 느끼지만, 펄 프로그래머들은 순수주의자가 결코 아니다. 그들은 OOP 규칙에 대해 융통성을 발휘하고 있다.
캡슐화(Encapsulation)는 객체 작성자가 액세스를 허용하기를 원치 않는다면 사용자에게 액세스를 불가능하게 하는 방식으로 객체 작동과 속성을 인클로징한다는 것을 의미한다. 그러한 방식으로 객체 사용자들은 하지 못하도록 정해진 일을 수행할 수 없으며, 액세스 할 수 없도록 지정된 것에 액세스 할 수 없다(Listing 1).
왜 OOP가 강력한 방식인가?
어떻게 OOP가 강력한 방식이 될 수 있었는지 생각해보자. OOP에는 절차적/함수 프로그래밍 (PP/FP)에 적용하기에 까다로운 여러 주요 개념들이 있다는 것을 알았다. 우선, 무엇보다도, PP나 FP 모두 상속이나 클래스 다형성 개념이 없다. PP와 FP에는 클래스가 없기 때문이다. PP와 FP에서 캡슐화는 존재한다. 하지만 단지 절차적 레벨에서 존재하는 것 뿐이다. 결코 클래스나 객체 속성으로서 존재하는 것은 아니다. 따라서 프로그래머는 호환되지 않는 방식들을 섞는 것보다 전체 프로젝트에 OOP를 고수하는 것이 낫다.
OOP는 절차적 프로그래밍 방식과 함께 잘 작동하지 않는다. 왜냐하면 OOP는 객체에 집중하고 절차 프로그래밍은 프로시져(procedure)에 기반하기 때문이다. 마치 메소드(method)와 같은 프로시져는 사용자에 의해 호출되는 함수이지만 그 둘 사이에는 차이점이 없다.
프로시져는 객체 데이터를 가지고 있지 않다. 그들은 매개변수 리스트에 있는 데이터로 전달되거나 범위내에서 데이터를 사용해야 한다. 프로시져는 호출 시 이것으로 전달된 모든 데이터나 글로벌 데이터에 액세스 할 수 있다. 메소드는 오직 그들의 객체 데이터에만 액세스 해야한다. 실제로, 메소드용 함수 범위는 메소드를 포함하고 있는 객체이다.
프로시져는 글로벌 데이터를 사용하여 찾는다. 단, 절대적으로 필요할 때만 수행되어야 한다. 글로벌 데이터를 사용하는 메소드는 가능하면 빨리 다시 작성되어야 한다. 프로시져는 다른 매개변수들을 가진 다른 프로시져들을 호출한다. 메소드는 단지 몇 개의 매개변수만을 가지고 있어야 하며 다른 프로시져들 보다 자주 다른 메소드들을 호출한다.
함수 프로그래밍(FP)은 여러가지 이유로 OOP와 잘 섞이지 않는다. 가장 중요한 이유는 FP는 문제를 해결하는 데 있어서 세부적인 함수 접근방식에 기반을 두고 있고, OOP는 개념을 표시하기 위해 객체를 사용한다. FP 프로시져들은 어느 곳에서나 사용될 수 있지만, OOP 메소드는 그것을 보유하고 있는 객체내에서만 사용될 수 있다.
이제는 펄이 OOP, FP, PP 방식들을 섞는 최상의 언어인지를 설명할 수 있다.
펄에서 OOP를 절차적/함수 프로그래밍과 섞는 방법
펄은 프로그래머가 원하는 무엇이든 할 수 있도록 하기위해 길이를 한없이 늘일 수 있다. 이는 Java와 C++ 같은 언어들과는 극명하게 대조된다. 예를 들어, 펄에서 프로그래머는 이전에 선언되지 않는 변수를 자동으로 만들수 있다.
따라서 펄은 여러 방법들을 남용하기에 적합한 언어이다. 내부 객체 데이터에 액세스하고, 클래스를 변경하고, 메소드를 재 정의하는 것이 모두 가능하다. 펄의 규칙은 프로그래머들이 코딩, 디버깅, 실행 효율성에 맞게 규칙을 파괴할 수 있다. 작업을 수행하는데 도움이 된다면 괜찮다. 따라서, 펄 그 자체는 프로그래머에게 베스트 프랜드가 될 수도 최악의 적이 될 수도 있다.
규칙을 위반하는 것임에도 왜 사람들은 OOP, FP, PP를 섞기를 원하는가? 다시 근본적인 문제로 되돌아가 보자. OOP, FP, PP는 무엇인가? OOP, FP, PP는 툴이고 모든 프로그래머들이 하는 첫 번째 작업은 그들의 툴을 이해하는 것이다. 만일 프로그래머가 해시를 소팅할 때 FP Schwartzian 변형을 사용하지는 못하는데 Sort::Hashtable
을 작성할 수 있거나 또는 Sys::Hostname
모듈의 재사용을 실패했지만 대신 시스템의 호스트네임을 얻기 위해 절차 코드를 작성한다면, 그 프로그래머는 시간, 노력, 돈을 낭비한 것이 되고, 코드 품질과 신뢰성도 깎인다.
프로그래밍 팀은 최상이라고 믿고 있는 툴에 만족할 수 있다. 이것은 그들에게 일어날 수 있는 최악의 일이다. 프로그래머는 효율성을 증대시키고, 더 나은 코드를 만들 수 있으며, 팀을 더욱 혁신적으로 만들수 있는 방법이 무엇이든지 그 방법을 섞을 수 있어야 한다. 펄은 이러한 태도를 인정하고 장려한다.
OOP의 효용성
OOP의 효용성을 이 글에서 설명하기에는 너무나 많다. 또한 앞서 언급했듯이 이 주제를 다루는 책들이 많이 있다.
OOP는 기본적인 클래스와 객체에 의존하기 때문에 OO 코드를 재사용한다는 것은 필요할 때 클래스를 임포팅한다는 것을 의미한다. 코드 재사용은 OOP를 사용하기 위한 가장 중요한 이유이고, OOP가 오늘날의 산업에서 중요성과 대중성을 얻을 수 있는 이유이다.
단점도 있다. 예를 들어, 이전 문제에 대한 솔루션이 현재 상황에도 이상적으로 적용되지 않을 수도 있고 형편없이 문서화 된 라이브러리는 이해하고 사용하기가 힘들어 오히려 다시쓰는 편이 더 나을 때도 있다. 시스템 아키텍트의 임무는 이러한 단점을 보완하는 것이다.
코드 품질은 OOP를 이용하여 향상된다. 캡슐화는 데이터 오염문제를 없애기 때문이다. 상속과 다형성은 새로 작성되어야 하는 코드의 복잡성을 줄인다. 코드 품질과 프로그래밍 혁신 사이에는 미묘한 균형이 있고 각 팀은 팀의 목적에 맞게 이것을 활용해야 한다.
OOP의 상속과 재사용은 코드의 일관성있는 인터페이스를 가능하게 한다. 모든 OO 코드가 일관성있는 인터페이스를 갖추어야 한다는 의미는 아니다. 프로그래머는 일반적인 아키텍쳐를 고수해야 한다. 예를들어, 확장이 가능하고 사용하기 매우 편리한 모듈식의 인터페이스를 통해 에러 기록을 위한 포맷과 인터페이스에 팀은 합의를 해야한다. 그때야 비로소 모든 프로그래머는 에러가 많은프린트 문장 대신 인터페이스를 사용한다.
적응성(adaptability)은 프로그래밍을 할 때 다소 막연한 개념이다. 개인적으로는 이것을 환경과 사용 변화의 수락과 예견으로 정의하고 싶다. 모든 소프트웨어는 진화하기 때문에 적응성은 중요하다. 잘 작성된 소프트웨어는 발전해야 한다. OOP는 모듈식의 디자인, 향상된 코드 품질, 일관성있는 인터페이스를 이용하여 새로운 오퍼레이팅 시스템이나 새로운 리포트 포맷이 핵심 아키텍쳐에 급진적 변화를 주어야하는 필요성을 감소시킨다.
펄에서 OOP 사용하기
믿거나 말거나 지만 :) 펄에서 OOP는 초급 이나 중급 레벨이 어렵지가 않다. 고급 레벨의 사용도 복잡하지 않다. 펄은 프로그래머들에게 몇 가지 제한을 둔다.
첫 번째 단계는 펄 패키지를 이해하는 것이다. 패키지는 C++ 과 Java 라이브러리에 있는 네임스페이스(namespace) 같은 것이다. 펄 패키지는 프로그래머에게 자문역할을 한다. 기본적으로 펄은 패키지들 간에 데이터 교환을 제한하지 않는다.
Listing 1. 패키지 이름, 패키지 바꾸기, 패키지들 사이에 데이터 공유, 패키지 변수
|
파일 범위의 어휘 변수인 $extra_sound
는 세 패키지 ("main", "Pig", "Cow")액세스 할 수 있다. 이 예제에서 같은 파일 안에서 모두 정의되었기 때문이다. 일반적으로 각 패키지는 각자의 파일 안에서 정의되고 어휘 변수들은 패키지에 속하게 된다. 따라서 캡슐화가 가능하다. ( "perldoc perlmod
"를 실행해보라.)
다음에는 패키지들을 클래스에 결부시켜야 한다. 펄에서의 클래스는 단지 형식적인 패키지이다. (그와는 반대로 객체는 bless()
함수로 특별하게 만들어졌다). 펄은 OOP 규칙을 융통성있게 적용하여 프로그래머는 규칙에 제약을 받지 않는다.
new()
메소드는 클래스 컨스트럭터용 이름이다. 클래스가 객체로 인스턴스화 될 때마다 호출된다.
|
Listing 2에 있는 코드를 Barebones.pm 파일에 놓고 디렉토리에서 다음을 실행시켜서 테스트 할 수 있다:
|
예를 들어, print 문을 new()
메소드 안에 두어 $classname
변수가 무엇을 포함하고 있는지를 볼 수 있다.
Barebones->new()
대신 Barebones::new()
을 호출하면 클래스 이름은 new()
로 전달되지 않을 것이다. 다시말해서, new()
는 컨스트럭터가 아닌 평범한 함수로서 작동한다.
Y$classname
가 왜 전달되어야 하는지 궁금할 것이다. bless {}, "Barebones";
라고 부르지 않는가? 상속이라는 것 때문에 이 컨스트럭터는 Barebones
에서 상속되는 클래스에 의해 호출되지만 Barebones
라고 하지는 않는다.
모든 클래스는 멤버 데이터와 new()
와는 다른 메소드를 필요로 한다. 이를 정의하는 것은 몇 개의 프로시져를 작성하는 것 만큼 쉽다.
|
이 코드를 다음을 이용하여 테스트 할 수 있다:
|
그러면 결과 '2'를 얻는다. 컨스트럭터는 두 번 호출되고 Barebones
객체 범위가 아닌 Barebones
패키지 범위로 한정된 어휘 변수($count
)를 수정한다. 객체 데이터는 객체 그 자체로 저장되어야 한다. 메소드가 호출될 때 마다 객체에 액세스 할 수 있는 이유는 객체로의 레퍼런스는 그러한 메소드에 전달된 첫 번째 매개변수이기 때문이다.
DESTROY()
와 AUTOLOAD()
같은 특정 메소드가 있는데 이것은 특정 조건에서 펄에 의해 자동으로 호출된다. AUTOLOAD()
는 동적 메소드 이름을 허용하는데 사용되는 메소드이다. DESTROY()
는 객체 소멸자(destructor) 이지만, 정말로 필요한 경우가 아니라면 사용하지 말아야 한다. 펄에서 소멸자를 사용하는 것은 C/C++ 프로그래머로서 자신을 생각하고 있다는 의미가 된다.
상속에 대해 살펴보자. 펄에서 이것은 @ISA
변수를 변경하여 수행된다. 클래스 이름 리스트를 그 변수에 할당하면 된다. 그것으로 충분하다. @ISA
에 무엇이든 둘 수 있다. 클래스를 Satan
의 자식으로 만들 수 있다. 펄은 상관하지 않는다.
|
이것은 펄 OOP의 기본이다. 다른 많은 언어들도 연구해야 한다. 이를 주제로 다룬 책들도 많다. 참고자료 리스트도 꼼꼼히 살펴보기 바란다.
h2xs
펄 클래스를 작성하고 문서 (POD) 스켈레톤(skeleton)을 작성할 툴이 있다면 더욱 행복해질 것 같지 않은가? 펄에는 이러한 종류의 툴이 있다: h2xs
.
"-A -n Module
" 플래그는 중요하다. 기억하기 바란다. 이것을 이용하여, h2xs는 유용한 파일로 가득찬 "Module" 이라는 스켈레톤 디렉토리를 만든다:
Module.pm
, 이미 작성된 스켈레톤 문서를 가진 모듈.Module.xs
, 모듈을 C 코드로 연결하는데 사용.MANIFEST
, 패키지용 파일 리스트.test.pl
, 스크립트를 테스트하는 스켈레톤.Changes
, 모듈의 변경 기록.Makefile.PL
, makefile 생성자(generator).
이 모든 파일을 사용할 필요는 없지만 이와 같은 것이 존재하고 있다는 것을 알아두는 것이 낫다.
- 1장 (코드 가이드라인), 2장 (코드 주석), 3장 (루프 신택스), 4장 (함수 프로그래밍): 보다 나은 프로그래밍으로 가는 길 시리즈.
- Object Oriented Perl: Damian Conway (Manning Publications, 2000).
- Perldoc.com.
- comp.object Object FAQ.
- Programming Perl, Third Edition: Larry Wall, Tom Christiansen, Jon Orwant (O'Reilly & Associates, 2000).
- developerWorks "Cultured Perl" 시리즈:
- 프로그래머의 Linux 지향 설정
- Perl로 애플리케이션 구성
- Perl을 이용하여 UNIX 시스템 관리 자동화하기
- 쉬운 Perl 디버깅 (US)
- "JAPH"
- Perl의 유전자 알고리즘
- One-liners 개론
- C와 Java 프로그래머를 위한 Perl
- Perl로 Excel 파일 읽기/쓰기
- "Programming Perl" the 3rd Edition
- Perl의 데이터 저장 기술 (US)
필자소개 Teodor Zlatanov는 보스턴 대학에서 컴퓨터 공학을 전공했다. 졸업 후 Perl, Java, C, C++를 사용하여 프로그램을 개발하였다. |
'Computer_language > C++' 카테고리의 다른 글
C++ class에서 함수정의에 사용된 static [출처] C++ class에서 함수정의에 사용된 static|작성자 정천사 (0) | 2009.01.12 |
---|---|
Static Class 에 대한 정의 (0) | 2009.01.12 |
정적 중첩 클래스 (0) | 2009.01.12 |
각 상속 테스트 (0) | 2009.01.12 |
Ubuntu에서, c, c++관련 컴파일러 설치 [출처] Ubuntu에서, c, c++관련 컴파일러 설치|작성자 용이 (0) | 2009.01.12 |
친구가 인공지능, 검색, 게임등에 공통적으로 쓰일 수 있는 가장 요긴한 알고리즘을 알려달라고 부탁하더군요. 뭐 가장 처음 떠오르는 것이 바로 이 A* 알고리즘이더군요. 컴퓨터쪽 전공자 또는 게임쪽 전공자라면 누구나 한번쯤은 거쳐갔을 알고리즘이죠.
A* 알고리즘은 기본적으로 트리구조에서 많이 쓰이고 Best First Search의 방법을 쓰기 때문에 효율성도 갖추면서 코드도 깔끔히 만들 수 있는 장점이 있죠. 특히 휴리스틱(Heuristic) 접근법이라 제한된 시간과 제한된 컴퓨팅 리소스로 최적의 해답과 근접한 만족할 만한 솔루션을 찾아낼 수 있으면서도 그다지 복잡하지 않다는 것이 매력적인 점입니다.
아주 간단히 알고리즘으로 표현하면 다음과 같습니다. 알고리즘으로 표현한 것이기 때문에 구현은 어떤 언어로든 가능하죠. 교육용이니 아카데믹 한 면이라고만 생각 할수도 있지만 실제 코드로 만들어도 g,h function만 잘만들면 놀라운 성능을 보이죠.
기본적으로 Data Structure의 큐(Queue) 개념과 알고리즘을 읽고 이해할 수 있는 능력, 그리고 recursive function에 대한 거부감이 없어야 이해하기 쉬울 듯 하네요...
1. 주요 함수
(출처는 제가 대학다닐때 썼던 교재입니다 : (Russell, Stuart, and Peter Norvig. Artificial Intelligence. New Jersey: Prentice Hall, 1995)
Evaluation,기본 함수 개념
f (n) = g(n)+h(n)
g(n) = 루트부터 현재의 노드 까지의 웨이트 값
**** 거리를 찾는 소프트웨어라면 출발지부터 현재 지점까지의 자동차 미터에 찍힐 값
h(n) = 현재 노드부터 목표점까지의 최선의 선택으로 갔을때 가질 수 있는 웨이트 값.
**** 가장 핵심적인 함수로 목적에 따라자알~ 정해야 합니다.
**** 거리로 말하자면 현재 지점에서 목적지까지의 직선거리라 할 수 있겠죠. 주의할점은 절대로 실제 나올값 보다 큰 값을 가질수 있는 함수로 만들면 안됩니다. Admissible Heuristic이라고 불리우는데 예를들면 A 지점부터 B지점의 실제 도로상 주행 거리가 10km라고 할때 h(n)으로 estimate한 값이 절대로 10km보다 크면 안된다는 이야기죠. 그래서 거리를 측정하는 소프트웨어에서는 h(n)=직선거리 로 쓰는 것입니다.
Queueing-Fn : 언제나 그렇듯 병렬컴퓨팅이 아니라면 순차적으로 문제를 풀어야 겠죠. 그래서 큐에 문제를 푸는 순서를 정해 넣습니다. A*의 경우 위의 f(n)이 그 순서를 정하게 되죠. 뭐 데이터 구조까지 설명은 안하겠습니다. 데이터 구조론(Data Structure)시간에 최소한 큐는 배웠을테니깐요. 큐의 형성은 최초에 General-Search 함수에서 Make-Queue에서 형성되고 loop 안에서 각 노드마다 child노드를 f(n)에 의거하여 큐에 추가시킵니다.
알고리즘 함수
function A*-SEARCH(problem) returns a solution or failure
return BEST-FIRST-SEARCH(problem, g+h)
(Russell 97)
function BEST-FIRST-SEARCH(problem,EVAL-FN) returns a solution sequence
inputs: problem, a problem
EVAL-FN, an evaluation function
Queueing-Fn <- a function that orders nodes by EVAL-FN
return GENERAL-SEARCH(problem, Queueing-Fn)
(Russell 93)
function GENERAL-SEARCH(problem,QUEUING-FN) returns a solution, or failure
nodes<- MAKE-QUEUE(MAKE-NODE(INITIAL-STATE[problem]))
loop do
if nodes is empty then return failure
node<- REMOVE-FRONT(nodes)
if GOAL-TEST[problem] applied to STATE(node) succeeds then return node
nodes <- QUEUING-FN(nodes, EXPAND(node, OPERATORS[problem]))
end
(Russell 73)
2. 간단한 그림으로 본 예제
위의 함수들만으로는 거의 패닉상태에 빠져들 것 같아 간단히 그림으로 설명하도록 하겠습니다. 그림의 출처는 저(bgyuh)입니다.

** 각 노드는 도시들입니다.
** 붉은색 h= 에 적힌 값은 현재 도시에서 목적지까지의 직선거리입니다.
** 붉은색 g= 에 적힌 값은 출발지에서 현재 노드도시의 자동차 미터거리를 나타냅니다.
** 붉은 색 선은 도시 사이의 길이 있음을 표시합니다. 선위에는 도시사이의 실제적인 자동차 미터 거리를 나타냅니다.
** 도시 뒤의 d는 노드의 depth (깊이)입니다.
** 서치의 목적은 출발지부터 목적지까지 최단 주행거리를 가지는 경로를 찾는 것입니다.
간단히 프로그램이 서치과정에서 노드를 선택하는 과정을 보면 출발점에서는 당연히 도시 1 d 1를 선택하겠죠. Eval 함수 f(n) = g(n)+h(n)=110+300 = 410으로 2 d 1의 560보다 더 짧으니깐요. 그다음은 3가지 선택(11,12,13) 중에서 f 값 즉 총 거리의 Estimate 값이 가장 작은 도시 13 d 2를 선택 할 겁니다. 그다음은 물론 목적지라 직선거리 0인 131 도시를 선택하고 검색을 종료하겠죠.
좀더 자세히 들여다 보면
1. 루트에서 출발해서 첫번째 선택과정에서 보면 알 수 있듯이 노드 자체에 모든 값이 저장이 되어 있지는 않습니다. (즉 루트에서는 모든 브랜치 노드의 정보를 알 수 없고 바로 밑 자녀들의 정보만 알 수 있죠)
h값은 실시간으로 계산되는 estimate 값이고 단지 루트(출발도시)와 도시 1, 2간의 거리만이 실제값이 저장이 되어있죠. 만약에 모든 데이터를 저장해놓고 검색한다면 가장 정확하고 빠른 결과 (hash테이블처럼)를 얻을 수 있겠지만 경우의 수는 b^depth (예:바이너리트리의 경우 2^depth) 가 되므로 거의 무한정의 리소스가 필요합니다. g(n)은 물론 조금만 머리쓰면 쉽게 계산할 수 있죠.
정리해 보자면 각 노드에 저장된 정보는 도시의 좌표, 도시 이름, 각 인접 도시간의 도로와 그 도로로 갈 경우 인접도시간 거리 정도가 됩니다. 물론 더 고급기술로 가면 인덱싱을 이용하여 서치시간을 팍 단축시킬수도 있지만 생략하도록 하겠습니다.
2. 도시 13 d 2와 12 d 2를 비교할 경우 g값은 12 d 2가 더 작지만 h값을 더한 f값이 13 d 2가 더 작으므로 프로그램은 13 d 2를 선택하게 됩니다. 확실한 선택이었나를 따진다면 컴퓨터는 모르죠. 12 d 2의 노드를 검색 하지 않았기 때문입니다. 그러나 최적값에 가까운 값을 얻는 것이 목적이므로 그정도의 오차는 허락 할 수 있는 문제에 A* 알고리즘 사용해야 겠죠.
따라서 A* 알고리즘을 사용할 경우 Heuristic Function인 h(n)을 얼마나 잘 만드는 가에 따라 성능이 좌우됩니다.
3. f 값이 가장 작은 노드를 따라가더라도 목적지 (h=0)인 곳에 도착하지 못한다면 failure 를 return 하고 위 노드로 거슬러 올라가 다음으로 가능성이 높은 노드들을 따라 갑니다.
4. f (n) = g(n) + h(n) 값이 출발점부터 목적지까지의 자동차 미터거리를 Estimate한 값인데 노드가 진행될수록 커져야만 합니다. Admissible Heuristic 조건 때문이죠. Russell 교수님의 책에 이 조건이 왜 필요하고 A*가 왜 Optimal한가를 수학적으로 증명해 놓았는데 관심있으신분은 책을 사서 보셔도 좋을 듯 합니다.
5. 도시 132 d 3의 경우 h값이 더 커졌는데 뭔가 돌아가게 된다는 암시가 되겠죠. Make-Node Function에 아예 이런 노드를 제거해서 큐에 안들어가게 하면 그만큼 시간이 절약되겠지만 굉장히 특이한 경우 산에 가로막혀서 반드시 돌아가는 외길만 존재한다면 제거하기도 뭐하죠. 그러니 특수케이스를 만들어 실험해 봐야 합니다. 솔루션이 존재하는데 프로그램이 찾아내지 못하면 그것 또한 버그죠.
위의 검색과정은 단순하고 가장 이상적인 적용례이지만 실제 적용과정에서 고려해야만 할 점들은 훨씬 많고 다양하죠
가장 흔하게 겪을 수 있는 버그들의 예입니다.
1. 갔던 노드를 또 가게 될 경우 무한 순환의 오류에 빠질 수 있으니 꼭 위에 알고리즘에서 그 부분을 추가 해 줘야죠.
2. 만약 h값이 작은 노드들로 서치하다가 어느순간 h 값이 Parent 노드보다 커진다면 의심해 보아야 합니다. 실제로 현재노드에서 목적지까지 절벽, 바다등으로 가로막혀서 목적지까지 돌아가야만 하는 경로만이 존재하고 부모나 그위의 노드에서 다른 경로를 선택할 경우 더 짧은 경로를 찾을 수 있었다면 최적의 솔루션과 상당히 동떨어진 결과를 얻을 수도 있겠죠. 그래서 h값이 부모노드보다 커진다면 부모노드나 그 윗 노드에서 재검색을 하여 더 나은 솔루션이 있나 찾아보는 것도 나쁘지는 않을 것입니다.
이 밖에도 실제 적용을 할때에는 수많은 테스트를 케이스를 만들어 고쳐가면서 해야지 그냥 덥석 알고리즘만 코드로 만들다가는 이놈의 네이버처럼 글쓰는데 한글변환도 잘 안되던가 사진올리다 포스트 날아가는 버그들을 양산하게 되겠죠.
친구의 부탁으로 자료를 좀 만들다가 그냥 심심해서 포슷에 끄적거려 봤습니다. 몇시간만에 예제를 만든거라 혹시 오류나 오타가 있을지도 모르겠네요. 책에 있는 예제도 좋은데 특수케이스들을 설명하기 뭐해서 새로 만들어 봤습니다.
[출처] A* 알고리즘에 대한 간단한 설명|작성자 방글
- 만들면서 배우는 OS 커널의 구조와 원리(by 김범준)
: x86 하시는 분들은 많은 도움을 얻을수 있는 서적입니다. 초반에 부트로더 분석하실때는 이책을 들고 다니시면서 참고하시면 좋을듯 하네요.
- Linux Booting 과정 이해(by 백창우)
http://iamroot.org/bbs/view.php?id=x86_data&page=11&sn1=&divpage=1&sn=off&ss=on&sc=on&select_arrange=headnum&desc=asc&no=2
: 부팅 과정에 대해 정리를 잘해 놓으셨습니다. 저희도 많은 도움을 받았던 자료입니다. 역시, 초반에는 출력해서 들고다니시면 좋을듯 하네요.
- Linux i386 Boot Code HOWTO
http://tldp.org/HOWTO/Linux-i386-Boot-Code-HOWTO/
: 역시 x86 하시는분들께 도움이 되는 자료입니다. 영어라는 압박이 있지만, 전체적으로 한번 ㅎㅜㅌ어보시고, 필요할때 찾아보기에 좋은 자료 입니다.
- 리눅스 커널의 이해 - 부록 A
: 부록 A에 부팅 과정에 대해 개괄적인 내용이 있습니다. 시간나실때 한번 읽어 보시면 좋겠군요.
- 리눅스 커널 구조와 원리 - 챕터.27
: 역시 개괄적인 내용이 잘 정리되어 있습니다.
- start_kernel() 이전 부팅과정 참고자료 모음(by 지현구)
http://iamroot.org/bbs/view.php?id=x86_data&page=7&sn1=&divpage=1&sn=off&ss=on&sc=on&select_arrange=headnum&desc=asc&no=94
: 지현구님께서 부팅 프로세스 관련 자료를 정리하여 주셨습니다.
- LILO 분석 자료(by 김태훈, 남현우)
http://iamroot.org/bbs/view.php?id=x86_data&page=5&sn1=&divpage=1&sn=off&ss=on&sc=on&select_arrange=headnum&desc=asc&no=121
: 저희 팀에서 분석한 자료 입니다.
- arch/x86_64/setup.S 전반부(by 이종우)
http://iamroot.org/bbs/view.php?id=x86_data&page=5&sn1=&divpage=1&sn=off&ss=on&sc=on&select_arrange=headnum&desc=asc&no=116
: 저희 팀에서 분석한 자료 입니다.
- arch/x86_64/setup.S 후반부(by 박경태)
http://iamroot.org/bbs/view.php?id=x86_data&page=6&sn1=&divpage=1&sn=off&ss=on&sc=on&select_arrange=headnum&desc=asc&no=110
: 저희 팀에서 분석한 자료 입니다.
- arch/x86_64/setup.S 후반부(발표자료, by 김태훈)
http://iamroot.org/bbs/view.php?id=x86_data&page=5&sn1=&divpage=1&sn=off&ss=on&sc=on&select_arrange=headnum&desc=asc&no=130
: 저희 팀에서 분석한 자료 입니다.
- arch/x86_64/boot/compressed/head.S(by 선준규)
http://iamroot.org/bbs/view.php?id=x86_data&page=5&sn1=&divpage=1&sn=off&ss=on&sc=on&select_arrange=headnum&desc=asc&no=125
: 저희 팀에서 분석한 자료 입니다.
- 커널 이미지 압축 해제(by 지현구)
http://iamroot.org/bbs/view.php?id=x86_data&page=5&sn1=&divpage=1&sn=off&ss=on&sc=on&select_arrange=headnum&desc=asc&no=128
: 저희 팀에서 분석한 자료 입니다.
- arch/x86_64/kernel/head.S(by 이백, 김정수)
http://iamroot.org/bbs/view.php?id=x86_data&page=5&sn1=&divpage=1&sn=off&ss=on&sc=on&select_arrange=headnum&desc=asc&no=119
: 저희 팀에서 분석한 자료 입니다.
< 어셈블리어 관련 >
- as86 man page
: LIlO는 as86 어셈으로 작성되어 있습니다. as86자료는 man page에 가장 잘 나와있는것 같더군요. "man as86"
- Intel 메뉴얼, 2A, 2B
http://www.intel.com/design/processor/manuals/253666.pdf
http://www.intel.com/design/processor/manuals/253666.pdf
: 어셈도 일종의 API라고 생각하시면 쉽게 접근이 가능합니다. 윈도우즈 프로그래밍시에 MSDN을 참고하여 코딩을 하는것처럼, 이 문서들을 참고하시면서 instruction을 분석하시면 됩니다.
- x86 interrupt & instruction table
http://iamroot.org/bbs/view.php?id=x86_data&page=11&sn1=&divpage=1&sn=off&ss=on&sc=on&select_arrange=headnum&desc=asc&no=1
: 백창우님께서 올려주신 자료입니다. 해당 instruction과 interrupt가 무엇인지 모를때 참고하시면 됩니다. instruction에 대한 내용은 인텔문서가 자세히 나와있지만 간단히 찾아보기엔 이자료가 더 좋은듯 합니다. 여기 없는것은 인텔문서에서 찾아보시면 됩니다.
< 아키텍처 관련 >
- 만들면서 배우는 OS 커널의 구조와 원리(by 김범준)
: x86 아키텍처에 관해 정리를 잘해 놓으셨더군요.
- amd64 메뉴얼, 2권
http://www.amd.com/us-en/assets/content_type/white_papers_and_tech_docs/24593.pdf
: AMD에서 만든 메뉴얼입니다. 아키텍처 메뉴얼은 해당 칩을 만드는 회사에서 나오는 메뉴얼보다 자세한건 없습니다. 저희팀에서 몇몇 분들이 제본을 떠서 들고 다니십니다.
- Intel 메뉴얼, 3A, 3B
http://www.intel.com/design/processor/manuals/253668.pdf
http://www.intel.com/design/processor/manuals/253669.pdf
: 인텔에서 만든 x86 & x86_64 메뉴얼 입니다. 인텔 문서가 분량이 많아서 인지, 좀더 디테일하게 나와있는듯 합니다. AMD와 INTEL 두개 비교하시면서 보시면 좋을듯 하네요.
< 리눅스 커널 관련 >
- 리눅스 커널의 이해 3판
: 말이 필요없습니다. 최고권위의 커널 서적, 버전이 올라가면서 틀려진 내용도 있고, 따분한 감이 없지않지만, 이만한 리눅스 커널 책은 없는듯 합니다. 바쁘신 분들은 중간중간 관련 내용이 나오면 참고하는 형식으로 활용하시면 좋을듯 합니다.
- 리눅스 커널 심층분석(by Robert Love)
: 제가 처음으로 접한 커널 책입니다. 지금은 잘 모르겠지만, 그때 당시에는 가장 쉽게 쓰여진 커널 서적 이었습니다. 분량이 적어서 커널에 대해 전체적으로 한번 보기에 좋은듯 합니다.
- 리눅스 커널 프로그래밍(by 한동훈)
: 실습할 내용이 많은 커널 서적입니다. 아직 보질 못해서;;
- 리눅스 커널 구조와 원리
: 최근에 나온 커널 서적입니다. 그림이 많고, 전체적으로 쉽게 쓰여진것 같더군요.
- Monac
http://monac.egloos.com/
: '리눅스 커널 프로그래밍'의 저자 한동훈님의 블로그 입니다. 유일하게 한글로된 최신 스케쥴러인 CFS에 대한 내용을 담고 있더군요.
http://iamroot.org에서 퍼옴
[출처] 리눅스 커널 스터디 참고자료 |작성자 jjhbest1
'Computer_language > Linux' 카테고리의 다른 글
우분투에서 사용하는 명령어 (0) | 2009.01.16 |
---|---|
Ubuntu 8.04 LTS sources.list (0) | 2009.01.16 |
초보자를 위한 리눅스 커널의 메모리 관리 (0) | 2009.01.12 |
GNU Make: 재컴파일을 지휘하는 프로그램(A Program for Directing Recompilation) (0) | 2009.01.12 |
MAKE를 이용한 프로젝트 관리 (Managing Projects with make) (0) | 2009.01.12 |
저자: 한동훈(traxacun)
이 글은 리눅스 커널을 처음 공부하는 분들에게만 적합하며, 이미 잘 알고 계시는 분들에게는 적합하지 않을 수도 있습니다. 본 기사에서는 다음 주제들을 다룰 것입니다.
1. Memory Model
2. i386 CPU에서의 메모리 관리
3. 리눅서 커널에서의 메모리 관리
4. 커널에서의 코드
메모리 모델
예를 들어서, 1M 메모리를 가진 시스템이 있습니다. 이 시스템에서 메모리를 300k 사용하는 프로그램 A가 있고, 500k를 사용하는 프로그램 B가 있습니다. 그런데, 프로그램 A는 데이터가 많아질수록 더 많은 메모리를 사용하게 되어 있어서 600k의 메모리를 사용하게 되었습니다. 이런 경우에는 메모리가 프로그램 A와 B가 서로 충돌하게 되어 더 이상 프로그램을 실행할 수 없게 될 것입니다. 프로그램을 작성하면서 계산기 프로그램이 내 프로그램의 메모리 영역을 침범하면 안되는데라고 고민하면서 프로그램을 작성하지는 않을 것입니다. 이와 같이 다양한 프로그램들이 메모리를 사용할 수 있게 하기 위해 OS는 메모리를 관리합니다.
http://network.hanbitbook.co.kr/view.php?bi_id=1113
메모리 모델의 종류
메모리를 관리하는 방법은 세그먼트(Segment) 기법과 페이징(Paging) 기법이 있습니다. 우리가 흔히 보는 책은 페이지를 매기는 방법이 두 가지가 있습니다. 예를 들어, 1,000 페이지짜리 책이 있을 때 페이지를 1번부터 1,000번까지 모두 매겨놓은 책이 있는가하면, 책을 챕터별로 나누어서 각 챕터에서 몇번째 페이지라고 표기하는 방법이 있습니다. 선형적으로 일괄되게 페이지를 매기는 방법을 페이징이라 하고, 각 챕터별로 책을 나누고, 챕터에서 몇번째 페이지(Offset)라고 나누어 관리하는 방법을 세그먼트라고 합니다.
현대 운영체제는 정확하게 세그먼트와 페이징으로 나누어 관리하기 보다는 이 두가지를 적절하게 혼합된 형태를 사용합니다.
여기까지는 메모리를 관리하는 방법을 이론적으로 나눈 것이고, CPU에서 메모리를 관리하는 것은 다릅니다. 즉, OS에서 메모리를 관리하는 방법은 OS를 제작하는 사람이 마음대로 정할 수 있는 것이지만 실제로 CPU와 데이터를 주고 받을 때는 CPU에 맞춰서 데이터를 주고 받아야 합니다. 우리가 사용하는 x86 CPU는 리얼 모드(Real Mode)와 보호 모드(Protected Mode)를 사용합니다. 리얼 모드는 예전에 도스(DOS)를 사용하던 시절에 사용하던 모드로 1M 까지의 메모리를 사용할 수 있습니다. 보호 모드에서는 메모리를 0-4G까지 사용할 수 있습니다. 요즘에는 모두들 512M 이상의 램을 장착해서 사용하는 것이 보편적이니 리얼 모드는 몰라도 되지 않아라고 생각할 겁니다. 그러나, 시스템이 처음 전원이 들어가고, 부팅이 될 때는 리얼 모드로 실행되고, 그 이후에 보호 모드로 넘어가게 됩니다. 그렇게 때문에 커널을 학습하는 사람들은 CPU에서 메모리를 관리하는 방법이 리얼 모드일때와 보호 모드일 때 다르다는 것을 알아야 합니다. 이에 대해서는 뒤에서 보다 자세히 설명하도록 하겠습니다.
커널의 메모리 모델
리눅스 커널이 생각하는 메모리 모델은 크게 두 가지 뿐입니다. 하나는 물리 메모리(Physical Memory)이고, 다른 하나는 가상 메모리(Virtual Memory)입니다.
많은 분들이 들어보았을 이야기는 프로세스 하나당 4GB까지의 메모리 공간을 가진다는 것입니다. 즉, 스타크래프트도 4GB의 메모리 공간을 사용한다고 생각하고, 인터넷 익스플로러도 4GB의 메모리 공간을 사용한다고 생각합니다. 잠깐만요! 저는 PC에 램이 512M 밖에 없는데요? 라고 생각할 수 있습니다.
잠시 생각하면 알 수 있는 것처럼 모든 프로그램은 4G라는 메모리를 모두 사용하는 것이 아닙니다. 실제로는 매우 작은 일부만 사용할 뿐입니다. 그러니 사용하지 않는 부분은 무시하고, 사용하는 부분만 메모리에 갖고 있으면 됩니다.
프로세스는 4G의 공간이 전부 자기것이라고 생각하고, OS는 프로세스가 실제로 사용하는 부분만 실제 메모리에 올려놓으면 됩니다. 즉, 가상 공간과 실제 메모리 공간을 연결할 수 있는 변환 테이블이 하나 있으면 되겠네요!
그림1. 프로세스 A의 가상 공간과 실제 메모리
그림에서 볼 수 있는 것처럼 프로세스 A는 페이지 1, 4, 7번을 사용하고 있습니다. 그리고 이들 각각은 실제 메모리 프레임 5, 3, 1에 저장되어 있습니다. 이와 같은 방법을 사용하기 때문에 각 프로세스는 각자가 4G의 공간을 사용하고 있다고 생각하고, 다른 프로그램이 사용하는 메모리 공간에 대해 염려하지 않고 프로그램을 작성할 수 있는 것입니다.
x86 아키텍처의 메모리 모델
CPU는 연산을 위해 메모리와 데이터를 주고 받습니다. 즉, CPU가 메모리를 어떻게 이용하는지 알고 있어야합니다. CPU마다 메모리를 이용하는 방법은 각기 다르지만, 여기서는 가장 흔하게 사용되는 x86 CPU에 대해서 살펴볼 것입니다.
그림2. x86 CPU의 메모리 모델
x86 CPU는 32bit 환경이라고 얘기합니다. 즉, 메모리도 32bit에 해당하는 2의 32승 = 4G까지 사용할 수 있습니다. 각 페이지를 4096 바이트로 나누어서 관리하고 있습니다. 즉, 2^12 = 4096입니다. 따라서, 12 비트는 각 페이지의 위치를 가리키기 위해서 사용됩니다.
4G의 공간을 4096 페이지 크기로 나누면 1,048,576이고, 이 숫자의 의미는 4G의 메모리 공간을 4k 크기의 페이지로 나누어 관리하기 위해서는 페이지 테이블이 1,048,576개나 필요하다는 의미가 됩니다. 즉, 2^20 = 1,048,576이고, 1개의 PTE는 4 바이트이기 때문에 페이지 테이블이 차지하는 메모리의 크기는 1,048,576 * 4 = 4M가 됩니다. 즉, 하나의 프로세스가 4G의 메모리 공간을 관리하는 페이지 테이블을 유지하기 위해서는 4M가 필요하다는 것입니다. 프로세스 1개 생성에 4M를 무조건 사용한다는 것은 꽤나 큰 낭비입니다.
20 bit table인 경우: 2^20 = 1,048,576 = 1M
1M * 2^12(4096) = 4G
PTE = 4 bytes, 1M * 4 = 4M each process
그렇다면, 20비트를 한꺼번에 이용하는 대신에 10비트씩 나누어서 사용하면 어떨까요?
2의 10승은 1024개이고, 한 항목은 4 바이트를 차지하므로 1024 * 4 = 4096 = 4k를 사용하게 되고, 4k는 메모리에서 1 프레임만 차지합니다.
그래서 그림2와 같이 페이지 디렉토리에 10비트, 페이지 테이블에 10비트를 사용합니다.
Page Directory = 1024개 * 4 bytes = 4k = 1 page
Page Table = 1024개 * 4 bytes = 4k = 1 page
간단하게 C언어의 정의대로 적어보면 다음과 같습니다.
unsinged int table[1024*1024];
unsigned int directory[1024], table[1024];
첫번째는 20비트를 사용했을 때의 배열 선언이고, 두번째는 10비트씩 나누어서 디렉토리, 테이블로 사용할 때의 선언입니다. 물론, 리눅스 커널의 선언이 이렇게 되어 있다는 것은 아닙니다.
첫번째 배열 선언이 차지하는 크기를 생각해보면 4M이고, 두번째 배열 선언이 차지하는 크기는 8k입니다.
0xC1234567이라는 논리 주소가 주어졌을 때 실제로 CPU에서 어떻게 실제 메모리를 찾아가는지 살펴보겠습니다.
그림3. 논리 주소
0xC1234567이라는 16 진수를 2진수로 풀어서 쓰면 11000001001000110100010101100111이며, 이를 각각 10, 10, 12 비트씩 끊으면 위 그림과 같다.
그림2에 나온 것처럼 CPU에는 CR3 레지스터가 있으며, 이 레지스터는 메모리 관리를 위한 페이지의 출발지 정보를 갖고 있다. 따라서 CR3 레지스터의 값을 읽어서 페이지 디렉토리가 시작하는 위치를 알아내고, 상위 10비트
2256위치에서 읽어들인 값이 0x40000이고, 오프셋의 값이 1383이면 실제 메모리 주소의 위치는 0x41383이 된다.
리눅스 커널의 메모리 모델
리눅스 커널은 64비트 선형 주소를 사용한다. 커널에서 64비트 주소를 사용하는 이유는 Alpha CPU와 같이 64비트 주소를 사용하는 시스템을 지원하기 위해서이다.
그림4. 리눅스 커널의 메모리 모델
리눅서 커널도 x86 CPU와 마찬가지로 메모리 관리의 효율성을 위해 페이지 디렉토리를 글로벌 디렉토리와 미들 디렉토리로 나누어서 관리한다. 즉, x86 CPU에서 2단계 페이징을 사용한다면 커널에서는 3단계 페이징을 사용하는 것이 차이점이다.
글로벌 디렉토리는 pgd_t, 미들 디렉토리는 pmd_t, 페이지 테이블은 pte_t로 나타내며, 오프셋은 상대위치이기 때문에 따로 나타낼 필요는 없다.
x86에서는 2단계를 사용하고, 리눅스 커널에서는 3단계를 사용한다면 커널은 x86 환경에서는 어떻게 해야할까? x86 CPU를 지원하기 위해 별도로 2단계 페이징을 만들어야 할까? 라고 생각할 수 있는데 실제로 커널은 위 구조를 그대로 유지하면서 2단계 페이징을 지원하는 방법을 택했다. 즉, 미들 디렉터리를 1개만 사용하는 것이다. 이렇게 하면 커널의 코드를 크게 변경하지 않으면서 64비트 환경과 32비트 환경을 쉽게 지원할 수 있다.
각 시스템마다 메모리 관리를 위해 사용하는 비트수는 다르기 때문에 위 그림에서 몇 비트씩 사용하는지 명시하지 않았다. 물론, 특정 플랫폼마다의 비트수를 적는다면 적을 수 있지만 여기서는 그렇게 하지 않았다.
x86 CPU와 커널에서 메모리를 찾는 방법을 보면 먼저 CR3 레지스터에서 글로벌 디렉터리의 시작 주소를 알아내고, 글로벌 디렉토리에서 몇번째 위치인지 알아낸다고 했다. 여기에 쓰이는 함수가 pgd_offset()이다. 마찬가지로 미들 디렉토리에서의 위치를 알아내는 것은 pmd_offset, 페이지 테이블에서의 위치를 알아내는 것은 pte_offset이다. pgd_offset, pmd_offset, pte_offset은 모두 2개의 인자를 갖는다.
pgd_offset(mm, address)인데 mm은 메모리 관리를 위한 구조로 각 프로세스마다 1개씩 갖고 있다. 즉, 페이지 글로벌 디렉토리의 시작 위치가 되며, address는 페이지 글로벌 디렉터리에서 몇 번째 위치라는 것을 나타낸다. 마찬가지로 pmd_offset의 첫번째 인자는 페이지 미들 디렉토리의 시작위치를, 두번째 인자는 페이지 미들디렉토리의 몇번째 페이지를 인자로 받는다. pte_offset도 동일하다. Redhat 9에 포함된 커널 2.4.20-8 버전에서는 pte_offset 대신에 pte_offset_kernel을 사용하면 된다. 상위 버전의 커널에서는 pte_offset으로 이용할 수 있다.
이들 함수(정확히는 매크로)를 이용하면 프로세스가 실제로 이용하고 있는 물리 메모리를 알아낼 수 있다. 이들 매크로는 arch/asm-i386/pgtable.h, pgtable-3level.h에서 찾아볼 수 있다. 참고로 실제로 존재하지 않는 페이지 글로벌 디렉토리, 미들 디렉토리 등을 액세스하려하면 중대한 커널 오류가 발생할 수 있다. 따라서, 페이지를 액세스하기 전에 각각 pgd_present, pmd_present, pte_present를 사용해서 실제 페이지가 있는지 확인하고 사용해야 한다.
프로세스에서 바라본 메모리
프로세스마다 4G의 가상 공간을 사용한다고 얘기했다. 그리고, 프로세스마다 메모리 관리를 하기 위해 페이지 글로벌 디렉토리, 페이지 테이블과 같은 구조를 갖고 있다고 했다. 프로세스를 나타내는 구조체는 task_struct이며, 여기에는 메모리 구조를 나타내는 mm_struct mm이 있다. mm은 메모리 관리를 위한 구조체이며, pgd_t* pgd는 페이지 글로벌 디렉토리의 시작 위치를 가리킨다. 즉, pgd가 가리키는 값과 CR3 레지스터가 가리키는 값이 같다. 그 이후부터는 앞에서 설명한 것처럼 각 페이지별로 주소를 찾아서 실제 메모리상의 프레임을 찾아간다.
그림5. 프로세스에서 바라본 메모리
이를 자세하게 표현하면 그림6과 같습니다. 각각의 프로세스는 task_struct 구조체로 표현되며, 이를 간단히 PCB(Process Context Block, 컨텍스트 문맥)이라고 이야기한다. 커널에는 프로세스 ID로 해당 PCB를 찾아내는 함수가 있는데 이 함수가 find_task_by_pid(pid)이다. 이 함수는 프로세스 ID, PID를 인자로 넘겨 받으면 task_struct에 대한 것을 반환해준다.
물론, 현재 프로세스는 current로 접근할 수 있지만, 다른 프로세스에 대한 task_struct를 얻어오려면 find_task_by_pid를 사용해야 한다. task_struct에는 mm이 있고, 이 mm이 mm_sturct 자료구조를 가리킨다.
그림1을 생각해보면 프로세스는 연속적인 가상 메모리를 할당 받는 것도 아니라 여러 개의 가상 메모리 블록을 할당받는다. 즉, 연속적으로 할당 받을 수도 있고, 따로따로 할당 받을 수도 있다는 얘기다. 이를 위해서 mm_struct에 보면 각각의 가상 메모리 블록을 관리하는 vm_area_struct 구조체가 있다. 그리고 vm_area_struct에는 각각 vm_start와 vm_end가 있는데 이는 가상 메모리 공간에서의 시작 위치와 마지막 위치를 가리킨다. 또한 vm_area_sturct* vm_next는 자신과 같은 형태의 구조체를 다시 가리키고 있는데, 이는 다른 가상 메모리 블록을 가리킨다. 즉, 단일 연결 리스트(Singly Linked List)로 연결되어 있다. vm_next를 따라 프로세스가 사용하는 전체 가상 메모리 공간을 알아낼 수 있다. 만약, vm_next가 NULL인지 아닌지를 알아내면 가상 메모리 블록의 끝을 알아낼 수 있다.
그림6. 프로세스에서 바라본 메모리 관리
마지막으로 task_struct에는 mm_struct로 선언된 변수가 두 가지가 있다. 하나는 mm이고, 다른 하나는 active_mm이다. mm은 프로세스가 사용하는 메모리 공간을 나타내고, active_mm은 CPU에 의해 현재 제어중인 주소공간을 가리킨다. 프로세스 A에게 있어서 mm과 active_mm은 같다. 즉, A->mm == A->active_mm이다. 그러나, 커널 스레드에서는 mm은 NULL이며, 커널 스레드가 사용중인 메모리는 active_mm으로 표현된다.
Linux 커널의 아버지인 Linus는 메모리 공간을 실주소공간(Real Address Spaces)과 익명주소공간(Anonymous Address Spaces)로 나눈다. 사용자 프로세스가 사용하는 주소 공간을 실주소공간아리하고, 커널 스레드가 사용하는 공간을 익명주소공간이라 하는 것이다.
모든 커널 스레드는 익명주소공간을 사용하며, 사용자 프로세스가 사용하는 실주소공간에 대해서는 관여하지 않는다.
foo라는 프로세스 A를 실행한다고 하자. 이 경우 A->mm == A->active_mm = foo일 것이다. 이때 커널 스레드 B에 의해 선점당한 경우 B->mm = NULL이지만, A->active_mm = foo를 여전히 가리키게 된다. 즉, 커널 스레드는 실주소공간을 신경쓰지 않지만, 작업 A가 다시 로드되었을 때 mm을 다시 로드하지 않기 위해 active_mm을 사용하는 것이다.
스왑
실제 메모리 사용량보다 더 큰 메모리가 필요한 프로그램을 실행할 수 있는 것은 가상 메모리 덕분이다. 앞에서는 가상 메모리에 대해서 살펴보았는데, 사용중이지 않은 부분은 스왑으로 저장해서 메모리에 여유공간을 확보하는 것이 스왑의 역할이다.
앞의 그림5에서 살펴본 것처럼 스왑을 한다는 것은 실제 메모리의 프레임을 디스크로 저장하는 것을 의미한다. 실제 메모리 프레임을 찾기 위해서는 페이지 테이블의 위치를 알아야하고, 페이지 테이블의 위치를 알기 위해서는 페이지 미들 디렉토리의 위치를 알아야한다. 마찬가지로, 페이지 미들 디렉토리의 위치를 알기 위해서는 페이지 글로벌 디렉토리의 위치를 알아야 한다. 이들 프로세스는 가상 메모리로 관리되고 있으니 가상 메모리 구조를 찾아봐야하고, 다시 실제 메모리 공간도 찾아봐야 한다. 즉, swap_out 함수는 이러한 순서들을 찾아가며 실제 메모리 프레임을 찾아가는 역할을 하고, 메모리 프레임을 디스크에 저장하는 것은 try_to_swap_out 함수의 역할이다.
그림7. 스왑의 흐름
kswapd 함수는 daemonize 함수를 호출해서 자신을 데몬으로 등록시키고, 무한 루프를 돌면서 kwapd_can_sleep 함수를 호출한다. 이 함수는 지금 스왑 작업을 해도 되는지 아닌지를 판별하며, 이를 판별하기 위해서는 다시 kswapd_can_sleep_pgdat를 호출해서 페이지 글로벌 디렉토리에서 스왑이 필요한 페이지가 있는지 없는지를 판별한다. 스왑이 필요한 페이지가 하나라도 있으면 kswapd_balance를 호출한다. kwapd_balance는 메모리의 균형을 맞춰준다. 마찬가지로 이 함수도 kswapd_balance_pgdat를 호출하여 페이지 글로벌 디렉토리를 확인한다. try_to_free_pages 함수에서 try_to_free_pages_zone을 호출해서 사용할 수 있는 페이지 영역을 찾아보고, 사용할 수 있는 페이지가 부족한 경우에는 필요한 페이지를 확보하기 위해 shrink_caches를 호출한다. shrink_caches는 캐시들을 돌아다니며 비울 수 있는 캐시인지 판별하기 위해 shrink_cache 함수를 사용한다.
전체적인 흐름은 이렇게 되어 있으며, 각 프로세스에서 사용중인 스왑 공간은 task_struct의 swap_address로 알 수 있다.
스왑 정책
커널의 스왑 정책에서 사용할 수 있는 방법은 여러가지가 있지만, 리눅스 커널에서는 LRU(Least Recently Used) 정책을 사용한다고 알려져 있다. LRU는 가장 적게 사용된 페이지를 스왑으로 대체시키는 것이다.
메모리 페이지를 LRU 정책에 따라 스왑시키려면 해당 메모리 페이지가 액세스 된 적이 있는지 알아낼 수 있는 방법이 있어야 한다. 각 페이지가 액세스 된 시간을 기록해서, ‘아, 이 페이지는 1시간 전에 액세스했고, 이 페이지는 5분전에 액세스했네’라는 사실을 이용할 수도 있을 것이다. 단, 이렇게 한다면 액세스 될 때마다 시간을 기록하고, 각 페이지의 시간을 기록하고 유지하는 것만으로도 굉장히 높은 작업부하가 걸릴 것이라고 예상할 수 있을 것이다. 이런 스왑 정책은 OS가 단독으로 하기엔 어려운 부분이다. CPU에서는 이를 위해 액세스 비트(Access Bit)를 제공한다. 이 비트는 이 페이지가 접근된 적이 없으면 0, 있으면 1로 설정된다. 그림8에서 A가 액세스 비트를 나타낸다. 액세스 비트는 페이지에 접근할 때 CPU에서 자동으로 1로 설정하지만, CPU가 이를 다시 0으로 설정할 수는 없다. 0으로 설정할 수 있는 것은 오직 커널 뿐이다.
그림8. 페이지 엔트리
U/S는 사용자 프로세스가 접근할 수 있으면 1이고, 커널만 접근할 수 있으면 0으로 설정된다. R/W는 0이면 읽기만 가능하고, 1이면 읽기/쓰기가 모두 가능하다. P는 Present Bit라는 것으로 페이지가 메모리상에 존재하는지를 나타낸다. 즉, 페이지가 디스크로 스왑 되었으면 0이 된다. 프로세스가 이 페이지를 접근하려하면 P 비트가 0이기 때문에 페이지 폴트 인터럽트 #14가 발생하고, 디스크에서 다시 메모리로 이 페이지를 읽어들이고, P 비트를 1로 설정하게 된다.
메모리 관리와 관련된 부분은 OS가 독단으로 결정할 수 있는 것이 아니며, CPU와 OS가 서로 조화를 이루어가며 관리하는 부분이다.
코드로 보는 리눅스
8086 시스템은 과거에 20개의 어드레스 핀을 가진 16비트 시스템이었다. 즉, 2^20 = 1M까지 사용할 수 있는 시스템이었다. 이 의미는 1M 이상의 메모리를 사용할 수 없다는 것이다. 1M + 1번째를 접근하려 하면 1번째로 접근하게 된다. 이는 마치 프로그래밍 언어에서 만나는 정수 오버플로우와 같다. 1M + 1 = 1로 만들려면 어떻게 하면 될까? 2^20은 16진수로 0x100000이고, 2진수로는 100000000000000000000이다. 1M를 넘어가는 비트가 공교롭게도 가장 맨 앞의 비트이다. 즉, 맨 앞의 1을 0으로 만들어 주기만 하면 된다. 11111111111111111111 + 1을 하면 100000000000000000000이 되어야 하는데 20번째 비트를 0으로 만들면 결과 값은 0이 된다. +2를 하면 결과값은 1이 될 것이다.
이를 위해 8086 시스템에서는 20번째 핀을 키보드 인터럽트 핸들러인 8042와 AND 게이트로 연결해 놓았다. 20번째 핀이 켜지지 않으면 사용자는 항상 저 주소를 이용할 수 없다.
펜티엄 4에서 DOS용 응용 프로그램을 실행할 수 있다는 의미는 하위 호환성이 좋다는 의미이기도 하지만, 위와 같은 단점도 고스란히 물려받았다는 의미가 된다.
요즘과 같이 512M 램을 사용하는데, 저걸 알아서 뭐해요? 라고 되물을수도 있다. 그러나 A20을 켜주지 않으면 20번째 비트가 항상 0이 되기 때문에 1M = 0이 되고, 3M = 0이 된다. 즉, 1M에 해당하는 메모리 주소를 CPU가 액세스하려하면 0번째를 가리키게 된다. 사용자는 1, 3, 5, 7…과 같이 홀수번째 메모리를 전혀 사용할 수 없게 된다. 그렇게 때문에 부팅 과정에서 CPU가 리얼 모드에서 보호 모드로 넘어가기전에 반드시 A20 게이트를 켜야한다.
리눅스 커널의 소스 코드를 보면 그런 부분이 있다. movb $0xDF, %al이 있고, 옆에는 주석으로 A20 게이트를 켠다고 되어 있다. 이 부분의 값은 몰라도 된다. 저것은 CPU 매뉴얼에 있는 특정한 명령어인 것이다.
여기서는 lgdt gdt_48을 볼 수 있다. lgdt 명령어는 전체 커널 소스에서 단 한번만 사용된다. CPU는 리얼 모드에서 보호 모드로 넘어가기전에 글로벌 디스크립터(Global Descriptor)를 작성해야 한다. 즉, 4G에 해당하는 메모리를 어떻게 사용하겠는가를 설계하는 명세서와 같은 역할을 한다. 코드 세그먼트 디스크립터, 데이터 세그먼트 디스크립터, 비디오 세그먼트 디스크립터등을 지정할 수 있다. gdt_48은 명령어가 아니라 이러한 디스크립터를 적어놓은 곳으로 C언어의 struct와 같은 것이다.
리얼 모드에서 보호 모드로 넘어가는 순서는 1. 디스크립터를 정의한다. 2. lgdt 명령어로 디스크립터를 로드하고 보호모드로 전환한다. 이다.
CPU에는 명령어를 실행하는 부분이 3가지로 나누어져 있다. 첫번째는 명령어를 읽어들이는 부분, 두번째는 명령어를 해석하는 부분, 세번째는 명령어를 실행하는 부분이다.
즉, lgdt 명령어를 실행하게 되는 순간에 읽기 유닛, 해석 유닛에 이미 2개의 명령어가 들어가 있다. 보호모드로 전환된 다음에 리얼 모드에서 들어가 있던 명령어가 실행되면 안되니까 최소한 읽기, 해석 유닛의 명령을 무시하기 위해 두 스텝을 쉬어줄 필요가 있다. 따라서, lgdt 명령어를 실행한 다음에 어셈블리로 nop(No Operation: 아무일도 하지마!)를 2번 실행해주는 것이 관례다. 그런데, 커널 소스에선 nop 대신에 call delay를 사용하는 것 같다고 나름대로 추측할 수 있었다. nop 명령어를 커널에선 사용하지 않는다.
다음으로 첫번째 줄을 보면 lidt 명령이 있다. 이것은 인터럽트 디스크립터를 로드한다. 즉, Devide By Zero(인터럽터 0번)이라든가, 페이지 폴트(인터럽터 14번) 같은 인터럽트가 발생했을 때 이를 처리할 루틴의 위치를 지정하는 부분이 idt_48이고, lidt는 이를 메모리에 로드해주는 것이다.
굳이 이렇게 GDT를 꺼내든 이유는 <<리눅스 커널의 이해>>의 2장 메모리 관리 부분의 처음 1/2 정도가 전부 이 GDT 구조를 설명하는데 할애되어 있다고 느꼈기 때문이다. 책만 펼치면 잠이 쏟아질 만큼 졸린데, 이는 어셈블리와 CPU 구조에 대한 이해조차 없이는 책을 이해하기 어렵기 때문이라 생각했다. 즉, 내공이 부족한 내가 보기엔 어려운 책이다.
이들 어셈블리 코드는 arch/i386/boot에 있으며, 비디오 디스크립터는 video.S에 정의되어 있다.
위 코드처럼 0xC0000000가 3G 영역을 가리킨다. 커널이 3G위의 영역을 사용한다는데 실제로 그 값이 있는지 확인해 본 부분이다.
여기서는 앞에 CR4 레지스터가 보인다. CPU에는 CR0-CR4까지의 레지스터가 있다. 이중에서 CR3는 페이지 글로벌 디렉토리의 위치를 가리키며, CR4 레지스터는 PAE 확장을 사용할 것인가를 설정한다. 펜티엄 프로 이후에는 어드레스 핀이 4개가 더 추가되어서 4G가 아니라 64G까지 사용할 수 있게 해준다. 따라서, 이 값이 설정된 경우에 CR4 비트를 1로 설정해서 PAE 확장을 사용하게 설정해 주는 부분이다.
movl %eax, %cr3에서 페이지 테이블이 시작하는 위치를 CR3 레지스터에 저장하고 있다.(GAS, GNU Assembler는 AT&T 스타일을 따르고 있어서 어셈블리 명령어 인자 위치가 서로 반대다. mov a, b는 B의 값을 A에 넣는다이지만 AT&T 스타일에서는 A의 값을 B에 넣는다가 된다)
마치며
아직은 커널을 잘 알지 못하고 커널을 공부하는 입장에서 준비된 글입니다. 국내에 나와있는 다양한 커널 책들을 많이 참고했습니다. 아직 다루지 못한 부분들이 많습니다. 커널 소스에 대한 세세한 설명보다는 커널의 전체적인 흐름을 다루는 것이 세미나에는 더 적합하다고 판단해서 전체적인 흐름을 다루는데 중점을 두었습니다. kmalloc과 vmalloc의 차이점을 다루는 것 보다는 전체적인 흐름이 더 중요하다고 생각했습니다. 흐름에 대한 이해를 바탕으로 소스 코드를 살펴보는 것이 이해에 더 도움이 된다고 생각합니다.
레퍼런스
각각의 책마다 같은 부분을 보아도 설명이나 보여주는 부분이 다릅니다. 결국, 저자가 커널을 바라보는 방식에 대해 생각해보는 기회도 되고, 책을 가이드삼아 커널을 직접 찾아보며 전체를 바라볼 수 있는 안목을 기르는 것은 자신의 몫이라 생각됩니다. 주로 리눅스 커널 프로그래밍을 많이 참고했으며, 리눅스 커널 심층 분석은 커널 API 이해에 많은 도움이 되었습니다. Operating System Concepts는 운영체제가 시스템을 관리하는 다양한 방법과 알고리즘에 대한 해설이 중심이고 이를 토대로 리눅스 커널이 채택한 방법을 살펴보는 데 좋은 참고가 됩니다. 리눅스 커널의 어셈블리 코드 부분은 만들면서 배우는 OS 커널의 구조와 원리에서 많은 부분을 참조했습니다.
• 리눅스 커널의 이해, 2판, 한빛미디어
• 만들면서 배우는 OS 커널의 구조와 원리, 한빛미디어
• 리눅스 커널 프로그래밍, 교학사
• 리눅스 매니아를 위한 커널 프로그래밍, 교학사
• 리눅스 커널 심층 분석, 에이콘
• 리눅스 커널 분석 2.4, 가메
• Operating System Cencepts, 6판, 홍릉
[출처] 초보자를 위한 리눅스 커널의 메모리 관리 |작성자 jabusunin
'Computer_language > Linux' 카테고리의 다른 글
Ubuntu 8.04 LTS sources.list (0) | 2009.01.16 |
---|---|
리눅스 커널 스터디 참고자료 (0) | 2009.01.12 |
GNU Make: 재컴파일을 지휘하는 프로그램(A Program for Directing Recompilation) (0) | 2009.01.12 |
MAKE를 이용한 프로젝트 관리 (Managing Projects with make) (0) | 2009.01.12 |
우분투 사용 총집합서 (0) | 2009.01.12 |