프로그래밍/임베디드 2013. 5. 23. 10:13


'프로그래밍 > 임베디드' 카테고리의 다른 글

7segment  (0) 2013.05.23
Led  (0) 2013.05.23
linux kernel build  (0) 2013.05.23
임베디드 시스템 구성 리눅스 소프트웨어  (0) 2013.05.23
HBE-SM5-S4210 M3(FPGA) Module  (0) 2013.05.23
//
프로그래밍/임베디드 2013. 5. 23. 10:12

리눅스 시스템 소프트웨어

•부트로더 : uboot-s4210

•리눅스 커널 : linux-2.6.35-s4210

•루트파일 시스템 : glibc-2.10.1, BusyBox v1.9.1 등

•크로스 컴파일러 : gcc 4.4.1

•Application module의 peripheral device 드라이버 모듈

-LED Device Driver Module

-7-Segment Device Driver Module

-TextLCD Device Driver Module

-DotLED Device Driver Module

-Keypad Device Driver Module

-Dip Switch Device Driver Module

-Base LED Device Driver Module

-Piezo Device Driver Module

-Base 7-Segment Device Driver Module

-OLED Device Driver Module

'프로그래밍 > 임베디드' 카테고리의 다른 글

7segment  (0) 2013.05.23
Led  (0) 2013.05.23
linux kernel build  (0) 2013.05.23
bootloader  (0) 2013.05.23
HBE-SM5-S4210 M3(FPGA) Module  (0) 2013.05.23
//
프로그래밍/임베디드 2013. 5. 23. 10:10

HBE-SM5-S4210 M3(FPGA) Module

•Character LCD(16 * 2)

•OLED

•6Digit 7-Segment

•18,752 Logic Elements FPGA EP2C20

•7 * 5 Dot Matrix 2ea

•4 * 4 Keypad

•8point DIP Switch 2ea

•LED 8ea

•Piezo

•Tact Switch 4ea

•Full Color LED

•Motor driver

'프로그래밍 > 임베디드' 카테고리의 다른 글

7segment  (0) 2013.05.23
Led  (0) 2013.05.23
linux kernel build  (0) 2013.05.23
bootloader  (0) 2013.05.23
임베디드 시스템 구성 리눅스 소프트웨어  (0) 2013.05.23
//
프로그래밍/C 2013. 5. 22. 06:28

TCP/IP 통신 함수 사용 순서

TCP/IP 예제 소개

  TCP/IP 예제를 서버와 클라이언트로 나누어서 설명을 드리도록 하겠습니다.

  1. 서버와 클라이언트는 port 4000번을 사용
  2. 클라이언트프로그램에서 서버에 접속하면 실행할 때 입력받은 문자열을 전송
  3. 서버는 클라이언트로부터 자료를 수신하면 문자열 길이와 함께 수신한 문자열을 클라이언트로 전송

서버 프로그램

  서버 프로그램에서 사용해야할 함수와 순서는 아래와 같습니다.

  우선 socket 부터 만들어야 합니다. TCP/IP에서는 SOCK_STREAM을 UDP/IP에서는 SOCK_DGRAM을 사용하는 것을 참고하여 주십시오. socket()에 대한 더 자세한 말씀은 "Unix C Reference의 11장 7절 소켓 열고 닫기"를 참고하십시오.

int     server_socket;

server_socket = socket( PF_INET, SOCK_STREAM, 0); 
if (-1 == server_socket)
{
   printf( "server socket 생성 실패"); 
   exit( 1) ;
}

  bind() 함수를 이용하여 socket에 server socket 에 필요한 정보를 할당하고 커널에 등록

  1. 만들어진 server_socket 은 단지 socket 디스크립터일 뿐입니다.
  2. 이 socket에 주소를 할당하고 port 번호를 할당해서 커널에 등록해야 합니다.
  3. 커널에 등록해야 다른 시스템과 통신할 수 있는 상태가 됩니다.
  4. 더 정확히 말씀드린다면 커널이 socket 을 이용하여 외부로부터의 자료를 수신할 수 있게 됩니다.
  5. socket에 주소와 port 를 할당하기 위해 sockaddr_in 구조체를 이용합니다.
  6. struct sockaddr_in server_addr;

    memset( &server_addr, 0, sizeof( server_addr);
    server_addr.sin_family      = PF_INET;              // IPv4 인터넷 프로토롤 
    server_addr.sin_port        = htons( 4000);         // 사용할 port 번호는 4000
    server_addr.sin_addr.s_addr = htonl( INADDR_ANY);   // 32bit IPV4 주소

    if( -1 == bindserver_socket, (struct sockaddr*)&server_addr, sizeof( server_addr) ) )
    {
       printf( "bind() 실행 에러n");
       exit( 1);
    }

  7. htonlINADDR_ANY) 는 주소를 지정해 주는 것으로 inet_addr( "내 시스템의 IP ")로도 지정할 수 있습니다. 그러나 프로그램이 실행되는 시스템 마다 IP 가 다를 것이므로 주소 지정을 고정 IP로 하지 않고 htonlINADDR_ANY) 를 사용하는 것이 편리합니다.

  이제 listen() 함수로 클라이언트 접속 요청을 확인합니다. 

if( -1 == listen( server_socket, 5))
{
    printf( "대기상태 모드 설정 실패n");
    exit( 1);
}

  1. listen() 함수를 호출하면 클라이언트의 접속 요청이 올 때 까지 대기 상태가 됩니다. 즉, 블록된 모습이 되죠.
  2. 함수가 리턴이 되었을 때에는 클라이언트의 접속이 요청 되었다든지, 아니면 에러가 발생했을 경우입니다.
  3. 에러 없이 함수가 복귀했다면 클라이언트의 접속 요청입니다.
  4. 접속 요청을 허락합니다. 

  클라이언트 접속 요청에 따라 accept()로 접속을 허락합니다. 

  1. accept()로 접속 요청을 허락하게 되면 클라이언트와 통신을 하기 위해서 커널이 자동으로 소켓을 생성합니다.
  2. 이 소켓을 client socket이라고 하겠습니다.
  3. client socket 정보를 구하기 위해 변수를 선언합니다.  그리고 client 주소 크기를 대입합니다. 
  4. int     client_addr_size;

    client_addr_size = sizeof( client_addr);

  5. accept()를 호출 후에 에러가 없으면 커널이 생성한 client socket 을 반환해 줍니다.
  6. client_socket = accept( server_socket, (struct sockaddr*)&client_addr,
                                                              &client_addr_size);

    if ( -1 == client_socket)
    {
       printf( "클라이언트 연결 수락 실패n");
       exit( 1);
    }

  이제 client socket까지 만들어 졌으므로 read(), write() 함수를 이용하여 자료를 송수신 할 수 있습니다. read() 함수를 이용하여 클라이언트로부터 전송되어 오는 자료를 읽어 들입니다.

