[리눅스] 5. System Calls

Nanggu_Pine·2023년 9월 19일
0

linux-kernel

목록 보기
3/12

Ch5. System Calls

현대 운영체제는 유저 공간에 있는 프로세스가 시스템과 상호작용할 수 있는 인터페이스를 제공한다.

1. Communicating with the Kernel

시스템콜은 하드웨어와 user-space의 프로세스간의 레이어를 제공한다. 크게 3가지 목적이 있는데, 첫째는 추상화된 하드웨어 인터페이스를 제공하는 것이다. 어떤 디스크를 이용하는지 알 필요 없이 파일을 읽고 쓸 수 있다. 두번째로 시스템콜은 시스템의 보안과 안정성을 보장한다. 커널은 권한, 유저, 및 다른 기준으로 접근을 제한할 수 있다. 마지막으로 user-space와 시스템 사이의 단일 레이어는 프로세스로 하여금 가상화된 시스템을 제공한다. 따라서 프로세스가 시스템의 자원을 함부로 접근하지 않는다. 리눅스에선 시스템콜은 커널로 진입할 수 있는 유일한 방법이다(단, exception과 trap은 제외).

2. APIs, POSIX, and C Library

애플리케이션들은 시스템 콜을 직접 실행하는 것이 아닌 유저공간에서 API를 호출해 사용하도록 되어있다. 유닉스 계열에서 가장 일반적인 API는 POSIX 표준이다. 리눅스에서의 시스템 콜 인터페이스는 C 라이브러리에 의해 부분적으로 제공된다.

3. Syscalls

시스템 콜은 long 타입을 반환하는데, 보통 이 값이 음수이면 실패를 의미하고, 0이면 성공을 의미한다. 또한 시스템 콜이 에러를 반환하면, 에러코드를 전역변수인 errno에 쓴다. 시스템 콜의 정의 코드는 아래와 같다.

asmlinkage long sys_getpid(void)

모든 시스템 콜은 asmlinkage를 modifier로 가진다. 또한 호환성을 위해 시스템 콜은 long을 반환한다. 함수명은 naming convention으로 앞에 sys_를 붙인다.

3.1. System Call Numbers

리눅스에선 각각의 시스템 콜은 syscall number라는 고유한 숫자를 부여받는다. 이는 한번 할당되면 변하지 않는다. 시스템 콜이 삭제되어도 해당 syscall number는 재사용될 수 없다. 커널은 등록된 시스템 콜을 system call table에 저장한다.

3.2. System Call Performance

리눅스에서의 시스템 콜은 다른 운영체제에 비해 빠르다. 이는 리눅스의 context switch가 빠르고, 시스템 콜 핸들러와 시스템 콜 자체가 간단하기 때문이다.

4. System Call Handler

User-space의 애플리케이션이 커널 코드를 직접 실행할 수 없고, 커널에 시스템 콜을 실행하고 싶고, 커널모드로 전환하고 싶다는 신호를 커널에 보내야한다. 커널에 신호를 보내는 방법은 interrupt이다: 예외를 발생시켜 커널모드에서 예외 핸들러를 실행시키는 것. x86에서 시스템 콜 핸들라의 인터럽트 번호는 128이다. 최근 x86 프로세서는 sysenter라는 기능을 도입했다. 기존에 인터럽트 번호를 이용한 실행보다 더 빠르고 더 특별한 방법을 커널에 trap 한다고 한다. 핵심은 user-space가 커널에 진입하기위해 예외나 trap을 발생시키면 시스템 콜 핸들러가 실행된다는 것이다.

4.1. Denoting the Correct System Call

시스템 콜이 유효한지 확인하기 위해 시스템 콜 번호를 커널로 전달하여 확인한다. system_call() 함수는 전달받은 시스템 콜 번호를 NR_syscalls와 비교한다. 만약 NR_syscalls보다 크거나 같다면 -ENOSYS를 반환한다.

4.2. Parameter Passing

대부분의 시스템 콜은 하나 이상의 parameter를 요구한다. 따라서 trap중에 user-space에서 커널에 인자를 전달해야한다. 가장 간편한 방법은 시스템 콜 번호가 전달되는 것과 마찬가지로, 인자를 레지스터에 저장하는 방법이다. 다만 레지스터 개수에 한계가 있기 때문에, 6개 이상의 인자가 존재하는 경우, 해당 데이터가 담긴 포인터를 하나의 레지스터에 저장하여 진행한다.

5. System Call Implementation

리눅스에서 시스템 콜의 구현은 시스템 콜 핸들러의 행동을 고려하지 않는다. 시스템 콜을 등록하는 것은 쉽지만, 이를 구현하고 디자인 하는 것이 정말 어렵다.

5.1. Implementing System Calls

시스템 콜을 구현하기 앞서, 그것의 목적을 정의해야한다. 시스템 콜은 명확하게 하나의 목적만을 가져야한다. 시스템 콜은 또한 간단명료한 인터페이스와 함께 적은 수의 인자를 가져야한다. 또한 시스템 콜의 동작이 가지는 의미가 변하지 않도록 설계해야하고, 새로운 기능의 추가나 호환성을 해치지 않는 선에서 버그를 수정할 수 있는지 등도 중요하다. 많은 시스템 콜은 플래그를 인자로 갖는데, 이 옵션은 하위 호환성을 해치지 않으면서 새로운 기능을 가능하도록 한다(여러 동작들을 구분하기 위함이 아니다! 시스템 콜 하나에는 하나의 목적을 가진 동작만 허용하는 것이 기본 원칙이다).
시스템 콜을 작성할 때 portablity와 robustness의 필요성에 대해 생각해야한다.

