리눅스 - 프로세스 관리
프로세스 관리
1. 프로세스의 개요
프로세스는 실행중인 프로그램이다.
조금 더 정확히 말하자면 프로그램 코드를 실행하면서 생기는 모든 결과물이라고 할 수 있다.
할당된 메모리, 사용중인 파일, 프로세서의 상태 등 모든 자원을 포괄하는 내용으로 프로그램 코드만을 말하지는 않는다.
실행중인 스레드는 프로세스 내부에서 동작하는 객체이다. 각 실행중인 스레드는 개별적인 프로그램 카운터, 스택, 레지스터를 갖고 있는데 이 말인 즉슨 작업 내용을 별도로 갖고 있다는 말과 같다. 프로세스는 이러한 스레드를 여러개 갖고 있을 수도 있고, 한 개만 갖고 있을 수도 있으며 커널은 이러한 스레드를 특정한 법칙에 따라 순서대로 처리한다.(이 특정 법칙에 대해서는 추후 추가 포스팅 예정)
프로세스가 없다면 스레드도 존재할 수 없다. 그렇다면 이 프로세스는 어떻게 생성되고 운영되며 삭제되는 걸까?
2. 프로세스 생성
1) 프로세스 구조
프로세스가 어떻게 생성되는지에 앞서 프로세스는 어떤 것으로 구성되는지 알아볼 필요가 있다.
리눅스 내에서는 이런 프로세스를 태스크 리스트라고 불르는 환형 양방향 연결 리스트 형태로 저장하는데, 각 항목들은 <linux/sched.h>에 정의된 struct task_struct 형식으로 되어 있으며 프로세스 서술자라고 부른다.
이 task_struct는 프로세스가 실행하는데 필요한 모든 정보를 갖고 있다.
예를 들면 사용중인 파일, 프로세스의 주소공간, 대기중인 시그널, 프로세스의 상태 등이 있다. 그 중에서도 Process를 구분하기 위한 구분자인 Process ID가 있는데, 줄여서 PID라고 부른다. PPID 또한 가지는데 이는 부모프로세서의 PID인 값이다.
PID는 초기 유닉스와 리눅스의 하위 호환성 문제로 기본값으로 최대값이 32,768(Short int 최대값)으로 되어있는데 설정에 따라 최대치를 최대 400만으로 상향 가능하다. 이 PID는 구동 당시의 유일한 값이기도 하지만 동시에 구동가능한 프로세스의 숫자를 나타내기도 하며, 상한에 도달한다면 Circular queue형태로 다시 작은 값으로 돌아오기 때문에 상한이 너무 작을시에 나중에 만들어진 프로세스가 작은 PID를 갖는 유용한 경우를 사용할 수가 없다.
2) fork와 exec
리눅스에서는 기존 프로세스를 복사해서 새 프로세스를 만든다. 이러한 작업을 하는 시스템 호출을 fork() 라고 한다. (리눅스에서는 clone() 시스템 호출을 이용해서 fork()를 구현한다) 이 fork()는 호출한 프로세스에게 생성한 프로세스의 PID를 반환하며, 호출한 프로세스는 부모 프로세스가 되고 새로 만들어진 프로세스는 자식 프로세스가 된다.
기존 프로세스를 복제하여 새 프로세스를 만들게 된다고 했다. 이 기존 프로세스를 복제한다는 것은 프로세스가 쓰고 있는 메모리와 스택, 힙, 레지스터 상태를 복사한다는 것으로 이렇게 구현하게 되면 너무 느리다. 따라서 복제된 프로세스는 기존 프로세스와 동일한 자원을 사용하되 변경 사항이 생길때 복제되게 되는데 이 경우를 copy-on-write(기록사항 발생 시 복사)라고 한다.
이렇게 생성된 자식 프로세스는 exec() 계열 함수를 호출하는데 이는 새로운 프로그램의 실행 파일 경로, 명령줄 인자, 환경 변수를 인자로 받으며 현재 프로그램을 종료하고 인자로 받은 정보를 바탕으로 새로운 프로그램을 실행한다.
3. 프로세스 운영
프로세스의 상태는 총 5가지이며 프로세스 서술자의 state 항목을 따른다.
1) 프로세스 상태
a. TASK_RUNNING
프로세스가 실행 가능한 상태이다. 현재 실행 중이거나, 실행 되기 위해 실행 대기열에 있다.
사용자 공간에서 실행된 프로세스는 이 상태만 가질 수 있따. 커널 공간에서 실행 중인 프로세스도 이 상태에 속한다.
b. TASK_INTERRUPTIBLE
프로세스가 특정 조건이 발생하기를 기다리며 중단된 상태이다. 기다리고 있던 조건이 TRUE가 되면 커널은 프로세스의 상태를 TASK_RUNNING로 바꾼다. 프로세스가 시그널을 받은 경우에는 조건에 상관없이 실행 가능한 상태로 바뀐다.
c. TASK_UNINTERRUPTIBLE
시그널을 받아도 실행 가능상태로 바뀌지 않는 다는점만 제외하면 TASK_INTERRUPTIBLE과 같다.
프로세스가 방해받지 않고 특정 조건을 기다려야하는 경우, 대부분 기다리는 조건이 금방 발생하는 경우에 사용한다.
d. __TASK_TRACED
디버거등 다른 프로세스가 ptrace를 통해 해당 프로세스를 추적하는 형태이다.
e. __TASK_STOPPED
프로세스 실행이 정지된 상태로, 실행중도 아니고 실행 가능한 상태도 아니다.
위의 프로세스 상태에 따라 상태 흐름도를 그려본다면 아래와 같다
2) 프로세스 상태 흐름도
3) 스레드 구현
리눅스에서는 윈도우나 솔라리스처럼 커널에서 별도의 스레드(lightweight process)를 지원하는 것과 방식이 크게다르다.
리눅스에서 스레드는 다른 프로세스와 자원을 공유하는 프로세스이다. 그렇다면 다른 시스템에 비해서 무거운게 아닌가 하는 생각이 들 수 있지만 리눅스에서 프로세스는 이미 충분히 경량이기 때문에 그런 방식을 차용한게 아닌가 싶다.
따라서 스레드가 4개라면 task_struct가 4개이다.
※ 커널 스레드
커널도 일부 동작을 백그라운드에서 실행하는 것이 좋을 때가 있다.
(USB 허브에 연결되는것을 감지하는 기능이라던지) 이럴때 커널 공간에서만 존재하는 표준 프로세스인 커널 스레드를 이용해서 이런 작업을 수행한다.
이러한 커널 스레드는 주소 공간이 없다는 점이 일반 스레드와는 다르다.
커널 공간에서만 구동하기 때문에 사용자 공간으로 컨텍스트 스위칭은 일어나지 않지만 스케줄링의 대상은 되기 때문에 스케줄링 되며 선점도 가능하다.
4. 프로세스 삭제
작업을 모두 마친 프로세스는 exit() 시스템 호출을 통해 종료된다.
정확하게는 명시적으로 exit() 함수를 호출하거나 main 함수 반환시 묵시적으로 exit() 함수를 반환하는데, 이 경우는 컴파일러가 넣어주는 경우이다. exit() 함수는 프로세스를 종료하고 task_struct, therad_info, 커널 스택를 제외한 나머지 자원들을 회수한다. 이후 자원이 회수된 프로세스는 최상위 프로세스인 init을 부모 프로세스로 지정하게 된다. 이후 완전히 종료되기를 기다리는 EXIT_ZOMEBIE 상태가 되는데 부모 프로세스가 해당 정보를 처리하거나 커널이 정보가 더 이상 필요없다고 알려주면 나머지 메모리도 완전히 반환된다.
참고문헌
- 리눅스 커널 심층분석 (에이콘 임베디드 시스템프로그래밍 시리즈 33, 로버트 러브 저자(글) · 황정동 번역)
- 문린이의 블로그 - 리눅스 구조 (프로세스 관리)