본문 바로가기
CS/OS

운영체제(OS)-가상화 (week1)

by assidu 2026. 3. 14.
프로그램이 실행될 때 무슨 일이 일어나는가?
  • Fetch: 프로세서(cpu)는 메모리로부터 지시사항(명령어)을 가져온다.
  • Decode: 이게 어떤 지시인지 밝힌다. CPU가 이해할 수 있는 언어(ISA)로 해석하는 과정임.
  • Execute: 실제로 명령을 수행.
  • move on: 프로세서는 다음 명령어로 이동하여 이 과정 계속 반복

 

OS의 역할
  • 프로그램이 잘 돌아가도록 함.
  • 프로그램들에게 메모리(자원) 할당, 배분함.
  • 프로그램들이 장치들과 상호작용 되게 함.
  • 시스템이 올바르고 효율적이게 작동하도록 한다.

프로그램 실행?  >  CPU 할당         /       프로그램들이 동시에 데이터와 명령들을 접근?  >  메모리 공유

프로그램들이 장치에 접근?  >  디스크 분배

 

"가상화(Virtualization)"

운영체제
는 물리적 자원(Processor, memory, disk)을 가져와서 가상 형태로 변환한다. (OS는 때때로 '가상 머신'이라 부르기도 함)

CPU 가상화
: CPU 한 개가 무한한 수의 CPU가 동시에 돌아가는 것처럼 만들어 많은 프로그램들이 동시에 돌아가는 거서럼 보이게한다. CPU는 한 번에 한 줄씩만 실행할 수 있는데, OS가 아주 빠르게 이 작업을 중단시키고 다른 프로그램의 코드를 가져오게(Fetch) 만들어오는 것을 빠르게 번갈아 실행, 여러 프로그램이 동시에 도는 것처럼 속인다.

메모리 가상화:
CPU가 메모리에서 명령어를 가져올 때(Fetch), 실제 물리적 주소가 아닌 OS가 만든 가상 주소를 참조하게 하여 프로그램들을 서로 격리시킨다. 물리적 메모리는 바이트 배열이며, 프로그램은 모든 데이터 구조를 메모리에 저장한다.
read memory(load): 데이터에 접근하기 위해 주소 지정
write memory(store): 주어진 주소에 기록할 데이터를 지정.

 

System Call
  • 사용자가 OS에게 뭘 할지 말하도록 도움
  • OS는 인터페이스를 제공함 (APIs, standard library)
  • 전형적인 OS는 백 여개의 시스템 콜을 내보낸다. (프로그램 실행, 메모리 접근, 장치들 접근)

cpu.c 코드>>

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h> //[리눅스전용] 시간 관련 함수
#include <assert.h> //조건검증
#include "common.h" //커스텀 헤더

int
main(int argc, char* argv[])
{
	if (argc != 2) { //문자열 입력안하면 에러 메시지 + 종료
		fprintf(stderr, "usage: cpu <string>\n");
		exit(1);
	}
	char* str = argv[1]; //입력받은 첫 문자열을 str에 저장
	while (1) {
		Spin(1);
		printf("%s\n", str);
	}
	return 0;
}

복습 포인트: argc는 인자 개수(실행부터 프로그램명을 인자 1개로 침), argv는 입력한 문자열 배열

 

 

mem.c 코드>>

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include "common.h"

int main(int argc, char* argv[])
{
	int* p = malloc(sizeof(int));

	assert(p != NULL);
	printf("(%d) address of p: %08x\n", getpid(), (unsigned)p);

	*p = 0;
	while (1) {
		Spin(1);
		*p = *p + 1;
		printf("(%d) p: %d\n", getpid(), *p);
	}
	return 0;
}

1. getpid()의 정체와 역할

정의: 현재 실행 중인 프로세스의 **고유 번호(PID, Process ID)**를 가져오는 함수다.

PID란?: 운영체제가 수많은 프로세스를 구분하기 위해 각 프로세스에 부여하는 고유한 숫지 이름표다.

