이 영역을 누르면 첫 페이지로 이동
시간의화살 블로그의 첫 페이지로 이동

시간의화살

페이지 맨 위로 올라가기

시간의화살

행복하세요

[Operating System] I/O Management

  • 2025.05.11 15:07
  • Computer Science/Operating System

 

 

 

I/O 장치는 프린터, 키보드처럼 사람이 사용하는 장치, 디스크 / 센서 / 모뎀 등 장치들 간 통신을 위해 사용되는 장치로 구분된다.

일반적으로 사람이 다루는 장치는 데이터 전송률이 낮고, 장치들 사이의 입출력을 담당하는 장치는 데이터 전송률이 높다. 

 

바이트 단위로 데이터를 주고받는 장치를 문자형 장치, 블럭 단위로 데이터를 주고받는 장치를 블럭 장치라고 부른다.

 

입출력 장치의 종류는 다양한데.. 운영체제는 모든 장치를 지원해야 한다.

 

 

 

 

 

하드디스크에서는 금속 판에 정보가 저장된다. 

Disk Arm은 금속 판의 중앙부터 가장자리로 이동하며 정보를 읽고, 모터를 통해 움직인다. 

 

디스크 하단에는 컨트롤러 보드가 있고, 컨트롤러 보드에는 여러 레지스터들이 부착되어있다. 

운영체제가 장치에 명령을 내리면 Control Register의 특정 값을 1로 설정한다.

Status Register는 상태를 기록하는 레지스터로, 데이터가 입력됐는지, 오류가 발생했는지, 입력이 끝났는지를 저장한다. 

 

레지스터의 값들은 모두 미리 약속되어있고, 정해진 약속대로 운영체제가 값을 조작한다고 생각하자. 

레지스터를 직접 조작하는 방식을 사용하는데.. 하드디스크를 만드는 회사는 여러 곳이고, 회사마다 장치의 레지스터를 조작하는 방식이 모두 다르다. 

 

운영체제가 모든 회사에서 나오는 디바이스들의 레지스터를 조작하는 방식을 아는건 불가능하다.

새로 나오는 하드웨어도 대응할 수 있어야 하고.. 

 

 

 

Controller는 Adapter라고도 불린다.

 

Device Driver는 컨트롤러를 직접 바라보고 하드웨어에 접근해 Control Register의 값을 조작하고 Status Register의 값을 읽어오는 역할을 수행한다.

 즉, 하드웨어를 직접 접근하는 요소는 Device Driver이고, 장치 제조사에서는 드라이버를 직접 구현해야 한다. 

 

드라이버는 소프트웨어로 장치 제조사에서 직접 만들고, 장치를 제어하기 위해 커널의 일부가 되어야 한다.

드라이버를 모듈 형태로 구현해 특정 장치를 연결하면 Plug & Play로 디바이스 드라이버 모듈을 커널과 붙여 줘야 한다. 

 

드라이버와 커널 간의 통신은 device-driver interface를 통해 이루어진다. 

당연히.. 드라이버 소프트웨어를 구현 할 때도 인터페이스의 규약대로 구현해 줘야 한다. 

 

현재 연결돼 사용되는 디바이스 드라이버만 메모리에 올려놓고 나머지는 보조기억장치에 저장해서 사용한다. 

 

 

 

위의 그림과 같은 그림이다.

응용 프로그램은 시스템 콜을 보내고.. OS의 I/O Management를 담당하는 부분이 해당 요청을 받고 어떤 장치를 통해 입출력이 처리되는지 파악한다.

 

컴퓨터는 장치를 파일로 간주하고 처리하기에 device-driver interface를 정의할 때도 file system에서 정의된 file operation을 사용한다.

 

 

Kernel I/O Management는 공통적인 제어 기능을 담당한다.

 

Device Reservation

말 그대로 장치를 예약하고 할당한다.

