Computer Science/Introduction

9혼자 공부하는 컴퓨터구조 + 운영체제: 3. 입출력 장치, 운영체제, 프로세스와 스레드

컴퓨터 탐험가 찰리 2022. 12. 12. 19:46
728x90
반응형

 

8. 입출력 장치

 

장치 컨트롤러와 드라이버

 
장치 컨트롤러 필요성
컴퓨터와 데이터를 주고 받는 모든 장치(마이크, 스피커, 프린터 등)들은 장치컨트롤러를 사용한다.

  • 입출력 구조가 다양하여 중간자 역할을 하는 장치 컨트롤러가 있어야 CPU와 규격화된 통신을 할 수 있다.
  • CPU에 비해 데이터 전송률이 일반적으로 낮으므로 장치 컨트롤러의 버퍼가 필요하다.

 

 
 
장치 컨트롤러 기본 구조
장치 컨트롤러 내부에는 버퍼 역할을 하는 데이터 레지스터, 입출력 장치의 상태를 표시하는 상태 레지스터, 입출력장치가 수행할 내용을 저장하는 제어 레지스터가 있다.
 

 
 
 
장치 드라이버
장치 컨트롤러의 동작을 감지하고 제어하는 프로그램. 각 운영체제가 이 장치 드라이버를 인식하고 실행한다. 프로그램이기 때문에 메모리에 저장되어 실행된다.
 
 
 
 
 
 

입출력 방식

 
1. 프로그램 입출력(programmed I/O)
CPU의 입출력 장치 관련 명령어에 의해 입출력이 이루어지는 방식이다. 위에서 언급한 장치 컨트롤러의 데이터, 상태, 제어 레지스터에 명령어들을 읽고 쓰는 방식으로 작동한다.
 
CPU가 각 장치들의 레지스터 주소를 확인하는 방법은 다음 두 가지가 있다.

  • 메모리 맵 입출력(memory mapped I/O): 메모리의 입출력을 위한 명령어, 주소와 동일한 방식을 사용한다. 예를 들어 1,024개의 주소 할당을 위한 공간 중 절반인 512개는 메모리용 주소, 나머지 512개는 입출력 장치를 위한 주소로 할당할 수 있다.
  • 고립형 입출력(isolated I/O): 명령어와 주소 공간을 메모리와는 독립적으로 사용하는 방식이다.

 
 
2. 인터럽트 기반 입출력(Interrupt Driven I/O)
장치 컨트롤러가 작업을 모두 수행하고 CPU에게 인터럽트 요청을 보내어 CPU-장치간 정보를 교환하는 방식이다.
 
폴링(polling): CPU는 주기적으로 장치 컨트롤러의 상태 레지스터를 확인하여 입출력 장치의 상태를 확인한다. CPU에 부담이 가는 방식이며 인터럽트 기반 입출력은 이와 대조되는 개념이라고 할 수 있다.
 
 
프로그래머블 인터럽트 컨트롤러(Programmable Interrupt Controller, PIC): 수많은 입출력 장치의 인터럽트를 우선순위별로 처리하기 위해 PIC라는 하드웨어를 사용한다. 양쪽으로 여러 핀이 다리처럼 나와있는 구조인데 가령 첫 번째 핀은 타이머의 인터럽트, 두 번째 핀은 키보드의 인터럽트를 받아들이는 핀 등으로 미리 CPU에 인터럽트를 보낼 수 있는 약속된 인터페이스로 구성된다.
 

https://www.jameco.com/z/8259C-5-Major-Brands-8259-Programmable-Interrupt-Controller-DIP-28_52783.html

 

http://www.embeddedlinux.org.cn/rtconforembsys/5107final/LiB0062.html

 
3. DMA(Direct Memory Access) 입출력
CPU의 부담을 줄이기 위해 입출력 장치 관련 데이터 관리와 제어를 DMA 컨트롤러라는 추가적인 하드웨어에 일임하는 방식이다. 다음 방식으로 입출력이 이루어진다.
 

  • CPU -> DMA 컨트롤러로 입출력장치 주소, 수행할 연산 등의 데이터 및 명령어를 전달
  • DMA -> 각 입출력 장치로 작업 수행. 필요한 경우 DMA 컨트롤러가 직접 메모리에 접근
  • DMA -> CPU로 작업 완료를 알리는 인터럽트 발생

 
입출력 버스(input/output bus): CPU, DMA 컨트롤러, 입출력 장치간 데이터 전송은 시스템 버스를 통해서 전기 신호가 전송되는 방식으로 이루어진다. 이 신호 전송 선도 CPU와 입출력용으로 구분하여 시스템 버스의 부담을 줄이기 위한 것이 입출력 버스이다. 대표적으로 PCI(Peripheral Component Interconnect) 버스, PCI Express(PCIe) 버스 등 여러 종류가 있다.
 
