리눅스 Tips, 리눅스 C/C++ 프로그래밍, 모바일 클라우드 동향 및 테스트 등

  Welcome to my Blog!

 °Name : KWON HAN SEUNG
 °Interested in
   - Linux
   - Mobile Cloud
   - Image Processing
   - Signal Processing

 Facebook Link...
 Univ. Team Link...

2012년 11월 30일 금요일

리눅스 시스템 프로그래밍 : 03 입력과 출력(1)



1. 소개

 기계는 사용자의 입력을 받아서, 프로그래밍 되어 있는 대로 일을 하고 그 결과물을 출력한다. 믹서기는 사과를 입력 받고 버튼을 누르면, 프로그래밍 되어 있는 대로 모터를 돌려서 사과를 잘개 쪼개고 그 결과물로 사과쥬스를 출력한다. 컴퓨터는 정보를 처리하기 위한 기계로 입력을 받아서 처리하고 그 결과를 출력한다는 점에서 봤을 때, 근본적으로 믹서와 다를 바가 없다. 믹서와 다른 점이라면 입력으로 사과 대신 (비트로 이루어진)정보를 입력 받아서 처리하고 그 결과물로 정보를 출력한다는 점 정도일 것이다.

 여러분은 이미 컴퓨터 시스템은 키보드를 통해서 입력받은 데이터를 프로그램에 넘겨서 처리하고 그 결과물을 모니터로 출력하고 있다는 것을 알고 있을 것이다. 처리하고자 하는 데이터의 종류에 따라서 입력기기가 마우스나 펜, 터치 스크린이 되고, 출력기기 역시 파일, 프린터, 테이프 등이 될 것이다.

 이번 장에서는 컴퓨터 시스템에서의 입력과 출력을 제어하는 방법에 대해서 알아볼 것이다.

2. 모든 것은 파일이다

 유닉스에서는 모든 것을 파일로 취급한다. 하드디스크에 존재하는 파일, 디렉토리는 물론이고 네트워크 카드, 사운드 카드, 키보드, 마우스, 하드디스크 그 자체 까지 모두 파일로 취급한다. 유닉스 시스템을 처음 접할 때 꽤나 혼동되는 부분이기도 하다. 윈도우에는 파일은 단지 하드디스크 상에 존재하는 논리적인 정보의 집합을 그 대상으로 하기 때문이다. 예를 들자면 하드디스크는 C: D:와 같은 파일이 아닌 장치로 인식한다. 그러나 유닉스 시스템에서는 장치도 파일로 취급된다. 리눅스도 유닉스와 동일한 파일시스템을 가지고 있으므로, 리눅스를 예로 들어서 설명하겠다. 리눅스에서 하드디스크는 /dev/hda1, /dev/hda2 이런식으로 하드디스크상의 파일로 존재한다. 뿐만 아니다. 사운드 카드는 /dev/dsp, 프린트는 /dev/lp, cdrom은 /dev/cdrom 의 이름을 가진 파일로 존재한다.

 일반 사용자의 입장에서 장치를 파일로 인식하는 것이 불합리해 보일 수 있다. 그러나 개발자 입장에서는 매우 합리적인 방법이다. 모든 장치라는 것은 입력을 받아들여서 출력하는 매커니즘을 가지는데, 이는 파일의 매커니즘과 완전히 동일하기 때문으로 파일을 다루는 것과 동일한 방식으로 다른 장치들도 접근할 수 있음을 의미한다. 사운드 카드를 예로 든다면, test.wav 파일을 읽어서 /dev/dsp에 쓴다는 식으로 사운드를 플레이 할 수 있다. 실제로 프로그래밍 할 때도 일반 파일을 읽고 쓰는 것처럼 장치들에 접근할 수 있다. 물론 일반 파일들에 읽고 쓰는 것 보다는 약간 복잡하긴 하지만 원리적으로는 동일하다.

3. 파일의 종류

 위에서 예상했겠지만 파일이라고 해서 다 같은 파일은 아니다. 일반적으로 알고 있는 비트 데이터를 저장한 파일이 있는가 하면, 장치와 대응되는 파일도 있다. 내부 통신과 외부 통신을 위한 소켓파일(리눅스는 네트워크 통신도 파일을 통해서 한다), 파이프와 대응되는 파일, 디렉토리와 대응되는 파일 등이 있다. 예컨데 모든 것이 파일이다.

 리눅스에서는 ls 명령을 이용해서 이러한 파일의 종류를 알아낼 수 있다.

# ls -al
drwxr-xr-x 12 root root       13820 2007-11-12 22:19 .
drwxr-xr-x 21 root root        4096 2007-10-31 23:47 ..
drwxr-xr-x  2 root root         100 2007-11-13 06:45 .initramfs
-rw-r--r--  1 root root           0 2007-11-13 06:45 .initramfs-tools
drwxr-xr-x  3 root root          60 2007-11-13 06:45 .static
drwxr-xr-x  5 root root         120 2007-11-12 22:19 .udev
lrwxrwxrwx  1 root root          13 2007-11-13 06:45 MAKEDEV -> /sbin/MAKEDEV
crw-rw----  1 root root     10,  63 2007-11-12 21:46 acpi
crw-rw----  1 root audio    14,  12 2007-11-12 21:46 adsp
crw-rw----  1 root audio    14,   4 2007-11-12 21:46 audio
drwxr-xr-x  3 root root          60 2007-11-13 06:45 bus
lrwxrwxrwx  1 root root           3 2007-11-13 06:45 cdrom -> hda


 ls 의 가장 앞 필드의 첫 문자가 파일의 종류를 나타낸다. 아래는 ls 를 통해서 알아낼 수 있는 파일의 종류이다. 파일들은 아래의 종류 중 하나에 포함된다. pipe, 링크, 소켓 등에 대해서는 나중에 자세히 언급할 것이다. 우선은 아래와 같은 다양한 종류의 파일이 있다는 것만 이해하고 넘어가도록 하자.

-
일반파일
txt, jpg, wav, pdf 등 ... 
d
디렉토리
디렉토리
l
링크
심볼릭 링크 또는 하드 링크
c
장치
프린터, 사운드 카드, CD ROM 등의 장치
s
소켓
프로세스 간 통신에 사용
p
파이프
파이프

4. 파일 열기

 파일을 다루는 기본적인 흐름은 다음과 같다.

  1. 파일을 연다(open).
  2. 데이터를 읽거나(read), 데이터를 쓴다(write).
  3. 파일을 닫는다(close).

 가장 먼저 해야할 일이 파일을 open하는 것임을 알 수 있다. 이것은 커널에게 파일을 가지고 작업할 수 있도록 요청하는 것으로, 커널은 여러 가지 조건을 판단해서 파일을 open 해줄 것인지 아닌지를 결정하고, 그 결과를 open을 요청한 프로세스에게 리턴한다.

 파일을 오픈해 줄것인지 아닌지를 결정하는 데에는 다음과 같은 이유가 있다.
  1. 실제로 존재하는 파일인지 여부 확인
  2. 존재하지 않는 파일인 경우 새로 파일을 생성할 것인지 여부 결정
  3. 파일을 쓸 수 있는 권한이 있는지 확인 :
    리눅스는 다중 사용자 운영체제로 파일을 비롯한 모든 자원에 대한 접근 권한이 설정되어 있다. 따라서 해당 파일을 사용할 수 있는 권한이 있는지 확인해야 한다.



  4.1 open 시스템콜을 이용한 파일 열기

 파일 작업을 하기로 마음을 먹었다면, 커널에 정해진 파일을 열 수 있도록 허용해 달라고 요청을 해야 할 것이다. 커널에 요청을 할 수 있도록 지원되는 함수를 시스템 콜(혹은 시스템 함수)라고 언급했던 것을 기억하고 있을 것이다. 리눅스는 파일 오픈과 관련된 요청을 위해서 open 이라는 시스템 함수를 제공한다.

 open함수는 다음과 같이 선언되어 있다.

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode)

pathname : 열기를 요청하는 파일이다. 상대 경로 혹은 절대 경로를 지정할 수 있다.
flags : 어떤 방식으로 열 것인지를 결정하기 위해서 사용하며, bitwise연산을 이용해서 다양한 방식으로 조합할 수 있다. 다음은 대표적으로 사용되는 flag 들이다.
  • O_RDONLY
    읽기 전용으로 파일을 연다. 쓸 수 없다.
  • O_WRONLY
    쓰기 전용으로 파일을 연다.
  • O_RDWR
    읽기와 쓰기 모두 가능하도록 파일을 연다.
  • O_CREAT
    파일이 존재하지 않을 경우 파일을 생성한다.
  • O_EXCL
    O_CREAT를 써서 파일을 오픈 할 경우, 이미 파일이 존재한다면 error를 리턴하게 한다. 파일을 덮어쓰거나 하는 실수를 방지하기 위한 용도로 사용할 수 있다.

mode : 파일의 권한을 결정하기 위해서 사용하며, 생략이 가능하다. 파일이 생성되면 파일에 대한 소유자와 그룹은 자신이 된다. 이 인자를 사용하면 owner(사용자), group(그룹), other(타인) 각각에 대해서 읽기, 쓰기, 실행 권한을 부여할 수 있다. 역시 bitwise 연산을 이용해서 다양한 조합이 가능하다.
  • S_IRWXU
    00700 모드로 파일 소유자에게 읽기, 쓰기, 쓰기 실행권한을 준다.
  • S_IRUSR
    00400 으로 사용자에게 읽기 권한을 준다.
  • S_IWUSR
    00200 으로 사용자에게 쓰기 권한을 준다.
  • S_IXUSR
    00100 으로 사용자에게 실행 권한을 준다.
  • S_IRWXG
    00070 으로 그룹에게 읽기, 쓰기, 실행 권한을 준다.
  • S_IRGRP
    00040 으로 그룹에게 읽기권한을 준다.
  • S_IWGRP
    00020 으로 그룹에게 쓰기권한을 준다.
  • S_IXGRP
    00010 으로 그룹에게 실행권한을 준다.
  • S_IRWXO
    00007 으로 기타 사용자 에게 읽기, 쓰기, 실행 권한을 준다.
  • S_IROTH
    00004 으로 기타 사용자 에게 읽기 권한을 준다.
  • S_IWOTH
    00002 으로 기타 사용자 에게 쓰기 권한을 준다.
  • S_IXOTH
    00001 으로 기타 사용자 에게 실행 권한을 준다.

 예를 들자면 다음과 같은 방식으로 파일을 열 수 있을 것이다.

// 파일이름 hello.txt 에 데이터를 (단지)쓰기 위해서 연다.
// 파일이 없을 경우 생성하며
// 권한은 640 으로 한다.
open("hello.txt", O_CREAT|O_WRONLY, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP);

 실제로 위의 권한으로 파일을 오픈하는 프로그램을 만들어 보도록 하자. 아래의 프로그램은 단지 파일을 열기만 할 뿐이지만 성공적으로 파일을 생성할 것이다. 프로그램의 이름은 hello.c 로 하겠다. 프로그램의 작성과 실행은 yundram계정을 이용했다.

#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main()
{
  int fd;
  fd = open("hello.txt", O_CREAT|O_WRONLY, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP);
}

 ls(1)를 이용해서 hello.txt를 확인해 보도록하자.

yundream@yundream:~$ ls -al hello.txt
-rw-r----- 1 yundream yundream 0 2007-11-20 20:23 hello.txt

 소유자과 그룹이 yundream이고 640의 권한을 가지는 파일이 생성되었음을 알 수 있다. 파일을 열기만 했을 뿐, 아무런 작업을 하지 않았기 때문의 파일의 크기는 0이다.

  4.2 file descriptor

 open 함수를 다시 보도록 하자. open 함수는 리턴 결과로 다루게 될 파일의 이름이 아닌 int형 정수를 넘겨주는 것을 알 수 있다. 이 int형 정수가 바로 파일을 가리키는 역할을 한다. 파일을 지정하기 때문에 'file descriptor' 혹은 '파일 지정번호' 라고 한다. 이것은 우리가 일반적으로 알고 있는 숫자가 아닌 열려진 파일객체를 가리키는 것임에 유의하기 바란다.

 open 을 이용해서 파일을 성공적으로 열었다면, 이후의 모든 쓰기/읽기 등의 작업은 파일이름 대신 파일지정번호를 이용하게 된다.

    +------+
    | FILE |<---- file discriptor = open
    |      |
    +------+
    open으로 리턴된 int형 정수는 file discriptor 로써, 열린 파일을 가리킨다.

 파일 지정번호는 0 이상이여야 한다. 0 보다 작은 경우는 어떤 이유로 파일을 여는 것이 실패했음을 의미한다. 위의 프로그램은 아래처럼 파일 오류까지 검사하는 좀 더 그럴듯한 프로그램으로 바꿀 수 있을 것이다.

  fd = open("hello.txt", O_CREAT|O_WRONLY, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP);
  if (fd < 0)
  {
    perror("file open error:");
    return 1;
  }

