프로세스(Processes)

프로세스는 리눅스와 Windows에서 모두 실행 프로그램을 뜻한다. Windows에서는 .exe 확장자가 있는 파일 이름이 실행파일이다. 프로세스는 소스 파일을 컴파일 하고 실행 파일을 생산함으로서 만들어진다. 컴파일 단계는 Windows와 리눅스 모두 비슷하다:

 


프로그램 컴파일하기
    WINDOWS:   cl -O2 create-pt2.cpp
    LINUX  :   gcc -O2 create-pt2.cpp -lpthread -o create-pt2

위 두개의 컴파일 결과는 create-pt2.exe (Windows)와, create-pt2 (리눅스)라는 실행파일이다.

프로세스는 오픈 파일 핸들을 상속 받을 수 있다. Windows에서, 핸들이 작은 정수가 아니기 때문에 정확한 값은 알 수 없다. Windows에서 기존의 오픈 파일을 위해 핸들을 정확한 값으로 초기화 하는 것은 문서화된 프로시져가 아니다. 리눅스에서 파일 디스크립터(descriptor)를 20으로 설정하고 사용하는 것은 매우 쉽다.

일단 프로세스가 만들어지면 시작할 때 명령행에서 프로그램 이름을 타이핑 하면 된다. 리눅스와 Windows 모두 같다:


프로그램 실행하기
    create-pt2

Windows 프로그램의 경우, .exe 서픽스를 타이핑 할 필요가 없다. 프로그램이 실행권한이 있고 합법적인 Windows 바이너리 형식의 프로그램이라면 Windows는 모든 프로그램 이름을 실행 파일로서 인식한다. 따라서 나는 마이크로소프트 컴파일러의 아웃풋을 리눅스와 마찬가지로 create-pt2 라고 이름을 붙였다.


리눅스 프로세스 만들기

리눅스는 매개변수가 없는 단일 시스템 호출을 사용하여 새로운 프로세스를 만든다. fork 시스템 호출로 정확한 부모 프로세스 카피(copy)를 만들고 부모에게는 자식 프로세스 ID를, 자식에게는 0(zero)을 리턴한다:


리눅스 프로세스 생성
        #include <sys/types.h>
        #include <unistd.h>

        pid_t child_pid;

        child_pid = fork();

        if(child_pid == -1) {
            ERROR;
        }
        else if(child_pid == 0) {
            do_child();
        }
        else {
            do_parent();
        }

Fork는 새로운 프로세스를 만들지만 어떻게 이것이 새로운 프로그램을 실행하는가? 표준 수행 방법은 다음과 같다:


리눅스 상에서 새로운 프로그램 실행하기
        child_pid = fork();
        if(child_pid == -1) ERROR

        if(child_pid == 0) { // Child executes a new program image
            execl("/bin/view", "view", "/etc/hosts");
            ERROR;
        }
        else {               // Parent waits for child to finish (exit).
            int status;

            while(wait(&status) != child_pid);
        }

프로그램을 만들고 실행하는 코드는 깊은 의미를 가지고 있다. 프로그램은 main 으로 부터 리턴하거나 시스템 호출을 사용하여 종료한다.


Windows 프로세스 만들기

Windows 프로세스는 CreateProcess() API를 사용하여 만들어진다. 리눅스의 단순한 fork 호출과는 달리, CreateProcess API는 시도하기에 앞서 공부를 해두는 것이 필요하다.


Windows에서 프로세스를 만드는 API
BOOL CreateProcess(
    LPCTSTR lpApplicationName,                 // name of executable module
    LPTSTR lpCommandLine,                      // command line string
    LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD
    LPSECURITY_ATTRIBUTES lpThreadAttributes,  // SD
    BOOL bInheritHandles,                      // handle inheritance option
    DWORD dwCreationFlags,                     // creation flags
    LPVOID lpEnvironment,                      // new environment block
    LPCTSTR lpCurrentDirectory,                // current directory name
    LPSTARTUPINFO lpStartupInfo,               // startup information
    LPPROCESS_INFORMATION lpProcessInformation // process information
);

