Process Creation

이번에는 다음과 같은 의문사항들에 대해 알아보도록 하겠습니다.

  • subshell 에서는 왜 parent shell 과 동일한 환경을 갖게되며 export 하지도 않은 변수, 함수를 그대로 사용할 수 있는가?

  • 그리고 child process 에서는 왜 export 한 변수, 함수만 사용할 수 있는가?

  • 변수를 export 했는데 터미널을 새로 열면 왜 값이 보이지 않는가?

  • subshell, child process 에서 설정한 변수는 왜 parent shell 에는 적용되지 않는가?

  • 또한 parent shell 에서 값을 변경하면 왜 실행 중인 subshell, child process 에 적용되지 않는가?

  • 왜 현재 shell 의 변수값을 ( ) $( ) ` ` | & 에서 변경할 수 없는가?

Process 주소공간 과 PCB

위 그림에서 1번은 프로그램( 명령 ) 을 실행했을때 메모리상의 주소공간을 나타냅니다. 만약에 컴퓨터 시스템에서 하나의 프로그램만 실행한다면 저 주소공간 하나 외에 다른것은 필요하지 않을것입니다. 하지만 실제로는 여러개의 프로세스가 동시에 실행되므로 프로세스들을 관리하고 스케줄하기 위해 Operating System 이 필요하게 됩니다. 이때 OS 는 프로세스의 PID, PPID 는 무엇이고, 현재 상태( stopped, running ... )는 어떻게 되는지, 사용중인 파일들은 어떻게 되고, cpu 사용시간은 얼마나 되는지와 같은 정보들을 기록하고 관리하는데 이때 사용되는 구조체가 그림에서 2번인 PCB (Process Control Block) 입니다. PCB 는 항목수가 140 개가 넘을 정도로 큰 구조체 이며 특정 프로세스에 대한 모든 정보가 담겨져 있다고 할 수 있습니다.

Shell 을 사용하면서 설정, 변경하게 되는 변수, 함수, 옵션설정등 프로그램에 관련된 사항은 모두 1번 프로세스 주소공간에 위치합니다. 프로세스 주소공간은 메모리 내에서 다른 프로세스 주소공간에 대해 각각 독립적으로 존재하므로 프로세스 A 에서 변수값을 설정, 변경해도 그상태가 다른 프로세스의 주소공간에 영향을 미치지 않습니다. 따라서 parent shell 에서 설정한 변수값이라도 그값이 subshell, child 프로세스에 적용되지 않으며, 또한 subshell, child 프로세스 에서 변경한 값도 parent shell 프로세스에 적용되지 않게 됩니다.

( ) $( ) ` ` | & 에서 설정한 값들은 subshell 상에서 설정한 값이므로 실행을 마치고 현재 shell 로 돌아오면 프로세스의 주소공간이 다시 바뀌게 되므로 설정한 값들이 존재하지 않게 되는 것입니다.

프로세스 간에 데이터를 전달하기 위해서는 파일이나 파이프를 이용하거나 OS 에서 제공하는 shared memory, massage queue, socket 같은 IPC (Inter Process Communication) 방법을 사용합니다.

Process 생성 단계

프로세스 생성 단계를 살펴봄으로써 subshell 과 child process 의 차이점에 대해 알아보겠습니다.
여기서 사용된 fork, exec, exit, wait 은 OS 에서 제공하는 system call 함수입니다.

1. ls 명령 실행

2. fork 단계

fork 은 unix 에서 새로운 프로세스를 만드는 방법입니다. 새로운 프로세스가 생기는 것이므로 pid, ppid 가 변경된 것을 볼 수 있습니다 (4번). fork 은 현재 프로세스와 동일한 주소공간과 PCB 를 갖는 프로세스를 생성합니다. 그러므로 생성 초기에는 현재 bash shell 과 동일한 환경이라고 할 수 있습니다. 주소공간이 같으므로 $$ 나 $PPID 와 같은 변수 값들이 변경 없이 그대로 나오게 되고 export 하지 않은 변수나 함수들도 사용할 수 있게 됩니다. 이 상태가 바로 subshell 입니다. subshell 에서 명령이 실행될 때는 $BASHPID 가 새로 생성된 pid 로 설정되어 실행됩니다.

3. exec 단계

새로운 프로세스가 생성된 상태에서 기존 프로그램의 주소공간을 삭제하고 새로 실행될 프로그램을 메모리로 로드하여 실행시키는 것이 exec 입니다. 위 그림에서 보면 기존의 bash 프로그램 주소공간이 exec 에 의해 ls 프로그램 주소공간으로 대체된 것을 볼 수 있습니다 (5번). 주소공간이 바뀌게 되므로 기존에 bash 프로그램에서 사용되던 변수들이나 함수들은 모두 사용할 수 없게 됩니다.

exec 함수를 실행할 때는 환경변수( export 한 변수, 함수 )를 인수로 전달하기 때문에 child process 에서 사용할 수 있게 됩니다. 현재 shell 에서 변수를 export 하였는데도 새로 터미널을 열었을때 보이지 않는 것은 새로 생성된 프로세스는 현재 shell 의 child process 가 아니기 때문입니다.

4. exit 단계

