'분류 전체보기' 카테고리의 글 목록 :: YJcode

오늘은 SUSv3에서 정의한 기능 테스트 매크로와 표준 시스템 데이터형을 소개하겠다.

 

  • 기능 테스트 매크로

시스템 호출과 라이브러리 함수 API의 동작에 대한 여러 가지 표준이 존재한다. 오픈 그룹 같은 포준 단체가 정의한 표준도 있고, 역사적으로 중요한 두가지 유닉스 구현인 BSD와 시스템 V 릴리스 4가 정의한 표준도 있다.

 

이식성 있는 응용 프로그램을 작설할 때는 헤더 파일이 특정 표준을 따르는지를 나타내는 정의가 있으면 편리할 때가 있다. 이를 위해 프로그램을 컴파일 할 때 아래 나열된 기능 테스트 매크로를 정의한다. 매크로를 정의하는 방법 중 하나는 프로그램 소스에서 헤더 파일을 선언하기 전에 매크로를 정의하는 것이다.

#define _BSD_SOURCE 1

또 다른 방법으로, 컴파일러의 -D 옵션을 쓸 수도 있다.

$ cc -D_BSD_SOURCE prog.c

다음과 같은 기능 테스트 매크로가 관련 표준에 정의되어 있으며, 따라서 이 매크로는 해당 표준을 지원하는 모든 시스템에서 이식성이 있다.

 

  • _POSIX_SOURCE : 특정 값으로 정의되어 있으면, POSIX.1-1990과 ISO C 호환기능을 제공한다. 이 매크로는 _POSIX_C_SOURCE로 대체됐다.
  • _POSIX_C_SOURCE : 1로 정의되어 있으면 _POSIX_SOURCE와 동일한 효과를 낸다. 199309 이상의 값으로 정의되어 있으면 POSIX.1b 기능도 제공한다. 199506 이상의 값으로 정의되어 있으면 POSIX.1c (스레드) 기능도 제공한다. 200112로 정의되어 있으면 POSIX.1-2001 기본 표준 기능도 제공한다. 200809로 정의되어있으면 POSIX.1-2008 기본 표준 기능도 제공한다.
  • _XOPEN_SOURCE : 특정 값으로 정의되어 있으면 POSIX.1, POSIX.2, X/Open 기능을 제공한다. 500 이상의 값으로 정의되어 있으면 SUSv2 확장 기능도제공한다. 600 이상의 값으로 설정하면 추가적으로 SUSv3 XSI 확장기능과 C99 확장기능을 제공한다. 700 이상으로 설정하면 SUSv4 XSI 확장기능도 제공한다.

 

다음은 glibc 고유의 기능 테스트 매크로다.

 

  • _BSD_SOURCE : 어떤 값이든 정의되어 있으면, BSD의 기능을 제공한다. 이 매크로를 정의하면 _POSIX_CSOURCE도 199506으로 정의된다. 명시적으로 이 매크로만 설정하면 표준이 상충되는 몇몇 경우에 BSD 표준을 따르게 된다.
  • _SVID_SOURCE : 어떤 값이든 정의되어 있으면, 시스템 V 인터페이스 정의의 기능을 제공한다.
  • _GNU_SOURCE : 어떤 값이든 정의되어 있으면, 이상의 모든 매크로를 설정해서 모든 기능을 제공할 뿐 아니라 다양한 GNU 확장 기능도 제공한다.

 

GNU C 컴파일러가 특별한 옵션 없이 실행되면 _POSIX_SOURCE, _POSIX_C_SOURCE=200809, _BSD_SOURCE, _SVID_SOURCE가 기본적으로 정의된다.

개별 매크로를 정의하거나 컴파일러를 표준 모드중 하나로 실행하면, 해당 기능만 제공된다. 예외가 하나 있는데, _POSIX_C_SOURCE가 정의되지 않고 컴파일러가 표준 모드중 하나로 실행되지 않으면, _POSIX_C_SOURCE가 200809로 정의된다.

여러 매크로를 동시에 정의하면 해당 기능이 모두 제공되므로, 예를 들어 다름과 같은 cc 명령을 이용해서 기본 설정과 동일한 매크로 설정을 명시적으로 선택할 수도 있다.

$ cc -D_POSIC_SOURCE -D_POSIX_C_SOURCE=199506 \
	-D_BSD_SOURCE -D_SVID_SOURCE prog.c

