일반적인 OS의 구성 요소, 라이브러리(library) 및 단독 프로그램
자바 개발을 항간에 배우면서, wrapper라는 함수의 유용성을 들은 적이 있다. 일반적인 정수형 타입(integer)을 클래스형으로 바꿔주는 함수였는데, 리눅스의 커널에서 시스템 콜만을 받는 역할을 하는 함수도 'Wrapper'(무엇을 감싸다)라고 한다.
즉, 사용자 모드에 있는 프로세스에서 디바이스 조작을 위해 커널을 호출해야 할 경우 시스템 콜이 이루어지는데, 이때의 인터럽트(interrupt, 방해) 과정 간에 사용자 모드를 커널이 감싸서(wrapping) 커널 모드로 진입하는 것을 지칭하는 거라고 생각했다. 마치 우주선이 도킹스테이션에 도킹되어 행성의 궤도를 돌기 전의 과정과 같이 말이다.
자바와 같은 객체지향 언어의 경우도 단독 프로그램의 일부(메서드, 특정 기능을 하는 프로그램 내장 함수)가 하드웨어를 조작(화면 출력을 통해 사용자에게 프로그램 결과물 보여줌)까지 가기 위해서는 OS 외의 라이브러리(단독 프로그램) 혹은 OS 라이브러리(내장 함수)를 통해 시스템 콜을 해야 하고 시스템 콜을 호출하기 위해 커널 모드를 통해 커널이 해당 프로세스(프로그램 실행)의 처리를 해야 하드웨어의 조작이 가능하다.
그림에서처럼 시스템 전체를 들여다보면 시스템에는 애플리케이션이나 미들웨어뿐만 아니라 OS에서 제공하는 프로그램도 여러 가지 있다. 이번 편에서는 시스템 콜, OS가 제공하는 라이브러리, OS가 제공하는 프로그램에 대해 설명하고 왜 필요한지 설명해보겠다(다케우치, 2019).
프로세스는 프로세스의 생성이나 하드웨어의 조작 등 커널의 도움이 필요할 경우 시스템 콜을 통해 커널에 처리를 요청한다. 다음은 시스템의 콜의 종류이다.
프로세스 생성, 삭제 → 주문 생성, 취소
메모리 확보, 해제 → 한 번에 주문받을 수 있는 가용량 확보, 해제
프로세스 간 통신(IPC) ☞ '더 이상의 비유는 어렵다. -작자'
네트워크
파일 시스템 다루기
파일 다루기(디바이스 접근)
아래는 C 언어로 ppidloop라는 프로그램을 만들어서 sar라는 명령어를 통해 CPU가 사용자 모드에서 프로세스를 실행하고 있는 비율(%user와 %nice 항목의 합계)의 점유율과 커널 모드에서 프로세스를 실행하고 있는 비율(%system 필드)을 확인한 값이다.
일단, ppidloop라는 프로그램은 아래와 같은 코드로 무한 루프(for문, while과 같은 구조) 속에서 getppid()라는 부모 프로세스 호출을 한다. 재귀 함수와 같다.
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
for ( ; ; )
getppid();
}
위의 프로그램이 CPU에서 사용자 모드와 커널 모드를 얼만큼의 비율로 부하를 주는지 시스템 통계치(system statics)를 보여주는 명령어가 sar 명령이다(명령어는 CentOS일 경우 yum install sysstat 명령구문으로 다운로드 가능). 위의 화면 맨 아래 sar의 실행결과를 보면, ppidloop가 백그라운드에서 실행되고 있을 때, %user가 60.40%, %system이 39.60%이다. 독해하면,
CPU 0 내에서만(가상 머신 생성 시, CPU 코어를 1개만 생성했다;) ppidloop 프로그램을 60%의 비율로 실행하고 있었다.
ppidloop 프로그램의 요청에 대응하여 이 프로그램의 부모 프로세스를 얻는 커널의 처리를 39% 비율로 실행하고 있었다.
왜 %user(사용자 모드)이 100%가 아니고 ppidloop 프로그램이 60%를 차지할까? 바로 main() 함수 안에 getppid()를 무한루프 시키기 위한 루핑 처리를 하는 데 ppidloop 프로그램이 프로세스를 사용하고 있기 때문이다(다케우치, 2019).
즉, 단독 프로그램인 ppidloop 프로그램의 점유율(사용자 모드)을 제외하고서라도, 로우 레벨(저수준) 단의 커널이 개입('시스템 콜' 호출)하는 부모 프로세스 호출 과정이 39% 정도로 프로세스 점유율에서 차지하고 있다. 여기서 사용자 모드가 아닌 커널 모드가 동작하는 타이밍과 비율을 나타내면 아래 그림과 같다.
대체로 %system의 수치가 크면(수십 이상) 시스템 콜이 너무 많이 호출되고 있거나 시스템에 과부하가 걸려 있는 등의 좋지 않은 상태를 의미한다(다케우치, 2019).
strace(프로그램이 어떤 시스템 콜을 호출하는지 살펴보기 위한 명령어)에 '-tt'라는 옵션을 붙이면 로우 레벨단에서 각 개별 시스템 콜이 호출될 때의 시간을 마이크로초 단위로까지 표시할 수 있다(아래 화면 노란색 형광 부분). 'strace -tt -o hello.log ./hello'라는 명령 구문을 통해 시스템 콜이 이루어지는 부분에서 write() 시스템 콜(데이터를 화면이나 파일 등에 출력하는 디바이스 조작 함수)이 호출되었다는 자리다. "hello world\n" 문자열이 화면에 출력하고 있다.
리눅스에는 프로그램의 작성을 도와주기 위해 대부분의 프로세스에 필요한 여러 라이브러리 함수가 있다. 시스템 콜은 보통의 함수 호출과는 다르게 C 언어 등의 고급언어에서는 직접 호출이 불가능하다. 그래서 아키텍처에 의존하는 어셈블리 코드를 사용해 호출할 필요가 있는데, x86_64 아키텍처에서는 getppid() 시스템 콜을 다음*과 같이 호출한다(다케우치, 2019).
mov $0x63,$eax
syscall
만약 OS(운영체제, 고놈의 식당 운영원리를 떠올려 보시라...)의 도움이 없다면 각 프로그램은 시스템 콜을 호출할 때마다 다음 그림과 같이 아키텍처에 의존하는 어셈블리 언어를 써서 고급언어로부터 어셈블리 코드를 호출해야만 했을 거다(OS의 생성 연유).
이 방식은 프로그램을 작성하는 데 시간이 오래 걸릴 뿐만 아니라, 다른 아키텍처에도 사용할 수 없어 이식성도 매우 낮으며 이식할 수 없는 경우도 있다(Intel x86의 아키텍처뿐만이 아니라, ARM 등).
이러한 문제를 해결하기 위해서 OS는 내부적으로 시스템 콜을 호출하는 일만 하는 함수를 제공하는데 이를 시스템 콜 'wrapper'라고 한다. wrapper 함수는 아키텍처별로 존재한다. 고급언어로 써진 사용자 프로그램부터 각 언어에 대응하여 준비된 시스템 콜의 wrappper 함수를 호출하기만 하면 된다(다케우치, 2019)
두 번째 실습 화면의 세 번째의 ldd 명령을 통해 /usr/bin/python 명령어에 어떤 라이브러리가 포함(링크)되어 있는지 확인할 수 있는데, 여기에서는 표준 C 라이브러리(libc) 뿐만 아니라, 파이썬 자체에 내부 라이브러리도 포함되어 있는 것을 볼 수 있었다. 보통은 GNU(GNU is Not Unix의 약자로 재귀되어지는 이니셜의 프로젝트명)가 제공하는 glibc를 표준 C 라이브러리로 사용한다. 대부분의 C 프로그램은 glibc를 링크하고 있다(그래서 C 언어가 하드웨어 구조, 다시 말해 OS가 어떻게 동작하는지 커널 라이브러리를 통해 쉽게 이해하는데 적당하다).
glibc는 시스템 콜의 wrapper 함수를 포함한다. 또한 POSIX 규격**에 정의된 함수도 제공한다. 앞서 파이썬 명령어에 어떤 라이브러리가 링크되어 있는지에서 파이썬 자체는 내부적으로 표준 C 라이브러리를 사용하고 있다는 것을 볼 수 있다고 했는데, 최근에는 C 언어를 직접 사용하는 개발자가 드물어졌지만 OS 레벨에서는 매우 중요한 언어임을 알 수 있다.
● 시스템 초기화 : init
● OS의 동작을 바꿈 : sysctl, nice, sync
● 파일/디렉터리 관련 : touch, mkdir
● 텍스트 데이터 가공 : grep, sort, uniq
● 성능 측정 : sar, iostat
● 컴파일러 : gcc
● 스크립트 언어 실행 환경 : perl, python, ruby
● 셸 : bash
● 윈도 시스템 : X
* 첫 행에서는 getppid의 시스템 콜 번호 '0x6e'를 eax 레지스터에 대입한다. 이것은 리눅스의 시스템 콜을 호출하는 규약(POSIX)에 의해 정해진 사항이다. 또한 둘째행에서는 syscall 명령을 통해 시스템 콜을 호출하고 커널 모드로 변환하고 있다. 이 뒤에 getppid를 처리하는 커널의 코드가 실행된다(다케우치, 2019).
** 유닉스 계열의 OS가 갖추어야 할 각종 기능을 정해둔 규격이다.
다음 편부터는 OS의 나머지 역할에 대해서 살펴볼 차례다.
발췌
0) 다케우치 시토루. (2019). 실습과 그림으로 배우는 리눅스 구조 (pp. 46-47). n.p.: 한빛미디어.