Session 과 Process group

리눅스 에서 실행되는 모든 프로세스들을 Process ID (PID) 와 Parent Process ID (PPID) 관계로만 관리하기엔 부족한점이 있는데요. 그래서 session 과 process group 을 만들어 사용합니다. 예를들어 터미널을 열면 shell 이 실행되는 이때 shell PID 가 Session ID (SID) 가 되며 session leader ( PID == SID ) 가 됩니다. 이후 프롬프트를 통해 명령을 실행시켜 생성되는 자손 process 들은 모두 같은 SID 를 갖게됩니다.

Process Group 은 작업 제어를 목적으로 함께 처리해야 할 관련 프로세스들의 그룹입니다. shell script 가 실행되면 process group 이 만들어 지는데 이때 스크립트 PID 가 Process Group ID (PGID) 가 되며 process group leader ( PID == PGID ) 가 됩니다. 새로 생성되는 프로세스는 parent process 의 PGID 를 상속하므로 이후 스크립트 내에서 실행되는 process 들은 모두 같은 PGID 를 갖게됩니다. 다음은 프롬프트 상에서 AA.sh 스크립트를 실행한 예입니다. 모두 bash shell PID 를 SID 로 가지고 있고 AA.sh PID 가 PGID 로 사용된 것을 볼 수 있습니다.

파이프를 통해 여러 명령을 동시에 실행시킬 때도 process group 이 만들어지는데 이때는 파이프로 연결된 명령들중에 첫번째 명령이 PGID 와 process group leader 가 됩니다. ( 스크립트 내에서 실행될 경우는 스크립트 PGID 를 따릅니다.) 다음은 프롬프트 상에서 cat | grep hello | wc -l 명령을 실행한 예입니다. 모두 bash shell PID 를 SID 로 가지고 있고 파이프로 연결된 명령 중에 첫번째 명령의 PID 가 PGID 로 사용된 것을 볼 수 있습니다.

이와 같이 process group 은 job control 을할때 기본단위 ( job ) 가 됩니다. 하나의 session 에서는 하나의 process group 만 foreground 가 될수있고 나머지는 background 가 됩니다.

실행되는 모든 프로세스는 프로세스 그룹에 속하게 되고, 또한 프로세스 그룹은 세션에 속하게 됩니다.

조회하고 신호보내기

현재 실행되고 있는 process 들의 PPID, PID, PGID, SID 관계는 다음과 같이 ps 명령을 통해 알아볼 수 있고 pgrep , pkill 명령을 사용하면 process group 이나 session 별로 조회하거나 시그널을 보낼 수 있습니다.

$ ps jf

$ ps fo user,ppid,pid,pgid,sid,comm

$ ps -o pgid= $PID     # $PID 의 PGID 를 조회

# pgrep 명령을 이용해 현재 실행중인 test.sh 스크립트와 같은 PGID 를 갖는 프로세스들을 출력
$ pgrep -g `pgrep -x test.sh` 
19788
19791

실행중인 스크립트를 종료하는 방법

스크립트를 종료할 때는 jobspec 을 이용하거나 PGID 를 이용해 process group 에 신호를 보내야 합니다. 또는 Ctrl-c 키를 누르는 것도 process group 에 신호를 보내는 방법 중에 하나입니다. 그렇지 않고 스크립트 PID 에만 신호를 보내게 되면 child process 는 종료되지 않고 남아 있게 됩니다. 다음은 ping 외부 명령을 실행하고 있는 스크립트에 PID 를 이용해 종료 신호를 보낸 경우인데 child process 인 ping 명령은 종료되지 않은 체 남아 있습니다.

-------- test.sh --------
#!/bin/bash
echo test.sh ... start

ping 0.0.0.1

echo test.sh ... end
-------------------------

$ ./test.sh &

# test.sh 스크립트 실행후 같은 PGID 를 갖는 프로세스 조회
$ ps jf $( pgrep -g `pgrep -x test.sh` )
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 4655 18946 18946  4655 pts/14   19642 S     1000   0:00 /bin/bash ./test.sh
18946 18949 18946  4655 pts/14   19642 S     1000   0:00  \_ ping 0.0.0.1

# test.sh 스크립트 PID 를 이용해 종료
$ kill 18946

# child process 인 ping 명령은 종료되지 않고 PPID 가 init 으로 바뀌어 남아있다.
$ ps j -C ping
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
    1 18949 18946  4655 pts/14   19688 S     1000   0:00 ping 0.0.0.1

따라서 다음과 같이 PGID 를 이용해 스크립트를 종료해야 합니다.

  • kill -- -12345 ( PGID 앞에 - 문자를 붙인다. )
  • pkill -g 12345
