거북이의 쉼터

(2022.04.09) Programming Interface 본문

코딩 삽질/OS 요약 정리

(2022.04.09) Programming Interface

onlim 2022. 4. 9. 17:40

수업에서는 꽤 늦게 진행했지만 책에서는 쓰레드 내용보다도 선행해서 나온 내용이다. 아마 중간에 포함된 fork와 exec 때문에 프로젝트 2와 맞춰서 진행하려고 한 것이 의도일 것이다. 오늘은 해당 내용을 정리하겠다. 사실 이게 시험에 많이 나올것 같진 않아....

1. 인터페이스

OS 디자이너는 OS를 설계했기 때문에 HW에 어떻게 동작하는지, 내부의 구체적인 동작이 어떻게 구현되었는지를 모두 알고 있다. 하지만 어플리케이션을 만들고 사용하는 유저 입장에서는 OS의 구체적인 사항을 알지 못한다. 때문에 OS 디자이너는 HW와 OS의 abstraction을 만들고, 유저 측에 해당 abstraction에 대한 application programming interface (API)를 제공해야 한다. 그제야 유저는 비로소 OS 위에서 본인이 원하는 어플리케이션을 작성하고 사용할 수 있기 때문이다.

 

그럼 무엇이 좋은 interface인가? 어떻게 설계해야 하는가? 기왕 만드는 거 좋게 만들어야 많이들 쓸 것 아닌가.

2. API를 설계할 때 고려할 것들

2-1. 커널에 포함되어야 할 기능의 API 설계

좋은 API를 설계하는 것은 어려운 문제이다. 여러 의견이 있는데, 강의 내에서 틀어준 동영상의 강사인 John Ousterhout에 따르면 deep interface를 만들라고 한다. 전체 기능성을 사각형의 면적이라고 하고, 윗 변의 길이를 인터페이스라고 생각하란 것이다. 사각형의 면적이 같더라도, 윗변의 길이가 작을 수록, 사각형이 길어질 것이다. 즉, 적은 수의 인터페이스로도 여러 기능성을 할 수 있도록 하는 것이 좋은 인터페이스 설계라는 뜻이다. 

 

deep interface의 좋은 예시로서 강사분께서 가장 아름답다고 생각하는 인터페이스는 UNIX file I/O 인터페이스라고 한다. 범용성 있고, 단순한 사용에 비해 그 아래에는 수많은 요소가 숨겨져 있다고 한다. 실제로 UNIX는 모든 것을 파일로 취급하기 때문에 모든 파일과 디바이스에 open, close, read, write 등의 같은 system call이 사용될 수 있기 때문에 일률적으로 활용될 수 있는 API이다. 이와 반대로, shallow interface로는 java의 이런 함수를 예로 들었다.

private void addNullValueForAttribute(String attribute) {
	data.put(attribute, "null");
}

정말 단순한 기능성밖에 할 수 없을 뿐더러 함수 이름이 그 기능성의 전부다.

 

좋은 인터페이스의 다른 예시로 이번에는 UNIX 프로세스 관리 API를 살펴보자. 지금부터는 프로세스 생성 및 관리를 위한 API를 설계하는 것에 초점을 맞춰 설명하도록 하겠다. 만약 프로세스를 생성하고 관리하는 기능성을 유저 레벨에서 구현한다면 어떤 문제가 생길까? 일단 표준화되지 못하는 것도 있겠지만 Protection 차원의 문제 또한 심각하다. 커널이 프로세스들을 관리할 수 없다면 어떤 프로세스가 실행될지는 순수히 유저 프로세스에 의해 관리될 것이고, 이는 Dual Mode 설계를 원천 부정하게 된다.

 

따라서 프로세스를 생성 및 관리하는 기능은 반드시 커널에 있어야 한다. 그럼 좋은 API는 어떻게 설계하는가? 이미 좋은 예시가 있다. 바로 UNIX에서 프로세스를 관리하는 방법이다. shell은 job control system으로서, 커널과 유저 사이의 인터페이스를 제공한다. shell에서 유저 프로세스를 생성할 때, shell은 fork와 exec, wait 등의 system call을 활용해서 생성하게 된다. 앞서 설명했듯, 프로세스 생성은 커널 모드에서 이루어져야 하기 때문에 syscall을 사용하는 것이며, 생성된 자식 프로세스의 프로세스 상태는 커널이 관리하게 된다. 궁극적으로는 부모 자식 관계의 프로세스라 하더라도 서로에게 protection이 요구되기 때문이다. 만약 자식 프로세스가 부모 프로세스에 간섭할 수 있게 된다면 shell이 변조될 가능성이 있기 때문에 위험하다.

 

