리눅스의 프로세스(process) 는 컴퓨터 내에서 프로그램을 실행하는 주체가 된다. computer science 에서 중요한 컨셉인 "OS"에서 다루는 process와 thread에 대해 알아보고, 리눅스에서 프로세스의 기본적인 관리법과 job control에 대해 체크하고 Automatic tasks, ssh 등 리눅스를 더 고차원적으로 활용할 수 있는 방법을 알아보자.
OS에서 프로세스에 대한 이해를 가볍게 먼저 이해하자
프로세스(process) 는 컴퓨터 내에서 "프로그램을 실행하는 주체"가 된다.
프로세스는 리눅스 시스템에서 메모리에 적재되어 실행되고 있는 모든 프로그램이 프로세스다.
OS는 프로세스에 실제 메모리의 일부를 "가상 메모리(virtual memory)로 떼어서" 제공함으로써, 프로세스가 컴퓨터의 메모리 전체에 접근할 수는 없도록 하고 다른 프로세스의 메모리 또는 운영체제 자체가 사용하고 있는 커널 메모리를 훔쳐볼 수 없도록 한다.
이를 위해 OS가 제공하는 "프로세스 격리(process isolation)" 가 있다. 다른 프로세스의 메모리에는 접근할 수 없기 때문에 프로세스들이 서로 소통하기 위해서는 별도로 허용된 프로세스 간 통신 (Inter-Process Communication, IPC) 기법 들을 사용해야 한다.
프로세스는 여러 가지 자원을 사용하게 되는데, 해당 명령을 수행하기 위해 운영체제에 따라 CPU를 점유 할 수 있다. 명령어와 데이터를 저장하기 위해 물리적인 메모리를 사용한다. 프로세스는 운영체제의 제어를 받으면서 실행(Running), 대기(Waiting), 중단(Stopped), 좀비(Zombie) 중 한 상태에 있게 된다.
프로세스는 Fork (OS 컨셉)
을 이용해 자신을 복사하여 자식 프로세스를 만든다.
이러한 프로세스는 프로그램에 사용되는 데이터와 메모리 등의 자원 그리고 스레드로 구성될 수 있다.
모든 프로그램은 실행될 때 하나 이상의 프로세스를 가지고, 병행적으로 실행이 가능하다.
커널에 의해 관리되며 모든 프로세스에는 소유자가 있다.
Ready State
로, 할당받을 수 있는 메모리가 없다면 Suspended Ready State
로 대기하게 된다.Running state
로 가게 되며 이를 "scheduled, dispatch" 되었다고 표현한다.필요한 자원 모두(CPU, 메모리 등)을 할당 받아 실행 중인 상태를 의미한다.
CPU를 차지하고 있으며 실제 작업이 실행된다. 무한히 계속해서 CPU를 점유하고 Running 으로 있는 것이 아니라 상태가 바뀐다.
Ready
상태로 바뀌는 경우는 스케줄링 우선순위 등으로 인해 프로세서(CPU) 할당을 반납하고 다시 프로세서(CPU) 할당을 대기하는 상태이다. 이렇게 바뀌는 것을 "preemption" 되었다고 표현한다.
Asleep(blocked)
상태로 바뀌는 경우는 프로세서(CPU)외 다른 자원을 기다리는 상태를 말합니다. 가장 대표적인 예로 I/O 자원을 할당받기를 기다리고 있는 상태를 말한다. 가볍게 예를 들자면 network I/O가 있다. 이 작업은 외부 응답, 수행 결과를 받아야 진행가능하기 때문에 이 상태로 잠시 가게 된다.
suspended ready
& suspended blocked
상태가 있다. 메모리를 할당받지 못한 상태이거나 빼앗긴 상태를 의미한다.
전자는 다른 자원은 할당받았지만 프로세서와 메모리를 할당받지 못한 상태를 말하며 후자는 프로세스, 메모리, 기타 자원 모두 할당받지 못한 상태를 말한다.
빼앗기는 경우, 전 까지 수행했던 결과를 잠시 저장해 두어야 한다. 이때 swap device에 저장하게 된다. 이를 swapping
이라고 표현하며 "주 기억 장치에서 보조 기억 장치로" 가는 흐름이라고 보면 된다.
메모리를 빼앗기는 것을 swap out
, 복구하는 것을 swap in
이라고 한다.
수행이 모든 끝난 프로세스는 모든 자원(CPU, 메모리 등)을 반납한다. 이 때 커널 내에는 PCB(Process Control Block) 정보만 남아 있다. 이 이유는 커널이 PCB 정보를 수집해서 기억하기 위해서다.
PCB는 프로세스의 상태, 프로그램 카운터, CPU 레지스터, CPU 스케줄링 정보 등 프로세스에 대한 중요한 정보를 포함하고 있다.
Zombie 는 위와 같이 즉시 PCB를 삭제하지 않기 때문에 발생한다. 이는 UNIX 계열 운영체제에서 특정한 상황에서 발생하는 프로세스의 상태이다. 이렇게 종료된 프로세스의 PCB가 아직 시스템에 남아 있는 상태를 "Zombie" 상태라고 한다.
부모 프로세스가 wait()
시스템 호출을 통해 "자식 프로세스의 종료 상태를 확인하면", 운영체제는 해당 "Zombie" 프로세스의 PCB를 삭제한다.
우선 process의 PCB(Process Control Block) 를 알아야 한다. PCB는 운영체제가 프로세스를 제어하기 위해 정보(CPU 레지스터 값들)를 저장해 놓는 곳으로 프로세스의 상태 정보를 저장하는 구조체이다.
운영체제에서 프로세스는 PCB 로 표현된다. PCB는 프로세스 상태 관리와 context switching 을 위해서 필요하다. PCB 는 프로세스의 중요한 정보들을 담고 있으므로 일반 사용자는 접근하지 못하는 보호된 메모리 영역에 존재 한다.
프로세스는 각 고유한 번호를 가지고 있으며 이를 PID(Process Identification Number) 라고 한다. 데비안 리눅스가 부팅될 때에는 모든 프로세스의 최상위 프로세스인 systemd (PID는 1) 이 생성되고 모든 프로세스들은 이 1번 프로세스의 자식 프로세스가 된다. 부모 프로세스의 PID를 줄여서 PPID 라고 한다.
리눅스에서 PID 1번은 init
프로세스, 2번은 kthreadd
(커널 스레드 데몬) 프로세스가 실행한다. 즉, init 프로세스는 나머지 모든 시스템 프로세스의 부모, kthreadd 프로세스는 모든 스레드의 부모 프로세스 가 된다.
CPU는 "기계어를 한 단위씩(클럭에 따라) 읽어서" 처리하는데, 프로세스를 실행하기 위해 다음으로 실행할 기계어가 저장된 메모리 주소를 가리키는 값을 알아야 한다.
즉 PC(Program Counter)는 CPU 내에 있는 레지스터 중 하나로, 현재 실행 중인 명령어의 주소를 가리키는 역할 을 한다.
OS는 프로세스를 효율적으로 실행하기 위해 작업 스케쥴링과 우선순위를 관리한다. 그에 맞게 프로세스는 자신이 어떻게 스케쥴링되고 자신이 가지는 우선순위 정보를 가지고 있다. Operating System - Process Scheduling, CPU Scheduling in Operating Systems
두 개의 글을 추천한다.
context switch, 작업 큐, 선점 비선점, 스케쥴링 알고리즘에 대한 내용이 주된 내용이다.
context switching은 위 그림과 같이 흘러가며, 글로 표현하면 다음과 같다.
요청 발생
: 인터럽트나 트렙에 의해서 컨텍스트를 바꿔야 한다는 요청이 들어옴 -> PCB 에 프로세스 정보 저장
: 기존에 실행중이던 프로세스 P0 와 관련된 정보들을 PCB 에 저장함 -> CPU 새롭게 할당
: 운영체제는 새롭게 실행할 프로세스 P1 에 대한 정보를 해당 PCB 에서 가져와 CPU 레스터에 적재함
사용자가 작성한 프로그램 함수들의 코드가 CPU 에서 수행할 수 있는 기계어 명령 형태로 변환되어 저장되는 공간이다. 즉, 실행할 프로그램의 코드가 저장되며 CPU는 이 영역에서 명령어를 하나씩 가져와 처리하게 된다.
Compile 시점에 결정되며 중간에 코드를 바꿀 수 없도록 Read-only로 되어있다.
전역 변수(global variable) 또는 static 변수 등 프로그램이 사용하는 데이터를 저장하는 공간이다. 프로그램이 시작될 때 할당되어 프로그램 종료 시 소멸된다.
전역 변수 또는 static 값을 참조한 컴파일이 완료되면 Data 영역의 주소값 가리키도록 바뀐다. 전역변수가 바뀔 것을 고려해 Read-Write 로 되어있다.
"동적 데이터 영역" 이다. 메모리 주소 값에 의해서만 참조되고 사용되는 영역이다. 따라서, 프로그램 동작 시(런타임)에 크기가 결정되며 프로그래머가 필요할 때마다 사용하는 메모리 영역이다.
java
를 예로 들자면 객체가 heap 영역에 생성되고 GC(Garbage Collection) 에 의해서 정리되는 영역이다. OOM error를 마주하면 본능적으로 가장 먼저 체크하게 되는 영역이다.
호출된 함수의 수행을 마치고 복귀할 주소 및 데이터(지역변수, 매개변수, 리턴값 등) 등 잠시 사용되었다가 사라지는 데이터를 저장하는 영역이다. 함수 호출 시 할당되고 함수 반환 시 소멸되며 "로드 시(컴파일 타임)" 크기가 결정된다.
컴파일시 Stack의 영역의 크기가 결정되기 때문에 무한정 할당할 수 없습니다. 재귀(recursive) 함수를 무한히 반복하면 stack overflow가 발생하는 이유이기도 하다.
우리가 일반적으로 작성하는 코드로 만들어진 어플리케이션은 컴파일 할 때 code, data, stack 영역의 크기를 계산해 메모리 영역을 결정한다.
메모리 구조는 크게 커널 주소 공간(kernel space)
, 사용자 주소 공간
으로 분리 할 수 있다. 이때 커널 부분은 사용자가 접근 할 수 없다.
그림 기준으로 stack 아래에 있다고 보면 된다.
요즘의 프로그램은 매우 복잡해지면서 하나의 프로세스만 가지고 실행하기에는 벅찬 상황이 오게 되었다. "한 프로그램을 처리하기 위해 여러 프로세스를 사용 하고 싶다" 라는 생각으로 동시성과 병렬성에 대한 고민을 시작하게 된다.
하지만 운영체제는 운영의 안정성을 위하여 각 프로세스마다 자신에게 할당된 메모리 내의 정보에만 접근할 수 있도록 제약을 두고 있었기 때문에 하나의 어플리케이션에서 여러개의 프로세스를 사용하게 되면 많은 제약조건이 생기는 한계가 있었다.
그래서 "프로세스 내에 메모리를 공유하면서 여러가지 일을 할 수 있게" 하기 위해 "쓰레드" 가 만들어진다.
쓰레드란 프로세스 내에서 실제로 작업을 수행하는 주체를 의미한다. 모든 프로세스는 한 개 이상의 쓰레드가 존재하여 작업을 수행하게 되고, 두 개 이상의 쓰레드를 가지는 프로세스를 멀티 쓰레드 프로세스라고 한다.
쓰레드는 특정한 시점에 프로그램의 특정한 부분을 수행하게 된다. 이러한 쓰레드를 사용하여 프로그램의 여러 부분을 동시에 수행시킬 수 있다. 즉 프로그램의 여러 부분을 수행하는 각각을 쓰레드라고 하게 된다.
이러한 쓰레드는 독립적으로 수행되기 위한 두 가지 정보를 가지고 있다.
Program Counter
: 프로그램의 어느 부분을 실행하고 있는지에 대한 정보를 저장Stack
: 함수를 호출하는 순서(Function Call)에 대한 정보를 저장프로세스는 독립된 각각의 프로세스들이 독립된 영역을 가지고 있는 반면, 쓰레드는 하나의 프로세스 내에서 여러개 존재하므로 그 프로세스에 있는 메모리 공간을 서로 "공유" 하게 된다. 그래서 메모리 공간을 독립적으로 가지고 있을 필요가 없으므로 Light weight process 라고도 한다.
싱글 쓰레드에 비해 멀티 쓰레드는 쓰레드가 세 개인 프로세스를 나타내고 있는데, 이 세 개의 쓰레드는 "데이터와 피일들을 공유" 하고 있고 동시에 독립된 registers와 stack 메모리 공간을 할당 받았다.
이렇듯 쓰레드는 하나의 주소공간에 존재하며 코드의 여러 부분을 동시에 수행하고 있다. 이 쓰레드들이 각각 수행되기 위해서 CPU의 프로그램 카운터와 스택을 가지고 있어야 하는 것이다.
ps
: 실행중인 프로세스의 정보를 출력해 준다.kill
: 프로세스를 종료시키는 명령어다. 포그라운드(Foreground) : 리눅스에서 실행시키는 거의 모든 명령어는 포그라운드. 앞에서(보고 있는 화면에서) 작동하는 것.
백그라운드(Background) : 뒤에서 작동하는 것. 보고있지 않는 상태에서 돌아가는 프로세스. -> 대표적으로 명령어에 &
를 붙이면 된다.
백그라운드로 실행되는 작업을 보여주는 명령어. job은 프로세스와 달리 터미널 명령을 통한 작업만을 의미한다.
job을 통해 프로세스를 실행할 수 있지만 터미널이 종료되면 job과 함께 프로세스도 종료. 터미널에 의존적이다. 각각의 "터미널 마다 job은 따로 존재"한다.
job을 활용하여 포그라운드와 백그라운드를 효율적으로 관리할 수 있다.