$ test.sh &

$ ps j -C test.sh
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
23973 25517 25517 23973 pts/16   25517 S+    1000   0:00 /bin/bash ./test.sh

# test.sh 스크립트 PGID 를 이용해 종료
$ kill -- -25517

# 같은 PGID 를 갖는 ping 명령도 함께 종료되었다.
$ ps j -C ping
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND

새로운 session id 로 실행하기

스크립트를 background 로 실행시킬때 setsid 명령을 이용하면 새로운 SID, PGID 가 할당되고 PPID 도 init 으로 바뀌어 실행됩니다. SID 가 바뀌므로 기존의 controlling terminal 에서 떨어져 나가 HUP, INT 같은 신호도 전달되지 않게 됩니다. 그리고 controlling terminal 에 해당하는 /dev/tty 도 사용할 수 없습니다. parent 도 init 이 되므로 스크립트를 daemon 으로 실행시키는 효과를 갖습니다.

$ cat test.sh 
#!/bin/bash

echo start .....
ls -l /dev/fd/ > /dev/tty    # controlling terminal 로 출력
echo end .....

$ ./test.sh          
start .....
total 0
lrwx------ 1 mug896 mug896 64 Mar 13 23:54 0 -> /dev/pts/15
l-wx------ 1 mug896 mug896 64 Mar 13 23:54 1 -> /dev/tty
lrwx------ 1 mug896 mug896 64 Mar 13 23:54 2 -> /dev/pts/15
lr-x------ 1 mug896 mug896 64 Mar 13 23:54 3 -> /proc/15343/fd
end .....

# controlling terminal 을 갖지 않게되어 /dev/tty 를 사용할 수 없다.
$ setsid ./test.sh 
start .....
./test.sh: line 5: /dev/tty: No such device or address
end .....

참조 데몬만들기 : http://blog.n01se.net/blog-n01se-net-p-145.html

$ setsid daemon.sh > /dev/null 2>&1 < /dev/null

PGID 를 변경하여 child process 실행

스크립트를 a.sh -> b.sh -> c.sh -> d.sh 순서로 실행하여 현재 sleep 상태에 있다면 Ctrl-c 를 누를경우 tty driver 에 의해 INT 신호가 foreground process group 에 전달되어 4개의 프로세스는 모두 종료하게 됩니다. 이때 만약에 c.sh 에서 d.sh 을 실행할때 PGID 를 변경할수 있다면 c.sh 과 d.sh 까지만 종료하고 a.sh, b.sh 은 나머지 명령이 실행되게 할 수 있습니다.

shell 에서는 setsid 외부 명령을 사용하여 SID, PGID 를 변경해 명령을 실행할수 있지만 setpgid 같은 명령은 없습니다. 하지만 간접적으로 set -o monitor 옵션 설정을 통해서 이후에 실행되는 명령이 다른 PGID 를 갖게 할 수 있습니다.

$ cat a.sh           $ cat b.sh           $ cat c.sh           $ cat d.sh
#!/bin/bash          #!/bin/bash          #!/bin/bash          #!/bin/bash

echo start a.sh      echo start b.sh      echo start c.sh      echo start d.sh

./b.sh               ./c.sh               ./d.sh               cat

echo end a.sh        echo end b.sh        echo end c.sh        echo end d.sh


# d.sh 에서 cat 명령에 의해 블록됐을때 ctrl-c 를 입력하면 
# 같은 PGID 를 갖는 a.sh b.sh c.sh d.sh 이 모두 종료된다.
$ ./a.sh 
start a.sh
start b.sh
start c.sh
start d.sh
^C              # 모두 종료된다.

이번에는 c.shset -o monitor 옵션을 추가

$ cat c.sh                    $ ./a.sh 
#!/bin/bash                   start a.sh
                              start b.sh
echo start c.sh               start c.sh
                              start d.sh
set -o monitor                ^Cend b.sh     # ctrl-c 를 입력하면 c.sh d.sh 만 종료가 되고
./d.sh                        end a.sh       # b.sh a.sh 은 나머지 명령들이 실행된다.

echo end c.sh

Quiz

ps axj 명령을 실행해 보면 맨 위에서 다음 두 항목을 찾을 수 있는데요. 하나는 시스템의 시작과 종료, 프로세스 생성, 모니터링에 관계된 init 이고 다른 하나는 kernel thread 를 생성하는 kthreadd 입니다. 그런데 모두 PPID 가 0 번인 것을 볼 수 있습니다. PID 0 은 어떤 프로세스일까요?