앞서 설명한대로 UNIX shell에서 새로운 프로세스를 형성하는데는 fork, exec이 핵심 역할을 하게 된다. fork는 현재 프로세스의 복사본을 생성하는 system call이며, 원래 프로세스를 부모, 새롭게 생성된 프로세스를 자식이라고 칭한다. 부모와 자식의 코드는 동일하나, fork에서 반환되는 값이 부모와 자식에서 다르기 때문에 분기로서 활용할 수 있다. 자식과 부모 중 어떤 것도 먼저 실행되고 끝날 수 있기 때문에 동기화가 필요하다. exec은 현재 프로세스의 context를 버리고 새로운 프로그램을 메모리에 로딩하여 실행되도록 하는 system call이다. 새로운 프로세스가 생성되는 것이 아니기 때문에 PCB가 생성되지 않으며, exec은 실행되면 원래 프로세스 context로 반환되지 않는다.  

 

UNIX fork는 다음 일련의 작업을 수행해야 한다.

 

  • Create and initialize the process control block (PCB) in the kernel
  • Create a new address space
  • Initialize the address space with a copy of the entire contents of the address space of the parent
  • Inherit the execution context of the parent (e.g., any open files)
  • Inform the scheduler that the new process is ready to run

물론 여기서 copy가 진정한 copy는 아닌 경우가 많다. 커널이 메모리를 아끼고 효율적으로 동작하기 위해서 동일한 물리 메모리가 매핑되도록 하는 sharing을 도입하기 때문이다. 다음으로, UNIX exec은 다음의 일들을 수행해야 한다.

 

  • Load the program into the current address space
  • Copy arguments into memory in the address space
  • Initialize the hardware context to start execution at "start"

이러한 기본적인 fork, exec을 활용하여 복잡한 수준의 기능을 구현할 수 있다. fork를 한 직후에 권한 설정이 가능하다는 점을 이용해 SSH 서버에 접속하기 위한 인증 과정에서는 fork를 두 번 사용하여 privilege를 분리한다. 이로서 악의적 사용자가 root privilege를 사용하는 것을 막고, 일반 사용자는 유저 권한으로서 shell을 사용할 수 있도록 한다.

 

마지막으로 간단한 문제들을 풀면서 끝내도록 하자.

 

Q : UNIX fork는 error를 반환할 수 있는가?

A : 그러하다. OOM (no memory) / pid는 int type이므로 유한한 개수의 프로세스가 실행되고 있어야 하며, 너무 많이 실행될 경우 메모리가 충분하더라도 오류를 반환한다.

 

Q : UNIX exec은 error를 반환할 수 있는가?

A : 그러하다. 실행하려는 파일이 없거나, 파일을 실행할 권한이 없거나, 메모리가 부족하면 오류가 난다.

 

Q : UNIX wait가 바로 반환될 수 있는가?

A : 그러하다. 자식이 먼저 실행되고 exit()으로 종료되었다면 바로 반환되어야 한다.

2-2. 유저 라이브러리에 포함되어야 할 기능의 API 설계

다음으로 유저 라이브러리에는 무슨 기능이 있어야 할지를 생각해보자. 우리가 지금까지 봐 왔던 것에 따르면 syscall은 유저가 여러 인자를 calling convention에 맞춰서 넣고 trap을 일으켜야 정상적으로 동작했다. 유저 입장에서는 이를 일일이 넣어주는 것이 번거로울 뿐더러, 실수할 가능성 또한 있다. 

 