<features.h> 헤더파일과 feature_test)macros(7) 메뉴얼 페이지를 보면 각기능 테스트 매크로에 정확히 어떤 값이 할당되어 있는지를 알 수 있다.

 

_POSIX_C_SOURCE, _XOPEN_SOURCE, POSIX.1/SUS

POSIX.1-2001/SUSv3에는 _POSIX_C_SOURCE와 _XOPEN_SOURCE 기능 테스트 매크로만 규정되어 있으며, 호환 응용 프로그램의 경우 이 값들을 각각 200112와 600으로 정의하도록 요구한다. _POSIX_C_SOURCE를 200112로 정의하면 POSIX.1-2001 기본 규격호환성을 제공한다. _XOPEN_SOURCE를 600으로 정의하면 SUSv3 호환성을 제공한다. POSIX.1 - 2008/SUSv4의 경우도 이와 유사하게, _POSIX_C_SOURCE와 _XOPEN_SOURCE를 각각 200809와 700으로 정의해야 한다.

SUSv3에 따르면, _XOPEN_SOURCE를 600으로 설정하면 _POSIX_C_SOURCE를 200112로 설정했을때 제공되는 모든 기능을 제공해야 한다. 따라서 SUSv3호환성을 위해 응용 프로그램은 _XOPEN_SOURCE만 정의하면 된다. SUSv4도 이와 유사하게 _XOPEN_SOURCE를 700으로 설정하면 _POSIX_C_SOURCE를 200809로 설정했을때 제공되는 모든 기능을 제공해야 한다.

 

함수 프로토타입과 소스코드 예제의 기능 테스트 매크로

매뉴얼 페이지를 보면 헤더 파일의 특정 상수 정의나 함수 선언을 쓰려면 어떤 기능 테스트 매크로를 정의해야 하는지를 알 수 있다.

 

 

  • 시스템 데이터형

프로세스 ID, 사용자ID, 파일 오프셋 등 여러가지 구현 데이터형이 표준 C 데이터형으로 표현되어 있다. 이런 정보를 저장하는 변수를 선언하기 위해 int나 long 같은 C의 기초 데이터형을 쓸 수도 있겠지만, 그렇게 하면 다음과 같은 이유로 유닉스 시스템 간의 이식성이 떨어진다.

 

  • 이 기초 데이터형의 크가기 유닉스 구현마다 다르거나, 심지어 같은 구현이라도 컴파일환경에 따라 다를 수 있다. 그 뿐 아니라 같은 정보를 나타내더라도 구현에 따라 다른 데이터형을 쓰기도 한다. 예를 들어, 프로세스 ID가 한 시스템에서는 int이지만 다른 시스템에서는 long일 수도 있다.
  • 같은 종류의 유닉스 구현에서도 버전에 따라 정보를 나타내는 데 쓰는 데이터형이 다를 수 있다. 리눅스상의 유명한 예는 사용자 ID와 그룹ID다. 리눅스 2.2까지는 이 값을 16비트로 나타냈는데, 리눅스 2.4부터는 32비트 값으로 바뀌었다.

 

이런 이식성 문제를 피하기 위해, SUSv3는 여러가지 표준 시스템 데이터형을 명시했고, 구현이 이 데이터형을 적절히 정의하고 사용하도록 요구했다. 이 데이터형은 C의 typedef 기능으로 정의됐따. 예를 들어 pid_t 데이터형은 프로세스 ID를 나타내는데, 리눅스 /x86-32에서는 다음과 같이 정의되어 있다.

typedef int pid_t;

표준 시스템 데이터형의 이름은 대부분 _t로 끝난다. 다른 헤더 파일에 정의되어 있는 것도 있지만, 상당수는 <sys/types.h> 에 정의되어 있다.

이식성을 위해 응용 프로그램은 이 데이터형 정의를 사용해야 한다. 예를 들어, 다음과 같이 선언하면 응용 프로그램은 모든 SUSv3 호환 시스템에서 올바르게 프로세스 ID를 나타낼 수 있다.

pid_t mypid;

아래 표는 시스템 데이터형 중 대표적인 몇가지의 예시이다.