read ( client_socketbuff_rcv, BUFF_SIZE);

  1. read() 를 이용하여 클라이언트로부터 전송된 자료를 읽어 들입니다.
  2. 만일 클라이언트로부터 전송된 자료가 없다면 송신할 때 까지 대기하게 됩니다. 즉, 블록된 모습이 됩니다.
  이번에는 wirte() 함수를 이용하여 클라이언트도 데이터를 전송합니다. 

  1. 수신된 데이터의 길이를 구하여 전송 데이터를 준비합니다.
  2. sprintf( buff_snd, "%d : %s", strlen( buff_rcv), buff_rcv);

  3. write() 를 이용하여 클라이언트로 자료를 송신합니다.

    write( client_socketbuff_snd, strlen( buff_snd)+1); // +1: NULL까지 포함해서 전송

  작업이 완료되면 close() 를 이용하여 client socket 을 소멸 시켜 데이터 통신을 종료합니다.

closeclient_socket);

클라이언트 프로그램

  클라이언트 프로그램은 서버에 비해 간단합니다. 바로 설명 들어갑니다.

  socket() 을 이용하여 소켓을 먼저 생성합니다.

int     client_socket;

client_socket = socket( PF_INET, SOCK_STREAM, 0);
if( -1 == client_socket)
{
   printf( "socket 생성 실패n");
   exit( 1);
}

  connect()를 이용하여 서버로 접속을 시도합니다. 

  1. 주소 정보에 서버의 주소와 포트번호를 지정하고
  2. 서버와의 연결을 시도합니다.
  3. 예제에서는 시스템 자기를 가르키는 IP, 127.0.0.1 을 사용했습니다.
  4. struct sockaddr_in    server_addr;

    memset( &server_addr, 0, sizeof( server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons( 4000);
    server_addr.sin_addr.s_addr= inet_addr( "127.0.0.1");  // 서버의 주소

    if( -1 == connect( client_socket, (struct sockaddr*)&server_addr, sizeof( server_addr) ) )
    {
       printf( "접속 실패n");
       exit( 1);
    }

  1. 접속에 성공하면 데이터를 전송합니다.
  2. writeclient_socket, argv[1], strlen( argv[1])+1); // +1: NULL까지 포함해서 전송

  3. 자료를 수신하고 화면에 출력합니다.
  4. read ( client_socket, buff, BUFF_SIZE);
    printf( "%sn", buff);

  5. socket 을 소멸하여 통신 작업을 완료합니다.
  6. closeclient_socket);

서버 프로그램 소스

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

#define  BUFF_SIZE   1024

int   main( void)
{
   int   server_socket;
   int   client_socket;
   int   client_addr_size;

   struct sockaddr_in   server_addr;
   struct sockaddr_in   client_addr;

   char   buff_rcv[BUFF_SIZE+5];
   char   buff_snd[BUFF_SIZE+5];



   server_socket  = socket( PF_INET, SOCK_STREAM, 0);
   if( -1 == server_socket)
   {
      printf( "server socket 생성 실패n");
      exit( 1);
   }

   memset( &server_addr, 0, sizeof( server_addr));
   server_addr.sin_family     = AF_INET;
   server_addr.sin_port       = htons( 4000);
   server_addr.sin_addr.s_addr= htonl( INADDR_ANY);

   if( -1 == bind( server_socket, (struct sockaddr*)&server_addr, sizeof( server_addr) ) )
   {
      printf( "bind() 실행 에러n");
      exit( 1);
   }

   while( 1)
   {
      if( -1 == listen(server_socket, 5))
      {
         printf( "대기상태 모드 설정 실패n");
         exit( 1);
      }

      client_addr_size  = sizeof( client_addr);
      client_socket     = accept( server_socket, (struct sockaddr*)&client_addr, &client_addr_size);

      if ( -1 == client_socket)
      {
         printf( "클라이언트 연결 수락 실패n");
         exit( 1);
      }

      read ( client_socket, buff_rcv, BUFF_SIZE);
      printf( "receive: %sn", buff_rcv);
      
      sprintf( buff_snd, "%d : %s", strlen( buff_rcv), buff_rcv);
      write( client_socket, buff_snd, strlen( buff_snd)+1);          // +1: NULL까지 포함해서 전송
      close( client_socket);
   }
}

클라이언트 프로그램 소스

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

#include "sample.h"

#define  BUFF_SIZE   1024

int   main( int argc, char **argv)
{
   int   client_socket;

   struct sockaddr_in   server_addr;

   char   buff[BUFF_SIZE+5];

   client_socket  = socket( PF_INET, SOCK_STREAM, 0);
   if( -1 == client_socket)
   {
      printf( "socket 생성 실패n");
      exit( 1);
   }

   memset( &server_addr, 0, sizeof( server_addr));
   server_addr.sin_family     = AF_INET;
   server_addr.sin_port       = htons( 4000);
   server_addr.sin_addr.s_addr= inet_addr( "127.0.0.1");

   if( -1 == connect( client_socket, (struct sockaddr*)&server_addr, sizeof( server_addr) ) )
   {
      printf( "접속 실패n");
      exit( 1);
   }
   write( client_socket, argv[1], strlen( argv[1])+1);      // +1: NULL까지 포함해서 전송
   read ( client_socket, buff, BUFF_SIZE);
   printf( "%sn", buff);
   close( client_socket);
   
   return 0;
}

//
프로그래밍/System program 2013. 4. 22. 10:58


1. 동기화 개체에 대한 잘못된 생각들

  스레드를 동기화한다 함은 스레드 간의 실행순서를 정하거나, 스레드 간 특정작업이 동시에 일어나지 않도록 구현하는 것을 말합니다. 

1) Manager Thread가 Worker Thread를 생성한 후, Worker Thread의 초기화가 끝날 때까지 기다려야 하는 경우. 이런 경우에는 보통 Event 개체나 메시지를 사용합니다.

2) 전역 데이터에 접근할 때에 한번에 스레드 한개씩만 해당 데이터를 사용하도록 구현하는 경우 : 이 경우에는 보통 CriticalSection 을 사용합니다. 일반적으로 스레드 동기화라 함은 이러한 경우를 말합니다.

3) 일정 숫자 이상의 스레드(혹은 프로세스)가 동시에 특정 자원을 사용하거나 동작하지 못하도록 하는 경우 : 이 경우에는 보통 Semaphore를 사용합니다.


CriticalSection을 사용하는 데에 있어서 초보자분들은 다음과 같은 착각을 하기 쉽습니다.

착각1) EnterCriticalSection()을 호출하면 Data에 Lock이 걸리고 다른 Thread가 접근하지 못하게 된다. (X)

==> CriticalSection 등의 동기화 개체에 Lock을 건다 함은 화장실이라는 Resource에 문을 걸어 잠그는 개념이 아니라, 화장실의 문에 "사용중"이라고 써붙이는 개념입니다. 이게 무슨 얘기냐 하면, "g_szData 라는 Data에 접근할 때는 crit_szData에 Lock을 걸어야 한다"라고 개발자가 스스로 규칙을 세우고, 코드의 모든 부분에서 g_szData를 사용할 때 CriticalSection에 Lock을 걸도록 구현해야 한다는 뜻입니다. 화장실의 문에 "사용중"이라고 써붙이고 들어갔는데, 어떤 사람이 문에 붙어있는 표시를 확인하지 않고 문을 열고 들어간다면? 운이 좋으면 문제가 안생기겠지만 재수가 없으면 민망한 상황이 벌어질 것입니다. 