입출력 프로세서(Input/Output Processor, IOP): 최근에는 입출력 장치 내부에 CPU를 별도로 장착하여 본 CPU에 인터럽트 요청만 하는 구조로 성능을 극대화 시키는 장치들도 등장하고 있다.
 
 
 
 


 
 

9. 운영 체제

 
실행할 프로그램에 자원들을 할당하고, 프로그램이 올바르게 실행되도록 돕는 특별한 프로그램
 

커널(kernel)과 이중모드(dual mode)

 
커널

  • 운영체제는 메모리상 커널 영역(kernel space)에 따로 적재된다. 일반 프로그램들은 사용자 영역(user space)에 할당된다.
  • 운영체제에서 자원에 접근하고 조작하는 기능, 프로그램을 안전하게 실행하는 기능 등 핵심 기능을 담당하는 부분을 커널이라고 부른다.

 
이중 모드

  • CPU가 커널 모드로 실행되면 커널 영역의 코드를 실행하여 하드웨어나 자원에 접근할 수 있게된다.
  • CPU가 사용자 모드로 실행되면 하드웨어 자원과 관련된 명령어는 실행할 수 없는 상태가 된다.
  • 사용자 모드로 실행되는 프로그램이 운영체제에게 자원을 요청하기 위해 커널 모드로 전환하는 요청을 시스템 요청(시스템 콜, system call) 이라 한다.
사용자 영역에서의 시스템 호출로 커널 영역 이동
시스템콜과 실행 흐름

 

시스템 콜의 대략적인 종류

 
 
 
가상 머신(Virtual Machine)과 하이퍼 바이저 모드(HyperViser, Hyper-V)
현대 CPU는 소프트웨어적으로 가상 컴퓨터를 컴퓨터 내에 만들어낼 수 있다. 다만, 이 가상 머신은 사용자 모드로 실행되는 응용 프로그램 중 하나로 인식되기 때문에 커널 영역의 코드를 사용할 수 없다. 따라서 가상화를 지원하는 CPU는 가상 머신을 위한 하이퍼 바이저 모드를 따로 둔다. 이 하이퍼바이저모드에 의해 가상 머신 상의 프로그램들은 가상머신에 설치된 운영체제로부터 운영 체제의 서비스를 받을 수 있게 된다.
 
 
 
 


 

10. 프로세스와 스레드

 

프로세스

  • 프로세스: 실행 중인 프로그램
  • 포그라운드(foreground) 프로세스: 사용자가 볼 수 있는 공간에서 실행되는 프로세스
  • 백그라운드 프로세스: 사용자와 상호작용은 하지 않고 정해진 일을 수행하는 프로세스. 유닉스에서는 데몬(daemon), 윈도우에서는 서비스(service)라고 부른다.

 
 
프로세스 제어 블록(PCB, Process Control Block)
CPU가 각 프로세스를 빠르게 번갈아가며 작업을 수행하는데, 이때 각 프로세스들을 구분하기 위해 필요한 정보가 PCB이다.
PCB는 프로세스 생성 시 커널 영역에 만들어지고, 실행이 끝나면 폐기된다.
 
PCB가 갖는 정보를 나열해보았다.

  • 프로세스ID(PID, Process ID): 프로세스를 식별하기 위한 고유번호. 같은 프로그램이라도 두 번 실행되면 개별 PID가 할당된다.
  • 레지스터값: 각 프로세스는 자신의 실행 차례에 맞춰 진행 중이던 값을 복원해야하므로, 이를 레지스터에 담아 놓는다.
  • 프로세스 상태: CPU 대기, 입출력 장치 대기, CPU 사용 중 등
  • CPU 스케줄링 정보: 언제 CPU를 할당받을지에 대한 정보
  • 메모리 관리 정보: 메모리 내 프로세스가 어떤 위치인지에 대한 정보. 베이스 레지스터, 한계 레지스터, 페이지 테이블 정보 등이 담긴다.
  • 사용한 파일과 입출력 목록

 
문맥 교환(context switching)
하나의 프로세스에서 다른 프로세스로 넘어가는 과정. 이때 각 PCB에는 중간 진행 정보가 담긴다.
 
 
프로세스 메모리 영역

일반적으로 메모리 영역의 중복을 피하기 위해 스택 영역은 높은 주소에서 낮은 주소로, 힙 영역은 낮은 주소에서 높은 주소로 할당된다.
 

  • 코드 영역(code segment, 텍스트 영역(text segment)): CPU가 실행할 명령어가 기계어로 작성되어 있다. 쓰기가 금지되며 읽기 전용 공간이다.
  • 데이터 영역(data segment): 프로그램이 실행되는 동안 유지할 데이터가 저장되는 공간. 전역 변수(global variable)이 대표적이다.
  • 힙 영역(heap segment): 프로그래머가 직접 할당할 수 있는 공간이다. 할당된 공간의 사용이 끝났다면 해당 공간을 반환해줘야한다. 그렇지 않으면 메모리 누수(memory leak)가 발생할 수 있다.
  • 스택 영역(stack segment): 일시적인 데이터를 보관하는 장소이다. 매개 변수, 지역 변수가 대표적이다.

 
 

