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 을 의미합니다.

  1. 프롬프트 상일 경우.
    SHLVL 값은 1 (shell process 가 실행상태 이므로), BASH_SUBSHELL 값은 0 이 됩니다.

  2. ( ) subshell 메타문자를 이용해서 실행
    BASH_SUBSHELL 값이 올라갑니다.

  3. shell script 를 작성해서 실행
    이경우는 child process 가 생성되고 SHLVL 값이 올라갑니다.

  4. 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 helloread 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 에 반영이 되지 않습니다.