| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
- 코테
- Spring
- 코딩테스트
- 백준 파이썬
- 프로세스
- 백준 15686
- 파이썬
- 백준 14499
- Spring Security
- 백준 2302
- 백준
- 락
- 프로그래머스
- 운영체제
- 후위 표기식
- docker
- CS
- 도커
- 자바스크립트
- 백준 1120
- CI/CD
- 스프링
- 백준 10819
- 증가하는부분수열
- 9465 스티커
- 다익스트라
- 스레드
- 네이버
- 백준 2529
- 백트래킹
- Today
- Total
개발
운영체제 3 본문
프로세스
프로세스는 컴퓨터 시스템에서 실행하는 주체이며 메모리, CPU 같은 자원을 할당받는 주체이다.
운영체제 수업을 듣다보면 운영체제에게 부탁한다, 운영체제가 해준다라는 표현을 자주 듣게 되는데, 이것은 운영체제가 프로세스로서 동작하는 것으로 오해할 수 있다. 하지만 운영체제는 메인 메모리의 어딘가에 코드와 데이터 형태로 존재하고, 현재 실행중인 프로세스가 운영체제 코드를 실행하는 것이다.
다중 프로그래밍

다중 프로그래밍은 Multiprogramming이라고 부르기도 하고 CPU가 여러 개의 프로세스를 병행 실행하는 것을 의미한다. 다중 프로그래밍은 여러 프로세스가 동시에 실행 되는 것과 같은 효과를 주는데 하지만 어느 시점이든 하나의 프로그램만 실행중이며, CPU의 처리 속도가 매우 빠르기 때문이다. 다중 프로그래밍을 사용하여 컴퓨터 시스템을 구축하는 이유는 우리가 사용하는 대부분의 프로그램은 동작 시간의 많은 시간을 입출력을 기다리는것에 사용하기 때문이다. 다중 프로그래밍을 사용하지 않는다면 CPU를 사용중인 프로세스가 입출력을 받거나 오래 걸리는 작업을 필요로 할 때 CPU는 동작하지 않게 되고 효율적이지 않게 된다.
프로세스의 생성
프로세스의 생성을 이해하기 위해 우리는 부모 프로세스와 자식 프로세스에 대해서 알아야 한다. 유닉스 기반의 운영체제에서는 최초의 프로세스인 0번 프로세스를 제외하고 모든 프로세스는 부모 프로세스가 시스템 호출로 만든 프로세스이다. 이런 프로세스들을 자식 프로세스라고 하고, 프로세스는 다른 프로세스들의 부모일수도 자식일수도, 둘 다 일수도 있다. 즉 새 프로세스를 만드는 주체는 기존의 프로세스이다. 프로세스는 트리구조로 형성된다. 유닉스 기반 운영체제인 경우 자식은 fork() 시스템 호출로 부모 프로세스를 복제하고 exec() 시스템 호출로 새로운 프로그램을 덮어 씌운다. fork() 시스템 호출을 하면 부모 프로세스의 프로그램 카운터 역시 복제하기 때문에 부모가 실행했던 부분은 실행하지 않는다. 부모 프로세스가 자식 프로세스가 실행이 다 끝날때까지 실행을 멈추는 wait() 시스템 호출도 있다.
프로세스의 실행 원리
프로그램은 어떻게 실행할까? 일반적으로 우리가 사용하는 그래픽 유저 인터페이스에서 프로그램을 실행하기 위해, 우리는 해당 프로그램의 아이콘을 더블 클릭한다. 아이콘이 실행되면 아이콘에 해당되는 실행파일에서 코드 데이터를 가져오고 힙, 스택에 메모리를 할당해서 수행 이미지의 형태를 만든 후 메모리에 적재한다. 그래픽 유저 인터페이스 프로그램이 시스템 호출을 한 후 운영체제 코드를 실행해서 새로운 프로그램의 부모 프로세스가 되는 것이다.

프로세스의 실행 원리를 이해하기 위해 다중 프로그래밍을 사용하는 컴퓨터 시스템에 A, B, C라는 3개의 프로세스가 실행중이라고 하자. 여기서 Dispatcher는 스케쥴링을 위한 운영체제 코드이다. 프로세스 A는 5000번지, B는 8000번지, C는 12000번지에서 시작한다.

