다음 이전 차례

4. 프로그래밍

엄청난 하드웨어의 성능 발전을 소프트웨어 기술이 따라가지 못하고 있다는 지적이 많다. 눈 깜짝할 사이에 이미 펜티엄 프로 200을 넘어서고 있고 멀티미디어 관련 MMX다 뭐다 해서 인텔사는 새로운 구매 욕구에 충동질을 하고 있다. 필자가 원하든 원하지 않든 또 한 번 하드웨어 갈아치우기 전쟁이 일어날 듯 하다.

하드웨어적 발전 중에서도 이번에는 SMP(Symmetrical MultiProcessing) 그리고 병렬 처리 개념을 사용하는 쓰레드(thread) 프로그래밍에 대한 얘기를 잠깐 해보고자 한다. 이번에 다루는 내용은 소개 수준 밖에 안된다는 것을 미리 일러두고자 한다.

병렬처리 개념을 사용하면 이익을 얻을 수 있는 분야는 역시 인터넷 서버라고 할 수 있다. 지금 현재는 웹 서버의 경우 HTTP 서비스를 요구하는 클라이언트의 요구가 있을 때마다 자기 스스로를 복제(fork)하여 그 복제 프로세스로 하여금 클라이언트에게 서비스를 제공하고 메인 프로세스는 계속적으로 특정 포트(일반적으로 웹 서버는 80번 포트 또는 8080번 포트)에 귀기울이는 형태를 지닌다. 이미 유닉스 시스템에는 웹 서버와 같이 클라이언트들의 동시 접속, 동시 서비스 제공을 해결할 능력을 가지고 있다. 그럼에도 불구하고 쓰레드 프로그래밍은 좀 더 나아가려고 한다. 기술적인 얘기로는 유닉스의 프로세스 복제에 걸리는 시스템의 부하가 많기 때문에 그보다는 개선된 형태 즉 쓰레드 (thread)라고 불리우는 경량급(light-weight) 프로세스 모델을 사용하여 빈번한 서버 처리 업무에 효율성을 기하자는 내용이다. 더군다나 ATM 교환기 등 초 고속 네트워크가 건설되면 약간의 시간 차이라 할지라도 서버의 처리 능력은 커다란 문제로 떠오르지 않을 수 없다. ATM 교환기를 통해서 쏟아져 오는 패킷을 제대로 처리하지 못한다면 비싼 돈 들여 건설한 네트워크 하드웨어가 무슨 소용 있겠는가?

쓰레드가 새롭게 만들어지는 것과 프로세스가 새롭게 만들어진 것 사이에는 약간의 차이가 있다고 한다. 프로세스가 복제될 때는 복제를 행하는 부모 프로세스와 상대적으로 적은 양의 정보를 공유한다고 한다. 하지만 쓰레드의 경우에는 예를 들어 전역 변수(global variable), 정적 지역 변수(static local variable), 그리고 열려진 파일 기술자, 프로세스 ID 등 더 많은 정보를 공유한다고 한다. 프로세스의 경우 개별적인 복제물을 만들어내는데 쓰레드에 비해 상대적으로 부하가 많이 걸리며 한 프로세스에서 다른 프로세스로 제어권을 넘기는데 (context switch라고 한다) 걸리는 시간이 한 쓰레드에서 다른 쓰레드로 이동하는 시간보다 상대적으로 더 많이 걸린다고 한다.

유즈넷 뉴스그룹에 가보면 쓰레드 프로그래밍에 대한 찬반이 엇갈리지만 어찌 되었든 쓰레드 방식의 프로그래밍은 표준적인 프로그래밍 라이브러리의 위치로 들어오기 시작했다. 리눅스 커널은 2.0 버전이 되면서부터 멀티-쓰레드 프로세스를 다룰 수 있게 되었고 따라서 쓰레드 프로그래밍에 길을 열어주고 있다.

4.1 쓰레드 프로그래밍을 하기 위해 필요한 것은?

리눅스 C 라이브러리 버전 5.x 대의 라이브러리에서는 사용자 레벨 (User-Level)의 쓰레드 라이브러리가 들어있으나 그렇게 쓸만 한 것은 아니라 는 말들이 많다. 몇 번 시험해본 결과 구현되지 않은 것들도 몇 가지 있어서 아예 컴파일 자체가 안되는 경우도 허다했다.