다음 포스트에 이어서...

2012년 11월 27일 화요일

리눅스 시스템 프로그래밍 : 02 시스템 프로그래밍의 시작


1. 소개

 시스템 프로그래밍이란 시스템 프로그램을 작성하기 위한 일련의 과정을 의미한다. 시스템 프로그램은 시스템을 제어하는 프로그램이므로 결국 시스템 프로그래밍은 시스템을 알아가는 과정이라고 볼 수 있을 것이다. 시스템을 알아가는 과정이라고 해서, 컴퓨터의 CPU, Memory, 하드디스크 등의 시스템 자체에 대해서 공부할 필요는 없으니 미리 겁을 먹을 필요는 없다. 앞 장에서 설명했던 것처럼 운영체제가 제공하는 함수를 통해서, 시스템에 간접적으로 접근할 수 있기 때문이다.

 운영체제 덕분에 우리는 컴퓨터 시스템에 대한 대략적인 이해를 하고 있는 정도로도 컴퓨터 시스템을 다룰 수 있다. 물론 여러분이 임베디드 환경에서 작동하는 프로그램을 만들어야 한다면 컴퓨터 시스템에 대한 좀더 깊은 지식이 필요로 하겠지만, 대게의 경우 운영체제를 통한 간접적인 지식만으로도 시스템 프로그램을 작성하는데 문제가 생기는 경우는 없을 것이다.

2. 운영체제와 커널

 운영체제는 컴퓨터를 운용하는 특별한 종류의 프로그램이다. 그리고 운영체제의 핵심이라고 할 수 있는 부분이 바로 '커널' 이다. 커널은 컴퓨터 시스템을 제어하는 일, 그러니까 메모리를 관리하고, 디스크에 데이터를 읽고 쓰고, 프로그램을 실행시키고, 네트워크 카드, 사운드 카드, 그래픽 카드 등의 하드웨어 장치를 관리하는 일을 한다.

 학문적인 관점에서 보자면 운영체제란 바로 커널을 가리키는 것이다. 예를 들어 리눅스 운영체제는 리눅스 커널을 의미한다. 하지만 커널만을 가지고 할 수 있는 일은 그렇게 많지 않다. 커널 위에 shell, 운영자 관리 도구와 같은 필수적인 다른 프로그램이 있어야 비로서 컴퓨터를 제어할만한 환경이 갖추어진다. 이런 이유로 일반적인 관점에서 '커널 + 필수 프로그램' 까지를 운영체제라고 부르고 있다.

다음 그림은 컴퓨터 시스템과 커널, 프로그램의 관계를 보여주고 있다.


  • Linux Kernel은 컴퓨터 시스템의 자원을 직접 제어한다.
  • 프로그래머가 작성한 프로그램은 직접 시스템 자원을 제어할 수는 없다.
  • 프로그램은 커널에서 제공하는 함수를 이용해서 간접적으로 시스템 자원을 제어한다.

  2.1 Kernel space와 User space

 이렇게 커널이 할 수 있는 영역과 일반 응용 프로그램이 할 수 있는 영역이 구분이 되어 있다. 응용 프로그램은 직접적으로 시스템에 도달할 수 없으며, 리눅스 커널에 요청을 하는 방식으로 간접적으로 시스템을 제어할 수 있다. 이때 커널이 다루는 영역Kernel space라고 하고, 일반 응용 프로그램이 다루는 영역User space라고 한다.

Kernel space와 User space가 나뉘는 가장 큰 이유는 보안의 문제다. 아무 프로그램이나 시스템 자원을 직접 제어할 수 있다면, 심각한 문제가 발생할 수 있기 때문이다. 하드웨어와 프로그램의 중간에 리눅스 커널이 존재함으로써, 시스템 자원을 안전하고 효율적으로 사용할 수 있게 해준다. 응용 프로그램은 절대로 시스템에 바로 접근할 수 없다.

  2.2 응용 프로그램

 운영체제를 제외한 프로그램, 즉 운영체제 위에서 작동하는 모든 프로그램을 응용 프로그램이라고 한다. 시스템 프로그램과 네트워크 프로그램 혹은 게임 등 모든 프로그램이 응용 프로그램이다. 응용 프로그램이 실행되면, '시스템 콜'을 이용해서 운영체제로 컴퓨터 시스템의 자원의 사용을 요청하는 방식으로 원하는 작업을 수행하게 된다. 예를 들어 주소록을 저장하는 프로그램은 키보드를 사용할 수 있도록 커널에게 요청해서 사용자 입력을 받아들이고, 받아들인 입력을 임시로 저장하기 위해서 메모리 공간을 요청하고, 영구히 저장하기 위해서 디스크를 요청하는 과정을 거쳐서 주어진일을 하게 될 것이다.

  2.3 시스템 콜과 라이브러리

 '시스템 콜'이란 운영체제에게 요청할 수 있도록 만들어진 함수임을 알 수 있을 것이다. 이런 이유로 시스템 콜은 종종 시스템 함수라고 부르기도 한다. 리눅스 운영체제는 대략 190개 정도의 시스템 콜을 제공한다. 시스템 콜에 대한 자세한 목록은 Linux System Call Table 문서를 참고하기 바란다. 시스템 콜은 운영체제마다 다르기 때문에 만약 Windows에서 프로그래밍 하기를 원한다면, 그에 맞는 시스템 콜 목록이 필요하다.

 기본적으로는 시스템 콜만을 이용하는 것으로도 어떠한 프로그램이든지 만들어낼 수 있기는 하다. 그러나 시스템 콜만을 이용해서 프로그램을 작성하는 것은 보통 어려운 일이 아니다. 시스템 콜만을 가지고도 이미지를 읽어서 화면에 출력하는 프로그램을 작성할 수는 있기는 할 것이다. 하지만 개발에 너무 많은 시간이 걸리기 때문에 시스템 콜만을 가지고 개발하지는 않는다. 보통은 이미 만들어진 이미지 제어를 위한 함수를 사용해서 개발을 하게 된다. 이렇게 함수를 만들고, 용도에 맞게 모아 놓은 것을 라이브러리라고 한다. 함수 모음집이라고 할 수 있을 것이다. 이러한 라이브러리에는 그래픽 라이브러리, 수학 라이브러리 등 여러 종류가 있으며, 몇 개의 함수만 모아 놓은 간단한 라이브러리부터, 돈을 받고 판매하는 상업용 라이브러리까지 폭넓게 존재한다.

3. 리눅스 운영체제

 리눅스 운영체제는 커널과 필수 프로그램으로 이루어지며 일반적으로 배포판을 리눅스 운영체제라고 간주한다. 배포판은 리눅스를 쉽게 설치하고 관리할 수 있도록 커널, 관리 프로그램, 필수 응용 프로그램을 패키징 한 것이라고 보면 된다. 패키징 방법에 따라서 다양한 배포판이 존재하는데, RedHat, Ubuntu와 같은 것들이 널리 사용되는 배포판이다.

 이 문서는 여러분이 리눅스 운영체제를 어느 정도 사용할 줄 안다고 가정하고 만들어졌으므로 설치와 명령어의 사용과 같은 내용을 다루진 않을 것이다. 이하 리눅스 운영체제의 시스템영역에서의 상세한 내용은 시스템 프로그래밍을 진행하면서 조금씩 익혀나가도록 하겠다.

  3.1 리눅스 커널 구조

 리눅스 운영체제의 핵심인 커널은 다음과 같은 구조를 가진다.

이미지 참고 : 리눅스 시스템 프로그래밍 고영웅

  • Process는 프로그램의 실행 이미지로 프로그래밍 된 대로 필요한 일을 수행한다. 리눅스 커널은 User Space와 Kernel Space가 분리되어 있다. 그러므로 User Space의 프로세스는 직접 하드웨어 자원이나 기타 커널에서 제공하는 (IPC 와 같은) 서비스를 사용할 수 없다.
  • System Call은 커널에서 제공하는 요청 인터페이스 이다. 프로세스는 System Call을 이용해서 커널에 필요한 작업을 요청할 수 있다.
  • 커널은 Device Driver를 이용해서 각종 장치들을 제어하고, 프로세스를 스케쥴링하고, 메모리를 관리한다. 또한 프로세스 간 통신을 위한 IPC를 제공한다.
  • 컴퓨터 하드웨어는 다양한 장치들로 구성이 된다. 커널은 이러한 장치들을 제어해야 하며, 이러한 장치를 제어하기 위한 단위 코드를 Device Driver이라고 한다.

  3.2 리눅스 도움말 시스템

 '도움말 시스템'은 일반 사용자와 개발자를 막론하고 중요한 요소 중 하나이다. Windows 같은 경우에는 MSDN이라는 도움말 시스템을 제공한다. 개발자는 프로그램을 작성하는 도중에 모르는 함수나 기술적인 사항이 있다면, 온라인 혹은 오프라인 상에서 매우 상세한 도움말을 얻을 수 있다.

 운영체제가 성공을 하기 위해서는 개발자를 끌어들일 수 있어야 한다. 개발자를 끌어들이는 가장 좋은 방법은 좋은 개발 도구를 제공하는 것과 좋은 도움말 시스템을 제공하는 것이다. 이런 이유로 MS, Apple, IBM, HP, SUN 등의 회사들은 전략적으로 좋은 도움말 시스템을 제공하기 위해서 노력한다.

 유닉스 운영체제는 'man page'라는 도움말 시스템을 제공한다. 개발자는 다음과 같은 방법으로 함수나 명령어들에 대한 도움을 얻을 수 있다.

# man sprintf
 
 리눅스 역시 유닉스와 동일한 man page 제공한다. man page에 대한 자세한 내용은 man 도움말 문서를 참고하기 바란다.

  3.3 리눅스 표준 라이브러리

 리눅스는 자주 사용되는 기능을 구현한 표준 라이브러리 함수를 제공한다. 이 라이브러리에는 간단한 수학 연산, 문자열의 변환, 문자열 입출력, 병렬 처리, 파일 제어 등을 손쉽게 할 수 있도록 도와주는 함수를 제공한다. 이들 함수를 시스템 콜만을 이용해서 직접 만든다고 한다면 짧게는 100라인에서 길게는 수 천 라인까지 작성을 해야 하겠지만 표준 라이브러리 함수를 이용하면 몇 줄 내에서 해결할 수 있다.

 리눅스 시스템 프로그래밍은 시스템 콜과 표준 라이브러리 함수를 이용한 프로그래밍 영역이라고 보면 된다. 나머지는 응용 프로그래밍 영역으로, 시스템 콜과 표준 라이브러리에 덧붙여 다른 광범위한 영역의 라이브러리들을 사용하게 된다. 예를 들어, 게임을 만들고자 한다면 시스템 프로그래밍과 관련된 함수들외에 그래픽, 사운드, 이벤트 관련 라이브러리들을 사용하게 될 것이다.

 리눅스 표준 라이브러리에서 제공하는 함수들은 표준 라이브러리 man page를 참고하기 바란다. 그냥 참고만 하도록 하자. 굳이 익히려고 노력할 필요 없다. 문서를 읽어나가다 보면 자연스럽게 익히게 될 것이다.

리눅스 시스템 프로그래밍 : 01 Intro.