엄밀히 말하면 데이터에 동기화개체는 스레드 들이 특정 데이터에 동시 Read/Write하지 못하도록 하는 기능을 제공하지 않습니다. 단지 동기화개체에 동시에 Lock을 걸지 못하는 기능을 제공할 뿐이죠. :)


착각2) 데이터에 Read할 때는 동기화가 필요 없다. 따라서 EnterCriticalSection은 Data에 Write를 수행할 때만 수행하면 된다. (X)

==> Data를 Read하는 스레드에서는 그냥 Read를 하면서 Data에 Write를 하는 스레드에서만 Lock을 건다고 해서 다른 스레드가 그 Data에 접근을 못하게 되는 것이 아닙니다. 어떤 CriticalSection에 Lock을 건다고 해서 스레드간 ContextSwitching이 중지되는 건 더더욱 아닙니다. 보호하고자 하는 데이터를 참조하는 모든 부분에 동기화 처리를 해주지 않으면 동기화 개체를 사용하는 것은 아무런 의미도 없습니다. 
간단하게 생각하면 CriticalSection이란 보호할 데이터를 사용하는 중에 BOOL형 변수에 "사용중"이라는 의미로 "TRUE" 값을 Setting하는 이상의 의미는 없습니다. 단지 두 Thread에서 동시에 TRUE를 설정하지 못하도록 OS에서 보장해준다는 점과 TRUE를 Setting하려고 할 때 이미 TRUE라는 값이 Setting되어 있다면 이 변수가 FALSE로 바뀔 때까지 스레드가 BLOCK되는 등의 처리가 자동으로 구현되어 있다는 정도가 차이가 있을 뿐입니다.
단지 프로그램 전체에 걸쳐서 어떠한 "Write"도 수행되지 않고 "Read"만 수행되는 데이터라면 동기화가 필요 없습니다. 

대부분의 개발자 분들은 이런 얘길 보면서 "이렇게 당연한  얘기를 왜 하나?"라고 생각하시겠지만... 바로 얼마 전에 십 수년간 개발을 하신 서버 개발자께서 "CriticalSection에 Lock을 걸면 말이쥐... 다른 스레드들은 이 데이터에 접근을 못하게 되는 거란다"라고 친절하게 설명해주시는 걸 듣고... 까무라치는 줄 알았습니다. ㅡ.ㅡ

※ 선언 함수나 초기화 함수를 보면 알수 있듯이, CriticalSection은 그 자체로서 어떠한 데이터와도 연관되지 않습니다. 어떠한 데이터를 보호하는데 어떤 CriticalSection 개체를 사용하겠다 하는 것은 순전히 개발자의 로직상으로 구현되는 부분입니다. 위에서 얘기했듯이 OS에서는 동시에 한개의 스레드만 EnterCriticalSection에 성공할 수 있다는 것 외에는 아무것도 보장하지 않습니다. 나머지는 개발자의 몫인 거죠.


2. Case I : Multiple-Reader + Multiple-Writer 환경 (CriticalSection Per Data Model)

  멀티스레드 환경에서 발생할 수 있는 가장 일반적인 경우는 다수의 스레드가 동시에 데이터를 읽고 쓰는 경우입니다. 이 경우에는 어떠한 "읽기"와 "읽기", "읽기"와 "쓰기" 혹은 "쓰기"와 "쓰기" 도 동시에 발생해서는 안되며, 완전하게 동시접속이 차단되어야 합니다. 이렇게 처리하는 것을 직렬화(Serialize)한다고 합니다.

  이런 환경을 구현하기 위해서는 CriticalSection을 접속하고자 하는 Data의 수만큼 만들고, (읽기/쓰기에 상관없이) Data에 접근할 때마다 해당 Data에 Matching 되는 CriticalSection에 대해 EnterCriticalSection을 수행해주어야 합니다. 말하자면 "CriticalSection Per Data" 정도 되겠네요.

가장 안전한 환경이 구현되지만, 병렬수행이 불가능해지므로 성능은 최악이 됩니다. 

그림으로 표현하면 다음과 같이 되겠네요.



3. Case II : Multiple-Reader + Single-Writer 환경 (CriticalSection Per Thread Model)

  모든 스레드가 읽기와 쓰기를 행한다면 Case I  과 같이 직렬화를 시켜야겠지만, 대부분의 Server용 Application에서는 다수의 Worker Thread는 전역 데이터에 대해 Read만을 수행하고 한개의 Manager Thread가 Write를 수행하는 구조인 경우가 많습니다. 대표적인 예가 Manager Thread가 기록한 정책을 참조하여 Worker Thread가 클라이언트의 접속을 처리하는 경우를 예로 들 수 있겠죠. (Thread Pool 이 보통 이런 동작을 합니다.)

이러한 경우는 다음과 같은 특징이 있습니다.


1) Read의 빈도가 매우 높고, Write의 빈도는 낮다. 

2) Read와 Read는 동시에 이루어지는 것이 바람직하지만 Write와 Read는 동시에 이루어져선 안된다.

3) WokerThread간의 병렬처리가 이루어져야 하므로 Read의 속도가 매우 중요하며 Write의 성능은 상대로 덜 중요하다.


위의 조건을 만족시키기 위해서는 스레드와 동기화 개체들을 어떤 식으로 구성해야 할까요? 제가 내린 결론은 다음과 같습니다. (더 좋은 방법을 아시는 분은 가르쳐주세요. ^^)

1) WorkerThread 마다 CriticalSection을 하나씩 생성한다. WorkerThread는 각자 자기의 CriticalSection을 가지고 있다. 말하자면 워커스레드가 6개라면 CriticalSection도 6개가 생성되며 이것이 Array처럼 접근되도록 구현된다.

2) WorkerThread 는 작업을 시작할 때마다 자기의 CriticalSection에 대해 EnterCriticalSection()을 호출하고 작업을 끝낸 후에는 LeaveCriticalSection을 호출합니다. 이렇게 되면 각 WorkerThread 간에는 작업이 병렬수행되며, 전역 Data에 대해서도 병렬 Read가 이루어집니다. (이때 ManagerThread 는 쉬고 있겠죠)

3) ManagerThread 가 정책을 업데이트해야 할 때는 For Loop 를 돌면서 모든 CriticalSection에 대해 EnterCriticalSection()을 호출합니다. 일단 ManagerThread가 CriticalSection에 Lock을 걸고 나면 해당 WorkerThread는 새로운 작업을 시작하지 못하기 때문에, For Loop가 끝나게 되면 결과적으로 ManagerThread는 모든 WorkerThread를 정지시키게 됩니다. (이때는 물론 시간이 좀 걸리겠죠? ^^)

4) ManagerThread는 정책을 수정합니다.

5) ManagerThread는 For Loop를 돌면서 모든 CriticalSection에 대해 LeaveCriticalSection()을 호출합니다. 이제 WorkerThread들은 다시 자신의 CriticalSection을 획득하고 밀린 일을 처리할 수 있습니다.


그림으로 표현하면 대략 다음과 같이 되겠네요.


WorkerThread간의 읽기작업을 표현하면 다음과 같습니다.


