YJcode :: YJcode

프로그램의 상당수는 명령행 옵션과 인자에 따라 다르게 동작한다.

전통적인 유닉스 명령행 옵션은 하이픈( - ) 으로 시작해서, 옵션을 나타내는 한 글자와, 경우에 따라 추가 인자가 뒤따르기도 한다. 여기에 GNU 유틸리티는 하이픈 2개( -- ) 로 시작해서, 옵션을 타나내는 문자열과, 경우에 따라 추가 인자가 뒤따르는 확장 옵션 문법을 제공한다. 이 옵션을 파싱할때는 getopt() 라이브러리 함수를 쓰면 편리하다.

명령행 문법이 복잡한 프로그램에는 사용자를 위한 간단한 도움말 기능이 있다.

--help 옵션으로 실행하면, 프로그램이 명령행 옵션과 인자의 문법에 대한 사용법을 보여준다.

 

대부분의 프로그램에서, 공통적으로 필요한 정의가 담겨있는 헤더 파일과 공통 함수를 사용한다.

 

  • 공통 헤더파일

아래 코드표의 헤더파일들은 거의 모든 프로그램이 매우 높은 빈도로 사용하는 헤더파일이다. 이 헤더 파일에는 여러 프로그램에서 쓰는 다른 많은 헤더 파일이 포함되어 있고, boolean 데이터형과 두 숫자중 최소값과 최대값을 구하는 매크로가 정의되어 있다.

							lib/tlpi_hdr.h

#ifdef TLPI_HDR_H
#define TLPI_HDR_H			//실수로 두번 포함하는 것을 방지한다.

#include <sys/types.h>			//여러 프로그램에 쓰이는 형 정의
#include <stdio.h>			//표준 I/O함수
#include <stdlib.h>			//표준 라이브러리 함수

#include <unistd.h>			//여러 시스템 호출 프로토타입
#include <errno.h>			//errno와 에러상수 정의
#include <string.h>			//흔히 쓰이는 문자열 처리 함수

#include "get_num.h"			//수 인자 처리함수 (getInt(), getLong()) 선언

#include "drror_functions.h"		//에러처리함수 선언

typedef enum { FALSE, TRUE } Boolean;

#define min(m,n) ((m) < (n) ? (m) : (n))
#define max(m,n) ((m) > (n) ? (m) : (n))

#endif

 

  • 에러 진단 함수

앞으로 예제 프로그램에서 에러 처리를 쉽게 하기 위해, 아래에 선언되어 있는 에러 진단 함수를 사용한다.

 

							lib/error_functions.h
#ifdef ERROR_FUNCTIONS_H
#define ERROR_FUNCTIONS_H

void errMsg(const char *format, ...);

#ifdef __GNUC__

#define NORETURN __attribute__ ((__noreturn__))
#else
#define NORETURN
#endif

void errExit(const char *format, ...) NORETURN ;

void err_exit(const char *format, ...) NORETURN ;

void errExitEN(int errnum, const char *format, ...) NORETURN ;

void fatal(const char *format, ...) NORETURN ;

void usageErr(const char *format, ...) NORETURN ;

void cmdLineErr(const char *format, ...) NORETURN ;

#endif

 

시스템 호출과 라이브러리 함수의 에러를 진단할 때 errMsg(), errExit(), err_exit(), errExitEN()을 쓴다.

 

#include "tlpi_hdr.h"

void errMsg(const char *format, ...);
void errExit(const char *format, ...);
void err_exit(const char *format, ...);
void errExitEN(int errnum, const char *format, ...);

 

errMsg() 함수는 표준 에러로 메시지를 출력한다. 인자 목록은 printf()와 동일하지만, 출력 문자열 끝에 줄마꿈 문자가 자동으로 추가되는 것이 다르다. errMsg() 함수는 errno의 값에 따라, EPERM 같은 에러이름, strerror()가 리턴하는 에러 설명으로 이뤄진 텍스트, 그리고 인자 목록에 따라 포맷된 내용을 출력한다.

