전공/운영체제

Chapter1 운영체제 개요1

문정훈 2022. 5. 5. 00:48

 

더보기

목차

1. Device Controller와 Local Buffer
2. Interrupt Vector Table, Interrupt Service Routine
3. I/O Structure(buffered IO)
4. Cacing이란(with locality)
5. DeadLock

1. Device Controller와 Local Buffer

I/O 디바이스의 처리와 CPU의 처리 작업은 동시(Parallelism)에 실행된다.

os가 바라보는 device(키보드, 마우스 등)는 논리적 형태의 파일로 바라본다.

이 논리적 파일을 Device Controller라고 한다. os는 프로그램 형태로 Device를 바라본다.

또한 Device Controllerlocal buffer를 가지고 있다.

 

이런 local buffer를 사용 하는 이유는 osdevice controller간에 처리 속도가 다르기 때문deviceI/O 작업의 내용을 local buffer에 저장한다.

예를 들어 여러 개의 device controller가 있다고 해보자.

 

os가 버퍼의 내용을 처리하는 방식1) polling 방식

os모든 Device Controller의 local buffer들을 일일이 살펴보다 처리해야할 내용이 있으면 처리한다.

이 방식은 os가 버퍼만 보다 끝날 수 있기 때문에 비 효율적인 방식이다. 

polling 방식을 비유하자면 택배가 문 앞에 오는데 내가 택배가 왔는지 안왔는지 계속 문을 열고 확인하러 나가는 것에 비유할 수 있다. 

 

os가 버퍼의 내용을 처리하는 방식2) interrupt driven 방식

device controller 내부에는 버퍼가 존재한다. os는 이 버퍼들을 모두 살펴볼 수 없다.

장치 드라이버는 os가 하는 일을 하다가 각 device controller 내부의 버퍼들은 처리해야할 내용들이 생기면 os에게 interrupt를 보내게 된다. os에게 알리게 된다.

비유하면 택배가 문 앞에 오는데 택배 기사가 벨을 누르면 나는 택배가 왔음을 인지하고 택배를 찾는다. 


 

2. Interrupt Vector Table, Interrupt Service Routine

장치 드라이버

하드웨어를 제어하기 위한 프로그램으로 커널의 일부분으로 동작하는 프로그램이다.

 

장치 컨트롤러

작은 CPU로 실제 하드웨어로 드라이버라는 프로그램을 인식하는데 이를 장치 컨트롤러라고 한다.

 

소프트웨어 인터럽트 예시

허가 되지 않은 메모리 주소 참조

0으로 나누는 경우

시스템 콜

 

하드웨어 인터럽트 예시

CPU 또는 기타 하드웨어 오류

타이머 인터럽트

하드웨어(키보드, 마우스) 입출력 장치

 

인터럽트 서비스 루틴

인터럽트 핸들러(메소드)라고도 하며 실제로 인터럽트를 처리하는 루틴으로 실행 중이던 레지스터와 PC를 스택에 저장하여 실행중이던 CPU의 상태를 보종하고 입터럽트 처리가 끝나면 원래 상태로 복귀한다.

 

운영체제의 코드 부분에는 각종 인터럽트 별로 처리해야할 내용이 프로그램 되어 있다.

 

interruptinterrupt vector table이란?

interrupt 발생 예시

키보드로부터 문자를 읽는 입출력 작업을 고려해보자

그럼 IO 장치는 interrupt를 발생시켜야한다. 누가 어떻게 누구에게 발생시키냐?

입출력 작업을 하기 위해

 

장치 드라이버는 장치 컨트롤러의 적절한 레지스터에 값을 적재한다.

컨트롤러는 레지스터의 내용을 검사하여 수행할 작업(키보드에서 문자열 읽기)을 결정한다.

컨트롤러는 내부 로컬 버퍼로 데이터를 전송한다. (키보드의 입력 값을 컨트롤러 로컬 버퍼에 저장한다는 의미)

로컬 버퍼에 데이터 전송이 끝났다면 컨트롤러는 장치 드라이버에게 interrup를 통해 작업이 완료됨을 알린다. ( 컨트롤러가 -> interrupt 발생시킴 -> 드라이버에게 )

