리눅스 - 프로세스(Process)와 쓰레드(Thread) & linux ps, 작업(job) handling

정현우·2021년 7월 14일
4

Linux Basic to Advanced

목록 보기
4/16
post-thumbnail

프로세스(Process)와 쓰레드(Thread)

리눅스의 프로세스(process) 는 컴퓨터 내에서 프로그램을 실행하는 주체가 된다. computer science 에서 중요한 컨셉인 "OS"에서 다루는 process와 thread에 대해 알아보고, 리눅스에서 프로세스의 기본적인 관리법과 job control에 대해 체크하고 Automatic tasks, ssh 등 리눅스를 더 고차원적으로 활용할 수 있는 방법을 알아보자.

1. 프로세스(Process)

1) 프로세스란?

  • OS에서 프로세스에 대한 이해를 가볍게 먼저 이해하자

  • 프로세스(process) 는 컴퓨터 내에서 "프로그램을 실행하는 주체"가 된다.

    • 프로그램은 명칭하는 분류가 다르다.
    • 프로세스가 해야 할 일을 programming 하여 명시한 것이 프로그램이다.
  • 프로세스는 리눅스 시스템에서 메모리에 적재되어 실행되고 있는 모든 프로그램이 프로세스다.

  • OS는 프로세스에 실제 메모리의 일부를 "가상 메모리(virtual memory)로 떼어서" 제공함으로써, 프로세스가 컴퓨터의 메모리 전체에 접근할 수는 없도록 하고 다른 프로세스의 메모리 또는 운영체제 자체가 사용하고 있는 커널 메모리를 훔쳐볼 수 없도록 한다.

  • 이를 위해 OS가 제공하는 "프로세스 격리(process isolation)" 가 있다. 다른 프로세스의 메모리에는 접근할 수 없기 때문에 프로세스들이 서로 소통하기 위해서는 별도로 허용된 프로세스 간 통신 (Inter-Process Communication, IPC) 기법 들을 사용해야 한다.

2) 프로세스 특징

  • 프로세스는 여러 가지 자원을 사용하게 되는데, 해당 명령을 수행하기 위해 운영체제에 따라 CPU를 점유 할 수 있다. 명령어와 데이터를 저장하기 위해 물리적인 메모리를 사용한다. 프로세스는 운영체제의 제어를 받으면서 실행(Running), 대기(Waiting), 중단(Stopped), 좀비(Zombie) 중 한 상태에 있게 된다.

  • 프로세스는 Fork (OS 컨셉)을 이용해 자신을 복사하여 자식 프로세스를 만든다.

  • 이러한 프로세스는 프로그램에 사용되는 데이터와 메모리 등의 자원 그리고 스레드로 구성될 수 있다.

  • 모든 프로그램은 실행될 때 하나 이상의 프로세스를 가지고, 병행적으로 실행이 가능하다.

  • 커널에 의해 관리되며 모든 프로세스에는 소유자가 있다.

3) 프로세스 상태

(1) Created(New)

  • process가 생성되는 단계. 실행 중인 것은 아니며 가용 메모리 공간 유무에 따라 메모리가 할당된다면 Ready State 로, 할당받을 수 있는 메모리가 없다면 Suspended Ready State 로 대기하게 된다.

(2) Ready

  • 프로세서(CPU) 할당을 제외한 모든 자원(메모리 등)을 할당 받은 상태를 의미한다. CPU 자원 할당을 받으면 즉시 실행이 가능한 상태이다. CPU 자원 할당을 받으면 Running state 로 가게 되며 이를 "scheduled, dispatch" 되었다고 표현한다.

