컴퓨터 구조는 위와 같다.
CPU와 메모리를 컴퓨터라 한다.
input은 io device에서 computer로 데이터가 들어가는 것,
output은 computer에서 io device로 다시 나가는 것
✅ 메모리
메모리는 CPU의 작업공간이다. CPU에서 하는 일은 기계어(instruction)을 실행하는 것이다. CPU는 매 clock cycle마다 기계어를 하나씩 읽어서 실행한다.
✅ IO device
하드디스크는 보조기억장치라고도 하지만 IO device로 볼 수도 있다. input, output device로의 역할을 동시에 수행한다.
각각의 IO device에는 IO device를 전담하는 작은 CPU 같은 것이 붙어있다. 그것을 device controller라 부른다.
디스크에서 헤드가 어떻게 움직이고 어떤 데이터를 읽을지(디스크의 내부를 통제하는 것)은 CPU의 역할이 아닌 디스크의 device controller가 그 역할을 하게 된다.
main cpu의 작업 공간인 main memory가 있듯이, device controller도 자기들의 작업 공간이 필요하다. 그걸 local buffer라 한다.
CPU와 IO device는 처리 속도가 많이 차이난다.
✅ register
CPU 안에는 메모리보다 빠르면서 정보를 저장할 수 있는 작은 공간들 레지스터가 있다.
✅ mode bit
mode bit은 CPU에서 실행되는 것이 운영체제인지, 사용자 프로그램인지 구분해주는 역할을 한다
사용자 프로그램의 잘못된 수행으로 다른 프로그램 및 운영체제에 피해가 가지 않게 하기 위해 필요하다
Mode bit 1 : 사용자 모드 , 사용자 프로그램 수행
Mode bit 0 : 모니터(커널) 모드 , OS 코드 수행
mode bit이 0일때, 운영체제가 cpu에서 실행중일 때는 무슨 일이든지 할 수 있게, 메모리 접근, io 접근도 모두 가능하게 정의가 되어 있다.
mode bit이 1일때는 사용자 프로그램이 cpu를 가지고 있을 때는 제한된 instruction만 cpu에서 실행할 수 있다. 보안상의 목적에 따라 io device를 접근하는 instruction은, 다른 프로그램, 운영체제의 메모리를 접근하는 instruction은 mode bit이 1인것을 보고 instruction이 실행이 안되게 구현이 되어 있다.
interrupt가 들어오면 cpu 제어권은 OS에게 넘어가면서 modebit은 자동으로 0으로 바뀌게 된다.
보안을 해칠 수 있는 중요한 명령어는 모니터 모드에서만 수행 가능한 특권명령이다.
그게 아닌 명령들은 일반 명령이다.
✅ Interrupt line
CPU는 항상 메모리에 있는 instruction만 실행한다. 하나가 실행되고 나면 다음번에 실행할 instruction의 주소값이 증가하고 그에 의해 다음 instruction을 수행한다.
CPU는 메모리하고만 일하는데, IO device에서 무슨 일을 해야 한다던지 등을 알기 위해 interrupt line이 있다.
항상 메모리만 가지고 작업하는게 아니라 키보드에서 뭘 읽어오고, 화면에 출력하고.. 등등 IO device에 접근해야 할 instruction이 있다.
CPU는 IO device에 직접 접근하지 않고 메모리를 접근하는 instruction만 실행하게 되어있다.
디스크에서 무언가를 읽어오라는 요청은 CPU에서 특정 데이터를 읽어오라는 일을 시킨다.
디스크는 요청한 데이터를 컨트롤러의 지시를 받아서 시킨 일을 한다.(데이터를 읽어와서 자신의 로컬 버퍼에 집어넣는다)
디스크는 CPU에 비해 느리니까 이 일을 하는 도중 CPU가 놀게 되면 손해이다. 따라서 IO 작업은 컨트롤러에게 시키고 CPU는 메모리 접근을 계속 하며 다음 insturction을 실행한다.
IO 장치에서 사용자에게 입력을 받거나 해서 할 일을 마쳤으면 해당 컨트롤러에서 CPU에 인터럽트를 건다. 인터럽트가 들어오면 CPU 가 OS에게 넘어가고, OS는 인터럽트가 왜 들어왔는지 살펴보고 , 아, 아까 어던 프로그램이 요청했던 프로그램의 입력이 들어왔구나. 하고 그 프로그램의 메모리 공간에 키보드의 버퍼 값을 복사해주고, 원래 돌아가던 프로그램한테 cpu를 다시 넘겨준다.(인터럽트 당했으니까) .. 그리고 다시 IO를 요청했던 프로그램의 차례가 되면 해당 프로그램에게 CPU를 넘겨준다.
✅ timer
컴퓨터 안에는 timer라는 하드웨어가 있다.
특정 프로그램이 cpu를 독점하는 것을 막기 위해 존재한다.
처음에는 OS가 cpu를 가지고 있다가 여러 사용자 프로그램이 실행되면 cpu를 넘겨주는데, timer에 값을 세팅한 다음에 사용자 프로그램에게 넘겨준다. 사용자 프로그램은 독점적으로 cpu를 계속 쓰는 것이 아니라 할당된 시간(일반적으로 수십 밀리세컨드) 동안 instruction을 실행하고 세팅된 시간이 되면 timer가 cpu에게 interrupt를 걸어서 세팅해둔 시간이 끝났음을 알려준다.
cpu는 instruction을 하나씩 실행하다가 하나의 instruction이 끝나면 interrupt line을 확인한다. interrupt 들어온게 없으면 다음 instruction을 실행하고 있으면 interrupt를 확인한다.
interrupt에 걸리면 cpu가 OS로 다시 넘어가게 된다.
OS는 또 타이머에 값을 세팅하고 다음 프로그램에게 CPU를 넘겨주게 된다.
즉 time sharing을 위한 장치이다.
프로그램이 IO를 해야 하면 자진해서 IO를 해달라고 OS에게 cpu를 넘겨준다.
사용자 프로그램은 직접 IO 장치를 접근할 수 없고, IO에 접근하는 모든 instruction은 OS를 통해서만 할 수 있게 막아놓았다.
✅ Device Controller(장치제어기) -> hardware
각 IO 장치를 통제하는 일종의 작은 CPU
제어 정보를 위해 control register, status register를 갖는다
local buffer를 갖는다
어떤 데이터를 파일에 저장하고 싶으면 파일에 저장하라는 명령을 제어 레지스터를 통해 내리고 저장할 데이터를 로컬 버퍼에 넣는 것이다.
✅ Device Driver(장치구동기) -> software
OS 코드 중 디바이스를 접근할 인터페이스에 맞게 접근할 수 있게 해주는 소프트웨어 모듈이다. 즉 CPU가 디바이스에 접근할 수 있게 해주는 소프트웨어이다. 각 IO 디바이스 내에서는 펌웨어에 의해 동작한다.
main 메모리는 원칙적으로 cpu만 접근할 수 있다.
io device는 자기 자신들의 로컬 버퍼가 존재해서 버퍼에서 데이터를 받아서 일한다. 버퍼에 쌓이게 되면 cpu가 그 내용을 읽어서 자신의 작업영역인 메모리에 복사한다.
cpu는 메모리 접근도 할 수 있고 로컬 버퍼 접근도 할 수 있다.
✅ DMA controller
근데 이러한 방식은 cpu가 인터럽트를 너무 많이 당한다.
io 장치가 뭐 할때마다 인터럽트.. 인터럽트.. 하니까 너무 많이 당함
그래서 DMA controller가 있다. DMA는 direct memory access이다.
직접 메모리에 접근할 수 있는 컨트롤러이다.
CPU도 메모리에 접근할 수 있지만 DMA 컨트롤러도 메모리에 접근할 수 있다.
IO 장치가 CPU에게 계속 인터럽트를 걸어서 CPU가 IO device의 값을 복사하게 하는 것이 너무 오버헤드가 크다.
CPU는 계속 자기 일을 하고, DMA가 로컬 버퍼의 값을 메모리에 복사하는 일 까지 해준다. 그 작업이 다 끝나면 CPU한테 인터럽트를 한번만 걸어서 CPU가 인터럽트 당하는 빈도가 줄어서 CPU를 더 효율적으로 사용할 수 있다.
✅ memory controller
CPU랑 DMA 컨트롤러랑 동시에 메모리 공간에 접근하면 문제가 될 수 있다.
메모리 컨트롤러가 DMA 컨트롤러랑 CPU랑 교통정리를 해준다. 누가 먼저 메모리에 접근해야 하고 이런 것.. 문제가 생기지 않도록
모든 입출력 명령은 특권명령이다.
사용자 프로그램이 직접 IO를 하지 못한다.
운영체제를 통해서만 IO 장치를 접근할 수 있다.
사용자 프로그램이 IO 장치에서 파일을 읽어와야 할 때는 운영체제한테 부탁한다.
사용자 프로그램이 운영체제를 호출하는 것을 시스템콜이라고 한다.
CPU에서 instruction을 순차적으로 수행하다가 함수 호출이나 제어구조에 해당하는 if문을 만족 안하는 등.. instruction의 메모리 위치를 점프하게 된다. 즉 내 프로그램 안에서 함수호출하는 것은 메모리 안에서 주소를 바꾸는 것이다.
그런데 내 프로그램이 실행되다가 IO요청을 하기 위해 운영체제의 함수 호출 (시스템콜) 은 메모리 주소를 바꿔서 되는 일은 아니다.
내 프로그램을 실행하다가 IO를 해야 하면 프로그램이 직접 interrupt line을 세팅하는 instruction을 수행한다. CPU는 그러면 운영체제에게 넘어가게 되고 mode bit이 0으로 바뀌고 운영체제가 IO device에게 읽어오라고 요청을 할 수 있게 되는 것이다.
✅ 인터럽트
인터럽트 당한 시점의 레지스터와 pc를 save 한 후 CPU의 제어를 인터럽트 처리 루틴에 넘긴다
✅ 하드웨어 인터럽트 : Interrupt, 보통 인터럽트라 하면 이것을 의미함
하드웨어가 발생시킨 인터럽트
(타이머, IO 컨트롤러)
✅ 소프트웨어 인터럽트 : Trap이라는 말로 부름
✅ 인터럽트 벡터
해당 인터럽트의 처리 루틴 주소를 가지고 있다
즉 어떤 인터럽트가 들어오면 어떤 인터럽트 루틴을 (함수를) 호출해야 하는지 주소를 정의해놓은 테이블 같은 것이다.
✅ 인터럽트 처리 루틴
인터럽트 종류가 여러가지가 있고 각각의 인터럽트마다 해야 할 일이 다르다. 각 인터럽트마다 실제 처리해야 할 코드가 운영체제에 있는데 이것을 인터럽트 처리 루틴이라 한다.
✅ 사용자 프로그램은 어떻게 IO를 하는가?
1. 시스템 콜
사용자 프로그램이 운영체제에게 IO 요청
2. trap을 사용하여 인터럽트 벡터의 특정 위치로 이동
3. 제어권이 인터럽트 벡터가 가리키는 인터럽트 서비스 루틴으로 이동
4. 올바른 요청인지 확인 후 IO 수행
5. IO 완료 시 제어권을 시스템 콜 다음 명령으로 옮김
즉 프로그램에서 IO를 하기 위해서는
먼저 프로그램에서 소프트웨어 인터럽트를 통해 OS에게 IO를 요청한다.
그리고 IO device에서 IO가 끝나면 하드웨어 인터럽트가 발생한다.
현대의 운영체제는 인터럽트에 의해 구동된다!
인터럽트가 들어올 때만 OS가 CPU를 제어하게 된다.
✅ 위 내용 정리
CPU에 program counter (pc)라는 레지스터가 있다. pc는 CPU가 메모리의 어느 주소에서 instruction을 읽어와야 할지를 가리킨다.
실행하고 나면 pc가 4byte 증가하며 순차적으로 다음 instruction을 실행하게 된다.
하지만 제어흐름에 따라 jump해서 멀리있는 instruction을 실행할 수도 있다.
즉 CPU는 아주 빠른 일꾼이고 pc가 가리키는 메모리 주소에 있는 instruction을 하나 읽어서 실행하는 일만 주구장창 하게 된다.
그런데, instruction을 실행하고 다음 instruction을 실행하기 전에 하는 일이 있다. interrupt가 들어왔는지 확인하고 interrupt가 들어오면 하던 작업을 잠시 멈추게 되고 CPU 제어권이 운영체제에 넘어간다.
운영체제는 인터럽트 마다 왜 처리해야 할 함수가 커널 함수로 정의되어 있다. 인터럽트 벡터(인터럽트 번호와 인터럽트 처리 루틴의 주소 쌍을 갖고 있는)에 의해 알맞은 인터럽트 처리 루틴을 수행하게 된다.
mode bit이 0 : 운영체제가 CPU를 갖고 있을 때, 모든 명령을 다 수행할 수 있다. 다른 프로그램의 메모리에 접근하거나, IO 기기에 접근하거나
mode bit이 1 : 사용자 프로그램이 CPU를 가지고 있을 때는 한정된 명령만 수행할 수 있다. 사용자 프로그램은 100% 믿을 수 없기 때문.
IO device를 접근하는 모든 instruction은 mode bit이 0일때만, 즉 OS만 실행할 수 있다.
사용자 프로그램이 IO 작업을 해야 한다면 운영체제에게 요청을 해야 한다. 그러기 위해서는 pc가 운영체제의 주소로 점프해야 하는데 이것은 mode bit이 1일때 못한다. 따라서 사용자 프로그램이 운영체제에게 서비스를 요청할 때는 system call을 하게 된다. 운영체제의 함수를 사용자 프로그램이 요청하는 것이다. 바로 점프는 불가능하고 의도적으로 interrupt line을 세팅한다. cpu는 interrupt line이 세팅되었으므로 하던 일을 멈추고 cpu의 제어권이 os에게 넘어가게 된다.
timer라는 하드웨어를 통해 운영체제가 사용자 프로그램에게 CPU를 넘겨줄 때는 타이머에게 시간 세팅을 한 다음에 넘겨준다. 할당된 시간이 끝나면 timer가 cpu에게 인터럽트를 걸어주고 사용자 프로그램으로부터 cpu를 뺏어서 OS가 다시 cpu를 얻게 된다.
즉 운영체제는 timer의 도움을 받아 여러 프로그램이 cpu에 의해 실행되도록 할 수 있다.
✅ 동기 식 입출력과 비동기식 입출력
✅ 동기식 입출력 synchronous IO
IO 요청 후 입출력 작업이 완료된 후에야 제어가 사용자 프로그램에 넘어감
쓰라고 한 다음 실제 써지는지 확인
그림을 보면 이해가 쉽다.
사용자 프로그램은 커널을 통해 입출력을 하고 입출력이 끝날 때까지 기다린 후(waiting) 끝난 후에야 다음 작업을 한다.
✅ 비동기식 입출력 asynchronous IO
IO가 시작된 후 입출력 작업이 끝나기를 기다리지 않고 제어가 사용자 프로그램에 즉시 넘어감
쓰라고 한 다음에 다른 일을 함
IO 작업 요청을 해놓고 그것을 기다리지 않고 CPU 제어권을 얻어서 다른 작업들을 한다.
동기, 비동기 모두 IO가 끝났다는 것은 IO device의 컨트롤러가 인터럽트를 걸어서 알려주게 됨
디스크에서 읽어오는 프로그램 : 보통 디스크에서 읽어온 결과를 보고 나머지 프로그램이 작동함. IO 요청을 했으면 요청된 IO 데이터를 읽어 와야 다음 작업이 가능한다. 즉 읽어오는 작업은 보통 동기식 입출력이다.
내가 IO 요청을 했지만, 그 결과를 보지 않고, 즉 그 결과와 상관 없이 할 수 있는 작업이 있다. 그동안 그 작업을 하도록 프로그램을 짤 수 있다. read지만 비동기식으로 할 수도 있다.
write의 경우 정말 출력이 제대로 되었는지 확인해야만 다음 작업을 할 수 있는 경우는 거의 없고 비동기식이 보통 자연스럽다.
write도 하지만 동기식으로 해야 할 때도 있다.
구현하기 나름..
✅ 동기식 입출력 구현방법
IO 요청은 오래걸리는 작업이다. 그동안 CPU를 가지고 있으면서 아무것도 안하면 낭비가 된다. 즉 보통은 동기식 IO를 할 때, 그 프로세스는 CPU를 가지고 있어봤자 기다리느라 아무것도 못한다.
✅ 구현방법 1
✅ DMA : Direct Memory Access
메모리를 접근할 수 있는 장치
원래는 메모리에 접근할 수 있는 장치는 CPU 밖에 없다.
IO 장치들이 메모리에 접근하기 위해 CPU에게 인터럽트를 건다
IO 장치가 워낙 다양하고, CPU가 인터럽트를 너무 많이 당한다.
CPU가 효율적으로 동작하기가 힘들다.
그래서 DMA를 두고 DMA가 메모리에 접근할 수 있게 한다.
IO device의 버퍼에 특정 크기만큼 데이터가 쌓이면 CPU에게 한번에 인터럽트를 걸도록 한다.
즉 바이트 단위가 아니라 block 단위로 인터럽트를 발생시킨다.
✅ 서로 다른 입출력 명령어
CPU에서 실행하는 instruction에는 메모리만 접근해도 되는 instruction이 있고, IO 장치에 접근해야 하는 instruction이 있다.
메모리 주소가 있듯이 IO 주소가 있어서 특정 device에 대해 주소로 접근한다.
즉 IO를 수행하는 special instruction에 의해 IO가 발생한다
즉 메모리 접근하는 명렁어 따로, IO 수행하는 명령어 따로
반면에 IO device에 memory address를 주고 메모리 접근하는 명령어로 즉 Memory mapped IO가 가능하다.
✅ 저장장치 계층 구조
맨 위에는 CPU가 있다.
CPU에는 레지스터가 있다.
캐시 메모리가 있다. 캐시 메모리가 CPU에 있기도 한다.
그다음 메인 메모리가 있다.
secondary storage는 하드디스크 등..
위로 갈수록 속도가 빠르고 단위공간당 가격이 비싸고 (따라서 용량이 적다) 휘발성이다.
하드디스크, 테잎은 전원이 나가도 휘발되지 않는다.
반면 dram, cache memory로 사용되는 sram, cpu 안의 register 등은 전원이 나가면 내용이 사라지는 휘발성이다.
primary 저장장치들이 일반적으로 휘발성이다.
Primary 저장장치들은 CPU에서 직접 접근 가능하며 executable 하다.
CPU가 직접 접근해서 처리하지 못하는 것들을 secondary 저장장치라 한다.
CPU가 직접 접근 가능하려면 byte단위로 접근이 가능해야 한다.
✅ 캐싱
위로 갈수록 용량이 적기 때문에 아래쪽의 내용을 전부 위에 올려놓지는 못한다. 위로 갈수록 빠르고 밑으로 갈수록 느리다.
CPU의 처리속도와 저장장치에서 읽는 속도의 차이를 완충하기 위해 캐시메모리와 레지스터가 있는 것이다.
캐시메모리는 main memory보다 용량이 작아서 모든걸 다 담아놓지는 못한다. 당장 필요한 것만 밑에서 위로 올려다 쓰는 것을 caching이라 한다.
빠른 매체로 정보를 읽어들여서 쓰는 것.
caching은 재사용을 목적으로 한다. 같은 것을 두번째 요청할 때는 밑에까지 가지 않고 이미 위로 읽어온 데이터를 가져가는 것.
✅ 프로그램의 실행
프로그램은 보통 실행파일 형태로 하드디스크에 저장되어 있다.
실행파일을 실행시키게 되면 메모리로 올라가서 프로세스가 된다.
정확하게는 바로 물리적 메모리에 올라가는 것이 아니라 가상 메모리를 거치게 된다.
프로그램을 실행시키게 되면 그 프로그램의 주소공간 address space이 형성된다. 프로그램마다 각각 0번지부터 시작하는 그 프로그램만의 독자적인 메모리 주소 공간이 생기게 된다.
주소 공간은 code, data, stack으로 이루어진다.
code는 cpu에서 실행할 기계어 코드를 담고 있다.
data는 변수같은 프로그램을 실행할 때 사용하는 자료구조를 담고 있다.
stack은 code가 함수 구조이므로 함수를 호출하고 리턴할 때 데이터를 쌓았다가 꺼내가는 용도이다.
모든 프로그램이 독자적인 주소 공간을 갖는데, 이것을 물리적인 메모리에 올려서 실행을 시킨다.
커널은 주소공간은 컴퓨터를 부팅하면 항상 메모리에 상주하고 있다.
반면 각 프로세스의 주소공간은 주소공간이 생겼다가 프로그램을 종료시키면 사라진다.
프로그램을 실행시켰을 때 만들어진 주소공간은 물리적인 메모리에 통째로 다 올리지 않는다. 그러면 메모리가 낭비되기 때문이다.
당장 필요한 부분만 메모리에 올린다. 그래야 메모리가 낭비되지 않는다.
주소공간에서 당장 필요한 부분은 메모리에 올려서 사용하고, 아닌 부분은 disk의 swap area에 내려놓는다.
File System의 디스크는 말 그대로 전원이 꺼져도 파일을 유지하기 위한 디스크 공간이다.
Swap area의 데이터는 전원이 나가면 의미가 없다. 메모리 공간의 한계로 메모리 공간의 연장선으로 존재하는 공간이다.
물리적인 메모리도 0번지부터 시작하는 주소공간이다.
따라서 가상메모리에서 물리적인 메모리로 올라가며 주소가 바뀌게 되는데 이것을 주소변환 address translation이라 하며 주소변환을 해주는 계층이 있다. 이것은 OS가 하는 것이 아니고 주소변환을 해주는 하드웨어 장치가 있다.
커널도 하나의 프로그램이므로 code data stack 구조로 구성된다.
커널의 code에는 운영체제가 하는 일들에 대한 코드가 있다.
자원을 효율적으로 관리하게 하는 코드들이 있다.
또 사용자에게 편리한 인터페이스를 제공하는 코드들이 있다.
운영체제는 언제 CPU를 얻게 되느냐? 인터럽트가 들어오면 얻게 된다.
각각의 인터럽트마다 무슨 일을 처리해야 하는지 운영체제 커널에 함수 형태로 정의되어 있다.
커널의 data 부분에는 운영체제가 사용하는 자료구조가 정의되어 있다.
운영체제는 CPU같은 자원을 관리하고 통제한다. 하드웨어 종류마다 (CPU, mem, disk 마다 ) 그 하드웨어를 관리하기 위한 자료구조가 필요하다.
또한 운영체제는 process들을 관리한다. 각 프로세스를 관리하기 위한 자료구조도 필요한다. 그것을 PCB, process control block이라 한다. 시스템 안에 프로그램이 하나 돌아가면 그 프로그램을 관리하기 위한 자료구조가 운영체제 커널에 하나씩 만들어지는데 이것이 PCB이다.
운영체제도 함수 구조로 코드가 짜여져 있어서 함수를 호출하고 리턴할 때 stack 영역을 사용한다. 이것을 위해 커널 stack 이 있다. 운영체제 코드는 여러 사용자 프로그램들의 요청에 따라 불러서 쓸 수 가 있다. 어떤 사용자 프로그램이 커널의 함수를 호출했는지 알기 위해서 사용자 프로그램마다 커널 스택을 따로 둔다.
✅ 함수
함수는 3가지 종류가 있다
사용자 정의 함수던, 라이브러리 함수 던 프로세스의 address space의 code 부분에 들어가 있다.
커널 함수는 커널 address space의 code 안에 정의가 되어 있다.
사용자 정의 함수나 라이브러리 함수는 프로그램을 실행시키게 되면 사용자 프로세서의 code 영역에 들어가 있기 때문에 그 안에서 점프를 하며 호출을 하는 것이고, 커널 함수는 시스템 콜을 통해 호출해야 한다. (실제로 점프도 안되고, 사용자 프로그램은 OS 에 접근할 수 없으니까)
✅ 프로그램의 실행
프로그램이 CPU를 잡고 있으면 user mode 라고한다.
유저모드에서 사용자 정의 함수나 라이브러리 함수를 사용해도 여전히 user mode이다.
system call을 하게 되면 운영체제 커널의 주소공간에 있는 함수를 호출하므로 kernel mode가 된다.
system call이 끝나면 프로그램에게 다시 CPU 제어권이 넘오오고 다시 user mode가 된다.
프로그램은 태어나서 죽을 때까지 유저모드, 커널모드 .. 를 반복하다가 종료된다.