CreateProcess API는 리눅스의 execl() 시스템 호출과 비교되어야 공정하다. execl() 는 프로그램 이름과 인자는 API에 있는 스트링으로 지정되어야 하는 반면, CreateProcess()는 많은 옵션들을 제공한다.

create-pt2.cpp 프로그램은 프로세스 또는 쓰레드를 만들 수 있다. 프로세스를 만드는 코드는 다음과 같다:


Windows에서 프로세스 만들기
    extern char *applname;
    extern char *progname;
    HANDLE t1;
    char buf[1024];
    BOOL b;
    #define errno   GetLastError()

    sprintf(buf, "%s-child", applname);
    buf[sizeof(buf)-1] = 0;

    STARTUPINFO         startInfo;
    PROCESS_INFORMATION pidInfo;

    //
    //    child process
    //
    startInfo.cb          = sizeof(STARTUPINFO);
    startInfo.lpReserved  = NULL;
    startInfo.lpTitle     = buf;
    startInfo.lpDesktop   = NULL;
    startInfo.dwX         = 0;
    startInfo.dwY         = 0;
    startInfo.dwXSize     = 0;
    startInfo.dwYSize     = 0;
    startInfo.dwXCountChars    = 0;
    startInfo.dwYCountChars    = 0;
    startInfo.dwFlags     = STARTF_USESTDHANDLES;
    startInfo.wShowWindow = 0; //SW_SHOWDEFAULT;
    startInfo.lpReserved2 = NULL;
    startInfo.cbReserved2 = 0;
    startInfo.hStdInput   = GetStdHandle(STD_INPUT_HANDLE);
    startInfo.hStdOutput  = GetStdHandle(STD_OUTPUT_HANDLE);
    startInfo.hStdError   = GetStdHandle(STD_ERROR_HANDLE);

    b = CreateProcess(
            progname,
            buf,
            NULL,
            NULL,
            TRUE,
            0,//CREATE_NEW_CONSOLE,
            NULL,
            NULL,
            &startInfo,
            &pidInfo);

    if(!b) {
        printf("Creation of child process failed: err=%d\n", errno);
        printf("applname=<%s>, buf=<%s>\n",applname,buf);
        return;
    }
    t1 = pidInfo.hProcess;

보다시피 필요한 코드의 양이 많다. 이와 유사한 리눅스의 wait() 호출을 보자:


Windows에서 프로세스 기다리기
    if(WaitForSingleObject(t1,INFINITE) == WAIT_FAILED) {
        printf("WaitForSingleObject FAILED: err=%d\n", GetLastError());
        return;
    }

Windows 프로그램은 종료 방식이 여러가지 이다. 두 가지는 리눅스와 유사하다. main 에서 리턴하거나 표준 C 라이브러리에서 exit() 루틴을 호출할 수 있다. 세 번째 옵션은 ExitProcess() API를 호출하는 것이다. 여기에서는 exit 루틴을 사용한다.


쓰레드(Threads)

쓰레드는 실행 콘텍스트(context)이다. 처음에 각 프로세스는 하나의 실행 콘텍스트를 갖는다. 이 실행 콘텍스트를 쓰레드라고 한다. 프로세스가 다른 실행 콘텍스트를 필요로 한다면 간단히 다른 프로세스를 만들 수 있다. 하지만 프로세스 생성은 프로세서 사이클과 메모리 사용의 관점에서 볼 때 사치스러운 일이다. 쓰레드는 다중 실행 콘텍스트를 만드는데 경량의(lightweight) 메커니즘을 제공하도록 되어있다. Windows와 리눅스는 한 쌍의 실행 환경을 제공할 목적으로 오퍼레이팅 시스템에서 쓰레드를 스케쥴링한다.