5000번지에 존재하는 명령어가 실행되는 것으로보아 현재 CPU를 사용하는 것은 A 프로세스이다. 5005번에 존재하는 명령어를 실행하는 도중 타이머 인터럽트가 발생했다. 5005번의 명령어까지는 실행한 후 프로세스 A는 시스템 호출을 하여 스스로 타이머 인터럽트 서비스 루틴을 실행한다. 타이머 인터럽트 서비스 루틴을 실행한 후 스케쥴링을 하는데, 스케쥴링의 결과 8000번지 즉 프로세스 B를 실행하기로 결정했다. 다른 프로세스를 실행하기로 결정했으므로 우선 이전 프로세스의 문맥(Context) 를 업데이트한다. 만약에 저장된 문맥이 없다면 생성한다. 그 후 Context를 B의 Context로 Switch한다. 스케쥴링 후 CPU는 프로세스 B가 사용한다. 마찬가지로 B는 명령어를 실행하고 인터럽트가 발생하면 스스로 시스템 호출을 하고 인터럽트 서비스 루틴을 실행한다.
시스템 호출이나 인터럽트가 발생한다고 해서 무조건적으로 문맥 교환(Context Switch) 이 발생하는 것은 아니다. 문맥 교환은 프로세스가 변경될 때에만 수행된다. 프로세스가 변경되지 않아도 CPU의 수행 정보 등 Context의 일부를 PCB에 저장해야 하는 오버헤드는 발생한다. 하지만 문맥 교환시 캐시 메모리를 flush해야 되고 이것은 아주 큰 오버헤드가 된다.

문맥은 커널 주소공간의 data에 PCB(Process Control Block)의 자료구조 형태로 저장된다. PCB에는 레지스터 값, 프로그램 카운터 값과 같은 다양한 것들이 저장된다.
프로세스의 종료
프로그램의 종료 역시 운영체제 코드를 실행해서 한다. 프로세스가 마지막 명령을 수행한 후 Exit 시스템 호출을 하게 되면 운영체제가 해당 프로세스를 종료시켜 준다. 종료는 프로세스가 가지고 있던 메모리와 같은 자원들을 반납하고 마지막에 커널의 Data에 있는 PCB를 지우면 비로소 종료가 된다. PCB를 삭제하기 전까지는 프로세스가 종료된 것이 아니며 이 프로세스의 상태를 좀비 프로세스라고 한다. 사용자 응용프로그램이 Exit 시스템 호출을 호출하여 종료하기도 하지만 0으로 나눴을 때, 프로세스를 실행했는데 프로세스의 일부분이 아닌 메모리를 참조하려고 했을 때 CPU는 실행하지 않고 스스로 인터럽트를 걸어서 운영체제 코드로 점프하게 되고 운영체제 코드는 해당 프로세스를 종료한다. 부모 프로세스가 자식 프로세스를 종료하는 경우도 존재하는데, 자식 프로세스의 할당 자원이 한계치를 넘어섰거나 자식에게 할당된 태스크가 더 이상 필요하지 않은 경우이다. 운영체제는 부모 프로세스가 종료되는 경우 자식 프로세스가 더 이상 수행되도록 두지 않는다.
스케쥴러


New 상태는 장기 스케쥴러를 사용하는 컴퓨터 시스템에서 장기 스케쥴러가 프로그램의 실행 후 메모리를 할당할지 하지 않을지 결정하고 승인이 되면 프로세스는 Ready 큐에 들어가있는 Ready 상태가 된다. 큐에 넣는 정보는 해당 프로세스의 PCB이다. 장기 스케쥴러는 메모리에 적재된 프로세스 수를 제어한다. 하지만 시분할 시스템에서는 장기 스케쥴러를 사용하지 않고 New 상태에서 Ready가 되는 것이 아니라 프로그램을 실행하면 곧바로 메모리에 적재하고 Ready 상태가 된다. Ready 상태에 있는 프로세스들은 이미 메모리에 적재가 된 프로그램들이며 CPU의 사용을 기다리고 있다. 단기 스케쥴러는 Ready 큐에 존재하는 프로세스 중에서 CPU를 사용하는 Running 상태로 만들 것인지 결정하는 스케쥴러이다. 시분할 시스템에서 타이머 인터럽트가 발생하면 단기 스케쥴러가 호출이 된다.
중기 스케쥴러는 장기 스케쥴러처럼 메모리에 적재된 프로세스의 수를 조절하는 역할을 한다. 메모리 공간이 부족한 경우 중기 스케쥴러는 프로세스를 통째로 메모리에서 디스크에 swap out 된다. 메모리가 부족해서 swap out을 해야할 필요가 있는 경우 가장 먼저 suspend 상태에 있는 프로세스들을 swap out 한다. suspend 상태에 있는 프로세스들은 메모리를 사용하지 않고 디스크에 swap out 된 상태로 존재한다. 이외에도 사용자가 프로세스를 의도적으로 중지하는 경우에도 suspend 상태가 된다.

