난이도 : 초급
George
Cross, 선임 소프트웨어 개발자, Business Objects Americas
옮긴이: 박재호 이해영 dwkorea@kr.ibm.com
2008 년 9 월 16 일
IBM® AIX®에서 공유 라이브러리 메커니즘과 메모리 크기에 대해 배워봅시다. 이 기사는 서버 코드를 작성하는 개발자와
AIX 시스템을 실제 운영하는 관리자에게 필요한 지식을 핵심만 간추려 설명합니다. 이 기사는 개발자와 관리자에게 명령어와 기법을 설명하고
AIX에서 서버 프로세스의 메모리 요구 사항을 분석하는 데 필요한 지식을 제공합니다. 이 기사는 또한 개발자와 관리자가 ps나 topas와 같은
표준 실시간 분석 도구로 파악하기 어려운 자원 부족을 회피하는 방법을 설명합니다. 이 기사는 시스템 관리자와 AIX용 응용 프로그램 개발자를
대상으로 작성했습니다.
소개
이 기사는 다음 명령어를 시연하면서 32비트 AIX 5L™(5.3)에서 공유 라이브러리가 메모리를 차지하는 방법을 살펴본다.
- ps
- svmon
- slibclean
- procldd
- procmap
- genkld
- genld
이 기사는 커널 공유 라이브러리 세그먼트는 물론이고 프로세스의 가상 메모리 공간을 설명하며, 가상 메모리를 살펴보는 방법, 위에서 언급한
다양한 진단 도구가 제공하는 출력 결과 해석 방법도 다룬다. 이 기사는 또한 커널 공유 세그먼트가 꽉 찬 상황을 진단하며 이런 상황을 해결하기
위해 가능한 접근 방법도 설명한다.
이 기사에서 사용하는 예제로 소프트웨어 제품인 Business Objects Enterprise Xir2®에서 따온 프로세스를 사용한다.
이는 임의로 든 예며, 여기서 소개하는 개념은 AIX 5L에서 동작하는 모든 프로세스에 적용할 수 있다.
검토
이제 무엇을 할지 공감했으니, 32비트 아키텍처를 조금 검토해보자. 검토 과정에서 아주 유용한 'bc' 명령행 계산기를 사용하겠다.
32비트 프로세서에서 레지스터는 2^32개의 가능한 값을 담을 수 있다.
$ bc
2^32
4294967296
obase=16
2^32
100000000
|
이 범위는 4기가바이트다. 이는 시스템에서 동작하는 프로그램이 0에서 2^32 - 1 범위 내에서 함수나 자료 주소에 접근할 수 있음을
의미한다.
$ bc
2^32 - 1
FFFFFFFF
obase=10
2^32 - 1
4294967295
|
이미 알고 있듯이, 운영체제는 잠재적으로 수백 개에 이르는 프로그램을 동시에 돌릴 수 있다. 응용 프로그램 각각이 4GB 메모리 범위에
접근이 가능할지라도, 개별 프로그램마다 물리 RAM을 4GB만큼 할당 받는다는 뜻은 아니다. 이렇게 하기란 비현실적이다. 그 대신 운영체제는
적당한 물리 RAM과 스왑(또는 페이징) 영역으로 지정된 파일 시스템 사이에서 코드와 자료를 스와핑하는 복잡한 정책을 구현했다. 또한 각
프로세서가 4GB라는 메모리 영역에 접근이 가능할지라도, 대다수 프로세서는 이 영역을 완전히 사용하지 않는다. 따라서 운영체제는 특정
프로세스마다 요구하는 코드와 자료를 올리고 스왑하기만 하면 된다. 그림 1. 가상 메모리를 개념으로
설명하는 도식
이런 방법은 종종 가상 메모리나 가상 주소 공간으로 부른다.
실행 파일이 동작할 때, 운영체제에 들어있는 가상 메모리 관리자는 파일을 구성하는 코드와 자료를 살펴서 어느 부분을 램으로 올리고 어느
부분을 스왑으로 올리고 어느 부분을 파일 시스템에서 참조할지 결정한다. 동시에, 운영체제는 몇몇 구조체를 만들어 4GB 범위 내에서 물리 영역을
가상 영역으로 사상한다. 이 4GB 범위는 (종종 VMM 구조와 함께) 프로세스의 이론적인 최대 범위를 표현하며, 프로세스의 가상 주소 공간으로
알려져 있다.
AIX에서 4GB 가상 공간은 256메가바이트짜리 세그먼트 16개로 나뉜다. 세그먼트에는 미리 정해진 기능이 있다. 몇 가지를
정리해보았다.
- 세그먼트 0: 커널 관련 자료
- 세그먼트 1: 코드
- 세그먼트 2: 스택과 동적 메모리 할당
- 세그먼트 3: 사상된 파일을 위한 메모리, mmap으로 설정한 메모리
- 세그먼트 d: 공유 라이브러리 코드
- 세그먼트 f: 공유 라이브러리 자료
반면에 HP-UX®에서 주소 공간은 4분면으로 나뉜다. 3사분면과 4사분면은 +q3p enable과 +q4p enable 옵션을 켜서
chatr 명령을 내릴 경우 공유 라이브러리 사상 목적으로 사용이 가능하다.
공유 라이브러리가 메모리에 올라오는 위치
당연한 이야기지만, 공유 라이브러리는 공유할 목적으로 만들어졌다. 좀 더 구체적으로 말하자면, 코드("텍스트"로 알려진)와 읽기 전용
자료(상수 자료, 기록 시점에서 복사 가능한 자료)를 포함한 이진 파일 이미지에서 읽기 전용 영역을 물리적인 메모리에 올리고 나면 이를 요구하는
프로세스에 여러 번 사상할 수 있다.
이를 확인하기 위해, AIX가 동작하는 기계를 구해 현재 메모리에 올라온 공유 라이브러리를 살펴보자.
> su
# genkld
Text address Size File
d1539fe0 1a011 /usr/lib/libcurses.a[shr.o]
d122f100 36732 /usr/lib/libptools.a[shr.o]
d1266080 297de /usr/lib/libtrace.a[shr.o]
d020c000 5f43 /usr/lib/nls/loc/iconv/ISO8859-1_UCS-2
d7545000 161ff /usr/java14/jre/bin/libnet.a
d7531000 135e2 /usr/java14/jre/bin/libzip.a
.... [ lots more libs ] ....
d1297108 3a99 /opt/rational/clearcase/shlib/libatriastats_svr.a
[atriastats_svr-shr.o]
d1bfa100 2bcdf /opt/rational/clearcase/shlib/libatriacm.a[atriacm-shr.o]
d1bbf100 2cf3c /opt/rational/clearcase/shlib/libatriaadm.a[atriaadm-shr.o]
.... [ lots more libs ] ....
d01ca0f8 17b6 /usr/lib/libpthreads_compat.a[shr.o]
d10ff000 30b78 /usr/lib/libpthreads.a[shr.o]
d00f0100 1fd2f /usr/lib/libC.a[shr.o]
d01293e0 25570 /usr/lib/libC.a[shrcore.o]
d01108a0 18448 /usr/lib/libC.a[ansicore_32.o]
.... [ lots more libs ] ....
d04a2100 fdb4b /usr/lib/libX11.a[shr4.o]
d0049000 365c4 /usr/lib/libpthreads.a[shr_xpg5.o]
d0045000 3c52 /usr/lib/libpthreads.a[shr_comm.o]
d05bb100 5058 /usr/lib/libIM.a[shr.o]
d05a7100 139c1 /usr/lib/libiconv.a[shr4.o]
d0094100 114a2 /usr/lib/libcfg.a[shr.o]
d0081100 125ea /usr/lib/libodm.a[shr.o]
d00800f8 846 /usr/lib/libcrypt.a[shr.o]
d022d660 25152d /usr/lib/libc.a[shr.o]
|
관찰 결과에 따르면, 현재 Clearcase와 자바(Java™)가 동작하고 있다. 여기서
libpthreads.a 라는 공통 라이브러리 중 하나를 찍어보자. 라이브러리를 탐색해서 구현 함수 내역을 살핀다.
# dump -Tv /usr/lib/libpthreads.a | grep EXP
[278] 0x00002808 .data EXP RW SECdef [noIMid] pthread_attr_default
[279] 0x00002a68 .data EXP RW SECdef [noIMid]
pthread_mutexattr_default
[280] 0x00002fcc .data EXP DS SECdef [noIMid] pthread_create
[281] 0x0000308c .data EXP DS SECdef [noIMid] pthread_cond_init
[282] 0x000030a4 .data EXP DS SECdef [noIMid] pthread_cond_destroy
[283] 0x000030b0 .data EXP DS SECdef [noIMid] pthread_cond_wait
[284] 0x000030bc .data EXP DS SECdef [noIMid] pthread_cond_broadcast
[285] 0x000030c8 .data EXP DS SECdef [noIMid] pthread_cond_signal
[286] 0x000030d4 .data EXP DS SECdef [noIMid] pthread_setcancelstate
[287] 0x000030e0 .data EXP DS SECdef [noIMid] pthread_join
.... [ lots more stuff ] ....
|
음, 흥미롭다. 이제 시스템에서 현재 메모리에 올라와 있는 동작 중인 프로세스를 살펴보자.
# for i in $(ps -o pid -e | grep ^[0-9] ) ; do j=$(procldd $i | grep libpthreads.a); \
if [ -n "$j" ] ; then ps -p $i -o comm | grep -v COMMAND; fi ; done
portmap
rpc.statd
automountd
rpc.mountd
rpc.ttdbserver
dtexec
dtlogin
radiusd
radiusd
radiusd
dtexec
dtterm
procldd : no such process : 24622
dtterm
xmwlm
dtwm
dtterm
dtgreet
dtexec
ttsession
dtterm
dtexec
rdesktop
procldd : no such process : 34176
java
dtsession
dtterm
dtexec
dtexec
|
멋지다! 이제 똑같은 작업을 하되, 중복을 없애보자.
# cat prev.command.out.txt | sort | uniq
automountd
dtexec
dtgreet
dtlogin
dtsession
dtterm
dtwm
java
portmap
radiusd
rdesktop
rpc.mountd
rpc.statd
rpc.ttdbserver
ttsession
xmwlm
|
현재 동작 중이면서 libpthreads.a 를 메모리에 올린 이진 파일 목록을 깔끔하게 분리해서 정리해보자. 이
시점에서 시스템에 더 많은 프로세스가 떠 있음에 주의하자.
이제 각 프로세스가 libpthreads.a 를 어디에 올렸는지 살펴보자.
# ps -e | grep java
34648 - 4:13 java
#
# procmap 34648 | grep libpthreads.a
d0049000 217K read/exec /usr/lib/libpthreads.a[shr_xpg5.o]
f03e6000 16K read/write /usr/lib/libpthreads.a[shr_xpg5.o]
d0045000 15K read/exec /usr/lib/libpthreads.a[shr_comm.o]
f03a3000 265K read/write /usr/lib/libpthreads.a[shr_comm.o]
#
# ps -e | grep automountd
15222 - 1:00 automountd
25844 - 0:00 automountd
#
# procmap 15222 | grep libpthreads.a
d0049000 217K read/exec /usr/lib/libpthreads.a[shr_xpg5.o]
f03e6000 16K read/write /usr/lib/libpthreads.a[shr_xpg5.o]
d0045000 15K read/exec /usr/lib/libpthreads.a[shr_comm.o]
f03a3000 265K read/write /usr/lib/libpthreads.a[shr_comm.o]
d10ff000 194K read/exec /usr/lib/libpthreads.a[shr.o]
f0154000 20K read/write /usr/lib/libpthreads.a[shr.o]
#
# ps -e | grep portmap
12696 - 0:06 portmap
34446 - 0:00 portmap
#
# procmap 12696 | grep libpthreads.a
d0045000 15K read/exec /usr/lib/libpthreads.a[shr_comm.o]
f03a3000 265K read/write /usr/lib/libpthreads.a[shr_comm.o]
d10ff000 194K read/exec /usr/lib/libpthreads.a[shr.o]
f0154000 20K read/write /usr/lib/libpthreads.a[shr.o]
#
# ps -e | grep dtlogin
6208 - 0:00 dtlogin
6478 - 2:07 dtlogin
20428 - 0:00 dtlogin
#
# procmap 20428 | grep libpthreads.a
d0045000 15K read/exec /usr/lib/libpthreads.a[shr_comm.o]
f03a3000 265K read/write /usr/lib/libpthreads.a[shr_comm.o]
d0049000 217K read/exec /usr/lib/libpthreads.a[shr_xpg5.o]
f03e6000 16K read/write /usr/lib/libpthreads.a[shr_xpg5.o]
|
각 프로세스는 libpthreads.a를 매번 동일 주소에 올린다는 사실에 주목하자. 라이브러리를 구성하는 목록에 현혹되지 말자.
AIX에서는 동적 공유 라이브러리(보통 .so 파일)는 물론이고 아카이브 라이브러리(보통 .a 파일)도 공유할 수 있다. 이런 공유 기능은
전통적인 링크와 마찬가지로 링크 시점에서 심볼을 결정하지만 최종 이진 파일로 구성 목적 파일(아카이브에서 .o 파일) 복사가 필요하지 않다.
그렇기 때문에 동적 공유 라이브러리(.so/.sl 파일)와는 달리 동적(실행 중) 심볼 결정을 수행하지 않는다.
또한 read/exec로 표시된 libpthreads.a 코드 영역은 세그먼트 0xd에 올라왔다는 사실에 주목하자.
이 세그먼트는 앞서 언급한 바와 같이 공유 라이브러리를 위한 세그먼트로 AIX에서 지정되어 있다. 다시 말해 커널은 공유 라이브러리의 공유
가능한 세그먼트를 동일한 커널에서 동작 중인 모든 프로세스가 공유하는 영역에 올린다.
자료 섹션 역시 동일한 세그먼트(공유 라이브러리 세그먼트 0xf)에 위치한다는 사실을 눈치챘을지도 모르겠다. 하지만 이는 각 프로세스가
libpthreads.a 의 자료 섹션까지 공유함을 의미하지는 않는다. 조금 느슨하게 정의해 보자면, 이런 배치는 동작하지
않는다. 각 프로세스 별로 다른 이름으로 다른 자료 값을 유지할 필요가 있기 때문이다. 물론 가상 메모리 주소는 동일할지 몰라도 세그먼트
0xf는 libpthreads.a 를 사용하는 각 프로세스마다 다르다.
svmon 명령어는 프로세스에 대한 Vsid(가상 메모리 관리자에서 세그먼트 ID)를 보여준다. 공유 라이브러리 코드 세그먼트는 Vsid가
같지만, 공유 라이브러리 자료 세그먼트는 Vsid가 제각각이다. 유효 세그먼트 ID인 Esid는 프로세스의 주소 공간 범위 내에서 세그먼트
ID를 의미한다(그냥 용어 설명이므로 혼동하지 말기 바란다).
# svmon -P 17314
-------------------------------------------------------------------------------
Pid Command Inuse Pin Pgsp Virtual 64-bit Mthrd 16MB
17314 dtexec 20245 9479 12 20292 N N N
Vsid Esid Type Description PSize Inuse Pin Pgsp Virtual
0 0 work kernel segment s 14361 9477 0 14361
6c01b d work shared library text s 5739 0 9 5786
19be6 f work shared library data s 83 0 1 87
21068 2 work process private s 56 2 2 58
18726 1 pers code,/dev/hd2:65814 s 5 0 - -
40c1 - pers /dev/hd4:2 s 1 0 - -
#
# svmon -P 20428
-------------------------------------------------------------------------------
Pid Command Inuse Pin Pgsp Virtual 64-bit Mthrd 16MB
20428 dtlogin 20248 9479 23 20278 N N N
Vsid Esid Type Description PSize Inuse Pin Pgsp Virtual
0 0 work kernel segment s 14361 9477 0 14361
6c01b d work shared library text s 5735 0 9 5782
7869e 2 work process private s 84 2 10 94
parent=786be
590b6 f work shared library data s 37 0 4 41
parent=7531d
6c19b 1 pers code,/dev/hd2:65670 s 29 0 - -
381ae - pers /dev/hd9var:4157 s 1 0 - -
40c1 - pers /dev/hd4:2 s 1 0 - -
4c1b3 - pers /dev/hd9var:4158 s 0 0 - -
|
|
|
산수 놀이
공유 세그먼트 0xd에서 얼마나 많은 메모리를 차지하는지 살펴보자. 다시 한번 bc 계산기를 써보자. 정신 바짝 차리고, 세그먼트 0xd
크기를 비교해보자.
# bc
ibase=16
E0000000-D0000000
268435456
ibase=A
268435456/(1024^2)
256
|
여기까지는 좋아 보인다. 위에서 언급한 내용처럼 각 세그먼트는 256MB다. 좋다. 이제 현재 사용 중인 메모리 용량을 살펴보자.
$ echo "ibase=16; $(genkld | egrep ^\ \{8\} | awk '{print $2}' | tr '[a-f]' '[A-F]' \
| tr '\n' '+' ) 0" | bc
39798104
$
$ bc <<EOF
> 39798104/(1024^2)
> EOF
37
|
현재 사용 중인 메모리는 37MB라고 알려준다. XIr2를 시작한 다음에 비교해보자.
$ echo "ibase=16; $(genkld | egrep ^\ \{8\} | awk '{print $2}' | tr '[a-f]' '[A-F]' \
| tr '\n' '+' ) 0" | bc
266069692
$
$ bc <<EOF
> 266069692/(1024^2)
> EOF
253
|
이제 253MB를 사용 중이다. 이는 256MB 한계에 아주 근접한 값이다. WIReportServer와 같은 프로세스를 임의로 골라 공유
영역으로 얼마나 많은 공유 라이브러리를 밀어넣었으며 얼마나 많은 라이브러리를 내부적으로 사상했는지 살펴보자. 공유 세그먼트 시작 주소가
0xd000000라는 사실을 알고 있으므로, procmap 결과에서 필터링해보자. 단지 코드 섹션만 세그먼트 0xd에 사상된다는 사실을
기억하자. 따라서 read/exec 행만 살펴보면 된다.
$ procmap 35620 | grep read/exec | grep -v ^d
10000000 10907K read/exec boe_fcprocd
31ad3000 14511K read/exec
/crystal/sj1xir2a/xir2_r/bobje/enterprise115/aix_rs6000/libEnterpriseFramework.so
3167b000 3133K read/exec
/crystal/sj1xir2a/xir2_r/bobje/enterprise115/aix_rs6000/libcpi18nloc.so
3146c000 1848K read/exec
/crystal/sj1xir2a/xir2_r/bobje/enterprise115/aix_rs6000/libBOCP_1252.so
31345000 226K read/exec
/crystal/sj1xir2a/xir2_r/bobje/enterprise115/aix_rs6000/btlat300.so
|
위에 나타난 네 가지 라이브러리는 공유 세그먼트로 사상될 수 없는 듯이 보인다. 필연적으로 네 가지 라이브러리는 mmap() 루틴을 호출해
할당한 범용 메모리로 쓰이는 내부 세그먼트 0x3에 사상되었다.
공유 라이브러리를 32비트 AIX에서 내부적으로 강제로 사상하기 위해서는 몇 가지 조건이 필요하다.
- (위에서 발생한 상황처럼) 공유 세그먼트 0xd 영역이 꽉 차 있다.
- 그룹과 다른 사람에 대한 실행 권한이 공유 라이브러리에 없다. 이런 문제를 해결하려면 접근 허가를 rwxr-xr-x로 지정하면 된다.
하지만 개발자들은 자신에게만 접근 허가를 주기를 원하므로(예: rwx------), 테스트 목적으로 공유 라이브러리를 컴파일해 배포할 때마다
sibclean을 돌릴 필요가 없다.
- 몇몇 문서는 nfs 위에서 공유 라이브러리를 메모리에 올리면 이렇게 된다고 말한다.
AIX 커널은 동일한 라이브러리라도 다른 위치에서 시작했다면 공유 메모리에 두 번 올릴 것이다.
sj2e652a-chloe:~/e652_r>genkld | grep libcplib.so
d5180000 678c6 /space2/home/sj2e652a/e652_r/lib/libcplib.so
d1cf5000 678c6 /home/sj1e652a/xir2_r/lib/libcplib.so
|
뭔가 잘못되었을 때
다른 디렉터리에 설치된 XIr2 인스턴스를 다시 한번 돌린다면, 프로세스 메모리 크기에 상당한 차이가 난다.
$ ps -e -o pid,vsz,user,comm | grep WIReportServer
28166 58980 jbrown WIReportServer
46968 152408 sj1xir2a WIReportServer
48276 152716 sj1xir2a WIReportServer
49800 152788 sj1xir2a WIReportServer
50832 152708 sj1xir2a WIReportServer
|
'jbrown' 계정에서 돌리는 인스턴스가 첫 번째로 시작했으며, 'sj1xir2a' 계정에서 돌리는 인스턴스가 두 번째로 시작했다. 두
번째 인스턴스를 돌리기 앞서 bobje/setup/env.sh 파일에서 적절한 위치에 다음과 같은 항목을 설정해 뭔가 조금 이상한 작업을
했다면
LIBPATH=~jbrown/vanpgaix40/bobje/enterprise115/aix_rs6000:$LIBPATH
|
메모리 사용량이 정규화된 상태를 확인할 것이다(이 LIBPATH 테스트에서는 WIReportServer를 시동할 수 없기에 프로세스
boe_fcprocd를 사용했다).
$ ps -e -o pid,vsz,user,comm | grep boe_fcprocd
29432 65036 jbrown boe_fcprocd
35910 67596 jbrown boe_fcprocd
39326 82488 sj1xir2a boe_fcprocd
53470 64964 sj1xir2a boe_fcprocd
|
그리고 기대한 바와 같이 procmap은 ~jbrown에서 올라온 파일을 보여준다.
53470 : /crystal/sj1xir2a/xir2_r/bobje/enterprise115/aix_rs6000/boe_fcprocd
-name vanpg
10000000 10907K read/exec boe_fcprocd
3000079c 1399K read/write boe_fcprocd
d42c9000 1098K read/exec
/home7/jbrown/vanpgaix40/bobje/enterprise115/aix_rs6000/libcrypto.so
33e34160 167K read/write
/home7/jbrown/vanpgaix40/bobje/enterprise115/aix_rs6000/libcrypto.so
33acc000 3133K read/exec
/home7/jbrown/vanpgaix40/bobje/enterprise115/aix_rs6000/libcpi18nloc.so
33ddc697 349K read/write
/home7/jbrown/vanpgaix40/bobje/enterprise115/aix_rs6000/libcpi18nloc.so
|
정리
응용 프로그램이 종료되었다면, 공유 라이브러리는 여전히 공유 세그먼트 0xd에 남아있을지도 모른다. 이런 경우에는 'slibclean'
유틸리티를 사용해 더 이상 참조하지 않는 공유 라이브러리를 메모리에서 내린다. 이 유틸리티에는 인수가 필요없다.
또한 -l 옵션을 추가하면 procmap과 비슷한 결과를 보여주는 genld라는 유틸리티는 현재 시스템에 존재하는 모든 프로세스를
보여준다.
종종 slibclean을 돌린 다음에도 공유 라이브러리 복사가 여전히 불가능할 경우가 있다. 예를 들면 다음과 같다.
$ cp /build/dev/bin/release/libc3_calc.so /runtime/app/lib/
cp: /runtime/app/lib/libc3_calc.so: Text file busy
|
이미 slibclean을 돌렸기 때문에 'genld -l'은 이 라이브러리가 메모리에 올라온 프로세스를 보여주지 않는다. 하지만 시스템은
여전히 이 파일을 보호하고 있다. 이런 문제점을 극복하려면 우선 목표 위치에 있는 공유 라이브러리를 지운 다음에 새로운 공유 라이브러리를
복사하면 된다.
$ rm /runtime/app/lib/libc3_calc.so
$ cp /build/dev/bin/release/libc3_calc.so /runtime/app/lib/
|
공유 라이브러리 개발 과정 동안, 컴파일, 링크, 실행, 예제 실행을 반복한다면 단지 소유주(r_xr__r__)만 실행 가능한 공유
라이브러리를 만드는 방법으로 매 주기마다 slibclean 명령을 내리지 않아도 된다. 이렇게 하면 테스트 목적으로 사용하는 프로세스를 메모리에
올려 공유 라이브러리를 내부적으로 사상할 것이다. 하지만 궁극적으로는 모든 사람이 실행 가능하도록 주의해야 한다(즉, 제품 배포 시점에서
r_xr_xr_x이 되어야 한다).
요약
공유 라이브러리가 메모리를 차지하는 방법과 이를 검사하기 위해 사용된 유틸리티에 대한 방법을 자세히 살펴봤으리라 믿는다. 이 기사를 통해,
응용 프로그램이 요구하는 메모리 크기 조건을 평가하고 AIX 시스템에서 돌아가는 프로세스에 대한 메모리 사용량 구성 요소를 분석할 수 있을
것이다. |