프로세스와 쓰레드의 가장 뚜렷한 차이는 프로세스의 모든 쓰레드가 같은 메모리 공간과 시스템 정의의 "도구(facility)"를 공유한다는 점이다. 오픈 파일 핸들(파일 디스크립터), 공유 메모리, 프로세스 동기화 프리머티브(process synchronization primitives), 현재 디렉토리 등이 "도구"이다. 글로벌 메모리가 공유되고 새로운 메모리가 할당되지 않기 때문에 쓰레드를 만드는 것은 프로세스를 만드는 것보다 간단하고 빠르다.


리눅스 쓰레드 만들기

리눅스는 POSIX 쓰레드를 지원한다. 리눅스 쓰레드는 다음의 코드로 만들어진다:


리눅스에서 쓰레드 만들기
        #    define DEC    ( void *(*)(void*) )

        if(pthread_create(&t1, NULL, DEC  threadwork, NULL)) {
            printf("pthread_create A failed: err=%d\n", errno);
            return;
        }

여기에서, t1은 쓰레드 ID를 받기위해 사용되는 매개변수이다. 두 번째 인자는 많은 스케쥴링 옵션을 지원하는 쓰레드 속성 인자이다. 우리는 여기서 디폴트 세팅을 사용할 것이다. 세 번째 인자는 쓰레드를 생성할 때 실행 될 서브루틴(subroutine) 이다. 네 번째 인자는 서브루틴으로 전달된 포인터이다. 이것은 쓰레드나 새로 만들어진 쓰레드에 필요한 것을 위해 저장한 메모리를 지시할 수 있다.

리눅스 쓰레드는 서브루틴에서 리턴하거나 pthread_exit() 루틴을 호출하여 종료한다. 쓰레드가 가지고 있는 리소스는 부모 쓰레드가 pthread_join() 루틴을 호출할 때 회복된다. pthread_join()는 프로세스용 wait() 함수와 비슷하다. pthread_join()는 쓰레드에 의해 할당된 힙 메모리(heap memory)를 회복하지 않는다. 전역으로(globally) 할당된 메모리가 프로그램 또는 쓰레드에 의해 자유로워 져야 한다.


Windows 쓰레드 만들기

Windows 쓰레드는 Windows 프로세스를 만드는 것보다 훨씬 간단하다. 쓰레드를 만드는 데에는 리눅스에서 사용한 매개변수를 비롯하여 보안 디스크립터와 초기 스택 크기도 포함된다.


Windows에서 쓰레드 만들기
        t1 = CreateThread(NULL,4096,(THRDFN)threadwork,"L",NULL,&threadid);
        if(t1 == NULL) {
            printf("CreateThread FAILED: err=%d\n", errno);
            return;
        }

첫 번째 인자는 보안 디스크립터 로서 쓰레드 핸들이 상속 될 수 있을 지의 여부를 결정한다. NULL 은 이것이 상속될 수 없다는 것을 의미한다. 두 번째 인자는 스택 사이즈이다. 세 번째와 네 번째 인자는 서브루틴과 패스된 매개변수이다. 다섯 번째 인자는 플래그 인자이다. 쓰레드는 플래그가 CREATE_SUSPENDED로 설정될 때 중지 상태에서 만들어진다. 여섯 번째 인자는 결과 쓰레드 ID를 시스템에 저장한 위치를 가리킨다.

Windows 쓰레드는 호출된 서브루틴에서 리턴하거나 ExitThread() API를 호출하여 종료한다. 부모는 WaitForSingleObject(threadid)를 호출하여 쓰레드를 정리하거나 WaitForMultipleObjects() API를 사용하여 다중 이벤트를 기다릴 수 있다.


퍼포먼스 결과

프로세스와 쓰레드의 생성과 리눅스와 Windows에서 그들의 유사성을 설명하기 위해 create-pt2.cpp를 작성했다. "시간 측정 (timed-test)" 방식을 사용한다.

