메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

앱이 끝나고 시스템이 시작하는 곳 : 고급 언어를 떠받치고 있는 시스템 콜과 제어 흐름 살펴보기

한빛미디어

|

2015-02-09

|

by 한빛

19,884

제공 : 한빛 네트워크
저자 : Martin Kalin
역자 : 한순보
원문 : Where apps end and the system begins


Martin Kalin편집자 노트: 시스템이 동작하는 방법을 완전히 이해하는 것이 얼마나 중요한지, 그리고 그 핵심 부분이 우리 일상 활동에 영향을 어떻게 미치는지를 가끔은 잊어버릴 수 있다. Martin Kalin은 우리에게 조금 더 깊은 몰두와 계산 사고력 강화를 요구한다.
프로세서, 메모리, 대개는 키보드나 화면 같은 입출력 장치 등 시스템 자원이 애플리케이션을 실행하는 데 필요하다는 것은 분명하다. 애플리케이션이 운영체제(OS)의 제어 아래에서 공유 자원에 접근하는 방법은 덜 분명하다. OS는 훌륭한 관리자처럼 애플리케이션의 자원 요청을 다룰 때 효율적이며 겉으로 드러나지 않는다. 애플리케이션과 OS의 상호작용 방법을 통상적(routine)인 방식과 극적인(dramatic) 방식 모두 살펴보자.

print문을 실행하면 어떤 일이 일어나는지 살펴보자. Ruby 예제가 있다. 
puts "Hello, world!"

 

Ruby에서 puts 문은 표준 C 라이브러리의 고수준 입출력 함수(printf)로의 콜(call)을 래핑(wrap)하고 자원을 요청하는 애플리케이션과 자원을 제공하는 OS 루틴 간 인터페이스 역할을 한다. 표준 라이브러리는 OS와 매끄럽게 상호 작용하며 약간의 어셈블리 언어와 함께 C로 작성됐다. 라이브러리 함수 printf는 이름의 f가 나타내는 것처럼 바이트를 정수, 실수, 그리고 Hello, world!와 같은 문자열 포맷으로 만들 수 있으므로 고수준이다. 시스템 관점에서는 Ruby 애플리케이션과 C 라이브러리 함수는 사용자 영역에서 실행되며 화면 같은 시스템 자원을 제어하는데 필요한 권한(right)과 특권(privilege)을 제공하지는 않는다.

printf 콜은 요청 프로세스 시작만 하고 write라는 저수준, 바이트 기반 라이브러리 함수로 진행한다.
	
write(1, "Hello, world!", 14);
write의 첫 번째 인자는 파일을 나타내는 정수 값인 파일 디스크립터이다. 이 예제에서 1은 표준 출력을 나타내며 기본값은 화면이다. 최신 OS에서 입출력 장치는 파일에 포함한다. 두 번째 인자는 C에서는 1byte char의 배열인 문자열(string)이다. 8bit 문자는 여기서 사용하는 7bit ASCII 문자 코드를 위한 충분한 공간을 제공한다. 마지막 인자 14는 쓰일 byte 수이다. 차례로 write 콜은 커널 영역에서 실행하는 OS 루틴을 일으키며 화면 같은 시스템 자원을 제어하는데 필요한 바로 그 자원과 특권을 제공한다. 이 예에서는 write 콜이 성공하면 화면에 쓰인 byte 수인 14를 리턴한다. 요청이 실패하면 write는 오류를 표시하는 데 종종 사용되는 특정한 값인 -1(바이너리로 모든 값이 1)을 리턴한다. 



fig1

[그림 1] 통상적인 시스템 콜 


요약하면, Ruby 같은 고급 언어에서 통상적인 print 문은 printf 같은 표준 C 라이브러리 함수를 지나가고, write 같은 시스템 수준의 함수를 실행하고, 차례로 화면 같은 공유 자원을 관리하는 책임이 있는 OS 루틴을 일으킨다(그림 1 참고). puts로의 Ruby 콜은 사실상 시스템 콜에서 종료되는 OS 요청이며, 시스템 콜은 그 요청을 허락하거나 거절한다.


명시적 시스템 콜

특별한 라이브러리 함수 syscall은 C 코드가 시스템 콜을 보통의 라이브러리가 완전히 거부하거나 다루기 어렵게 하는 방식을 사용해 세부적으로 조정할 수 있도록 한다. 적절한 이름이 붙은 syscall 함수는 특정한 콜에 따라 0개에서 5개의 다양한 인자 개수를 가진다.

 

[예제 1.1] 명시적 시스템 콜

#include <unistd.h>

#include <sys/syscall.h>

#include <errno.h>

#include <stdio.h>

void main() {

 /* 755는 소유자(owner)는 읽기/쓰기/실행 권한을 갖고 

다른 사람들(others)은 읽기/실행 권한을 가진다는 것을 의미 */

 int perms = 0755; 

 int status = syscall(SYS_chmod, "/usr/local/website", perms);

 if (-1 == status) printf("chmod failed: errno = %in", errno);

 else printf("chmod succeededn");

}

 

 

