이번 챕터에서는 시스템 프로그래밍에 대한 선행지식들을 다룬다.
시스템 콜은 어떤 프로세스가 커널에게 특정한 작업을 취하도록 요청하는 명령어이다. 이러한 시스템 콜 API를 통해, 프로세스는 커널 없이는 불가능한 파일 I/O나, pipe 생성, 타 프로세스와의 통신 등을 할 수 있다.
시스템 콜의 일반적인 특징은 다음과 같다.
시스템 콜은 프로세서 상태를 사용자 모드 (user mode) 에서 커널 모드 (kernel mode) 로 변경한다. 이로 인해 CPU가 kernel 메모리 영역을 접근할 수 있게 된다.
시스템 콜 명령어 모음은 정해져있다. 각 시스템 콜은 고유한 넘버링을 받는다.
시스템 콜 호출은 겉보기에는 단순한 C 함수 호출처럼 보이지만, 실상은 다양한 단계로 이루어져 있다.
응용 프로그램이 wrapper 함수를 통해 시스템 콜을 호출한다.
wrapper 함수란?
wrapper 함수는 시스템 콜에 필요한 모든 인자를 커널이 지정한 특정한 레지스터에 넣는다.
커널이 각 시스템 콜을 식별하기 위하여 wrapper 함수가 해당 시스템 콜 넘버를 특정한 CPU 레지스터에 기입한다. (%eax)
wrapper 함수가 trap (소프트웨어 인터럽트) 를 실행하여 프로세스의 모드를 사용자 모드에서 커널 모드로 변경한다.
...
라이브러리 함수란, 단순하게 표준 C 라이브러리를 구성하는 함수들을 의미한다.
대부분의 라이브러리 함수에서는 시스템 콜을 사용하지 않지만, 몇몇 라이브러리 함수는 시스템 콜 위에 레이어링 하고 있다.
fopen() 함수는 open() 이라는 시스템 콜을 사용한다.
또한 라이브러리 함수는 시스템 콜 함수보다 조금 더 사용자 친화적인 인터페이스를 제공한다. 대표적 예시가 printf()로, 시스템 콜인 write()를 사용하지만 훨씬 다양한 기능 및 편의성을 제공한다.
UNIX 버전이 다양한 만큼, 표준 C 라이브러리 역시 다양할 수 밖에 없다. 하지만, Linux에서 가장 보편적으로 쓰이는 버전은 바로 GNU C 라이브러리 (glibc) 이다.
시스템 콜 함수의 man 페이지를 보게 되면 리턴 값들에 대한 내용이 등장한다. 거의 대부분의 에러는 리턴 값 -1 을 가지게 된다.
fd = open(pathname, flags, mode); /* system call to open a file */
if (fd == -1) {
/* Code to handle the error */
}
...
if (close(fd) == -1) {
/* Code to handle the error */
}
만약 시스템 콜에서 에러가 나게 되면, 전역 변수 errno가 에러에 해당하는 양수값으로 바뀌게 된다. 다음은 errno를 사용하여 시스템 콜 에러를 진단하는 예시이다.
cnt = read(fd, buf, bytes);
if (cnt == -1) {
if (errno == EINTR)
fprintf(stderr, "read was interrupted by a signal\n");
else {
/* Some other error occured */
}
}
일반적으로, 에러가 발생 시 errno 값에 따른 에러 메시지를 출력한다. perror()와 strerror()함수가 이를 위한 함수다. 다음은 시스템 콜 에러를 핸들링하는 예시이다.
fd = open(pathname, flags, mode);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}