1. 시스템 프로그래밍의 중요성

 '현실에서는 시스템 프로그래밍 기술이 정말 중요한가?' 라는 얘기를 듣곤 한다. 프로그램을 만드는 프로그램을 이용해서 프로그램을 만드는 세상인데, '굳이 별로 재미도 없어 보이는 시스템 프로그래밍 쪽을 공부해야 하나?' 하는 것이다. 훌륭한 Visual 개발 도구와 잘 만들어진 라이브러리나 컴포넌트 기타 제작 도구들이 널려있다.

 이론상으로는 그럴 듯 하다. 실제로 그런 세계가 올지도 모른다. 컴퓨터에게 '은행 업무를 효율적으로 하기 위한 고객 관리 프로그램이 필요하거든 한번 만들어봐' 라고 하고 몇 가지 조건만 주면 그럴듯한 프로그램을 만들어내는 시대가 올지도 모른다.

 그러나 (프로그래머의 입장에서는)다행스럽게도 지금의 컴퓨터는 산업 전반에서 요구하는 모든 종류의 요청에 대해서 프로그램을 만들어낼 만큼 똑똑하지 못하다. 조금씩 인간이 개입하는 여지가 줄어들기는 하겠지만 당분간은 말 만으로 프로그램이 생성되어서, 프로그래머가 필요 없는 세상은 오지 않을 것 같다.

 결국 컴퓨터가 자동으로 할 수 없는 것은 인간이 책임질 수 밖에 없다. 이러한 영역이 주로 시스템 프로그래밍 영역이 된다. 메뉴, 창, 버튼, 입력 상자의 배치와 같은 프로그램의 외형은 컴퓨터가 자동으로 해낼 수 있지만 그 밑 단의 시스템 영역에서 이루어지는 데이터의 저장, 다중 작업, 데이터 통신과 같은 일들은 경험 있는 프로그래머가 반드시 필요하다. 여기에 최적화라는 컴퓨터가 자동으로 수행할 수 없는 영역도 있다. 최적화라는 것은 프로그램의 성능을 좀 더 높이기 위해서 시스템의 자원 사용을 최적화 하는 일련의 작업들이다. 메모리를 어떻게 사용할 것인지, 프로그램 간에 데이터 통신은 어떻게 할 것인지, 대량의 데이터를 병렬 처리 하기 위해서 어떤 시스템 통신 모델을 사용할 것 인지에 따라서 수십% 혹은 수백 %의 성능의 차이가 날 수 있는데, 이런 일은 자동화가 불가능한 영역이다. 역시 시스템에 대한 지식을 가진 경험 있는 시스템 프로그래머가 필요하다.

 결론은 이렇다. 아주 간단한 프로그램이 아니라 제대로 돌아가는 프로그램을 만들고자 한다면, 혹은 취미로 프로그램을 작성하고자 하는 생각 이 아니라면 시스템 프로그래밍은 선택 사항이 아닌 필수 사항이다.

2. 일반적인 프로그래밍 학습 과정

 공부에는 정도가 없다고 하지만 다음과 같은 순서로 프로그래밍을 학습한다면 효율이 좋을 것으로 생각된다.
  1. 프로그래밍 언어(Java, C, C++ 등)를 익힌다.
  2. 시스템 프로그래밍에 대하여 공부한다.
  3. 네트워크 프로그래밍에 대하여 공부한다.
  4. 네트워크 프로그래밍을 바탕으로 좀 더 깊은 수준의 시스템 프로그래밍에 대하여 공부한다.
  5. 효율적인 프로그래밍을 위하여 자료구조와 알고리즘에 대한 공부를 한다.
  6. 게임 및 보안 관련 프로그램을 제작하는 등의 일을 통하여 응용해 본다.

3. 운영체제와 시스템 프로그래밍

 시스템 프로그래밍이란 컴퓨터 시스템을 다루는 프로그래밍을 의미한다. 여기에서 컴퓨터 시스템이란 컴퓨터를 이루는 구성 요소들인 모니터, 키보드, 메모리, 하드디스크, NIC를 의미한다. 그렇다고 해서 프로그래머가 모니터, 키보드, 메모리와 같은 기계장치에 직접 명령을 내려야 하는 것은 아니다. 인간과 컴퓨터 사이에 운영체제가 존재하기 때문이다.  운영체제가 있는 덕분에 프로그래머는 컴퓨터 기계장치에 직접 명령을 내리지 않고, 운영체제를 통해서 간접적으로 명령을 내리는 정도로 컴퓨터 시스템을 제어할 수 있게 된다.
 

 은행 창구가 있는 덕분에 복잡한 금융거래를 간단하게 할 수 있는 것과 같은 이치다. 돈을 송금하는 과정은 여러 단계의 매우 복잡한 과정을 거쳐야 할 것이다. 그러나 고객은 그 과정을 알 필요 없다. 그냥 '이 돈을 어디 어디 계좌로 보내주세요' 라는 내용을 적은 명세표만 직원에게 주면 된다. 나머지 일은 창구 직원이 알아서 해준다.

 시스템 프로그래머에게 있어서 운영체제는 프로그래머와 컴퓨터 시스템 간의 창구 역할을 하는 프로그램으로 이해하면 될 것이다. 프로그래머가 운영체제에게 어떤 요청을 할 때에는 '이러한 일을 해주세요' 라고 명세표를 작성해야 할 것이다. 이러한 요청을 쉽게 하도록 하기 위해서 운영체제는 시스템 함수라는 것을 제공한다. 예를 들어, 리눅스 운영체제는 메모리할당과 관련되어서 malloc()이라는 시스템 함수를 제공한다. 그러면 프로그래머는 다음과 같은 방법으로 메모리 할당을 요청하게 된다.
malloc(2*1024*1024) // 2메가의 메모리 공간을 요청

 요청을 받은 운영체제는 복잡한 과정을 거쳐서 메모리를 할당하고, 메모리가 할당된 공간을 프로그래머에게 알려준다. 이제 프로그래머는 할당된 공간을 이용하기만 하면 된다.

 이러한 시스템으로의 명령은 안타깝게도 운영체제마다 약간의 차이가 있다. 윈도우와 리눅스가 다르고, 리눅스와 솔라리스가 또 다르다. 때문에 윈도우 시스템 프로그래밍 기술을 리눅스 시스템 프로그래밍을 하는데 이용하는 데에는 많은 어려움이 따른다. 물론 운영체제가 다르다고 해도, 작동 방식은 비슷하기 때문에 한쪽 운영체제의 시스템 프로그래밍 지식을 다른 운영체제에서 전혀 써먹을 수 없는 건 아니다. 많은 운영체제들이 Unix 운영체제를 그 기반으로 하고 있기 때문에 함수명이나 사용 방법, 기능이 비슷한 경우가 많기 때문이다. 하지만 여전히 어려운 것은 사실이다.

 그러므로 시스템 프로그래밍에 발을 들여놓으려고 한다면, 운영체제를 선택하는 것이 중요한 요소가 된다. 물론 여기에서는 여러분이 리눅스 운영체제를 선택했다고 가정할 것이다.

2012년 11월 20일 화요일

리눅스 C/C++ : 13장 Input / Output


 이번 포스트에서는 모든 응용분야를 통틀어서, 공통적으로 다루어지는 분야인 입력과 출력에 대해서 다음과 같이 알아보도록 할 것이다.

  • 표준입력, 표준출력, 표준에러
  • 형식화된 입출력
  • 파일로 부터의 입력과 출력 처리
  • exit

프로그램이 하는 일의 전부는 입력을 받아서 처리한 다음 그 결과를 출력하는데 있다고 보면 된다. 어떤 프로그램을 구현함에 있어서 가장 먼저 하는 일이, 프로그램이 하는 일을 담은 명세서를 작성하는 것인데, 명세서란 것이 결국은 어떤 데이터를 입력 받아서 어떻게 출력할 것 인지를 정의 하는 일이다. 예를 들자면 시스템 프로그램과 네트워크 프로그램의 5할이 입력과 관련된 일이고 3할이 문자열 처리와 관련된 것이라고 보면 된다. 그러므로 입/출력에 대해서 반드시 알고 넘어갈 필요가 있다.

1. 입출력 장치

 아마도 컴퓨터의 3가지 구성장치에 대해서 배운기억이 있을 것이다. 이들 구성요소는 다음과 같다.

  1. 중앙연산장치
  2. 입력장치
  3. 출력장치

 기본적으로 중앙연산장치를 제외한 모든 것은 입력장치 혹은 출력장치로 구분이 된다. 대표적인 입력장치는 키보드, 마우스, 터치패드, 카메라 등이 될 것이다. 출력장치는 모니터, 프린터, 사운드 카드를 예로 들 수 있다.

 어떤 형식으로든지 간에 데이터를 받아들이는 장치는 입력장치가 되고, 받아들인 데이터를 소리,문자, 이미지의 형태로 다시 표현해주는 장치를 출력장치라고 정의 할 수 있다. 키보드로 입력된 정보는 중앙연산장치에서 처리된 다음에 적당한 과정을 거쳐서 모니터로 출력되고, 필요한 경우 프린트를 이용해서 종이로 출력되거나 할 것이다.

2. 모든 입출력은 파일로 처리한다

 유닉스 운영체제는 모든 입출력과 관련된 장치는 파일과 동일하게 보고 처리를 한다. 하드디스크의 경우 C:, D:와 같이 다루어지는 것과는 달리, 유닉스는 장치파일이라는 특수한 파일 형태로 다른다. 뿐만 아니라 프린트, 사운드 카드 까지도 파일의 형태로 다룬다. 이러한 장치파일들은 /dev 디렉토리 밑에 존재한다. 예를 들어 윈도우즈에서 말하는 C: 드라이브는 /dev/hda1 과 같은 파일이름으로, 프린터기는 /dev/lp0 이라는 파일이름으로 존재한다.

 컴퓨터에 사용되는 장치를 파일의 개념으로 놓고 본다는 것은 윈도우즈환경을 주로 사용했던 유저에게는 그리 익숙하지 않을 것이다. 그러나 조금만 생각해 보면 적어도 개발자입장에서는 매우 합리적인 개념임을 알 수 있을 것이다. 파일과 마찬가지로 읽고, 쓴다라는 개념이 그대로 적용되며, 일반 파일에 사용되는 방법과 동일한 방식으로 다양한 종류의 장치를 다룰 수 있기 때문이다. 물론 각 장치들은 읽고 쓰기 위한 전용의 프로토콜을 사용하기 때문에 프로토콜에 대한 학습이 필요하긴 하지만 사용되는 함수 등은 일반 파일을 다룰 때와 동일하다고 보면 된다. 이렇게 모든 것을 파일로 다루게 됨으로써, 장치에 대한 별도의 학습없이도 일관성있게 프로그램을 작성할 수 있게 된다.

 하지만 여기에서는 다른 장치들에 대한 입출력을 다루지는 않을 것이다. 가장 기본이 되는 파일에 대한 입출력 만을 다룰 것이다.

3. file description

 "abc.txt" 라는 파일을 읽거나 쓰기 위해서는 파일을 open하는 과정을 거친다. 그러면, 운영체제는 파일을 쉽게 다룰 수 있도록 int 형의 숫자를 넘겨준다. 이후에 파일에 쓰거나 읽는 작업은 파일이름이 아닌 이 숫자를 통해서 이루어지게 된다. 이 숫자를 file description이라고 한다. 혹은 파일지시자라 부르기도 한다. 실제 프로그래밍 과정에서 파일지시자를 어떻게 사용하는지는 나중에 예제를 통해서 알아보도록 하겠다.

4. 표준입력, 표준출력, 표준에러

 운영체제에서 주어진 일을 하는 최소 단위는 프로세스라는 것을 알고 있을 것이다. 프로세스는 프로그램의 실행된 이미지로 운영체제의 메인 메모리에 위치한다.

 이렇게 프로그램이 실행되어서, 프로세스가 생성되면 프로세스는 컴퓨터의 여러가지 기본자원을 사용할 수 있게 된다. 이 기본자원에 3개의 파일이 포함되어 있다. 바로 입력출력, 에러를 담당하는 3개의 파일들이다. 이것들을 표준입력, 표준출력, 표준에러라고 한다.

  • 표준입력 : 키보드로부터 입력을 받기 위해서 사용한다.
  • 표준출력 : 모니터로 출력하기 위해서 사용된다.
  • 표준에러 : 에러도 역시 모니터를 통해서 출력 된다는 점에서는 표준출력과 마찬가지다. 그러나 어떠한 구분도 없다면, 화면에 출력 된 값이 정상적으로 처리된 메시지인지, 에러메시지 인지 확인할 수 없을 것이다. 때문에 표준에러라는 것을 따로 두어 구분할 수 있도록 하고 있다.

 이들 표준입/출력/에러 역시 파일의 형태로 다루어지며, 운영체제는 이를 위해서 예약된 숫자를 파일지시자로 할당한다. 표준입력은 0, 표준출력은 1, 표준에러는 2로 예약되어 있다.

 간단한 예를 들어서 설명해보도록 하겠다.
 
#include <unistd.h>
#include <string.h>

