운영체제는 프로그램들 간의 올바른 실행을 돕고, 다양한 하드웨어 자원을 프로그램에 배분하는 프로그램이다.
OS 종류에 관계 없이 운영체제가 제공하는 핵심적인 기능은 비슷하다.
i) 프로세스 및 스레드 관리(동기화와 교착 상태)
ii) 자원 할당 및 관리(CPU 스케줄링, 가상 메모리, 파일 시스템)
시스템 콜과 이중 모드
운영체제도 일종의 프로그램이기 때문에 반드시 메모리에 적재되어야 한다.
이 때 메모리 내의 커널 영역이라는 공간에 따로 적재되어 실행된다.
운영체제가 적재되는 커널 영역 외에 응용 프로그램이 적재되는 공간은 사용자 영역이다.


운영체제의 기능을 제공받기 위해 커널 영역에 적재된 운영체제 코드를 실행해야 한다.
일반적으로 웹브라우저나 게임 같은 응용 프로그램은 CPU, 메모리와 같은 자원에 직접 접근하거나 조작 할 수 없다.
운영체제는 자신의 응용 프로그램의 자원 접근 및 조작을 대행해준다.
이 때 사용하는 것이 시스템 콜이다. 시스템 콜은 운영체제의 서비스를 제공 받기 위한 인터페이스이다. (함수)

이러한 시스템 콜은 소프트웨어 인터럽트의 일종이다. (예외처리도)
- 사용자 영역을 실행하면서 시스템 콜이 호출되면 CPU는 현재 수행 중인 작업을 백업
- 커널 영역 내의 시스템콜을 구성하는 코드를 실행
- 다시 사용자 영역의 코드 실행을 재개
※ CPU의 flag 레지스터 속 supervisor flag를 통해 사용자 모드인지 커널 모드인지 알 수 있다.
만약 CPU가 사용자 모드라면 입출력 명령어와 같이 자원에 접근하는 명령어를 만나면 실행되지 않는다.

일반적으로 응용 프로그램은 실행 과정에서 운영체제 서비스들을 매우 빈번하게 이용한다.
그 과정에서 빈번하게 시스템 호출을 발생시키고 사용자 모드와 커널 모드를 오가며 실행한다.
https://300-29-1.tistory.com/62
“hello world” 프로그램도 strace라는 도구를 사용해보면
실행과정에서 600회가 넘는 시스템 콜을 호출하는 것을 알 수 있다.
프로세스의 종류
foreground process
- 사용자와 직접 상호작용하는 프로세스
- 터미널, GUI, 키보드/마우스 입력을 통해 제어
- 실행 결과가 화면에 바로 보임, vim, python script, top, 브라우저 등등,
background process
- 사용자가 직접 제어하지 않고 백그라운드에서 실행되는 프로세스
- 그중 시스템 서비스를 위해 항상 실행되거나 대기하는 것을 Daemon(window에서는 service)라 한다.
메모리 영역
프로세스 유형을 막론하고 하나의 프로세스를 구성하는 메모리 내의 정보는 크게 다르지 않다.

코드 영역
실행 가능한 명령어가 저장되는 공간
데이터 영역
프로그램이 실행되는 동안 유지할 데이터가 저장된다.
위 2개는 실행 중 크기가 변하지 않는다. 반면 아래는 2개는 크기가 변한다.
힙 영역
개발자가 실행 도중 비교적 자유롭게 할당하여 사용 가능한 메모리 공간
할당했다면 언젠가는 해당 공간을 반환해야한다. (메모리 누수, gabage collection)
스택 영역
일시적으로 사용할 값들이 저장되는 공간. 함수의 실행이 끝나면 사라지는 매개변수, 지역변수, 함수복귀주소 등등
스택 트레이스를 통해 특정 시점에 스택 영역에 저장된 함수 호출 정보가 저장되고 이를 통해 문제의 발생 지점을 추적할 수 있다.