callExplicit 프로그램(예제 1.1)은 syscall 문법을 보여준다. 이 콜은 OS 루틴 chmod를 대상으로 하며 같은 이름을 가진 래퍼가 표준 라이브러리에 있다. 래퍼 함수인 chmod를 직접 실행할 수 있어 다소 부자연스러운 예이다. 예제에서 다양한 인자 개수를 갖는 syscall은 다음과 같은 인자가 있다.
  • SYS_chmod: 호출되는 시스템 함수(chmod, change mode)를 나타내는 정수 값을 위한 심볼릭 상수(C의 매크로)이다. chmod 함수는 디렉터리를 포함하는 파일의 접근 권한을 바꿀 수 있다.
  • /usr/local/website: 다른 파일을 포함할 수 있는 일종의 파일인 디렉터리이다. 디렉터리의 접근 권한은 syscall을 통해 바꿀 수 있다. website라는 이름은 동적 콘텐츠 전달을 위한 웹 사이트에서 자주 사용되는 종류의 실행 가능한 스크립트를 포함하는 디렉터리라는 것을 암시한다. 디렉터리 이름은 임의로 정할 수 있지만, 오류를 피하기 위해 반드시 디렉터리가 존재해야 한다. 
  • perms: 0755라는 정수 값으로 디렉터리 소유자(owner)의 읽기/쓰기/실행 권한과 다른 사람들(others)의 읽기/실행 권한을 나타낸다. 값은 0으로 시작하는 것이 나타내듯이 8진수이다.

 

syscall 함수는 상태 코드로 정수 값을 리턴한다. 관습상 리턴 값 0은 성공적인 요청을 나타내며 음수 값은 오류를 나타낸다. syscall 함수는 오류를 나타내기 위해 항상 -1을 리턴한다. 그러면 특정한 오류 코드가 errno 변수에 저장된다. 그러한 오류 코드를 위한 심볼릭 상수가 있다. 예를 들면, EPERM의 errno 값인 1은 실행 권한이 없다는 의미이다. ENOENT의 errno 값인 2는 그러한 항목(entry)이 없다는 의미이다.
애플리케이션과 시스템의 상호작용은 지금까지 보여준 예보다 더 극적일 수 있다. 사용자가 키보드에서 Control-C를 입력하거나, 명령 줄 혹은 다른 프로그램에서 kill 명령을 실행해 애플리케이션을 종료할 수 있다. 배열 범위를 벗어나는 (예, 배열의 끝을 넘어서면) 실행 중인 프로그램은 일반 보호 폴트(general-protection fault)를 생성하며, 보통은 OS가 프로그램을 즉시 종료하게 한다. 프로그램의 일반적인 제어 흐름을 바꾸는 시스템 콜을 살펴보자.


시스템 콜과 예외적인 제어 흐름

C++, Java, C# 같은 고급 언어는 일반적인 프로그램 제어 흐름을 갑자기 바꾸는 예외(exception)라는 개념을 바탕으로 만들어진 프로그래밍 구조가 있다. 이런 언어에서 실행 중인 구문은 (예, throw 문으로) 명시적 혹은 (예, 파일 읽기를 위해 존재하지 않는 파일 열어) 암시적 예외를 발생시킨다. 

 

[예제 2.1] 예외적인 제어 흐름

