임베디드/[ Linux Kernel ]

[ Linux Kernel ] 04. 시스템콜(System Call)

kim.svadoz 2020. 8. 15. 14:48
반응형

6. 시스템 콜(System Call)


시스템 콜(System Call)은 것은 정확히 언제 일어나는 것일까? 우리가 I/O관련 function을 하려고 하면 그때 바로 일어나는 것일까? 이것을 알아보기 위해 먼저 밑에 그림을 보자.

image-20200806163106579

리눅스 명령어는 옆에 붙은 숫자에 따라 커맨드(1), 시스템 콜(2), 라이브러리 함수(3)*로 구분된다.*

위 그림의 좌측을 보면 유저 영역 안에 내가(유저)가 작성한 코드 my code가 있다. 이 코드에서 printf()를 호출(call) 하는데 이 printf() 코드는 내가 작성한 게 아니라 library function이다. C언어를 배울 때 #include <stdio.h>를 하는 이유를 생각해보면 금방 이해할 것이다. 그럼 이제 printf()가 내가 작성한 코드 my code에 들어오는데 printf()는 출력 즉, I/O를 해 줘야 한다. 1장에서 배웠듯, 멀티 유저 시스템에서 I/O는 오직 커널만 할 수 있기 때문에 I/O를 하는 모든 library function은 무조건 System Call을 사용해야 한다. 커널에게 부탁한다는 뜻이다.

시스템 콜을 하게 되면 Wrapper Routine이라는 공간에 가게 되고 이 공간에는 왜 커널로 가게 되는지 알려주는 정보들을 담고 있는 Prepare parameter와 CPU의 모드 비트를 커널로 바꾸는 chmodk가 들어있다.

chmodk가 실행되면서 프로그램은 런타임 중 트랩에 걸려 커널 영역으로 가게 된다. 커널에서는 Prepare parameter에 담겨있는 내용을 보고 적절한 System call function으로 처리를 해준다.

커널 안에 있는 모든 System call function의 이름은 sys_로 시작한다. 리눅스의 naming convention(명명 규칙)이니 알아두길 바란다


6.1 Wrapper Routine

트랩으로 넘어갈 내용들을 준비하고 실질적으로 트랩을 일으키는 공간인 Wrapper Routine에 대해 조금 더 알아보자.

image-20200806163401735

Wrapper Routine에서 (인텔의 경우) $0x80등 의미 없는 문자들을 이용해 Machine Instruction을 주어 트랩을 발동한다. 그런데 위에서 트랩을 일으키기 전에 Prepare parameter들을 준비하게 되는데 그 중에 가장 중요한 것은 바로 system call number라는 것이다. 이 system call number는 커널이 가지고 있는 system call function의 시작 주소를 담고있는 Array(배열)의 Index 번호로 사용이 된다.

system call number의 예를 들어 보면 다음과 같다. file과 관련된 system call에는 open, close, read, write등이 있는데 open은 0번, close는 2번, read는 3, write는 4번 등 call number을 이용해 Array의 Index 위치에 접근을 한다.

지금까지의 과정을 순차적으로 정리해 보면 아래와 같다.

  1. 컴파일러(gcc)가 유저가 짠 코드를 보고 라이브러리(printf())를 호출한다.
  2. 라이브러리에서 시스템 콜(write)을 호출한다. 위 그림의 write(2)의 2는 시스템 콜을 의미하는 숫자일 뿐 매개변수와 같은 의미는 없다.
  3. Wrapper Routine에서 write에 대응하는 system call number가 나오고 트랩을 건다.
  4. 커널이 system call number을 가지고 system call function table에 접근해 function의 시작 주소에 접근한다.
# Note
여기서 하나 알아둬야 할 점은 이렇게 system call number를 지정한 컴파일러와 그 system call number를 받고 system call function table에서 function을 찾는 운영체제의 번호가 서로 일치해야 한다는 점이다. 이러한 번호들은 컴파일러를 쓰는 회사에서 결정을 한다. 실례로 만약 다른 회사의 플랫폼으로 시스템을 옮기면 소스파일들을 다시 컴파일을 해줘야 system call number가 얽혀서 오동작하는 오류를 방지할 수 있다.

