검색 엔진의 방문이 늘어나고 있군...

Posted
Filed under 시스템
참조 원문: Life cycle of a process

  시스템에서 뭔가를 하려면 프로세스가 필요합니다. 프로세스는 일반적으로 바이너리 실행 프로그램을 실행했을 때 생성됩니다. 코드가 프로세스로 변환되는 과정, 프로세스가 생성되는 방법, 죽기 전까지 거치는 상태를 이해하는 것은 상당히 중요합니다. 이 글에서는 코드가 실행 파일이 되고 거기서 다시 프로세스로 되는 과정, 프로세스와 관련이 있는 식별 요소들, 프로세스의 메모리 구조, 프로세스의 상태, 리눅스 내에서 프로세스의 전체적인 라이프 사이클에 대한 요약을 알아볼 것입니다.

코드에서 실행 프로그램으로
  소프트웨어 프로그램의 생명은 개발자가 코드를 작성할 때부터 시작됩니다. 여러분이 사용하는 모든 소프트웨어 프로그램은 특정 프로그래밍 언어로 작성됐습니다. 코드 작성이 끝나면 다음 단계로 그것을 실행 프로그램으로 변환합니다. 컴파일 프로세스가 코드를 기계 수준의 명령어(실행 프로그램)로 변환하죠. 그러면 실행 프로그램은 OS가 이해할 수 있는 기계 코드를 갖게 됩니다.

실행 프로그램에서 프로세스로
  프로그램이 실행되면 ps 명령어로 관련 프로세스를 확인할 수 있습니다. 리눅스에서 프로세스와 관련된 주요 식별 요소는 3가지로 프로세스 ID, 부모 프로세스 ID, 그룹 ID가 있습니다.

  init이란 프로세스는 리눅스 시스템에 생성되는 첫 번째 프로세스이며 프로세스 ID는 1입니다. 다른 모든 프로세스는 init의 자식 또는 그 이하입니다. pstree 명령어를 사용하면 활동 중인 프로세스들을 계층 구조로 볼 수 있습니다.

리눅스 프로세스의 메모리 구조
  리눅스 프로세스의 메모리 구조는 아래의 메모리 세그먼트들로 이뤄져 있습니다.

Stack: 지역 변수와 함수 인자(프로그램 코드에 정의되어 있음)가 있는 세그먼트입니다. 이 안에 있는 내용은 LIFO(last in, first out) 순서로 저장됩니다. 함수가 호출되면 새로운 함수와 연관된 메모리는 stack에 할당됩니다. 필요할 경우 메모리가 동적으로 증가하지만 일정한 수준까지만 가능합니다.

메모리 맵핑: 이 영역은 맵핑 파일을 위해 사용합니다. 이것이 존재하는 이유는 메모리에 맵핑된 파일에 대한 입출력 작업은 (일반적으로 파일이 저장된 곳인)디스크에 대한 입출력과 비교했을 때 프로세서(CPU)와 시간을 별로 소모하지 않기 때문입니다. 그렇기 때문에 이 영역은 거의 대부분 동적 라이브러리를 위해 사용합니다.

Heap: Stack에는 2가지 제한이 있습니다. 크기가 그리 넉넉치 못하다는 것과 stack을 생성한 함수가 종료되거나 값을 반환할 때 stack의 모든 변수도 사라진다는 겁니다. 이런 점에 있어서 heap 메모리 세그먼트가 빛을 발합니다. 이 세그먼트를 통해 매우 큰 메모리를 할당할 수 있으며 이렇게 할당한 메모리는 프로그램이 끝날 때까지 사용할 수 있습니다. 그러므로 heap에 할당한 메모리는 프로그램이 종료되거나 프로그래머가 함수 호출을 통해 명시적으로 해제하기 전까지 해제되지 않습니다.