응용 프로그램에서 입출력 요청이 들어오면, 해당 요청을 처리할 장치를 선택하는데.. 장치가 사용 중이면 대기 큐에 넣고 기다리게 된다.

 

Device Scheduling

여러 출력 요청이 큐에 들어온 경우 다음으로 어떤 요청을 처리할지를 결정한다.

입출력을 요청한 순서대로 처리하는 것도 방법이지만, 장치에 따라서는 들어온 순서대로 처리하는게 성능이 좋지 않은 경우가 있다. 

 

Error Handling

입출력 과정 중 발생한 오류를 처리한다. 

우선 오류가 발생하면 Control Register에 해당 내용이 기록되고, 그 내용을 디바이스 드라이버가 읽어 오류를 감지한다.

드라이버는 인터페이스를 통해 Kernel I/O Management에 보고하고, 커널은 오류를 처리한다. (처리 방법은 장치마다 다름)

 

Buffering

입출력 할 내용을 메모리에 임시로 저장한다.

입출력 장치의 처리속도와 CPU의 처리속도는 다르다. CPU의 속도가 훨씬 빠르니 속도를 맞춰 줘야 한다. 

CPU가 한번에 많은 양의 데이터를 입력장치에게 준 경우 출력장치의 부담이 커질 수 있으니..

응용 프로그램이 많은 양의 데이터를 장치에 보낼 경우 임시로 커널이 받아서 입출력 장치에게 조금씩 전달해준다.

 

컴퓨터 네트워크의 전송 프로토콜, Copy Semantics를 맞추기 위해서도 버퍼링을 사용한다.

 

Caching

저속 입출력장치에서 작업한 내용을 메모리에 저장한다.

이 캐시를 Kernel I/O Management가 관리한다.

 

Spooling 

주변 장치와 컴퓨터 간 데이터를 전송할 때 처리 지연을 단축할 때 사용한다.

메인 메모리의 버퍼가 부족할 수 있으니 보조 기억 장치의 일부를 버퍼링에 사용한다. 

 

 

 


 

 

 

 

인터럽트가 발생했을 때, 커널에 진입하는 방법으로는 세 가지가 있다. - Interrupt, Trap, System Call

 

 

Interrupt

발생하는 시점이 정해지지 않은 비동기적인 이벤트가 발생했음을 주변 pheripheral device가 운영체제에게 알린다.

 

 

 

tty, disk 등에서 입력이 끝나면 PIC에 신호를 보내고, PIC는 CPU로 신호를 중개한다.

clock은 자주 인터럽트를 걸기 때문에 PIC 장치와 독립적으로 작동한다.

 

CPU는 명령어 하나를 실행한 후 인터럽트가 발생했는지를 확인한다. 

인터럽트가 걸리지 않았으면 다음 명령어를 실행하고, 인터럽트가 걸렸으면 레지스터 값을 커널 스택에 저장하고 mode change로 커널에 진입하는데, 이미 커널 스택에 들어와서 작업하고 있던 경우 mode change하지 않고 레지스터 값만 저장한다. 

 

커널 내부에서는 Interupt Handler 함수를 실행한다. 

유저 프로그램을 실행할 때 인터럽트가 걸렸다면 유저 프로그램을 실행할 당시의 레지스터 값을 커널 스택에 저장하고, 

커널 코드를 실행할 때 인터럽트가 걸렸다면 또 그때 당시의 레지스터 값을 커널 스택 윗부분에 저장하고 Interrupt Handler를 실행한다. 

각 유저 프로세스는 자신만의 커널 스택을 가지고, 커널 스택은 컨텍스트 저장, 중첩 인터럽트 처리, 시스템 콜 처리에 사용된다.

 

Interrupt Handler는 누가 인터럽트를 걸었는지를 걸었는지를 확인한다. 

clock이 걸었는지, PIC를 통해 걸렸다면 몇 번 장치가 인터럽트를 걸었는지 확인하고 해당 번호를 받아온다.

 