(3) Running

  • 필요한 자원 모두(CPU, 메모리 등)을 할당 받아 실행 중인 상태를 의미한다.

  • CPU를 차지하고 있으며 실제 작업이 실행된다. 무한히 계속해서 CPU를 점유하고 Running 으로 있는 것이 아니라 상태가 바뀐다.

  • Ready 상태로 바뀌는 경우는 스케줄링 우선순위 등으로 인해 프로세서(CPU) 할당을 반납하고 다시 프로세서(CPU) 할당을 대기하는 상태이다. 이렇게 바뀌는 것을 "preemption" 되었다고 표현한다.

  • Asleep(blocked) 상태로 바뀌는 경우는 프로세서(CPU)외 다른 자원을 기다리는 상태를 말합니다. 가장 대표적인 예로 I/O 자원을 할당받기를 기다리고 있는 상태를 말한다. 가볍게 예를 들자면 network I/O가 있다. 이 작업은 외부 응답, 수행 결과를 받아야 진행가능하기 때문에 이 상태로 잠시 가게 된다.

(4) Suspended state

  • suspended ready & suspended blocked 상태가 있다. 메모리를 할당받지 못한 상태이거나 빼앗긴 상태를 의미한다.

  • 전자는 다른 자원은 할당받았지만 프로세서와 메모리를 할당받지 못한 상태를 말하며 후자는 프로세스, 메모리, 기타 자원 모두 할당받지 못한 상태를 말한다.

  • 빼앗기는 경우, 전 까지 수행했던 결과를 잠시 저장해 두어야 한다. 이때 swap device에 저장하게 된다. 이를 swapping 이라고 표현하며 "주 기억 장치에서 보조 기억 장치로" 가는 흐름이라고 보면 된다.

  • 메모리를 빼앗기는 것을 swap out, 복구하는 것을 swap in 이라고 한다.

(5) Terminated / Zombie

  • 수행이 모든 끝난 프로세스는 모든 자원(CPU, 메모리 등)을 반납한다. 이 때 커널 내에는 PCB(Process Control Block) 정보만 남아 있다. 이 이유는 커널이 PCB 정보를 수집해서 기억하기 위해서다.

  • PCB는 프로세스의 상태, 프로그램 카운터, CPU 레지스터, CPU 스케줄링 정보 등 프로세스에 대한 중요한 정보를 포함하고 있다.

  • Zombie 는 위와 같이 즉시 PCB를 삭제하지 않기 때문에 발생한다. 이는 UNIX 계열 운영체제에서 특정한 상황에서 발생하는 프로세스의 상태이다. 이렇게 종료된 프로세스의 PCB가 아직 시스템에 남아 있는 상태를 "Zombie" 상태라고 한다.

  • 부모 프로세스가 wait() 시스템 호출을 통해 "자식 프로세스의 종료 상태를 확인하면", 운영체제는 해당 "Zombie" 프로세스의 PCB를 삭제한다.

4) 프로세스의 구성요소

  • 더 깊게 들어가면 자연스럽게 OS에 대한 얘기가 된다. (물론 프로세스 자체가 OS에 대한 얘기이기도 하다.) 그리고 구성요소 하나 하나가 책이 있을 만큼 중요한 내용이기도 하다. 가볍게 핵심 구성요소 위주로 살펴보자.

(1) PCB

  • 우선 process의 PCB(Process Control Block) 를 알아야 한다. PCB는 운영체제가 프로세스를 제어하기 위해 정보(CPU 레지스터 값들)를 저장해 놓는 곳으로 프로세스의 상태 정보를 저장하는 구조체이다.

  • 운영체제에서 프로세스는 PCB 로 표현된다. PCB는 프로세스 상태 관리와 context switching 을 위해서 필요하다. PCB 는 프로세스의 중요한 정보들을 담고 있으므로 일반 사용자는 접근하지 못하는 보호된 메모리 영역에 존재 한다.

(2) PID

  • 프로세스는 각 고유한 번호를 가지고 있으며 이를 PID(Process Identification Number) 라고 한다. 데비안 리눅스가 부팅될 때에는 모든 프로세스의 최상위 프로세스인 systemd (PID는 1) 이 생성되고 모든 프로세스들은 이 1번 프로세스의 자식 프로세스가 된다. 부모 프로세스의 PID를 줄여서 PPID 라고 한다.

  • 리눅스에서 PID 1번은 init 프로세스, 2번은 kthreadd (커널 스레드 데몬) 프로세스가 실행한다. 즉, init 프로세스는 나머지 모든 시스템 프로세스의 부모, kthreadd 프로세스는 모든 스레드의 부모 프로세스 가 된다.