데이터형 SUSv3 데이터형 요구사항 실행
blkcnt_t 부호 있는 정수 파일 블록 수
blksize_t 부호 있는 정수 파일 블록 크기
cc_t 부호 없는 정수 터미널 특수문자
clock_t 정수 또는 부동소수점 실수 clock tick 으로 나타낸 시스템시간
clockid_t 산술형 POSIX.1b 클록과 타이머 함수용 클록ID
comp_t SUSv3에 없음 압축된 클록 틱
dev_t 산술형 주번호와 부번호로 이루어진 디바이스번호
DIR 데이터형 요구사항 없음 디렉토리 스트림
fd_set 구조체형 select()용 파일 디스크립터
fsblkcnt_t 부호 없는 정수 파일 시스템 블록 수
fsfilcnt_t 부호 없는 정수 파일 수
uid_t 정수 숫자로 나타낸 사용자 ID
gid_t 정수 숫자로 나타낸 그룹 ID
id_t 정수 ID를 담는 일반적인 데이터형, 최소한 pid_t, uid_t, gid_t를 담을 만큼 커야한다.
in_addr_t 32비트 부호 없는 정수 IPV4 주소
in_port_t 16비트 부호 없는 정수 IP 포트번호
ino_t 부호 없는 정수 파일 i-노드번호
key_t 산술형 시스템 V IPC키
mode_t 정수 파일 권한과 종류
mqd_t 데이터형 요구사항 없지만, 배열형은 안됨 POSIX 메시지 큐 디스크립터
msglen_t 부호 없는 정수 시스템 V 메시지 큐에 허용되는 바이트 수
msgqnum_t 부호 없는 정수 시스템 V 메시지 큐에 들어있는 메시지 수
nfds_t 부호 없는 정수 poll()용 파일 디스크립터 수
nlink_t 정수 파일을 가리키는 하드링크의 수
off_t 부호 있는 정수 파일 오프셋 또는 크기
pid_t 부호 있는 정수 프로세스 ID, 프로세스 그룹 ID, 세션 ID
ptrdiff_t 부호 있는 정수 부호 있는 정수로 나타낸, 두 포인터 값의 차이
rlim_t 부호 없는 정수 자원 한도
sa_family_t 부호 없는 정수 소켓 주소 체계
shmatt_t 부호 없는 정수 시스템 V 공유 메모리 세그먼트에 부착된 프로세스의 수
sig_atomic_t 정수 아토믹하게 접근할 수 있는 데이터형
siginfo_t 구조체형 시그널의 출처에 대한 정보
sigset_t 정수 또는 구조체형 시그널
size_t 부호 없는 정수 바이트 수로 나타낸 객체의 크기
socklen_t 최소 32비트 정수형 바이트 수로 나타낸 소켓 주소 구조체의 크기
speed_t 부호 없는 정수 터미널 라인 속도
ssize_t 부호 있는 정수 대체 시그널 스택 설명
suseconds_t -1 ~ 1000000 범위내의 부호있는 정수 마이크로 초 시간 간격
lcflag_t 부호 없는 정수 터미널 모드 플래그 비트 마스크
time_t 정수 또는 부동소수점 실수 기원 이후 흐른 초로 나타낸 달력시간
timer_t 산술형 POSIX.1b 타이머 함수용 타이머

 

변수란 무엇인가? 말 그대로 변하는 수. 즉, 고정되지 않은 수를 변수라고 말한다.

 

하지만 프로그래밍을 하는 여러분은 앞으로 프로그래밍적인 측변에서 이에 대해 접근해야 할 것이다.

 

 

프로그래밍 언어에서 변수란, 어떤 수, 문자, 혹은 위치정보를 저장하는 저장 공간을 의미한다.

 

앞으로는 변수 'A'를 선언한다 와 같은 문구를 많이 접하게 될 것인데, 여기서 변수 'A'를 선언한다.

라는 말은 다르게 말하면 무언가를 담을 수 있는 이름이 'A' 인 저장공간을 만들겠다. 라고 해석하면 되겠다.

물론 이게 완벽하게 정확한 의미는 아니다. 다만 지금은 이렇게 알고 있고, 차후 각자 자신만의 정의를 내리며 다시한번 정리를 해 나가면 되겠다.

 

그렇다면 변수는 어떤식으로 사용되는지 알아보자.

 

변수를 사용하려면 변수가 있다는 것을 컴퓨터에게 알려주어야 한다. 이것을 "변수를 선언한다." 라고 표현하는데,

C언어에서는 변수선언에 대한 규칙이 미리 정해져 있다. 아래가 변수를 선언하는 가장 기본적인 형태이다.

 

  • 변수타입 + 변수이름 + ;