여러분이 구해야 할 것은 Xavier Leroy씨의 커널 수준(Kernel-Level) 쓰레드 라이브러리이다. 커널 수준의 쓰레드란 리누스씨가 커널 버전 2.0 이하에서 제공하고 있는 clone()이라고 하는 새로운 기능에 의거한 쓰레드를 말한다. 현재 소개 수준에 그치는 이 글에서 사용자 레벨의 쓰레드와 커널 수준의 쓰레드는 크게 구분할 필요 없으며 API는 같으므로 상관하지 않아도 좋다.

소스를 구하여 수동으로 설치해야 하는데 그 위치는 다음과 같다. pauillac.inria.fr/~xleroy/linuxthreads/ 레드햇 배포판 사용자는 손쉽게 커널 쓰레드 라이브러리를 설치할 수 있다. 레드햇 배포판 버전 4.1에 패키지가 추가되어 있기 때문이다. 여러분이 설치해야 할 패키지명은 linuxthreads, linuxthreads-devel 이렇게 2 개의 패키지이다. 마찬가지로 Leroy씨의 커널 쓰레드 라이브러리이다.

다음 예제를 컴파일해보자. (이 소스는 리눅스 저널 97년 2월호 ISSUE 34호, Martin McCarthy씨의 원고에서 Listing 6.에서 인용한 것입니다)