kernel 초기화 단계에서 실행되는 startup function 을 swapper 또는 process 0 으로 부릅니다. 여기서는 paging 관련 메모리 설정, interrupt handling 설정, 커널에서 사용하는 구조체 설정 등 여러 가지 초기화 작업을 하고 첫 번째 user space 프로세스인 PID 1 번 init 과 kernel space 프로세스인 PID 2 번 kthreadd 을 생성합니다. 이후에는 idle 상태가 되어서 시스템 내에 running 프로세스가 없을 경우 반복적으로 hlt 어셈블리 명령을 실행합니다. init 프로세스는 원래 커널 스레드였는데 이후에 exec() 시스템콜을 실행하여 user space 프로세스가 되어 이름에 [ ] 가 없습니다.

따라서 모든 user space 프로세스는 init 의 자손 프로세스가 되고, 모든 kernel space 프로세스는 kthreadd 의 자손 프로세스가 됩니다.

idle 프로세스도 프로세스이므로 시스템에는 항상 프로세스가 존재하는 것과 같습니다.

# 시스템 내의 모든 kernel space 프로세스를 출력하려면 PPID 가 2 번인 프로세스를 출력하면 된다.
$ ps f --ppid 2 2 
  PID TTY      STAT   TIME COMMAND
    2 ?        S      0:00 [kthreadd]
    3 ?        I<     0:00  \_ [rcu_gp]
    4 ?        I<     0:00  \_ [rcu_par_gp]
    8 ?        I<     0:00  \_ [mm_percpu_wq]
    9 ?        S      0:48  \_ [ksoftirqd/0]
   10 ?        I     20:53  \_ [rcu_sched]
   11 ?        S      0:06  \_ [migration/0]
   12 ?        S      0:00  \_ [idle_inject/0]
   14 ?        S      0:00  \_ [cpuhp/0]
   15 ?        S      0:00  \_ [cpuhp/1]
   . . . .
   . . . .
# kernel space 프로세스를 제외하고 ( --deselect ) 출력하면 모든 user space 프로세스가 된다.   
$ ps f --deselect --ppid 2 2

커널의 경우 하드웨어 인터럽트가 발생하거나, system call, exception 이 발생했을때 현재 실행 중인 프로세스가 중단된 상태에서 kernel mode 로 진입하여 실행되는 형태이므로 따로 프로세스라는 개념이 없습니다( 커널 스레드는 제외 ). 아래 그림은 실제 프로세스 주소공간을 (virtual address space) 나타내는데요. 커널 코드와 데이터가 위치한 커널 쪽 주소는 shared library 처럼 모든 프로세스에서 공유되므로 스케줄러에 의해 다른 프로세스로 스위칭이 되어도 항상 실행될 수가 있습니다. 또한 user mode 에서 접근할 경우 page fault 가 발생하므로 접근할 수가 없습니다.

Quiz

다음은 터미널을 두개 열어서 한쪽에서는 date 명령을 실행하고 한쪽에서는 sh 프로세스를 strace 하려고 한것인데요. 그런데 실행을 해보면 timeout 에의해 strace 명령이 종료될 때 vi 명령도 함께 종료가 되어 제대로 결과를 얻을 수가 없습니다. 어떻게 하면 vi 명령은 함께 종료되지 않게 할 수 있을까요?

아래 명령 사용 동영상은 여기 (후반부) 에서 볼 수 있습니다.

# terminal 1
$ timeout 10 strace -f -p 1234 |& vi -    # terminal 2 의 sh 프로세스를 strace

# terminal 2
sh$ echo $$
1234
sh$ date; date; date

이와 같은 결과가 생기는 이유는 파이프로 연결된 명령들은 실행될 때 같은 process group 을 형성하기 때문입니다. timeout 에의해 종료 신호가 전달될 때는 PGID 를 이용해 process group 에 전달되므로 같은 그룹에 속하는 vi 명령도 함께 종료가 됩니다. 따라서 이때는 { ;} 를 이용하여 process group 을 분리해 주어야 합니다.

보통 { ;} 을 이용해 명령을 실행해도 같은 process group 을 갖게 되지만 bash 에서는 timeout 명령이 실행될 때 process group 이 분리됩니다. ( sh 에서는 분리되지 않음 )

# PGID 가 같아서 timeout 에의해 strace 명령이 종료될 때 vi 도 함께 종료된다.
$ timeout 10 strace -f -p 1234 |& vi -

# { ;} 를 이용하면 subshell 이 생성되고 process group 이 분리된다.
$ { timeout 10 strace -f -p 1234 ;} |& vi -

sh 에서는 다음과 같이 setsid 명령을 사용하면 가능합니다.

$ setsid timeout 10 strace -f -p 1234 2>&1 | vi -