osinterruptinterrupt 백터 테이블에 기록한다. CPUinterrupt 요청 라인을 통해 interrupt 가 감지되면 해당 서비스 루틴으로 jump 하게 된다.

 

interrupt 백터 테이블 동작 과정

컨트롤러에 의해 로컬 버퍼에 저장 값이 모두 저장 된 후 컨트롤러는 드라이버에게 interrupt를 전달하고 드라이버는 이 interrupt를 아래와 같은 Interrupt Vector Table에 기록하게 된다.

이와 같이 interrupt vector라는 곳에 체크 공간에 1로 표시를 하고 주소 공간에는

해당 interrupt 의 서비스 루틴의 첫 주소를 저장한다.

위 예시의 서비스 루틴의 내용은 키보드로부터 입력 받은 문자열을 키보드 디바이스 컨트롤러의 로컬 버퍼에 저장된 값을 읽어와 처리하는 서비스 루틴의 시작 주소이다.

 

ospolling 방식으로 즉 계속 vector table를 살펴보고 있다가 체크 공간에 체크가 잇다는 것을 확인하면 해당 vector의 주소공간에 저장된 명령어를 실행한다.

 

예를 들어 2/0이라는 명령어가 있을 때 이 명령어는 interrupt를 발생시키는데

이 명령어를 만나면 vector에 체크 공간에 1을 할당하고 해당 인터룹트 서비스 루틴의 첫 시작 주소를 백터에 주소 공간에 할당한다.

OSPolling 방식으로 백터를 감시하다 벡터의 interrup가 감지되면 CPU는 해당 작업을 멈추고 작업을 flush한 뒤 해당 프로세스는 스택에 레지스터의 값들을 저장한 뒤 인터룹트 서비스 루틴의 시작으로 jump하게 되어 interrupt가 실행된다.

해당 interrupt(소프트웨어 interrupt)는 예외가 발생하였으므로 프로그램이 정상 종료 되도록 하는 것이 이 예시의 인터럽트 서비스 루틴이다. 따라서 프로그램을 정상 종료하게 된다.

(2/0이 명령어를 만난다고 프로그램이 바로 죽는 것이 아니다 )

 

maskable interrup

interrupt의 종류 중 하나로 interupt가 들어오면 바로 처리해야하는 interrupt가 있다. interrup를 미뤄도 되는 것을 maskable interrup라고한다.

 

 

non-maskable interrupt

미루지 않고 inpterupt 가 들어오면 바로 처리해야하는 interrupt를 말한다.

interrupt 백터라는 곳에 interrupt 우선순위가 매겨지게 되는데 상위로 갈 수 록

non-maskable이 된다.

 

이미지 출처: io - The interrupt timeline for a single process doing output - Unix & Linux Stack Exchange

CPU에서 IO 작업 즉 HDD에 저장된 파일로부터 데이터를 가져올 필요가 있을 때 사용자 프로그램을 통해 CPUI/O 디바이스에게 I/O request를 요청한다.

그럼 놀고 있던 디바이스는 (idle 상태가 노는 상태임) trsnfering 상태로 바뀐다. 그리고 램으로 파일을 가져오는 작업이 끝나면 다시 I/O 디바이스의 상태는 idle -> transfering 상태가 된다.

근데 CPUI/O 디바이스가 transfering 하는 동안 자기 할 일을 묵묵히 수행한다.

그러다가 I/O 디바이스의 데이터 전송이 끝났을 때 즉 transfering -> idle이 다시 되면 디바이스는 interrupt를 발생시킨다. 그리고 CPUinterrupt를 감지하고 interrupt 서비스 루틴으로 jump하게 된다. 그 작업을 끝내고 다시 사용자 프로그램으로 돌아온다.


 

3. I/O Structure(buffered IO)

시스템 콜

IO 작업의 종류에 따라 제어권이 user program에게 넘어 갈 수 있다.

컴퓨터는 IO작업을 할 때 느려진다.

 

CPU에서 A라는 프로그램을 실행하는 동안 A라는 프로그램에 IO작업이 일어난다면 그 동안 CPU는 노냐? 아니다. 시스템 콜이라는 것을 통해 osA라는 프로세스가 io 작업을 할 것을 os에게 시스템 콜이라는 것을 통해 요청을 한다.