ManagerThread가 쓰기 작업을 진행할 때는 다음과 같이 됩니다.


말하자면... Write속도를 희생시켜서 Read속도를 증가시킨다고나 할까요? ^^


4. Case III : 동기화를 하지 않는 방법(?)

  "멀티스레드 프로그래밍에 관한 고찰 (1)" 에서 논했듯이 멀티스레드가 Data에 동시접근한다고 해도 경우에 따라서는 동기화 하지 않고 사용할 수 있습니다. 만약 이게 가능하다면 멀티스레드 프로그램은 최고의 효율을 얻을 수 있습니다. 이렇게 구성하기에 앞서 해당 Case가 이런 방식으로 구성 가능한지 검토해야 하며, 가능하다면 자료구조가 안전한 동시접근이 가능하도록 설계되어야 합니다.

정수형 Data가 Linked List에 저장되는 경우의 예를 들어 보겠습니다.

1) Data가 변경될 때 Linked List에 Entry가 추가/삭제되는 경우에는 반드시 스레드 동기화가 필요합니다. 반대로 시종일관 LinkedList 자체는 변화가 없이 각 Entry의 정수형 데이터값만 변경시키는 경우에는 동기화 없이도 Concurrent Read/Write가 안전하게 수행될 수 있습니다. 
   이러한 경우의 예를 들면 오목게임의 바둑판을 들 수 있습니다. 모든 좌표에 대해 게임을 시작하기 전에 19 X 19 개의 좌표 Data를 생성한 후, 게임이 진행되는 동안에는 해당 좌표에 대한 Data(흑/백/無)만 Update될 뿐 새로운 좌표가 추가되지는 않습니다. 이런 경우라면 동기화가 필요하지 않습니다.

2) 만약 Data가 Linked List에 추가/삭제되어야 하는 경우라면, 추가되는 데이터의 "경우의 수"에 대해 생각해보아야 합니다. 경우에 따라서는 Insert될 수 있는 Data의 경우의 수가 한정되어 있어 초기화시에 모든 경우의 수에 대한 메모리를 미리 생성해놓고 시작할 수 있는 경우가 있습니다. 이러한 경우라면 동기화 없이 멀티스레드가 동작할 수 있습니다.
   예를 들어, 사내 IP체계를 B Class를 사용하는 회사에서 IP를 사용하는 Mac주소와 사용자 정보를 저장하는 자료구조가 있다고 가정해보겠습니다. 

. 새로운 IP가 생성될 때마다 Linked List에 Entry가 생성되는 방식으로 설계한다면 스레드 동기화가 필요하며, 동기화하지 않을 경우 Entry를 LinkedList에 Insert하는 순간 Access Violation이 발생할 수 있습니다.

. 모든 경우의 수 (255 * 255 = 65525개)만큼의 Entry를 처음에 일괄 생성해놓고, 사용하지 않는 IP 에 대해서는 FALSE, 사용하는 IP에 대해선 TRUE를 기록하여 미사용 IP를 관리하고, 사용중인 IP에 대해서는 Mac주소와 사용자정보를 미리 할당된 메모리에 memcpy하는 방식으로 자료구조를 설계할 경우 동기화 처리 없이도 안전하게 Concurrent Read/Write가 가능합니다. 한마디로 "메모리를 희생하여 속도를 향상"시키는 방법이죠 :)




출처 : http://kuaaan.tistory.com/116

'프로그래밍 > System program' 카테고리의 다른 글

멀티스레드에 관한 글  (0) 2013.04.22
mmap  (0) 2013.04.21
//
프로그래밍/System program 2013. 4. 22. 10:57

  예전에는 고사양이라 하면 힘쎈 CPU를 의미했지만 지금 시대의 고사양이란 CPU 여러 개를 의미합니다. 이른바 멀티코어의 시대죠. 예전에 3.4GHz 4CPU가 최고사양 서버였다고 하면 요즘에는 1.6GHz 16Core (4Core * 4ea)가 동급으로 받아들여집니다. 

 이러한 H/W적인 패러다임의 변화는 S/W 개발에도 영향을 주어 예전에는 (어셈블리) 코드 한줄이라도 줄이는 게 퍼포먼스 향상의 열쇠였다고 한다면 지금은 여러 스레드(Thread) 들이 한 머쉰에서 서로 엉키지 않고(!) 조화롭게 돌아가는 구조를 구현하는 것이 퍼포먼스 향상의 열쇠라고 볼 수 있습니다. 

 그렇다면 멀티스레드 프로그램의 성능을 결정하는 열쇠는 무엇일까요?

 먼저 "스레드의 수"를 생각해 볼 수 있습니다.
 멀티스레드 프로그래밍을 해보시지 않으신 분들은 일단 스레드를 많이 만들면 퍼포먼스도 올라갈 것이라고 생각하시는 경향이 있읍니다만 이것은 잘못된 생각입니다. 
 중요한 것은 한개의 CPU는 동시에 한 개 씩의 스레드만 실행시킬 수 있다는 사실입니다. 스레드가 여러개가 생성되면 CPU는 각각의 스레드를 시분할하여 각각의 스레드를 번갈아가며 실행하게 되는데, 이때 이전 스레드의 문맥 정보 (레지스터 값, 실행중인 스택 정보 등)을 백업받고 백업받아놓았던 다음 스레드의 문맥정보를 로딩하는 과정을 거치게 됩니다. 이 과정을 Context Switching(문맥 교환이라고 번역하더군요) 이라고 하는데, 이러한 스레드가 많아질 수록 Context Switching 에 많은 부하가 걸리기 때문에 오히려 퍼포먼스는 떨어지게 됩니다. 
 그렇다면 스레드가 적을수록 퍼포먼스가 좋아질까요? 물론 그렇지는 않습니다. ^^
 

 위 그림과 같이 스레드는 작업을 진행함에 따라 Running <-> Waiting , Sleeping, Blocked 등으로 상태변화를 하게 되는데, 멀티스레드 프로그램은 한 스레드가 Waiting, Sleeping, Blocked 중일 때 CPU가 다른 스레드를 실행시킬 수 있기 때문에 동시성(Concurrency)이 높아져서 성능이 좋아지는 것입니다. 바꾸어 말하면 I/O가 많이 발생하는 프로그램일 수록 멀티스레딩의 효과를 크게 볼 수 있다고 말할 수도 있습니다.

