Job Control

터미널에서 단순히 한번에 하나의 명령만 실행시킬 수 있는 것이 아니라 & 메타문자를 이용해 background job 을 생성함으로써 멀티태스킹을 할 수 있습니다. 가령 인터넷에서 파일을 다운로드하는 job 을 background 로 실행시켜놓고 동시에 vi 에디터로 파일 수정 작업을 할 수 있습니다.

Job id 와 Job specification

$ curl -sO http://cdimage.ubuntu.com/.../ubuntu-mate-15.10-desktop-amd64.iso &
[3] 26558

명령을 & 메타문자를 이용해 background 로 실행시키면 결과로 job id 와 process id 를 보여줍니다. 위의 예에서 [3] 부분이 job id 에 해당되고 26558 은 process id (pid) 에 해당됩니다.

만약에 kill 명령을 사용해 job 에 신호를 보낼때 job id 를 사용한다면 pid 와 구분할 수 없게 됩니다 ( 둘 다 숫자이므로 ). 그래서 job id 대신 job specification ( 줄여서 jobspec ) 을 사용하는데 이때 jobspec 은 job id 앞에 % 문자를 붙여서 만듭니다. ( 예: %3 )

이 jobspec 은 job control 에 사용되는 명령들 ( jobs, bg, fg, wait, disown, kill ) 에서 사용됩니다. jobspec 을 이용해 kill 명령으로 신호를 보내면 같은 pgid ( process group id ) 를 갖는 프로세스 들에게 모두 전달되므로 만약에 child process 가 생성되어 실행 중이라면 함께 종료하게 됩니다.

Jobspec 과 pid 가 다른점

pid 는 개별 프로세스를 나타내지만 jobspec 은 파이프로 연결된 모든 프로세스를 포함합니다.

# 파이프로 연결된 경우 마지막 명령의 pid 를 표시
$ sleep 10 | sleep 10 | sleep 10 &
[1] 12782

# jobspec 은 파이프로 연결된 3 개의 프로세스를 모두 포함.
$ jobs %1 
[1]+  Running                 sleep 10 | sleep 10 | sleep 10 &

# pid 는 개별 프로세스를 나타냄.
$ ps f
  PID TTY      STAT   TIME COMMAND
...
 1643 pts/10   Ss     0:00 bash
12780 pts/10   S      0:00  \_ sleep 10   # pid
12781 pts/10   S      0:00  \_ sleep 10   # pid
12782 pts/10   S      0:00  \_ sleep 10   # pid
...

jobs

jobs [-lnprs] [jobspec ...] or jobs -x command [args]

현재 job table 목록을 보여줍니다. -l 옵션을 주면 process id 도 함께 보여줍니다. job id 옆에 보이는 +, - 기호는 jobspec 에 사용되며 %+ 는 current job 그러니까 가장 최근에 background 상태가된 job 을 나타내고 %- 는 previous job 을 나타냅니다. fg 명령을 사용해 이동함에 따라 + , - 위치도 바뀌게 됩니다.

jobspec %%%+ 와 동일한 의미를 가집니다.

$ vi 111 &
$ vi 222 &
$ vi 333 &

$ jobs                             # vi 로 3 개의 파일을 열고난 후의 상태
[1]   Stopped              vi 111
[2]-  Stopped              vi 222  # previous job
[3]+  Stopped              vi 333  # current  job ( 가장 최근 job )

$ fg %1                            # %1 로 이동
                                   # vi 화면에서 ctrl-z
$ jobs                             
[1]+  Stopped              vi 111  # current
[2]   Stopped              vi 222
[3]-  Stopped              vi 333  # previous

$ fg %2                            # %2 로 이동
                                   # vi 화면에서 ctrl-z
$ jobs                             
[1]-  Stopped              vi 111  # previous
[2]+  Stopped              vi 222  # current
[3]   Stopped              vi 333

-p 옵션은 job table 에서 pid 만 표시합니다. 파이프로 연결된 명령 그룹일 경우 첫번째 pid 만 표시됩니다.

fg

fg [jobspec]