Interrupt Vector Table (Interrupt Descriptor Table)에는 각 장치 번호별로 인터럽트를 처리할 함수인 Interrupt Service Routines가 정의되어 있고, Interrupt Handler는 Interrupt Service Routines를 호출하는 중간 다리 역할을 수행한다. 

 

Time Sharing에서는 Time Slice만큼의 CPU시간이 주어지는데, 이 시간이 끝났는지 확인할 때는 clock이 사용된다. (100ms 사용)

리눅스에서 Clock Interrupt는 1ms마다 걸리고, 그 때 마다 Interrupt Handler가 Interrupt Service Routines의 0번 주소인 timer_intr() 함수를 실행한다. 

해당 함수는 counter 값을 하나씩 증가시키고, 값이 100이 됐다면 Time Slice가 만료됐으니 스케쥴러를 호출한다. 

 

100ms 말고 1ms를 인터럽트 실행 단위로 설정한 이유는.. CPU가 운영체제를 최대한 많이 실행시키기 위해서이다. (커널 코드 실행)

시그널이 왔는지 조사하거나, 우선순위가 높은 프로세스를 처리한다거나, 인터럽트 처리를 마저 처리한다거나.. 이런 작업들을 커널에 들어갔을 때 처리해 줘야 한다.

100ms마다 clock interrupt가 걸린다고 하면, 공백기가 최대 100ms 까지 늘어날 수 있으니 위의 세 가지 작업이 밀릴 수 있다. 

급한 일을 빠르게 처리하게 하기 위해.. 아래 그림에서 빨간 부분을 거의 실시간으로 수행하기 위해 1ms단위로 인터럽트를 건다.

일단 리눅스는 1ms단위로 걸지만, 실시간 패치를 사용하면 1ms보다 더 짧은 단위로 인터럽트를 걸 수 있다.

 

커널에서 중요한 작업을 수행할 때 인터럽트가 걸린 경우, 인터럽트를 처리해 줘야 하는데.. 인터럽트 처리가 오래 걸리는 경우 진행 중이던 중요한 작업을 미루게 된다.

 

ISR은 인터럽트를 처리하는 부분을 두 가지로 구분해서 처리한다. 

우선 급하게 처리해야 하는 인터럽트만 처리하고, 나머지 부분(bottom half)은 잠시 미뤄두고 커널에서 수행하는 중요한 작업을 처리한 후 이어서 처리한다. 

 

 

 

 

 

유저 프로그램을 실행하던 중 인터럽트가 걸리면 커널 모드로 전환한다.

인터럽트를 처리하던 중 새로운 인터럽트가 걸리면 기존 인터럽트 처리를 중단하고 새 인터럽트를 처리를 시작한다. 

 

Interrupt Handler를 통해 ISR을 호출하는데, 이 때 실행하는 부분이 "급하게 처리해야 하는 부분"이다.

이런 식으로 인터럽트가 걸릴 때 마다 새로운 인터럽트를 처리하되, 해당 인터럽트의 급한 부분을 먼저 처리하는 방식으로 동작한다.

 

위의 사진에서 빨간색으로 칠해진 부분에서 급하지 않은 부분을 한 번에 처리한다.

 

 

Trap

Software Interrupt라고도 불리며, 이벤트가 발생하는 시점이 정해진 동기 방식이다.

CPU가 Software Instruction을 실행하는 사이클 내에서 인터럽트를 발생시키기 때문에 동기 방식이다. 

 

divide_by_zero / invalid machine code / segmentation fault / protection fault / page fault 

CPU가 해석할 수 없는 명령어가 들어온다거나, 허용되지 않은 메모리 영역을 조작한다거나.. 비정상적인 접근이 발생할 때 Trap이 실행된다.

 

Trap을 감지하는 주체는 CPU이니 Trap이 Software Interrupt라고 하지만 실제로 인터럽트는 Hardware가 건다.

즉, CPU가 Trap Handler를 수행하기 위해 자신에게 인터럽트를 건다. 

 