try {

 s1  ;; throws an exception   //line 1

 s2

 s3

catch(Exception e) {print(e)}  //line 2 

 

 

try 코드 조각(그림 2.1)이 예외 처리 구조의 의사 코드(pseudo-code)를 나타낸다. try 블록의 아무 구문이나 예외를 발생할 수 있으며 일반적인 제어 흐름을 변경한다. 이 예에서, 일반적인 제어 흐름은 s1에서 시작하여 s2를 거쳐 s3로 진행된다. 구문 s1이 입출력 예외를 생성하는 존재하지 않는 디스크 파일을 연다고 가정해보자. (line 1) 예외는 일반적인 제어 흐름을 중단한다. 구문 s2는 실행되지 않고 대신에 try 블록의 끝에 있는 catch 블록 바디의 print 문을 실행하도록 흐름이 바뀌기 때문이다. (line 2) 이 예에서 예외가 동기적이라고 설명할 수 있다. 이는 제어 흐름의 변화가 실행 중인 특정한 구문(s1)에 의해 일어나고 그것과 동기화되기 때문이다.
제어 흐름의 갑작스러운 변화가 모두 동기적이지는 않다. 예를 들면, 프로그램 P가 명령 줄에서 시작했다면, 키보드로 control-C를 누르면 대개 P의 실행이 종료된다. P는 즉시 멈추며, 이는 제어 흐름의 급진적인 변화의 또 하나의 예가 된다. 중단(abort)은 프로그램에 외부적이고 예측 불가능한 것이 제어 흐름을 변경한다는 점을 강조하여 비동기적으로 설명할 수도 있다.
제어 흐름의 급격한 변화를 설명하는 용어가 확고한 표준화된 의미가 있지는 않다. 하지만 전통적인 용어를 명확하게 할 가치는 있다.
  • 인터럽트(Interrupt) - 인터럽트는 입출력 장치가 만드는 신호에서 발생한다. 입출력 장치인 키보드에서 control-C를 입력하는 예제를 다시 떠올려보자. 이는 실행 중인 프로그램의 중단 신호를 만든다. 시스템 루틴인 OS 인터럽트 핸들러가 프로그램을 종료시켜 반응한다. 이 점에서 인터럽트는 비동기이다. 인터럽트는 실행중인 프로그램에는 외부인 이벤트로부터 생겨난다.
  • 트랩(Trap) - 결국, 전형적인 시스템 콜은 OS 코드를 실행하게 하는 printf 같은 보통의 라이브러리 함수를 호출해 시작한다. 트랩이라는 언어를 사용하여 이 상황을 다른 식으로 표현할 수 있다. 이 printf 콜은 트랩이라는 이벤트를 일으키고 OS 트랩 핸들러가 라이브러리 요청을 적절한 OS 코드에 매핑해서 실행한다. 이 점에서 트랩은 동기적이다. 트랩은 애플리케이션 코드에서 특별한 명령어 실행을 통해 일어난다.

 

다음 두 가지는 트랩의 하위 타입으로 보거나 그들 자체를 하나의 타입으로 볼 수 있다. 어떻게 되든지 Java에서 유사한 것으로 그 차이를 설명할 수 있다. Java Throwable 타입은 두 개의 하위 타입이 있다. Exception은 일반적으로 복구할 수 있다(recoverable). 그래서 애플리케이션은 FileNotFoundException 같은 예외(exception)를 잡으려고(catch) 시도(try)해야 하고, 몇몇 적합한 방식으로 (예를 들면, 기본 파일로 돌려서) 예외를 다루어야 한다. 대조적으로, 어떤 Error는 애플리케이션이 전형적으로 잡고(catch) 복구하려고(recover) 시도하지 않아야 할 만큼 심각하다. 예를 들면, OutOfMemoryError는 애플리케이션이 이 상태로부터 복구할 방법이 없다. 그래서 그러한 오류를 잡고 처리하도록 시도할 지점이 없다. 폴트와 중단의 차이는 비록 정확히 대응되지는 않지만, Exception과 Error의 차이와 유사하다.
  • 폴트(Fault) - 폴트는 애플리케이션이 일반적으로 복구할 수 있지만, 복구가 확실한 것은 아닌 상태이다. 예를 들어, 실행 중인 애플리케이션이 현재 메모리에 있지 않은 데이터나 명령어를 참조한다고 해보자. 이것은 페이지 폴트를 일으킨다. 페이지란 메인 메모리와 디스크 간에 이동하는 고정 길이 바이트 블록을 말한다. (인텔에서 표준 페이지 크기는 약 4KB) 폴트는 OS가 필요한 페이지를 메모리로 로딩하는 곳에서는 복구할 수 있으며, 그렇게 해서 애플리케이션을 계속 실행할 수 있게 한다. 
하지만 몇몇 경우 폴트는 복구할 수 없다. 예를 들면, 실행 중인 애플리케이션이 애플리케이션의 주소 공간 밖의 메모리 위치를 참조하면 OS는 일반 보호 폴트를 생성한다. 이러한 폴트에서는 애플리케이션을 복구할 수 없다. 
  • 중단(Abort) - 중단은 애플리케이션을 복구할 수 없는 심각한 상태이다. 앞서 언급한 일반 보호 폴트는 위반하는 애플리케이션이 중단을 일으킬 때 종료되므로 일반 보호 중단이라는 이름을 붙일 수 있다.

 

특히 중단은 애플리케이션에서 OS로의 모든 요청을 지키지는 않는다는 점을 강조한다. 애플리케이션이 범위를 벗어나는 메모리 위치에 접근 요청을 한다면 OS는 보통 중단의 형태로 강한 no와 함께 응답한다. 비록 시스템 콜이 너무 일상적이라 성공을 기대하더라도 실패 확률은 모든 시스템 콜에 존재한다.

요약할 시간이다. 일상적인 애플리케이션과 시스템의 상호작용에서 애플리케이션은 그 언어의 기본 문법을 사용해 표준 라이브러리 함수를 호출한다. puts에 대한 Ruby 콜이 딱 들어맞는 사례이다. 애플리케이션과 라이브러리 함수는 사용자 공간에서 실행된다. 즉, OS 권한과 특권이 없다. 라이브러리 콜은 시스템 콜을 일으키고 커널 공간에서 실행하는 OS 코드를 실행한다. 일단 OS 콜이 성공적으로 완료되면 애플리케이션은 평상시처럼 계속된다. 애플리케이션과 시스템 상호작용은 그런 상호작용이 애플리케이션의 보통 제어 흐름을 바꿀 때 가장 극적인 결과인 애플리케이션 중단을 일으키며 극적으로 바뀐다.

 

 

TAG :
댓글 입력
자료실

최근 본 책0