변수 타입은 int, long, float, double 과 같은 변수의 형태를 말하고, 변수이름은 그 변수를 다른 변수와 구분하기 위해 사용되는 닉네임이라고 보면 된다. 그리고 ;를 붙여주는데, ';'는 C언어에서 명령이 하나 끝났음을 컴파일러에게 알려주는 역할을 한다. 예를 들어서 변수를 하나 선언해보겠다.

 

  •  int number;

 

int 는 변수타입, number는 변수이름이 되겠다.

 

변수타입은 여러 종류가 있고 앞으로 다루게 되겠지만 당장은 이러한 형태가 있다는 것만 알아도 충분하다.

 

이렇게 변수를 선언하고 나면 필수적으로 필요한게 변수 초기화이다.

위의 변수선언 기본형은 변수를 사용하겠다는 것을 표현한 것으로, 이문장을 만나면 컴파일러는 차후 실제로 만들 프로그램에 변수공간을 할당(배정)해 주게 된다. 이때 어느 공간이 할당이 될지는 알 수 없으며, 이전에 누가 사용했는지는 더더욱 알수없다.

 

지금 할 설명은 아니지만 조금 자세히 설명을 하자면, 변수가 어디에 저장되는지 혹시 알고 있는가?

알고 있다면 좋겠지만 모르고 있다면 가볍게 듣고 넘어가길 바란다. 만약 위와 같이 int number라는 소스코드가 포함된 실행파일을 실행하면 프로그램(프로세서)는 메모리공간에 number라는 이름의 공간을 만들어 주는데, 이전에 누가 사용했는지, number라는 변수공간안에 뭐가 들어있는지 아무도 알지 못한다. 왜냐하면 그전에는 사용하지 않던 공간을 사용하는 것이므로 관리가 되지 않고 있던 메모리 공간이기 때문이다. 만약 이 공간이 일정한 값으로 정해져 있다면, 컴퓨터는 절대 지금처럼 빠르게 동작할수 없다는 사실을 알고 있어야 한다. 그래서 현대 컴퓨팅 시스템은 메모리를 사용하기 전에 정리하기 보다는 사용을 할때 필요한 놈이 직접 정리하도록 암묵적으로 약속하였고, 그렇게 실행되고 있다. 그것이 효율적이기 때문이다.

 

그렇기에 변수를 선언하면, 그 변수에 담겨있는 내용물은 아무 의미없는, 알수없는 값이 들어 있으며, 이것을 흔히 쓰레기값, 혹은 NULL값 이라고 부른다. 이 NULL값을 우리가 초기화해야 하며, 이것은 앞으로 코딩하는데 있어서 필수적으로 습관을 들여놓기를 바란다. 그렇다면 초기화는 도대체 무엇을 말하는 것인가?

 

  • 초기화는 NULL값을 임의의 값으로 처음 지정해주는 것을 말한다.
  • 변수이름 = 내용물;
  • number = 100;

즉, number라는 변수에 100을 처음으로 저장해주는 것을 초기화라고 부르는 것이다. 이 초기화하는 과정은 이후에도 다시 적용할 수 있으며, 이를 초기화한다고 하지 않고 변수에 값을 저장한다라고 표현한다. 정리하자면.

 

  • 변수는 선언을 하면 처음에는 값이 정해지지 않은 상태이다.
  • 변수에는 언제든 값을 저장할수 있다.
  • 이 값을 저장하는 행동은 몇번이고 반복할수있다.
  • 최초로 값을 저장하는 행위를 변수를 초기화한다. 라고 표현한다.
  • 변수를 선언하면, 초기화는 꼭 해주자.

 

보통은 초기화는 변수의 선언과 동시에 많이 사용하며 그럴때는 아래와 같은 형식을 따른다.

 

  • 변수타입 + 변수이름 + 내용물 + ;
  • int number = 100;

이러한 형식을 변수선언과 함께 초기화한다. 라고 표현하기도 한다.

 

아래 코드를 작성해서 빌드하고 눈으로 직접 확인해보기를 바란다.

 

#include <stdio.h>

int main() {

    int number;
    int number2 = 100;

    printf("number = %d, number2 = %d \n", number, number2);

    nunber = 200;

    printf("number = %d, number2 = %d \n", number, number2);

    number2 = 500;

    printf("number = %d, number2 = %d \n", number, number2);
    
    return 0;
}

 

차이가 확인이 되는가?

이 코드가 무엇을 전달하고자 했는지 의미를 모르겠다면 이 글을 위에서 다시한번 살펴보기를 바란다.

'C언어 > 문법' 카테고리의 다른 글

1. 맛보기 - hello, world 출력하기(Visual Studio 2019)  (0) 2019.09.27