{{{{#define _REENTRANT
#include <stdio.h>
#include <pthread.h>

#define MATSIZE     4

/* 함수 원형 */
void* matMult ( void* );

/* 전역 행렬 자료 */

int mat2[MATSIZE][MATSIZE] =
  {  {1, 2, 3, 4 },
     {4, 5, 6, 7 },
     {7, 8, 9, 10 },
    {10, 11, 12, 13 } };
int mat1[MATSIZE][MATSIZE] =
  {  { 9, 8, 7, 6 },
    { 6, 5, 4, 3 },
    { 3, 2, 1, 0 },
    { 0, -1, -2, -3 } };
int result[MATSIZE][MATSIZE];

int
main( void )
{
    pthread_t thr[MATSIZE];

    int i, j;

    for ( i = 0 ; i < MATSIZE ; ++i )    {
        pthread_create ( &thr[i], NULL, matMult, (void*)i );
    }
    for ( i = 0 ; i < MATSIZE ; ++i )    {
        pthread_join ( thr[i], NULL);
    }
/* 소스 다음에 계속 */
/* 소스 앞에서 이어짐 */

    for ( i = 0 ; i < MATSIZE ; ++i )    {
        printf ("|");
        for ( j = 0 ; j < MATSIZE ; ++j )
            printf ("%3d ", mat1[i][j] );
        printf ("|%c|", ( i==MATSIZE/2 ? 'x' : ' ') );
        for ( j = 0 ; j < MATSIZE ; ++j )
            printf ("%3d ", mat2[i][j] );
        printf ("|%c|", ( i==MATSIZE/2 ? '=' : ' ') );
        for ( j = 0 ; j < MATSIZE ; ++j )
            printf ("%3d ", result[i][j]);
        printf ("|\n");
    }

    return 0;
}

void*
matMult ( void* col )
{
    int i, j;
    int val;

    for ( i = 0 ; i < MATSIZE ; ++i )
    {
        result[i][(int)col] = 0;
        for ( j = 0 ; j < MATSIZE ; ++j )
            result[i][(int)col] += mat1[i][j] *
                mat2[j][(int)col];
    }

    return NULL;
}

아래는 실행 결과를 보여준다.

$ gcc thread_ex1.c -o thr_ex1 -lpthread
$ ./thr_ex1
| 9  8  7  6 |   | 1  2  3  4  |   | 150 180 210 240 |
| 6  5  4  3 |   | 4  5  6  7  |   |  84 102 120 138 |
| 3  2  1  0 | x | 7  8  9 10  | = |  18 24 30 36    |
| 0 -1 -2 -3 |   | 10 11 12 13 |   | -48 -54 -60 -66 |

쓰레드 프로그래밍 방식으로 행렬을 계산한 예이다. 사실 쓰레드 프로그래밍을 배우기 어렵다기 보다는 어디에 활용할 것인가를 찾는 것이 더 어렵다고 말할 수 있을 것 같다. 쓰레드의 활용 영역은 앞서 얘기한 다중 클라이언트 지원 서버 프로그래밍 그리고 여기서 보는 것처럼 행렬 연산 그리고 둠과 같이 독립적으로 움직이는 적들이 많이 등장해야 하는 게임을 들 수 있다. 가까운 예로는 지난달에 소개했던 MPEG Layer3 디코더/플레이어인 splay가 바로 pthread 라이브러리를 사용하고 있다.

행렬 연산에 대해서 알아보자. 위에서는 4x4 정방행렬 2 개의 행렬 곱을 처리하는 과정을 보여주고 있다. 여러분이 고등학교 수학을 마쳤다면 행렬 곱이 어떤 식으로 이뤄지는지 그 규칙을 알고 있을 것이다. 앞 행렬의 m 번째 행과 뒤 행렬의 n 번째 열을 계산하여 결과 행렬의 m 행 n 열의 원소가 된다. 행렬 곱 규칙을 잘 살펴보면 행과 열의 곱은 서로의 결과에 영향을 받지 않고 독립적으로 행해지는 연산임을 알 수 있다. 따라서 각 행과 열의 곱은 병렬 처리하기가 아주 좋다.

필자는 쓰레드 프로그래밍을 보고 있노라면 손오공이 적을 물리치기 위하여 자신의 머리털을 뽑아 자그마한 분신들을 만들어 공격하는 모습을 연상하곤 한다.

4.2 POSIX 쓰레드

멀티쓰레딩에 관하여 POSIX 표준이 이미 마련되어 있다. 따라서 POSIX 쓰레드 표준에 따른 프로그래밍을 한다면 소스 코드 수준에서 리눅스에서뿐 아니라 다른 비슷한 유닉스에서 같은 프로그램을 운영할 수 있다. 리눅스 쓰레드 라이브러리들은 POSIX 표준을 따르므로 걱정할 필요없다.

4.3 관련된 이야기

쓰레드에 대하여 이야기하면서 빠뜨릴 수 없는 주제들이 있다. 바로 병렬 다중 프로세서 SMP와 마크(Mach) 커널이 바로 그것이다. 요즘 우리는 심심지 않게 2 개의 프로세서를 장착할 수 있는 보드를 볼 수 있다. 그리고 기존의 유닉스 커널과는 다른 설계 방식의 마크(Mach) 커널에 대한 얘기를 심심치 않게 듣게 된다. 쓰레드 프로그래밍은 바로 이런 환경에서 가장 뛰어난 효율을 발휘한다고 한다.

카네기 멜런 대학(CMU)에서 시작한 새로운 운영체계 연구 프로젝트의 의 하나인 마크 프로젝트는 1985년부터 시작되었다고 하니 꽤 오랜 시간을 거친 시스템이라고 할 수 있다. 1994년 CMU의 마크 커널 개발은 중단되었고 마크 커널의 개발은 Open Software Foundation, 유타 대학의 Flexmach, 헬싱키 대학의 LITES 시스템, 그리고 FSF의 Hurd 시스템으로 개발이 진행 중이다. 마크 커널에 대한 자세한 정보는 다음 사이트에서 얻을 수 있다. www.cs.cmu.edu/afs/cs.cmu.edu/project/mach/public/www/mach.html

안타깝게도 리눅스는 마크 커널 위에 만들어진 것이 아니며 전통적인 유닉스 스타일로 만들어진 것이다. 하지만 이미 애플 사에 의해 만들어진 MkLinux는 마크 커널 위에 리눅스를 올려놓는데 성공하였다. 아직 현실적인 힘을 발휘하기에는 많은 시간을 기다려야 할 것으로 예상되는 운영체계 GNU는 마크 커널 방식이며 Hurd라는 커널을 사용한다. 최근 들어 GNU 측에서도 마크 커널을 내놓았다고 한다. 마크 커널에 대한 관심은 무엇보다도 애플 사로 인수된 넥스트스텝의 하부 기술이기에 더욱 커지리라 본다. GNU Hurd에 관한 정보는 다음 사이트를 방문해보기 바란다. www.gnu.ai.mit.edu/software/hurd/hurd.html


다음 이전 차례