A라는 프로세스에서 io작업이 필요한 시스템 콜을 통해 os에게 이를 알린다.

그럼 Asleep하게 되고 B라는 프로세스를 CPU가 실행한다.

그러다가 AIO작업이 끝나면 osinterrupt를 받는데 그 때 다시 A 프로세스가 CPU에서 실행되는 구조이다. (이러한 과정을 시스템 콜이라고 한다)

 

Device-status table

os라는 것은 시스템에 설치된 다양한 디바이스들에 대한 상태를 테이블로 저장하는데 이를 ‘Device-status-table이라고 한다.

 

DST가 왜 필요하냐면 NIC 카드를 A프로세스도 쓰고 싶고 B프로세스도 쓰고 싶은 상황일 때

(NIC 카드에 쓰는 작업도 IO 작업이라한다.)

A프로세스가 NIC에게 데이터를 보내고 있을 때 B라는 프로세스도 NIC에 무엇인가를 보내고 싶다면 OSDevice-status-table을 보고를 B를 잠시 대기시킨다.

ANIC 작업이 끝낫다는 interrupt 신호가 발생하면 OS는 그 때 B를 사용할지 말지를 결정한다.

 

 

● Buffered 설명

프로세스는 CPU 자원이 할당된 프로그램을 의미한다.

프로그램이 프로세스 형태로 메인 메모리에 적재되었다고 다 실행중인 것은 아니다.

프로세스는 CPU의 자원을 할당 받아야 진짜 프로세스가 되는 것이다. 따라서 프로세스의 정의는 CPU자원이 할당된 프로그램을 의미한다.

예를 들어 메인 메모리에 프로세스A, 프로세스B, 프로세스C 이렇게 올라와있다.

그 중에서 CPU자원이 할당된 것만이 현재 실제 실행중인 프로세스인 것이다.

 

먄약 프로세스 가 CPU 자원을 할당 받고 실행하고 있었다.

이때 프로세스 A가 파일을 load(HDD->RAM)을 한다고 해보자.

load 작업을 1개 하는 시간동안 CPU는 100개 이상의 명령어를 실행할 수 있는 시간인데 그럼 프로세스 A가 파일을load(HDD->RAM)을 하면 CPU는 그냥 쉬게 냅두나? => 아니다

이때 잠시 CPU의 자원이 다른 프로스세로 이동하면 된다.

그리고 프로세스 A는 load(HDD->RAM)이 끝날 때 까지 기다리게 된다.

이것을 sleep 한다라고 한다. (CPU 자원이 다른곳으로 갔기 때문에 상관 없음)

 

자, 이제 프로세스A에서 파일의 load가 끝났다. (필요한 모든 파일의 block 이 HDD->RAM으로 load 되었다.) 그럼 CPU 자원이 다시 프로세스 A에게 할당되어 프로세스 A가 실행된다.

지금까지 가정으로는 이런 context switching이 너무 자주 일어날 것이다.

 

context switching할 때마다 즉 프로세스 A에서 B로 넘어갈 때 CPU 자원이 교대되는 순간에는 CPU는 아무것도 안하는 것이다. (비유를 하면 수학공부를 하다 영어공부를 하기 위해 수학 책을 덮고 영어책을 피는 과정에는 공부를 했다고 할 수 없다.)

context switching이 자주 일어면 CPU가 아무것도 안하는 시간이 늘어나게 되고 이는 CPU 성능의 감소를 의미한다. 따라서 우리는 CPU성능을 개선하는 방법을 생각해야한다.

IO 작업(RAM에서 데이터를 읽고 쓰는)시간이 좀 줄어들면 CPU 성능을 개선할 수 있지 않나? => IO시간 자체는 줄일 수 없다.

프로세스 A가 HDD에서 RAM으로 필요한 파일의 block을 load할 동안 기다리는 것이 아닌 load되기 전부터 버퍼의 내용을 사용자는 접근하는 것이다.

이것을 가능하게 해주는 이유가 buffer를 사용하기 때문이다.

 

 

※ 여기까지 정리