(3) PC

  • CPU는 "기계어를 한 단위씩(클럭에 따라) 읽어서" 처리하는데, 프로세스를 실행하기 위해 다음으로 실행할 기계어가 저장된 메모리 주소를 가리키는 값을 알아야 한다.

  • PC(Program Counter)는 CPU 내에 있는 레지스터 중 하나로, 현재 실행 중인 명령어의 주소를 가리키는 역할 을 한다.

(4) 스케쥴링과 우선순위

  • OS는 프로세스를 효율적으로 실행하기 위해 작업 스케쥴링과 우선순위를 관리한다. 그에 맞게 프로세스는 자신이 어떻게 스케쥴링되고 자신이 가지는 우선순위 정보를 가지고 있다. Operating System - Process Scheduling, CPU Scheduling in Operating Systems
    두 개의 글을 추천한다.

  • context switch, 작업 큐, 선점 비선점, 스케쥴링 알고리즘에 대한 내용이 주된 내용이다.

(5) 문맥, 문맥 교환

  • PCB에 프로세스의 "문맥(Context)" 이 저장되어 있고, Context Switching은 CPU가 현재 작업중인 프로세스에서 다른 프로세스로 넘어갈 때, 이전의 프로세스 정보를 PCB에 저장하고 새롭게 실행할 프로세스의 정보를 PCB에서 읽어와 레지스터에 적재하는 과정을 말한다.

  • context switching은 위 그림과 같이 흘러가며, 글로 표현하면 다음과 같다.

  • 요청 발생: 인터럽트나 트렙에 의해서 컨텍스트를 바꿔야 한다는 요청이 들어옴 -> PCB 에 프로세스 정보 저장: 기존에 실행중이던 프로세스 P0 와 관련된 정보들을 PCB 에 저장함 -> CPU 새롭게 할당: 운영체제는 새롭게 실행할 프로세스 P1 에 대한 정보를 해당 PCB 에서 가져와 CPU 레스터에 적재함

(6) 메모리

  • 프로세스는 모두 자기 자신의 메모리를 가지게 된다. 참조, 개발자에게는 이 부분이 꽤 중요하다. 다음 섹션에서 더 자세하게 살펴보자.

5) 프로세스 메모리 구조

(1) Text(Code) 영역

  • 사용자가 작성한 프로그램 함수들의 코드가 CPU 에서 수행할 수 있는 기계어 명령 형태로 변환되어 저장되는 공간이다. 즉, 실행할 프로그램의 코드가 저장되며 CPU는 이 영역에서 명령어를 하나씩 가져와 처리하게 된다.

  • Compile 시점에 결정되며 중간에 코드를 바꿀 수 없도록 Read-only로 되어있다.

(2) Data 영역

  • 전역 변수(global variable) 또는 static 변수 등 프로그램이 사용하는 데이터를 저장하는 공간이다. 프로그램이 시작될 때 할당되어 프로그램 종료 시 소멸된다.

  • 전역 변수 또는 static 값을 참조한 컴파일이 완료되면 Data 영역의 주소값 가리키도록 바뀐다. 전역변수가 바뀔 것을 고려해 Read-Write 로 되어있다.

(3) BSS 영역

  • Block Stated Symbol 영역이며 초기화 되지 않은 전역변수가 저장된다. 초기화 된 전역변수는 Data 영역에 저장되어 비휘발성 메모리인 ROM에 저장되는데 이 부분은 비용이 많이 들어 RAM에 저장될 것과 ROM에 저장될 것을 구분하기 위해 영역을 구분해 사용한다.

(3) Heap 영역

  • "동적 데이터 영역" 이다. 메모리 주소 값에 의해서만 참조되고 사용되는 영역이다. 따라서, 프로그램 동작 시(런타임)에 크기가 결정되며 프로그래머가 필요할 때마다 사용하는 메모리 영역이다.

  • java 를 예로 들자면 객체가 heap 영역에 생성되고 GC(Garbage Collection) 에 의해서 정리되는 영역이다. OOM error를 마주하면 본능적으로 가장 먼저 체크하게 되는 영역이다.

