본문 바로가기
CS/OS

운영체제(OS)-week2

by assidu 2026. 3. 17.

#1. CPU 가상화의 난제 (2~6p)

 

비유: “하나의 tv 채널로 수백 명이 각자 보고 싶은 프로그램을 동시에 보게 만드는 법”

설명: “실제 cpu 하드웨어는 하나지만, OS는 수백 개의 프로세스가 동시에 돌아가는 **환상(Illusion)을 만들어야 한다. 이를 위해 OS는 여러 프로세스를 아주 짧은 시간 단위로 번갈아 실행하는 시분할 기법을 사용한다. 각 프로세스는 자기만의 CPU를 가졌다고 착각하며 실행된다.

 

 

원래 cpu가 동작하는 방식(한번에 하나!), 가상화로 여러 cpu가 여러 프로세스 돌아가게 하는 것처럼 보임
OS를 통한 CPU 가상화

OS를 통해서 CPU 1개를 가지고 여러 프로세스를 동작한다. 프로세스끼리 알아서 교대하는 것처럼 보이지만, 사실 OS가 중간에 개입하여 제어권을 가져온 뒤 다음 프로세스에게 넘겨주는 방식이다. (시분할 방식) 

 

 

 

 

#2. 프로세스란 무엇인가? (7~9p)

 

비유: “실행 중인 프로그램의 인스턴스”

설명: 디스크에 저장된 정적인 ‘프로그램’이 메모리에 올라가 살아 움직이면 ‘프로세스’가 된다. 프로세스는 고유한 **PID(Process ID)로 구분되며, 실행을 위한 코드(Code), 데이터(Data), 힙(Heap), 스택(Stack) 영역을 가진다. 또한 CPU의 현재 상태를 나타내는 레지스터 값(CPU context : Program Counter, Stack Pointer 등)도 포함한다.

 

자바로 비유하면 클래스는 "프로그램(정적)", 객체는 "프로세스(동적)"이다.

  • 메모리

텍스트는 프로그램 코드를 포함한다.

스택은 함수의 매개변수, 반환주소, 지역 변수 등을 포함한다.

Data는 전역 변수와 정적 변수를 포함한다.

힙은 메모리 공간을 동적으로 할당한다.

 

  • 레지스터

프로그램 카운터, 스택 포인터, 프레임 포인터를 가진다.

 

 

 

 

#3. 프로세스의 탄생 과정 (10~11p)

 

비유: “공연을 위해 무대를 세팅하고 배우를 대기시키는 과정”

설명:

 

 

  1. 로드(Load): 디스크의 코드를 메모리로, 프로세스의 주소 공간으로 가져온다. (필요할 때만 가져오는 Lazy 방식 선호)
  2. 스택/힙 생성: 지역 변수를 위한 스택(인수들 이용해 스택 초기화 => main함수의 argc, argv배열)과 동적 할당을 위한 힙을 만든다.(프로그램은 메모리 공간 할당을 위해 malloc(), free()로 해제.)
  3. 입출력 초기화: 기본적으로 표준 입력, 출력, 에러 채널을 연다.
  4. 시작: main() 함수로 CPU 제어권을 넘겨 실행을 시작한다.

 

 

 

 

 

#4. 프로세스의 상태 변화 (13~14)

 

 

비유: “은행 대기실의 세 가지 구역”

설명:

Running(업무 중): 실제로 cpu를 점유해 실행 중인 상태

Ready(대기 중): 실행 준비는 끝났지만, OS가 아직 순서를 안 준 상태

Blocked(일시 정지): 입출력(I/O) 요청 등을 하고 결과를 기다리느라 CPU를 써도 의미가 없는 상태

 

 

 

 

#5. 프로세스 생성과 복제: fork() 와 exec() (15~19, 30-33p)

 

 

비유: “세포 분열(fork) 후 성격 개조(exec)”

설명:

fork() : 현재 프로세스를 똑같이 복제하여 자식 프로세스를 만듭니다. 부모는 대부분의 자원들과 특권들을 상속한다. 자식은 또한 부모의 주소공간을 복제한다. 부모에게는 자식의 PID를, 자식에게는 0을 반환하여 구분합니다.

  • fork의 첫 번째 의미 "복제" : 현재 실행 행중인 부모의 상태를 그대로 로복사하여 새로운 프로세스를 생성한다는 물리적인 의미.
  • fork의 두 번째 의미 "분기" : fork()의 리턴값(0 또는 자식 PID)를 이용해 부모와 자식이 서로 다른 길을 가게 만든다는 논리적인 의미.