위의 두가지 면을 종합할 때 멀티스레드 프로그램의 성능은 스레드의 "동시성 향상 효과"와 "Context Switching 비용"의 Trade Off 에 의해 결정된다고 말할 수 있습니다.
 따라서 가장 적절한 스레드의 수는 CPU의 수와 스레드가 수행하는 작업의 성격을 함께 고려하여 결정되는데 일반적으로는 연산 위주의 작업의 경우 CPU당 2~3개, I/O 위주 작업의 경우 CPU당 5개 내외를 적절한 스레드의 수로 가이드하며, 보통 스레드 풀(Thread Pool)을 생성할 때 워커 스레드 (Worker Thread)의 수를 산정하는 방식으로 사용됩니다.

 요즘 네트워크 프로그램에서 많이 사용되는 IOCP (Input Output Completion Port)가 성능이 좋은 이유는 I/O가 진행되는 동안 스레드가 스위칭되거나 블러킹되지 않기 때문에 스레드의 Context Switching을 최소화할 수 있고, 따라서 필요한 스레드의 수를 최소화할 수 있기 때문입니다. 그런 면에서 어찌보면 최소한의 스레드를 써야 높은 성능을 낼 수 있다고 볼 수 있는 거죠.


   그렇다면 "동시성 향상 효과"를 높이는 요인은 무엇이 있을까요? 바로 "효율적인 스레드 동기화" 를 들 수 있습니다.
  멀티스레드 프로그래밍은 프로세스의 동시성을 향상시켜 성능을 향상시키는 효과가 있지만, 여러 스레드 들이 동시에 데이터를 접근하면서 생기는 문제들이 빛과 그림자처럼 쫓아다니게 됩니다. 예를 들어 스레드 A가 링크드 리스트에서 Read를 시도하려고 하는 순간에 스레드 B가 해당 링크드 리스트에 ClearAll() 을 수행한다면 어떤일이 벌어질까요? 스레드 A는 메모리 폴트를 발생시키고 해당 프로세스는 중지되고 말 것입니다. 이러한 일을 방지하기 위해서는 스레드들이 링크드 리스트에 동시에 접근하지 못하도록 직렬화(Serialize)시킬 필요가 있는데 이런 작업을 "스레드 동기화"라고 합니다. 
  스레드 동기화가 잘못되었을 때 생기는 문제들이나 동기화 개체 사용법 등에 대해서는 인터넷에 좋은 포스트가 많이 공개되어 있으니, 여기서는 어떻게 하면 보다 효율적인 스레드 동기화를 구현할 수 있을 것인가에 대해 생각해보겠습니다. 
 문제는 스레드 동기화의 정도가 높아질수록 데이터에 대한 단일접근이 보장되어 안정성과 데이터 무결성은 높아지지만, 반면에 데이터에 접근할 때의 동시성이 저하되어 성능은 (형편없이) 떨어지게 됩니다. 여기서도 Trade Off 문제가 발생하는 거죠. 원칙적으로 모든 전역 데이터(Global Data)에 접근할 때 CriticalSection등의 동기화 개체를 사용하여 동시접속을 차단하는 것이 원칙이겠지만 그렇게 하게 되면 너무 성능이 떨어지기 때문에, 대부분의 서버 개발자들은 "이정도는 괜찮더라"는 나름대로의 선을 정해놓고, 그 범위 안에서 "적당한 물타기"를 시도하게 됩니다. 
 멀티스레드 프로그래밍을 할 때 제 나름대로의 동기화 기준을 공개한다면 대략 다음과 같습니다.

1. 전역 데이터에 멀티스레드가 읽기를 동시에 시도하는 경우에는 동기화 할 필요가 없습니다. 

2. 전역 데이터에 멀티스레드가 읽기와 쓰기를 혹은 쓰기와 쓰기를 동시에 시도하는 경우에는 "원칙적으로" 직렬화를 시켜야 합니다. (직렬화를 한다 함은 동시접근을 차단한다는 뜻입니다.) 이때, 저는 읽기 및 쓰기가 행해지는 데이터의 성격에 따라 다음과 같이 "적당한 대처"를 합니다. ^^
   1) 멀티CPU 환경에서는 기본적으로는 4바이트 정수 연산 하나에 대해서도 원자성을 100% 보장할 수 없습니다. 따라서 동기화의 비용(성능 감소)와 데이터 무결성이 깨어졌을 때의 발생가능한 손해를 비교하여 동기화 수준을 결정해야 합니다.
   2) 포인터 데이터에 대한 무결성 문제는 바로 메모리 폴트로 이어집니다. 따라서 대상 데이터 중에 "포인터"가 포함된 경우에는 반드시 동기화를 시켜야 합니다. 
   3) 단순한 정수/실수형의 경우 "대부분의 경우"에는 동기화에 신경쓸 필요가 없습니다. 하지만 이 데이터가 무결성에 얼마나 민감한지를 검토해볼 필요는 있습니다. 예를 들어, 어떤 일을 해야 할지 말지를 나타내는 Boolean (True/False) 값 등은 굳이 동기화할 필요가 없습니다. 만약 어떤 값을 카운트하는 변수일 경우, 카운트가 1~2개정도 어긋나면 어떤 문제가 생길지를 체크해봅니다. 만약 ++연산을 100번했을 때 99만 증가해도 큰 문제가 되지 않는다면 동기화하지 않아도 되겠지만, 정확한 카운트가 보장되어야 하는 경우라면 해당 변수를 Volatile 로 선언한 후 InterlockedAdd 함수 등을 이용해 동기화해주어야 합니다.
   4) Linked List, Binary Tree 등의 자료구조는 각 Entry들이 포인터로 연결되어 있습니다. 따라서 반드시 동기화해주어야 합니다. STL도 예외는 아닙니다. 다만, Linked List에 Entry가 Insert, Delete되지 않는다면 (포인터 연산이 일어나지 않기 때문에) 동기화가 필요하지 않을 수도 있습니다.
   5) 구조체나 클래스, 배열 등의 경우에는 각 Entry 의 데이터 타입이나 성격에 따라 위의 1)~3) 기준에 따라 판단해주면 됩니다.
   6) Memory Mapped File 등 프로세스간 공유되는 데이터의 경우에는 가급적이면 동기화해주는 것이 좋습니다.

3. 요즘에는 그런 일은 거의 없겠지만, 멀티CPU에서 실행되지 않는다는 것이 보장된다면... 사실 왠만한 동기화는 신경 안써도 됩니다. 뒤집어서 말하면... 멀티CPU에서는 동기화에 대해 깊이 고민해야 합니다.


다음번에는 각 케이스 별로 데이터 무결성을 보장하면서 성능 저하를 최소화하는 동기화 방법(구조)에 대해 생각해 보려고 합니다.


'프로그래밍 > System program' 카테고리의 다른 글

멀티스레드에 관하여 2  (0) 2013.04.22
mmap  (0) 2013.04.21
//
프로그래밍/System program 2013. 4. 21. 03:36

void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);

파일이나 디바이스를 응용 프로그램의 주소 공간 메모리에 대응시킨다.

 

1인자 => 시작 포인터 주소 (아래의 예제 참조)

2인자 => 파일이나 주소공간의 메모리 크기

3인자 => PROT 설정 (읽기, 쓰기, 접근권한, 실행)

4인자 => flags는 다른 프로세스와 공유할지 안할지를 결정한다.

5인자 => fd는 쓰거나 읽기용으로 열린 fd값을 넣어준다.

6인자 => offset은 0으로 하던지 알아서 조절한다.

 

int munmap(void* start, size_t length);

할당된 메모리 영역을 해제한다.

 

1인자 => 위에 mmap으로 지정된 포인터값 넣어주고

2인자 => 위에서 사용했던 length와 동일하게 넣어준다.

(왜냐면.. 할당했던거 동일하게 해제해야 하니깐..)

 