(4) Stack 영역

  • 호출된 함수의 수행을 마치고 복귀할 주소 및 데이터(지역변수, 매개변수, 리턴값 등) 등 잠시 사용되었다가 사라지는 데이터를 저장하는 영역이다. 함수 호출 시 할당되고 함수 반환 시 소멸되며 "로드 시(컴파일 타임)" 크기가 결정된다.

  • 컴파일시 Stack의 영역의 크기가 결정되기 때문에 무한정 할당할 수 없습니다. 재귀(recursive) 함수를 무한히 반복하면 stack overflow가 발생하는 이유이기도 하다.

  • 우리가 일반적으로 작성하는 코드로 만들어진 어플리케이션은 컴파일 할 때 code, data, stack 영역의 크기를 계산해 메모리 영역을 결정한다.

(5) 커널 주소 공간

  • 메모리 구조는 크게 커널 주소 공간(kernel space), 사용자 주소 공간 으로 분리 할 수 있다. 이때 커널 부분은 사용자가 접근 할 수 없다.

  • 그림 기준으로 stack 아래에 있다고 보면 된다.


2. 쓰레드(Thread)

1) 쓰레드란?

  • 요즘의 프로그램은 매우 복잡해지면서 하나의 프로세스만 가지고 실행하기에는 벅찬 상황이 오게 되었다. "한 프로그램을 처리하기 위해 여러 프로세스를 사용 하고 싶다" 라는 생각으로 동시성과 병렬성에 대한 고민을 시작하게 된다.

  • 하지만 운영체제는 운영의 안정성을 위하여 각 프로세스마다 자신에게 할당된 메모리 내의 정보에만 접근할 수 있도록 제약을 두고 있었기 때문에 하나의 어플리케이션에서 여러개의 프로세스를 사용하게 되면 많은 제약조건이 생기는 한계가 있었다.

  • 그래서 "프로세스 내에 메모리를 공유하면서 여러가지 일을 할 수 있게" 하기 위해 "쓰레드" 가 만들어진다.

  • 쓰레드란 프로세스 내에서 실제로 작업을 수행하는 주체를 의미한다. 모든 프로세스는 한 개 이상의 쓰레드가 존재하여 작업을 수행하게 되고, 두 개 이상의 쓰레드를 가지는 프로세스를 멀티 쓰레드 프로세스라고 한다.

2) 쓰레드 특징

  • 쓰레드는 특정한 시점에 프로그램의 특정한 부분을 수행하게 된다. 이러한 쓰레드를 사용하여 프로그램의 여러 부분을 동시에 수행시킬 수 있다. 즉 프로그램의 여러 부분을 수행하는 각각을 쓰레드라고 하게 된다.

  • 이러한 쓰레드는 독립적으로 수행되기 위한 두 가지 정보를 가지고 있다.

    • Program Counter: 프로그램의 어느 부분을 실행하고 있는지에 대한 정보를 저장
    • Stack: 함수를 호출하는 순서(Function Call)에 대한 정보를 저장
  • 프로세스는 독립된 각각의 프로세스들이 독립된 영역을 가지고 있는 반면, 쓰레드는 하나의 프로세스 내에서 여러개 존재하므로 그 프로세스에 있는 메모리 공간을 서로 "공유" 하게 된다. 그래서 메모리 공간을 독립적으로 가지고 있을 필요가 없으므로 Light weight process 라고도 한다.

3) 쓰레드 구성요소

  • 프로세스는 PCB가 있고 쓰레드는 TCB(Thread Control Block) 이 존재한다. 쓰레드는 기본적으로 프세스에 귀속되기 때문에 PCB와 TCB의 관계는 아래 그림과 같다.

  • TCB는 아래와 같은 구성요소를 포함하고 있다.
  1. Thread Identifier: 쓰레드를 구분하는 유일한 식별자
  2. Stack pointer: 쓰테드 별로 고유한 Stack의 pointer
  3. Program counter: 현재 instruction의 주소
  4. 쓰레드의 상태 (running, ready, waiting, start, done)
  5. Thread's register values
  6. 쓰레드가 소속된 processor의 PCB주소
  • 보통 TCB는 커널 레벨에서 Context Switching의 기본 단위가 되며, 같은 프로세스에서의 스위칭에 대해서는 TCB 정보만 저장하면 된다. 하지만 다른 프로세스 간의 스위칭을 할 때에는 PCB / TCB 정보를 모두 저장해야 한다.