복제만 하고 끝난다면 똑같은 일만 반복하겠지만, fork() 이후 if-else문을 통해 자식은 새로운 프로그램으로 변신(exec)하고 부모는 자식을 기다리는(wait)등 역할 분담이 일어나기 때문. 

 

 

exec() : 새로운 프로세스를 만드는 게 아니라, 현재 몸체에 새로운 프로그램 코드를 덮어씌워 완전히 다른 일을 하게 만듭니다.

 

 

 

+ 프로세스 종료

  • Normal exit (자발적)
  • Error exit (자발적)
  • Fatal error (비자발적) 
  • • 세그멘테이션 오류 – 잘못된 메모리 접근
  • • 보호 오류
  • • 할당된 리소스 초과 등​
  • 다른 프로세스에 의해 종료 (비자발적) - 시그널을 받음으로써
  • Zombie process: 종료, 그러나 삭제되지않음

 

 

+ 프로세스 계급

  • 부모-자식 관계

하나의 프로세스가 다른 프로세스를 만들 수 있음.

Unix가 그 계급을 "process group"이라고 부름.

윈도우는 프로세스 계급의 개념이 없음.

  • 프로세스들의 목록을 탐색: ps(Unix에서), Tsk Manager (tastmgr) (윈도우에서!)

 

 

fork() 코드의 예시>>

#include <sys/types.h>
#include <unistd.h>
int main()
{
	int pid;
    if ((pid = fork()) == 0) //자식이다!
    	printf("Child of %d is %d\n", getppid(), getpid());
    else //부모다!
    	printf("I am %d. My child is %d\n", getpid(), pid);
}

 

세포분열: fork()가 호출되면 OS는 부모 프로세스를 똑같이 복제해서 새로운 자식 프로세스를 만든다.

서로 다른 리턴값: 똑같은 코드지만, OS는 부모와 자식을 구분하기 위해 다른 값을 던져준다. 자식에게는 0을, 부모에게는 새로 태어난(복제된) 자식의 진짜 PID를 리턴한다.

동시 실행: 이제 두 프로세스(부모와 자식)은 if 문부터 각자 갈 길을 가기 시작한다.

 

 

프로세스는 스케줄링에 따라 누가 먼저 실행될지 알 수 없으므로 출력 순서는 두 가지 경우가 생긴다.

출력 결과

 

복기: fork() 는 자식에게 0을 반환하고, 부모 프로세스에게는 복제된 자식 프로세스의 PID 반환함.

fork()가 일어날 때, 커널 내부(do_fork())에서는 다음과 같은 일이 벌어집니다.

  • 새 장부 만들기: 먼저 새로운 PID를 할당하고 , 새로운 PCB를 생성 및 초기화합니다.
  • 내용 복사: 부모의 PCB 내용을 거의 그대로 복사하여 자식의 PCB를 채웁니다.
  • Ready 큐 등록: 이제 이 자식의 PCB를 Ready 큐에 넣어서, 언제든 CPU를 쓸 수 있게 준비시킵니다.

+exec() : 보통 fork()로 복제만 하고 끝내지 않는다. 자식이 부모랑 똑같은 일만 하면 효율이 떨어짐. 복제된 자식 프로세스에서 exec() 호출하여 자식이 가지고 있던 부모의 코드를 다 버리고 완전히 새로운 프로그램으로 탈바꿈하게 한다.

그러나 자식 PCB에 저장한 부모 PCB내용을 다 버리지는 않는다. 중요한 PID, PPID, 파일 디스크립터, 상태 및 우선순위 등의 정보는 그대로 유지된다.  

 

 

의문점: fork()를 쓰면 프로세스가 복제되는데, 복제된 자식 프로세스에서 또 복제 즉 무한 복제되는 것은 아닌가? 재귀함수처럼?

답: fork()를 읽는 순간 복제가 일어나고, 새로 생긴 자식 프로세스는 처음부터 읽는게 아니라 부모가 읽던 그 지점부터  읽기 시작함.

의문점처럼 무한 복제되는 코드는 "포크 폭탄(Fork Bomb)"

while(1) {
	fork();
 }

 

 

 

 

#6. 운영체제의 장부: PCB(Process Control Block) (Process Descriptor라고도 부름)(23~25p)

"존재하지 않는다(하드웨어아님)"

 

PCB는 메모리의 커널 영역(Kernel Space)에 존재하는 데이터 조각, 일부일 뿐이다.

프로세스가 종료되면 해당 PCB데이터는 메모리에서 삭제되어 사라진다.

