Subshells
Shell 에서 명령을 실행하면 새로운 프로세스가 생성되어 실행됩니다.
이때 명령을 호출한 process 가 parent 가 되고 새로 실행되는 명령이 child process 가 됩니다.
다음은 프롬프트 상에서 /bin/sleep
외부 명령을 실행한 예인데 현재 bash shell process 아래서
sleep 명령의 child process 가 실행되는 것을 볼 수 있습니다.
이번에는 AA.sh 라는 shell script 를 만들어 실행시킨 예인데 스크립트 실행을 위해 bash child process 가 하나 더 생성되고 그 아래서 sleep 명령이 실행되는 것을 볼 수 있습니다.
이번에는 프롬프트에서 아래와 같은 4 종류의 명령을 실행한 결과입니다. 그런데 위에서처럼 shell script 를 실행한 것도 아닌데 bash child process 가 하나 더 생긴 후에 그 아래서 sleep 명령이 실행되는 것을 볼 수 있습니다. 이렇게 ( )
$( )
` `
|
&
를 이용하여 명령을 실행시킬 때 생성되는 shell 을 subshell 이라고 합니다.
$ ( sleep 10; echo ) # 1. ( ) , $( )
$ `sleep 10; echo` # 2. ` ` backtick 명령치환
$ echo | { sleep 10; echo ;} # 3. | 파이프
$ command & # 4. background 로 실행되는 명령
Child process 가 parent process 로부터 물려받는 것들
- 현재 디렉토리
- export 된 환경 변수, 함수
- 현재 설정되어 있는 file descriptor 들 ( stdin(0), stdout(1), stderr(2) ... )
- ignore 된 신호 ( trap '' INT )
테스트를 위해 터미널에서 실행한 명령
$ echo "pid : $$, PPID : $PPID"
pid : 10875, PPID : 1499 # 현재 shell pid 와 ppid
$ pwd # 현재 working 디렉토리
/home/mug896/tmp
$ var1=100 var2=200
$ export var1 # var1 만 export
$ f1() { echo "I am exported function" ;}
$ f2() { echo "I am not exported function" ;}
$ export -f f1 # f1 만 export
$ trap '' SIGINT # INT 신호 ignore
$ trap 'rm -f /tmp/tmpfile' SIGTERM
$ tty
/dev/pts/13
$ exec 3> /dev/null # FD 3 생성
child process 생성을 위한 test.sh 파일 내용과 실행 결과
---------- test.sh ----------
#!/bin/bash
echo "pid : $$, PPID : $PPID"
pwd
echo "var1 : $var1"
echo "var2 : $var2"
f1
f2
trap
ls -l /proc/$$/fd
-----------------------------
$ ./test.sh
pid : 11717, PPID : 10875 # ppid 는 parent shell pid
/home/mug896/tmp
var1 : 100
var2 : # export 안된 변수
I am exported function
./test.sh: line 6: f2: command not found # export 안된 함수
trap -- '' SIGINT # ignore 된 신호만 표시됨
total 0
lrwx------ 1 mug896 mug896 64 Aug 8 16:07 0 -> /dev/pts/13 # FD 0,1,2 는 동일
lrwx------ 1 mug896 mug896 64 Aug 8 16:07 1 -> /dev/pts/13
lrwx------ 1 mug896 mug896 64 Aug 8 16:07 2 -> /dev/pts/13
l-wx------ 1 mug896 mug896 64 Aug 8 16:07 3 -> /dev/null # parent 에서 open 했던 FD
Subshell 이 추가해서 물려받는 것들
- export 안된 현재 shell 에서 사용중인 변수, 함수들
- 현재 pid 를 나타내는
$$
변수값 - parent pid 를 나타내는
$PPID
변수값 - trap handler 설정 ( trap 'rm -f tmpfile' INT )
다시 말해서 subshell 은 현재 shell 환경과 동일하다고 생각하면 됩니다. subshell 에서 테스트를 실행한 결과는 현재 shell 에서 실행한 것과 차이가 없습니다.
$ ( echo "pid : $$, PPID : $PPID"
> pwd
> echo "var1 : $var1"
> echo "var2 : $var2"
> f1
> f2
> trap
> ls -l /proc/$BASHPID/fd )
pid : 10875, PPID : 1499 # pid, ppid 가 현재 shell 과 동일하게 나온다
/home/mug896/tmp
var1 : 100
var2 : 200
I am exported function
I am not exported function
trap -- '' SIGINT
trap -- 'rm -f /tmp/tmpfile' SIGTERM
total 0
lrwx------ 1 mug896 mug896 64 08.08.2015 15:20 0 -> /dev/pts/13
lrwx------ 1 mug896 mug896 64 08.08.2015 15:20 1 -> /dev/pts/13
lrwx------ 1 mug896 mug896 64 08.08.2015 15:20 2 -> /dev/pts/13
l-wx------ 1 mug896 mug896 64 08.08.2015 15:20 3 -> /dev/null
Parent process 와 child process 의 관계
- parent 에서 물려받은 값들은 child 에서 마음대로 읽고 쓸 수 있으나 변경한 값이 parent 에 적용되지는 않습니다. 또한 child 에서 shell 환경 변수를 변경하거나 옵션 설정을 변경해도 parent 에는 영향을 미치지 않습니다.
- parent 에서 특정 값을 변경해도 그것이 이미 실행 중인 child 에 반영되지 않습니다.
- child 에서 export 할 경우 child 의 child 에게 적용이 되지 parent 나 다른 shell 에는 적용되지 않습니다.
Subshell 의 특징
parent process 에서 설정한 변수나 함수는 export 해야지 child process 에서 사용할 수 있습니다. 하지만 subshell 에서는 export 하지 않아도 사용할 수가 있습니다.
$ AA=100
$ ( echo AA value = "$AA" ) # 변수 AA 를 export 하지 않아도 값을 사용할 수 있다.
AA value = 100
현재 shell 에서 사용중인 변수는 subshell 에서 읽고, 쓰고 할 수 있습니다. 그러나 변경된 값이 현재 shell 에 적용되지는 않으므로 주의해야 합니다.
$ AA=100
$ ( AA=200; echo AA value = "$AA" ) # subshell 에서 값을 200 으로 변경.
AA value = 200
$ echo "$AA" # 하지만 현재 shell 에서는 변함이없다.
100
subshell 을 생성하여 사용한 변수나 shell 환경 설정 변경은 subshell 이 종료되면 사라집니다.
$ echo -n "$IFS" | od -a
0000000 sp ht nl
# subshell 에서 IFS 값을 ':' 로 변경하여 사용함.
$ ( IFS=:; echo -n "$IFS" | od -a )
0000000 :
# subshell 이 종료된 후에는 기존의 IFS 값으로 복귀 되었다.
$ echo -n "$IFS" | od -a
0000000 sp ht nl
##################################################
$ test -o pipefail; echo $?
1
# subshell 에서 쉘옵션을 변경하여 사용.
$ ( set -o pipefail; test -o pipefail; echo $? )
0
# subshell 이 종료후엔 이전상태로 복귀되었다.
$ test -o pipefail; echo $?
1
##################################################
$ set -- 11 22 33
$ echo "$@"
11 22 33
# positional parameters 를 subshell 에서 설정하여 사용
$ ( set -- 44 55 66; echo "$@" )
44 55 66
# subshell 종료후 기존 값으로 복귀
$ echo "$@"
11 22 33
##################################################
$ ulimit -c
0
# core file 생성을 위해 subshell 을 이용해 일시적으로 ulimit 값을 설정
$ ( ulimit -c unlimited; ulimit -c ; ... )
unlimited
# subshell 종료후 'ulimit -c' 값이 0 으로 복귀되었다
$ ulimit -c
0
#################################################
$ echo $LANG
en_US.UTF-8
# LANG 환경변수 값을 일시적으로 subshell 이하 프로세스에 적용
$ ( export LANG=ko_KR.UTF-8
join -j 1 -a 1 <(sort file1) <(sort file2) )
$ echo $LANG
en_US.UTF-8
subshell 에서 cd
한것은 종료후 이전 상태로 돌아옵니다.
$ pwd
/home/mug896/tmp
# subshell 을 이용하여 cd 사용
$ ( cd ~/tmp2; pwd; ... )
/home/mug896/tmp2
# subshell 종료후 이전 상태로 복귀
$ pwd
/home/mug896/tmp
subshell 은 프로세스 이므로 exit
명령을 사용하여 종료합니다.
$ ( echo hello; exit 3; echo world )
hello
$ echo $?
3
디버깅을 위해 -E | set -o errtrace
or -T | set -o functrace
옵션을 이용해 trace 할때도 subshell 까지만 됩니다. child process 는 trace 되지 않습니다.
$$ 와 $BASHPID
$$
변수는 현재 shell pid 를 나타내는 변수인데 subshell 에서도 동일한 값을 가집니다.
하지만 subshell 도 엄연히 프로세스 이므로 pid 값을 가지는데 $BASHPID
변수를 통해서 구할 수 있습니다.
$ echo $$ $BASHPID
2881 2881
$ ( echo $$ $BASHPID )
2881 4609
$SHLVL 과 $BASH_SUBSHELL
SHLVL 은 child process 를, BASH_SUBSHELL 은 subshell 을 의미합니다.
프롬프트 상일 경우.
SHLVL 값은 1 (shell process 가 실행상태 이므로), BASH_SUBSHELL 값은 0 이 됩니다.( )
subshell 메타문자를 이용해서 실행
BASH_SUBSHELL 값이 올라갑니다.shell script 를 작성해서 실행
이경우는 child process 가 생성되고 SHLVL 값이 올라갑니다.bash -c 'command ...' [ arg1 arg2 ... ]
이때도 child process 가 생성되고 SHLVL 값이 올라갑니다.
Subshell 주의할점
다음은 shell script 작성시 subshell 로인해 범하기 쉬운 오류들에 대한 예제입니다.
예제 .1
#!/bin/bash
index=30
change_index() {
index=40
echo "changed to 40"
}
result=$(change_index)
echo $result
echo $index
########### output ###########
changed to 40
30
결과값으로 "changed to 40", 40 을 기대하였으나 "changed to 40", 30 이 나왔습니다.change_index
함수가 ( )
안에서 실행됐으므로 subshell 에서 실행되어 parent 변수인 index 값을 갱신할 수 없습니다.
예제 .2
$ echo hello | read var; echo $var
$
$ echo hello | { read var; echo $var ;}
$ hello
echo hello
와 read var
는 |
에 의해 연결돼 있으므로 각각 subshell 에서 실행이 되고 종료후에 echo $var
가 현재 shell 에서 실행되므로 subshell 에서 설정한 변수값을 echo $var
에서 읽을 수 없습니다. 두번째와 같이 { ;}
를 이용한 명령 group 을 사용하면 read var; 와 echo $var
두 명령이 함께 subshell 에서 실행되어 원하는 값을 얻을 수 있습니다.
예제 .3
#!/bin/bash
nKeys=0
cat datafile | while read -r line
do
#...do stuff
nKeys=$((nKeys + 1))
done
echo Finished writing $nKeys keys
########### output ###########
Finished writing 0 keys
결과값으로 while 문으로 인해 증가한 nkeys 값을 기대하였으나 마지막 문장의 $nkeys 값은 0 이 표시 되었습니다. while 문이 파이프 |
로 인해 subshell 에서 실행되어 parent 변수인 nkeys 값을 변경할 수 없습니다. 다음과 같이 수정합니다.
#!/bin/bash
nKeys=0
while read -r line
do
#...do stuff
nKeys=$((nKeys + 1))
done < datafile
echo Finished writing $nKeys keys
########### output ###########
Finished writing 10 keys
Subshell Optimization
위에서 살펴본 바와 같이 subshell 에서 builtin 명령이 아닌 외부 명령이 실행될 때는 subshell 프로세스 아래 또 해당 명령의 프로세스가 생성되어 실행됩니다. 이것은 subshell 에서 실행될 명령이 하나만 존재하거나 아니면 명령 리스트 중에서 마지막에 위치한 명령을 실행할 때는 필요가 없는 것입니다. 별책부록의 subshell 부분을 보면 subshell 에서 실행되는 3 개의 date 명령 중에서 마지막 명령은 따로 fork 을 하지 않고 바로 exec 하여 실행하는 것이 여기에 해당됩니다. 일종의 optimizing 으로 위에서 subshell 을 설명할 때 뒤에 echo 명령을 붙인 이유이기도 합니다. 이것은 특정 명령을 실행할 때 문제가 될 수 있는데 다음과 같은 방법으로 해결할 수 있습니다.
# PGID 가 같아서 timeout 에의해 strace 명령이 종료될 때 vi 도 함께 종료된다.
$ timeout 10 strace -f -p 1234 |& vi -
# { ;} 를 이용하면 subshell 아래서 프로세스를 생성하여 실행되므로 정상적으로 실행.
$ { timeout 10 strace -f -p 1234 ;} |& vi -
정리하면
현재 shell 과 subshell, child process 가 가지는 관계는 프로그래밍 언어에서 사용하는 closure 와 비슷합니다. subshell 생성시에 parent 환경을 그대로 갖게 되지만 subshell 안에서 별도로 존재한다고 할 수 있습니다. 그래서 subshell 내에서 값을 변경해도 그 값이 parent 에 적용이 되지 않고 또한 subshell 생성 후에 parent 에서 특정 값을 변경해도 그 값이 subshell 에 반영이 되지 않습니다.