C언어 문법을 공부하기에 앞서 맛보기이자 누구나 처음으로 다루는 hello, world를 작성해 보겠다.

IDE는 Visual Studio 2019 커뮤니티를 사용하였고, 다른 컴파일러를 사용해도 정상적인 컴파일 환경과, 코드만 갖춰진다면 동일한 결과를 얻을 수 있으니 참고하자.

비쥬얼 스튜디오 2019를 설치하였다면, 실행시 다음과 같은 화면이 뜨게 된다.

여기서 새 프로젝트 만들기를 클릭한다.

hello, world 프로젝트 만들기 화면1

새 프로젝트 만들기 대화 상자에서 검색창에 Windows 데스크톱 마법사 중 Windows, 데스크톱 등의 단어위주로 검색을 하고,

hello, world 프로젝트 만들기 화면2

아래처럼 목록에서 Windows 데스크톱 마법사를 찾아서 클릭 후 다음을 누른다.

hello, world 프로젝트 만들기 화면3

이제 프로젝트가 위치할 디렉토리 패스와 프로젝트 이름을 설정한다. 여기서 주의할 점이 있는데,

  • 특수문자는 '-', '_' 외에는 사용하지 않도록 한다. 이외의 특수문자는 차후 컴파일시 오류의 원인이 된다.
  • 프로젝트 이름에는 공백문자를 포함하지 않는다. 마찬가지로 컴파일 오류의 원인이 된다.

위 사항을 지키면서 프로젝트 이름을 설정한다. 다음으로 만들기를 누른다.

hello, world 프로젝트 만들기 화면4

그럼 아래와 같은 대화상자가 뜨는데, 아래와 같이 콘솔 애플리케이션, 빈 프로젝트를 선택하도록 하자

여기서 콘솔 애플리케이션은 터미널 환경. 즉, 예전 컴퓨터 처럼 텍스트 기반(콘솔 기반)의 프로그램을 만들겠다는 의미이고, hello, world를 출력하기 위해 특별히 다른 프로그램이 필요한게 아니기 때문에 빈 프로젝트를 만든다.

hello, world 프로젝트 만들기 화면5

그럼 아래와 같이 프로젝트 생성중이라는 대화상자가 잠시 뜬 후,

hello, world 프로젝트 만들기 화면6

프로젝트가 생성된다. 이곳에서 오른쪽에 보면 솔루션 탐색기가 있는데 이걸 클릭해준다.

hello, world 프로젝트에 소스코드 추가하기1

그러면 비쥬얼 스튜디오 2019가 자동으로 생성한 프로젝트 구조가 보인다. 이제, 소스코드를 추가할텐데, 각자 만든 프로젝트 이름에서 마우스 우클릭을 하면,

hello, world 프로젝트에 소스코드 추가하기2

아래처럼 여러 메뉴들이 보일 것이다. 여기서 추가 메뉴를 눌러준다.

hello, world 프로젝트에 소스코드 추가하기3

새 항목을 클릭하고,

hello, world 프로젝트에 소스코드 추가하기4

아래 대화상자에서 C++ 파일을 선택한다.

그 다음으로 소스파일 이름을 설정할 텐데, 여기서 중요한 것이 있다. 아마 처음에는 이름이 소스.cpp 이런식으로 되어 있을건데, .cpp 는 C언어 소스파일이 아니라 C++(C plus plus -> cpp) 파일을 의미한다. 우리는 C언어 소스코드를 만들 것이므로 'hello_world.c' 와 같은 이름으로 코드를 만들어야 한다. 다 되었다면 추가 버튼을 누르자.

hello, world 프로젝트에 소스코드 추가하기5

아래처럼 아까는 보이지 않았던 소스코드가 생성되었다. 이제 이곳에 소스코드를 작성하면 된다.

hello, world 소스코드 추가완료 화면

코드는 아래와 같이 작성하면 된다.

#include <stdio.h>

int main() {

    printf("hello, world!!!");
    
    return 0;
    
}

소스코드를 자세히 알 필요는 없지만 혹시나 왜 저런 코드를 사용하였는지 궁금하다면 간략하게 설명을 하겠다.

 

#include <stdio.h> 는 일종의 약속이다. 여기서는 stdio.h 라는 파일을 참고하겠다 라는 의미인데, 여기서 stdio.h는 standard I/O header파일을 의미한다. 즉, 표준(standard) 입출력(input, output) 머리(header) 그러니까 입력하고 출력하기 위한 파일들의 요약(?)본을 참고하겠다는 의미를 하고 있는 것이다.

 