현재 background 에 stopped 또는 running 상태에 있는 job 을 foreground 로 실행하고 current job 으로 만듭니다. 그러므로 이후에 ctrl-z 로 stopped 되었을때 job table 에는 + 로 표시됩니다. jobspec 을 인수로 주지 않으면 current job ( + 표시된 job ) 이 사용됩니다.

bg

bg [jobspec ...]

Ctrl-z ( SIGTSTP ) 에 의해 현재 stopped 상태에 있는 background job 에 SIGCONT 신호를 보내 background running 상태로 만듭니다. jobspec 을 인수로 주지 않으면 current job 이 사용됩니다.

suspend

suspend [-f]

suspend 명령을 실행하는 shell 은 이후에 SIGCONT 신호를 받기 전까지 중단됩니다.
login shell 에서는 사용할 수 없으나 -f 옵션을 이용하면 override 할 수 있습니다.

disown

disown [-h] [-ar] [jobspec ...]

disown 은 "이 job 은 내것이 아니다" 라고 선언합니다. job 이 종료되는 것은 아니고 결과로 job table 목록에서 삭제되어 더이상 control 할 수 없게 됩니다. 터미널 프로그램을 종료시킬때, 또는 login shell 에서 exit 시에 HUP 시그널에 의해 job 이 종료되는 것을 방지할 수 있습니다 ( shopt -s huponexit 옵션이 설정되어 있을경우 ). -h 옵션도 동일한 역할을 하지만 job table 에는 계속 남아있으므로 control 할 수 있습니다.

wait

wait [-n] [jobspec or pid …]

Background 로 실행되는 job 이 종료될 때까지 기다립니다. child 프로세스에 해당할 경우만 wait 할 수 있습니다. 인수로 jobspec 이나 pid 를 주게되면 해당 job 이 종료될 때까지 기다린 후 종료 상태 값을 리턴합니다. 인수 없이 실행하면 모든 프로세스를 기다리며 종료 값으로는 0 을 리턴합니다. 특정 작업이 완료된후에 다음단계로 진행해야 할경우 사용할 수 있습니다.

$! 변수는 가장 최근에 background 로 실행된 pid 값을 나타냅니다. | 파이프로 연결된 명령 그룹일 경우 마지막 명령의 pid 가 되며 전체 pipeline 이 종료될 때까지 wait 합니다.

#!/bin/bash

( sleep 1; exit 3 ) &

wait $!
echo $?

########### output ###########

3

---------------------------------------
#!/bin/bash

( echo start process 1...; sleep 3; echo end process 1.; exit 1 ) &
( echo start process 2...; sleep 2; echo end process 2.; exit 2 ) &
( echo start process 3...; sleep 4; echo end process 3.; exit 3 ) &

wait                          # 3 개의 background process 를 모두 기다림.            
echo exit status: $?

########### output ###########

start process 1...
start process 2...
start process 3...
end process 2.
end process 1.
end process 3.
exit status: 0                # 종료 상태값이 0 이 됨

------------- wait 활용 ---------------
#!/bin/bash

echo main start ...

(
    ( echo start process 1...; sleep 3; echo end process 1; exit 1 ) &
    wait $!
    echo process 1 exit status : $? 
) &
(
    ( echo start process 2...; sleep 2; echo end process 2; exit 2 ) &
    wait $!
    echo process 2 exit status : $? 
) &

wait
echo main end ...

########### output ###########

main start ...
start process 1...
start process 2...
end process 2
process 2 exit status : 2
end process 1
process 1 exit status : 1
main end ...

위의 예에서 보는 것과 같이 여러개의 background job 을 생성하면 각 프로세스의 종료 상태 값을 main 프로세스에서 알 수가 없습니다. 그럴땐 다음과 같은 방법을 사용해볼 수 있습니다.

#!/bin/bash

trap 'rm -f $tmpfile' EXIT

tmpfile=`mktemp`
number_of_jobs=10

do_job() {
    echo start job $i...
    sleep $((RANDOM % 5)) 
    echo ...end job $i
    exit $((RANDOM % number_of_jobs))
}

for i in $( seq $number_of_jobs ); do
(
    do_job &

    wait $!
    echo job$i : exit status : $? >> $tmpfile
) &
done