Trap은 Interrupt와 똑같이 처리된다. (Interrupt Handler -> ISR)

 

 

System Call

커널 함수를 불러서 유저 프로세스가 원하는 운영체제 서비스를 받는 작업이다.

프로그램 실행 중 입출력 명령어도 System Call로 변환된다.

 

유저 프로그램에서 시스템 콜이 등장하면, 컴파일 할 때 컴파일러가 시스템 콜에 해당되는 어셈블리 명령어로 변환한다.

fork, open, read, socket 등 시스템 콜은 여러 개의 어셈블리 명령어로 변환되는데, 그 중 하나가 int $0x80 이라는 명령어이다.

 

 

 

 

여기서 int는 interrupt이고, 80은 16진수로 128번을 의미한다.

IDT의 128번지로 가면 System Call Handler 함수의 주소가 있고, Handler를 통해 실제 시스템 콜을 처리하는 함수를 호출한다.

 

시스템 콜의 종류가 다양하고, 255까지가 최대인 IDT에 모든 시스템 콜을 넣을 수 없기에 두 단계를 거쳐서 시스템 콜을 호출한다.

(유닉스의 Indirect 와 유사)

 

새로운 시스템 콜을 만들 때는 IDT는 그대로 두고 sys_call_table의 빈 칸에 함수를 기록하고 새로 컴파일 해 주자.

 

 

 


 

 

운영체제가 장치를 제어하는 방법으로는 크게 세 가지가 있다. - Polling, Interrupt-Driven I/O, Direct Memory Access

 

 

Polling 

 

 

사용자 프로그램에서 printf 등 출력문은 컴파일 될 때 출력에 해당하는 시스템 콜로 변환된다. 

 

이 때 mode change를 수행하고 CPU가 Kernel에 할당된 후 Kernal I/O Management가 실제로 입출력을 담당하는 출력 장치가 뭔지 확인한다.

 

출력 장치가 모니터라고 하면 모니터의 디바이스 드라이버에게 출력을 지시하는데, 이 때 인터페이스의 함수를 호출하고 CPU를 Device Driver에게 할당한다. 

 

드라이버는 장치가 사용 가능한지 확인하고 명령을 내려야 한다.

이 때 장치의 busy bit를 읽고, 해당 값이 1이라면 장치를 사용할 수 있을 때 까지 기다린다.

 

해당 값이 0으로 바뀌면 control register의 write bit 값을 1로 설정하고 쓸 내용을 data-out register에 작성한다.

 

이후 control register의 command-ready bit를 1로 설정하고, 이 값을 컨트롤러가 읽어 busy bit를 1로 설정한다. 

 

장치는 앞서 설정된 레지스터의 값을 읽고 명령을 처리한 후 command-ready bit와 busy bit를 0으로 바꾼다. 

 

 

 

status register 값을 확인하면서 상태를 체크하는 작업을 Poll이라고 부른다.

CPU가 다른 작업을 수행하지 않고 드라이버 소프트웨어를 실행해 반복적으로 값을 읽어오는 작업을 Polling, Programmed I/O 라고 부른다.

 

입출력이 완료되면 다시 Kernel I/O Management에게 CPU를 할당하고, user mode로 돌아가기 전에 새로운 시그널이 도착했는지, 새로운 프로세스가 Ready 상태가 됐는지 등을 처리한다. 

 

Polling 과정에서는 Context Switch가 전혀 발생하지 않고, Busy Waiting을 실행하기에 입출력이 느린 장치에서는 매우 비효율적이다.

 

 

Interrupt I/O 

 

 

user mode에서 실행되는 프로그램이 printf 명령어를 처리한다고 하자.

3번 과정까지는 Polling과 동일하다.

 

Polling에서는 디바이스 드라이버를 반복적으로 실행하면서 출력이 끝날 때 까지 Busy-Waiting 하며 기다렸는데, 이 부분이 비효율적이기에 Interrupt-Driven I/O가 도입됐다. 

 