int main(int argc, char **argv)
{
  char data[80];
  char *msg = "Input Msg : ";
  memset(data, 0x00, 80);

  write(1, msg, strlen(msg));   // 1

  read(0, data, 80);            // 2
  write(1, data, strlen(data)); // 3
  write(2, "Error\n", 6);       // 4
} 
 
  1. "Input Msg"메시지를 모니터에 표준출력 한다.
  2. 표준입력을 통해서 데이터를 읽어들인다. 읽어들인 데이터는 data 에 복사한다.
  3. data에 복사된 내용을 data의 길이 만큼 모니터에 표준출력 한다.
  4. Error라는 메시지를 모니터에 표준에러형태로 출력한다. 

 이 프로그램을 실행시키면 다음과 같은 결과를 확인할 수 있을 것이다.
 
# ./write
Input Msg : hello world
hello world
Error
 
  • write(2) 이 함수는 파일지시자(첫번째 인자)가 가리키는 파일에 "쓰기" 위해서 사용한다.
  • read(2) 이 함수는 파일지시자가 가리키는 파일에서 데이터를 "읽기" 위해서 사용한다.

 표준입력과 표준출력을 이해하는 데에는 별 어려움이 없을 것이다. 그럼 표준에러와 표준출력을 어떻게 구분할 수 있느냐 하는 문제가 남는다.

 표준에러는 쉘의 재지향을 이용해서 따로 분리해서 읽을 수 있다. 재지향은 >을 쓰면 된다. - 2장 리눅스와 C언어참고 - 또한 2>와 같이 표준에러만을 분리해서 재지향 시킬 수가 있다. 여기에서 2가 바로 표준출력을 가리키는 파일지시자 번호다. 이제 다음과 같이 표준에러만 별도의 파일에 재지향 시켜보도록 하자.

# ./write 2> dump.log
Input Msg : hello world
hello world
# cat dump.log
Error

 표준에러가 모니터 화면이 아닌, dump.log 파일에 저장된 것을 확인할 수 있을 것이다. 이로써, 표준에러와 표준출력이 모니터를 출력방향으로 하고 있지만, 명백히 구분되는 것임을 이해했을 것이다.