4) 싱글 쓰레드와 멀티 쓰레드

  • 아래 그림은 싱글 쓰레드와 멀티 쓰레드의 차이점이다.

  • 싱글 쓰레드에 비해 멀티 쓰레드는 쓰레드가 세 개인 프로세스를 나타내고 있는데, 이 세 개의 쓰레드는 "데이터와 피일들을 공유" 하고 있고 동시에 독립된 registers와 stack 메모리 공간을 할당 받았다.

  • 이렇듯 쓰레드는 하나의 주소공간에 존재하며 코드의 여러 부분을 동시에 수행하고 있다. 이 쓰레드들이 각각 수행되기 위해서 CPU의 프로그램 카운터와 스택을 가지고 있어야 하는 것이다.


3. 리눅스에서 프로세스 관련 명령어

1) 조회와 종료

  • ps: 실행중인 프로세스의 정보를 출력해 준다.

ps

  • 기본적으로 ps만 치면 아래 4개가 출력된다.
  1. PID
  2. TTY : 현재 수행중인 터미널의 번호
  3. TIME : 프로세스가 CPU사용한 시간 -> 생성되고 얼마나 지났는지
  4. CMD : 프로세스의 이름
  • 여기에 -ef 옵션을 붙이면 아래 3가지가 더 뜬다.
  1. UID : 프로세스 실행한 유저아이디
  2. PPID : 부모 -> PID과 1, 2는 PPID가 0 -> 부모가 없다.
  3. STIME : 프로세스가 시작한 날짜
  • -aux 옵션을 모두 붙이면 아래 사진과 같다 => 계속 새로고침 된다.

aux option

  • kill: 프로세스를 종료시키는 명령어다.

kill

2) 백그라운드(Background)와 포그라운드(Foreground)

  • 포그라운드(Foreground) : 리눅스에서 실행시키는 거의 모든 명령어는 포그라운드. 앞에서(보고 있는 화면에서) 작동하는 것.

  • 백그라운드(Background) : 뒤에서 작동하는 것. 보고있지 않는 상태에서 돌아가는 프로세스. -> 대표적으로 명령어에 &를 붙이면 된다.

  • How to Foreground a Background Process in Linux 글을 추천한다.

3) job

  • 백그라운드로 실행되는 작업을 보여주는 명령어. job은 프로세스와 달리 터미널 명령을 통한 작업만을 의미한다.

  • job을 통해 프로세스를 실행할 수 있지만 터미널이 종료되면 job과 함께 프로세스도 종료. 터미널에 의존적이다. 각각의 "터미널 마다 job은 따로 존재"한다.

  • job을 활용하여 포그라운드와 백그라운드를 효율적으로 관리할 수 있다.

4) at

  • 지정된 시간에 1회 실행되는 작업 명령어. 시간이 되면 수행되고 작업 리스트에서 사라진다.

at

  • atq는 현재 실행 예약이된 at의 리스트를 보여준다.

atq

  • atrm 삭제를 원하는 at 넘버를 뒤에 붙여 삭제가 가능하다. 실행 예시는 아래와 같다.

at ex

5) cron

  • 지정된 시간에 1회 실행되는 at와는 달리, 지정된 시간에 따라 '주기적'으로 실행 하는 작업이다.

crontab

crontab ex


출처

profile
도메인 중심의 개발, 깊이의 가치를 이해하고 “문제 해결” 에 몰두하는 개발자가 되고싶습니다. 그러기 위해 항상 새로운 것에 도전하고 노력하는 개발자가 되고 싶습니다!

0개의 댓글