BSS와 데이터 세그먼트: BSS 세그먼트에는 최초에 명시하지 않은 정적/전역 변수를 저장하며, 데이터 세그먼트에는 값을 최초에 명시한 변수를 저장합니다. 참고로 전역 변수란 함수 안에 정의되어 있지 않으며 프로그램과 스코프/라이프타임이 동일한 변수를 말합니다. 유일한 예외는 함수 안에 있지만 static 키워드로 정의된 변수로서 그 스코프는 함수 내로 제한됩니다. Static으로 정의된 변수들은 BSS 또는 데이터 세그먼트에 전역 변수들과 함께 저장합니다.

텍스트 세그먼트: 프로세서가 읽고 실행할 프로그램의 모든 기계 수준 코드 명령어를 보관하고 있습니다. 이 세그먼트는 쓰기 보호가 되어 있어서 수정이 불가능합니다. 만약 그런 시도를 하면 crash나 segmentation fault가 발생합니다.

참조: 실제 메모리 구조는 위의 내용보다 더 복잡하지만 이 정도만으로도 개념을 잡기엔 충분합니다.

리눅스 프로세스의 상태
  리눅스에서 프로세스를 실시간으로 관찰할 때는 top 명령어를 사용합니다. 이 명령어의 8번째 칼럼에는 프로세스의 상태가 적혀 있습니다. 여기에 나오는 용어의 뜻을 이해하는 것은 매우 중요합니다. 리눅스의 프로세스는 다음 중 하나의 상태에 놓입니다.

Running: 프로세스가 실제로 실행되고 있거나 실행되기 위해 스케줄러의 큐에서 대기 중(실행 준비 완료)인 상태입니다. 때문에 경우에 따라 'runnable'이나 R로 표시하기도 합니다.

Waiting 또는 Sleeping: 어떤 일을 발생하는 것을 기다리거나 완료를 위해 특정 리소스가 필요한 작업을 위해 기다리는 상태입니다. Waiting 상태는 상황에 따라 interruptible(S)과 uninterruptible(D)로 나눠지기도 합니다.

Stopped: 멈추라는 신호를 받은 상태입니다. 일반적으로 디버그를 할 때 볼 수 있습니다. 이 상태는 T로 표시합니다.

Zombie: 실행은 끝났지만 부모가 종료 상태를 회수하기 전까지 기다리는 상태입니다. 이 상태는 Z로 표시합니다.

  위 상태들이 끝나면 프로세스는 죽습니다. 정확히 따지자면 죽은 프로세스는 존재 자체가 사라지므로 '죽음'이란 상태는 없습니다.