5. 파일 입출력

 그러면 유닉스 프로그래밍에 있어서 가장 중요한 파일 입출력에 대해서 알아보도록 하겠다. 기본적으로는 표준입력/출력/에러를 다루는 방식과 동일 하다. 다른 점이 있다면, 표준입력 등은 운영체제가 알아서 열어주지만, 다른 파일들은 프로그래머가 함수를 이용해서 파일을 직접 열어주어야 한다는 점이다.

 해서 파일 입출력에는 read(2)와 write(2)외에 파일을 열기 위한 open(2)과 닫기 위한 close(2)가 사용된다. 원칙적으로는 이 네 가지의 함수만 있으면 거의 대부분의 파일과 관련된 작업을 할 수 있다. 이들 함수에 대한 자세한 설명은 (http://www.joinc.co.kr/modules/moniwiki/wiki.php/Site/system_programing/Book_LSP/ch03_Env)를 참고하길 바란다.

6. 예제 1 - 전형적인 파일 읽기 프로그램

 cat 이라는 프로그램을 알 것이다. 이 프로그램은 인자로 주어진 파일을 화면에 출력하는데, cat과 비슷한 프로그램을 만들어 보도록 하자. 이 프로그램의 이름은 mycat으로 하겠다.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>

// 사용하기 쉽도록 표준입력,출력,에러를 다른 이름으로 정의 한다.
#define STDIN 0
#define STDOUT 1
#define STDERR 2

int main(int argc, char **argv)
{
    char *filename;
    int fd;
    int readn;
    char buf[80];

    // 이 프로그램이 실행되기 위해서는 파일이름을 인자로 받는다.
    // 그러므로 인자를 체크해주어야 한다.
    // 만약 인자가 부족하다면, 프로그램의 사용방법을 출력하고 종료한다.
    if (argc != 2)
    {
        printf("Usage : %s [file]\n", argv[0]);
        return 1;
    }

    // filename 이 argv[1] 을 가리킨다.
    // argv[1] 을 그대로 사용해도 되겠지만, 코드의 가독성을 위해서
    // 다른 변수이름을 사용하도록 했다.
    filename = argv[1];

    // 파일을 읽기전용 모드로 연다.
    fd = open(filename, O_RDONLY);
    // 에러 체크를 한다.
    if (fd < 0)
    {
        perror("file open err :");
        return 0;
    }

    // read 함수를 이용해서, 파일지시자로 부터 데이터를 읽어들인다.
    // read 함수는 읽어들인 데이터의 크기를 리턴한다.
    // 더이상 읽을 데이터가 없다면 0을 리턴하니, 그때 while 루프를 빠져나오면 된다.
    while((readn = read(fd, buf, 80)) > 0)
    {
        // 읽어들인 데이터의 크기 만큼을 화면에 출력한다.
        write(STDOUT, buf, readn);
    }
    close(fd);
    return 1;
}
 
 이 프로그램은 기존의 프로그램들과는 달리 꽤 완성된 모습을 보여주고 있다. 어떤 점에서 그런지 살펴보도록 하자.

  1. 프로그램의 실행인자를 검사하고 있다.
  2. 에러를 체크하고 있다.
    모든 함수는 리턴값을 이용해서 에러를 체크할 수 있도록 하고 있다. 견고한 프로그램을 만들기 위해서는 반드시 에러에 대한 처리를 해줘야 한다.
  3. main 함수도 실행결과를 값으로 리턴하고 있다.
    프로그램의 실행결과는 중요한 정보다. 프로그램이 성공했는지 실패했는지에 대한 정보를 이용해서 다른 일들을 할 수 있기 때문이다. 이는 여러개의 프로그램이 연속적으로 실행되는 배치작업에서 특히 중요하다. 유닉스 프로그램은 전통적으로 제대로 실행이 되었다면 0을 그렇지 않다면 0이 아닌 다른 수를 리턴한다.
  4. 가독성
    표준입력,출력,에러 등을 위해서 0,1,2등을 사용하는건 직관적이지 못하다. 직관적이지 못하다는 것은 프로그래머가 실수를 할 수 있음을 의미한다. define 문을 이용하면 가독성 좋은 직관적인 프로그램을 만들 수 있다.

 이제 이 프로그램을 컴파일 한다음에 실행시켜 보도록 하자. 프로그램의 이름은 mycat 이라고 하겠다. 내용이 길어서 실행결과는 생략했다.

# ./mycat mycat.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>

#define STDIN 0
#define STDOUT 1
#define STDERR 2
....
....

 재지향을 이용하면 다음과 같은 응용도 가능 할 것이다.
 
# ./mycat mycat.c > mycat.bak

결과적으로 mycat.c 를 mycat.bak 라는 파일로 복사를 한 것과 같다.

7. 예제 2 - 표준입력으로 부터 읽어들이도록 해보자

 위에서 다루었던 예제 프로그램을 약간 수정해서, 표준입력으로부터 입력된 데이터를 출력하는 프로그램을 만들어 보도록 하자.
 
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>

#define STDIN 0
#define STDOUT 1
#define STDERR 2

int main(int argc, char **argv)
{
    char *filename;
    int readn;
    char buf[80];

    while((readn = read(STDIN, buf, 80)) > 0)
    {
        // 읽어들인 데이터의 크기 만큼을 화면에 출력한다.
        write(STDOUT, buf, readn);
    }
    return 1;
}
 
 프로그램이 훨씬 간단해졌다. 표준입력은 프로세스가 생성될 때 자동으로 열린다. 덕분에 파일을 여는 등의 코드가 필요 없기 때문이다. 위 프로그램을 컴파일 한 다음에 실행시켜보도록 하자.

# ./mycat2
hello world
hello world
ok
ok

 키보드 입력을 받아들이고 엔터키를 누르면, 입력된 내용이 출력되는 것을 확인할 수 있을 것이다. Ctrl+D를 누르면 프로그램을 빠져나올 수 있다.

 혹은 파이프를 이용할 수도 있다. 파이프를 통해서 넘어오는 정보는 표준입력으로 읽을 수 있기 때문이다.

# cat mycat.2 | ./mycat2
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
...
...
 
 이렇게 파이프와 표준입력을 잘 사용하면, 복잡하게 파일을 열거나 하는 일 없이 다른 프로그램으로부터 생성된 문자열 등의 데이터를 표준입력으로 받아서 간단히 처리할 수 있다.

8. 파일 복사하기

 위의 파일 입출력작업은 키보드나 파일로부터 입력 받아서, 모니터로 표준출력 하는 것만 수행해보았다. 그렇다면 이제 표준출력이 아닌 파일로 출력 하는 방법에 대해서 알아보도록 하자.

 유닉스에서는 모니터나 파일이나 모두 동일하게 파일로 입력하기 때문에, 표준출력과 마찬가지로 write()함수를 이용해서, 파일로 쓸 수 있다. 다른 점이라면, 이미 열려있는 표준출력과는 다르게, 파일출력의 경우 쓰고자 하는 파일을 직접 열어주어야 한다는 것이다.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

// 사용하기 쉽도록 표준입력,출력,에러를 다른 이름으로 정의 한다.
#define STDIN 0
#define STDOUT 1
#define STDERR 2

int main(int argc, char **argv)
{
    char *orgfile, *dstfile;
    int rfd, wfd;
    int readn;
    char buf[80];

    // 인자로 원본파일과 복사파일 이름을 받는다.
    // src file이 dst file로 복사된다.
    if (argc != 3)
    {
        printf("Usage : %s [src file] [dst file]\n", argv[0]);
        return 1;
    }

    // orgfile 이 argv[1] 을, dstfile 이 argv[2] 를 가리킨다.
    // argv[1], argv[2] 을 그대로 사용해도 되겠지만, 코드의 가독성을 위해서
    // 다른 변수이름을 사용하도록 했다.
    orgfile = argv[1];
    dstfile = argv[2];

    // 원본 파일을 읽기전용 모드로 연다.
    rfd = open(orgfile, O_RDONLY);
    // 에러 체크를 한다.
    if (rfd < 0)
    {
        perror("org file open err :");
        return 0;
    }

    // 복사파일은 쓰기전용으로 연다.
    // 복사파일의 권한은 00700 즉 사용자에게 읽기,쓰기,실행 권한을 준 상태로 한다. 
    wfd = open(dstfile, O_WRONLY|O_CREAT,S_IRWXU);
    // 에러 체크를 한다.
    if (wfd < 0)
    {
        perror("dst file open err :");
        return 0;
    }
  
    // 원본파일로 부터 데이터를 읽은다음
    // 읽어들인 데이터의 크기만큼 복사파일에 쓴다.
    while((readn = read(rfd, buf, 80)) > 0)
    {
        write(wfd, buf, readn);
    }
    close(rfd);
    close(wfd);
    return 1;
}
 
 위 코드를 컴파일 한다음 테스트해보도록 하자.
 
# ./mycopy mycopy.c mycopy.bak

2012년 11월 11일 일요일

리눅스 Tips : 리눅스를 공부하기 전에...

출처 : http://wiki.kldp.org/wiki.php/LinuxUserBookmarkForNovice

리눅스를 처음 사용하는 분들을 위한 북마크

1. 리눅스(Linux)란?

 리눅스는 운영체제입니다. 마이크로소프트사의 윈도우와 같은 역할을 하는 프로그램입니다. 또한 리눅스는 커널이라고 불리우는 운영체제 핵심 부분의 이름이기도 하며 보통 운영체제의 이름이기도 합니다. 따라서 윈도우에서 하는 모든 일을 리눅스에서도 할 수 있습니다. 다만 하는 방법이 다를 뿐입니다. 그래서 어렵게 느껴질 뿐입니다.

 리눅스는 특정한 회사에서 제작되는 프로그램이 아닙니다. 전세계 개인 또는 회사의 프로그래머들과 사용자들의 오픈소스 프로젝트입니다. 여기에 참여하는 대표적인 회사들로는 IBM, HP, DELL, ORACLE 등이 있습니다. 유명한 개인으로는 리누스 토발즈, 에릭 레이몬드 등이 있습니다. 더 궁금하시면



 배포판은 리눅스라는 운영 체제를 사용자가 쉽게 설치하여 사용할 수 있도록 구성하여 배포하는 것을 말합니다. 무료로 배포하는 것도 있고, MS사의 윈도우처럼 판매되는 것도 있습니다. 판매되는 제품들은 CD/DVD로 배포되며, 상용 프로그램이 들어 있기도 하고, 기술 지원을 받을 수 있습니다.

 참고할 만한 사이트입니다.

2. 배포판 선택

 아래에 있는 배포판들은 서버 또는 데스크톱용으로 주로 사용되는 배포판입니다. 이 목록에 있는 것이 배포판의 전부는 아니지만, 가급적이면 이 중 하나를 선택하여 최신 버전으로 사용하십시오. 대개 한국어 환경을 무리 없이 지원하며, 사용자가 많아서 돌발 상황이나 모르는 것이 생기면 질문하기도 쉽습니다. 설치하여 사용할 수 있는 프로그램 패키지도 많이 지원합니다. 하드웨어가 오래 되었거나 사양이 낮다면 대안으로 http://www.damnsmalllinux.org 이나 http://www.xubuntu.org 를 고려해볼 만합니다.

참고로 레드햇 9.0은 제작사에서 지원이 중단된 아주 오래된 배포판입니다. 윈도우 XP를 사용하지 않고 윈도우 95를 사용하면서 안 좋다고 투덜대는 격입니다.

 더 많은 배포판 정보는 http://en.wikipedia.org/wiki/List_of_Linux_distributionshttp://distrowatch.com 를 참고하세요. 2009년 7월 시점에서 최신 배포판은 페도라 11, 우분투 9.04, 맨드리바 2009.1, 데비안 5.0, 오픈수세 11.1 등입니다. 각각 배포판별 설치 및 설정 방법을 알고 싶으면 http://howtoforge.com/howtos/desktop 을 참고하십시오. 갓 리눅스를 사용하신 분들에게 강력히 추천해 드립니다.


3. 설치하기 전에 알아둘 것

 윈도우가 설치되어 있는 경우 리눅스를 사용할 수 있는 방법은 아래와 같습니다.
  • 윈도우를 삭제하고 리눅스를 설치한다.
  • 윈도우 파티션 크기를 조절(resize)하여 리눅스를 설치할 공간을 만들어 설치한다.
  • 윈도우에서 VMWare와 같은 가상 머신 프로그램을 이용하여 설치한다.
  • 윈도우에서 coLinux나 andLinux 같은 리눅스 배포판을 설치하여 사용한다. 또는 우분투의 Wubi 를 이용한다.
  • 라이브 CD나 라이브 USB로 배포되는 리눅스 배포판을 사용한다.


3.1 파티션과 드라이브

 IDE 장치만 있는 컴퓨터에서 파티션과 드라이브는 다음과 같이 인식됩니다.

리눅스하드웨어윈도우
/dev/hdaIDE 프라이머리 마스터보통 윈도우 하드 디스크 'C'
/dev/hdbIDE 프라이머리 슬레이브보통 윈도우 하드 디스크 'D' 또는 ODD
/dev/hdcIDE 세컨더리 마스터보통 윈도우 하드 디스크 'E'
/dev/hddIDE 세컨더리 슬레이브보통 윈도우 하드 디스크 'F'

 SCSI나 USB 이동식 디스크, 최근 많이 사용되는 SATA 하드 디스크 등은 sda, sdb, sdc 등의 이름이 붙습니다. 배포판에 따라서 IDE 장치라도 SATA 장치처럼 하드디스크에 sda, sdb, sdc 등의 이름이 붙을 수도 있습니다.

 윈도우에서는 하드 디스크 파티션을 나누면 C, D, E와 같이 알파벳 한 글자 드라이브 이름이 붙지만 리눅스에서는 /dev/sda1, /dev/sda2, /dev/sda3과 같이 /dev 아래에 (장치 이름)(파티션 순서)처럼 이름이 붙습니다. 윈도우에서는 보통 자동으로 되던 부분이라 리눅스를 처음사용하시는 분들이 많이 어려워 하시지만 사실 간단합니다.


3.2 하드웨어 설치

 최근 리눅스 배포판은 설치할 때 대부분 하드웨어를 자동으로 인식하여 설정을 하므로 별 문제가 없습니다. 다만 일부 하드웨어의 경우 수동으로 설정해야 할 때도 있습니다. 대개 그래픽 카드나 무선랜 카드입니다. 자신의 하드웨어를 별도로 설정해야 하는가 알아보려면, 아래의 홈페이지 등을 참고하여 하드웨어 호환 여부를 확인합니다.

 다음 목록은 리눅스 배포판에서 제공하는 하드웨어 호환성 목록입니다.
 다음 목록은 특정 하드웨어의 리눅스 호환성을 다루고 있습니다.
 대개 이 정도 사이트만 탐색하면 리눅스 지원 여부를 찾을 수 있지만, 그래도 모르겠으면 다음과 같이 해 보십시오.

  • 검색 엔진에서 "하드웨어 모델명(또는 칩셋명) linux"로 검색해 보십시오. 사용기가 나올 수도 있습니다.
  • 일부 하드웨어 제조사는 홈페이지에서 리눅스 드라이버를 제공합니다. HP 프린터, 엔비디아 및 ATI 그래픽 카드 등이 있습니다.


4. 한글 설정

 윈도우에서 사용하는 한글 글꼴은 굴림, 궁서, 돋움, 바탕, 맑은 고딕이 있습니다. 이들 글꼴은 윈도우와 함께 배포되는 상용 글꼴로, 대부분 리눅스 배포판에 포함될 수 없습니다. 따라서 이들 대신 은 글꼴이나 은진체 등의 공개 한글 글꼴이 사용됩니다. 윈도우에서 사용하던 글꼴과 모양이 달라서 처음에 적응이 힘들 수도 있습니다.

 최근에는 다양한 공개 한글 글꼴이 나와서, 이들을 설치하여 사용할 수도 있습니다.
 좀 더 참고할만한 자료입니다.
 리눅스에서 사용하는 글꼴은 fontconfig 라는 프로그램이 관리합니다. 따라서 글꼴 미세 조정에 관심이 많다면 이 프로그램의 설정 방법을 연구하시면 됩니다. 대부분의 경우 배포판의 글꼴 설정을 사용해도 무방합니다.

5. 리눅스 프로그램

 리눅스에서 프로그램은 어떻게 설치할까요? 윈도우에서 새 프로그램을 설치하려면 프로그램 홈페이지에서 exe나 msi 파일을 다운로드 받아서 설치하였습니다. 리눅스에서는 rpm, deb과 같은 패키지 파일을 프로그램 홈페이지가 아닌 배포판 중앙 저장소에서 다운로드 받아서 설치합니다. 배포판 중앙 저장소에 모든 프로그램 정보가 다 들어 있을 때의 장점은 필요한 프로그램이 있을 때 일일이 홈페이지에서 다운로드받지 않아도 리눅스 배포판에 포함된 프로그램 추가/제거 도구를 통하여 모든 프로그램을 손쉽게 설치할 수 있습니다.

 초보자들은 이 방식을 잘 이해하지 못할 수 있습니다. 그러나 한 번에 많은 프로그램을 설치하려고 한다면 일일이 설치하는 것보다 배포판 중앙 저장소에서 한번에 설치하는 것이 편하게 느껴질 겁니다.

 많은 대형 배포판은 중앙 저장소가 잘 갖추어져 있어서 필요한 프로그램은 그냥 가져다 설치하면 됩니다. 하지만 중앙 저장소에 원하는 프로그램이 없다면 어떻게 해야 할까요? 일부 프로그램 제작자들은 홈페이지에서 패키지 파일을 배포하기도 합니다. 그런 경우에는 패키지 파일을 받아다 설치하면 됩니다. 패키지 파일도 없다면, 그 때 프로그램 원본 코드에서 실행 파일을 만들어서 설치하면 됩니다. 간혹 가다가 리눅스 초보자들이 '모든 프로그램을 소스에서 컴파일해서 설치해야 한다'고 생각하실 수도 있는데, 보통 소스 컴파일은 최후의 방법으로 여겨집니다.

 다음 웹 사이트는 리눅스에서 사용하는 프로그램을 다루고 있습니다.

6. 리눅스 데스크톱 구조

오픈소스 프로그램
|
그놈, KDE 및 Xfce, Fluxbox 등
|
X.org + Qt/GTK
|
리눅스 커널 + GNU 프로그램

 리눅스 데스크톱을 구성하는 프로그램을 알고 싶으면 http://en.wikipedia.org/wiki/Freedesktop.org 를 참고하십시오.

 또한 윈도우의 테마처럼 리눅스 데스크톱도 테마, 배경화면, 아이콘 등을 자신의 취향대로 변경할 수 있습니다.

7. 초보자 필독

 그리고 http://wiki.kldp.org/wiki.php/초보자코너 의 내용을 읽어보세요. 책이 필요없을 정도입니다.


8. 보안 문제

 윈도우가 자동 업데이트를 통해서 매주 업데이트를 하듯이 리눅스도 자동 업데이트를 제공합니다. 리눅스 배포판마다 업데이트 방법이 다르기 때문에 배포판별 업데이트에 관한 문서를 참고하여 늘 최신 버전의 리눅스를 사용하십시오. 자동 업데이트를 사용하면 프로그램을 새 버전으로 업그레이드하거나, 보안 문제를 해결하거나, 성능이 더 좋아집니다.

 그런데 왜 리눅스에는 별도의 백신이 없을까요? 리눅스에서는 윈도우에서 실행되던 바이러스, 악성코드가 실행되지 못합니다. 그리고 리눅스 프로그램의 보안 문제가 발생하면 프로그램 제작자들이 재빠르게 문제를 해결하여 새 버전을 배포합니다. 예로 과거 모질라 파이어폭스에서 보안 결함이 발견되었을 때 사흘만에 패치된 새 버전이 나왔습니다. 따라서 리눅스 환경에서는 별도의 백신 없이도 시스템을 안전하게 유지할 수 있습니다. 리눅스용 백신들은 리눅스 시스템 자체를 검사하기보다는 리눅스 시스템을 통해서 전달되는 파일에 붙어 있는 바이러스를 진단하는 데 사용됩니다.


9. 누가 리눅스를 만드는지 알고 싶다면?

Linux Kernel Development: How Fast it is Going, Who is Doing It, What They are Doing, and Who is Sponsoring It http://www.linuxfoundation.org/docs/lf_linux_kernel_development_2010.pdf


10. 추천 사이트

 만약 리더를 두고선 리눅스 설치를 위한 세미나를 진행하고자 한다면
http://tldp.org/HOWTO/Installfest-HOWTO/http://kldp.org/node/81966 를 참고하세요.

2012년 11월 10일 토요일

리눅스 C/C++ : 12장 라이브러리


Hello World 다시 보기


 hello world 프로그램을 다시 한번 보기로 하자.

          #include <stdio.h>

          

          int main(int argc, char **argv)

          {

              printf("Hello World!!!\n");

              return 1;

          }          

 위 프로그램을 자세히 뜯어보면, #include문이 보일 것이다. 이것이 어디에 쓰는 물건인지 자세히 알아보도록 할 것이다. 우리는 7장 함수편에서 함수에 대해서 다루었다. 이를 통해서 우리는 함수를 사용하기 위해서는 함수선언함수원형이 필요하다는 것을 알게 되었다. 함수를 사용하는 이유는 중복되는 코드를 따로 묶어 둠으로써, 코드관리를 쉽게 하기 위함이라는 것도 역시 알고 있다. 하지만 편하겠지라고만 알고 있을 뿐, 실제 어떻게 편하게 사용되는지는 경험을 해보진 못했다.

 자.. 우리는 함수라는 것을 알고 있다. 그렇다면 어떻게 해야 함수를 더 편하게 사용할 수 있을까. 답은 함수를 위한 코드를 따로 분리시킨다 이다. 위의 Hello World 프로그램은 이러한 전형적인 모습을 보여주고 있다. printf 함수는 거의 모든 프로그램에서 필수적으로 사용되는 함수다. 이런 코드를 사용자가 필요할때 마다 일일이 사용하는건 여간 귀찮은일이 아닐 것이다. 그렇다면 printf 함수를 별도의 코드로 만들어서, 모듈형태로 만들어 두면 될것이다. 그래서 printf 함수가 필요할 때, 가져다 쓰기만 하면 된다.

 그런데, 컴파일러는 printf 함수가 어떤 모습을 가지는지 알 수가 없다. 그러니까 리턴값이 무엇이고, 인자로 무엇이 사용되는 지를 알 수가 없다는 얘기가 된다. 그러므로 컴파일러에게 printf 함수의 정보를 알려줄 수 있어야 한다. 그게 #include 문이 하는 일이다. stdio.h는 표준입출력과 관련된 함수의 정보가 들어있는 파일로 헤더파일이라고 부른다. 이 헤더파일에는 printf 함수가 선언되어 있다. stdio.h 헤더파일은 /usr/include 디렉토리 밑에 존재한다.

 이제 우리는 stdio.h 만을 include 시킴으로써, 어느 코드에서든지 간단하게 printf 함수를 사용할 수 있게 된다.

컴파일 과정


 이제 위의 hello world 프로그램이 어떻게 컴파일이 되는지 알아보도록 하자. 프로그램의 이름은 hello.c 이다.

  1. 컴파일러는 hello.c 프로그램을 읽어 들인다.
  2. hello.c 코드를 해석해서, 기계어 형태의 object 파일로 만든다. object 파일은 컴퓨터가 해석할 수 있는 단위 모듈이다.
  3. 여기에 printf 함수가 정의되어 있는 이미 만들어져 있는 object 파일과 hello object 파일을 서로 링크(연결)한다.
  4. 완전한 실행 파일이 만들어 진다.

덧셈 함수를 가진 프로그램


 그럼 덧셈 함수를 가진 계산기 프로그램을 만들어서, 모듈별로 작성하고 이것들을 object 파일로 만들어서 link 시켜서 실행파일을 만드는 방법에 대해서 알아보도록 하겠다.

 모듈별로 작성하지 않고, 하나의 파일로 이루어진 프로그램은 아래와 같이 작성할 수 있을 것이다.

          #include <stdio.h>

          int sum(int a, int b);

          int main(int argc, char **argv)

          {

              int value;

              value = sum(120, 199);

              printf("%d\n", value);

          }

          int sum(int a, int b)

          {

              return a+b;

          }
          

 아주 간단하며 문제없이 작성될 것이다. 또한 덧셈을 위한 sum 이라는 함수를 만들었으니, 이후 덧셈계산이 필요할 때 마다, 그냥 sum 함수를 그대로 쓰기만 하면 될것이다. 그런데 이 프로그램은 통짜로 작성되어 있기 때문에, 다른 프로그램에서 sum 함수를 사용할려고 하면 copy & paste 할 수 밖에 없다. 이것은 비효율 적이다. 이제 sum 함수를 모듈형식으로 완전히 분리 시켜 보도록 하자. 이를 위해서는 다음과 같이 3개의 파일이 만들어져야 한다.

  • sum 함수의 선언이 들어 있는 include 파일
  • sum 함수를 사용할 main 함수가 있는 C 파일
  • sum 함수가 정의되어 있는 C 파일

 include 파일은 아주 간단하게 만들 수 있다. include 파일의 이름은 sum.h 로 하겠다.

          int sum(int a, int b);
          

 이제 sum 함수가 정의되어 있는 C 코드를 만들어보자. 역시 간단하다. 파일이름은 sum.c로 하겠다.

          int sum(int a, int b)

          {

              return a + b;

          }
          

 마지막으로 main 함수를 만들어 보자. 파일이름은 calc.c 로 하겠다.

          #include "sum.h"

          #include <stdio.h>

          

          int main()

          {

              int value;

              value = sum(130, 199);

              printf("%d\n");

          }
          

헤더파일의 경로


 우리는 #include 키워드를 이용해서, 포함시킬 헤더파일을 지정할 수 있다. 이때 헤더파일의 완전한 경로를 포함시켜 줘야 한다. 만약 따움표를 사용했다면, 이는 현재 디렉토리에서 헤더파일을 찾겠다는 것을 의미한다. 그렇지 않고 <> 를 사용했다면, 표준 Include 디렉토리와 컴파일러 옵션을 통해서 지정된 디렉토리에서 찾게된다. 유닉스 시스템의 경우 /usr/include 가 표준 Include 파일이 된다.

 헤더파일을 찾을 경로의 지정은 -I옵션을 이용하면 된다. 예를 들어 /home/yundream/include 에서 찾도록 하길 원한다면

          # gcc -I/home/yundream/include -o sum sum.c
          

 와 같이 하면 된다.

 만약 /home/yundream/include 를 헤더파일 찾기 경로로 지정하고 싶다면, 다음과 같이 하면 된다.

          #include "/home/yundream/include"
          

모듈별 분할 컴파일


 자 이제 sum.h, sum,c, calc,c 3개의 파일이 만들어졌다. 이 3개의 파일을 컴파일해서 실행가능한 프로그램을 만들어보자.

 위에서 언급되었듯이 가장 먼저 해야할일은 sum.c 와 calc.c 를 기계어가 해석가능한 object 코드로 만드는 일이다. 오브젝트 코드는 gcc에 -c 옵션을 이용해서 만들어낼 수 있다.

          # gcc -c sum.c calc.c          

 이제 sum.o 와 calc.o 라는 파일이 만들어진걸 확인할 수 있을 것이다. 확장자 .o는 이 파일이 오브젝트 파일이라는 것을 알려준다. 이제 두개의 object 파일을 링크시켜서 실행파일을 만들면 된다. -o 옵션을 이용하면, 만들어진 오브젝트 파일들을 합쳐줄 수 있다.

          # gcc -o calc sum.o calc.o          

 이제 실행파일인 calc가 만들어졌다.

 이렇게 만들어진 object 파일은 기계어로 만들어져 있기 때문에, 이후에 사용할때는 sum.c를 다시 object 파일로 컴파일할 필요가 없다. 그냥 sum.o 프로그램에 링크시켜주기만 하면 된다. 다음과 같은 프로그램을 만들어보자. 프로그램의 이름은 mycal.c로 하자.

          #include "sum.h"

          #include <stdio.h>

          

          int main(int argc, char **argv)

          {

            int value;

            int a;

            int b;

            if (argc != 3)

            {

              printf("Usage : %s num1 num2\n", argv[0]);

              return 1;

            }

          

            a = atoi(argv[1]);

            b = atoi(argv[2]);

            value = sum(a, b);

            printf("%d + %d = %d\n", a, b, value);

            return 0;

          }          

 이 프로그램은 첫번째 프로그램보다 더 진보된 프로그램으로, 프로그램의 명령행 인자로 받아들인 숫자를 더할 수 있도록 되어 있다. atoi(3)는 문자열을 int형 숫자로 변환해주는 함수다. sum 함수는 이미 컴파일 되어서 object 파일로 만들어져 있으므로, 별도로 컴파일할 필요가 없다. 다음과 같은방법으로 실행파일을 만들 수 있다.

          # gcc -c mycal.c

          # gcc -o mycal sum.o mycal.o
          
          - sum.o라는 오브젝트 파일을 실행파일에 링크 시켜 놓은 것이다. 
          

4칙연산 프로그램


 위의 프로그램은 덧셈만을 지원하고 있다. 여기에 덧붙여 뺄셈, 나눗셈, 곱샘까지 지원하는 프로그램을 만들어 보도록 하자. 각각의 연산은 모두 함수로 작성되며, 각각의 함수가 헤더파일과 함수가 정의된 C 코드 파일을 가지게 될 것이다. 그렇다면, 이 프로그램은 4칙연산을 위한 4개의 함수와 4개의 헤더파일 1개의 main 함수를 포함하는 C 파일로 구성될 것이다.

  • 헤더파일 : sum.h, sub.h, mul.h, div.h
  • 함수정의된 C 파일 : sum.c, sub.c, mul.c, div.c
  • main 함수파일 : simplecal.c

 먼제 헤더파일을 작성해보자.

sum.h
          int sum(int a, int b);
          

sub.h
          int sub(int a, int b);
          

mul.h
          int mul(int a, int b);
          

div.h
          int div(int a, int b);
          

 이제 함수의 정의를 담고 있는 4개의 C 소스코드 파일을 만들어야 한다.

sum.c
          int sum(int a, int b)

          {

              return a + b;

          }
          

sub.c
          int sub(int a, int b)

          {

              return a - b;

          }
          

mul.c
          int mul(int a, int b)

          {

              return a * b;

          }
          

div.c
          int div(int a, int b)

          {

              return a / b;

          }
          

 이제 main 함수를 가진 코드를 만들면 된다.

          #include "sum.h"

          #include "sub.h"

          #include "mul.h"

          #include "div.h"

          

          int main(int argc, char **argv)

          {

             int a = 1200, b=25;

          

            printf("sum : %d\n", sum(a, b));

            printf("sub : %d\n", sub(a, b));

            printf("mul : %d\n", mul(a, b));

            printf("div : %d\n", div(a, b));

          }
          

 코드를 만들었다면, gcc를 이용해서 object를 만들고 이것들을 링크시켜서 실행파일로 만들면 된다.

          # gcc -c sum.c sub.c mul.c div.c simplecalc.c

          # gcc -o simplecalc sum.o sub.o mul.o div.o simplecalc.o
          

라이브러리


 이렇게 단위 함수를 별개의 소스코드와 헤더파일로 나누어서 관리하게 되면, object 혹은 단위 소스코드 파일을 재활용할 수 있다는 장점을 가진다. 그러나 여전히 불편한 점이 있다. 함수가 많아지면, 자칫 수십개의 오브젝트 파일이 생성될 수 있을건데, 이들을 관리하는건 매우 귀찮은 일이기 때문이다.

 그렇다면 4개의 object 파일을 하나로 묶을 수만 있다면, 함수들을 더 편리하게 관리할 수 있을 것이다. 이렇게 오브젝트들을 하나의 파일로 다시 묶은 것을 라이브러리(library)라고 한다.

 라이브러리는 다시 정적 라이브러리공유 라이브러리로 나뉜다.

- 정적 라이브러리는 실행파일에 완전히 포함되어버리는 형식의 라이브러리를 말한다.
- 공유 라이브러리는 실행파일에 포함되지 않고, 실행될때 해당 라이브러리를 불러오는 형식의 라이브러리를 말한다.


정적라이브러리

 static library라고 부르기도 한다. 이 라이브러리는 단순한 오브젝트의 모음일 뿐이다. 정적라이브러리는 ar이라는 프로그램을 통해서 만들 수 있다. 그럼 ar을 이용해서 위의 사칙연산을 위한 4개의 오브젝트를 모아서 libmycalc.a라는 이름의 정적라이브러리를 생성해보도록 하자. rc 옵션을 이용하면, 정적라이브러리를 만들 수 있다.

 r은 정적라이브러리를 만들겠다는 옵션이고, c는 새로 생성을 하겠다는 옵션이다.

          # ar rc libmycalc.a sum.o sub.o mul.o div.o

 libmycalc.a 라는 파일이 생성된걸 확인할 수 있을 것이다. t 옵션을 이용하면, 해당 라이브러리가 어떤 오브젝트를 포함하고 있는지도 확인할 수 있다. t 옵션을 사용하면 된다. 참고로 정적 라이브러리의 이름은 libNAME.a의 형식을 따라야 한다.

          # ar t libmycalc.a

          div.o

          mul.o

          sum.o

          sub.o

 그럼 정적라이브러리를 이용해서 실행파일을 만들어 보도록 하자. 이전에는 4개의 오브젝트 파일을 모두 링크시켜줘야 했지만, 이제는 libmycalc.a 만 링크시켜주면 된다.

 라이브러리의 링크방식은 오브젝트를 링크하는 것과는 약간 차이가 있다. library의 위치를 명확히 명시해 주어야 한다. 를 명시해-L 옵션을 이용해서 라이브러리가 있는 디렉토리의 위치주고, -l옵션을 이용해서, 라이브러리 파일의 이름을 정해줘야 한다. 다음은 simplecalc.c 를 정적라이브러리를 이용해서 컴파일하는 방법을 보여준다.

          # gcc -o simplecalc simplecalc.c -L ./ -l mycalc
          
- -L./은 현재 디렉토리를 라이브러리 찾기 디렉토리로 하겠다는 의미
- -l 옵션뒤에 붙이는 라이브러리 파일의 이름에 주목할 필요가 있다. 라이브러리 이름은 lib.a를 제외한 이름을 사용한다.


공유 라이브러리

 공유 라이브러리는 함께 사용하는 라이브러리라는 의미다. 즉 정적 라이브러리 처럼 실행파일에 붙는 것이 아니고, 시스템의 특정디렉토리에 위치하면서, 다른 모든 프로그램들이 공유해서 사용할 수 있게끔 제작된 라이브러리다. 그러므로 공유 라이브러리를 사용하도록 제작된 프로그램은 실행시에 사용할 라이브러리를 호출하는 과정을 거치게 된다.

 공유 라이브러리역시 오브젝트를 이용해서 만든다는 점에서는 정적라이브러리와 비슷하지만, 호출시에 링크하기 위한 부가적인 정보를 필요로 하므로, 정적라이브러리와는 전혀 다른 형태로 만들어 진다. 정적라이브러리와 이름이 헛갈릴 수 있으니, 라이브러리 이름은 mycalcso 로 하겠다.

          # gcc -fPIC -c sum.c sub.c mul.c div.c

          # gcc -shared -W1,-soname,libmycalcso.so.1 -o libmycalcso.so.1.0.1 sum.o sub.o mul.o div.o
          
  • 오브젝트 파일을 만들때 부터 차이가 있는데, -fPIC 옵션을 줘서 컴파일 한다.
  • 그다음 -shared 옵션을 이용해서 공유라이브러리 파일을 생성한다.

 위의 과정을 끝내고 나면, libmycalcso.so.1.0.1 이라는 파일이 생성이 된다. 이 라이브러리는 프로그램을 컴파일할때와 실행시킬때 호출이 되는데, 호출될때는 libmycalcso.so 를 찾는다. 그러므로 ln 명령을 이용해서 libmycalcso.so 링크파일을 생성하도록 하자.

          # ln -s libmycalcso.so.1.0.1 libmycalcso.so          

 이렇게 링크를 만들게 되면, 여러가지 버전의 라이브러리 파일을 이용할 수 있으므로 관리상 잇점을 가질 수 있다. 새로운 버전의 라이브러리가 나올 경우, 오래된 버전의 라이브러리를 쓰는 프로그램은 실행시 문제가 발생할 수 있는데, 이런 문제를 해결할 수 있기 때문이다.

 이제 링크하는 과정이 남았다. 링크과정은 정적 라이브러리를 사용할때와 동일하다.

          # gcc -o simplecalcso simplecalc.c -L ./ -l mycalcso        

 이제 프로그램을 실행시켜 보도록 하자. 아마 다음과 같은 에러메시지를 만나게 될 것이다.

          # ./simplecalcso

          ./simplecalcso: error while loading shared libraries: libmycalc.so: 

          cannot open shared object file: No such file or directory          

 이러한 에러가 발생하는 원인에 대해서 알아보도록 하자. 정적라이브러리는 실행파일에 라이브러리가 붙여지므로, 일단 실행파일이 만들어지면, 독자적으로 실행이 가능하다. 그러나 공유라이브러리는 라이브러리가 붙여지는 방식이 아니고, 라이브러리를 호출해서 해당 함수코드를 실행하는 방식이다. 그러므로 공유라이브러리 형식으로 작성된 프로그램의 경우 호출할 라이브러리의 위치를 알고 있어야만 한다.

 위의 simplecalcso 프로그램을 실행시키면, 이 프로그램은 libmycal.so 파일을 찾을 것이다. 이때 파일을 찾는 디렉토리는 /etc/ld.so.conf에 정의 되어 있다.

          # cat /etc/ld.so.conf

          /usr/lib

          /usr/local/lib          

 만약 위에서 처럼되어 있다면, 프로그램은 /usr/lib 와 /usr/local/lib 밑에서 libmycal.so 를 찾게 될 것이다. 그런데 libmycal.so 가 없으니, 위에서와 같은 에러가 발생하는 것이다.

 가장 간단한 방법은 라이브러리 파일을 ld.so.conf에 등록된 디렉토리중 하나로 복사하는 방법이 될 것이다. 혹은 환경변수를 이용해서, 새로운 라이브러리 찾기 경로를 추가할 수도 있다. 이때 사용되는 환경변수는 LD_LIBRARY_PATH 다.

          # export LD_LIBRARY_PATH=./:/home/myhome/lib          

 이제 프로그램을 실행시키면 LD_LIBRARY_PATH 에 등록된 디렉토리에서 먼저 검색하게 되고, 프로그램은 무사히 실행 될 것이다.

공유라이브러리와 정적라이브러리의 장단점

 이들 2가지 라이브러리의 장단점에 대해서 알아보도록 하자. 장단점을 알게되면 어떤 상황에서 이들 라이브러리를 선택할 수 있을지 알 수 있을 것이다.

 정적라이브러리의 장점은 간단한 배포방식에 있다. 라이브러리의 코드가 실행코드에 직접 붙어버리는 형식이기 때문에, 일단 실행파일이 만들어지면 간단하게 복사하는 정도로 다른 컴퓨터 시스템에서 실행시킬 수 있기 때문이다. 반면 동적라이브러리는 프로그램이 실행될때 호출하는 방식이므로, 라이브러리까지 함께 배포해야 한다. 라이브러리의 호출 경로등의 환경변수까지 덤으로 신경써줘야 하는 귀찮음이 따른다.

 일반적으로 정적라이브러리는 동적라이브러리에 비해서 실행속도가 빠르다. 동적라이브러리 방식의 프로그램은 라이브러리를 호출하는 부가적인 과정이 필요하기 때문이다.

 정적라이브러리는 실행파일 크기가 커진다는 단점이 있다. 해봐야 얼마나 되겠느냐 싶겠지만, 해당 라이브러리를 사용하는 프로그램이 많으면 많을 수록 X 프로그램수만큼 디스크 용량을 차지하게 된다. 반면 공유라이브러리를 사용할 경우, 라이브러리를 사용하는 프로그램이 10개건 100개건 간에, 하나의 라이브러리 복사본만 있으면 되기 때문에, 그만큼 시스템자원을 아끼게 된다.

 마지막으로 버전 관리와 관련된 장단점이 있다. 소프트웨어 개발 세계의 불문율이라면 버그 없는 프로그램은 없다이다. 어떠한 프로그램이라도 크고작은 버그가 있을 수 있으며, 라이브러리도 예외가 아니다.

 여기 산술계산을 위한 라이브러리가 있다. 그리고 정적 라이브러리 형태로 프로그램에 링크되었어서 사용되고 있다고 가정해보자. 그런데 산술계산 라이브러리에 심각한 버그가 발견되었다. 이 경우 산술계산 라이브러리를 포함한 A 프로그램을 완전히 새로 컴파일 해서 배포해야만한다. 문제는 이 라이브러리가 A 뿐만 아니라 B, C, D 등의 프로그램에 사용될 수 있다는 점이다. 결국 B, C, D 프로그램 모두를 새로 컴파일 해서 배포해야 하게 된다. 더 큰 문제는 어떤 프로그램이 버그가 있는 산술계산 라이브러리를 포함하고 있는지 알아내기가 힘들다는 점이다.

 공유라이브러리 형태로 작성하게 될경우에는 라이브러리만 새로 컴파일 한다음 바꿔주면된다. 그러면 해당 라이브러리를 사용하는 프로그램이 몇개이던간에 깔끔하게 문제가 해결된다.

 실제 이런 문제가 발생한 적이 있었다. zlib 라이브러리는 압축을 위한 라이브러리로 브라우저, 웹서버, 압축관리 프로그램등에 널리 사용된다. 많은 프로그램들이 이 zlib를 정적라이브러리 형태로 포함해서 배포가 되었는데, 심각한 보안문제가 발견되었다. 결국 zlib를 포함한 모든 프로그램을 새로 컴파일해서 재 설치해야 하는 번거로운 과정을 거치게 되었다. 공유라이브러리였다면 문제가 없을 것이다.

 이상 정적라이브러리와 공유라이브러리를 비교 설명했다. 그렇다면 선택의 문제가 발생할 것인데, 자신의 컴퓨터나 한정된 영역에서 사용할 프로그램을 제작하지 않는한은 공유라이브러리 형태로 프로그램을 작성하길 바란다. 특히 인터넷을 통해서 배포할 목적으로 작성할 프로그램이라면, 공유라이브러리 형태로 작성하는게 정신건강학적으로나 프로그래밍 유지차원에서나 좋을 것이다.

2012년 11월 5일 월요일

리눅스 C/C++ : 11장 구조체


1. 원시데이터 타입

 C 언어는 매우 기본적으로 사용하는 5가지 정도의 원시 데이터 타입이라는 것을 가지고 있다는 것을 앞서 배웠다. 이들 기본 타입은 다음과 같은 것들이다.

  • int, float, double, char, long long int, Pointer

 인간이 다루는 매우 복잡한 데이터들도 숫자와 문자, 도형 이라는 걸 생각하면 컴퓨터가 이렇게 단지 몇가지만의 데이터 타입을 가지는 것도, 어찌보면 당연한 결과라고 할 수 있을거 같다. C 언어뿐만 아니라 거의 대부분의 언어가 6-8개정도의 원시데이터 타입만을 가지고 있을 뿐이다. 종류역시 한두개 정도만 제외하고는 C와 거의 차이가 없다.


2. 원시데이터 타입의 구조화

 인간이 다루는 데이터로 보자면, 숫자,문자,도형만 있어도 모든 정보를 다룰 수 있기는 하다. 그렇지만 너무나 비효율적이다. 그래서, 이들 데이터 타입을 구조화해서 새로운 데이터 타입을 만들어서 사용하게 된다. 예를 들자면, 주소 정보를 관리하기 위해서 주소록을 만들고, 개인신상관리를 위해서 신상카드를 만들어서 사용하는 것이다. 이렇게 구조화하게 되면, 정보를 훨씬 깔끔하게 다룰 수 있게 된다.

 만약 유저정보를 관리할 목적이라면, 아래와 같이 데이터를 구조화 할 수 있을 것이다.

            +--- User Info  -----------------------+

            | Name : Text                          |

            | Age  : Number                        |

            | Address : Text                       |

            | Email   : Text                       |

            | Home    : Text                       |

            +--------------------------------------+
          
 TextNumber만으로 유저정보 관리를 위한 User Info라는 새로운 데이터 타입을 만들었다.


3. 구조체

 C언어도 원시데이터 타입을 구조화해서 새로운 데이터 타입을 만들 수 있도록 지원하고 있다. 이것을 우리는 구조체(Structure)라고 한다. 구조체는 다음과 같은 방식으로 만들 수 있다.

- 조작을 쉽게 하기 위한 단일 이름으로 묵어 놓은 하나 이상의 변수들의 집합 = 구조체

          struct 구조체이름 

          {

             데이터타입 변수명;

             데이터타입 변수명;

             데이터타입 변수명; 

          };

 위에서 예로 들었던, 유저정보를 구조체로 만들어 보도록하자. 이름은 문자열이 들어가게 되므로 char의 배열이나 포인터형식으로 선언해야 할 것이다. 포인터는 좀 귀찮으니, 모든 문자열은 char의 배열로 하도록 하겠다. 나이는 int형으로 하면 될것이고, 주소, 이메일, 홈페이지는 모두 char 배열로 하면 문제없을 것이다.

- 구조체에 포함된 각각의 변수 = 구조체 변수

          struct userInfo

          {

              char name[12]; 

              int age;

              char address[80];

              char email[40];

              char home[40];

          };

 구조체는 내부적으로 자신이 사용할 변수들을 유지하게 되는데, 이러한 변수를 멤버변수라고 한다.


4. 구조체의 정의, 선언 그리고 사용

 구조체는 원시데이터 타입을 요소로 가지는 사용자 정의 데이터타입으로 볼 수 있다. 그러므로 다른 원시데이터 타입과 마찬가지로 선언해서 사용하면 된다. 그러나 사용자 정의 데이터 타입이기 때문에, 구조체의 구조를 먼저 정의해줘야 한다. 인사기록 카드를 만들려면, 카드에 어떤 내용이 들어가야 하는지를 먼저 정의해야 하는것과 마찬가지다.

 구조체의 정의는 위에서 이미 설명한바가 있다. 이제 정의를 하는 위치가 문제가 되는데, 구조체는 프로그램 전체에서 선언되고 사용될 수 있으므로, 글로벌영역에서 정의가 된다. 예를 들자면 아래와 같다.

          // userInfo 구조체를 정의한다.

          struct userInfo

          {

              char name[12]; 

              int age;

              char address[80];

              char email[40];

              char home[40];

          };

          

          int main()

          {

              struct userInfo MyUser;

          }         


 선언은 일반데이터타입과 마찬가지다. 구조체의 이름뒤에 변수명을 적어주면 된다.


          struct userInfo Myuser;
          
 이렇게 정의와 선언이 끝났다면, 이제 사용하는 일만 남았다. 구조체는 다른 원시 데이터 타입들과는 달리, 내부에 멤버변수를 가진다. 그러므로 각각의 멤버변수별로 접근할 수 있어야 한다.

 C 언어는 멤버 연산자 "."을 이용해서 멤버변수에 접근할 수 있도록 하고 있다. userInfo 구조체 선언인 MyUser에서 각각의 멤버변수는 다음과 같이 접근할 수 있다.

          strcpy(MyUser.name, "yundream\0");

          MyUser.age = 33;

          strcpy(MyUser.email, "yundream@gmail.com\0");

          strcpy(MyUser.home, "http://www.joinc.co.kr\0");          

- strcpy는 문자열 복사 함수로써 strcpy()를 쓰기 전에는 반드시 복사할 문자열을 검사해 줘야 한다
- strcpy는 헤더파일 #include<string.h>가 필요 하다


5. 구조체와 배열

 어렵게 생각할 필요는 없다. 구조체도 데이터 타입이므로, 다른 원시 데이터처럼 배열을 이용해서 동일하게 구조화할 수 있다. 만약 유저정보를 5개를 저장하는 프로그램을 만든다면, 다음과 같이 배열로 선언하면 된다.


          struct userInfo Myuser[5];
          

 접근 역시 배열첨자를 이용하면된다.


          strcpy(MyUser[0].name, "yundream\0");

          MyUser[0].age = 33;

          strcpy(MyUser[0].email, "yundream@gmail.com\0");

          strcpy(MyUser[0].home, "http://www.joinc.co.kr\0");
          

 아주 간단하다.

- 구조체를 포함하는 구조체
: 멤버 연사자"."을 두 번 적용하면 된다. 즉 구조체를 포함한 변수가 my라고 하고 구조체에 포함된 구조체를 you라고 할 때 my.you.** 과 같은 방식으로 하면 된다.

- 구조체 초기화
: 구조체 초기화는 구조체 변수이름을 정할 때 초기값을 적어 주면 되는데 만약 구조체 변수가 "mysale"이라고 정할 때 }mysale={{"*****"}}; 와 같고 배열을 초기화 할 때는 변수뒤에 배열을 나타내는 "[]"을 넣으면 된다.