프로세스 상태와 계층 구조

 

  • 생성(new): 프로세스가 생성되고 PCB가 커널영역에 등록된 상태
  • 준비(ready): CPU를 할당받아 실행할 수 있는 상태
  • 실행(running): CPU에 의해 실행 중인 상태
  • 대기(blocked): 입출력, 이벤트 등에 의해 해당 작업이 완료되길 기다리는 상태
  • 종료(terminated): 프로세스가 종료되면 운영체제는 PCB와 프로세스가 사용한 메모리를 정리한다.

 
부모-자식 프로세스
프로세스는 계층 구조를 가져, 실행 중 또 다른 프로세스를 생성할 수 있다. 트리 구조 형태로 프로세스의 가계도를 그릴 수 있으며, 이것을 프로세스 계층 구조라 한다. PPID(Parent PID)로 부모 프로세스의 PID를 따로 표시하는 경우도 있다.
 
- 최초의 프로세스
최초의 프로세스를 유닉스에서는 init, 리눅스에서는 systemd, macOS에서는 launchd라고 한다. 이 프로세스들은 항상 PID 1번이 할당된다.
 
- 자식 프로세스의 구체적인 생성 과정
부모 프로세스 fork -> 부모 프로세스와 메모리 내용 등 똑같은 프로세스가 상속됨 -> exec 시스템 호출을 통해 자식 프로세스의 메모리에 새로운 프로그램을 덮어씀
만약 부모 프로세스를 병렬적으로 실행하는 경우에는 exec을 호출하지 않는다.
 
 
 
 

스레드

프로세스를 구성하는 실행 흐름의 단위이다.
 

하나의 프로세스에서 여러 스레드(명령어)가 동시에 실행될 수 있다.
 
 
<책의 참고 자료> : https://github.com/kangtegong/self-learning-cs/blob/main/thread/thread_python.md
-파이썬에서 멀티 스레딩 해보기

01 import threading
02 import os
03 
04 def foo():
05     print('thread id', threading.get_native_id())
06     print('process id', os.getpid())
07 
08 if __name__ == '__main__':
09     print('process id', os.getpid())
10     thread1 = threading.Thread(target=foo).start()
11     thread2 = threading.Thread(target=foo).start()
12     thread3 = threading.Thread(target=foo).start()

실행을 해보면, 프로세스의 pid는 3개의 스레드 모두 같은 값으로 나오는데 반해, 스레드의 id값들은 모두 다르게 출력된다. 하나의 프로세스가 공유되며 각 스레드가 실행되는 것이다.
 
 

01 import threading
02 import os
03 
04 def foo():
05     print('This is foo')
06  
07 def bar():
08     print('This is bar')
09  
10 def baz():
11     print('This is baz')
12 
13 if __name__ == '__main__':
14     thread1 = threading.Thread(target=foo).start()
15     thread2 = threading.Thread(target=bar).start()
16     thread3 = threading.Thread(target=baz).start()

각자 다른 역할을 수행하도록 스레드를 구성할 수 있다. 다만 싱글 코어를 가정한다면 이 세 개의 스레드가 완전히 동시에 수행되는 것이 아니라 세 스레드를 아주 빠르게 번갈아가며 처리하기 때문에 마치 병렬적으로 처리하는 것처럼 보인다.
 
그래서 리눅스를 만든 개발자 리누스 토발즈는 리눅스에서 프로세스와 스레드를 딱히 구분하지 않도록 했다고 한다. 프로세스와 스레드는 정확히는 개념적 차이일뿐 실행의 진짜 주체는 CPU이기 때문이다.
 
 
 

스레드는 실행에 필요한 최소 정보인 프로그램 카운터, 레지스터, 스택만을 갖고 실행된다. 나머지 정보는 프로세스의 데이터, 힙 영역에 있는 데이터들을 공유한다.
 
멀티 스레드 환경에서는 공유 자원이 있기 때문에 하나의 스레드에 문제가 생기면 전체 스레드에 문제가 생길 수 있다.
 
 
+ 프로세스간 통신
기본적으로 프로세스 간에는 자원을 공유하지 않으나, 프로세스 간 통신(IPC, Inter-Process Communication)으로 자원을 공유할 수 있다. 또한 공유 메모리(shared memory)라는 공통의 메모리 영역을 두어 데이터를 주고 받을 수 있다.
 
 
 
 
 
 

728x90
반응형