PCB와 문맥 교환
운영체제가 메모리에 적재된 다수의 프로세스를 관리하기 위한 정보
구조체의 일종이다. 새로운 프로세스가 메모리에 적재되면 커널 영역에 새로 만들어진다.
프로세스 ID, 실행 과정에서 사용한 레지스터 값, 프로세스 상태, CPU 스케줄링(우선순위 정보), 메모리 관련 정보
프로세스가 비정상 종료되어 프로세스 테이블에 종료된 프로세스의 PCB가 남아 있는 경우 -> zombie process
프로세스와 스레드
프로세스의 상태
하나의 프로세스는 여러 상태를 거치며 실행된다.
OS는 PCB를 통해 프로세스의 상태를 인식하고 관리한다.

생성 상태(new)
프로세스를 생성중인 상태, 메모리에 적재되어 PCB를 할당받은 상태.
생성 상태를 거쳐 실행할 준비가 완료된 프로세스는 준비 상태가 되어 CPU의 할당을 기다린다.
준비 상태(ready)
당장이라도 CPU를 할당받아 실행할 수 있지만 아직 자신의 차례가 아니기 때문에 기다리고 있는 상태.
준비 상태인 프로세스가 CPU를 할당받으면 실행 상태가 되고, 준비 상태인 프로세스가 실행 상태로 전환되는 것을 Dispatch라고 한다.
실행 상태(running)
실행 상태는 CPU를 항당받아 실행 중인 상태로, 일정 시간 동안만 CPU를 사용 할 수 있다.
타이머 인터럽트가 발생하여 프로세스가 할당된 시간을 모두 사용하면 다시 준비 상태가 된다.
실행 도중 입출력 장치를 사용하여 입출력장치의 작업이 끝날 때까지 대기 상태가 된다.
대기 상태(blocked)
프로세스가 입출력 작업을 요청하거나 바로 확보할 수 없는 자원을 요청하는 등 곧장 실행이 불가능한 조건에 놓이는 경우
대기 상태였던 프로세스는 입출력 작업이 완료되면 실행 가능한 상태가 되면 준비 상태가 되어 CPU할당을 기다린다.
종료 상태(terminated)
프로세스가 종료된 상태. 프로세스가 종료되면 운영체제는 BCP와 프로세스가 사용한 메모리를 정리한다.
프로세스 및 스레드 관리
멀티프로세스
- 동시에 여러 프로세스가 실행되는 것
- 이때 각기 다른 프로세스들은 기본적으로 자원을 공유하지 않고 독립적으로 실행된다.
- 같은 작업을 수행하고 있지만 각각의 PID값이 다르고 프로세스 별로 파일과 입출력 장치 등의 자원이 독립적으로 할당된다.
- 다른 프로세스에 영향을 거의 끼치지 않는다.
멀티 스레드
- 프로세스를 구성하는 코드를 동시에 실행하는 방법(여러 스레드를 이용해서)
- 하나의 스레드는 스레드를 식별할 수 있는 고유 정보인 스레드 ID와 프로그램 카운터, 레지스터 값, 스택 등으로 구성된다.
- 스레드마다 각각의 프로그램 카운터 값과 스택을 가지고 있기 때문에 스레드마다 다음에 실행할 주소를 가질 수 있고 연산 과정의 임시 저장 값을 가질 수 있다.