HDD에 있는 파일에 있는 내용 중 1byte만 읽고 싶은 경우를 생각해보면 HDD에서 RAM으로 카피를 하는데, 메인 메모리에 있는 프로세스가 접근할 수 있는 메모리 위치에 copy가 된다. 하지만 실제로 1byte만 RMA으로 적재될 수는 없다.

OS가 block 단위로 수행한다.

따라서 1byte에 딸려있는 하나의 block이 통체로 HDD에서 RAM으로 옮겨진다.

 

프로세스 A가 load(HDD->RAM)을 하는동안 프로세스 A는 무엇을 하냐? 노냐? -> 안논다.

load가 일어나는 동안 CPU는 다른 자원에게 할당되고 프로세스A는 sleep모드로 간다.

그리고 HDD에서 필요한 데이터가 block 단위로 RAM으로 load 된다. (OS의 역할)

이때 프로세스 A는 언제까지 sleep을 하냐?

HDD에서 RAM으로 block단위 load가 다 끝나고나서 sleep을 깨고 RAM에 접근하는건가?

 

해결 방법

이러면 sleep 기간이 너무 길어져 CPU 성능에 좋지 못하기 때문에 buffer의 개념이 도입되어 필요한 데이터의 block 단위 load가 끝나기 전부터 미리 프로세스 A는 해당 데이터에 접근할 수 있다. (sleep 기간을 최대한 줄이고하 함.)

이것을 가능하게 해주는 것이 fopen, fread, fwrite API 메소드이다.


 

 

4. Cacing이란(with locality)

Caching 이란

정보를 느린 저장소에서 빠른 저장소로 복사하는 것을 말한다.

예를 들어 HDD -> RAM으로 데이터를 불러오는 작업 등을 말한다.

캐쉬와 레지스터는 CPU이다.

cache라고 하는 저장 장치가 CPU에 포함되어 있는데 캐시가 만약 없다면 RAM -> CPU의 레지스터에 값을 저장하고 CPU는 레지스터에 저장된 데이터를 실행한다.

RAM에서 register에 저장하는 과정 역시 속도가 느리다.

그래서 CPU에 캐시를 두어 RAM에서 캐시에 저장하도록 하고 CPU는 캐시로부터 데이터를 읽어들여 레지스터에 저장한다.

CPU는 코드를 보고 소스코드에서 특정 부분이 많이 실행되는 지역성의 특성을 가지는 소스 코드 부분를 캐시에 저장한다. 따라서 RAM으로 접근하지 않아도 되며 캐시에 접근하여 처리를 하여 그 처리 속도가 빠르다.

 

캐시에 있는 내용이 실행되는 것을 Hit라고하고 캐시에 없어서 RAM으로 내려가 데이터를 가져와야하는 경우를 Miss라고 한다.

Hit이 많이 일어나면 CPU의 성능이 증가한다.

소스코드의 지역성이 없다면 성능의 큰 증가는 어렵다.

 

다른 예로 예를 들어

캐싱은 네트워크 디바이스에서도 많이 쓰인다. 검색 엔진에서 실검이 뜬다고 해보자.

자주 검색 되는 키워드는 해당하는 페이지를 만들어 캐시 서버를 넣는다. 그리고 웹 브라우저들이 키워드(자주 검색하는)를 검색하면 캐시에 접속하도록 하여 바로바로 웹 페이지를 사용자에게 전달하도록 한다. 이는 처리 속도를 증가시킨다.


 

5. DeadLock

concurrency에 의해서 race condition이 발생한다면 이런 race condition을 막기 위해 mutexes를 사용하는데 이 mutexes deadlock을 만든다.

데드락은 스레드1이 공유 작업1을 사용하고 있고 스레드 2가 공유 작업2를 가지고 있는데

스레드1은 자원2를 필요로하고 스레드2는 자원1을 필요로하는 경우 이러한 상황을 deadlock이라고 한다.

이런 데드락에 대해서 완벽히 해결할 방법은 없으며 누구 하나가 양보를 해야하며

OS는 스레드1 이 자원1을 사용할때는 결국 이후 자원2도 필요로 하기 때문에 T1이 자원1과 자원2를 모두 lock하게 되는 방식으로 deadlock이 해결 가능하다.