Interrupt-Driven I/O에서는 출력 명령을 내려놓고 프로세스 관리자로 넘어가 다른 프로세스로 Context Switch를 실행한다.

Process Management를 실행하고 A의 상태를 Block 상태로 변경하는 과정 까지는 CPU가 A에게 할당되어있음에 주의하자.

 

Polling에서는 busy bit가 0이 됐는지를 계속 확인했는데, 지금은 CPU가 드라이버에 할당되지 않았으니 계속해서 확인할 수 없다.

장치에서 출력이 마무리되면 busy bit와 command bit를 0으로 변경하고, 컨트롤러가 CPU에게 IRQ(Interrupt Request)를 보내 출력이 끝났음을 알려준다.

 

CPU는 사용자 코드 한 줄을 실행하고 인터럽트가 걸렸는지를 확인하는데, 인터럽트가 걸렸다면 레지스터 값을 커널 스택에 저장하고 mode change로 커널로 접근해 Interrupt Handler를 호출한다.

당연히.. 커널 모드에서 작업을 처리하고 있었다면 mode change를 하지 않아도 된다.

 

Interrupt Handler를 호출하는 주체는 Context Switch된 프로세스 B임에 집중하자.

이후 ISR이 실행되고 인터럽트를 처리하는데, Process Management는 프로세스 A를 Ready상태로 바꿔준다. 

 

인터럽트 처리가 마무리된 후 Return from interrupt를 실행해 미뤄둔 인터럽트 처리, 도착한 시그널 확인, Ready 상태로 들어온 프로세스 확인 등을 처리한다.

여기서 프로세스 A가 Ready 상태가 됐으니 스케쥴러가 실행되고 스케쥴링 알고리즘에 따라서 A나 B가 실행된다. 

A가 우선순위가 더 높다면 Context Switch가 한 번 더 발생한다. 

 

A입장에서는 장치의 입출력이 끝날 때 까지는 작업을 진행할 수 없어 어차피 가만히 있어야 한다. 

프로세스 B만 작업 처리 중 인터럽트로 방해받아 A만 유리하고 B는 손해를 보는 방식인 것 같지만.. 꼭 그런 건 아니다. 

 

Time Sharing 기반으로 처리되는 경우 A에게 Time Slice가 주어지고, 이후 B에게 Time Slice가 주어진다.

Polling 방식을 사용할 경우 A의 작업이 끝날 때 까지 B는 CPU를 할당받을 수 없는데, Interupt-Driven I/O에서는 A가 CPU를 양보했으니 B는 CPU를 좀 더 일찍 할당받을 수 있다.

 

입출력이 빠르면 Polling을 쓰는게 더 효율적이고, Interrupt-Driven I/O에서는 입출력이 빠르면 비효율적이다.

Context Switch작업은 오버헤드가 크다. A의 레지스터 값을 PCB에 저장하고.. B의 컨텍스트를 복구해주고.. 

Context Switch도중에 입출력이 끝나 인터럽트가 걸리면 다시 되돌아와야 하는데.. 너무 비효율적이다. 

 

인터럽트가 걸리더라도 Polling 방식으로 처리하는 경우 Context Switch가 발생하지 않고, Interrupt-Driven I/O 방식으로 처리하면 프로세스의 우선순위에 따라 Context Switch가 발생할 수도 있다.

 

 

Direct Memory Access

 

 

입출력은 CPU, I/O Controller, I/O Device를 통해 이루어진다.

 

출력 대상이 들어있는 메모리에서 버스를 통해 CPU 레지스터로 가져온다. 

이후 CPU는 해당 내용을 I/O 장치로 보내고 출력을 진행한다.

 

입력된 내용은 I/O 장치를 통해 CPU로 전달되고, 다시 CPU를 통해 메모리로 전달된다. 

 

이렇듯 입출력은 항상 CPU를 경유해서 이루어지는데, 입출력할 데이터가 적으면 아무 문제 없지만..

