Joinc 팀블로그 리눅스 메뉴얼 정리 Joinc 위키
댓글

Recent Comments

Powered by Disqus
팀블로그 카테고리
  전체 (1105)
   공지사항 (1)
   검색엔진 (21)
   기술동향 (58)
   게임 (2)
   독서 (6)
   리눅스 (12)
   보안 (1)
   사회문제 (22)
   어셈블리 (43)
   영화 (3)
   오픈소스 (10)
   음악 (9)
   인물 (1)
   포인터 (4)
   프로그래머 (23)
   팀블로그 (20)
   테터툴즈 (29)
   C/C++ (152)
   FireFox (11)
   Gimp (2)
   Google (98)
   Java (13)
   Perl (2)
   Pthread (11)
   STL (13)
   TCP/IP (8)
   Tools (31)
   Web2.0 (42)
   Wiki (1)
«   2010/03   »
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      
2009/04/09 22:55

리눅스 시스템 프로그래밍 7장 쓰레드

아오.. 일단 포스팅 하고 보자..

<!> 문서는 완성된게 아니며, 틀린 내용이 있을 수 있습니다. 수정해야 할 부분이 있음 알려주세요. 확인 후 반영하도록 하겠습니다.

1 Thread에 대해서

프로그램을 병렬로 실행시키는 방법으로 fork()에 대해서 알아보았다. fork()는 매우 이해하기 쉬운 프로그래밍 방법이긴 하지만 자원효율성에서 몇가지 문제점을 가지고 있다. 프로세스는 기본적으로 code, data, stack, file I/O, 그리고 signal table의 5가지 요소로 구성이 된다. fork() 를 이용해서 새로운 프로세스를 생성하게 되면, 이러한 5가지 구성요소가 모두 복사가 된다. 그러하다 보니 프로세스를 생성하는데 많은 비용이 소비될 것이다. 대게의 경우에는 프로세스를 새로 생성시킬때 발생하는 성능저하가 문제가 되지는 않겠지만 웹서비스처럼 대량의 접근이 발생하는 영역에서는 문제가 될 수 있다.

fork의 이러한 방식은 상당히 효율이 떨어지는 측면이 있다. 어떤 프로그램을 병렬로 실행시킨다고 했을 때, 실제 우리가 병렬로 실행되기를 원하는 영역은 코드의 일부분이지 프로그램 전체는 아니기 때문이다.
// ... 
pid = fork();
if (pid > 0)
{
// 실제는 이 부분의 코드만 병렬로 실행되면 된다.
// fork()는 다른 모든영역의 코드가 복사되어 버린다.
}

게다가 전혀다른 프로세스를 생성시킴으로써, 프로세스간 통신이라는 상당히 복잡한 문제까지를 해결해야 한다. 병렬로 작동하는 프로그램은 특성상 데이터를 공유하거나 서로 통신을 해야 하는 경우가 많다. 그런데 프로세스는 서로 독립된 객체이므로 일반적인 방법으로는 데이터를 공유할 수가 없다. 이러한 프로세스간 데이터 통신을 위해서 리눅스는 IPC라는 설비를 제공하는데, IPC라는게 사용하기가 여간 까다롭지가 않다. IPC에 대해서는 별도의 장을 할애해서 다룰 계획이다.

Thread를 이용하면 fork()를 이용한 프로세스 기반의 병렬처리의 문제점의 많은 부분을 해결할 수 있다. Thread는 새로운 프로세스를 생성시키지 않고, 특정 문맥(코드)만을 병렬로 실행할 수 있도록 허용한다. 새로운 프로세스를 생성시키지 않기 때문에 그만큼 자원을 아낄 수 있으며, 더 효율적으로 빠르게 움직일 수 있다. 또한 같은 프로세스이기 때문에, 데이터를 공유하기가 쉽다는 장점도 가진다.

1.1 Thread vs Process

Thread는 프로세스와 다음과 같은 차이점을 가진다.
  • 프로세스는 독립적이다. 쓰레드는 프로세스의 서브셋이다.
  • 프로세스는 각각 독립적인 자원을 가진다. 쓰레드는 stat, memory 기타 다른 자원들을 공유한다.
  • 프로세스는 자신만의 주소영역을 가진다. 쓰레드는 주소영역을 공유한다.
  • 프로세스는 IPC를 이용해서만 통신이 가능하다.
  • 일반적으로 쓰레드의 문맥교환(context switching)는 프로세스의 문맥교환보다 빠르다.

1.2 Multi Thread 프로그램의 단점

모든 도구가 그러하듯이 Multi Thread 프로그램이라고 해서 장점만 가진 것은 아니다. Multi Thread 프로그램은 Multi Process 프로그래밍 방식에 비해서 다음과 같은 단점을 가진다.
  • 하나의 쓰레드에서 발생된 문제가 전체 프로세스에 영향을 미친다.
    멀티 프로세스의 경우에는 프로세스하나가 문제가 생기더라도 단일 프로세스로 문제가 제한된다. 그러나 멀티쓰레드 프로그램의 경우 하나의 쓰레드에 생긴 문제가 다른 쓰레드에까지 영향을 줄 수 있다. 예를 들어 쓰레드 하나가 다른 프로세스의 메모리 영역을 침범할 경우 프로세스 자체가 죽어버림으로써, 프로세스에 생성된 다른 모든 쓰레드도 프로세스와 함께 죽어버리게 된다. - 이 문제는 해결 가능하지만 여기에서는 다루지 않도록 하겠다. 시그널을 잘 활용하면 된다. 관심있으면 한번 고민해 보기 바란다. -
  • 디버깅이 어렵다. 문맥이 서로 교환되므로 추적하기가 까다롭다.

이러한 단점이 있음에도 불구하고 멀티쓰레딩 프로그래밍 기법을 선호하고 있다.

1.3 PThread

Thread운영체제에서 제공하는 병렬처리 메커니즘으로, 실제 이 메커니즘을 이용하기 위해서는 Thread의 구현체가 필요하다.

리눅스에서는 pthread라는 thread 구현 라이브러리가 사용되고 있다. pthread는 POSIX thread 의 줄임말로 POSIX 표준을 따르고 있다. pthread는 리눅스 뿐만 아니라 다른 거의 대부분의 유닉스에서도 사용할 수 있다. 이외에도 BSD 계열에서 사용하는 'Light Weight Kernel Threads , Apple 에서 사용하는 Multiprocessing Services등의 구현체가 있다. 이 문서는 pthread구현만을 설명하도록 할 것이다.

pthread는 리눅스 운영체제에서 제공하는 thread 를 제어하기 위한 함수들을 모아 놓은 C 라이브러리로, 다음과 같은 기능의 함수군을 제공한다.
  • 쓰레드 생성과 종료 관련 함수들
  • 쓰레드 동기화 관련 함수들
    쓰레드는 많은 데이터를 공유한다. 그러므로 데이터에 대한 동기화 문제를 해결해야할 필요가 있다.
  • 쓰레드 시그널 제어 함수들
    signal은 프로세스단위로 작동한다. 그러나 쓰레드 프로그램의 경우, 각 쓰레드 마다 다른 시그널 정책이 필요하므로, 쓰레드 전용의 시그널 제어 함수가 필요하다.

1.4 Multi Thread 프로그램