6. 구조체와 포인터

 배열과 포인터는 메모리 상에서 근본적으로 동일한 구조를 가진다는 것을 배웠다. 구조체를 배열로 다룰 수 있으니, 마찬가지로 포인터로도 다룰 수 있으며, 사용하는 방법도 10장에서 배웠던것과 동일하다.

 참, 다른 원시데이터 타입과 다른점이 있다. 구조체는 멤버변수를 가지고 있기 때문이다. 앞에서 구조체의 멤버변수에 접근하기 위해서 멤버연산자 .를 사용하면 된다는 것을 배웠다. 그러나 구조체를 포인터로 선언했을 경우에는 멤버연산자를 사용할 수가 없다. 멤버연산자는 을 가져오기 위해서 사용하는 연산자인데, 포인터는 이 아닌 주소를 다루기 때문이다. 그러므로 주소가 가리키는 곳의 을 가져오기 위한 새로운 연산자가 필요하게 된다. C는 구조체 멤버변수의 포인터연산을 위해서 참조연산자라는 것을 제공한다. 참조연산자는 ->를 사용하면 된다.

          strcpy(MyUser->name, "yundream\0");

          MyUser->age = 33;

          strcpy(MyUser->email, "yundream@gmail.com\0");      

 당연하지만, 포인터는 주소만 가리키는 도구이므로, 실제 데이터를 저장하기 위해서는 메모리를 할당해야만 한다. 메모리 할당은 malloc(3) 함수를 이용하면 된다. 아래 코드는 userInfo 구조체를 포인터로 선언한다음, 5개의 userInfo 정보를 저장할 수 있도록 메모리를 할당하는 프로그램이다. 메모리를 할당하기 위해서는 구조체의 크기를 알아야 할것인데, 다른 데이터 타입과 마찬가지로 sizeof명령을 이용해서 알아낼 수 있다.

          #include <unistd.h> - 소켓 프로그래밍 

          #include <stdlib.h>

          

          struct userInfo

          {

              char name[12];

              int age;

              char address[80];

              char email[40];

              char home[40];

          };

          

          int main()

          {

              struct userInfo *MyUser;

          

              printf("structure Size is %d\n", sizeof(struct userInfo));

              MyUser = (struct userInfo *)malloc(sizeof(struct userInfo) * 5);

          }

          