errExit() 함수는 errMsg()와 비슷하지만, 에러 메시지를 출력한 뒤 exit()를 호출해 프로그램을 종료시키거나, 환경 변수 EF_DUMPCORE가 비어 있지 않은 문자열로 정의되어 있으면 abort()를 호출해 디버그용 코어 덤프 파일을 만든다.

err_exit() 함수는 errExit()와 비슷하지만 다음과 같은 차이점이 있다.

  • 에러 메싲지를 출력하기 전에 표준 출력 버퍼 내에 남아있던 내용을 모두 출력하지 않는다.
  • exit()대신 _exit()를 호출해 프로세스를 종료시킨다. 이로 인해 stdio 버퍼내에 남아있던 내용을 출력하거나 종료 핸들러를 부르지 않고 프로세스를 종료한다.

err_exit()의 이런 차이점에 대해서는 차후 자세히 살펴몰 것이다. 우선은 err_exit()는 에러 발생시 종료해야 하는 자기 프로세스를 만드는 라이브러리 함수를 작성할 때 특히 유용하다고만 알고 있으면 된다. 이렇게 종료할 때는 부포 프로세스의 stdio 버퍼를 복사해서 만든 자식 프로세스의 stdio 버퍼에 남아 있던 내용을 모두 출력하거나 부모 프로세스가 설정한 종료 핸들러를 부르지 않고 종료해야 한다.

errExitEN() 함수는 errExit()와 비슷하지만, errno 값에 따라 에러 텍스트를 출력하는 대신, 인자 errnum으로 넘겨준 에러 번호에 따른 텍스트를 출력한다.

errExitEN()은 주로 POSIX 스레드 API를 채택한 프로그램에서 사용한다. 에러 발생시 -1을 리턴하는 전통적인 유닉스 시스템 호출과 달리, POSIX 스레드 함수는 에러번호를 리턴한다.

POSIX 스레드 함수의 에러는 다음과 같은 코드로 확인할 수 있다.

errno = pthread_create(&thread, NULL, func, &arg);
if (errno != 0) {
	errExit("pthread_create");
}

하지만 이 방법은 비효율적이다. 스레드를 쓰는 프로그램에서 errno는 바뀔 수 있는 lvalue를 리턴하는 함수 호출로 확장되는 매크로로 정의되어 있기 때문이다. 따라서 errno를 쓸 때마다 함수를 호출하게 된다. errExitEN()함수를 쓰면 다음과 같이 좀 더 효율적인 코드를 작성할 수 있다.

int s;

s = pthread_create(&thread, NULL, func, &arg);
if (s != 0) {
	errExitEN(s, "pthread_create");
}

 

또 다른 종류의 에러를 조사할 때는 fatal(), usageErr(), cmdLineErr() 를 쓸 수 있다.

 

#include "tlpi_hdr.h"

void fatal(const char *format, ...);
void usageErr(const char *format, ...);
void cmdLineErr(const char *format, ...);

 

  • fatal() 함수는 errno를 설정하지 않는 라이브러리 함수를 포함하는 일반적인 에러를 조사할 때 쓴다. 인자 목록은 printf()와 같지만, 출력 문자열 끝에 줄바꿈 문자가 자동으로 추가되는 것이 다르다. 포맷된 메시지를 표준 에러로 출력하고는 errExit()를 호출해 프로그램을 종료시킨다.
  • usageErr() 함수는 명령행 인자의 용법이 잘못됐을때 쓴다. printf() 스타일의 인자 목록을 받아서 포맷된 메시지 앞에 Usage:를 붙여서 표준 에러로 출력한 뒤 exit()를 호출해 프로그램을 종료시킨다.
  • cmdLineErr()함수는 usageErr()와 비슷하지만 프로그램에 넘긴 명령행 인자의 에러를 알려줄 때 사용한다.