int main() { . . . } 부분이 C언어에서 일종의 약속된 코드인데 이부분을 실행하도록 약속이 되어있다. 자세한 이유는 더 낮은 레벨에서 설명해야 하는데 지금으로써는 너무 어려운 설명이 될 것이기에 차후에 설명하겠다. 지금은 main() 이 C코드에는 무조건 있고, 이부분부터 시작한다고 보면 된다.

 

printf("hello, world!!!");  이 부분이 실행할 프로그램에서 유일하게 무언가 실질적인 일을 하는 부분이다. 여기서 printf는 "" 안에 있는 문자열을 콜솔창에 출력하겠다 라는 의미의 함수이다.

 

return 0; 메인함수가 이 메인함수를 호출한 프로그램(콘솔)에게 "나 정상적으로 프로그램 실행되었어!!" 라고 알려주는 부분이 되겠다.

 

그럼 아래와 같이 소스코드를 작성하고, 메뉴에 있는 "로컬 Windows 디버거" 를 클릭하면,

hello, world 소스코드 빌드 및 실행하기1

아래처럼 자동으로 컴파일 및 링크작업을 하게되는데 이를 통틀어서 보통 프로그램을 빌드한다 라고 표현한다.

컴파일과 링크의 의미는 차후 설명하도록 하겠다.

hello, world 소스코드 빌드 및 실행하기2

잠시 후 콘솔창이 뜨며 아래와 같이 hello, world!!! 를 출력하는 프로그램을 만나볼 수 있게 된다.

hello, world 소스코드 빌드 및 실행하기3

비록 보잘것 없고 간단한 프로그램이지만 맛보기로는 더할나위 없는 소스코드이다. 다음부터는 본격적으로 C언어의 문법을 다루도록 하겠다.

'C언어 > 문법' 카테고리의 다른 글

2. 변수(variable) 의 의미, 선언과 초기화, 저장  (1) 2019.09.28

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

전통적인 유닉스 명령행 옵션은 하이픈( - ) 으로 시작해서, 옵션을 나타내는 한 글자와, 경우에 따라 추가 인자가 뒤따르기도 한다. 여기에 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 같은 문자열을 가리키는 포인터를 리턴한다.

라이브러리 함수는 표준 C 라이브러리를 구성하는 수많은 함수 가운데 하나일 뿐이다. 라이브러리 함수의 목적은 매우 다양해서, 파일을 연다든지, 시간을 사람이 읽을 수 있는 형태로 바꾼다든지, 문자열 2개를 서로 비교한다든지 하는 것들이 모두 포함된다.

상당수의 라이브러리 함수는 시스템 호출을 전혀 사용하지 않는다. 반면에 시스템 호출을 써서 구현된 라이브러리 함수도 있따. 예를 들어, fopen() 라이브러리 함수는 실제로 파일을 열기 위해 open() 시스템 호출을 사용한다. 라이브러리 함수는 종종 하부의 시스템 호출보다 사용하기 편리한 인터페이스를 제공하도록 설계됐다. 예를 들어 printf() 함수는 출력 서식과 데이터 버퍼링을 제공하지만, write() 시스템 호출은 단지 한 블록의 바이트를 출력할 뿐이다. 마찬가지로 malloc() 과 free() 함수는 다양한 관리 작업을 수행해서 이를 이용하면 하부의 brk() 시스템 호출보다 훨씬 쉽게 메모리를 할당하고 해제할 수 있다.

 

시스템 호출은 커널로의 관리된 진입점으로서, 이를 통해 프로세스가 커널에게 프로세스 대신 어떤 동작을 수행하도록 요청할 수 있다. 커널은 시스템 호출 API를 통해 광범위한 서비스를 제공한다. 이 서비스에는, 예를 들어 새로운 프로세스 생성, 프로세스간 통신용 파이프 생성 등이 있다.

시스템 호출이 동작하는데 있어 일반적이 사항은 다음과 같다.

 

  • 시스템 호출은 프로세서의 상태를 사용자 모드에서 커널 모드로 변경해서 CPU가 보호된 커널 메모리에 접근할 수 있게 한다.
  • 시스템 호출 목록은 고정되어 있다. 각 시스템 호출에는 고유한 숫자가 붙어있다.
  • 각 시스템 호출은 사용자 공간에서 커널공간으로 가져올 정보를 나타내는 인자를 포함할 수 있다.

 