5.2. Verifying the Parameters

시스템 콜은 인자로 들어온 값들이 유효하고 옳은지 반드시 확인해야한다. File descriptor가 유효한지, PID가 유효한지, 접근하고자 하는 리소스에 권한이 있는지 등 면밀하게 확인해야한다. 중요하게 확인해야 할 것중 하나가 유저가 제공한 포인터들의 유효성이다. 아래의 사항들을 확인해본다.

  • 포인터는 유저 공간의 메모리를 나타낸다. 프로세스는 커널 공간의 데이터를 읽도록 속이면 안된다.
  • 포인터는 프로세스의 주소 공간의 메모리를 나타낸다. 프로세스는 커널을 속여 다른 누군가의 데이터를 읽도록 허용되면 안된다.
  • 읽을 때, 메모리는 읽을 수 있는 상태로 마킹된다. 쓸 때, 메모리는 쓸 수 있는 상태로 마킹된다. 실행할 때, 메모리는 실행할 수 있는 상태로 마킹된다. 프로세스는 메모리의 접근 제한을 우회할 수 있어선 안된다.

커널은 위의 사항을 체크하기 위해 두 방법을 제공한다. 유저 공간에 쓰는 경우, copy_to_user()를 이용한다. 유저 공간에서 읽어오는 경우, copy_from_user()를 이용한다. 두 함수 모두 실패한 바이트 수를 반환하고, 성공하면 0을 반환한다.

SYSCALL_DEFINE3(silly_copy,
				unsigned long *, src,
				unsigned long *, dst,
				unsigned long len)
{
	unsigned long buf;
    
    if(copy_from_user(&buf, src, len)) // 실패하면 참, -EFAULT 반환
    	return -EFAULT;
        
    if(copy_to_user(dst, &buf, len))
    	return -EFAULT;
    
    
    return len;
}

마지막으로 유효한 권한인지 확인해야한다. 과거에는 시스템 콜을 위해 루트 권한이 필요했고, suser() 함수를 통해 루트인지 확인하였다. 현재는 이것이 삭제되고 capable() 함수를 통해 caller의 특정 자원에 대한 특정 권한을 확인한다. 권한이 있다면 nonzero를 반환하고, 권한이 없다면 0을 반환한다.

6. System Call Context

시스템 콜 실행중에 커널은 프로세스의 컨텍스트에 놓인다. current 포인터가 현재 태스크(시스템 콜을 호출한)를 가리킨다. Process Context에서 커널은 sleep 할 수 있고, fully preemptible하다. 이 두가지 가능성이 중요한데, 7장 부터 이에 관해 설명한다.

6.1. Final Steps in Binding a System Call

시스템 콜을 작성하면, 아래와 같이 등록할 수 있다.

  • 시스템 콜 테이블의 가장 마지막에 엔트리를 추가한다. 시스템 콜 번호는 0부터 시작하여 1씩 증가된다.
  • 지원되는 각각의 아키텍쳐에서 <asm/unistd.h>에 시스템 콜 번호를 정의한다.
  • 시스템 콜을 커널이미지에 컴파일한다.

시스템 콜 테이블의 매 다섯번째 엔트리마다 번호를 적어주는 것이 규약이다.

6.2. Accessing the System Call from User-Space

리눅스에서는 시스템 콜에 접근할 수 있는 매크로를 제공한다. _syscalln()을 이용해 실행하고자 하는 시스템 콜을 실행할 수 있다(n은 0-6까지, 인자 수).

#define __NR_foo 283
_syscall0(long, foo)

int main() {
	long stack_size;
    stack_size = foo();
    printf("The kernel stack size is %ld\n", stack_size);
    
    return 0;
}

6.3. Why Not to Implement a System Call

이전까지는 시스템 콜을 구현하고 등록하는 방법에 대해 논하였지만, 실제로는 구현하는 것을 권하지 않는다. 다음은 장단점과 대안에 관한 설명이다.
장점:

  • 시스템 콜은 구현하기 간단하고 이용하기 쉽다.
  • 리눅스의 시스템 콜은 빠르다

단점:

  • 공식적으로 할당받은 시스템 콜 번호가 필요하다.
  • 시스템 콜이 안정화된 커널에 포함된다면, 인터페이스는 바꿀 수 없다.
  • 각각의 아키텍쳐에 시스템 콜을 등록하고, 지원해야한다.
  • 시스템 콜은 스크립트에서 쉽게 사용되지 않고, 파일시스템에서 직접 접근할 수 없다.
  • 등록된 시스템 콜 번호가 필요하기 때문에, 마스터 커널 트리 밖에서 사용하거나 유지하기가 어렵다
  • 간단한 정보의 교환에서 시스템 콜을 사용하는건 과하다.

대안:

  • Device node를 구현하고 거기에 read()write()를 한다. ioctl()을 이용해 설정값을 조작하거나 정보를 받아온다.
  • 세마포어와 같은 특정 인터페이스는 file descriptor로 표현될 수 있으니, 이를 조작한다.
  • sysfs의 적당한 장소에 정보를 파일로써 추가한다.

리눅스는 단순히 새로운 abstraction을 지원하기 위해 시스템 콜을 추가하는 행위를 피하고 있고, 이로 인해 시스템 콜 레이어가 굉장히 깔끔하다. 때문에 상대적으로 안정적이고, 요소가 완벽한 운영체제라 할 수 있다.


References

  • Linux Kernel Development (3rd Edition) by Robert Love
profile
학부생 기록남기기!

0개의 댓글