병렬로 작동하지 않는 하나의 문맥흐름만을 가지는 프로그램을 단일 쓰레드 프로그램이라고 한다. 반대로 아래와 같이 문맥이 나뉘어서, 동시에 두개 이상의 쓰레드가 실행되면, 이를 멀티 쓰레드 프로그램'''이라고 한다.


1.5 Process, Kernel Thread, User Thread

프로세스는 가장 무거운 커널의 스케쥴링 단위이다. 프로세스는 운영체제에게 할당받은 자원들 - 파일 핸들러,소켓,장치 핸들러 - 을 할당받게 된다. 프로세스는 독립된 단위로써 파일이나 주소영역 등을 공유하지 않는다.

kernel thread는 가장 가벼운 커널 스케쥴링 단위다. 하나의 프로세스는 적어도 하나의 커널 쓰레드를 가지게 된다. 만약에 프로세스가 하나이상의 쓰레드를 가지고 있다면, 이들 쓰레드는 같은 메모리와 파일자원등을 공유하게 된다. 만약 커널의 프로세스 스케쥴러가 선점형이라면 쓰레드의 스케쥴러도 선점형인 경우가 많다. 참고삼아서 선점형과 비선점형에 대해서 간략하게 설명하도록 하겠다.
  • 비선점형 : 특정 프로세스가 CPU를 독점하는 것이 가능하다.
  • 선점형 : 특정 프로세스가 CPU를 독점하는게 불가능하다.
특정 프로세스가 CPU를 독점하는게 불가능하게 하는 것은 프로세스가 인터럽트를 무시하기 못하게 하는 것으로 구현한다. 선점형은 어떤 프로세스가 시스템콜을 수행중이더라도 커널이 인터럽트를 보내면, 즉시 빠져 나와야 한다. 즉 운영체제가 CPU를 선점한다는 얘기가 된다. 시스템콜이 수행중이더라도 인터럽트를 걸고 다른 일을 수행하도록 할 수 있으므로 보다 빠른 반응성을 보여준다.

때때로 쓰레드가 유저영역 라이브러리로 구현되는 경우가 있는데, 이를 user Thread 라고 부른다.

1.6 쓰레드의 생성과 종료

멀티 쓰레드 프로그램이라고 하더라도, 처음 시작되었을 때는 main()에서 시작되는 단일 쓰레드 상태로 작동이 된다. 이 상태에서 pthread_create(3) 함수를 호출함으로써, 새로운 쓰레드를 생성할 수 있다. pthread_create를 이용해서 생성된 새로운 쓰레드를 worker 쓰레드라고 하자.

멀티 쓰레드 프로그램은 다음과 같은 흐름을 가진다.



생성된 worker thread는 언젠가 종료가 될 것이다. Master Thread (이하 부모 쓰레드)는 pthread_join()을 이용해서 worker thread들의 종료를 기다린다. pthread_join()는 종료된 worker thread의 자원을 정리하는 일을 한다. fork()를 이용한 멀티 프로세스 프로그램에서, 부모 프로세스가 wait()를 이용해서 자식 프로세스를 기다리는 것과 같은 이유라고 보면 된다.

1.6.1 pthread_create : 쓰레드 생성

pthread_create(3)함수를 이용하면 새로운 쓰레드를 생성할 수 있다. 이 함수는 다음과 같이 사용할 수 있다.
#include <pthread.h> 

int pthread_create(pthread_t * thread, pthread_attr_t *attr,
void * (*start_routine)(void *), void * arg);
  1. thread : 쓰레드가 성공적으로 생성되었을 때, 넘겨주는 쓰레드 식별 번호.
  2. attr : 쓰레드의 특성을 설정하기 위해서 사용한다. NULL일 경우 기본 특성
  3. start_routine : 쓰레드가 수행할 함수로 함수포인터를 넘겨준다.
  4. arg : 쓰레드 함수 start_routine를 실행시킬 때, 넘겨줄 인자
이 함수는 성공적으로 수행되었다면, 0을 리턴한다. 그렇지 않을 경우 1을 리턴한다.

1.6.2 pthread_join : 쓰레드 정리

쓰레드가 실행시키는 것은 함수 이다. 그러므로 return이나 exit(0)등을 이용해서 쓰레드를 종료시킬 수 있게 된다. 그러나 쓰레드 함수가 종료되었다고 해서 곧바로 쓰레드의 모든자원이 종료되지 않는다. fork()기반의 멀티프로세스 프로그램에서 종료된 자식프로세스를 정리하기 위해서 wait()로 기다리듯이, 종료된 쓰레드를 기다려서 정리를 해주어아만 한다. 그렇지 않을 경우 쓰레드의 자원이 되돌려지지 않아서 메모리 누수현상이 발생하게 된다.

pthread_create()로 생성시킨 쓰레드는 pthread_join()을 통해서 기다리면 된다. pthread_join 함수는 다음과 같이 사용할 수 있다.
#include <pthread.h> 

int pthread_join(pthread_t th, void **thread_return);
  1. th : pthread_create에 의해서 생성된, 식별번호 th를 가진 쓰레드를 기다리겠다는 얘기다.
  2. thread_return : 식별번호 th인 쓰레드의 종료시 리턴값이다.

pthread_join이 하는 일은 명확하다. 다만 주의 할것은 pthread_join은 반드시 joinable 한 상태로 생성된 쓰레드만을 기다릴 수 있다는 점이다. pthread_create로 쓰레드를 생성시킬 때, 나중에 join되지 않을 것으로 생각하고 생성시킬 수 있는데, 이렇게 되면 이 쓰레드는 종료하자마자 모든 자원을 해제하며, pthread_join으로 기다릴 수가 없다. 부모쓰레드와 떨어져서 완전히 독립적으로 작용한다고 하여, 이를 detach 한다고 한다. 쓰레드를 detach하는 방법은 아래에서 다룰 것이다.

1.6.3 쓰레드 생성 예제

pthread_create와 pthread_join을 알고 있다면, 이제 thread를 생성시킬 수 있다.
#include <pthread.h> 
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

// 쓰레드 함수
void *t_function(void *data)
{
int id;
int i = 0;
id = *((int *)data);

while(1)
{
printf("%d : %d\n", id, i);
i++;
sleep(1);
}
}

int main()
{
pthread_t p_thread[2];
int thr_id;
int status;
int a = 1;
int b = 2;

// 쓰레드 생성 아규먼트로 1 을 넘긴다.
thr_id = pthread_create(&p_thread[0], NULL, t_function, (void *)&a);
if (thr_id < 0)
{
perror("thread create error : ");
exit(0);
}

// 쓰레드 생성 아규먼트로 2 를 넘긴다.
thr_id = pthread_create(&p_thread[1], NULL, t_function, (void *)&b);
if (thr_id < 0)
{
perror("thread create error : ");
exit(0);
}

// 쓰레드 종료를 기다린다.
pthread_join(p_thread[0], (void **)&status);
pthread_join(p_thread[1], (void **)&status);

return 0;
}
아주 전형적인 프로그램이긴 하지만 pthread_join부분에 문제가 있다. pthread_join은 쓰레드가 종료될 때까지 블럭되기 때문이다. 이래서는 쓰레드를 두개이상 생성시키지 못할 것이다. 그렇다고 pthread_join을 이용하지 않는다면, 메모리 누수가 생기게 되니, 생략할 수도 없는 노릇이다.

1.6.4 자식쓰레드를 부모쓰레드로 부터 분리하기

pthread_join의 사용으로 발생할 수 있는 문제점을 해결하기 위한, 가장 좋은 방법중의 하나는 pthread_detach 를 이용해서, 자식 쓰레드를 부모쓰레드와 완전히 분리해 버리는 방법이다. 이 경우 자식 쓰레드가 종료되면, 모든 자원이 즉시 반환된다. 반면, 자식 쓰레드의 종료상태를 알 수 없다는 문제가 발생한다. 대게의 경우 자식 쓰레드의 종료상태가 중요한 문제가 되지는 않을 것이다.

만약 자식 쓰레드의 종료상태를 알아내는게 중요하다면, 종료상태를 저장할 전역변수를 두고, 여기에 종료상태를 기록하는 방식을 사용할 수 있을 것이다. 자식 쓰레드가 종료할때, 변수의 값을 바꾸고, 부모쓰레드에 시그널을 전송하는 방법이다. 이 방법은 이 문서의 뒤에서 따로 다루도록 하겠다.
#include <pthread.h> 
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

// 쓰레드 함수
// 1초를 기다린후 아규먼트^2 을 리턴한다.
void *t_function(void *data)
{
char a[100000];
int num = *((int *)data);
printf("Thread Start\n");
sleep(5);
printf("Thread end\n");
}

int main()
{
pthread_t p_thread;
int thr_id;
int status;
int a = 100;

printf("Before Thread\n");
thr_id = pthread_create(&p_thread, NULL, t_function, (void *)&a);
if (thr_id < 0)
{
perror("thread create error : ");
exit(0);
}

// 식별번호 p_thread 를 가지는 쓰레드를 detach
// 시켜준다.
pthread_detach(p_thread);
pause();
return 0;
}

1.7 쓰레드 동기화

이제 우리는 간단한 다중쓰레드 프로그램을 만들 수 있게 되었다. 그러나 이들 쓰레드 생성 함수만 가지고는 복잡한 쓰레드 프로그램을 만들 수가 없다. 쓰레드간 동기화라고 하는 문제가 놓여있기 때문이다. 아주 간단한 프로그램이 아닌한은 반드시 동기화문제를 고민해야만한다.

동기화란 여러가지 의미로 사용될 수 있는데, 이 경우에 있어서 동기화서로의 시간을 맞춘다를 의미한다. 멀티쓰레드 프로그램은 하나의 시간에 여러개의 프로세스가 돌아가는 형태를 취한다. 또한 멀티쓰레드 프로그램은 자원의 상당부분을 서로 공유하는 경우가 많다. 만약 단지 자원을 읽어들이는 거라면 상관없지만 읽고/쓰는 것이라면 동기화와 관련된 문제가 발생할 수 있다.

예컨데 다음과 같은 경우다.
  1. A와 B 두개의 프로세스가 있다. 이 프로세스는 int count=1 이라는 자원을 공유한다.
  2. A가 count를 읽어들이고 1을 더한다.
  3. B가 count를 읽어들인다. 아직 A가 count에 쓰지 않았기 때문에, B도 1을 읽어들인다.
  4. A가 count에 2를 쓴다.
  5. B도 count에 2를 쓴다.
  6. count에는 2가 저장되었다.
우리가 원하는 값은 2가 아닌 3이다. 그러나 쓰레드가 동기화 되지 않음으로써, 원치않은 잘못된 연산을 하게 되었다. 우리는 이 문제를 해결해야 한다.

1.8 접근제어

동기화 문제는 현실세계에서도 자주 발생한다. 화장실을 생각하면 된다. 화장실은 공유자원이며, 여러명이 사용한다. 누군가 화장실을 사용하고 있다면, 다른 사람은 화장실을 사용하면 안된다. 이 문제를 우리는 접근을 제어하는 방식으로 해결한다. 문을 걸어 잠궈서 한번에 한사람만 화장실에 들어가도록 하는 방법이다. 매우 이해하기 쉬운 방식이다.

다중쓰레드 프로그램에서도 마찬가지로 접근제어를 이용해서 이 문제를 해결한다. 이를 위해서 pthread는 mutex라는 잠금 메커니즘을 제공한다.

1.8.1 mutex 잠금

동시에 여러개의 쓰레드가 하나의 자원에 접근하려고 할때 발생하는 문제를 pthread는 임계영역을 두는 것으로 해결하고 있다. 임계영역안에는 접근하고자 하는 자원이 놓여있고, 오직 하나의 쓰레드만 임계영역안으로 진입할 수 있도록 제한한다. pthread는 이를 위해서 mutex를 제공한다. mutex는 그 자체가 가지는 잠금의 특성 때문에 mutex 잠금이라고 불리워지기도 한다.



위 그림은 mutex가 작동하는 방식을 보여준다. thread 1이 자원에 접근하면 mutex 잠금을 얻게 된다. 이 잠금은 단지 하나만 존재하기 때문에 thread 2는 잠금을 얻지 못하고 임계영역 밖에서 대기하게 된다. thread 1이 자원을 모두 사용하고 임계영역을 벗어나면 thread 2는 잠금을 얻게 되고 임계영역에 진입해서 자원을 사용할 수 있게 된다.

1.8.2 mutex의 사용

mutex를 사용하기 위해서는 다음의 4가지 함수가 필요하다.
  • mutex 잠금객체을 만드는 함수
  • mutex 잠금을 얻는 함수
  • mutex 잠금을 되돌려주는 함수
  • mutex 잠금객체를 제거하는 함수

1.8.3 pthread_mutex_init

mutex를 사용하기 위해서는 먼저 pthread_mutex_init() 함수를 이용해서, mutex 잠금 객체를 만들어줘야 한다.
pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr *attr); 
이 함수는 두개의 인자를 필요로 한다.
  1. mutex : mutex 잠금객체
  2. mutex_attr : mutex는 fast, 'recursive, error checking의 3종류가 있다. 이 값을 이용해서 mutex 타입을 결정할 수 있다. NULL 일경우 기본값이 fast가 설정된다.
    • fast : 하나의 쓰레드가 하나의 잠금만을 얻을 수 있는 일반적인 형태
    • recursive : 잠금을 얻은 쓰레드가 다시 잠금을 얻을 수 있다. 이 경우 잠금에 대한 카운드가 증가하게 된다.
mutex_attr을 위해서 다음의 상수값이 예약되어 있다.
  • fast : PTHREAD_MUTEX_INITIALIZER
  • recursive : PTHREAD_RECURSIVE_MUTEX_INITIALIZER
  • error checking : PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP

1.8.4 pthread_mutex_lock

mutex 잠금을 얻기 위한 함수다.
int pthread_mutex_lock(pthread_mutex_t *mutex); 
mutex 잠금을 얻는다라는 표현보다는 mutex 잠금을 요청한다라는 표현이 더 정확할 것 같다. 만약 mutex 잠금을 선점한 쓰레드가 있다면, 선점한 쓰레드가 mutex 잠금을 되돌려주기 전까지 이 코드에서 대기하게 된다.

때때로 잠금을 얻을 수 있는지만 체크하고 대기(블럭)되지 않은 상태로 다음 코드로 넘어가야할 필요가 있을 수 있을 것이다. 이 경우에는 아래의 함수를 사용하면 된다.
int pthread_mutex_trylock(pthread_mutex_t *mutex); 

1.8.5 pthread_mutex_unlock

mutex 잠금을 되돌려주는 함수다.
int pthread_mutex_unlock(pthread_mutex_t *mutex); 

1.8.6 mutex 잠금 예제

count 프로그램을 예제로 할 것이다. 임계영역안에서 보호되어야할 자원은 count이고, 여러개의 쓰레드가 count에 접근해서 +1을 시도하려고 한다. 이때 제대로된 count를 위해서는 한번에 하나의 쓰레드만이 counting을 하도록 해야할 것이다. mutex를 이용해서 임계영역을 보호하도록 할 것이다.

임계영역을 보호하지 않을 경우 다음과 같은 문제가 발생할 수도 있을 것을 예상할 수 있다.
int a = 1;  
Thread A 에서 a를 읽어들인다.
Thread B 에서 a를 읽어들인다.
Thread A 에서 a = a+1를 한다.
{
a = a+1;
결과는 2;
}
Thread B 에서 a++를 한다.
{
a = a + 1; // 읽어들인 값이 1이기 때문에
역시 결과는 2가 된다.
}
두번의 count가 발생했기 때문에 3이되어야 하겠지만 임계영역이 보호되지 않음으로써 2가 되어 버렸다.

mutex는 임계영역을 잠금으로서 이러한 문제를 해결한다. 이러한 문제를 해결하기 위해서는 임계영역에 단지 하나의 쓰레드만 접근하는걸 보장해줘야 할 것이다. mutex는 아래의 요소들을 보장함으로써 이를 보장한다.
  • Atomicity - mutex 잠금은 최소단위 연적 - atomic operation - 을 보장한다. atomic operation에 대해서 간단히 설명하고 넘어간다. 자세한 내용은 [http]Atomic Operation을 참고하기 바란다.
    1. aotomic operation은 일련의 연산 즉 mutex 잠금 연산이 끝날때 까지 다른 프로세스가 그 연산의 변화를 알 수 없는 상태가 되는 연산을 의미한다. (일반적으로 연산은 이전의 연산의 결과를 관찰한 후에서야 이루어질 수 있게다)
    2. 전체연산중 하나라도 실패할 경우 모든 연산이 실패하며 시스템은 전체 연산이 시작하기 전의 상태로 복구된다.
  • Singularity : 한 쓰레드가 뮤택스 잠금을 얻었다면, 이 쓰레드가 뮤택스 잠금을 내어놓기 전까지는 다른 쓰레드가 뮤택스 잠금을 얻을 수 없도록 한다.
  • None Busy Wait : 이것은 성능과 관련된 것이다. 바쁜대기상태에 놓이지 않는다는 뜻이다. 뮤택스 잠금을 얻을 수 있는지를 확인하기 위한 연산이 필요하지 않는 다는 의미로 받아들이면 될 것이다.

이상 mutex는 위의 3가지를 지원하는 것으로 공유되는 자원을 충돌없이 그리고 효율적으로 사용할 수 있도록 보장해준다.

다음은 mutex를 사용한 count 예제프로그램이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <stdio.h>  
#include <unistd.h>
#include <pthread.h>

int ncount; // 쓰레드간 공유되는 자원
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 쓰레드 초기화

// 쓰레드 함 수 1
void* do_loop(void *data)
{
int i;

pthread_mutex_lock(&mutex); // 잠금을 생성한다.
for (i = 0; i < 10; i++)
{
printf("loop1 : %d", ncount);
ncount ++;
sleep(1);
}
pthread_mutex_unlock(&mutex); // 잠금을 해제한다.
}

// 쓰레드 함수 2
void* do_loop2(void *data)
{
int i;

// 잠금을 얻으려고 하지만 do_loop 에서 이미 잠금을
// 얻었음으로 잠금이 해제될때까지 기다린다.
pthread_mutex_lock(&mutex); // 잠금을 생성한다.
for (i = 0; i < 10; i++)
{
printf("loop2 : %d", ncount);
ncount ++;
sleep(1);
}
pthread_mutex_unlock(&mutex); // 잠금을 해제한다.
}

int main()
{
int thr_id;
pthread_t p_thread[2];
int status;
int a = 1;

ncount = 0;
thr_id = pthread_create(&p_thread[0], NULL, do_loop, (void *)&a);
sleep(1);
thr_id = pthread_create(&p_thread[1], NULL, do_loop2, (void *)&a);

pthread_join(p_thread[0], (void *) &status);
pthread_join(p_thread[1], (void *) &status);

status = pthread_mutex_destroy(&mutex);
printf("code = %d", status);
printf("programing is end");
return 0;
}

1.9 앞으로 할 것

쓰레드는 매우 광범위한 주제로 여기에서는 쓰레드를 사용하기 위한 가장 기본적인 내용만 다루었다. 쓰레드에 대한 좀더 자세한 내용은 별도의 장을 할애해서 다룰 생각이다.

:::
2009/03/05 23:53

command pattern

Command Pattern

패턴과 패턴에 대한 (UML을 포함한)설명글을 보면 역시 사용하는 것보다 설명하는게 더 어렵군이라는 느낌이다. 내 생각을 잘 이해하기 쉽게 잘 전달해보자라는 마음가짐으로 가능한 평이하게 글을 만들어 봐야겠다.

Command Pattern은 행동자체를 객체로 본다. 행동을 객체로 봄으로써 좀더 쉽게 명령을 내리기 전의 상태로 복귀할 수 있다. 문제가 생길때 마다 그때 그때 쪽지로 일거리를 만들어서 주는 경우를 생각해 보자. 나중에 그 결과를 정리하는 것도 매우 복잡한 일이 될 확률이 높을 것이다. 이렇게 하지 않고 문제자체를 객체로 만들어서, 그러니까 문제를 분류하고 묶어서 서류철을 만들어서 넘기고 결과를 포함한 서류철을 다시 받게 된다면 문제에 대한 처리가 훨씬 간결해 질 것이다. 일반적인 업무처리에서도 흔히볼 수 있는 방식이기도 하다. Command Pattern이란 즉 수행해야할 문제 혹은 명령을 객체로 처리하는 방식이라고 정의내릴 수 있을 것이다.

이러한 특징은 다음과 같은 응용에 유용하다.
  1. Multi-level undo
    유저의 명령을 객체로 만든다. 만들어진 명령객체은 stack에 유지시키면,가장 최근에 실행된 명령이 가장 위에 놓이게 될 것이다. undo를 원한다면 undo() 메서드를 실행시켜서 pop을 하면 된다.
  2. Progress bar
    프로그램에게 주어진일의 명령의 연속으로 이루어진다고 가정한다. 이때 각각의 명령객체는 getEstimatedDuration메서드를 가진다. 프로그램은 이 메서드를 이용해서 명령이 어느정도 수행되었는지를 쉽게 판단할 수 있다.
  3. thread pool
  4. parallel processing
  5. networking

Command_pattern.png

Command
이 패턴에서 가장 중요한 클래스는 명령을 수행하는데 필요한 인터페이스를 가지고 있는 Command객체다. 일상업무에서의 작업요청서라고 볼 수 있을 것이다. 우리가 이 패턴을 사용하는 이유는 명서세에 적혀있는 명령을 실행하는 것이므로 명령을 수행하기 위한 execute메서드를 포함한다.
ConcreteCommand
Receiver객체가 어떤 Action을 취할지를 정의한다.
Receiver
주어진 요청에 대해서 어떤일을 수행해야 하는지를 알고 있는 객체.
Invoker
명령의 수행을 요청한다.
Client
ConcreteCommand 객체를 생성한다.

예제

C++

간단한 요리 프로그램이다. 재료를 준비를 위한 준비명령객체, 준비된 재료를 가지고 프라이팬에 볶는등의 작업을 하는 행동명령객체가 준비된다. 요리는 사람이 하는 거라서 때때로 실수가 있을 수 있기 때문에 Undo기능을 가진다. Undo 기능은 명령객체를 stack에 쌓아두고 pop하는 것으로 구현할 수 있다. 빠른 구현을 위해서 STLvector를 사용했다.

#include <iostream> 
#include <vector>
#include <string>

using namespace std;

// 가상메서드를 포함한 인터페이스 슈퍼 클래스
class Command{
public:
virtual void execute(void) =0;
virtual ~Command(void){};
};

class Ingredient : public Command {
public:
Ingredient(string amount, string ingredient){
_ingredient = ingredient;
_amount = amount;
}
void execute(void){
cout << " *Add " << _amount << " of " << _ingredient << endl;
}
private:
string _ingredient;
string _amount;
};

class Step : public Command {
public:
Step(string action, string time){
_action= action;
_time= time;
}
void execute(void){
cout << " *" << _action << " for " << _time << endl;
}
private:
string _time;
string _action;
};

// command를 저장하고 꺼내기 위한 stack 클래스
class CmdStack{
public:
void add(Command *c) {
commands.push_back(c);
}
void createRecipe(void){
for(vector<Command*>::size_type x=0;x<commands.size();x++){
commands[x]->execute();
}
}
void undo(void){
if(commands.size() > 0) {
commands.pop_back();
}
else {
cout << "Can't undo" << endl;
}
}
private:
vector<Command*> commands;
};

int main(void) {
CmdStack list;

//Create ingredients
Ingredient first("두스푼", "식용유");
Ingredient second("세컵", "쌀");
Ingredient third("한스푼","케찹");
Ingredient fourth("네홉", "콩");
Ingredient fifth("한스푼", "간장");

//Create Step
Step step("후라이팬에 볶기","3-4분 정도");

//Create Recipe
cout << "볶음밥을 만들어봅시다." << endl;
list.add(&first);
list.add(&second);
list.add(&step);
list.add(&third);
list.undo();
list.add(&fourth);
list.add(&fifth);
list.createRecipe();
cout << "요리시작!" << endl;
return 0;
}

C

음.. 그닥 객체지향적이지 않다고 생각되는 C를 이용해서도 구현해 봐야 겠다. 다음과 같은 요소들이 필요하지 않을까 싶다.구조체에 함수포인터 다발을 두는 것으로 클래스 비슷하게 만들어낼 수 있긴 하겠다.

PHP

애초에 전혀 객체지향적이지 않게 만들어졌던 php는 php4가 되어서야 class 키워드를 제공하기 시작했고 php5에 이르러서 그럭저럭 객체지향적이다라는 얘기를 듣게 되었다. 이제는 객제지향 패턴을 사용해서 만들어진 PHP 프로그램도 어렵지 않게 찾아볼 수 있다.
:::
2008/05/14 19:38

C++ 클래스의 동적적재

1 이유

다양한 애플리케이션에 사용가능한 로그분석 프로그램을 만들려고 한다. 이러한 프로그램의 경우 어떤 애플리케이션에서 사용할지 알 수 없기 때문에, 원본소스에는 수정할 필요 없이 로그분석 알고리즘만 적재가능하도록 만들 필요가 있다.

가장좋은 방법은 main 코드와 알고리즘을 분리시키는 것으로, 이것은 라이브러리의 동적적재를 이용해서 달성가능 하다. 동적적재는 라이브러리문서에 언급되어 있다.

즉 알고리즘을 플러그인 형태로 적재하는 기술인데, 여기에 더해서 C++의 클래스를 동적으로 적재시킬 수 있기를 원했다. 그렇다면, 클래스의 추상화를 이용해서, 좀더 일관된 개발자 인터페이스를 제공할 수 있을 것이기 때문이다.

그러므로 다음의 두가지를 달성하는게 주요 목표가 될 것이다.
  1. 로그분석 알고리즘을 플러그인 형태로 적재할 수 있도록 한다.
  2. 클래스를 적재함으로써, 일관된 개발자 인터페이스를 제공한다.

2 C++로 작성된 라이브러리사용의 문제

C++에서 클래스의 동적적재 가능성에 대해 생각해 본다.

C에서의 라이브러리의 동적적재는 명료하다. 이에 대한 내용은 라이브러리만들기 문서를 참고한다.

C++에서는 name mangling 때문에 dlopen()을 이용해서 라이브러리를 적재하는데, 어려움이 있다. 애시당초 dlopen()이 C++을 염두에 두지 않고 만들었다는 것도 문제일 것이다.

C와 C++은 함수를 가리키기위한 symbol 테이블을 가지고 있다. 어떤 함수를 호출하면 symbol 테이블을 뒤져서, 이진파일 내에서 함수의 원본의 위치를 알아내어서 읽어들이고 실행하는 것으로 묘사할 수 있다.

C는 symbol이 하나의 함수와 대응한다. 그렇지만 C++은 overloading으로 인하여서, 함수이름과 심볼이 일치하지 않는 경우가 발생한다. 이름은 같지만 인자가 다른 함수가 대표적인 경우다. 그러므로, C++로 공유라이브러리를 만들고자 할경우, 이 함수는 반드시 유일하다는 것을 컴파일러에게 알려줄 필요가 있다.

3 extern "C" 를 이용한 해결

오버로딩으로 인해서 발생하는 문제는 extern "C"를 이용하면 해결할 수 있다. extern "C" 는 해당 함수가 심볼이름과 일치될 것이라는 것을 알려준다. 이를테면 C 함수와 마찬가지로 사용하겠다는 의미다. 대신 C++의 기능인 overload등은 사용할 수 없게 된다.

예컨데 hello란 함수가 있다면, 다음과 같이 extern "C"를 이용해서 정의할 수 있다.
#include <iostream> 

using namespace std;
extern "C" void hello()
{
cout << "hello" << "\n";
}

4 class의 동적적재

exern "C"를 이용해서, 오버로딩이 필요없는 함수를 적재시키는 법에 대해서 알아봤다. 그렇다면, class는 어떨까.

안타깝게도 class는 dlopen()을 이용해서 호출할 수가 없다. 애초에 dlopen이 class를 염두에 두고 만들어진게 아니기 때문이다. 가장 일반적으로 사용할 수 있는 방법은 class에 대한 포인터를 넘겨주는 factory 함수를 만들고, 이 포인터를 이용해서 메서드를 호출하는 방법일 것이다.

다음은 factory 함수를 이용해서 클래스를 호출하는 예제 프로그램이다. 프로그램이름은 main.cc로 하겠다.
#include <dlfcn.h> 
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "mymodule1.h"

int main(int argc, char **argv)
{
void *handle;

char *error;
Test *LTest;

handle = dlopen("libmymodule.so", RTLD_LAZY);
if (!handle)
{
perror("Open Library\n");
exit(0);
}

// Func_Init
init_t* init_myFunc = (init_t *)dlsym(handle, "Func_Init");
if ((error = dlerror()) != NULL)
{
printf("ERROR : %s\n",error);
exit(0);
}
LTest = init_myFunc();
LTest->Count();
LTest->Count();
LTest->Print();

// Func_destory
destroy_t *destroy_myFunc = (destroy_t *)dlsym(handle, "Func_destory");
destroy_myFunc(LTest);
}

mymodule1.h로 class Test가 선언되어 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#ifndef _MYMODULE_H_ 
#define _MYMODULE_H_

#include <stdio.h>
#include <string>

using namespace std;

class Test
{
private:
int count;
public:
Test();
~Test()
{
printf("Destory\n");
}
void Count();
void Print();
};
extern "C" class Test* Func_Init();
extern "C" void Func_destory(class Test *aTest);

typedef void destroy_t(Test*);
typedef Test* init_t();
#endif
Funic_initFunc_destory라는 factory 함수가 선언되어있다.

다음은 Test 클래스와 factory 함수가 정의되어 있는 코드로 파일명은 mymodule1.cc다.
#include "mymodule1.h" 

Test::Test()
{
printf("Init\n");
count=0;
}

void Test::Count()
{
count++;
}

void Test::Print()
{
printf("Count is %d\n", count);
}

class Test* Func_Init()
{
Test *myTest;
myTest = new Test;
return myTest;
}

void Func_destory(class Test *aTest)
{
delete aTest;
}

void Func_Test(class Test *aTest)
{
aTest->Count();
aTest->Print();
}

다음은 컴파일 방법이다. 먼저 mymodule1.cc를 공유라이브러리형태로 컴파일 한다.
# g++ -fPIC -c mymodule1.cc 
# g++ -shared -W1,-soname,libmymodule.so.1 -o libmymodule.so.1.0.1 mymodule1.o

main.cc를 컴파일 한다.
g++ -o main main.c -ldl -lmymodule 
일반적인 동적적재의 경우 함수원형이 필요 없지만 class의 경우에는 메서드를 호출해야 하기 때문에 -l옵션을 이용해서 메서드에 대한 정의가 있는 라이브러리 명시적으로 지시해줘야 한다.

만들어진 프로그램을 실행시켜 보자.
$ ./main 
Init
Count is 2
Destory
제대로 실행됨을 알 수 있다.

5 다형성의 구현

factory 함수를 이용해서 클래스를 로딩할 수 있는 방법을 터득했으니, 이제 로그분석 프로그램 제작에 대한 계획을 세워보기로 하자.

이 로그분석 프로그램은 다음과 같은 사항들을 만족시켜야 한다.
  1. 다양한 로그에 대응할 수 있도록 플러그인형식으로 적재할 수 있어야 한다.
  2. 프로그래머에게 공통의 인터페이스를 제공할 수 있어야 한다.
1번 요구사항은 라이브러리의 동적적재를 이용해서 해결할 수 있다. 즉 설정파일을 만든다음에, 해당 로그파일에 대응되어서 적재할 라이브러리를 명시해주면 된다. 이것에 대한 간단한 예는 라이브러리의 사용에 언급되어 있으니, 응용하는데 문제없을 것이다.

2번 요구사항은 클래스를 적재시키고, 클래스의 메서드를 가상함수화 하는 것으로 달성할 수 있을 것이다. 일종의 Interface 클래스를 만들고, 개발자는 Interface의 메서드를 구현하는 방식으로 필요한 모듈을 개발하는 것이다.

6 예전 방식 구현

예전에도 로그분석 비스무레한 프로그램을 만들었던 적이 있다. 보안로그분석프로그램이였는데, 보안장비로 부터 syslog, snmp 혹은 전용 socket으로 부터 보안로그를 받아서 분석하고, 이벤트를 생성하는 프로그램이였다. 기본개념은 DOS 공격검사 프로그램의 제작에 소개된 바가 있다.

기본 시스템/네트워크 구성은 다음과 같다.

dos.png

보안장비는 다양한 종류의 보안소프트웨어가 설치되어 있으며, 로그 또한 전혀표준화 되어 있지 않으며, 나름대로의 정책을 가지고 만들어진다. 만약 새로운 장비가 추가된다면, 분석소프트웨어는 해당 장비의 보안로그를 분석할 수 있는 기능이 추가되어야 한다.

이 프로그램은 다음의 조건을 만족할 수 있어야 했다.
  1. 새로운 보안장비 혹은 새로운 보안 제품이 추가되면, 다음과 같은 이유로 새로운 분석모듈을 개발해야 한다.
    • 보안로그가 표준화 되어 있지 않았기 때문
    • plain text, binary 데이터 혹은 DB로 log를 저장하는 등 방식의 차이
    • snmp, syslog, 전용 socket 등 다양한 통신 방법
  2. 가능한 main 코드는 수정되지 않아야 한다.
  3. 모듈의 개발은 일관성이 담보될 수 있어야 한다.
위의 요건들을 만족시키기 위해서 다음의 방법을 사용했다. 일종의 전술패턴의 응용이라고 볼 수 있을 것이다.
  1. 각 제품에 대응되는 라이브러리를 생성한다. 10개의 제품의 로그를 분석해야 한다면, 10개의 라이브러리가 만들어질 것이다.
  2. {해당장비 => 해당장비의 로그를 분석할 수 있는 알고리즘이 포함된 라이브러리}를 선택하도록 전술을 구사한다.
이 방식은 함수포인터를 이용한 프로토콜 처리에 비슷하게 구현되어 있으니 참고하기 바란다.

이 방식은 만족시켜야할 조건중 1번과 2번은 어느정도 만족시킬 수 있지만, 3번을 만족시키지는 못했다. 코드가 객체지향적이지 않았기 때문에, 즉 C로 개발되었기 때문이다. 물론 좀더 노력과 시간을 들였다면, 3번을 만족시킬 수 있는 프로그램의 제작이 가능했겠지만, 그때는 그럴만한 실력을 갖추지 못했다.

7 방법의 개선

그래서 class의 가상함수를 이용해서 개발자 인터페이스를 만들고, 이 인터페이스를 상속받아서 실제 구현을 하도록 하는 방법을 생각했다. 여기에서는 대략적인 개념만 소개하는 정도로 하겠다.

다음과 같이 순수가상함수를 포함하는 Interface 클래스를 만들도록 한다. interface.h로 하겠다.
#ifndef _INTERFACE_H_ 
#define _INTERFACE_H_

class Log
{
private:
int data;
public:
virtual int Create()=0;
virtual int Anly()=0;
virtual int Read()=0;
virtual int Destroy()=0;
virtual ~LogAnly()
{
}
};

#endif
  • 이제 개발자는 모듈의 작성시 위의 Log클래스를 상속받고, 각 가상함수를 실구현하면 된다.
    • Create : 객체를 생성한다.
    • Anly : 실제 분석을 한다.
    • Read : 분석된 데이터를 읽어들인다.
    • Destroy : 객체를 파괴한다.
다음은 실구현을 포함한 코드다.
#include <interface.h> 
#include <iostream>

class TestLogAnly : public Log
{
private:
struct _CountData
{
int count1;
int count2;
};
_CountData CData;
Config *Cfg;
public:
TestLogAnly();
int Create(char *);
int Anly();
int Read();
int Destroy();
~TestLogAnly();
};

using namespace std;
int TestLogAnly::Create()
{
cout << "Create " << endl;
return 1;
}

int TestLogAnly::Destroy()
{
cout << "Module Anly Destroy" << endl;
return 1;
}

int TestLogAnly::Anly()
{
CData.count1 = 100;
CData.count2 = 200;
cout << "Log Anly" << endl;
return 1;
}
int TestLogAnly::Read()
{
cout << "ReadData count 1 : "<< CData.count1 << endl;
cout << "ReadData count 2 : "<< CData.count2 << endl;
}

TestLogAnly::~TestLogAnly()
{
cout << "Module Destroy" << endl;
}

TestLogAnly::TestLogAnly()
{
memset((void *)&CData, 0x00, sizeof(CData));
}

// factory 함수의 선언
extern "C" TestLogAnly *Obj_Create();
extern "C" void Obj_Destroy(TestLogAnly *);

TestLogAnly *Obj_Create()
{
TestLogAnly *rtv;
rtv = new TestLogAnly();
return rtv;
}

void Obj_Destroy(TestLogAnly *aLog)
{
delete aLog;
}
이것으로 동적적재에도 클래스의 가상화, 추상화, 은닉을 적용할 수 있게 되었다.
:::
2008/04/28 23:39

joinc와 함께하는 리눅스 시스템 프로그래밍 5장 - 프로세스

음.. 실로 거의 두달만에 하나의 챕터를 완성하는 군요. 그나마 아직 완성되었다고 보기는 힘들겠지만.. 수정할점이 있다면 댓글로 남겨주시기 바랍니다. 이 문서는 wiki로 관리됩니다. 최신문서는 wiki에서 확인하세요.

Contents

1 프로세스에 대해서
2 프로세스의 상태
3 프로세스의 모드
4 프로세스의 실행
5 멀티 - 다중 - 프로세스
5.1 fork를 이용한 자식프로세스 생성
5.2 fork와 exec를 이용한 새로운 프로세스의 생성
6 프로세스 관계
6.1 부모프로세스와 자식프로세스 init 프로세스
6.2 프로세스의 identify와 관계에서의 위치
6.3 고아 프로세스
6.4 Daemon 프로세스
7 코멘트

1 프로세스에 대해서

리눅스 운영체제가 하는 가장 중요한 일중의 하나는 프로그램을 실행시키는 것이다. 프로그램은 컴퓨터가 이해할 수 있는 명령어들과 명령을 수행하기 위한 데이터를 포함한 실행가능한 객체다. 이들 프로그램은 하드디스크와 같은 보조기억장치에 위치하는데, 실행을 시키게 되면, 운영체제는 이들을 읽어서 주기억장치에 위치시키게 된다. 이를테면, 프로그램이 복사된 이미지가 올라가는 것이라고 볼 수 있는데, 이러한 프로그램의 실행 된 객체를 프로세스라고 한다. 혹은 프로그램의 실행 이미지라고 말하기도 한다.
 
Hard Disk | | Memory
+-------------+ | |
| Program | | | ...
|-------------+ | COPY | +------------+
| Instruction |--|------>| | Process |
| Data | | | | Exec Image |
+-------------+ | | +------------+
| |
이렇게 프로그램을 직접실행시키지 않고, 메모리로 이미지를 카피해서 실행시키는 데에는 다음과 같은 이유가 있다.
  1. 서로 완전히 독립적인 프로그램의 실행 가능
  2. 여러개의 이미지를 만들 수 있으므로, 멀티프로세스/멀티쓰레딩 지원

2 프로세스의 상태

앞서 살펴봤듯이, 프로세스는 프로그램의 실행이미지로 동시에 수많은 동일한 혹은 다른 프로그램들이 실행될 수 있다. 이 동시란 말에 주목할 필요가 있는데, 이 동시' 라는 것은 시간적으로 정확히 동시라는 것을 의미하지는 않는다. 리눅스 운영체제에 있어서의 동시라는 것은 여러개의 프로세스를 짧은 시간동안 switching하면서 실행하는 것을 의미한다. A, B, C 4개의 프로그램이 있다면, A프로그램이 끝날때까지 기다렸다가 B를 실행하는 것이 아닌, A실행을 잠깐 중단시키고, B로 스위칭해서 실행을 하고 다시 중단시키고, C를 실행 하는 방식이다. C를 실행한 후에는 다시 짧은 시간에 A로 넘어가서 이전의 중단된 시점에서 다시 프로세스를 수행한다.

이러한 switching 시간은 매우 짧기 때문에, 실제로는 동시에 실행되지 않지만 동시에 실행되는 것처럼느껴진다.

이렇게 (완벽한 동시는 아니지만)동시에 프로세스를 실행하는 것을 멀티태스킹 운영체제라고 하며, 시간을 쪼개는 방식으로 멀티태스킹을 구현하는 것을 시분할 방식 멀티태스킹이라고 한다. 리눅스 운영체제는 시분할 방식 멀티태스킹환경을 지원한다. 아래 그림은 시분할 방식에서의 프로세스가 실행되는 방식을 보여주고 있다.

timeline.png

프로세스가 한번에 실행되지 않고, 시간을 기준으로 switching 됨으로써, 프로세스의 현재 상태가 중요해진다. 프로세스가 중단된 상황인지, 실행되고 있는지등의 정보를 알고 있어야만 올바른 시간에 switching이 가능하기 때문이다.

프로세스는 다음과 같은 4가지의 상태중 하나를 가지게 된다.
  1. running 상태 : 실행가능한 상태를 말한다.
  2. waiting 상태 : 어떤 조건을 기다리는 상태.
  3. stopped 상태 : 실행이 중단된 상태.
  4. zombie 상태 : 실행이 끝나고, 메모리 상에서 프로세스의 이미지가 제거 되었으나 운영체제의 커널은 여전히 프로세스의 정보를 가지고 있는 상태. zombie에 대해서는 뒤에서 자세히 다루도록 하겠다.

3 프로세스의 모드

프로세스는 유저모드커널모드의 두가지 모드를 오가면서, 실행이 된다.
  1. 유저모드 : 사용자 연산을 위한 모드로 사칙연산과 같은 연산작업으로 사용자 권한으로 각정 명령이 실행된다.
  2. 커널모드 : 주로 컴퓨터의 자원인 메모리, 하드디스크등의 장치에 접근하기 위한 모드로, 커널권한으로 실행된다.

kermode.png

굳이 귀찮게 커널모드라는 걸 두는 이유는 자원에 대한 보안의 목적이 가장 크다. 리눅스는 다중사용자 운영체제이다. 만약 메모리, 사운드카드, 하드디스크와 같은 자원에 아무런 제한없이 접근이 가능해진다면, 심각한 보안문제가 발생할 수 있을 것이다. 리눅스는 커널모드라는 것을 두어서 이문제를 해결하는데, 만약 프로세스가 시스템자원을 사용하길 원한다면, 커널에서 제공하는 API를 이용해서, 커널에 자원을 사용하겠음을 요청해야만 한다. 이렇게 되면, 운영체제 차원에서 자원에 대한 접근을 제어할 수 있게 될 것이다.

이렇게 커널에서 커널로 요청을 하기 위해서 제공하는 함수를 시스템콜이라고 부른다. 우리는 이미 몇개의 시스템콜을 사용해 봤는데, read(2), write(2), open(2)등이 대표적인 시스템콜이다.

4 프로세스의 실행

리눅스에서 새로운 프로세스를 실행시키는 유일한 방법은 execl(2)을 이용하는 것이다. 이 함수는 다음과 같이 사용할 수 있다.
#include <unistd.h> 
int execl(const char *path, const char *arg, ...);
  • path : 실행되는 프로그램의 완전한 경로다.
  • arg : 이것은 프로그램이 실행될때, 넘겨질 실행인자들로, 여러개가 정의될 수 있다. 더이상 넘겨질 실행인자가 없다는 것을 분명하 하기 위해서, 마지막에 NULL을 입력해줘야 한다. 간단히 ls(1)를 실행시키는 프로그램을 만들어 보자.
#include <unistd.h> 

int main(int argc, char **argv)
{
execl("/bin/ls", "ls", "-al", NULL);
}

execl 함수는 프로그램을 실행시켜서 새로운 프로세스를 실행하면, 현재의 프로세스 이미지를 덮어써 버린다. 예를들어 위의 프로그램이름이 execTest이라고 가정해보자. execTest 프로그램을 실행시키면, execTest의 실행이미지인 execTest 프로세스가 생성될 것이다. 여기에 execl을 이용해서 /bin/ls 를 실행시키면, /bin/ls의 실행이미지로 완전히 대체되어 버린다. 아래의 프로그램을 실행시켜 보자.
1
2
3
4
5
6
7
8
9
10
#include <unistd.h> 
#include <stdio.h>

int main(int argc, char **argv)
{
printf("Start\n");
execl("/bin/ls", "ls", "-al", NULL);
printf("End\n"); // 실행되지 않는다.
}
다음은 실행결과다.
$ ./execTest 
Start
drwxr-xr-x 2 yundream yundream 4096 2008-02-29 00:08 .
drwxr-xr-x 60 yundream yundream 4096 2008-02-29 00:08 ..
-rwxr-xr-x 1 yundream yundream 6585 2008-02-29 00:08 execTest
-rw-r----- 2 yundream yundream 81 2007-12-17 23:59 hello.txt
-rw-r--r-- 1 yundream yundream 12 2007-11-26 23:58 test.txt
-rw-r--r-- 1 yundream yundream 489 2007-11-26 23:53 write.c
$
우선 6번째 코드인 printf가 실행된건 분명히 확인할 수 있을 것이다. 그다음 7번째 줄인 execl이 호출되어서 /bin/ls -al 이 실행되었다. 그런데, 8번째 줄은 실행되지 않았다 ? 앞에서 말했다 시피, execl이 호출되면서 프로세스의 이미지 자체가 /bin/ls 로 덮어써져 버렸기 때문에, 8번째 코드가 아예 실행이 되지 않기 때문이다.

execl을 호출하면 프로세스의 이미지를 완전히 덮어쓰게 된다는 점을 이해하는건 그리 어렵지 않을 것이다. 그렇다면, 새로운 프로세스를 호출하고 나서, 원래의 프로그램으로 되돌아 오려면 어떡해야 하나 라는 고민이 생겨날 것이다. 새로운 프로세스를 실행시키고 나서, 프로세스가 종료되면 원래 상태로 되돌아오는 가장 대표적인 프로그램은 shell일 것이다. 쉘의 프롬프트에서 ls 를 입력하면, ls 실행된 후 다시 쉘상태로 되돌아 와서 프롬프트가 떨어지는 것을 확인할 수 있다. 어떻게 쉘과 같은 작동을 하는 프로그램을 작성할 수 있는가.

이 문제는 fork(2)를 이용한 다중 프로세스 생성기법으로 해결할 수 있는데, 이에 대한 내용은 조금 뒤에 알아보도록 할 것이다.

5 멀티 - 다중 - 프로세스

유닉스 운영체제는 다중 프로세스를 지원한다고 알고 있다. 그런데 앞에서 프로세스를 생성하는 유일한 방법은 execl 함수를 이용하는 것이라고 배웠다. 문제는 execl 함수는 원본 프로세스의 이미지를 덮어써 버린다는 것으로, 이렇게 되면 운영체제는 동시에 단지 하나의 프로세스만을 가질 수 있게 될 것이다.

유닉스 운영체제는 fork라는 프로세스 복사 함수를 이용해서 이 문제를 해결할 수 있다. fork는 원본프로그램의 복사판을 만드는 함수다. fork와 execl 함수는 분명히 다르다는 점을 인지하도록 하자. execl은 다른 프로세스를 생성하지만 fork는 자기자신을 복제한다. 즉 유닉스 운영체제에서 새로운 프로세스를 생성시키는 유일한 방법은 여전히 execl을 사용하는것 뿐이다.
  +---------+        +--------------+ 
| Process |----+---| Copy Process |
+---------+ | +--------------+
| +--------------+
+---| Copy Process |
| +--------------+
|
+--- ...
프로세스를 복사하는게 포크와 비슷하다고 해서 fork라는 이름을 붙이게 되었다.

이때 원본 프로세스를 부모 프로세스라고 하고, 부모 프로세스로 부터 복사 되어서 새로 생성된 프로세스를 자식 프로세스라고 한다. 이들 프로세스의 관계에 대해서는 뒤에 따로 살펴보도록 하겠다.

이제 fork함수를 이용함으로써, execl이 가지는 원본 프로세스 이미지를 덮어쓰는문제를 해결할 수 있다. fork를 해서 원본프로세스의 복사본을 만들고, 여기에서 execl을 이용해서 새로운 프로세스를 실행시키는 것이다. 이러한 식으로 프로그램을 생성시키는 가장 대표적인 프로그램이 바로 shell 프로그램이다. shell 에서 ls 프로그램을 실행시키면, 다시 sehll로 되돌아오는 것을 확인할 수 있을 것이다. 이는 shell 이 ls 명령을 받으면 fork함수를 이용해서 자식 프로세스를 만들고, 이 자식 프로세스에서 execl을 이용해서 ls를 실행시키기 때문에 가능해진다.

이렇게 fork & execl 을 이용하면, 진정한 멀티 프로세스 환경이 가능해 지게 된다. 유닉스 운영체제는 fork & execl 을 통해서 생성된 수많은 프로세스를 시분할방식으로 동시에 수행함으로써, 멀티 프로세스 환경을 제공한다.

5.1 fork를 이용한 자식프로세스 생성

fork 는 자기자신을 복사해서 프로세스를 생성하는 운영체제에서 제공하는 함수로, 그 분기되는 모습이 포크와 비슷하다고 해서 fork라고 이름지워졌다.

fork 함수는 다음과 같이 선언되어 있다.
#include <unistd.h> 

pid_t fork(void);
코드내에서 fork(2) 함수를 호출하면, 자식프로세스가 생성이 된다. 이 과정은 자식이 부모의 유전학적 정보를 상속받는 것과 비슷한데, 실제 자식프로세스는 부모로 부터 많은 정보들을 그대로 상속받는다. 예를들자면, 부모프로세스의 정보들, 열려있는 파일, signal정보, 메모리에 있는 많은 정보들이다. fork함수가 성공적으로 수행되어서 자식 프로세스가 생성되면, 부모프로세스에게는 새로 생성된 자식프로세스의 PID가 리턴이 되고, 자식프로세스에게는 0이 리턴된다.

다음은 fork를 이용해서 자식프로세스를 생성시킨 프로그램이다. 프로그램의 이름은 forktest.c 로 하자.
#include <unistd.h> 
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

int main()
{
int pid;
int i;

i = 1000;
pid = fork();
if (pid == -1)
{
perror("fork error ");
exit(0);
}
// 자식프로세스가 실행시키는 코드
else if (pid == 0)
{
printf("자식 : 내 PID는 %d\n", getpid());
while(1)
{
printf("-->%d\n", i);
i++;
sleep(1);
}
}
// 부모프로세스가 실행시키는 코드
else
{
printf("부모 : 내가 낳은 자식의 PID는 %d\n", pid);
while(1)
{
printf("==>%d\n", i);
i += 4;
sleep(1);
}
}
}
컴파일 한 후 실행시켜 보기 바란다. 부모프로세스와 자식프로세스가 동시에 주어진 코드를 실행시키는 것을 확인할 수 있을 것이다. ps 를 이용하면 이들 프로세스와의 관계를 명확하게 확인할 수 있다.
$ ps -ef | grep forktest 
UID PID PPID C STIME TTY TIME CMD
yundream 12119 8557 0 17:33 pts/0 00:00:00 ./forktest
yundream 12120 12119 0 17:33 pts/0 00:00:00 ./forktest
우리는 PID 12120을 가지는 자식프로세스가 생성되었음을 확인할 수 있다. PID 12120인 프로세스가 자식프로세스인 것은 PPID값을 이용해서 확인가능 하다. PPID 는 parent Process ID의 줄임말인데, PID, PPID 등에 대한 것은 이 문서의 후반부에 자세히다루도록 할 것이다.

5.2 fork와 exec를 이용한 새로운 프로세스의 생성

그럼 예제 코드를 이용해서 fork & execl의 작동방식에 대해서 알아보도록 하겠다. 여기에서 만들고자 하는 프로그램은 간단한 shell 프로그램이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <stdlib.h> 
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>

#define chop(str) str[strlen(str)-1] = 0x00;

int main(int argc, char **argv)
{
char buf[256];
printf("My Shell\n");
int pid;
while(1)
{
// 사용자 입력을 기다린다.
printf("# ");
fgets(buf, 255, stdin);
chop(buf);

// 입력이 quit 라면, 프로그램을 종료한다.
if (strncmp(buf, "quit", 4) == 0)
{
exit(0);
}

// 입력한 명령이 실행가능한 프로그램이라면
// fork 한후 execl을 이용해서 실행한다.
if (access(buf, X_OK) == 0)
{
pid = fork();
if (pid < 0)
{
fprintf(stderr, "Fork Error");
}
if (pid == 0)
{
if(execl(buf, buf, NULL) == -1)
fprintf(stderr, "Command Exec Error\n\n");
exit(0);
}
if (pid > 0)
{
// 부모 프로세스는 자식프로세스가 종료되길 기다린다.
int status;
waitpid(pid, &status, WUNTRACED);
}
}
else // 만약 실행가능한 프로그램이 아니라면, 에러메시지를 출력
{
fprintf(stderr, "Command Not Found\n\n");
}
}
}
이 프로그램은 아주 간단한 shell으로, 프로그램의 인자를 처리하지도 못하지만, fork 와 execl을 설명하는데에는 부족함이 없을 것이다. 다음은 실행시킨 예이다. 프로그램이름은 myshell 로 했다.
MY Shell 
$ myshell
# /usr/bin/w
01:15:32 up 2:58, 4 users, load average: 0.47, 0.50, 0.62
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
yundream :0 - 00:05 ?xdm? 14:20m 0.05s /bin/bash /usr/
yundream pts/1 :0 00:06 9.00s 1.47s 1.24s w3m -F http://w
yundream pts/3 :0 00:54 0.00s 0.22s 0.00s ./myshell
yundream pts/4 :0.0 00:53 22:13m 0.40s 0.27s BitchX irc.nuri

# ll
Command Not Found

# quit
$

6 프로세스 관계

6.1 부모프로세스와 자식프로세스 init 프로세스

위에서 fork와 exec 함수를 이용해서 프로세스를 실행시키는 방법에 대해서 알아보았다. 여기에서 우리는 프로세스가 전혀 독립적으로 생성되는게 아닌, 부모 프로세스에서 생긴다는 것도 덤으로 배우게 되었다. 부모프로세스가 있다면, 자식뻘이 되는 프로세스가 있을 것이다. 부모프로세스로 부터 fork되어새 생성된 프로세스를 자식 프로세스라고 부른다.

부모프로세스와 자식프로세스의 관계는 쉽게 이해되었을 것이다. 그렇다면, 부모의 부모의 부모의 부모의 프로세스가 있을 것이고, 최초의 아담격인 프로세스가 있으리라는걸 추리할 수 있을 것이다.

바로 init가 모든 프로세스의 조상이 되는 프로세스다. 모든 프로세스는 init로 부터 fork & exec 되어서 생성이 된다. pstree명령을 이용하면, 프로세스의 관계를 확인할 수 있다.
$ pstree 
init─┬─NetworkManager───{NetworkManager}
├─NetworkManagerD
├─acpid
├─amarok───11*[{amarok}]
├─atd
├─avahi-daemon───avahi-daemon
├─bonobo-activati───{bonobo-activati}
├─console-kit-dae───61*[{console-kit-dae}]
├─cron
├─cupsd
├─2*[dbus-daemon]
├─dbus-launch
....

6.2 프로세스의 identify와 관계에서의 위치

프로세스는 운영체제위에서 실행되는 실행객체이다. 객체가 객체로써 정체성을 가지기 위해서는 다른객체와 자신을 분리할 수 있는 identify 를 가지고 있어야 한다.

각 프로세스는 다음의 2가지 요소를 이용해서 자신의 identify 를 확보할 수 있다.
  1. name : 프로세스의 이름이다.
  2. PID : Process ID로 운영체제가 각각의 프로세스에 부여하는 유일한일련 번호다. 프로세스의 이름만으로도 identify를 확보할 수 있을거라고 생각할 수 있지만 이름이 같은 프로세스가 생성될 수 있으므로, name만 가지고는 identify를 확보할 수 없다. 때문에 운영체제에서 일련번호를 부여하게 된다. 이 번호는 중복되지 않는 유일한 번호다. 일종의 주민등록번호 정도로 보면 될 거 같다.

프로세스 이름과 PID 를 이용해서 프로세스를 identify(식별)할 수 있게 되었다. 하지만 이것만으로는 부족하다. 프로세스는 운영체제 위에서 독립적으로 존재하지만 또한 다른 프로세스들과 관계를 맺고 있기 때문이다. 어떤 프로세스는 반드시 어떤 프로세스의 자식 프로세스가 되어야 한다. 혹은 다른 프로세스의 부모가 되기도 한다.

즉 프로세스의 identify와 함께, 프로세스의 관계에서의 위치도 정의할 수 있어야 한다.

그래서, 각 프로세스는 name과 PID외에도 프로세스군에서의 자신의 위치를 정의하기 위한 다음과 같은 정보들을 가진다.
  1. PPID : 부모프로세스의 ID로 어떤 프로세스로 부터 생성이 되었는지를 알려준다.
  2. PGID : 프로세스는 여러개의 자식프로세스를 만들어낼 수 있다. 그렇다면, 이들 프로세스는 {부모-->{자식,자식,자식}}과 같이 하나의 가계를 만들 수 있을 것이다. 운영체제에서는 이것을 가계라고 하는 대신 group이 라고 한다. PGID는 프로세스가 어느 그룹에 포함되어 있는지에 대한 정보를 알려준다. PGID는 일련번호로 되어 있으며, 보통 부모프로세스의 PID즉 PPID가 PGID가 된다. 즉 프로세스그룹은 부모프로세스의 PID를 공통분모로 해서 하나의 그룹을 만들게 된다.

위의 forktest.c 프로그램을 실행시키고 다음과 같이 ps 를 이용해서 프로세스 정보를 알아보도록 하자.
$ ps -efjc | grep forktest  
UID PID PPID PGID SID CLS PRI STIME TTY TIME CMD
yundream 12198 8557 12198 8557 TS 24 17:40 pts/0 00:00:00 ./forktest
yundream 12199 12198 12198 8557 TS 21 17:40 pts/0 00:00:00 ./forktest
프로세스의 상세정보들이 출력됨을 알 수 있다. PID가 12199인 프로세스가 12198로 부터 생성된 자식프로세스임을 확인할 수 있다. 또한 12198 프로세스는 PID 8557 로 부터 생성된 자식 프로세스임을 미루어 짐작할 수 있다. 그렇다면 PID 8557인 프로세스가 어떤 프로세스인지 확인해 보도록 하자.
$ ps -efjc | grep 8557 
UID PID PPID PGID SID CLS PRI STIME TTY TIME CMD
yundream 8557 8550 8557 8557 TS 24 13:37 pts/0 00:00:00 bash
그렇다. bash 프로그램임을 알 수 있다. bash는 우리가 forktest 프로그램을 실행시킨 쉘프로그램으로, bash도 fork()를 이용해서 forktest 를 실행시켰을 것임으로 forktest의 부모프로세스가 된다. 이들의 관계는 다음과 같은 Tree 형태로 표현할 수 있을 것이다.
        fork&exec                    fork 
bash ---+----------- forktest ---+-------- forktest
bash의 부모프로세스는 PID 8550을 가지는 프로세스일 것이고, 거슬로 올라가면 결국 init 프로세스를 만나게 될 것이다.

그렇다면 왜 그룹이 중요한 걸까. 단지 분류하기 좋게 하기 위해서 ? 물론 그런이유도 있기는 하지만, 좀 더 근본적인 이유가 있다.

그룹은 실생활에서의 가족들이 그렇듯이, 공통의 자원을 공유하는 관계로 서로에게 영향을 끼친다. 즉 부모프로세스가 종료되면 자식 프로세스도 따라서 종료되어버리거나 부모로부터 버려진 고아가 되는 등의 영향을 받는다. 또한 부모프로세스는 자식프로세스를 종료시킬 수 있으며, 아예 분가시켜버릴 수도 있다.

부모와 자식프로세스간의 어떤 매체를 이용해서 소통 이루어진다는 건데, 리눅스 운영체제signal이라는 매체를 이용해서, 부모와 자식프로세스간에 소통을 한다. 예를들자면 부모프로세스가 너 그냥 죽어라라 고 신호를 보내건나 보내지 않는 식이다. 만약 부모프로세스가 죽으면서, 자식프로세스들에게 너희도 따라서 죽어라 - 좀 잔인한가? - 라고 하면, 자식프로세스들도 함께 죽는 거고, 자기만 죽겠다고 하고 신호를 보내지 않는다면, 자식프로세스는 고아프로세스가 되는 식이다.

signal의 사용과 고아프로세스에 대한 것은 따로 언급될 것이다.

6.3 고아 프로세스

위에서 프로세스는 부모프로세스와 그룹을 맺는다는 것을 배웠다. 그리고 고아 프로세스에 대해서도 간단하게 알아보았다. 고아 프로세스란 즉, 부모프로세스가 죽으면서 자신만 죽어서 자식프로세스는 그대로 남아있는 상태다. 부모프로세스가 죽었으니 고아가 될수 밖에...!!!

고아 프로세스는 어떻게 될까. 그냥 버려질까 ? 그렇다면 너무 비정한것 같다는 생각이 든다. 유닉스 운영체제를 만들던 개발자들이 매우 인간적이여서 그랬는지는 모르겠지만 이들은 고아가된 프로세스를 init 프로세스가 관리해서 버려지지 않도록 설계를 했다. 현실에서 고아를 버리지 않고, 사회에서 보호하는 것처럼 말이다.

이론적으로 고아 프로세스는 아주 간단하게 만들수 있다. 자식프로세스를 생성시킨 후 부모프로세스를 종료시키기만 하면 된다.

위의 forktest.c에서 자식 프로세스가 고아 프로세스가 되도록 수정해 보았다. 프로그램의 이름은 forktest2.c 로 하겠다.
#include <unistd.h> 
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

int main()
{
int pid;
int i;

i = 1000;
pid = fork();
if (pid == -1)
{
perror("fork error ");
exit(0);
}
// 자식프로세스가 실행시키는 코드
else if (pid == 0)
{
printf("자식 : 내 PID는 %d\n", getpid());
while(1)
{
printf("-->%d\n", i);
i++;
sleep(1);
}
}
// 부모프로세스가 실행시키는 코드
else
{
printf("부모 : 내가 낳은 자식의 PID는 %d\n", pid);
sleep(1);
printf("T.T 나죽네\n");
exit(0);
}
}
이제 실행시켜 보도록 하자.
$ ./forktest  
부모 : 내가 낳은 자식의 PID는 8207
자식 : 내 PID는 8207
-->1000
T.T 나죽네
-->1001
yundream@yundream-desktop:~$ -->1002
-->1003

yundream@yundream-desktop:~$ -->1004
-->1005
-->1006
-->1007
-->1008
쉘과는 따로 자식프로세스가 계속 실행되는걸 알 수 있을 것이다. 이제 Ctrl+C 를 눌러보자. Ctrl+C를 누르면 일반적으로 프로세스는 종료가 된다는 것을 경험적으로 알고 있을 것이다. - 정확히 말하자면 SIGINT라 는 시그널이 전달되고, 이에 대한 반응으로 프로세스가 죽는다. 시그널은 나중에 다룰 것이다 -. 그러나 Ctrl+C를 아무리 눌러도 자식프로세스가 죽지 않는걸 알 수 있을 것이다. 왜냐하면, bash 의 자식의 자식 프로세스, 즉 같은 그룹에 속하지 않은 전혀 다른 그룹의 프로세스가 되었기 때문이다. ps 결과로 확인해 보도록 하자.

#ps -efjc | grep forktest 
UID PID PPID PGID SID CLS PRI STIME TTY TIME CMD
yundream 8207 1 8206 8093 TS 24 00:16 pts/5 00:00:00 ./forktest
PPID가 1 즉 init의 자식프로세스가 되었음을 확인할 수 있다. 집도 절도 없는 고아프로세스라는 얘기가 되겠다.

6.4 Daemon 프로세스

고아프로세스는 어감이 좋지 않아 보이기는 하지만, 프로세스의 또다른 가능성을 보여준다. 즉 현재 유저와 프로세스의 영향을 받지 않고 백그라운드에서 실행되는 프로세스의 제작에 관한 것이다.

이렇게 현재 화면과 프로세스에서 떨어져 나가서 독립적으로 실행되는 프로세스를 데몬 프로세스라고 한다. 가장 대표적인 프로그램이 웹서비스를 위한 웹서버 프로그램일 것이다. 이런 프로그램들은 거의 운영체제가 시작됨과 동시에 시작되어서 운영체제가 끝날때까지 뒤에서 우리가 눈치채지 못하는 상태에서실행이 된다.

데몬프로세스가 되려면 다음과 같은 조건을 갖추어야 한다.
  1. 일단 고아 프로세스가 되어야 한다.
    데몬 프로세스는 완전히 독립된 프로세스다. 그러므로 고아 프로세스가 되어야 한다. 예컨데, 가족으로 부터 독립해서 사회로 나가야 된다는 얘기가 되겠다.
  2. 표준입력, 표준출력, 표준에러을 닫는다.
    표준입력표준출력, 표준에러는 사용자와 프로세스가 상호작용 하기 위한 장치로, 표준입력은 키보드, 표준출력은 모니터로 대응된다. 데몬 프로세스는 뒤에서 독립적으로 돌아가는 프로세스 이므로 사용자와의 상호작용을 해서는 안된다. 그러므로 표준입력과 표준출력을 표준에러를 닫아줄 필요가 있다. 뒤에서 혼자 돌아야 하는 프로그램인데, 모니터에 (forktest2.c 와 같이) 잡다한 메시지를 출력해서는 안될 것이기 때문이다.
  3. 터미널을 가지지 않는다.
    터미널이란 사용자가 컴퓨터에 접속된 상태를 말한다. 이 터미널에 키보드와 모니터와 같은 장치가 연결되어 있고, 이것을 이용해서 사용자의 프로세스가 컴퓨터와 연결이 된다. 데몬 프로세스는 사용자 환경과 독립되어야 하므로 터미널을 끊어줘야 한다.

그렇다면, 고아 프로세스를 만든다음 고아 프로세스로 부터 표준입력,출력,에러를 닫고 터미널을 제거시키면 데몬 프로세스가 될 것이라는 것을 추리할 수 있을 것이다. 데몬 프로세스를 만드는건 이 3가지의 과정의 코드화다.

다음은 완전한 데몬 프로세스다. 프로그램이름은 daemon.c로 하자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <unistd.h> 
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

int main()
{
int pid;
int i;

i = 1000;
pid = fork();
if (pid == -1)
{
perror("fork error ");
exit(0);
}
// 자식프로세스가 실행시키는 코드
else if (pid == 0)
{
printf("자식 : 내 PID는 %d\n", getpid());
close(0);
close(1);
close(2);
setsid();
while(1)
{
printf("-->%d\n", i);
i++;
sleep(1);
}
}
// 부모프로세스가 실행시키는 코드
else
{
printf("부모 : 내가 낳은 자식의 PID는 %d\n", pid);
sleep(1);
printf("T.T 나죽네\n");
exit(0);
}
}
  1. 39 에서 부모프로세스를 종료한다.
  2. 22,23,24 에서 표준입력,표준출력,표준에러를 닫았다.
  3. setsid()를 이용해서, 사용자환경에서 독립된 자신의 환경을 만든다. 기존의 환경이 리셋되면서 터미널이 사라진다. 또한 새로운 터미널을 지정하지 않았기 때문에, 이 프로세스는 결과적으로 터미널을 가지지 않게 된다.

ps를 통해서 프로세스의 상태를 확인해 보도록 하자.
$ ps -efjc | grep daemon  
UID PID PPID PGID SID CLS PRI STIME TTY TIME CMD
yundream 8252 1 8252 8252 TS 24 00:43 ? 00:00:00 ./daemon
PPID가 1 이고, 새로운 Session ID인 8252를 가졌으며 (이 프로세스가 세션의 주인이므로, PID가 SID가 된다) 터미널(TTY)가 없음을 확인할 수 있다. 28번 줄의 printf 결과도 화면에 출력되지 않는 것을 확인할 수 있다. 완전한 데몬 프로세스가 만들어진 것이다.

데몬프로세스는 특히 인터넷 서버 프로그램을 만드는데, 중요하게 사용되는 기법으로 매우 빈번하게 보게 될 것이다.
:::
2008/04/07 19:53

C,Perl만 믿고가자구요.


http://www.indeed.com 의 jobtrend 서비스를 이용한, 언어/플랫폼별 구인 트랜드 현황입니다. 우리나라의 상황과는 차이가 있을 수 있으니, 그냥 참고삼아서 보면될듯 합니다.

job5.png

일단 스크립팅 언어입니다. 우리나라에서의 상황과는 달리 여전히 perl이 압도적입니다. javascript는 성격이 약간 다른언어이지만, Ajax덕분인지 상승세가 뚜렷하네요.

job2.png

역시 오래된 컴퓨팅 역사로 인해서 보수화 된것 같습니다. 우리나라에 비해서 여전히 C를 필요로 하는 회사가 많다는걸 알 수 있습니다. 심심하다면, cobol등의 언어도 넣어보시길

job3.png

여전히 윈도우즈 관련직종이 압도적인거 같습니다. 그렇지만 linux가 꾸준히 증가하고, 윈도우즈는 꾸준히 감소하고 있다는 점이 주목할만 합니다. 그래프 내용만 보자면, 윈도우즈 직종이 리눅스로 이동하는 걸로 보입니다.

job4.png

최근 이슈인 blogwiki, sns 서비스들에 대한 trend 입니다. blog의 성장과 하락이 재미있습니다. blog가 폭발했을 저당시 무슨일이 있었는지 궁금해지네요.

언어측면에서 봤을때, 외국은 보수적이며 산업적으로 검증된 언어를 여전히 선호하고 있는 것으로 보입니다. 우리나라에서는 폐물취급 받고있는 C와 Perl이 여전히 널리 사용되어지고 있습니다.

결론은 C, Perl만 믿고 가자는..
:::
2008/02/01 15:26

리눅스 프로그래밍 구글 그룹 개설

인구의 차이 그리고 문화의 차이등도 있겠지만 그래도 internet 사용자 10위권이라는 말이 무색하게 뉴스그룹은 있는지 없는지도 모르는게 현재 우리나라의 상황입니다. 커뮤니티 자체가 끈끈한 커뮤니티를 지향하는 사이트의 게시판 위주로 이루어지다 보니, 상대적으로 매우 느슨한 커뮤니티라고 할 수 있는 뉴스그룹에는 그다지 매력을 느끼지 못하는 것이지 싶습니다.

하긴 개발자 커뮤니티라고 해봐야 그럭저럭 운영되는 커뮤니티가 열손가락에 꼽을 정도이니, 뉴스그룹과 같은 아웃어브안중 서비스를 이용해서 정보를 공유한다는 것은 어찌보면 사치일 수도 있을 것 같습니다.

느끼시고 계시겠지만, 우리나라의 개발자 커뮤니티는 친목 동호회화 되고 있습니다. 잡담게시판화 되어가고 있다고 말씀하는 분들도 있습니다. 끈끈한 커뮤니티가 가지는 단점중 하나 입니다. 정이 넘친다. 따뜻하다. 인간적이다. 편안함을 느낄 수 있다라는 등의 장점을 가질 수도있지만 분명한 단점도 있는 거죠.

문제는 우리나라의 개발자 커뮤니티는 끈끈한 커뮤니티가 거의 강제되고 있다는 겁니다. 그렇지 않은 커뮤니티들도 있긴 하지만, 있는지 없는 지도 모르게 되는 경우가 대부분입니다. 정이많은 문화의 경우 집단내에서 편안함을 느끼기 때문에, 가능한한 작은집단 보다는 큰 집단을 선호하기 때문이 아닌가 생각됩니다. 작은집단은 여러가지 이유로 해체될 가능성이 높으니까요. 이를테면 보수주의적이 된다고나 할까 ?

우리나라에도 느슨한 커뮤니티의 성공적인 모델이 있어야 하지 않을까 생각해 봅니다. 아니 성공적인 모델이라기 보다는 커뮤니티의 다양함이 어느정도 유지되어야 되야 된다고 보는게 제 입장이구요.

해서 구글 그룹스를 이용해서 한번 시도해보려고 합니다. 쉽지는 않겠지만, 꾸준히 관리하면, 좋아지지 않을까 생각하면서 말이죠.

그룹이름은 [http]joinclinuxprog입니다. 원래는 linuxprog로 할려고 했는데, 이미 등록이 되어 있더군요. 그래서 걍 내키는데로.. joinc만 앞에다 가져다 붙였습니다. 카테고리는 컴퓨터 > 운영체제, 컴퓨터 > 프로그래밍 입니다.

처음에는 comp.unix.programmer, comp.lang.c 와 같은 활동적인 그룹에 올라오는 글들중 유익한 글을 선택해서 번역 포스팅하는 작업을 하면서, 참여를 유도해볼 생각입니다. 지금의 joinc와 같이 서두르지 않고, 긴 호흡으로 조금씩 커뮤니티를 만들어 보도록 노력해야 겠습니다.
:::
2008/01/24 19:56

다중 Level Merged Sort

소개

여기에서는 다중의 머지소트(Merged Sort)에 대해서 생각해 보도록 하겠다. 머지소트를 하기 위해서는 3개의 버퍼가 필요하다는 것을 알고 있을 것이다.

merged.png

  1. A에 자료를 집어 넣고
  2. B에 자료를 집어 넣은다음
  3. A와 B를 Merge 해서 결과를 C에 넣는다.
  4. 새로운 데이터가 있다면 다시 A에 넣고
  5. A와 C를 Merged 해서 결과를 B에 놓는다.
  6. 위의 순서를 반복한다.

이러한 일반적인 merged sort는 그다지 어려울 것이 없다고 생각된다. 그러나 다음과 같은 경우를 생각해볼 수 있다.

mmerged.png

3개의 A, B, C 그룹이 있고, C그룹의 데이터들을 머지소트한 결과와 B그룹의 데이터들을 머지소트한 결과를 다시 머지 소트하고, 이 결과를 다시 A그룹의 머지소트한 결과의 머지소트해서 최종 결과를 가져온다.

이러한 요구는 같이 가중치(Level)를 달리하는 서로다른 그룹에 대해서 머지소트하고 이들 그룹을 다시 머지소트해서 Score를 산출해야 하는 검색엔진등에서 발생할 수 있을 것이다.

간단히 생각하자면, 각 그룹을 머지소트하는데 3개의 버퍼가 필요하므로, 3x2 = 6개의 버퍼로 필요한 계산을 할 수 있을 것이다. 그러나 이 경우 대량의 메모리를 낭비할 수 있다는 문제가 발생한다. 머지소트해야할 데이터가 1,000,000의 레코드로 이루어져 있고, 각각의 레코드의 크기가 20byte 정도라면 한번의 머지소트를 위해서 120M의 메모리를 소비해야 한다. 여기에 동시접속까지 제어해야 하는 서비스에 사용될 엔진이라면, 10명이 접속했을 때, 1.2기가의 메모리를 소비하게 된다.

이러한 경우 memory풀을 유지해서, 할당하는 등의 방법을 고려해야 겠으나 우선적으로 사용한 메모리의 양을 줄일 필요가 있다. 이 방법에 대해서 고민해보기로 했다.

버퍼 레퍼런스 관리

이 문제를 해결하기 위해서 가능한 최소의 버퍼를 만들어 레퍼런스를 이용해서 재사용하는 방법을 생각해 보았다. 버퍼풀을 유지하기 위한 방법이라고 볼 수 있을 것이다.

doublemerged.png

여기에는 4개의 버퍼가 사용된다.
  1. Buff Manager가 4개의 버퍼중 3개를 얻어낸 다음 Lv1 Merged Sorter 에게 돌려준다.
    1. Lv1 Merged Sorted는 주어진 3개의 버퍼를 이용해서 Merged Sorted를 한다.
    2. 마지막으로 결과를 저장한 버퍼의 레퍼런스(주소값)을 저장한다.
  2. Buffer Manager가 4개의 버퍼중 3개를 얻어낸 다음 Lv2 Merged Sorted 에게 돌려준다.
    1. 이때 Lv1 Merged Sorter의 최종결과물이 저장된 버퍼는 사용하면 안된다.
    2. 우리는 Lv1 Sorter의 결과물 버퍼의 레퍼런스를 알고 있으므로, 이것을 제외한 3개의 버퍼를 Lv2 Sorter에게 할당할 수 있다.
    3. Lv2 Sorter은 주어진 3개의 버퍼를 이용해서 merged Sorted를 수행한다.
    4. 최종결과를 저장한 버퍼의 레퍼런스를 저장한다.
  3. 이제 두개의 버퍼에 Lv1 Sorter 과 Lv2 Sorter의 결과물이 있다. 두개를 merged해서 사용가능한 버퍼에 결과물을 저장한다.

다음은 구현코드다. 아이디어가 실현되는지를 확인하기 위한 코드로 데이터는 int형으로 대신했으며, 실제 merged sort를 하지 않고, 2개의 int형 버퍼에 있는 값을 더해서 남는 버퍼에 저장하도록 했다. 실제 구현은 int형대신, 클래스나 구조체와 같은 자료구조가 들어가고, 덧셈연산대신에 Merged Sort 연산이 들어갈 것이다. 어쨋든 아이디어를 테스트하는데에는 무리가 없을 것으로 생각된다.

아이디어 구현에 집중하기 위해서 STL을 사용했다.

  1. 4개의 int형 버퍼중에 3개의 int형 버퍼를 할당한다.
    1. 첫번째 버퍼에 1이 들어간다.
    2. 두번째 버퍼에 2가 들어간다.
    3. 3번째 버퍼에 1+2=3 이 들어간다.
    4. 4번째 버퍼에 3이 들어간다.
    5. 5번째 버퍼에 3+3=6 이 들어간다.
    6. 즉 101번을 루프를 돌면 5050이라는 값이 나와야 한다.
  2. 4개의 int형 버퍼중에, 이전 결과값이 저장된 버퍼를 제외한 3개를 할당한다.
    1. 1번을 반복한다.
  3. 2개의 버퍼가 모두 찼다면, 이걸 다시 더해서 남는 버퍼에 넣는다.

#include <iostream> 
#include <vector>
#include <stdlib.h>
#include <unistd.h>

using namespace std;

#define BUFSIZE 4

class Buff
{
private:
int currentIdx;
vector<int *> LBuf;
int *tmpptr;

public:
Buff()
{
currentIdx = 0;
}

void Set(int *a)
{
LBuf.push_back(a);
}

void Clear()
{
LBuf.clear();
}

// Merged Sort를 한다.
// 여기에서는 + 연산을 한다.
void SetUnion(int a)
{
int current = (currentIdx+1)%3;
int prev = (currentIdx)%3;
int tmp = (currentIdx+2)%3;

if (*LBuf[prev] != -1)
{
currentIdx++;
}
*LBuf[current] = a;
*LBuf[tmp] = a + *LBuf[prev];
printf("Debug %d + %d = %d\n", *LBuf[current], *LBuf[prev], *LBuf[tmp]);
currentIdx ++;

tmpptr = LBuf[(currentIdx)%3];
}

// 결과가 저장된 버퍼의 레퍼런스를 넘겨준다.
int *GetSumResult()
{
return tmpptr;
}

int TmpIdx()
{
return currentIdx;
}
};

class BufManager
{
private:
int *a;
vector<Buff> NodeBuf;
int checkIdx;

public:

BufManager()
{
// 버퍼를 4개 할당한다.
a = (int *)malloc(sizeof(int) * BUFSIZE);
checkIdx = 3;
}

void Run()
{
Buff lBuff1, lBuff2;

// 각각의 Buff1에 3개씩의 버퍼의 레퍼런스를 할당한다.
for (int i = 0; i < BUFSIZE; i++)
{
a[i] = -1;
if (&a[i] != &a[checkIdx])
{
lBuff1.Set(&a[i]);
}
}
NodeBuf.push_back(lBuff1);

for (int i = 0; i < BUFSIZE; i++)
{
a[i] = -1;
if (&a[i] != &a[checkIdx])
{
lBuff1.Set(&a[i]);
}
}
NodeBuf.push_back(lBuff2);

int idxflag = 0;
int idx;

// 2개의 그룹이 있다고 가정하고 루프를 돌렸다.
for (int i = 0; i < 2; i++)
{
Buff Bf;
idxflag = i % 2;
printf("Node Number %d\n", idxflag);
getchar();

// 101번의 merged Sort가 이루어진다고 가정한다.
for (int k = i; k < 101+i; k++)
{
NodeBuf[idxflag].SetUnion(k);
}

// merged Sort가 끝났다면, 버퍼를 초기화 한다.
NodeBuf[idxflag].Clear();
checkIdx = NodeBuf[idxflag].TmpIdx();

// 2개의 버퍼에 결과값이 만들어졌다면, 이 두개를 다시 Merged sort한다.
// 아래의 코드는 SetUnion 메서드를 사용하도록 바꿀 수 있을 것이다.
if (i > 0)
{
printf("RESULT : %d : %d\n", *NodeBuf[0].GetSumResult(), *NodeBuf[1].GetSumResult() );
getchar();
}

// 다음 Level의 Merged Sort를 위해서 3개의 메모리의 레퍼런스를 할당한다.
// 이때 이전에 결과값이 있는 메모리는 사용하지 않도록 한다.
for (int k = 0; k < BUFSIZE; k++)
{
// 주소값이 같지 않을 경우에만 할당한다.
if (&a[k] != NodeBuf[idxflag].GetSumResult())
{
a[k] = -1;
printf("SET %d\n", k);
Bf.Set(&a[k]);
}
}
NodeBuf[idxflag?0:1] = Bf;
printf("Current Result : %d\n", *NodeBuf[idxflag].GetSumResult());
getchar();
}
}
};

int main(int argc, char **argv)
{
BufManager *MyBuff;
MyBuff = new BufManager();
MyBuff->Run();
}

약간 수정된 코드

동일하게 SetUnion 메서드를 사용하도록 약간 수정했다.
#include <iostream> 
#include <vector>
#include <stdlib.h>
#include <unistd.h>

using namespace std;

#define BUFSIZE 4

class Buff
{
private:
int currentIdx;
vector<int *> LBuf;
int *tmpptr;
public:

Buff()
{
currentIdx = 0;
}

void Set(int *a)
{
LBuf.push_back(a);
}

void Clear()
{
LBuf.clear();
}

void SetUnion(int a)
{
int current = (currentIdx+1)%3;
int prev = (currentIdx)%3;
int tmp = (currentIdx+2)%3;

if (*LBuf[prev] != -1)
{
currentIdx++;
}
*LBuf[current] = a;
*LBuf[tmp] = a + *LBuf[prev];
printf("Debug %d + %d = %d\n", *LBuf[current], *LBuf[prev], *LBuf[tmp]);
currentIdx ++;

tmpptr = LBuf[(currentIdx)%3];
}

int *GetSumResult()
{
return tmpptr;
}
int TmpIdx()
{
return currentIdx;
}
};

class BufManager
{
private:
int *a;
vector<Buff> NodeBuf;
int checkIdx;

public:

BufManager()
{
a = (int *)malloc(sizeof(int) * BUFSIZE);
checkIdx = 3;
}

void Run()
{
Buff lBuff1, lBuff2;

for (int i = 0; i < BUFSIZE; i++)
{
a[i] = -1;
if (&a[i] != &a[checkIdx])
{
lBuff1.Set(&a[i]);
}
}
NodeBuf.push_back(lBuff1);

for (int i = 0; i < BUFSIZE; i++)
{
a[i] = -1;
if (&a[i] != &a[checkIdx])
{
lBuff1.Set(&a[i]);
}
}
NodeBuf.push_back(lBuff2);

int idxflag = 0;
int idx;
for (int i = 0; i < 3; i++)
{
Buff Bf;
idxflag = i % 2;
printf("Node Number %d\n", idxflag);
getchar();
for (int k = 0; k < 10; k++)
{
NodeBuf[idxflag].SetUnion(k);
}

NodeBuf[idxflag].Clear();

checkIdx = NodeBuf[idxflag].TmpIdx();
if (i > 0)
{
int another = idxflag?0:1;

printf("RESULT : %d : %d\n", *NodeBuf[idxflag].GetSumResult(), *NodeBuf[another].GetSumResult() );
NodeBuf[idxflag].SetUnion(*NodeBuf[another].GetSumResult());
printf("Current Result : %d\n", *NodeBuf[idxflag].GetSumResult());
getchar();
}

for (int k = 0; k < BUFSIZE; k++)
{
if (&a[k] != NodeBuf[idxflag].GetSumResult())
{
a[k] = -1;
Bf.Set(&a[k]);
}
}
NodeBuf[idxflag?0:1] = Bf;

//printf("Current Result : %d\n", *NodeBuf[idxflag].GetSumResult());
getchar();
}
}
};

int main(int argc, char **argv)
{
BufManager *MyBuff;
MyBuff = new BufManager();
MyBuff->Run();
}

결론

버퍼풀을 이용한 merged sort는 유연하게 확장가능하다는 장점이 있다. 검색엔진을 예로 들자면, 어절이 분리될 경우, 2개 이상의 트리를 가지게 될건데, 이렇게 다수의 트리를 가지는 데이터들을 merged sort 하길 원한다면, 5개의 버퍼를 가지는 풀을 유지하면 될 것이다.

:::
2008/01/15 10:27

linux man page - fopen : 파일 stream을 오픈한다.


1장. fopen(3)

차례
1.1. 사용법
1.2. 설명
1.3. 반환값
1.4. 에러
1.5. 예제
1.6. 참고문헌

파일로 부터 스트림을 생성한다.


1.1. 사용법

#include <stdio.h>

FILE *fopen(const char *path, const char *mode);
FILE *fdopen(int fildes, const char *mode);
FILE *freopen(const char *path, const char *mode, FILE *stream);


1.2. 설명


1.3. 반환값


fopen 은 종종 open(2) 함수와 비교되어서 설명된다. byte 단위로 열린파일을 다루는 open 과는 달리 fopen 는 stream 단위로 열린파일의 데이타를 다룬다. 즉 fopen 은 자체적으로 버퍼를 관리함으로써, 함수 사용자가 저수준에서 다루어야할 많은 것들을 대신 해결해 줌으로 좀더 쉽게 사용할수 있다.

path 에 지정된 파일을 mode 에 주어진 모드로 연다.

이 함수는 fputs(3), fgets(3), ungetc(3), getc(3) 등과 같이 쓰이며 표준 I/O(입출력) 함수라고 부른다.

다음과 같은 mode 를 path 에 대해서 지정해줄수 있다.

r

파일을 읽기 전용으로 연다. stream 포인터의 위치는 파일의 시작위치를 가리킨다.

r+

파일을 읽기/쓰기로 연다. stream 포인터의 위치는 파일의 시작위치를 가리킨다.

w

쓰기전용의 파일을 열기 위해서 사용된다. 열린 파일크기는 0 이되며, stream 포인터의 위치는 파일의 시작위치를 가리킨다.

w+

파일을 읽기/쓰기로 연다. 파일이 이미 존재한다면, 파일의 크기는 0이 된다. stream 포인터의 위치는 파일의 시작위치를 가리킨다.

a

쓰기위해서 파일을 연다. 파일이 존재하지 않는다면 새로 생성한다. stream 포인터의 위치는 파일의 시작위치를 가리킨다.

a+

파일을 읽기/쓰기로 연다. 파일이 존재하지 않는다면 새로생성한다. stream 포인터의 위치는 파일을 끝을 가리킨다.

fdopen(3)함수는 파일지정자 fildes로 부터 스트림을 얻어온다. mode는 fopen에 사용하는 것들을 그대로 사용할 수 있다. 그렇지만 파일지정자의 mode와 조화를 이룰 수 있어야 한다.

freopen(3)함수는 streampath파일과 연결 한다. 이때 최초의 stream은 닫히게 된다. mode는 fopen에서 사용하는 것과 동일하다. 이함수는 stderr, stdin, stdout 등을 파일과 연결시키고자 할때 주로 사용한다.


1.4. 에러

EINVAL

잘못된 mode를 지정했을 경우

이 외의 에러는 open(2)와 동일하다.

1.5. 예제

#include <stdio.h>
#include <unistd.h>

int main(int argc, char **argv)
{
FILE *fp;

fp = fopen("/home/test/test.txt", "r");
if (fp == NULL)
{
perror("File open error: ");
exit(0);
}
// 표준 I/O 함수를 이용한 여러가지 작업을 한다.

fclose(fp);
return 0;
}


1.6. 참고문헌

  1. open(2)

  2. fopen(3)

  3. fileno(3)

  4. stdio.h 사용하기

:::
2008/01/04 16:16

프로그래머의 유형 - 당신은 어느 유형 ?

DC 3대 개념갤 중 하나인 프겔 눈팅중 발견한 글입니다. 어느 타입에 속하는지 생각해보세요. 저는 대략 코드카우보이..

간달프(Gandalf)

이 프로그래머 타입은 ‘반지의 제왕’에 나오는 마법사 간달프와 닮았다. 이 타입의 외관은 턱수염을 기르고, 이상한 모자를 쓰고, 겨울에 망토 같은 외투를 입을지도 모르며, 좋게 보면 간달프와 같은 마법으로 팀을 위하고, 안 좋은 면은 팀원들이 간달프가 눈길을 걸어올라 오는 시간을 기다리듯이 그가 전산실에 오는 시간을 오랫동안 기다려야 한다는 것이다.

이런 타입은 실력이 아주 뛰어난 중요한 인물이지만 보통은 같이 일하기를 꺼려한다. 하지만 문제가 해결이 안 될 때는 간달프의 마법이 필요하듯 이런 타입의 도움도 필요한 법이다.

순교자

다른 업종에서는 순교자(The Martyr)는 워커홀릭이다. 하지만 개발 분야에서 순교자는 그 차원을 넘어 선다. 워커홀릭은 최소한 집에 가서 샤워하고 잠은 자기 때문이다. 순교자 타입은 다 먹은 피자 박스에 둘러싸인 책상에 엎드려 자는 것을 자랑스럽게 생각한다.

문제는 아무도 이렇게 일하는 것을 원하지 않았다는 것이다. 순교자 타입은 다른 팀원에게 부담스러운 말을 한다. “먼저들 들어가. 저녁 맛있게 먹고…. 나는 오늘밤에 3주 동안 해야 할 코딩을 모두 하고 들어갈래.”라는 식으로 말이다.

팬보이


팬보이(Fanboy) 는 조심해야 한다. 이런 사람이 여러분 주의에 있다면, 그는 드래곤볼 Z와 건담 윙 중 어느 것이 재미있는지 또는 플레이스테이션3와 X박스 360중 어느 것이 더 좋은지에 관해 과장 좀 해서 3시간 동안은 이야기를 들어야 할 것이다.

팬보이의 책상 주변에는 일본에서 수입한 액션 피규어, 포스터 또는 장식품 등을 진열해 놓았을 것이다. 이들은 자신들이 가지고 있는 장식품을 가지고 생각하는 것을 좋아해서, 그 생각에 시간을 많이 허비한다. 이런 타입은 가끔 무엇 때문에 채용을 했는지 모를 때가 가끔 있다.

빈스 (Vince Neil): 미국 밴드 머틀리 크루의 리드싱어

마치 1984년으로 돌아간 것 같은 타입이다. 긴 머리카락, 찢어진 청바지에 큰 스카프를 목에 두르고 업무 시간 동안 본 조비, 데프 레퍼드(Def Leppard)와 같은 음악을 따라 흥얼거리며 일한다. 빈스 타입은 일반적으로 재미있고 경험도 많지만 발전이 없다. 게다가 힙합 스타일과 아웅다웅할지도 모른다. 이런 타입과 매일 일하는 것은 꽤 힘들 것이다.

닌자

닌자 타입은 여러분 팀의 MVP이지만, 아무도 누구인지 모른다는 것이다. 전설적인 자객처럼, 닌자 타입은 일을 하는 건지 안 하는 건지 모르지만 아침이 되면 결과물이 나와 있는 것을 발견할 수 있다.

여러분이 소스 제어 시스템을 가동하고, 새벽 4시에 한번 확인 해보라. 여러분은 닌자가 그 프로젝트를 알고 있을 것이라고 생각하지도 않았지만, 여러분이 일주일 동안 작업한 계획의 문제를 코드 레벨에서 확인하고 알려 두었을 것이다. 여러분이 다른 회의에 참석해 있을 때, 닌자 타입은 일을 하고 있을 테니 확인해 보라.

닌자 타입은 아주 비밀스럽게 일을 한다. 여러분은 그 사람의 이름조차 모르지만 모든 프로젝트마다 아주 깔끔하게 정리되어 있는 것을 볼 수 있다. 이런 타입은 신중하게 다가가야 한다. 이런 사람을 조직 내에서 순위를 매기거나 파일로 업무를 관리하려고 하지 마라. 닌자 타입은 혼자 일하는 전사이며, 관리 당하는 것을 싫어한다.

이론가(The Theoretician)

이 타입은 프로그래밍에 관해 알아야 하는 모든 것을 알고 있다. 이 타입은 애매한 프로그램 언어의 역사에 관해 4시간 정도 떠드는데 시간을 소비하거나 어떻게 프로그래밍 하면 런타임을 줄이고, 최적화 프로그래밍을 할 수 있는지에 대해 시간을 허비할 수 있다.

문 제는 이런 타입은 소프트웨어 개발에 관한 것을 알지 못하고 있다는 것이다. 이론가 타입이 코딩을 하면 정말 말도 안 되게 ‘엘레강스’하다. 또 좋아하는 기술은 ‘반복’이며, 모든 코드는 최대한 꼬여 있어 읽는 데 시간이 많이 걸린다.

이런 타입은 주의가 산만해 쉽게 다른 일에 관심을 돌린다. 몇 시간이면 개발할 수 있는 일을 이런 타입은 석 달은 족히 걸린다. 왜냐하면 기존의 툴은 충분하지 않다며 새로운 라이브러리를 만들어 새로운 툴을 만들어 사용하려고 하기 때문이다.

이런 타입은 잘만 컨트롤 하면 아주 잘 활용할 수 있다. 프로젝트에서 정확히 할 일에 대한 범위를 정해 주고 다른 일에 시간을 허비하지 못하게 한다면 말이다.

코드 카우보이(The Code Cowboy)

이 타입은 절대 스스로 멈추는 법이 없다. 이런 타입은 거의 항상 최고의 프로그래머이며, 다른 사람보다 두세배는 빠르게 일을 할 수 있다. 하지만 문제는 그렇게 빠르게 하는 일의 반을 대충 한다는 것이다. 소스 컨트롤 하는 코드 확인에 시간이 걸리고, 외부 컨피규레이션 데이터 저장에 시간이 걸리고, 다른 사람과 대화중에 생각을 이해하는 데도 시간이 걸린다.

이 타입의 코드는 스파게티처럼 혼란스럽다. 이유는 프로그래밍 하면서 리팩토링 하는 것이 절대 일어나지 않게 빨리 하기 때문이다. 프로그래밍 책에 예제로 되어 있는 “이렇게 하지 마세요”라고 7페이지에 걸쳐 중요하게 설명되어 있는 것과 비슷하게 프로그래밍을 했지만 신기하게도 프로그램은 돌아간다.

코드 카우보이 타입은 다른 사람과 함께 일을 잘 하지는 못한다. 그리고 여러분이 이런 타입 두 명을 같은 프로젝트에 투입시키면, 서로의 변화에 대해 인정을 하지 않고 싸우기 때문에 확실히 실패 한다.

이 타입은 정확하게 해야 하는 프로젝트보다 납기 일정이 더욱 중요한 프로젝트에 투입하는 것이 좋고, 코드는 항상 일정 전에 완성되어 있을 것이다. 코드 카우보이는 ‘시끄러운 닌자 버전’이라고 보면 된다. 닌자가 정교한 외래 수술을 하는 것에 비유한다면, 코드 카우보이는 성난 소처럼 저돌적으로 자신의 길을 달려가는 것에 비유할 수 있다.

공수부대요원(The Paratrooper)

여러분은 영화에서 적지 깊숙이 침투하여 비밀스럽게 업무를 수행하는 특공대 요원을 본적이 있을 것이다. 이 타입은 소프트웨어 개발 세계에서 ‘공수부대 요원’이라고 한다. 이 요원은 다 죽어 가는 프로젝트를 살리기 위해 마지막으로 보내는 프로그래머이다.

이 요원은 장기 프로젝트에 대해서는 부족하지만, 그들의 최대 자산은 친숙하지 않는 코드를 배워서 작업을 하는 불가사의한 능력이다. 다른 프로그래머들은 이러한 것을 충분히 배워서 프로젝트를 실행하는 데 몇 주 내지 몇 달이 걸릴지도 모르지만, 공수 부대 요원들은 몇 시간 또는 하루 정도면 충분하다.

이 요원들은 그 코드의 핵심을 알 정도로 배우지는 못하지만, 전체의 팀이 실패할지도 모르는 곳에서 성공할 수 있는 것들을 찾아낼 수 있다.

보통사람(Mediocre Man)

"충분히 좋다"라고 듣는 것이 이 ‘보통사람’ 타입에게서 들을 수 있는 최고의 찬사이다. 이 이름에 속지 말아라. ‘보통사람’이라는 타입에는 엄청난 다양함이 있다. 그리고 이들은 다른 팀원들보다 더 나쁜 코드를 만드는 데 시간이 더 걸린다.

이 타입들의 특징은 느리고 침착하게 하지만 프로젝트가 언제 끝날지 모르며, 회사에 오랫동안 일을 하기 위해 항상 “충분히 좋다”라는 슬로건을 외친다.

이 런 타입을 인터뷰 할 때 그들은 많은 프로젝트에 관여한 사실을 여러분에게 이야기 하겠지만 실제로 관여한 프로젝트는 많지 않다. 이러한 타입을 알아내는 것은 쉬운데 그들이 한 일에 대한 자세한 질문을 하면 아마 갑자기 건망증 증세를 보일 것이다. 이런 타입을 채용한다면 퇴사시키는 데 몇 년은 걸릴 것이다.

이반젤리스트(The Evangelist)

여러분이 어떤 환경에 처해 있더라도, 이반젤리스트는 지금 사용하고 있는 툴, 프로세스를 버리고 다른 것들로 대체해 업무 향상을 할 수 있다고 주장한다. 이반젤리스트는 실제로 이론가(The Theoretician)와 정반대이다. 이들은 솔직하고, 소프트웨어 개발에 관하여 많이 알지만 실질적으로 프로그래밍 작업은 거의 하지 않는다.

이들은 자신이 프로젝트 매니저나 부서장이라고 마음속으로 생각하고 있으나 지식이나 프로젝트 경험은 부족하다. 따라서 이들은 순수하게 경영자 역할을 할 수 있으므로 다른 사람들은 이들이 변혁을 시도하는 것을 참을 필요가 있다. @
:::
2007/12/18 01:31

joinc와 함께하는 리눅스 시스템 프로그래밍 3장 - 입출력 완성?

.. 어언 3주만에 한장을 완성하는 군요. 역시 귀차니즘의 압박이란.. 쩝..

1 소개

기계는 조작자의 입력을 받아서, 프로그래밍 된데로 일을 하고 그 결과물을 출력한다. 믹서기는 사과를 입력받고 버튼을 누르면, 프로그래밍 된데로 모터를 돌려서 사과를 잘개 쪼개고 그 결과물로 사과쥬스출력한다.

컴퓨터는 정보를 처리하기 위한 기계로 입력을 받아서 처리하고 그 결과를 출력한다는 점에서 봤을 때, 근본적으로 믹서와 다를 바가 없다. 믹서와 다른 점이라면 입력으로 사과 대신 (비트로 이루어진)정보를 입력받아서 처리하고 그 결과물로 정보를 출력한다는 점 정도일 것이다.

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

system2_2.png

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

2 모든건 파일이다

유닉스에서는 모든걸 파일로 취급한다. 하드디스크에 존재하는 파일, 디렉토리는 물론이고 네트워크카드, 사운드카드, 키보드, 마우스, 하드디스크 그 자체 까지 몽땅 파일로 취급한다.

유닉스 시스템을 처음 접할때 꽤나 혼동되는 부분이기도 하다. 윈도우에는 파일은 단지 하드디스크상에 존재하는 논리적인 정보의 집합을 그 대상으로 하기 때문이다. 예를 들자면 하드디스크는 C: D:와 같은 파일이 아닌 장치로 인식한다.

그러나 유닉스 시스템에서는 장치도 파일로 취급된다. 리눅스도 유닉스와 동일한 파일시스템을 가지고 있으므로, 리눅스를 예로 들어서 설명하겠다. 리눅스에서 하드디스크는 /dev/hda1, /dev/hda2 이런식으로 하드디스크상의 파일로 존재한다. 뿐만 아니다. 사운드 카드는 /dev/dsp, 프린트는 /dev/lp, cdrom은 /dev/cdrom 의 이름을 가진 파일로 존재한다.

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

system2.png

결국 프로그래머는 복잡한 장치제어와 관련된 학습을 최소화 하면서, 파일을 사용하는 것처럼 여러 장치들을 사용할 수 있게 된다.

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 장치 프린터, 사운드카드, cdrom 등의 장치
s 소켓 프로세스간 통신에 사용
p pipe 파이프

4 파일 열기

파일을 다루는 기본적인 흐름은 다음과 같다.
  1. 파일을 연다.
  2. 열려진 파일에서 데이터를 읽거나, 데이터를 쓴다
  3. 모든 작업이 끝났다면, 파일을 닫는다.

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

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

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

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

open(2)함수는 다음과 같이 선언되어 있다.
#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를 리턴하게 한다. 파일을 덮어쓰거나 하는 실수를 방지하기 위한 용도로 사용할 수 있다.

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

open(2)을 이용해서 파일을 성공적으로 열었다면, 이후의 모든 쓰기/읽기 등의 작업은 파일이름 대신 파일지정번호를 이용하게 된다.
  +------+ 
| FILE |<---- file discriptor = open(2)
| |
+------+
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;
}


5 파일에서 읽기

파일을 성공적으로 열었다면, 이제 읽거나 쓰는 등의 작업을 하면 된다. 여기에서는 파일을 읽는 법에 대해서 알아보도록 할 것이다. 우선 다음의 내용을 가지는 셈플 파일을 하나 준비하도록 하자. 우리는 아래의 파일의 내용을 읽어서 화면에 출력하는 프로그램을 작성하는 것으로 파일읽는 법에 대해서 배울 것이다. 파일의 이름은 fly.txt로 하겠다.
Fly me to the moon And let me play among the stars 
Let me see what spring is like on Jupiter and Mars
In other words hold my hand
In other words darling kiss me
유명한 fly me to the moon가사중 일부분이다. 어떤 노래인지 궁금하다면 머리도 식힐겸 여기를 방문해 보기 바란다.

파일에서 읽기 위해서는 당연히 읽기전용 혹은 읽기/쓰기 상태로 파일이 열려야 한다. 여기에서는 읽기 모드로 열도록 할것이다. 만들어진 파일을 여는 것이기 때문에 O_CREAT는 필요가 없을 것이다. 파일을 생성하는 것이 아니기 때문에 모드 인자역시 필요없다. open 함수는 다음과 같이 사용할 수 있을 것이다.
  fd = open("fly.txt", O_WRONLY); 
if (fd < 0)
{
...
}

성공적으로 파일을 열었다면 fd는 0보다 큰수가 리턴되었을 것이고, - 대부분의 경우 2보다 큰수가 리턴될 것이다. 이 이유는 아래에서 설명할 것이다 - 리턴받은 정수를 파일지정번호로 사용하게 된다. 우리는 이 파일지정번호를 이용해서 파일의 내용을 읽어들이게 된다.

5.1 read 시스템콜

열린 파일로 부터 데이터를 읽기 위해서 제공하는 시스템함수가 read(2)이다. 이 함수는 인자로 주어진 파일지정번호가 가리키는 파일로 부터, 지정된 크기만큼의 데이터를 읽어들이게 된다. 다음은 read(2) 함수의 원형이다.
#include <unistd.h> 

size_t read(int fd, void *buf, size_t count);
  1. fd : open(2)으로 열린 파일을 가리키는 파일지정번호
  2. buf : 읽어들인 데이터를 저장할 공간
  3. count : 읽어들일 데이터의 크기로 byte 단위

함수는 단순하며 직관적이다. read 함수는 성공적으로 실행될 경우 0보다 큰수를 리턴한다. 파일의 끝에 다다라서 더이상 읽어들일 데이터가 없다면 0을 리턴한다. read 함수를 이용하는 일반적인 방법은 루프를 돌면서 리턴값이 0이 될때까지 - 즉 파일의 끝을 만날 때까지 - 데이터를 읽어들이는 것이다. 다음과 같은 형태로 사용할 수 있을 것이다.
int readn = 0; 
int fd;
char buf[80];
fd = open(...);

memset(buf, 0x00, 80);
while( (readn = read(fd, buf, 79) )
{
// 읽어들인 데이터가 있는 buf를 이용해서 필요한 작업을 한다.
memset(buf, 0x00, 80);
}

주의해야할 점은 데이터가 저장되는 buf를 memset(3) 함수를 이용해서 초기화 시켜줘야 한다는 점이다. read 함수는 count만큼 데이터를 읽어들여서 buf에 복사하기만 할뿐, 내용을 초기화 시키기 않기 때문이다. 예를들어 이전에 79byte를 읽어들였고, 이번에 읽어들인 데이터가 20byte였다면, 21byte 이후의 이전 데이터가 그대로 남아 있어서 잘못 처리할 수 있기 때문이다. 물론 read의 리턴값을 이용해서 20byte를 읽어왔다는 것을 알 수 있으므로 주의해서 처리하면 되긴 하겠지만 실수할 만한 여지는 미리 제거하는게 좋을 것이다.

아래의 프로그램을 실행시켜 보기 바란다. 프로그램의 이름은 fly.c로 하자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24



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

#define MAXLEN 80
int main()
{
int fd;
int readn = 0;
char buf[MAXLEN];
fd = open("fly.txt", O_RDONLY);
if (fd < 0)
{
perror("file open error:");
return 1;
}
memset(buf, 0x00, MAXLEN);
while( (readn = read(fd, buf, MAXLEN-1 )) > 0)
{
printf("%s", buf);
}
}
프로그램을 실행시켜 보면, 마지막에 다음과 같이 이전에 읽어들인 값이 출력되는 것을 볼 수 있을 것이다.
Fly me to the moon And let me play among the stars 
Let me see what spring is like on Jupiter and Mars
In other words hold my hand
In other words darling kiss me
on Jupiter and Mars
In other words hold my hand
...

이제 21줄 다음에 memset(buf, 0x00, MAXLEN); 을 추가하고나서 다시 실행시켜 보도록 하자. 문제없이 깔끔하게 출력되는걸 확인할 수 있을 것이다.

또하나 코드에서 궁금한점이 있을 것이다. read 에서 버퍼의 최대크기인 MAXLEN 만큼을 읽어들이지 않고 MAXLEN-1 만큼을 읽어들이는 점이다. 이는 역시 버퍼의 크기를 넘어서서 데이터를 읽어버리는 만약의 실수를 막기 위함이다. printf(3) 함수의 경우 널문자('\0')를 만나기 전까지 데이터를 읽어들이게 된다. 버퍼를 가득채워서 읽어들였는데, 버퍼메모리 영역의 마지막이 '\0'이 아닐 경우 끝이 아니라고 판단해서, '\0'을 만날때까지 메모리영역을 벗어나서 계속 읽어 버리는 문제가 발생할 수 있기 때문이다. 그러하니 버퍼의 마지막라인을 '\0'으로 만들어 버리는게 깔끔하다.
  01 ...  89 80 
+-------+--+--+--------------------------+
| ..... | | | '\0'이 아닌 알수 없는 값 |
+-------+--+--+--------------------------+
|<--- buf --->|

이 경우에도 읽어들인 데이터의 크기를 알 수 있기 때문에 snprintf()와 같은 함수를 이용해서 문제를 해결할 수 있을 것이다. 그렇지만 가능한 문제 발생 여지를 없애는 쪽으로 코딩을 하는게 좋을 것이다.

그렇다고 해서, 모든 경우에 있어서 문제가 되는건 아니다. 파일로부터 읽어들일 데이터가 char, int 와 같은 원시데이터 타입일 경우에는 크기가 명확히 명시되므로 위에서와 같은 초기화 관련된 문제가 발생하지 않는다.

5.2 버퍼공간의 크기

buffer가 사용되는 일반적인 이유는 잡음을 없애고 성능을 높이기 위함이다. 읽어들일 데이터가 1024 만큼이 있다고 가정해보자. 버퍼의 크기를 1로 잡았다면, read(2) 함수를 1024번 호출해야 될것이다. 만약 버퍼의 크기를 512로 잡는다면, 단 2번만 read(2)함수를 호출하면 될 것이다. 후자가 더 효율적일 거라는 것은 분명하다.

그렇다고 해서 무작정 메모리를 크게잡는 것도 낭비다. 시간과 비용이 관련된 대부분의 현상이 그렇듯이 어느정도 크기가 지나면 성능의 증가폭이 줄어드는 지점이 오기 때문이다. 적당한 선에서 트레이드오프 해야할 필요가 있다.

어떤 데이터를 처리하느냐에 따라다르겠지만 512byte1024byte정도의 크기로 하는게 무난하다고 알려져 있다.

6 파일에 쓰기

파일에 데이터를 쓰기 위해서는 쓰기전용 혹은 읽기/쓰기 가능모드로 열어야 한다. 리눅스 커널은 쓰기요청을 위한 write(2) 함수를 제공한다.
#include <unistd.h> 

ssize_t write(int fd, const void *buf, size_t count);
이 함수는 buf에 있는 내용을 count 크기만큼 파일지정번호 fd가 가리키는 파일에 쓸 것을 커널에 요청한다. 성공하게 되면은 쓴 byte 크기만큼을 리턴한다.

다음은 data.txt 파일을 열어서 int형 데이터를 쓰는 프로그램이다. 이 프로그램의 이름은 write.c로 하겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <fcntl.h> 
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main()
{
int fd;
int i;
int wdata = 0;
int wsize = 0;
fd = open("data.txt", O_CREAT|O_WRONLY);
if (fd < 0)
{
perror("file open error");
return 1;
}

for (i = 0; i < 100; i++)
{
wdata = i * 2;
wsize = write(fd, (void *)&wdata, sizeof(int));
printf("Write %d (%d byte)\n", i*2);
}
close(fd);
}

22줄을 주의깊게 살펴보도록 하자. fd에 int형 변수인 wdata에 저장된 값을 쓰려고하고 있다. wdata는 int형 데이터이기 때문에, void *형으로 형변환을 했다. 마지막으로 sizeof 함수를 이용해서 쓰고자 하는 데이터의 크기를 구해서, write의 3번째 인자로 되돌려 줬다. int 데이터 타입의 크기는 4byte라는 것을 이미 알고 있기 때문에, sizeof를 쓰지 않고 4를 직접 명시해도 될 것이다.

그러나 어떤 운영체제컴파일러의 환경에 따라서 int가 2byte 혹은 8byte가 되는 경우도 있다. 그러므로 이식성을 고려한다면 sizeof 함수를 이용해서 데이터타입의 크기를 얻어내는 방법을 사용하는걸 권장한다.

프로그램을 컴파일 하고 실행시키면 다음과 같은 결과를 볼 수 있을 것이다.
# gcc -o write write.c 
# ./write
Write 0 (4 byte)
Write 2 (4 byte)
Write 4 (4 byte)
Write 6 (4 byte)
Write 8 (4 byte)
ls로 data.txt 파일이 생성된걸 볼 수 있을 것이다. 이 파일의 내용을 살펴보기 위해서 vi로 열어도 내용을 알아볼 수는 없을 것이다. 이 파일의 내용은 ASCII printable 데이터 - 흔히 말하는 문자데이터 - 가 아니기 때문이다.

7 파일 닫기

열린 파일을 더이상 쓰지 않는다면, 닫아주어야 한다. 그렇지 않을경우 프로그램이 종료될때까지, 계속 남아서 컴퓨터 시스템의 자원을 소비하게 된다. 파일의 종료는 close(2) 함수를 이용하면 된다.
#include <unistd.h> 

int close(int fd);

8 원시 데이터타입의 데이터와 구조체데이터 읽고 쓰기

그럼 바로 위에서 다룬 data.txt에 저장된 int형 정보를 읽어서 화면에 출력하는 프로그램을 작성해 보도록 하자. 프로그램의 흐름은 간단하다. data.txt 를 읽기전용 으로 연다음 read(2) 함수를 이용해서 4byte씩 읽어들인다. 읽어들인 데이터는 printf(3) 함수를 이용해서 화면에 출력하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdlib.h> 
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define MAXLEN 80
int main()
{
int fd;
int readn = 0;
int buf;
fd = open("data.txt", O_RDONLY);
if (fd < 0)
{
perror("file open error:");
return 1;
}
while( (readn = read(fd, (void *)&buf, sizeof(buf) )) > 0)
{
printf("Read Data : %d\n", buf);
}
}
fly.c와 달라진 점은 19줄 정도다. 읽을 정보가 int형 데이터 이므로, int형 데이터타입의 크기만큼 - 대부분의 경우 4byte일 것이다 - 읽어서 buf에 복사하고 있다. 읽어들인 정보는 21줄에서 print를 통해서 출력을 했다.

이 프로그램을 실행시키면 다음과 같은 결과를 보여줄 것이다.
# ./read 
Read Data : 0
Read Data : 2
Read Data : 4
Read Data : 6
Read Data : 8
...

문자열 데이터이든지 아니면 int 형 숫자 데이터이든지간에 컴퓨터의 입장에서는 비트의 나열일 뿐임을 명심할 필요가 있다. 단지 표현의 차이을 뿐이다. 똑같은 비트의 나열이라도 문자열로 표현하고자 하면 문자열이 되는 거고, 숫자로 표현하고자 하면 숫자로 펴현된다.

8.1 좀더 복잡한 구조체 데이터 읽고 쓰기

컴퓨터 관점에서 모든 데이터는 동일한 bit의 나열일 뿐이라는걸 이해한다면, 구조체를 읽고 쓰는 것 역시 전혀 문제될게 없다. 대부분의 고수준 응용 프로그램이라면 문자열이나 int, long과 같은 원시데이터타입 보다는 구조체 데이터를 읽고 쓰도록 되어 있다.

유저정보를 저장하고 읽는 간단한 프로그램을 작성한다고 가정해보자. 하나의 유저를 나타내기 위해서 필요한 정보는 다음과 같다고 가정해보자.
이름 
나이
성별
취미
프로그래머는 대략 다음과 같은 구조체를 사용할 것이다. 구조체는 데이터를 레코드 단위로 관리할 수 있도록 도와주기 때문이다.
struct userInfo 
{
char name[28];
int age;
int sex;
char hobb6[28];
};

그럼 위의 구조체 정보를 저장하는 간단한 유저정보 과리프로그램을 만들어 보도록 하자. 제대로 만들려고 하면, 각 구조체변수 데이터를 입력받기 위환 인터페이스까지 만들어야 겠지만 생략하도록 할 것이다. 이 프로그램은 뒷부분에서 사용자 입력을 받을 수 있도록 확장될 것이다.

테스트를 위해서 읽기와 쓰기를 전담하는 2개의 프로그램을 작성할 것이다. 우선 쓰기 프로그램이다. 이 프로그램의 이름은 UserInfoWrite.c 로 하겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
nclude <stdlib.h> 
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>

struct userInfo
{
char name[28];
int age;
int sex;
char hobby[28];
};

void makeUserInfo(struct userInfo *uinfo,
char *name, // 이름
int age, // 나이
int sex, // 성 (남: 0, 여: 1)
char *hobby) // 취미
{
memset((void *)uinfo, 0x00, sizeof(struct userInfo));
strcpy(uinfo->name, name);
uinfo->age = age;
uinfo->sex = sex;
strcpy(uinfo->hobby, hobby);
}

int main()
{
int fd;
struct userInfo myAddrBook;
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;
}

makeUserInfo((void *)&myAddrBook, "yundream", 19, 0, "프로그래밍");
write(fd, (void *)&myAddrBook, sizeof(myAddrBook));

makeUserInfo((void *)&myAddrBook, "hello", 22, 1, "게임");
write(fd, (void *)&myAddrBook, sizeof(myAddrBook));

makeUserInfo((void *)&myAddrBook, "드라고너", 33, 1, "사냥");
write(fd, (void *)&myAddrBook, sizeof(myAddrBook));

close(fd);
return 0;
}

유저정보와 유저정보와 유저정보 구조체인 userInfo를 넘기면, 구조체를 체우는 makeuserInfo라는 함수를 만들었다. 현재는 프로그램상에 직접 유저정보를 집어 넣었지만 나중에는 키보드로 입력받을 수 있도록 할 것이다.

40 ~ 47에서 유저정보 구조체 myAddrBook을 파일에 쓰고 있다. int형데이터 char 형 데이터를 쓰는 것과 발다를게 없음을 알 수 있다. 컴퓨터의 입장에서는 int형 데이터이든지 char 형 데이터이든지 간에 비트의 나열일 뿐임으로 근본적으로 다를게 없기 때문이다. 몇바이트의 정보를 저장할 것인지에 대한 저장크기에만 차이가 있을 뿐이다.

다음은 hello.txt에 저장된 유저정보를 읽어들여서 출력하는 프로그램이다. 프로그램의 이름은 UserInfoRead.c 로 하겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdlib.h> 
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>

struct userInfo
{
char name[28];
int age;
int sex;
char hobby[28];
};

int main()
{
int fd;
struct userInfo myAddrBook;
int dataSize;
fd = open("hello.txt", O_RDONLY);
if (fd < 0)
{
perror("file open error");
return 1;
}

dataSize = sizeof(myAddrBook);
printf("User Info =====================\n\n");
while(read(fd, (void *)&myAddrBook, dataSize) == dataSize)
{
printf("name : %s\n", myAddrBook.name);
printf("age : %d\n", myAddrBook.age);
printf("sex : %d\n", myAddrBook.sex);
printf("hobby : %s\n", myAddrBook.hobby);
printf("===============================\n");
}
close(fd);
return 0;
}
데이터를 읽기만 할 것이기 때문에, O_RDONLY를 사용했다. 다음 while문을 돌면서 userInfo 구조체의 크기만큼, 파일로 부터 데이터를 읽어들여서 myAddrBook에 저장하고 있음을 알 수 있다.

9 파일의 권한과 모드

리눅스는 다중사용자 운영체제이며, 때문에 모든파일에는 권한이 부여된다. 리눅스상에서는 모든 것이 파일로 표현되기 때문에, 파일에 권한을 부여한다는 얘기는 운영체제와 컴퓨터의 모든 것에 대한 권한이 부여될 수 있다는 것과 마찬가지가 된다.

예컨데 권한이라함은 이파일은 내것이며 나만 읽을 수 있다라든지 스터디 그룹에 포함된 사람들은 읽을 수 있지만 다른 사람들은 읽을 수 없다등과 같은 접근 권한을 말한다. 여기에 접근권한 외에도 읽기쓰기 가능에 대한 행위까지 세부적으로 분류할 수 있다. 현실에서와 마찬가지다. 현실에서도 직위나 직책, 부서에 따라서 문서에 대한 접근권한이 정해져 있으며, 읽기와 쓰기에 대한 행위도 정의된다.

정리하자면 파일에 대한 소유자가 누구인가에 대한 것이 권한이고, 권한을 가진 사용자에 대한 읽기/쓰기에 대한 가능한 접근범위가 모드이다.

리눅스 운영체제의 파일은 읽기와 쓰기외에 실행에 대한 모드도 가지고 있다는게 현실세계에서의 문서시스템과 다른점이라 할 수 있을 것이다.

파일에 대한 권한은 소유자, 그룹, other 세부분으로 나뉜다. 소유자는 개인이라고 생각할 수 있다. 파일에 대한 모드는 위에서 언급했듯이 읽기,쓰기,실행 3개로 세분화 될 수 있다. 이들의 조합으로 파일의 권한과 모드가 정의 된다.

우리는 ls를 통해서 파일의 권한과 모드를 확인할 수 있다.

# ls -al 
-rw-r--r-- 1 yundream yundream 4806656 2006-07-28 14:00 My_sweet_darlin.mp3
drwxr-xr-x 5 yundream yundream 4096 2007-07-29 01:26 PicasaDocuments
-rwxr-xr-x 1 yundream yundream 7402 2007-11-26 00:01 UserInfoRead
-rw-r--r-- 1 yundream yundream 751 2007-11-26 00:02 UserInfoRead.c
-rwxr-xr-x 1 yundream yundream 7433 2007-11-25 23:33 UserInfoWrite
-rw-r--r-- 1 yundream yundream 1087 2007-11-25 23:56 UserInfoWrite.c
drwxr-xr-x 2 yundream yundream 4096 2007-07-31 23:51 backup

파일의 권한과 모드에 대한 내용은 리눅스 환경에서의 C 프로그래밍에서 자세히 언급하고 있으니 참고바란다. 여기에서는 이정도로만 설명하고 다음으로 넘어가도록 하겠다.

10 파일의 종류와 권한,모드 알아내기

모든 것이 파일로 표현될 수 있다는 점과 다중사용자 운영체제라는 리눅스 운영체제의 특성상 파일의 종류와 권한,모드를 알아내는 것은 매우 중요하다. 파일을 다루는 프로그램을 작성할 경우 가장 먼저하는 일이 접근가능한 파일인지를 확인하는 일이다. 리눅스는 파일에 대한 정보를 얻어올 수 있는 stat라는 함수를 제공한다.
#include #include #include int stat(const char *file_name, struct stat *buf); 파일 이름 file_name를 인자로 주면, 그에 대한 정보를 stat구조체에 담아서 되돌려준다. stat에는 다음과 같은 파일 정보들이 담겨져 있다.
struct stat { 
dev_t st_dev; /* device */
ino_t st_ino; /* inode */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device type (if inode device) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for filesystem I/O */
blkcnt_t st_blocks; /* number of blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last change */
};
주석을 보는 정도로 각 멤버변수가 의미하는 바를 쉽게이해할 수 있을 것이다. 그러니 몇개 생소한 멤버변수들만을 설명하도록 하겠다.
  • st_ino : 파일의 일련번호다. 이 번호는 하나의 장치에서 유일하게 존재하며, 파일과 파일을 구분하게 해준다. 하나의 장치에서만 유일하다는 것에 주의하기 바란다.
  • st_dev : 파일이 속한 장치의 식별번호다. st_ino 와 st_dev 의 쌍은 전체 시스템에서 유일하다.
  • st_nlink : 파일의 hard link(이하 하드링크)의 갯수를 알려준다. 하드링크에 대한 내용은 따로 자세히 다루도록 하겠다.
  • st_mode : 파일의 형식을 알려준다. 이 값을 이용해서, 파일이 디렉토리인지, 링크인지, 장치 파일인지등을 알아낼 수 있다. 이 값을 분석하기 위한 다음과 같은 메크로를 제공한다. 각 메크로는 검사하고자 하는 내용이 참이면 0이 아닌 값을 리턴한다.
    1. S_ISDIR(st_mode) : 파일이 디렉토리 인지 검사한다.
    2. S_ISCHR(st_mode) : 파일이 문자장치 파일인지 검사한다.
    3. S_ISREG(st_mode) : 일반파일인지 검사한다.
    4. S_ISFIFO(st_mode) : FIFO 혹은 pipe 파일인지 검사한다.
    5. S_ISLNK(st_mode) : symbolic 링크 인지 검사한다.
    6. S_ISSOCK(st_mode) : 소켓 파일인지 검사한다.

st_mtime, st_atime, st_ctime 에서 되돌려주는 시간은 Unix 시간으로 1970년 1월 1일 00:00:00 부터 현재까지 흐른시간을 초로 환산한 값이다. 이 초로된 시간을 인간이 읽기 쉬운 형태로 만들어주는 시간관련 함수가 있는데, 이들 내용은 따로 다루도록 할 것이다.

다음은 파일의 각종 정보를 읽어오는 프로그램이다. 프로그램의 이름은 stat.c로 하겠다.
#include <unistd.h> 
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <pwd.h>
#include <grp.h>

int main(int argc, char **argv)
{
int return_stat;
char *file_name;
struct stat file_info;

mode_t file_mode;

if (argc != 2 )
{
printf("Usage : ./file_info [file name]\n");
exit(0);
}
file_name = argv[1];

if ((return_stat = stat(file_name, &file_info)) == -1)
{
perror("Error : ");
exit(0);
}

file_mode = file_info.st_mode;
printf("파일이름 : %s\n", file_name);
printf("=======================================\n");
printf("파일 타입 : ");
if (S_ISREG(file_mode))
{
printf("정규파일\n");
}
else if (S_ISLNK(file_mode))
{
printf("심볼릭 링크\n");
}
else if (S_ISDIR(file_mode))
{
printf("디렉토리\n");
}
else if (S_ISCHR(file_mode))
{
printf("문자 디바이스\n");
}
else if (S_ISBLK(file_mode))
{
printf("블럭 디바이스\n");
}
else if (S_ISFIFO(file_mode))
{
printf("FIFO\n");
}
else if (S_ISSOCK(file_mode))
{
printf("소켓\n");
}

printf("OWNER : %d\n", file_info.st_uid);
printf("GROUP : %d\n", file_info.st_gid);
printf("dev : %d\n", file_info.st_dev);
printf("inode : %d\n", file_info.st_ino);
printf("FILE SIZE IS : %d\n", file_info.st_size);
printf("마지막 읽은 시간 : %d\n", file_info.st_atime);
printf("마지막 수정 시간 : %d\n", file_info.st_mtime);
printf("하드링크된 파일수 : %d\n", file_info.st_nlink);
}

테스트 삼아서 stat.c 에 대한 조사를 해보자.
$ ./stat stat.c 
파일이름 : stat.c
=======================================
파일 타입 : 정규파일
OWNER : 1000
GROUP : 1000
dev : 2051
inode : 6603353
FILE SIZE IS : 1692
마지막 읽은 시간 : 1196074251
마지막 수정 시간 : 1196074249
하드링크된 파일수 : 2

10.1 hard link와 symbolic link

바로 위에서 link(이하 링크)가 몇번 언급되었었다. 이 링크에 대해서 자세히 알아보도록 하겠다.

링크는 파일을 가리키는 일종의 별칭으로 주로 관리의 목적으로 사용한다. 예를 들어 오픈오피스의 문서 프로그램을 실행시키기 위해서 /usr/local/openoffice/bin/openoffice_swrite를 실행시켜야 한다고 가정해보자. 이거 기억해서 수행시킬려면 보통 곤욕스러운 일이 아닐 것이다. 이 경우 링크를 이용해서 간단하게 문제를 해결할 수 있다.
# ln -s /usr/local/openoffice/bin/openoffice_swrite /usr/bin/swrite  
/usr/bin 은 환경변수 PATH에 등록이 되어있을 것이기 때문에, 간단하게 swrite만 입력하는 정도로 /usr/local/openoffice/bin/openoffice_swrite 를 실행할 수 있게 된다. 이제 ls를 이용해서 /usr/bin/swrite 의 정보를 확인해 보도록 하자.
$ ls -al swrite 
lrwxrwxrwx 1 root root 43 2007-11-26 23:44 swrite -> /usr/local/openoffice/bin/openoffice_swrite
swrite가 원본 openoffice_swrite 를 링크하고 있는 것을 확인할 수 있을 것이다.

직관적으로 이해할 수 있을 것이다. 그러나 link 는 두가지 종류가 있다. 심볼릭 링크와 하드링크가 그것이다. 이둘의 차이점에 대해서 알아보도록 하겠다.

10.2 hard link

앞서 파일은 장치내에서 식별되기 위해서 inode 를 가진다는 것을 언급했었다. 여기에 inode 가 1234 인 파일 myfile이 있다고 가정해보자. 이것을 다른 Directory에 복사하기 위한 가장 일반적인 방법은 파일을 copy 하는 것으로 이경우 새로운 inode 를 가지는 파일이 생길 것이다. 그럼 cp(1)를 이용해서 파일을 복사해보도록 하자.
# mkdir testdir 
# cp myfile testdir/myfile2
이제 두개 파일의 inode를 확인해 보도록하자. stat함수를 이용해서 프로그램을 만들 필요는 없다. ls 의 -i옵션을 사용하면 간단하게 파일의 inode 값을 알아낼 수 있다.
# ls -i myfile  
1131883 myfile
# ls -i testdir/myfile2
1163816 testdir/myfile2
내용은 동일하지만 완전히 다른 파일이 생성되었음을 알 수 있다.

이 방법은 대부분의 경우 유용하게 사용할 수 있겠지만 하나의 파일을 여러개의 디렉토리에 공유할 목적으로 사용하고자 할 경우 문제가 발생한다. 예를 들어 주소록 파일인 /home/yundream/mydata.txt 가 있다고 가정해보자. 이 파일을 /home/dragona 에 공유하길 원한다. 만약 mydata.txt에 새로운 내용이 추가되거나 삭제되면 /home/dragona 에도 그대로 적용되어야 한다. 단순히 copy 할경우에는 한쪽에서 변경하면, 다른 한쪽에는 반영되지 않을 것이다. 링크를 사용하면 이 문제를 간단하게 해결할 수 있다.

# ln mydata.txt /home/dragona 
이제 한쪽에서 파일을 수정해보자. 다른 쪽도 그대로 수정된 내용이 반영되어 있음을 확인할 수 있을 것이다. ls -i 로 확인해보면 두개의 파일이 동일한 inode를 가지고 있음을 확인할 수 있을 것이다. 이것을 링크라고 하며, 위에서와 같이 inode를 공유해서 사용하는 것을 하드 링크 라고 한다. 이해하기 쉽게 그림으로 나타내보자면 다음과 같다.

inode.png

file_name 1과 file_name2 가 서로동일한 inode를 가리키고 있음을 확인할 수 있다. 이러한 하드링크로 얻을 수 있는 장점은 데이터를 공유할 수 있다는 것 외에, 디스크를 아낄 수 있다는 장점도 가진다. 데이터를 직접복사하는게 아니기 때문이다. 원본은 하나이고 inode 만 공유할 뿐이다. 하드링크를 하나 생성하면 inode 공유 카운터가 1증가할 뿐이다. ls -al 로 mydata.txt 원본파일의 정보를 확인해 보자
# ls -al mydata.txt 
-rw-r----- 2 yundream yundream 192 2007-11-26 23:57 mydata.txt
하드링크 카운터가 하나 증가해서 2가 되어 있는걸 확인할 수 있을 것이다. 파일을 하나 지우고 나서 ls 결과를 보면 카운터가 하나 줄어서 1이 되는걸 확인할 수 있을 것이다.

하드링크를 사용할 때, 주의해야할 점이 있다. 하드링크는 inode 를 가리킨다. 이 때, inode 는 하나의 장치에서만 유일하므로 다른 장치로의 하드링크는 불가능 하다는 점이다. 왜냐하면 다른 장치에서 유일하다는 것을 보장할 수 없기 때문이다. 이런 경우에는 심볼릭링크를 사용해야 할 것이다.

11 심볼릭 링크

이와 달리 심볼릭 링크는 별도의 inode를 가지는 파일로 원본파일에 대한 inode와 함께 장치 정보까지를 가지고 있다. 어떤파일에 대한 inode와 장치정보를 알고 있다면, 전 시스템에서 유일한 파일을 가리킬 수 있기 때문에 장치에 관계없이 링크를 걸 수 있게 된다.

inode2.png

그럼 mydata.txt 를 원본파일로 하는 심볼릭링크 mydata2.txt를 만들어 보도록 하자. ln 명령에 -s옵션을 주면 심볼릭링크를 생성할 수 있다.
# ln -s mydata.txt mydataln.txt 
이제 -i 옵션을 이용해서 두개 파일의 inode를 비교해 보면 서로 다른 별개의 inode를 유지하고 있음을 알 수 있을 것이다. ls -l 을 이용해서 심볼릭링크가 가리키는 원본파일의 이름을 얻어올 수 있다.

# ls -l mydataln.txt  
lrwxrwxrwx 1 yundream yundream 10 2007-11-28 01:49 mydataln.txt -> mydata.txt

12 표준입력 표준출력 표준에러

프로그램은 어떤 값을 입력 받아서 처리하고 그 결과를 출력하는 일을 한다. 입력은 보통 키보드를 통해서 이루어지고 출력은 모니터를 통해서 이루어진다. 대부분의 프로그램이 이러한 입/출력 방식을 사용한다.

해서 프로그램이 실행될때에는 기본적으로 키보드장치와 모니터장치를 열어서 입출력이 가능하게 해놓았다. 이렇게 키보드 장치를 통한 입력을 표준입력이라하고 모니터를 통해 출력하는 것을 표준출력이라고 한다.

키보드를 통한 입력을 표준입력이라고 정의 하는건 문제가 없다. 그러나 모니터를 통한 표준출력에는 약간의 문제가 있다. 프로그램이 모니터에 출력하는 정보에는 입력 데이터를 정상적으로 처리해서 나오는 결과값외에 잘못 처리되어서 출력되는 결과값이 있기 때문이다. 덧셈 프로그램을 만들었는데, 피연산자에 숫자대신 알파벳 문자등을 넣었다면, 프로그램은 에러메시지를 출력할 것이다. 그런데 똑같이 모니터에 출력이 되어버리면, 결과값이 에러인지 아닌지 구분할 수가 없을 것이다.

이렇게 출력값이 정상인지 에러인지를 구분하기 위해서 표준출력외에 표준에러를 제공한다. 결과적으로 프로그램은 최초 실행시 다음과 같은 3개의 입출력 장치를 open하게 된다.
  • 표준입력 : 키보드를 통한 입력
  • 표준출력 : 모니터로 출력되는 정상 메시지
  • 표준에러 : 모니터로 출력되는 에러 메시지

우리는 리눅스는 모든것을 파일로 처리한다는 것을 배워서 알고 있다. 표준입력,출력,에러 역시 파일로 처리된다. 더불어 리눅스에서 파일을 다룰때에는 파일이름이 아닌, 파일지정번호를 이용한다는 것도 알고 있다. 리눅스는 이들 3개의 파일에 대해서는 아예 고유번호를 지정하고 있다.
  • 표준입력 : 0
  • 표준출력 : 1
  • 표준에러 : 2

다음은 표준입력, 표준출력, 표준에러를 이용해서 만든 간단한 나눗셈 프로그램이다. 표준입력을 통해서 2개의 수를 입력받아서 나눈 결과를 표준출력을 통해서 출력한다. 이 프로그램의 이름은 stdio.c 로 하겠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <unistd.h> 
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>

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

#define ERRMSG "Devide Not Zero\n"
int main()
{
int a;
int b;
char buf[80];

read(STDIN, buf, 80);
a = atoi(buf);

read(STDIN, buf, 80);
b = atoi(buf);
if (b == 0)
{
write(STDERR, ERRMSG, strlen(ERRMSG));
return 1;
}

sprintf(buf, "%d / %d = %d\n", a, b, (int)(a/b));

write(STDOUT, buf, strlen(buf));
return 0;
}
  • 18
    표준입력으로 분자를 입력받는다. 키보드로 부터 입력받는 값은 문자열로 atoi(3) 함수를 이용해서 int형 값으로 변경했다.
  • 21
    표준입력으로 분모를 입력받는다.
  • 23 ~ 27
    나눗셈에서는 분모가 0이 되는걸 허용하지 않고 있다. 그러나 실수로 0을 입력할 수 있으므로, 입력값을 검사해서 0이면 에러메시지를 출력하고 종료하도록 하고 있다. 이때 결과값은 에러메시지 이므로 표준에러를 통해서 출력했다.
  • 31
    나눈 결과값을 표준출력을 이용해서 모니터에 출력한다.

표준출력과 표준입력을 어떻게 사용하는지 이해하는 것은 어렵지 않을 것이다. 그러나 표준에러를 어떻게 사용해야 할지는 감이오지 않을 것이다. 걱정할 필요 없다. 아래의 재지향을 공부하다 보면, 자연스럽게 감이 오게 될 것이다.

13 입출력 재지향

입출력 재지향 혹은 I/O Redirection에 대해서 알아보자. 일단 재지향의 의미에 대해서 알고 넘어갈 필요가 있을것 같다. 재지향의 사전적 의미는 다른 방향으로 보낸다 이다. 여기에 마추어 입출력 재지향을 사전적의미 그대로 해석을 하자면, 입력과 출력을 다른 방향으로 보낸다가 될 것이다. 실제 입출력 재지향은 사전적의미 그대로 이해하면 된다.

리눅스에서 모든 것은 파일로 다루어진다고 했다. 이는 입력과 출력에도 예외없이 적용이 되므로, 입력과 출력을 다른 방향으로 보낸다입력과 출력을 다른 파일로 보낸다와 동일함을 의미한다. 예컨데, 키보드로 부터 입력 받은 데이터 - 즉 표준입력 - 일반 파일로 보내거나 프린터 - 프린터도 파일이니까로 보내는등의 일이 가능하다는 얘기가 된다. 일반 파일을 프린터로 보내거나, 표준출력을 표준에러로 보내는 등의 일역시 가능하다. 모든 것이 파일이기 때문에 모든 방향으로의 재지향이 가능하다.

stdio.c 파일을 예로 들어서 설명해보도록 하겠다.

stdio.c 프로그램은 입력을 검사해서 분모가 0이 되면, 에러메시지를 출력하도록 했다. 이 에러메시지는 표준에러 형태로 모니터에 출력된다. 입력이 제대로 이루어져서 결과값이 나올경우에는 표준출력 형태로 모니터에 출력이 된다. 이걸 파일로 재지향 해보도록 하자.

쉘에서는 꺽쇠를 이용해서 재지향을 이용할 수 있다. 표준출력을 result.txt 파일로 재지향하고 결과를 확인해 보도록 하자.
# ./stdio > result.txt 
1234
2
# cat result.txt
1234 / 2 = 617

프로그램을 만들어서 테스트 할경우 디버깅등의 목적으로 에러메시지를 파일로 따로 저장해둬야 할필요가 생긴다. 그렇다면 stdio의 표준에러를 파일로 재지향 시키면 될것이다.
# ./stdio 2> err.txt 
1000
0
# cat err.txt
Devide Not Zero
표준에러를 표준출력으로 재지향 시킬 수도 있다.
# ./stdio 2>1&  
이제 표준에러도 표준출력 형태로 모니터에 뿌려지게 된다. 표준에러를 표준출력으로 재지향 시키는 예는 grep 등을 이용해서 결과를 모니터링 하는 스크립트를 만들기 위해서 자주 이용된다. 아래의 경우를 보도록 하자.
# ./stdio | grep Not  > err.log 
위의 스크립트는 stdio의 실행결과중 분모가 0인 경우를 err.log로 남기기 위한 목적으로 작성되었다. 그렇지만 예상과는 다르게 분모가 0인 경우도 err.log로 남겨지지 않을 것이다. 왜냐하면 파이프 |는 표준출력 결과만을 grep로 넘기는데 Device Not Zero는 표준에러이므로 파이프를 통해서 grep로 넘어가지 않기 때문이다.

이때 표준에러를 표준출력으로 재지향 시키는 방법으로 문제를 해결할 수 있다. 위의 스크립트를 아래와 같이 수정한다음에 테스트해보도록 하자.
# ./stdio 2>&1 | grep Not > err.log 
1000
0
# cat err.log
Devide Not Zero
표준출력결과가 파일로 저장된걸 확인할 수 있을 것이다.

이론적으로는 파일에 저장된 내용을 각 장치에 재지향 시키는 것만으로도 해당 장치를 이용할 수 있다. 예를 들자면 wav 파일을 읽어서 사운드카드를 가리키는 장치파일에 재지향 시켜서 wav 파일을 플레이 하는 것이다.
# cat sound.wav > /dev/audio 

재지향의 개념에 대해서 알아보았는데, 정작 중요한건 재지향이 시스템 프로그래밍의 관점에서 어떻게 구현이 되는가 하는 것이다. 간단히 생각해 보자면, 두개의 파일을 연다음에 하나의 파일의 내용을 읽어서 다른 파일로 복사하면 된다. 표준출력을 파일로 재지향한다면, 표준출력과 파일을 연다음에 표준출력의 내용을 읽어서 파일에 그대로 쓰는 형식이다.

그러나 이 방식은 매우 복잡하다. 이보다는 dup2()함수를 이용해서 좀더 간단하게 재지향을 구현할 수 있다. 아래의 코드를 컴파일 한다음 실행시켜 보도록 하자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <unistd.h> 
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>

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

int main(int argc, char **argv)
{
int fd;
fd = open("test.log", O_CREAT|O_WRONLY, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP);
fd = dup2(fd, STDOUT);
printf("Hello World %d\n", fd);
}
  • 15 : 재지향할 파일로 test.log를 오픈했다. fd는 아마도 3일 것이다.
  • 16 : dup2()함수를 이용해서 fd를 STDOUT로 복사했다.
  • 17 : printf 함수를 이용해서 표준출력했지만 STDOUT는 test.log의 파일지정자로 복사가 되었기 때문에, 모니터로 출력이 되느넥 아니고 파일로 출력이 된다.
이정도면 재지향의 구현개념에 대해서 정리가 되었으리라 생각된다. dup와 dup2는 프로세스간 입출력을 공유하기 위한 용도로 나중에자세히 언급이 될 것이다. 우선은 이런 함수가 있다는 정도만 이해하고 넘어가도록 하자.

14 사용한 함수들 정리

  • open(2) : 파일을 연다.
  • write(2) : 파일을 쓴다.
  • read(2) : 파일의 내용을 읽는다.
  • close(2) : 열린 파일을 닫는다.
  • printf(3) : 문자열을 화면에 표준출력 한다.
  • dup2(2) : 파일지정번호를 복사한다.
  • perror(3) : 에러메시지를 표준에러로 출력한다.
:::