프로그래밍 관점에서 시스템 호출은 C 함수 호출과 매우 비슷하다. 하지만 막후에서는 시스템 호출을 실행하는 동안 여러 단계를 거치게 된다. 이를 보여주기 위해, 여기서는 특정 하드웨어(x86-32)상에서의 발생 순서를 따라가 보겠다.

 

  1. 응용 프로그램이 C 라이브러리의 래퍼 함수를 호출해서 시스템 호출을 한다.
  2. 래퍼 함수는 시스템 호출의 모든 인자를 시스템 호출 트랩 처리 루틴에게 전달해야 한다. 이 인자는 스택을 통해 래퍼에게 전달되지만, 커널에게 전달하려면 특정 레지스터에 넣어야 한다. 래퍼 함수는 인자를 이 레지스터로 복사한다.
  3. 모든 시스템 호출이 같은 방법으로 커널에 진입하므로, 커널은 시스템 호출을 식별하는 방법이 필요하다. 이를 위해 래퍼 함수는 시스템 호출 번호를 특정 CPU 레지스터에 복사해넣는다.
  4. 래퍼 함수는 트랩 기계어 명령을 실행하고, 이는 프로세서를 사용자 모드에서 커널 모드로 전환해 시스템의 트랩 벡터 0x80 이 가리키는 코드를 실행한다.
  5. 0x80 트랩을 처리하기 위해, 커널은 system_call() 루틴을 호출한다. 이 핸들러는

    a) 레지스터 값들을 커널 스택에 저장한다.
    b) 시스템 호출 번호가 유요한지 확인한다.
    c)적절한 시스템 호출 서비스 루틴을 호출한다. 시스템 호출 서비스 루틴은 시스템 호출 번호를 시스템 호출 서비스 루틴 테이블에 대한 인덱스로 삼아서 찾는다.시스템 호출 서비스 루틴이 인자가 있으면 먼저 유효성을 확인한다. 예를 들어, 해당 주소가 사용자 메모리의 유효한 위치를 가리키는지 확인한다. 그 다음에 서비스 루틴이 필요한 작업을 수행하는데, 이 과정에서 인자가 가리키는 주소의 값을 변경하거나 사용자 메모리와 커널 메모리 사이에 데이터를 전송할 수도 있다. 마지막으로 서비스 루틴은 결과 샅애를 system_call() 루틴에게 리턴한다.
    d) 커널 스택에서 레지스터 값들을 복원하고 시스템 호출 리턴값을 스택에 넣는다.
    e) 래퍼 함수로 돌아오면서, 동시에 프로세서도 사용자 모드로 되돌린다.

  6. 시스템 호출 서비스 루틴의 리턴값이 에러를 나타내면, 래퍼 함수는 전역 변수 errno를 그 값으로 설정한다. 래퍼 함수는 그 다음에 시스템 호출의 성공 여부를 나타내는 정수 리턴값을 제공하면서 호출 함수로 리턴한다.

 

위의 내용을 모두 이해해야 시스템 프로그램이 가능한 것은 아니다. 하지만 매우 간단한 시스템 호출이다로 상당량의 작업이 필요하므로, 시스템 호출에는 작지만 주목할 만한 오버헤드가 있다는 점을 유의해야 한다.

리눅스는 유닉스와 마찬가지로 /proc 파일 시스템을 제공한다. /proc 파일 시스템은 /proc 디렉토리에 마운트된 디렉토리와 파일로 이뤄져있다.

/proc 파일 시스템은 엄연히 따지면 실제로 존재하는 파일은 아니고, 커널 데이터 구조를 파일 시스템상의 파일과 디렉토리 형태에 대응하여 가상으로 파일화 한 인터페이스이다. 많은 이들이 이를 리눅스의 대표적인 특징으로 들고 있다.

/proc 파일 시스템은 다양한 시스템 시스템 속성을 보고 변경할 수 있는 손쉬운 방법을 제공한다. 또한 /proc/PID라는 디렉토리를 통해 커널위에서 운영중인 모든 프로세스에 대한 정보를 확인할 수 있다.

/proc 파일의 내용은 일반적으로 사람이 읽을 수 있는 텍스트 형태이고, 쉘 스크립트로 파싱할 수 있다. 프로그램에서 원하는 파일을 쉽게 열고, 읽거나 쓸 수 있다. 대부분의 경우 /proc 디렉토리의 파일을 수정하기 위해서는 프로세스가 권한 을 가지고 있어야 한다.