Linux 2.4.2 커널( Red Hat 7.2), Windows 2000 Advanced Server, Windows XP Professional에서 프로그램을 실행했다. 세 개의 오퍼레이팅 시스템은 같은 Thinkpad 600X (320 MB메모리)에서 실행되었다. 그림1과 2는 오퍼레이팅 시스템의 퍼포먼스를 나타낸다.


그림 1. 쓰레드 생성 속도
Figure 1. Thread creation speed

그림 2. 프로세스 생성 속도
Figure 2. Process creation speed

쓰레드와 프로세스를 만들 때 리눅스가 Windows(2000, XP)보다 상당히 빠르다는 것을 알 수 있다.

create-pt2.cpp 는 주어진 시간 간격에서 가능한 오랜 시간동안 쓰레드(프로세스)를 만들고 소멸시키는 단순한 프로그램이다. 다음과 같이 테스트를 실행했다:


테스트 스크립트
    create-pt 2         # create threads for 2 seconds
    create-pt 4
    create-pt 8
    create-pt 16

    create-pt -p 2      # create processes for 2 seconds
    create-pt -p 4
    create-pt -p 8
    create-pt -p 16

두 개의 Windows 결과는 프로그램 실행이 길어질수록 퍼포먼스가 악화되었음을 나타냈다. 그림 3은 이 문제를 보여주고 있다.


그림 3. 쓰레드 생성 속도
Figure 3. Thread creation speed decay

Windows가 쓰레드를 생성할 때 동시에 핸들도 생성한다. 이 핸들은 쓰레드를 만들었던 프로세스 상에서 열려있다. 프로세스의 경우, Windows는 두 개의 핸들을 만든다. 하나는 프로세스 용이고 다른 하나는 그 프로세스의 실행 쓰레드용이다. 이 핸들들은 나중에 OpenProcess()이나 OpenThread() API를 이용하여 얻을 수 있기 때문에 불필요한 것들이다. 그럼에도 불구하고 그들이 닫히지 않는다면 프로그램은 계속해서 핸들을 얻을 것이고 시스템 전역의 리소스를 소비하는 결과가 된다.

create-pt2.cpp 의 첫 번째 버전은 생성된 쓰레드와 프로세스를 위해 핸들을 닫지 않았다. 쇠퇴하는 퍼포먼스는 메모리 유출을 증명하고 있다. Task Manager 를 사용하거나 프로세스 탭에 나타내기 위해 칼럼으로서 "핸들" 을 선택함으로서 create-pt2.exe 프로그램이 가진 핸들의 수는 증가한다. 핸들의 유출은 Windows 프로그램의 퍼포먼스가 실행 시간이 길어질수록 쇠퇴하는 이유가 된다.

문제를 고쳤을 때 Task Manager는 create-pt2a.cpp가 고정된 수의 핸들을 가지고 있다는 것을 나타냈다. 퍼포먼스 결과는 테스트 실행이 길어져도 더 이상 쇠퇴하지 않았다.

Windows XP는 쓰레드 생성 속도에 있어서 Windows 2000를 능가했다. 하지만 리눅스 속도의 60% 에 그치는 수준이다. 프로그램이나 측정 기술에 이상이 없는 한, Windows XP의 프로세스 생성은 Windows 2000보다 현저하게 느리고, Windows 2000의 프로세스 생성은 리눅스 보다 느리다.


요약

프로세스와 쓰레드에 대해 간단히 설명했고 내가 작성한 프로그램에서 두 가지의 프로그래밍을 설명했다. 프로세스와 쓰레드의 퍼포먼스를 측정한 결과 리눅스가 Windows 2000 이나 Windows XP 보다 빨랐다. 여러분도 테스트 스크립트 (create-pt2a-sh.sh) 와 create-pt2a.cpp용 소스 코드를 다운로드 하여 직접 실행해보기 바란다.

Windows 프로세스와 쓰레드 생성속도를 높이는 코드를 만드는 방법을 알고있다면 discussion forum에서 의견을 나눠주기 바란다.


+ Recent posts