더 자세한 사항은 man page에 모든게 나와있음.

 

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

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
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/mman.h" /* mmap() is defined in this header */
 
int main (int argc, char *argv[])
{
    int fdin, fdout;
    char *src, *dst;
    struct stat statbuf;
 
    if (argc != 3) {
        printf("usage: a.out <fromfile> <tofile>\n");
        return -1;
    }
 
    /* open the input file */
    if ((fdin = open (argv[1], O_RDONLY)) < 0) {
        printf ("can't open %s for reading", argv[1]);
        return -2;
    }
    /* open/create the output file */
    if ((fdout = open(argv[2], O_RDWR|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR)) < 0) {
        printf ("can't create %s for writing", argv[2]);
        return -2;
    }
 
    /* find size of input file */
    if (fstat (fdin,&statbuf) < 0) {
        printf ("fstat error");
        return -2;
    }
 
    /* go to the location corresponding to the last byte */
    if (lseek (fdout, statbuf.st_size - 1, SEEK_SET) == -1) {
        printf ("lseek error");
        return -2;
    }
 
    /* write a dummy byte at the last location */
    if (write (fdout, "", 1) != 1) {
        printf ("write error");
        return -2;
    }
    /* mmap the input file */
    if ((src = mmap(0, statbuf.st_size, PROT_READ,
                    MAP_SHARED, fdin, 0)) == (caddr_t) -1)
    {
        printf ("mmap error for input");
        return -2;
    }
 
    /* mmap the output file */
    if ((dst = mmap(0, statbuf.st_size, PROT_READ | PROT_WRITE,
                    MAP_SHARED, fdout, 0)) == (caddr_t) -1)
    {
        printf ("mmap error for output");
        return -2;
    }
 
    /* this copies the input file to the output file */
    memcpy (dst, src, statbuf.st_size);
 
    munmap(src, statbuf.st_size);
    munmap(dst, statbuf.st_size);
 
    return 0;
} /* main */
/* The end of function */
</tofile></fromfile></fcntl.h></string.h></stdlib.h></stdio.h>
1
2
3
4
5
6
7
8
9
실행 후 결과 화면
$ ./a.out test test_out
$ ls -al
drwxr-xr-x  2 jeon jeon 4096 Jul 17 08:39 .
drwx------ 12 jeon jeon 4096 Jul 17 08:39 ..
-rwxr-xr-x  1 jeon jeon 6410 Jul 17 08:39 a.out
-rw-r--r--  1 jeon jeon 1844 Jul 17 08:39 mmap.c
-rw-r--r--  1 jeon jeon  469 Jul 17 08:29 test
-rw-------  1 jeon jeon  469 Jul 17 08:39 test_out

 

'프로그래밍 > System program' 카테고리의 다른 글

멀티스레드에 관하여 2  (0) 2013.04.22
멀티스레드에 관한 글  (0) 2013.04.22
//
프로그래밍/JSP 2013. 4. 20. 20:56

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ page import="java.sql.*,javax.sql.*, java.io.*"%> <%! // DB 설정 String url, id = "DB네임", passwd = "DB패스워드"; public void jspInit() { try { url = "jdbc:mysql://서버주소:포트/디비명"; String option="? useUnicode=true&characterEncoding=UTF8"; url = url + option; Class.forName("com.mysql.jdbc.Driver").newInstance(); } catch (Exception e) { e.printStackTrace(); } } %> <% Connection conn=DriverManager.getConnection(url,id,passwd); PreparedStatement ptmt =conn.prepareStatement("쿼리문"); ResultSet rs=ptmt.executeQuery(); %> <% while(rs.next()) { } ptmt.close(); conn.close(); %>

'프로그래밍 > JSP' 카테고리의 다른 글

HttpseverletRequest 정리  (0) 2014.06.10
Media query에 대한 이해  (0) 2014.06.06
xml 노드 검색  (0) 2013.05.30
xml 노드 검색  (0) 2013.05.30
XML 노드 삽입  (0) 2013.05.30
//
프로그래밍/C 2013. 4. 20. 20:54

scanf 함수의 개념은 "입력 버퍼(stdin)에서 format에서 지정한 대로 읽어들
인다" 입니다. 위의 두개의 차이는 다음과 같습니다.

"%d %d" - stdin에서 숫자, 1자 이상의 공백문자(white-space character), 숫
자를 읽어들인다.
"%d %d\n" - stdin에서 숫자, 1자 이상의 공백문자, 숫자, 1자 이상의 공백문
자를 읽어들인다.

C99의 FCD인 N869 문서의 7.19.6.2.5절을 인용하겠습니다.

A directive composed of white-space character(s) is executed by reading
input up to the first non-white-space character (which remains unread),
or until no more characters can be read.

형식(format) 문자열에 있는 공백문자(white-space)는 그 다음에 공백 문자가
아닌 문자(non-white-space)가 올때까지 입력버퍼(stdin)에서 읽어들이라는
뜻입니다. 즉, "%d %d\n"는 숫자 2개, 1자 이상의 공백문자, 공백문자가 아닌
문자를 받아야만 입력이 완전하게 끝나는 것입니다. 참고로 "%d %d\n"과 "%d
%d ", "%d %d\t"는 같은 뜻을 가집니다.

말이 어렵게 되어버렸는데, 예제로 설명하겠습니다.

scanf("%d %d", &a, &b);
숫자값, 1자 이상의 공백문자, 숫자값이 입력버퍼에 있기를 기대한다.
숫자값, 1자 이상의 공백문자, 숫자값까지 읽어들인다. 그 나머지는 입력 버
퍼(stdin)에 그대로 놔둔다.

실행 결과 - 1 2를 입력했을 경우 형식(format) 문자열과 완벽히 일치한다.
그러므로 a와 b에 읽어들인 값을 저장하고 함수(scanf)를 종료한다.