거의 대부분의 시스템 호출과 라이브러리 함수가 어떤 형태로든 성공 여부를 나타내는 상태값을 리턴한다. 이 상태값은 해당 호출이 성공했는지를 알기 위해 언제나 확인해야한다. 만약 실패했다면 적절한 동작을 취해야 한다. 최소한 프로그램이 뭔가 기대하지 않은 일이 발생했음을 알리는 에러 메시지라도 표시해야 한다.

이런 확인을 생략함으로써 타이핑 시간을 절약하고 싶은 유혹에 빠리기 쉽지만, 그것은 결국 절약이 아니다.

'실패할 리 없는' 시스템 호출이나 라이브러리 함수의 상태값을 확인하지 않은 것 때문에 수많은 디버그 시간이 낭비될 수 있다.

 

 

  • 시스템 호출 에러 처리

 

각 시스템 호출의 메뉴얼 페이지에는 가능한 리턴값들이 나와 있고, 어느 값이 에러를 나타내는지도 나와 있다. 보통 -1을리턴하면 에러를 뜻한다. 따라서 시스템 호출은 다음과 같은 코드로 확인할 수 있다.

 

fd = open(pathname, flags, mode);
if(fd == -1) {
	//에러처리코드
}
. . .
if(close(fd) == -1) {
	//에러처리코드
}

 

시스템 호출이 실패하면 전역 정수 변수 errno를 특정 에러를 나타내는 양수로 설정한다. 헤더 파일 <errno.h>를 사용하면 errno 선언뿐만 아니라 다양한 에러 상수도 포함된다. 이 상수들의 이름은 모두 E로 시작한다. 각 메뉴얼 페이지의 errors 섹션에는 각 시스템 호출이 리턴할 수 있는 errno값의 목록이 나와 있다. 다음은 errno를 사용해서 시스템 호출 에러를 진단하는 간단한 예다.

 