7. 예제 프로그램

 그럼 간단한 예제 프로그램을 만들어 보도록하자. 이 프로그램은 사용자 정보를 입력받아서 출력하는 일을 한다. 입력받는 정보는 다음과 같다.

  • 이름 : 문자열
  • 나이 : 숫자

 다음과 같이 구조체를 정의할 수 있을 것이다.

          struct userinfo

          {

              char name[20];

              int  age;

          };
          
 나이는 100살을 넘기기 힘들 것이다. 그러므로 age 변수의 경우 short int로 정의를 할 수도 있을 것이다. short int는 2byte이므로 4byte의 age에 비해서 2byte의 크기를 절약할 수 있을것이라고 생각할 수 있다. 하지만 다른 여러가지 이유들 때문에, 꼭 메모리 크기를 절약할 수 있는 것은 아니다. 이에 대한 내용은 따로 기회가 되면 다루도록 하겠다. 우선은 그냥 int형으로 하겠다.

 사용자 정보는 5개까지만 입력하도록 하겠다.

          #include <stdio.h>

          #include <string.h>

          

          struct userinfo

          {

            char name[20];

            int age;

          };

          

          int main(int argc, char **argv)

          {

            int age;

            int i;

            char buf[40];

            struct userinfo myfriend[5];

          

            for (i = 0; i < 5; i++)

            {

              printf("Name : ");

              fgets(buf, 19, stdin);

              buf[strlen(buf)-1] = '\0';            // <--- 1

              sprintf(myfriend[i].name, "%s", buf);

          

              printf("Age  : ");

              fgets(buf, 19, stdin);

              age = atoi(buf);

              myfriend[i].age = age;

            }

          

            printf("=======================\n");

            for (i = 0; i < 5; i++)

            {

              printf("%12s : %d\n", myfriend[i].name, myfriend[i].age);

            }

          }
          
 fgets(3)은 키보드로 부터 문자열을 입력받기 위해서 사용하는 함수다. 1은 키보드로 입력된 개행문자를 제거하기 위해서 사용했다.

 atoi(3) 함수는 문자열을 int형 값으로 변경하기 위해서 사용한다. 위의 예제는 나이를 숫자로 받아서 하는일이 없으니, 그냥 문자열 그대로 저장해도 상관은 없을 것이다. 그러나 나이를 가지고 비교한다던지 하는 숫자연산 작업이 있을 수 있으므로, 나중을 위해서 int 형으로 변환하는게 좋을 것이다.

 다음은 테스트 결과다.

          # ./userinfo

          Name : yundream

          Age  : 32

          Name : kopete

          Age  : 28

          Name : dream

          Age  : 31

          Name : minsu

          Age  : 29

          Name : test

          Age  : 32

          =======================

              yundream : 32

                kopete : 28

                 dream : 31

                 minsu : 29

                  test : 32