child 프로세스가 exit 함수를 실행하면 자신이 사용하던 모든 자원을 해제하고 종료하게 됩니다. 이때 종료 상태 값을 PCB 항목에 설정하는데 parent 프로세스는 wait 함수를 통해 값을 구할 수가 있습니다. child 프로세스가 종료하였는데도 parent 에서 wait 함수를 호출하지 않으면 child 프로세스는 OS 가 관리하는 프로세스 테이블에 계속 남아 있게 되는데 이 상태가 좀비에 해당합니다.

만약에 parent 프로세스가 child 보다 먼저 종료하게 된다면 child 프로세스는 pid 1번인 init 프로세스로 reparent 가되고 init 프로세스는 주기적으로 wait 함수를 호출하므로 프로세스 테이블에서 정리되게 됩니다.

5. continue

fork-exec

위에서 살펴본 바와 같이 shell 에서 명령을 실행하면 fork 에의해 새로 프로세스가 생성되고 exec 에의해 새로운 프로그램으로 주소공간이 대체되어 실행됩니다. 보통 fork-exec 이라고 말하는데 이 과정을 shell 에서 표현해보면 fork 은 subshell 에 해당하고 exec 은 exec builtin 명령으로 나타낼 수 있으므로 아래 두 명령은 같은 결과를 갖게 됩니다.

$ date
Mon Dec 28 13:49:17 KST 2015

$ ( exec date )
Mon Dec 28 13:49:25 KST 2015

man page 색상 넣기

# 함수를 정의할때 { ;} 대신 ( ) 를 사용
man() (
    export LESS_TERMCAP_md=$'\e[01;35m'
    export LESS_TERMCAP_us=$'\e[04;36m'
    export LESS_TERMCAP_me=$'\e[0m'
    export LESS_TERMCAP_se=$'\e[0m'
    export LESS_TERMCAP_ue=$'\e[0m'

    exec man "$@"
)

subshell 과 child process 구분

subshell 도 parent 프로세스에서 생성된 자신의 pid 를 갖는 child process 입니다. 하지만 exec 에 의해 생성된 프로세스와 구분하기 위해 분리하여 설명하였습니다.

Thread

워드프로세서나 웹브라우저 같은 프로그램을 실행하면 보통 내부적으로 여러 개의 스레드를 생성하여 사용합니다. 만약에 스레드라는 기능이 없어서 모두 프로세스로 실행해야 된다고 한다면, 프로세스는 독립적인 주소공간도 생성해야 하고 PCB 도 만들어야 하고 필요로 하는 정보와 리소스가 많습니다. 또한 프로세스들끼리 서로 데이터도 주고 받아야 할텐데 각기 주소공간이 독립적이라 IPC 방법을 이용해야 합니다.

thread 는 기존의 프로세스가 가지는 주소공간과 리소스를 공유하면서 OS 가 스케줄하고 관리하는데 필요한 최소한의 정보만을 가지고 프로세스(thread)를 만들어 사용할 수 있게 해줍니다. 스레드는 프로세스의 주소공간을 공유하므로 데이터를 주고받기 위해 따로 IPC 방법을 사용할 필요가 없습니다. 그냥 한 스레드가 global, static 변수에 값을 저장하면 다른 스레드가 변경된 값을 읽을 수가 있습니다.

아래 그림은 하나의 프로세스에서 실행되는 thread 들을 보여주는데요. 프로세스가 가지는 text, data, heap 주소공간과 Files, Locks, Sockets 같은 리소스들을 공유하면서 각자 실행에 필요한 stack 을 가지고 독립적으로 실행되는 것을 볼 수 있습니다. OS 가 스케줄하는 단위도 프로세스가 아니라 스레드 입니다. 그래서 스레드를 이용하면 각각의 cpu 코어에서 실행될 수 있습니다.

사실 프로세스도 하나의 스레드 입니다. 프로세스가 생성되면 하나의 스레드가 생기는 것입니다. 스레드는 TID (Thread ID) 를 갖는데 TGID (Thread Group ID) 는 pid 와 같습니다. 다음은 ps 명령을 이용하여 chrome 브라우저의 pid 와 tid 를 조회하는 예인데요. LWP (light weight process) 로 표시되는 두번째 컬럼이 tid 인데 첫번째 값이 pid 와 같은 것을 알 수 있습니다.

tid 는 ps 명령의 H, -L, m, -m, -T 옵션을 이용해 조회해볼 수 있습니다.
타이틀 값이 LWP, SPID 로 나오는것이 tid 입니다.

$ ps j -L -C ttt
 PPID   PID  PGID   SID   LWP TTY      TPGID STAT   UID   TIME COMMAND
 3292  9664  9664  3292  9664 pts/5     9664 Sl+   1000   0:00 ./ttt
 3292  9664  9664  3292  9665 pts/5     9664 Sl+   1000   0:00 ./ttt
 3292  9664  9664  3292  9666 pts/5     9664 Sl+   1000   0:00 ./ttt

/proc 파일 시스템을 통해서도 알아볼 수 있습니다.

$ ls /proc/$PID/task/
19242/  19244/  19246/  19248/  19250/  21141/  21143/
19243/  19245/  19247/  19249/  19258/  21142/  21305/

shell 에서는 기본적으로 명령 (fork-exec 에 의해 생성되는 프로세스) 를 다루므로 thread 를 직접 생성하거나 컨트롤할 수 없습니다.