What is POSIX error handling?

다른 언어들에선, 예외 상황에 수행되는 error handling을 봤을지도 모르겠습니다. C에서도 기술적으로 사용할 수 있지만, (try/catch block stack을 가지고 있다가 setjmp나 longjmp를 사용해 그 블록들로 돌아가는 방식을 사용해) C에서 error handling은 일반적으로 POSIX error handling을 사용합니다. 이 POSIX error handling은 주로 아래와 같이 사용됩니다.


int ret = some_system_call()
if(ret == ERROR_CODE){
switch(errno){
// Do different stuff based on the errno number.
}


커널에서 goto는 어플리케이션의 다른 파트를 정리하기 위해 주로 사용됩니다. goto는 코드를 더 읽기 어렵게 만들기 때문에 사용하지 않는 것이 좋습니다.


What is errno and when is it set?

POSIX는 system call이 실패하면 특정한 정수인 errno를 정의합니다. errno의 최초 값은 0입니다(즉, 에러가 없다). 시스템 콜이 실패하면 일반적으로 -1을 리턴하고 error를 의미하는 errno를 셋합니다.

What about multiple threads?

각각의 스레드는 errno의 복사본을 가지고 있습니다. 그렇지 않다면 한 스레드에서 일어난 에러가 다른 스레드의 error status에 영향을 미칠 것이기 때문입니다.

When is errno reset to zero?

일부로 0으로 리셋하지 않는 한 errno가 0으로 리셋되는 일은 일어나지 않습니다. system call이 성공적으로 호출된다 하더라도 errno가 0으로 초기화되지는 않습니다.

What are the gotchas and best practices of using errno?

복잡한 library call이나 system call로 error handling이 복잡해질 때 errno의 값이 변할 수 있습니다. 사실 errno의 값을 int 변수에 복사하는 것이 더 안전합니다.


// Unsafe - the first fprintf may change the value of errno before we use it!
if (-1 == sem_wait(&s)) {
   fprintf(stderr, "An error occurred!");
   fprintf(stderr, "The error value is %d\n", errno);
}
// Better, copy the value before making more system and library calls
if (-1 == sem_wait(&s)) {
   int errno_saved = errno;
   fprintf(stderr, "An error occurred!");
   fprintf(stderr, "The error value is %d\n", errno_saved);
}


비슷한 맥락으로, Signal handler가 system call이나 library call을 했을 때, errno의 원래 값을 저장했다가 함수들이 반환된 이후에 복구하는 것은 좋은 습관입니다.

void handler(int signal) {
   int errno_saved = errno;

   // make system calls that might change errno

   errno = errno_saved;
}


How can you print out the string message associated with a particular error number?

strerror을 사용하면 error value에 대한 짧은 설명을 출력할 수 있습니다.

char *mesg = strerror(errno);
fprintf(stderr, "An error occurred (errno=%d): %s", errno, mesg);


How are perror and strerror related?

perror는 standard out에 error 메세지를 찍어 내보냅니다. strerror을 이용하면 간단한 perror를 구현할 수 있습니다.
void perror(char *what) {
   fprintf(stderr, "%s: %s\n", what, strerror(errno));
}


What are the gotchas of using strerror?

strerror은 threadsafe하지 않습니다. 즉, 두 스레드가 동시에 호출할 수 있다는 뜻입니다. 

이를 해결하기 위한 2가지 대안이 있습니다. 첫번째로는 Critical Section과 local buffer에 mutex lock을 이용하는 것입니다. strerror이 호출되는 모든 스레드에 같은 뮤텍스가 사용되어집니다.


pthread_mutex_lock(&m);
char *result = strerror(errno);
char *message = malloc(strlen(result) + 1);
strcpy(message, result);
pthread_mutex_unlock(&m);
fprintf(stderr, "An error occurred (errno=%d): %s", errno, message);
free(message);


다른 대안으로는 포터블하지는 않지만 Thread-safe 인 strerror_r을 사용하는 것입니다. perror은 thread-safe이므로 가능하다면 perror를 사용하는 것이 multithread 환경에서 선호되어집니다.


What is EINTR? What does it mean for sem_wait? read? write?

일부 시스템 콜은 프로세스에 전달되는 signal에 의해 interrupted될 수 있습니다. 이 때 시스템 콜은 아무런 작업도 하지 않고 리턴될 수도 있습니다. 예를 들면 bytes는 아무것도 읽거나 쓰지 않을 수도 있고, semaphore wait은 wait하지 않을 수도 있습니다.


이러한 interruption은 errno의 값이 EINTER인지 체크해 확인할 수 있습니다. 이러한 경우 system call은 다시 시도되어야 합니다.  System call(sem_wait같은) 을 감싸고 있는 아래와 같은 반복문이 일반적입니다.


while ((-1 == systemcall(...)) && (errno == EINTR)) { /* repeat! */}


= EINTR이 아닌== EINTR 로 쓴다는 것에 주의하세요.

또는, system call의 반환값이 나중에 사용된다면 아래와 같이 사용합니다.


while ((-1 == (result = systemcall(...))) && (errno == EINTR)) { /* repeat! */}

리눅스에서 local disk에 read와 write를 호출하는 것은 일반적으로 EINTR로 반환되지 않습니다.(대신 자동적으로 재시작됩니다.) 하지만 network stream에 대응하는 File Descriptor에 read와 write를 호출할 때에는 EINTR을 리턴할 수 있습니다.


Which system call may be interrupted and need to be wrapped?

man page를 사용하면 어떤 시스템 콜이 interrpt되는지 알 수 있습니다. man page는 시스템 콜에 설정되어 있는 에러값들의 리스트도 포함하고 있습니다. 경험에 의하면 느린(blocking) call(socket에 write)은 interrupt될 수 있지만 빠른 non-blocking call은(pthread_mutex_lock)은 발생하지 않습니다. 


리눅스 signal 7 man page에 의하면 

System call이나 library function이 블록되었을 때 signal hanndler가 동작하면, 


  • signal handler가 반환 된 후 자동적으로 재시작
  • error EINTR로 실패. 
두 방법 중 하나로 동작합니다. 이러한 두 방법 중 어떤 것이 동작할지는 signal handler가 SA_RESTART flag를 사용해 동작하는지 아닌지에 달렸습니다. 

만약 signal handler에 의해 interrupt된 blocked call이 아래와 같은 장치들에게 호출되었을 때, SA_RESTART flag가 사용되었다면 signal handler가 반환된 후 자동적으로 재시작되고, 그렇지 않다면 실패하고 errno가 EINTR이 될 것입니다. 

  • Read(2), readv(2), write(2), wirtev(2), ioctl(2) 호출이 "Slow" device에 이루어질 때. Slow device는 I/O 호출이 명확하지 않은 시간동안 블록되는 것입니다. 예를 들면, 터미널, pipe, 또는 socket같은 것입니다. (이 정의에서 disk는 느린 디바이스가 아닙니다) 만약 느린 디바이스에서 I/O 호출이이미 데이터를 조금이라도 전송한 상태에서 signal handler에 의해 interrupt되었다면 그 system call은 성공한 status를 반환할 것입니다(일반적으로 전송된 바이트 수).
SA_RESTART flag를 세팅하는 것으로 충분히 문제를 해결할수 있을것 처럼 보이지만, 사실은 그렇지 않습니다. 이 설정을 해도 초기에 반환되어 EINTR를 세팅하는 경우가 존재합니다. signal(7)을 보면 좀 더 자세히 알 수 있습니다.

Errno exceptions?

몇몇 POSIX 유틸리티들은 스스로의 error number를 가지고 있어 errno를 사용하지 않습니다. 한 가지 예로는 getaddrinfo를 호출하면 error code를 결과물로 반환합니다. 반환된 error number를 문자열로 바꿔주는 함수는 gai_strerr입니다.



Posted by 몰랑&봉봉
,