프로세스의 라이프 사이클
  프로세스는 생성되는 순간부터 종료되기(또는 kill 당하기)까지 다양한 단계를 거칩니다. 여기서는 리눅스 프로세스의 탄생부터 죽음까지의 라이프 사이클 전체를 설명합니다.

  리눅스 시스템이 처음 켜지면 압축된 커널 실행 코드가 메모리에 적재됩니다. 이 실행 코드가 리눅스 시스템의 다른 모든 프로세스를 생성하는 것에 대한 책임을 지는 init 프로세스(또는 시스템의 첫 프로세스)를 생성합니다.

  Running 프로세스는 자식 프로세스를 생성할 수 있습니다. 자식 프로세스는 fork() 함수나 exec() 함수(exec() 함수는 어떤 실행 파일을 실행시키고 그로 인해 생긴 프로세스를 자신에게 덮어씌우는 함수로 자식 프로세스를 생성하는 함수가 아님)로 생성할 수 있습니다. 만약 fork()를 사용하면 해당 프로세스는 부모 프로세스의 주소 공간을 사용하며 부모와 같은 모드에서 실행됩니다. 새롭게 탄생한 (자식)프로세스는 부모로부터 모든 메모리 세그먼트를 복사해서 물려받지만 (부모나 자식이)세그먼트를 수정하려고 하기 전까진 계속해서 같은 세그먼트를 사용합니다. 이에 반해 exec()를 사용하면 프로세스에 새로운 주소 공간이 할당되므로 처음에는 커널 모드에 진입합니다. 참고로 부모 프로세스가 새로운 프로세스를 생성하려면 running 상태(이면서 프로세스에 의해 실제로 실행 중인 상태)여야 합니다.

  커널 스케줄러의 상황에 따라 다른 프로세스가 CPU를 선점하면 그 전에 CPU를 사용하던 running 프로세스는 큐로 쫓겨나 다시 실행되기 전까지 대기하게 됩니다.

  만약 프로세스가 하드웨어 리소스를 확보하거나 파일 입출력 작업을 할 필요가 있다면 프로세스는 일반적으로 시스템 콜을 하여 커널 모드로 진입합니다. 만약 리소스가 이미 사용 중이거나 파일 입출력에 시간이 걸린다면 프로세스는 sleeping 상태로 들어갑니다. 리소스를 사용할 수 있거나 파일 입출력이 끝나면 프로세스는 '프로세스를 깨우는 신호(signal)'를 받고 일어나 커널 모드로 실행을 계속하거나 유저 모드로 되돌아갈 수 있습니다. 참고로 그 프로세스가 즉시 실행을 시작할 것이란 보장은 없으며 그것은 순전히 (실행 준비를 위해 프로세스를 프로세스 큐에 넣는)스케줄러에 달려 있습니다.

  만약 프로세스를 디버그 모드(예: 디버거가 프로세스에 부착됨)로 실행하면 디버그 프레이크포인트를 만났을 때 정지 신호를 받습니다. 이 단계에서 해당 프로세스는 stop 상태에 들어가며 유저는 프로세스의 메모리 상태, 변수 값 등을 가지고 디버그할 시간을 갖게 됩니다.

  프로세스가 정상적으로 값을 반환하거나 종료될 수도 있고 다른 프로세스에 의해 죽을 수도 있습니다. 어느 쪽이 됐든 간에 프로세스는 zombie 상태에 빠지게 되며 그렇게 되면 그 프로세스에 대한 것들 중 (커널에 의해 관리되는)프로세스 테이블에 있는 그 프로세스 항목을 제외한 모든 것이 사라집니다. 그 항목은 부모 프로세스가 자식 프로세스의 반환 상태를 회수하기 전까지 지워지지 않고 남습니다. 반환 상태는 프로세스가 일을 정상적으로 마쳤는지를 나타냅니다. 'echo $?' 명령어를 통해 커맨드 라인로 실행한 마지막 명령어의 상태(기본적으로 반환 상태가 0인 경우만 정상 종료로 취급)를 확인할 수 있습니다. 프로세스가 zombie 상태로 진입하면 다른 상태로 진입하기 위해 필요한 것들이 모두 사라지기 때문에 더 이상 다른 상태로 진입할 수 없게 됩니다.

  만약 자식 프로세스가 죽기 전에 부모 프로세스가 죽으면 자식 프로세스는 고아가 되어 init 프로세스에게 입양되는데 이는 init이 그 프로세스의 새 부모가 된다는 것을 의미합니다.

Load Average란 무엇인가?
  시스템 자원 사용률을 측정하는 프로그램들에서 load average라는 것을 쉽게 볼 수 있는데 이는 실행을 기다리는 프로세스 큐의 현재 길이의 평균값을 뜻합니다. 리눅스의 경우 이 수치는 실제 실행 중인 큐(actual run queue), 실행 가능한 큐(runnable queue), 깨울 수 없는(uninterruptable) sleep 상태에 빠진 프로세스 개수의 조합으로 결정됩니다. CPU 사용률과 load average 값이 함께 높을 때도 있지만 일반적으로 load average 값이 높다는 것은 프로세스들이 입출력 대기 때문에 일을 끝내지 못하고 기다리고 있다는 것을 의미합니다. 그러므로 리눅스의 경우 CPU 사용률과는 무관하게 load average가 높다면 프로세스가 CPU를 사용하기 위해 대기하는 시간이 길어지게 되어 모든 것이 느려지게 됩니다.
2013/12/10 17:50 2013/12/10 17:50