소속: 리눅스 환경에서 #include <unistd.h>를 통해 사용할 수 있는 **시스템 콜(System Call)**이다.

 

관련 함수)

getppid(): 부모 프로세스의 ID를 가져옴(Parent PID)

fork(): 새로운 프로세스 만들 때, 부모에게는 자식의 PID를 알려주고 자식에게는 0을 아려주어 자기가 누구인지 알게함.

2. mem.c 코드에서 구체적인 쓰임새

신분증 출력: printf("(%d) address of p: ...", getpid(), ..) 문장은 "지금 이 메시지를 찍는 프로세스의 번호는 **(PID)**이고, 내가 가진 p의 주소는 이거야"라고 말하는 것과 같다.

가상화 증명: 이 프로그램을 동시에 실행(&붙여서 백그라운드 실행을 같이)하면 각각의 getpid() 값은 다르지만 p의 주소값은 똑같이 나온다.

결론: "번호가 다른 두사람(PID 다름)이 각자 자기 집의 101호(가상 주소 동일)를 쓰고 있는데, 실제로는 서로 다른 실제 집(물리 메모리)에 살고 있다"는 것을 보여주는 실습. 


The Problem of Concurrency(병행성 문제)

운영체제가 여러 작업을 동시에 처리하려고 할 때 발생하는 까다로운 문제.

쉽게 말해 "두 명 이상의 작업자가 하나의 데이터를 동시에 고치려다가 값이 꼬여버리는 현상"

 

CPU 가상화의 부작용: 여러 프로그램을 동시에 돌리는 것처럼 보이게 하려고 CPU를 번갈아 쓰게 했더니(Virtualizing CPU), 하필 계산 중간에 끊겨서 데이터가 꼬이는 부작용이 생긴 것

 

실습 thread.c

#include <stdio.h>
#include <stdlib.h>
#include "common.h"

volatile int counter = 0; //모든 스레드가 공유해서 사용하는 변수. 
//volatile은 컴파일러에게 "이 변수는 언제든 값이 변할 수 있으니 함부로 최적화하지마라"고 경고하는 역할함
int loops;

void *worker(void *arg) {
	int i;
    for (i = 0; i < loops; i++) {
    	counter++;
        }
        return NULL;
}

int main(int argc, char *argv[]) {
	if (argc != 2) {
    	fprintf(stderr, "usage: threads <value>\n");
        exit(1);
    }
    loops = atoi(argv[1]);
    pthread_t p1, p2;
    printf("Initial value : %d\n", counter);
    
    Pthread_create(&p1, NULL, worker, NULL);
    Pthread_create(&p2, NULL, worker, NULL);
    Pthread_join(p1, NULL);
    Pthread_join(p2, NULL);
    printf("Final value : %d\n", counter);
    return 0;
 }

 

병행성 문제를 보여줌. 두 명의 작업자(스레드)가 하나의 공유 자원(counter)을 동시에 건드릴 때 어떤 대참사가 발생하는지 증명하는 코드

 

 이 코드를 컴파일해서 실행하면 다음과 같은 현상을 목격하게 됩니다.

  1. 적은 횟수 실행 시: ./thread 1000을 입력하면 결과가 2000으로 잘 나옵니다. 너무 순식간에 끝나서 충돌이 안 일어난 거죠.
  2. 많은 횟수 실행 시: ./thread 100000을 입력하면 결과가 200,000이 아니라 143,012 같은 엉뚱한 숫자가 나옵니다. 심지어 실행할 때마다 결과가 계속 바뀝니다.

3. 왜 이런 일이 벌어질까? (원자성 결여)

CPU가 counter++를 수행할 때, 우리 눈엔 한 줄이지만 실제로는 다음의 **3단계(Instruction)**로 쪼개서 일하기 때문.

  1. Load: 메모리에 있는 counter 값을 가져와서 CPU 레지스터에 저장한다.
  2. Increment: 레지스터에 있는 값에 1을 더한다.
  3. Store: 더해진 결과값을 다시 메모리의 counter 위치에 저장한다.