둘의 가장 큰 차이점은 자원의 공유 여부이다.
서로 다른 프로세스들은 기본적으로 자원을 공유 하지 않는다.
스레드들은 동일한 주소 공간의 코드, 데이터, 힙 영역을 공유하고,
열린 파일과 같은 프로세스의 자원을 공유하기 때문에 쉽게 협력하고 통신할 수 있다.
import threading
import os
def foo():
pid = os.getpid()
tid = threading.get_native_id()
print(f"foo: PID={pid}, Thread ID={tid}")
def bar():
pid = os.getpid()
tid = threading.get_native_id()
print(f"bar: PID={pid}, Thread ID={tid}")
def baz():
pid = os.getpid()
tid = threading.get_native_id()
print(f"baz: PID={pid}, Thread ID={tid}")
if __name__=="__main__":
thread1 = threading.Thread(target=foo) # 첫 번째 스레드 생성, 실행할 함수는 foo
thread2 = threading.Thread(target=bar)
thread3 = threading.Thread(target=baz)
thread1.start() # 첫 번째 스레드 실행
thread2.start()
thread3.start()
- 함수 foo, bar, baz를 실행하는 3개의 스레드 생성
- 각각의 함수는 현재 프로세스의 PID와 스레드 ID를 출력하는 코드로 구성
- 만약 워드 프로세서 프로그램의 경우 사용자로부터 입력받는 기능은 foo함수,
사용자가 입력한 값을 화면에 출력하는 기능은 bar함수,
사용자가 입력한 값의 맞춤법을 검사하는 기능을 baz함수라 할 수 있다.
스레드를 제대로 사용한다면 여러 작업을 동시에 수행하는 소스 코드를 작성할 수 있다.
join
thread1.join()
thread2.join()
thread3.join()
스레드를 생성한 주체가 ‘생성/실행된 스레드가 종료될 때까지 대기’해야 함을 의미
thread1, thread2, thread3이 모두 종료될 때까지 메인 쓰레드가 대기
이렇게 join()은 메인 쓰레드에 붙이지 않고 ‘기다릴 대상 쓰레드’마다 붙인다.
그리고 OS 스케줄러가 실행 순서를 결정하고 출력의 순서는 매번 다르다.
프로세스간 통신
프로세스는 기본적으로 자원을 공유하지 않지만,
프로세스 간에도 자원을 공유하고 데이터를 주고 받을 수 있는 방법이 있다.
i) 공유메모리
데이터를 주고받는 프로세스가 공통적으로 사용할 메모리 영역을 두는 방식
프로세스가 공유하는 메모리 영역을 확보하는 시스템콜을 기반으로 수행 될 수도 있고,
간단하게 프로세스가 공유하는 변수나 파일을 활용할 수도 있다.
ii) 메시지 전달
프로세스 간에 주고받을 데이터를 메시지의 형태로 주고받는 방식. 커널을 걸쳐 송수신 된다.
메시지를 보내는 수단과 받는 수단이 명확하게 구분되어 있다.
예로, 메시지를 보내는 시스템 콜인 send()라는 시스템 콜과
메시지를 받는 시스템 콜인 recv()라는 시스템 콜이 정해져있고,
각 프로세스는 두 시스템 콜을 호출하며 메시지를 송수신 할 수 있다.
메시지 전달 기반 IPC는 공유 메모리 기반 IPC보다 커널의 도움을 적극적으로 받을 수 있으므로
race condition, 동기화 등의 문제를 고려하는 일이 상대적으로 적다.
다만 주고받는 데이터가 커널을 통해 송수신되므로 공유 메모리 기반 IPC보다 통신 속도는 느리다.
- 파이프
단방향 프로세스 간의 통신 도구 - 시그널
프로세스에게 특정 event가 발생했음을 알리는 비동기적인 신호.
이러한 signal을 발생시키는 이벤트의 대부분은 인터럽트와 관련한 이벤트들, 사용자가 직접 정의할 수 있는 시그널도 있다.
만약 시그널이 발생하면 여느 인터럽트 처리 과정과 유사하게 하던 일을 잠시 중단하고, 시그널 처리를 위한 signal hanlder를 실행한 뒤 실행을 재개한다. - 소켓
- RPC
원격 코드를 실행하는 IPC 기술.
한 프로세스 내의 특정 코드 실행이 로컬 프로시저 호출이라면, 다른 프로세스의 원격 코드 실행이 원격 프로시저 호출이다.
프로그래밍 언어나 플랫폼과 무관하게 성능 저하를 최소화하고 메시지 송수신이 가능하기 때문에
대규모 트래픽 처리환경, 서버 간 통신 환경에서 사용되는 경우가 많다.
출처: 이것이 취업을 위한 컴퓨터 과학이다 with CS 기술 면접