=> PCB는 하드웨어적으로 존재하는 물건이 아니라, 운영체제가 프로세스를 관리하기 위해 메모리에 그려놓은 '가상의 관리 대장'

 

비유: “100대의 스마트폰을 관리하기위한 상세 관리 대장”

설명: OS는 프로세스를 관리하기 위해 **PCB(Process Control Block)라는 구조체를 유지합니다. 프로세스마다 하나씩 PCB를 생성한다.여기에는 cpu 문맥(context), 프로세스의 상태, 식별 정보(PID), 레지스터 값, 메모리 관리 정보, 현재 작업 디렉토리, 열린 파일 목록 등 모든 정보가 들어있습니다. 리눅스에서는 task_struct(3248 바이트 크기)라고 부릅니다.

 

 

"프로세스가 여러 개 실행된다"는 환상을 유지하기 위해 OS는 PCB를 끊임없이 갈아치운다.

저장 > 교체 > 복원 > 재개 로 진행되는데, 타임 아웃이 발생하면 하드웨어는 현재 실행 중인 프로세스의 레지스터 값들을 그 프로세스의 PCB에 저장한다. 그 후 OS는 다음에 실행할 프로세스를 결정 후 정한 그 프로세스의 PCB에 적혀 있던 에전 레지스터 값들을 다시 CPU로 불러온다.(복원) CPU는 예전에 자기가 그 프로세스를 어디까지 실행했는지 알게되고 그 지점부터 다시 재개한다.

 

이것이 문맥 교환!!!

 

 

 

 

#7. 문맥 교환: Context Switch (26~28p)

 

비유: “하던 일을 멈추고 다음 사람을 위해 모든 도구를 정리하고 새 도구를 꺼내는 작업”

설명: CPU가 프로세스 A에서 B로 넘어갈 때, A의 레지스터 상태를 PCB에 저장하고 B의 PCB에서 이전 상태를 복구하는 과정입니다. 이는 시스템에 부하를 주는 **오버헤드(overhead)지만, 초당 수백~수천 번 일어나며 우리를 속인다.

 

 관리 오버헤드
• 레지스터 및 메모리 맵 저장 및 복원
• 메모리 캐시 플러시 및 재로드
• 다양한 테이블 및 목록 업데이트 등

 

 

 

+추가 정보

init 프로세스의 역할: "모든 프로세스의 조상님"

컴퓨터가 켜지고 운영체제가 메모리에 올라간 뒤, 가장 먼저 만들어지는 사용자 프로세스가 바로 **init**입니다. (PID가 보통 1번입니다.) 역할이 많은 이유는 이 프로세스가 시스템의 '관리자'이자 '부모' 역할을 도맡아 하기 때문입니다.

  • 시스템 초기화: 부팅 과정에서 디스크 검사, 네트워크 설정, 터미널(로그인 창) 실행 등 시스템이 돌아가기 위한 모든 환경을 설정합니다.
  • 고아 프로세스 입양 (Adoption): 부모 프로세스가 자식보다 먼저 죽어버리면 자식은 '고아 프로세스'가 됩니다. 이때 init이 이 자식들을 자신의 자식으로 입양하여, 나중에 자식이 종료될 때 그 종료 상태를 확인해 주고 자원을 정리해 줍니다.
  • 서비스 관리: 백그라운드에서 계속 돌아가야 하는 프로그램(데몬)들을 실행시키고, 혹시 죽으면 다시 살려내는 등 시스템 전체의 생태계를 유지합니다.

 

인터럽트(Interrupt)와 타임 인터럽트(Timer Interrupt)

운영체제가 CPU 제어권을 다시 가져오기 위해 사용하는 '강제 중단 벨'.

  • 예를 들어, 키보드를 누르거나 데이터 읽기가 완료되었을 때 발생함.
  • 인터럽트 (Interrupt): 하드웨어나 소프트웨어가 CPU에게 "지금 하던 일 잠시 멈추고 이 급한 일부터 처리해 줘!"라고 보내는 신호입니다.
  • 타임 인터럽트 (Timer Interrupt): CPU 가상화(시분할)를 위해 가장 중요한 장치입니다.
    • 원리: 하드웨어 타이머가 수 밀리초(ms)마다 주기적으로 인터럽트를 발생시킵니다.
    • 역할: 특정 프로세스가 CPU를 너무 독점하지 못하도록 OS가 개입할 틈을 만들어 줍니다. 이 신호가 오면 OS는 현재 프로세스를 멈추고(Running → Ready), 다음 차례인 프로세스에게 CPU를 넘기는 **문맥 교환(Context Switch)**을 수행합니다.