cnt = read(fd, buf, numbytes);
if(cnt == -1) {
	if(errno == EINTR) {
    	fprintf(stderr, "read was interrupted by a signal \n);
    } else {
    	//기타 에러발생
    }
}

 

성공적인 시스템 호출과 라이브러리 함수는 절대 errno를 0으로 설정하지 않으므로, 이 변수는 이전의 함수 호출로 인해 0이 아닌 값을 갖고 있을 수도 있다. 더욱이 SUSv3는 성공적인 함수 호출이 errno를 0이 아닌 값으로 설정하는 것을 허용한다. 따라서 에러를 확인할 때는 언제나 함수의 리턴값이 에러를 나타내는지를 확인하고, 그 경우에만 errno를 통해 에러의 원인을 찾아야 한다.

몇몇 시스템 호출은 성공 시에도 -1을 리턴할 수 있다. 그런 호출에서 에러가 발생했는지를 알기 위해서는 호출 전에 errno를 0으로 설정하고, 호출 후에 다시 확인해야 한다. 호출이 -1을 리턴하고 errno가 0이 아니면, 에러가 발생한 것이다

시스템 호출이 실패한 뒤의 일반적인 동작은 errno 값에 따라 에러 메시지를 출력하는 것이다. 라이브러리 함수 perror()와 strerror()를 사용하면 편리하다.

 

#include <stdio.h>

void perror (const char *msg);

 

시스템 호출의 에러를 처리하는 단순한 방법은 다음과 같다.

 

fd = open(pathname, flags, mode);
if(fd == -1) {
	perror("open");
    exit(EXIT_FAILURE);
}

 

strerror() 함수는 인자 errnum으로 주어진 에러 번호에 해당하는 에러 문자열을 리턴한다.

 

#include <string.h>

char* strerror(int errnum);		//errnum에 해당하는 에러 문자열을 가리키는 포인터를 리턴

strerror()가 리턴한 문자열은 정적으로 할당되어 있을 수 있으므로, 이후의 strerror()에 의해 다른 값으로 바뀔 수 있다.

errnum이 알 수 없는 에러 번호를 담고 있으면, strerror() 는 Unknown errornnn 이라는 형태의 문자열을 리턴한다. 어떤 구현에서는 이런 경우 strerror()가 NULL을리턴하기도 한다.

perror()와 strerror() 함수가 로케일을 고려해서 동작하기 때문에 에러 설명이 현지어로 출력된다.

 

 

  • 라이브러리 함수의 에러처리

 

다양한 라이브러리 함수가 실패를 알리기 위해 여러가지 데이터형의 여러가지 값을 리턴한다. 여기서는 라이브러리 함수를 다름과 같이 분류해봤다.

 

  • 시스템 호출과 똑같이 에러 정보를 리턴하는 라이브러리 함수. -1을 리턴하고 errno에 구체적인 에러번호를 적는다. 이러한 함수의 예로는 파일이나 디렉토리를 삭제하는 remove() 가 있다. 이 함수의 에러는 시스템 호출의 에러와 똑같은 방법으로 진단할 수 있다.
  • 에러 발생 시 -1을 리턴하지는 않지만 errno에 구체적인 에러 번호를 적는 라이브러리 함수, 예를 들어, fopen()은 에러 발생 시 NULL포인터를 리턴하지만 하부의 시스템 호출 결과에 따라 errno를 설정한다. perror() 와 strerror() 함수도 이 에러를 진단할 때 쓸 수 있다.
  • errno를 전혀 쓰지 않는 나머지 라이브러리 함수. 에러의 발생 여부와 원인을 알아내는 방법은 함수마다 다르며 해당 함수의 메뉴얼 페이지에 적혀 있다. 이 함수의 경우 errno나, perror(), strerror()를 써서 에러를 진단하려고 하면 안된다.

유닉스 구현에 따라 표준 C 라이브러리 구현이 다를 수 있다. 리눅스에서 가장 널리 쓰이는 구현은 GNU C 라이브러리다.

 

때로 시스템의 glibc 버전을 판별해야 할 때가 있다. 쉘에서는 마치 실행 프로그램인 양 실행시켜보면 알 수 있다.라이브러리를 실행파일처럼 실행시키면, 버전 번호등을 출력한다.

 

$ /lib/libc.so.6

 

일부 리눅스 배포판에서는 GNU C 라이브러리가 /lib/libc.so.6 말고 다른 곳에 있을 수도 있다. 라이브러리의 위치를 찾는 방법 중 하나는 glibc와 동적으로 링크된 실행파일을 대상으로 ldd 프로그램을 실행하는 것이다. 출력된 라이브러리 의존관계 목록을 보고 glibc 공유 라이브러리의 위치를 찾을 수 있다.

 

$ldd myprog | grep libc

 

응용 프로그램이 시스템에 현존하는 GNU C 라이브러리의 버전을을 알아낼 수 있는 두가지 방법이 있다. 상수를 확인하거나 라이브러리 함수를 호출하는 것이다. 버전 2.0부터 glibc는 __GLIBC__ 와 __GLIBC_MINOR__라는, 컴파일할때 참조할 수 있는 두 상수를 정의한다. glibc 2.12가 설치된 시스템에서 이 상수들의 값은 각각 2와 12다.하지만 이들 상수는 한 시스템에서 컴파일해서 glibc버전이 상이한 다른 시스템에서 실행하는 프로그램의 경우에는 그 쓰임이 제한될 수밖에 없다. 이런 경우에 대비해서, 프로그램은 gnu_get_libc_version() 함수를 호출해 glibc의 버전을 실행 시에 알아낼 수 있다.

 

#include <gnu/libc-version.h>



const char* gnu_get_libc_version(void) ;

 

gnu_get_libc_version() 함수는 2.12 같은 문자열을 가리키는 포인터를 리턴한다.

+ Recent posts