scanf(%d %d\n", &a, &b);
숫자값, 1자 이상의 공백문자, 숫자값, 1자 이상의 공백문자, 공백문자가 아
닌 문자가 입력버퍼에 있기를 기대한다.
숫자값, 1자 이상의 공백문자, 숫자값, 1자 이상의 공백문자까지 읽어들인다.
그 나머지는 입력 버퍼(stdin)에 그대로 놔둔다.

실행 결과 - 1 2를 입력했을 경우 "%d %d"까지 일치가 되지만 \n 문자 때문에
1자 이상의 공백문자와 그 뒤에 공백문자가 아닌 문자를 기다리게 됩니다. 여
기에 3을 또 입력하게 되면 공백문자까지 읽어들이게 되고 나머지(3)는 그대
로 입력버퍼에 남게 됩니다. 이 남아있는 3은 다음 scanf문이 실행될 때 영향
을 주게 됩니다.

그리고 한가지 더. 다음의 예제 코드를 봐 주십시오.

#include <stdio.h>

int main(void)
{
    int a, b;

    printf("\n>>");


    scanf("%d %d\n", &a, &b);
    printf("[%d,%d]\n>>", a, b);

    scanf("%d %d\n", &a, &b);
    printf("[%d,%d]\n>>", a, b);

    return 0;
}

이 프로그램의 실행 결과는 다음과 같습니다.

$ gcc 3.c -o 3
plsj@localhost: ~
$ ./3

>>1 2    <- 1과 2를 입력한다.
3    <- 숫자 2개가 입력되었는데도 입력을 기다린다. 3을 입력한다.
[1,2]    <- 입력된 숫자 출력
>>4 5    <- 숫자 2개를 또 입력
[3,4]    <- 4 5를 출력하는게 아니라 3과 4를 출력한다.
>>plsj@localhost: ~
$ ./3

>>1 2    <- 1과 2를 입력한다.
a    <- 숫자 2개가 입력되었는데도 입력을 기다린다. 3을 입력한다.
[1,2]    <- 입력받은 값을 출력한다.
>>[1,2]    <- 여기에서 입력을 한번 받아야되는데 받지 않고 입력받은 값을

              또한번 출력한다.
>>plsj@localhost: ~
$


첫번째 실행에서 1 2가 입력되면, "%d %d\n" 중에서 "%d %d"까지 일치가 됩니
다. 그러나 \n이 남아있기 때문에 1자 이상의 공백문자와 공백문자가 아닌 문
자의 입력을 기다리게 됩니다. 여기에 3을 입력하면 3 이전의 공백문자까지
읽어들이게 되어 첫번째 scanf 함수가 끝나게 되고 3은 입력버퍼에 그대로 남
습니다. 그다음의 scanf 함수가 실행되면 입력버퍼에 있는 3을 읽어들이게 되
고 " %d\n"이 남게 됩니다. 여기에 4 5를 입력하게되면 "4 "까지 읽어들이게
되고 5는 그대로 입력버퍼에 남게 됩니다. 즉 a에는 3이, b에는 4가 입력되는
것이죠.

두번째 실행에서도 마찬가지 입니다. 다만 첫번째 scanf 함수가 끝난 후에 3
대신 a가 입력버퍼에 남게 되고, 두번째 scanf 함수가 실행되면 형식(format)
문자열의 %d가 a와 일치가 안되기 때문에 두번째 scanf 함수는 하는일 없이
그대로 끝나게 됩니다. 따라서 1 2가 두번 출력되게 됩니다.

//
프로그래밍/C 2013. 4. 19. 00:26

1. stack memory(스택메모리)

stack 이란 단어를 컴퓨터 사전에서 찾아보면 이렇게 정의되어 있습니다.

“후입선출(LIFO, Last-In, First Out)방식에 의하여 정보를 관리하는 자료구조. 스택에서는 톱(Top)이라고 불리우는 스택의 끝부분에서 자료의 삽입과 삭제가 발생한다. 즉, 스택에 자료를 삽입하게 되면 톱 위치에 삽입된 정보가 위치하게 된다. 그리고 스택에서 정보를 읽어오려 하면 스택의 톱 위치에 있는 정보가 반환된다. 따라서 스택에서는 가장 나중에 삽입된 정보가 가장 먼저 읽혀지는 특징을 가지고 있다.”

그림으로 설명하자면 밑의 그림과 같습니다. A→B→C→D 라는 순서대로 자료가 쌓이게 됩니다. 그리고 빠져 나올 때는 위에서부터 차례대로 D→C→B→A 빠져나오게 됩니다. 이러한 구조를 stack 구조라고 합니다.

 

  |  |     |  |     |  |     |  |     |  |     |  |      

  |__|     |__|     |__|     |__|     |  |     |  |    

  |  |     |  |     |  |     |D |     |  |     |  |        

  |__|     |__|     |__|     |__|     |__|     |  |        

  |  |     |  |     |C |     |C |     |C |     |  |        

  |__|     |__|     |__|     |__|     |__|     |__|      

  |  |     |B |     |B |     |B |     |B |     |B |        

  |__|     |__|     |__|     |__|     |__|     |__|      

  |A |     |A |     |A |     |A |     |A |     |A |      

  |__|     |__|     |__|     |__|     |__|     |__|

    

이러한 스택구조가 실생활에서 쓰이는 예로는 택시에서 볼 수 있는 동전을 넣는 원형통입니다. 동전은 원통형의 공간에 들어가고, 원통 아래쪽엔 스프링이 달려서 동전을 밀어 내줍니다. 손님에게 동전을 거슬러 줄 때 위에서부터 동전이 하나씩 나오게 되죠. 그럼 버스에서 보게 되는 동전교환기도 스택일까요? 이것은 스택이 아니라 큐라는 구조 입니다. 원통에 동전이 차곡차곡 쌓여있고, 아래쪽 동전부터 빠져나오게 됩니다.

즉 여기서 동전을 처음에 넣는 것을 push 라는 단어를 사용하게 됩니다.

    push : 하나의 데이터를 스택에 추가한다.

그리고 동전을 꺼내는 것을 pop 이라고 합니다

    pop : 하나의 데이터를 스택에서 꺼낸다.

보통 스택구조를 LIFO 라고 합니다. 즉 Last In First Out 입니다. 말 그대로 마지막에 저장한 데이터는 처음 얻어진다는 뜻입니다. 처음 집어넣은 데이터는 제일 마지막에 얻어지게 되겠지요. 이러한 방식이 컴퓨터에서는 메모리 영역 한 부분이 이런 식으로 동작하고 그 메모리 영역을 스택이라고 부르는 것입니다. 스택을 생각해 내지 못했다면 많은 변수의 사용이나, 함수 호출은 할 수가 없었을 것입니다. 그리고 스택은 프로세스가 생성될 때 필요한 만큼이 할당되어 집니다. 즉 고정사이즈 입니다. 스택 오버플로우라던가, 스택 언더플로우는 스택이 고정사이즈이기 때문에 발생하는 에러입니다.

위에서 프로세스 생성시에 스택은 정적으로 프로세스 공간에 할당 된다고 하였습니다. 그리고 스택은 내부에서도 할당과 반환이 이루어 집니다. 예를들어 스택공간이 1000byte라고 한다면 이 스택의 상대적인 주소값은 0~999까지 갖을 수 있습니다. 이 비어있는 스택에 1바이트 값을 하나 넣어주면 1바이트가 할당되는 것이라고 할 수 있습니다. 1바이트 값을 하나 스택에서 빼오는 것은 스택에서 1바이트를 반환하는 것으로 생각 할 수 있습니다. 이런 스택의 할당과 반환을 처리하기 위해서 CPU에서는 “스택 포인터”라는 레지스터가 있습니다. 이 스택포인터란 것으로 스택이 작동하며 위에서는 1byte 넣고 빼고 하는 예를 들었는데 스택 포인터를 조작하여 스택 내부에 가변적인 크기를 할당해 놓을 수도 있습니다.

그럼 일반적으로 스택이 사용되는 예를 알아보도록 하겠습니다.

스택이라는 메모리 공간에는 우리가 변수로 잡아준 것들이 위에서 설명한 방식으로 할당되고 반환됩니다. 외부 정적 변수를 제외하고 보통 자동변수라고 함수 내에서 선언하는 변수들은 스택 내부에 확보됩니다. 그리고 고정되게 위치하는 것이 아니고 동적으로 스택에 확보가 됩니다.

int ABC;

위의 ABC 변수는 4바이트가 스택에 확보되고 그 스택메모리가 변수로 사용됩니다. 블록이 닫히는 지점에서 스택 포인터 값이 변경되며 확보되어있던 부분이 소멸되지요.

배열변수도 마찬가지 입니다.

void func(void)

{

   char DEF[10];

}

프로그램이 func() 로 실행될때 스택에 10바이트를 할당하고 그 할당된 스택 메모리가 변수를 담는 공간으로 이용됩니다. 물론 10바이트만 할당 되는 것은 아닙니다. 함수호출 자체가 스택을 사용하기 때문에 기본적으로 확보되어야 할 스택에다가 덤으로 10바이트를 할당합니다.

변수가 항상 스택에 할당 되는 것은 아닙니다. 하지만 대체로 스택에 할당 됩니다.

또한 포인터 변수에서도 사용이 됩니다. 포인터 변수라고 특별난 것은 없고 그냥 일반 변수처럼 포인터 변수 또한 스택이라는 공간에 잡힙니다. 단지 포인터 변수는 그 변수에 담겨있는 값이 메모리 주소값 이라는 것뿐입니다. 포인터 변수는 그냥 일반 변수와 똑같고, 사이즈는 언제나 32비트입니다. 안에 담겨있는 값이 단지 주소라는 것 뿐입니다. 변수 자체는 일반변수처럼 스택에 할당됩니다. 단지 C 라는 컴파일러에서 포인터 변수라는 의미를 부여하는 것입니다.

또한 함수 호출 후 복귀 할 때도 사용이 됩니다. 함수를 호출하기 직전에 프로그램 카운터 값을 스택에 한번 넣어주고 호출하게 됩니다. 그 호출된 함수고 종료되어 리턴되면 CPU는 스택에서 최근 넣어진 프로그램 카운터 값을 빼오고 현재의 프로그램 카운터를 빼온 값으로 변경하여 실행하게 됩니다. 그럼 프로그램은 함수 호출 후에 다시 원래대로 카운트 증가해 나가면서 실행되는 것입니다. 이것이 함수 호출의 원리이고, 스택메모리가 사용되는 예입니다.

여기서 주의할 점은 스택에 생성된 메모리는 선언된 함수나 블록문이 끝나면 자동해제됩니다.

 

2. heap memory(힙메모리)

heap memory 는 컴퓨터 사전을 찾아보면 이렇게 정의되어 있습니다.

“ 프로그램의 실행 도중에 요구되는 기억 장소를 할당하기 위하여 운영 체제에 예약되어 있는 기억 장소 영역. 프로그램에서 실행 도중에 자료를 저장하기 위하여 기억 장소를 요청하게 되면 운영 체제에서는 힙에 존재하는 기억 장소를 프로그램에 할당한다. 그리고 프로그램에서 기억 장치를 더 이상 필요로 하지 않는 경우에는 앞에서 할당 받았던 기억 장소를 운영체제에 반납하게 되는데, 이때 운영체제에서는 반납된 기억 장소를 다시 힙에 연결하게 된다. 힙에 대한 기억 장소는 포인터를 통해 동적으로 할당되거나 반환이 되는데 연결 리스트, 트리, 그래프 등과 같이 동적인 특성을 가지고 있는 자료구조에서 널리 사용된다.”

힙은 프로그램이 실행될 때까지 알 수 없는 가변적인 양만큼의 데이터를 저장하기 위해 프로그램의 프로세스가 사용할 수 있도록 미리 예약되어 있는 메인 메모리의 영역입니다. 예를들면 하나의 프로그램은 처리를 위해 한명 이상의 사용자로부터 서로 다른 양의 입력을 받을 수 있으며 즉시 모든 입력데이터에 대해 처리를 시작합니다. 운영체계로부터 이미 확보된 일정량의 힙 저장공간을 가지고 있으면 저장과 관련된 처리를 좀 더 쉽게 할 수 있으며 일반적으로 필요할 때마다 운영체계의 운영체계에게 매번 저장공간을 요청하는 것보다 빠르게 됩니다. 프로세스는 필요할 때 heap 블록을 요구하고 더 이상 필요 없을 때 반환하며 이따금씩 자투리 모으기를 수행함으로써 자신에게 할당된 heap을 관리하기도 합니다. 여기서 자투리 모으기란 더 이상 사용되지 않는 블록들을 사용 가능한 상태로 만들고 또한 heap 내의 사용 가능한 공간을 인지함으로써 사용되지 않은 작은 조각들이 낭비되지 않도록 하는 것을 말합니다.

힙이란 컴퓨터의 기억 장소에서 그 일부분이 프로그램들에 할당되었다가 회수되는 작용이 되풀이 되는 영역입니다. 스택영역은 엄격하게 후입선출(LIFO)방식으로 운영되는데 비해 힙은 프로그램들이 요구하는 블록의 크기나 요구/횟수 순서가 일정한 규칙이 없다는 점이 다릅니다. 대개 힙의 기억장소는 포인터변수를 통해 동적으로 할당받고 돌려줍니다. 이는 연결 목록이나 나무, 그래프 등의 동적인 자료 구조를 만드는데 꼭 필요한 것입니다.

그럼 힙 메모리를 프로그램을 사용할 수 있는 자유메모리라고 할 수 있습니다. 프로그램 실행 시에 함수로 내는 데이터 등을 일시적으로 보관해 두는 소량의 메모리와 필요시 언제나 사용할 수 있는 대량의 메모리가 있습니다. 이때, 소량의 메모리를 ‘스택’이라 하고 대량의 메모리를 ‘힙’ 이라고 합니다. 이 ‘힙’이 없어지면 메모리 부족으로 ‘이상종료’를 하게 됩니다.

프로세스에 exe 이미지가 로드되고, 할당되고, 이것저것 필요한 동적 라이브러리가 로드되고 사용되지 않는 미사용 구간이 있는 것은 분명한데 그 미사용 영역이 ‘힙’이라고 합니다. 프로그램을 짤 때 new나 malloc()함수를 이용한 동적 할당을 하게 되면 힙 영역이 사용 가능하도록 되는 것입니다. 님께서 물어보신 생성자, 소멸자 역시 힙 영역에 메모리를 할당하고 해제하는것입니다. 필요한 메모리 사이즈만큼 OS에게 할당해 달라고 부탁할 수도 있으며, 사용을 다 했으면 다시 OS에게 넘겨줘야 합니다.

예를들어 

   char *p = new char[1000];

위와 같은 코드가 런타임시 힙영역에 메모리를 1000바이트 할당하는 동작을 합니다. 물론 할당을 하였으면 반환도 해주어야 겠지요. 참고로 힙을 마구 할당하고 반환시키다가 보면 선형 메모리의 중간중간이 끊어지게 됩니다. 즉 다른말로 표현하자면 메모리 블록이 여러조각으로 나뉘게 되어 비효율적으로 되어버리기도 합니다.

힙 메모리는 프로그램이 종료 될 때까지 관리하고 유지 되는 메모리 영역이기 때문에 전역함수를 쓰지 않고 프로그램 내에서 지속적으로 메모리를 참조해야 할 경우 힙 영역에 메모리를 할당 받으면 됩니다.

//