입출력할 데이터가 많아지면 오고가는 사이클이 많아지고 그만큼 인터럽트도 많이 걸리게 되고 CPU의 효율이 떨어진다.

 

DMA는 입출력 데이터가 CPU 대신 DMA Controller라는 프로세서를 거치도록 한다. 

 

CPU는 DMA 컨트롤러에게 출력 명령을 내리고, DMA 컨트롤러는 메모리를 읽어서 자신에게 가져오고 해당 I/O 장치에게 전달해 출력을 진행한다. 

보조 프로세서 DMA를 사용하니 CPU는 그동안 사용자 프로그램을 실행할 수 있게 돼 CPU를 효율적으로 사용할 수 있다.

 

 

반응형
저작자표시 (새창열림)

'Computer Science > Operating System' 카테고리의 다른 글

[Operating System] Memory Management  (0) 2025.05.17
[Operating System] Disk Scheduling  (0) 2025.05.13
[Operating System] File System  (0) 2025.05.10
[Operating System] Deadlock and Starvation  (0) 2025.04.13
[Operating System] Semaphore  (0) 2025.04.08

댓글

이 글 공유하기

  • 구독하기

    구독하기

  • 카카오톡

    카카오톡

  • 라인

    라인

  • 트위터

    트위터

  • Facebook

    Facebook

  • 카카오스토리

    카카오스토리

  • 밴드

    밴드

  • 네이버 블로그

    네이버 블로그

  • Pocket

    Pocket

  • Evernote

    Evernote

다른 글

  • [Operating System] Memory Management

    [Operating System] Memory Management

    2025.05.17
  • [Operating System] Disk Scheduling

    [Operating System] Disk Scheduling

    2025.05.13
  • [Operating System] File System

    [Operating System] File System

    2025.05.10
  • [Operating System] Deadlock and Starvation

    [Operating System] Deadlock and Starvation

    2025.04.13
다른 글 더 둘러보기

정보

시간의화살 블로그의 첫 페이지로 이동

시간의화살

  • 시간의화살의 첫 페이지로 이동

검색

방문자

  • 전체 방문자
  • 오늘
  • 어제

카테고리

  • 분류 전체보기 (606) N
    • Algorithm (205)
      • Data Structure (5)
      • Theory && Tip (33)
      • Baekjoon (166)
      • ALGOSPOT (1)
    • Spring (123)
      • Spring (28)
      • Spring Web MVC (20)
      • Spring Database (14)
      • Spring Boot (6)
      • Spring 3.1 (11)
      • Spring Batch (6)
      • Spring Security (16)
      • JPA (12)
      • Spring Data JPA (5)
      • QueryDSL (4)
      • eGovFramework (1)
    • Programming Language (74)
      • Java (19)
      • JavaScript (15)
      • C (25)
      • C++ (12)
      • Python (1)
      • PHP (2)
    • Computer Science (69) N
      • Operating System (18)
      • Computer Network (17) N
      • System Programming (22)
      • Universial Programming Lang.. (8)
      • Computer Architecture (4)
    • Database (21)
      • Database (7)
      • MySQL (3)
      • Oracle (3)
      • Redis (5)
      • Elasticsearch (3)
    • DevOps (20)
      • Docker && Kubernetes (8)
      • Jenkins (4)
      • Github Actions (0)
      • Amazon Web Service (8)
    • Machine Learning (28)
      • AI Introduction (28)
    • Mobile (28)
      • Android (21)
      • Flutter (7)
    • Solutions (13)
    • Life Logs (0)
    • 낙서장 (25)

최근 글

나의 외부 링크

메뉴

  • 홈

정보

13months의 시간의화살

시간의화살

13months

블로그 구독하기

  • 구독하기
  • RSS 피드

티스토리

  • 티스토리 홈
  • 이 블로그 관리하기
  • 글쓰기
Powered by Tistory / Kakao. Copyright © 13months.

티스토리툴바