본문 바로가기

카테고리 없음

[01] 불친절한 Hello, World!

불친절한 C++ 노트는 작성자가 C++을 복습한 내용을 적은 것이며,
학부생 수준의 운영체제(리눅스)나 컴퓨터 구조 관점에서 C++을 바라봅니다.

Hello, World!

프로그래머란 직업은 참으로 겸손합니다.

무엇이든 처음보는 상대를 마주하면 인사부터 하기 때문이죠.

  • Hello, World!
  • Hello, C++!
  • Hello, Web Server!


그 중에서 "Hello, World!"는 가장 일반적이고 간단한 인사법으로 유명하지만,

안에 숨겨진 내부적인 동작원리는 결코 간단하지 않습니다.


아래의 단순한 프로그램이 얼마나 복잡한 매커니즘 위에서 움직이는지,

차분하게 알아보도록 합시다.



Standard Streams

스트림(Stream)이란 입출력 대상과의 추상화된 데이터 흐름(Abstracted Data Flow)이며,

통신방법(How data is communicated)이 추상화된 데이터 통신을 의미합니다.


데이터를 전송하기 위해 모든 일련의 전송과정을 제어하는 대신,

전송과정을 추상화하여 데이터에만 집중하자는 것이죠.




데이터가 움직이는 방향에 따라 입력 스트림과 출력 스트림으로 나뉘지만.

데이터의 방향이 다른 것 뿐이지 근본적인 차이는 없습니다.

  • 데이터 송신자

  • 데이터 수신자

  • 데이터 채널




OS는 프로세스가 만들어지면 3개의 통신채널(Communication Channels)을 생성하는데,

각각 표준 입력/표준 출력/표준 에러라고 부르며, 묶어서 표준 스트림(Standard Streams)이라고 합니다.


우리는 이 표준 스트림을 통해 키보드와 디스플레이와 통신할 수 있는데,

표준 스트림은 터미널 키보드터미널 디스플레이와의 I/O를 추상화한 통신채널이기 때문입니다.



Standard Streams are NOT Unique

표준 스트림이 키보드와 모니터를 추상화한 것은 맞지만,
모든 프로세스가 하나의 표준 스트림을 공유하는 것은 아닙니다.

실제 키보드와 디스플레이를 추상화한 것이 아닌,
터미널마다 가지고 있는 가상 키보드(Terminal Keyboard)와 가상 디스플레이(Terminal Display)를 추상화했기 때문입니다.

각자의 고유한 가상장치를 사용하기 때문에,
서로다른 터미널에서의 표준 스트림 추상화 대상은 서로 다릅니다.

이것이 A 터미널에 출력한 데이터가 
B 터미널에 출력되지 않는 이유입니다.


하지만 유닉스 기반의 운영체제에서는 이러한 가상장치가 파일의 형태로 노출되어 있으며,
/proc/<pid>/fd/ 디렉토리에서 해당 장치를 볼 수 있습니다.

이것을 이용하면 외부에서 콘솔의 I/O 동작에 관여가 가능하고,

아래처럼 아무것도 없는 프로그램에 출력을 찍히게 할 수 있죠.




Raw Input

그렇다면 실제 키보드(Real Keyboard)의 데이터는 어떻게 터미널 키보드(Terminal Keyboard)로 전송될까? 

먼저 입력장치에 대해 알아볼 필요가 있습니다.



데이터를 생성하여 컴퓨터에게 제공하는 주변장치를 입력장치라고 하며,

사람이 쉽게 사용할 수 있다는 의미로  HIDs(Human Interface Devices) 라고 합니다.


예를 들면 아래와 같은 장치들이 되겠죠.

  • 키보드, 마우스, 터치스크린

  • 조이스틱, 컨트롤러

  • 마이크



하지만 곧바로 입력장치를 사용할 수 없습니다.

유닉스에서는 입력장치를 사용하기 위해서는 아래 2가지의 모듈이 필요합니다.

  • 드라이버

  • 핸들러


드라이버는 장치와 어떻게 통신할 것인지 정의하고, 데이터가 도착하면 하드웨어 인터럽트를 발생시킵니다.
핸들러는 인터럽트가 감지되었을 때, 장치로부터 얻은 데이터를 어떻게 활용할 것인지 정의하죠.


먼저, 키보드가 신호를 발생시키면 CPU는 이것을 감지해야 합니다.
CPU가 신호를 알아차리는 방법에는 2가지가 있죠.
  • Pooling
  • Interrupt
하지만 Pooling 방식은 CPU와 입출력장치와의 속도차이가 괴랄하기 때문에,
키보드를 하염없이 기다릴 수 없으므로 Interrupt 방식을 사용합니다.

CPU 사이클을 계속해서 돌다가 인터럽트가 ON이 된 것을 감지하면,
키보드 버퍼에서 데이터를 가져가는 것이죠.



이렇게 가져간 데이터는 핸들러가 처리하는데,
보통 키보드 핸들러는 다음과 같이 동작합니다.
  • 1) 시스템상 특수키인지 검사한다. (예를들면, 윈도우에서 Ctrl + Alt + Delete)
  • 2) 프로그램상 특수키인지 검사한다. (예를들면, 크롬 브라우저에서 Ctrl + F)
  • 3) 여기까지 왔다면, 포커싱된 프로세스의 표준 입력으로 넘긴다.

만약 포커싱된 프로세스가 표준 입력을 받을 수 없는 상황이라면 해당 데이터는 무시됩니다.



이 때, 입출력장치에서 바로 가져온 데이터를 Raw Input이라고 하는데,

다음과 같은 정보가 포함되어 있습니다.

  • 데이터를 발생시킨 장치의 정보

  • 데이터가 발생된 시간 (Time-Stamp)

  • 데이터 타입 (KEY_PRESS,   KEY_RELEASE,   KEY_REPEAT)

  • 데이터 값


연결된 입력장치를 OPEN 하면 Raw Input을 얻어올 수 있습니다.

즉, Raw Input을 사용하면 포커싱되지 않은 상황에서도 키보드의 입력을 얻어올 수 있습니다.


민감한 주제이므로 루트권한이 필요합니다.



참고자료

TCP School # 기본적인 입출력

Wikipedia # Standard Streams

Stack Exchange # How to view the output of a running process in another bash session?

How stuff works # From the Keyboard to the Computer

Linux Kernal Document # Input Interface

컴퓨터 시스템 구조 3rd Edition (M. Morris Mano, 김종상 역) # 입출력과 인터럽트