wait 

i=0
while read -r res; do
    echo "$res"
    let i++
done < $tmpfile

echo $i jobs done !!!

Job control 관련 키

  • Ctrl-c

    interrupt 신호 ( SIGINT ) 를 foreground job 에 보내 종료시킵니다.

  • Ctrl-z

    suspend 신호 ( SIGTSTP ) 를 foreground job 에 보내 suspend 시키고 background 에 있던 shell 프로세스를 foreground 로 하여 명령을 입력받을 수 있게 합니다.

Input and Output

  • Input

    입력은 foreground job 에서만 받을 수 있습니다. background job 에서 입력을 받게되면 SIGTTIN 신호가 전달되어 suspend 됩니다.

  • Output

    출력은 기본적으로 현재 session 에서 실행되고 있는 모든 job 들이 공유합니다. 그러므로 background job 을 실행할때 제대로 redirection 처리를 하지 않으면 터미널로 출력되는 메시지들이 서로 섞이게 됩니다.

    stty tostop 명령을 사용하면 background job 에서 출력이 발생할시 suspend 시킬 수 있습니다.

Background job 은 subshell 에서 실행됩니다.

{ ;}( ) 를 이용한 명령그룹을 background 로 실행 시키면 둘 다 동일하게 subshell 에서 실행됩니다. 그러므로 { ;} 를 이용해 정의한 shell 함수를 background 로 실행 시킬때도 subshell 에서 실행되게 됩니다.

$ AA=100; echo $$ $BASHPID;
31653 31653

$ { AA=200; echo $$ $BASHPID ;} &
31653 9203

$ echo $AA
100

Script 파일 실행 중에 background 로 실행

스크립트 파일을 실행 중에 background 로 명령을 실행하게 되면 이때 실행되는 명령은 job table 에 나타나지 않고 stdin 은 /dev/null 에 연결됩니다. parent process 에 해당하는 스크립트 파일이 먼저 종료하게 되면 PPID 가 init 으로 바뀌어 실행을 지속하므로 데몬 프로세스를 만드는 방법으로도 사용됩니다.

Script 파일 에서는 job control 이 기본적으로 disable 됩니다.

Non-interactive shell 인 script 실행 시에는 기본적으로 job control 이 disable 됩니다. 그렇다고 해서 & 메타 문자를 이용해 background process 를 생성하지 못한다는 것은 아니고 다만 bg, fg, suspend 명령을 사용할 수 없습니다. 하지만 jobs, wait, disown 명령들은 사용할 수 있습니다. 필요에 따라 set -o monitor 옵션 설정을 통해 enable 할 수도 있습니다.

Shell 이 종료되면 background job 들은 어떻게 될까?

  • 프롬프트 상에서 exit 이나 logout 명령으로 종료할 경우

    background job 은 두가지 상태를 가집니다. stopped 와 running 인데요. shell 을 exit 할때 stopped 상태에 있는 job 이 있으면 메시지를 통해 프롬프트 상에 알려줍니다. 하지만 stopped job 을 처리하지 않고 다시 exit 명령을 하게되면 shell 이 종료하는데 이때 stopped job 도 함께 종료됩니다.

    running 상태에 있는 job 은 기본적으로 shell 이 종료되어도 background 로 실행을 계속합니다. 그러나 바뀌는게 하나 있는데요, 바로 parent process id (ppid) 입니다. background job 들은 shell 에서 실행이 됐기 때문에 ppid 가 shell 이 되는데요. shell 이 종료가 됐기때문에 ppid 가 1 번 그러니까 init process 로 바뀌게 됩니다.

    login shell 일경우 shopt -s huponexit 옵션을 설정하게 되면 logout 시에 모든 running background job 들이 HUP 신호를 받고 종료하게 됩니다.

  • 윈도우 상에서 터미널 프로그램을 종료시키거나 시스템이 종료될 경우

    remote login 에서 넷트웍, 모뎀 연결이 끊기거나, interactive shell 에 kill -HUP 신호를 주는 경우도 해당되며 이때는 shell 의 stopped, running job 들이 모두 HUP 신호를 받고 종료합니다.