2012년 11월 1일 목요일

리눅스 C/C++ : 10장 포인터(2)

 출처 http://coolprogramming.springnote.com    저작자 NetGong


< Reveiw >

 포인터는 주소를 저장하는 변수라고 정의 했습니다. 이때 포인터 변수가 4byte 메모리 공간을 사용하므로 4개의 주소를 사용합니다. 그렇다면 포인터 변수의 시작주소를 저장하려면 어떤 포인터 변수에 저장해야 할까요?

(힌트)
  • int 변수의 주소는 int * 형 변수에 저장한다.
  • char 형 변수의 주소는 char 형 변수에 저장한다.
 그렇다면,
  • int * 형 변수의 주소는 int *  * 형 변수에 저장한다.
  • char * 형 변수의 주소는 char *  * 형 변수에 저장한다.

1, int형과 포인터

    
     int n = 100;
     int *pn = &n;
          int **ppn = &pn;

 n은 정수를 저장하는 변수(메모리)이며, pn은 int형 주소를 저장하는 변수(메모리)이며 ppn은 int*형 주소를 저장하는 변수(메모리)입니다. 아래 그림을 참고하세요.




 위 그림에서 ppn은 pn의 주소(&pn)을 pn은 n의 주소(&n)을 각각 저장합니다. 또 * 연산자를 주소앞에 붙이면 그 주소의 메모리이름이 된다고 했습니다. 그러므로

  • ppn에 * 붙인 *ppn은 pn 메모리가 됩니다.
  • pn에 * 붙인 *pn은 n 메모리가 됩니다.
  • *ppn에 * 붙인  **ppn은 n 메모리가 됩니다.

 위 내용을 확인하는 코드입니다.

void main( )
{    
    int n = 100;
    int *pn = &n;    
    int **ppn = &pn;
    printf("%x %x %x\n", &n, pn, *ppn);
    printf("%x %x \n", &pn, ppn);
    printf("===================\n");
    printf("%d %d  %d\n", n, *pn, **ppn);
}
  1. 12ff60 12ff60 12ff60
    12ff54 12ff54
    ===================
    100 100  100

 위 그림을 이해 했다면 어렵지 않습니다.

 int n에서 n은 변수(정수, 값을 저장하는)입니다.
 int *pn에서 pn은 포인터입니다.(싱글 포인터라고도 합니다.)
 int **ppn에서 ppn은 포인터의 포인터라합니다.(더블 포인터라고도 합니다.)
 int ***pppn에서 pppn은 포인터의 포인터의 포인터라합니다.
...

 부르기 힘들죠? 그래서 우리는

 int *pn에서 pn은 1차 포인터라고 합니다.
 int **ppn에서 ppn은 2차 포인터라고 합니다.
 int ***pppn에서 pppn은 3차 포인터라고 합니다.
 ...
 pn이 저장하는 &n은 1차 주소라고 합니다. 
 ppn이 저장하는 &ppn 2차 주소라고 합니다.
 pppn이 저장하는 &ppn은 3차 주소라 합니다.

 꼭 정리하고 기억하세요~!

 또 한가지 정리하고 가야합니다. 값(value)와 address(주소)입니다. 메모리 정리를 보시면 아시겠지만 메모리 내의 내용물을 값이라고 합니다. 한마디로,

  • n에 저장되어 있는 정수 10도 값(value)입니다.
  • pn에 저장되어 있는 주소 &n(12ff60)도 pn의 값(value)입니다.
  • ppn에 저장되어 있는 주소 &pn(12ff54)도 ppn의 값(value)입니다.

 하지만 그것은 컴퓨터의 일반적인 메모리에서의 이야기이며 보통 C언어에서는,

  • n에 저장되어 있는 정수 10을 값(value)라합니다. (raw value라고도 합니다.)
  • pn에 저장되어 있는 &n(12ff60)은 값이라 하지 않고 그냥 주소라고합니다. -> 12ff60은 숫자만의 의미가 아닌 int형 주소의 의미이며 데이터이기 때문입니다.
  • ppn에 저장되어 있는 &pn(12ff54)도 값이라 하지 않고 그냥 주소라고합니다. -> 12ff54도 숫자만의 의미가 아닌 int*형 주소의 의미이며 데이터이기 때문입니다.

 한마디로 기본 자료형 변수의 저장된 내용물만 값(value)라 하고 포인터 변수에 저장된 내용물은 주소라 합니다. * 연산자와 []연산자를 보여주는 예제입니다.

void main( )
{
    int n = 100;
    int *pn = &n;
    int **ppn = &pn;
    int ***pppn = &ppn;
    printf("%d %d %d\n", *pn, **ppn, ***pppn);
    printf("%d %d %d\n", pn[0], ppn[0][0], pppn[0][0][0]);
}
  1. 100 100 100
    100 100 100

 모두 100이 출력됩니다. *pn은 pn[0]와 같습니다. 우리 눈에는  달라도 컴파일러 눈에는 같다고 했습니다. 혹 기억이 안나시면 앞 페이지를 참고하세요.

  • * *pn은 pn[0][0]와 같습니다.
  • * **pn은 pn[0][0][0]와 같습니다.

 그러나 여기서 어떤 연산자를 사용할까요? 두 연산자 모두 사용 가능하지만 여기서는 []연산자 보다 *연산자를 사용하는게 좋습니다. []연산자는 주소를 기준으로 여러 메모리를 접근할 때 사용합니다. 조금만 공부해 보시면 알겠지만 주소로 메모리의 내용을 접근할 때 *연산자를 사용하는게 좋을 때가 있고 []연산자를 사용하는게 좋을 때가 있습니다.

2, char형과 포인터


void main( )
{
    char c = 'A';
    char *pc = &c;
    char **ppc = &pc;
    printf("%c %c %c\n", c, *pc, **ppc);
    printf("%x %x %x\n", &c, pc, *ppc);
}
  1. A A A
    12ff63 12ff63 12ff63
 위 내용을 공부하고 이해했다면 이 예제도 너무 당연한 결과입니다. 아래 그림을 참고하세요.




 여기까지입니다. ^^


.....