디스패쳐 즉 스케쥴러는 레디 큐에 들어 있는 프로세스를 실행한다. 만약 실행중인 프로세스가 입출력을 요구한다면 해당 입출력을 위한 장치에 따로 존재하는 블록 큐(이벤트 큐)에 해당 프로세스를 넣는다. 요청한 입출력이 끝나기 전까지 해당 입출력 장치의 블록 큐에 존재하는 Blocked 상태가 되고, 입출력이 끝나면 입출력 장치가 CPU에 인터럽트를 걸고 CPU는 인터럽트 서비스 루틴을 실행해서 해당 프로세스를 레디 큐에 넣어준다.
스레드

운영체제를 공부할 때 대부분 CPU가 하나인 컴퓨터 시스템이라고 가정하고 진행된다. 이러한 컴퓨터 시스템에서 하나의 프로세스로 동작해도 충분한 것으로 보인다. 하지만 멀티 프로세서 컴퓨터 시스템으로 발전하면서 여러 개의 CPU가 존재하고 여러 개의 프로세스가 동작하지 않으면 CPU를 여러 개를 사용할 수 없는 문제가 발생했다. 프로세스끼리는 데이터르 공유하는 것이 어렵기도 하고 같은 프로그램을 여러번 반복해야 하는 경우 각각 새로운 프로세스를 만든다면 앞서 문맥 교환에서 캐시 메모리를 Flush를 해야하는 큰 오버헤드가 발생하게 되고 이는 스레드를 사용하면 개선할 수 있다. 여태까지 하나의 프로세스가 하나의 실행흐름을 가지고 있었다면 스레드를 사용하는 컴퓨터 시스템에서는 하나의 프로세스에 여러 스레드가 존재하고 여러 개의 실행흐름을 가지고 있게 된다.

스레드의 필요성이 생기면서 스레드의 개념이 없고 하나의 프로세스만 실행하던 유닉스 시스템에서 스레드를 구현하기 위해서는 상당한 양의 코드를 수정해야 할 필요성이 생겼다. 임시 방편으로 유저 레벨 스레드 패키지를 만들어 스레드를 지원했는데 기존과 동일하게 스케쥴러는 프로세스만 신경쓰지만 프로세스 내에 스케쥴링과 같은 운영체제 역할을 하는 작은 운영체제가 들어가 있는 것처럼 구현해서 이 작은 운영체제가 스레드를 스케쥴링 해주는것으로 스레드를 구현했다. 하지만 이 방식은 입출력을 요구하면 Blocked 상태가 존재하는 Blocking 시스템에서 입출력을 요구한 스레드만 Blocked 상태가 되는 것이 아니라 프로세스 전체가 Blocked 상태가 되는 문제점이 있었다. 물론 지금은 유저 레벨이 아닌 커널 레벨 스레드가 지원되기 때문에 상관없다. 커널 레벨 스레드는 운영체제가 스레드의 존재를 알고 스레드 단위로 스케쥴링을 한다.

스레드는 PCB에 존재하는 메모리주소, 프로세스 상태와 같은 여러 자원들을 공유하고 CPU 수행과 관련된 프로그램 카운터, 레지스터들의 내용, 스택들은 스레드마다 별도로 가진다. 다중 스레드로 구성된 태스크 구조에서는 하나의 스레드가 입출력을 기다리는 Blocked 상태여도 다른 스레드는 Running 할 수 있기 때문에 빠른 처리가 가능하다는 장점이 있다.