이 때문에 syscall의 복잡한 세부 사항을 은닉하고, 보다 간단하게 사용할 수 있게 하기 위해 유저 라이브러리가 존재하는 것이다. 해당 라이브러리 내부에서 제공하는 함수는 syscall의 wrapper 함수로서, 간단한 함수 호출을 통해 syscall이 가능하도록 도와준다. 예를 들어 printf를 부르면 라이브러리 내부에서 write system call을 호출하는 것이다. syscall 자체의 수가 많고 (리눅스의 경우 332개), 호출 규약 등의 활용이 어렵기 때문에 유저 라이브러리는 유저가 간단히 이러한 기능성을 활용할 수 있도록 wrapper를 제공하는 것이다.

2-3. OS는 어떻게 구성되어야 하는가?

OS를 구성할 때는 두 가지 옵션이 있다. 첫 번째는 모든 기능을 한 커널에 넣는 것이고, 두 번째는 기능성을 커널과 여러 서비스 모듈에 분산하여 넣는 것이다. 첫 번째 커널의 종류를 monolithic kernel이라고 칭하며, 두 번째는 microkernel이라고 한다. 

 

monolithic kernel의 경우, OS 기능이 전부 커널에 들어있다. 때문에 꽤 좋은 수준의 성능을 보여준다. 대표적으로 윈도우, BSD, 리눅스 같은 OS가 여기 해당된다. 다만, 커널 내부 요소들간에는 Protection이 적용되지 않는 특성상, 제 3자가 만든 device driver가 기존 커널과 같이 실행될 때 예상하지 못한 일들이 일어날 수 있는 등 buggy 할 수 있다. 때문에 미사일 발사소 같이 높은 보안 수준이 요구되는 곳에는 적합하지 못하다.

 

이에 비해 microkernel은 OS의 최소한의 기능만을 포함하며, device driver, 메모리 관리 등의 나머지 필요한 기능들은 유저 레벨에 모듈로서 구현한다. 이 때문에 microkernel은 크기 면으로 봤을 때 굉장히 작으며, minimal core services만을 제공한다. 예시로는 Mach, L4의 OS가 있다고 한다.

 

둘을 비교했을 때, 인지도나 사용 빈도는 monolithic kernel이 월등히 높다. 모듈성이나, 확장성, 안전성, 그리고 오류 확인 차원에서 microkernel이 좋음에도 monolithic kernel을 사용하는 이유는 microkernel의 처참한 퍼포먼스에 있다. 유저 어플리케이션에서 어떠한 기능을 수행하고자 할 때, 반드시 유저-커널-유저 순으로 cross domain information transfer가 일어나게 된다. 예시로 어플리케이션이 디스크를 읽고 싶다고 하자. monolithic kernel에서는 syscall을 통해 유저에서 커널로, 읽기가 다 완료된 뒤에 커널에서 유저로 변환이 한 번씩만 일어나면 된다. 그러나 microkernel에서는 어플리케이션이 파일 시스템 모듈로, 파일 시스템 모듈이 드라이버로, 그 역 과정 모두에서 request가 발생하며, 해당 request들은 반드시 커널을 거치면서 유저-커널 모드 전환과 함께 IPC가 일어나게 된다. 때문에 단순한 작업도 엄청나게 느려진다. 참고로 communication mechanism을 속도 순으로 나열했을 때, IPC가 가장 느리다는 것을 확인할 수 있다.

 

  • Function calls within same process
  • System calls (mode switch)
  • Context switch (process switch)
  • IPC between processes (message passing, on the same or different machines)

때문에 보편적으로 가정에 보급되는 윈도우, 리눅스 등의 운영체제는 퍼포먼스를 중시하므로 monolithic kernel을, 느리지만 정확성과 안전성이 요구되는 시스템에는 microkernel을 사용하고 있다. 현재 microkernel을 주제로 한 연구에서는  모두 microkernel의 퍼포먼스 증대를 위하여 IPC를 어떻게 줄일지가 연구되고 있다.

'코딩 삽질 > OS 요약 정리' 카테고리의 다른 글

(2022.04.10) Scheduling  (0) 2022.04.10
(2022.04.09) Concurrency and thread  (0) 2022.04.09
(2022.04.09) Kernel Abstraction (2)  (0) 2022.04.09
(2022.04.08) Kernel Abstraction (1)  (0) 2022.04.08
(2022.04.08) Introduction  (0) 2022.04.08
Comments