마지막으로 아래 그림에 나온 예시를 통해 시스템 콜의 과정을 자세히 살펴보자.

image-20200806163451277

  • 유저 프로그램이 시스템 콜을 호출한다.
  • Machine Instruction이 트랩을 발동한다.
  • 하드웨어가 유저 모드에서 커널 모드로 mode bit를 바꾼다.
  • 하드웨어가 sys_call()이라는 커널안의 트랩 핸들러(Trap Handler)로 가게 된다.
  • 이런 핸들러는 커널안의 assembly function을 수행한다.
  • 지금까지 유저 프로그램에서 진행했던 단계를 저장을 한다. (커널 쪽 일이 다 끝나면 시스템 콜을 호출 했던 곳으로 돌아가서 다시 진행을 해야하기 때문에 저장하는 것이다.)
  • 시스템 콜 번호가 커널 안에 sys_call table에 있는 번호에 맞는 번호인지 확인한다.
  • 맞다면 system call function의 주소를 가져온다.
  • 그리고 system call function을 불러 작업한다.
  • (만약 진행 과정 중 디버깅이 필요하다면 디버거를 실행시킨다.)
  • 다시 시스템 콜 호출했던 유저의 영역으로 돌아가고 mode bit를 유저 모드로 전환한다.

6.2 Kernel System Call Function

스마트폰 어플리케이션으로 찍은 사진을 볼 수 있는 갤러리 어플리케이션을 만들었다고 생각해보자. 갤러리 어플은 사용자가 자신이 촬영하여 폰에 저장한 사진을 볼 수 있게끔 해준다.

어플리케이션을 제작할 때 소스코드에는 분명 스마트폰에 저장된 사진을 읽어오는 기능이 있을 것이다. 이 기능은 library함수를 사용하여 구현했을 것이고, 실제 동작할 때 library는 I/O를 하기위해 System Call을 호출할 것이다. 커널에게 부탁한다는 매커니즘이 시스템 콜이라는 점을 다시한 번 떠올리자.

# Note
별도의 함수를 만들어서 스마트폰에 저장된 파일을 읽어오는 것보다는 라이브러리로 구현된 소스코드를 사용하는 것이 훨씬 효율적이다. 만약 코드를 직접 만든다고 해도 시스템 콜을 적절히 배합해서 원하는 동작을 하게끔 구현해야하는데 굳이 이렇게 할 필요가…

커널에서는 유저가 원하는 사진 파일을 시스템 콜을 호출한 유저 영역으로 넘겨줘야 할 것이다. 때로는 커널이 유저 영역으로부터 데이터를 가져와야 하는 경우도 있을 것이다. 즉 어플리케이션이 제대로 동작하기 위해서는 유저 프로그램과 커널 프로그램이라는 서로 독립된 프로그램 사이에 데이터를 주고 받을 수 있는 수단이 반드시 필요하다.

image-20200806163607107

그러한 기능들은 오직 커널만이 가지고 있다. 리눅스는 멀티 유저 시스템이고 시스템의 보안을 위해서 오직 커널만이 모든 메모리에 접근이 가능하다. 좀 더 자세히 살펴보면, 커널이 유저에게 데이터를 보내줄 수는 있어도 유저가 커널로부터 데이터를 읽어 올 수는 없고 커널이 유저한테서 데이터를 읽어올 수는 있어도 유저가 커널한테 데이터를 보낼 수는 없다. 모든 I/O는 커널을 통해서만이 이루어 진다.