클라이언트/서버 응용프로그램이란 2개의 요소 프로세스로 나뉘어 있는 응용 프로그램을 말한다.

 

  • 클라이언트는 설버에게 요청 메시지를 보내어 어떤 서비스를 수행하라고 요청한다.
  • 서버는 클라이언트의 요청을 검사하고, 적절한 동작을 수행한 뒤, 클라이언트에게 응답 메시지를 보낸다.

 

클라이언트와 서버는 요청과 응답으로 이뤄진 확장된 대화에 참여하기도 한다.

일반적으로 클라이언트 응용 프로그램은 사용자와 상호작용하는 데 비해, 서버 응용프로그램은 공유 자원에 대한 접근을 제공한다. 보통 하나 또는 소수의 서버 프로세스와 통신하는 다수의 클라이언트 프로세스가 존재한다.

클라이언트와 서버는 같은 호스트 컴퓨터 내에 존재하기도 하고, 네트워크로 연결된 서로 다른 호스트에 존재하기도 한다. 서로 통신하기 위해 클라이언트와 서버는 IPC통신 메커니즘을 사용한다.

서버는 다음과 같은 다양한 서비스를 구현할 수 있다.

 

  • 데이터베이스나 기타 공유 정보 자원에 대한 접근을 제공
  • 네트워크로 연결된 원격 파일에 대한 접근을 제공
  • 비즈니스 로직을 캡슐화
  • 공유 하드웨어 자원에 대한 접근을 제공
  • 웹페이지를 제공

 

단일 서버 내의 서비스를 캡슐화하면 다음과 같은 여러 이유로 편리하다.

 

  • 효율성 : 같은 자원을 컴퓨터마다 제공하는 것보다 서버가 관리하는 하나의 제원을 제공하는 편이 비용이 덜 든다.
  • 제어, 조정, 보안 : 자원을 한곳에 둠으로써, 서버가 자원에 대한 접근을 조정하거나, 선택된 클라이언트만 접근할 수 있도록 보안 장치를 둘 수 있다.
  • 이종 환경에서 동작 : 네트워크상의 다양한 클라이언트와 서버가 각기 다른 하드웨어와 OS에서 동작할 수 있다.

 

실시간 응용 프로그램은 입력에 대해 빠른 시간 안에 응답해야 하는 응용 프로그램이다. 흔히 그런 입력은 외부 센서나 특수한 입력 디바이스로부터 들어오고, 출력은 외부 하드웨어를 제어하는 형태를 취한다. 실시간 응답을 요구하는 응용프로그램으로는 자동 조립라인, 은행 ATM, 항공기 항법장치 등 많이 있다.

많은 실시간 응용프로그램이 입력에 대해 빠른 응답을 요구하지만, 결정적인 요소는 이벤트 발생 뒤 응답이 특정 데드라인 안에 전달됨이 보장되는지이다.

실시간 응답성을 제공하기 위해서는 , 특히 짧은 응답 시간이 요구될 때, 하부에 있는 운영체제의 지원이 필요하다. 대부분의 운영체제는 그런 지원을 제공하지 않는데, 실시간 응답성이 다중 사용자 시분할 운영체제의 요구사항과 충돌할 수 있기 때문이다. 전통적인 유닉스 구현은 실시간 운영체제가 아니지만, RTOS버전이 만들어졌다. 리눅스의 RTOS 버전도 만들어졌고, 최근의 리눅스는 그 자체로서 실시간 응용 프로그램을 완전히 지원하는 방향으로 발전하고 있다.

POSIX.1b는 실시간 응용 프로그램을 지원하기 위해 비동기 I/O, 공유 메모리, 메모리에 매핑된 파일, 메모리 잠금, 실시간 클록과 타이머, 대체 스케줄링 정책, 실시간 시그널, 메시지 큐, 세마포어 등 여러 확장 기능을 정의하고 있다. 엄격히 실시간으로 인정되지는 않아도, 대부분의 유닉스 구현은 현재 이들 확장 기능의 일부 또는 전부를 지원한다.

'리눅스 > 기본개념 및 용어' 카테고리의 다른 글

/proc 파일 시스템  (0) 2019.09.26
가상 터미널, 날짜와 시간  (0) 2019.09.25
세션, 제어 터미널, 제어 프로세스  (0) 2019.09.24
스레드, 프로세스 그룹, 쉘 작업 제어  (0) 2019.09.23
시그널  (0) 2019.09.23

+ Recent posts