대참사 시나리오:

  • 스레드 1이 값을 '100'으로 읽어왔는데(Load), 갑자기 **문맥 교환(Context Switch)**이 일어나서 스레드 2에게 CPU를 뺏긴다.
  • 스레드 2도 아직 업데이트 안 된 '100'을 읽어온다.
  • 두 스레드 모두 각자 1을 더해 '101'을 만들고 메모리에 쓴다.
  • 결과적으로 1이 두 번 더해져서 102가 되어야 하는데, 메모리에는 101만 남게 된다.

4. 운영체제 관점에서의 교훈

이 코드를 통해 전달하고자 하는 메시지는 명확합니다.

 

원자성(Atomicity): 계산 중간에 방해받지 않고 한 번에 끝내는 장치가 필요하다는 것. 운영체제에서 원자적이라는 말은 "더 이상 쪼갤 수 없는 하나의 단위" 라는 뜻. 어떤 일이 시작되면 중간에 절대 끊기지 않고 끝까지 완료되거나, 아니면 아예 시작조차 안 된 상태로 남는 것이 원자적 실행. (비유: 송금, 입금은 중간에 멈추면 안되는 하나의 세트(원자적)이어야 함.)

 

공유 자원 관리: 여러 프로세스가 동시에 자원을 공유할 때(Sharing resources), OS가 이를 제대로 통제하지 않으면 결과가 예측 불가능해진다는 점.

 


 

Persistence (지속성)

HW와 SW 모두 데이터를 지속적으로 저장하는게 필요하다.

HW: 입출력 장치(HDD, SSD) / SW:  파일 시스템이 디스크 관리, 사용자가 만든 파일들 저장하는 역할

운영체제는 디스크에 데이터를 쓰기 위해 '새 데이터가 디스크의 어느 위치에 저장될지 결정', '기본 저장 장치에 I/O 요청 전송', '파일 시스템은 쓰기 작업 중 시스템 충돌을 처리', '저널링 또는 쓰기 시 복사', '디스크 쓰기 순서 신중히 지정' 등을 한다.

 

운영체제 설계의 5대 목표 (Design Goals)

 

① 추상화 구축 (Build up Abstraction)

  • 의미: 복잡한 하드웨어를 사용자가 쓰기 편한 가짜 모습(Virtual form)으로 바꾸는 것입니다.
  • 비유: 복잡한 엔진 구조를 몰라도 '핸들'과 '페달'만 알면 운전할 수 있게 만드는 것과 같습니다.

 

② 고성능 제공 (Provide High Performance) 성능 저하 XXX

  • 의미: 운영체제가 하드웨어를 관리하느라 정작 사용자의 프로그램이 느려지는 일을 최소화하는 것입니다.
  • 비유: 식당 지배인이 주문 정리를 너무 꼼꼼히 하느라 요리가 늦게 나오면 안 되는 것과 같습니다.

 

③ 애플리케이션 간의 보호 (Protection / Isolation) 침범 XXX

  • 의미: 한 프로그램이 사고를 쳐도 다른 프로그램이나 OS 본체에는 영향을 주지 않도록 격리하는 것입니다.
  • 비유: 식당의 한 테이블에서 물을 쏟아도 다른 테이블 손님의 신발이 젖지 않게 칸막이를 치는 것입니다.

 

④ 높은 신뢰성 (High degree of Reliability)

  • 의미: 운영체제는 멈추지 않고 365일 24시간 계속 돌아가야 한다는 원칙입니다.

 

⑤ 기타 이슈 (Energy, Security, Mobility)

  • 의미: 에너지를 아끼고(배터리 효율), 보안을 지키며, 이동 중에도 잘 작동해야 합니다

 


운영체제의 역사 1G

빈 박스!

 

한 번에 작업 하나만!

이 떄부터 칩 점점 좋아짐, 디스크 생김, ISA 등장, 컴퓨터 구조 개념 생김!

스케쥴링 개념이 들어섰다. 2세대는 한 번에 업무 한 개였다.

CPU 점점 좋아짐(듀얼, 쿼드, 옥타코어..) OS의 GUI 점점 좋아짐 등등