# Note
이쯤 되면 유저는 거의 커널의 노예라고 할 수 있다. 모든 중요한 행위는 커널에게 부탁해야한다. 감히 컴퓨터에 직접적으로 데이터를 쓴다거나 읽어온다든가 하는 행위는 절대 할 수 없다.
유저가 요청하는 데이터의 바이트의 수는 커널이 디스크에서 받아오는 것처럼 일정한 바이트의 단위가 아닌 4바이트, 7바이트 등 여러가지가 될 수 있기 때문에 커널에는 유저가 원하는 바이트 만큼 넘겨주는 기능 등이 존재한다.

6.3 System Call Number

그럼 커널에 대해 더 자세히 알아보기에 앞서 트랩전에 정해지는 시스템 콜 번호에 대해 구체적으로 알아보고 가자.

image-20200806163714470

System call number는 커널의 system call table의 인덱스 번호로 사용되어 system call function의 주소의 시작값을 불러오는 용도로 사용된다. System call number는 컴파일러와 OS를 제작한 회사에서 정하며 이렇게 정해진 번호는 변경 할 수 없다.

그렇다면 리눅스에 자신만의 시스템 콜(System Call)을 만들 수는 없을까? sys_write()sys_read()처럼 내가 특정 기능 수행하는 시스템 콜을 정의하고 사용할 순 없을까? 물론 직접 만들 수 있다!

image-20200806163738941

시스템 콜을 만들기 전에 먼저 새로운 시스템 콜을 만드는 것의 장점을 살펴보자. 우리는 새로운 시스템 콜을 만들 때 우리가 원하는 특정 기능만을 위한 코드를 작성할 수 있다. 즉 기존에 존재하는 시스템 콜 보다 간단하고 성능 또한 좋게 만들 수 있다.

# Note
예를 들어, 여러분은 컴퓨터 화면에 특정 알파벳만을 출력하는 기능을 새로 정의할 수 있을 것이고 이는 알파벳 뿐만 아니라 숫자, 기호 등을 출력해줄 수 있는 기존의 printf() 함수보다 훨씬 코드도 간결하고 효율적일 것이다.

분명 시스템 콜을 직접 만들어 사용하면 성능도 좋고 기존 시스템 콜보다 간결할 수 있다는 장점이 존재하지만 이보다 훨씬 큰 단점이 존재한다. 새로운 시스템 콜을 만들게 되면, 그 시스템 콜만의 새로운 system call number가 필요하게 된다.

이렇게 새로 제작할 때마다 system call number를 정의하게 되면 새로만든 시스템 콜은 그것을 제작한 플랫폼에서만 사용할 수 있다. 즉 다른 플랫폼에서 본인이 만든 시스템 콜(예를 들어 99번)을 호출하는 것은 불가능하다. 다른 플랫폼에는 99번에 해당하는 시스템 콜이 존재하지 않거나 다른 시스템 콜일 수 있기 때문이다. 플랫폼 의존적이라는 치명적인 단점 때문에 보통 시스템 콜을 직접 만들어서 사용하는 일은 거의 없다.

또한 한번 만든 시스템 콜은 추가만 가능하고 변경은 불가능하기 때문에 나중에 수정을 하는 것도 불가능하다. 그렇다면 새로운 시스템 콜은 만드는 건 아예 하지 말아야 할까? 다행히도 방법은 있다.

image-20200806163811969

그 방법은 바로 기존에 있던 시스템 콜인 readwrite에 있는 파일 디스크립터(File Descriptor)을 활용하는 것이다. 파일 디스크립터는 뒤에 다루겠지만 먼저 간단히 설명을 하자면 운영체제가 만든 파일이나 소켓을 편하게 부르기 위해서 부여한 숫자이다.

파일 디스크립터는 보통 적은 숫자만이 활용이 되고 있어 보통은 잘 쓰지 않는 999번 등에 본인의 파일 디스크립터를 지정하고 사용하면 커널안에 내장된 시스템 콜에 영향을 주지 않고도 사용할 수 있다. 훨씬 안전한 방법이다.

Robert M. Love의 책에서도 권장하는 방식이고 전 세계 모든 유닉스 사용자들이 이러한 방식을 사용하고 있다고 한다.

반응형