데몬의 메모리 누수문제

shamlock의 이미지

안녕하세요. 씨를 공부하고 있는 초보 개발자입니다.
이번에 데몬을 만들어서 서비스를 하고 있는데요
원인을 알수 없는 메모리누수 문제때문에 삽질을 열심히..하고 있습니다.

.. 네트워크 서버프로그램입니다.

Makefile에 옵션설정으로 데몬방식으로 컴파일할수도 있고
그냥 일반 애플리케이션으로 컴파일할 수도 있는데요

일반애플리케이션으로 동작할때는 메모리 누수 현상이 발생하지
않는데.. 데몬방식으로 동작할때는 메모리누수현상이 발생합니다.
top 명령으로 확인해보면 점차적으로 메모리가 올라갑니다.
아주 조금씩 올라가기때문에 패킷을 많이 날려본 후에야 알았습니다.
제 생각에 데몬방식은 파일에 로그를 남기고, 일반애플리케이션은
stdout에 로그를 남기는 차이일뿐인데...
데몬만들때 fork()를 두번에서 부모프로세스가 init 이 되도록 했습니다.(fork()를 한번 해도 되긴 하겠지만..걍 확실히 하기 위해서 ..)

혹시 비슷한 경험이 있으시거나
데몬방식과 일반 애플리케이션 방식에서 실수할 수 있는 부분을
말씀좀 해주십시요
해결할 수 있는 툴에 대한 소개도 감사..답답해서 미치겠네요
아무말이나 좀 던져주십시요

------------- 참고 --------------
개발환경은
SunOS 5.7 Generic_106541-36 sun4u sparc SUNW,Ultra-60 이구요

멀티스레드 형식으로 동작하거든요
클라이언트 요청이 오면 큐에 집어넣었다가 스레드풀( 비스무리하게 구현)에서 하나 꺼내와서 작업을 하는 방식입니다.
accept() 하는 스레드는 enque하고 pthread_cond_signal()해주고, 스레드 풀의 어떤 스레드는 deque하는 방식입니다.
(학교에서 생산자-소비자 알고리즘이라고 과제작성한적 있는데 그와 비슷한 방식이겠죠.. )
큐에 들어가는 내용은 클라이언트의 소켓번호(4바이트)뿐입니다.

익명 사용자의 이미지

malloc하는 곳과 free하는 곳을 다 꼼꼼히 체크해 보세요.

익명 사용자의 이미지

일반 어플리케이션으로 컴파일 했다는 말씀은
1회만 실행된다는 의미인 것 같습니다.
1회만 실행이 된다면 아마 메모리 누수 현상을 찾기가 힘들겠지요.
데몬으로 실행한다면 계속 메모리 할당과 해제를 해야하는데,
아마도 메모리 할당만 하고 해제를 제대로 못하는 것으로 추측되어 집니다.

shamlock의 이미지

관심과 답변 갑사합니다.

Quote:

일반 어플리케이션으로 컴파일 했다는 말씀은
1회만 실행된다는 의미인 것 같습니다.
1회만 실행이 된다면 아마 메모리 누수 현상을 찾기가 힘들겠지요.
데몬으로 실행한다면 계속 메모리 할당과 해제를 해야하는데,
아마도 메모리 할당만 하고 해제를 제대로 못하는 것으로 추측되어 집니다

일반애플리케이션도 루프를 돌면서 프로그램이 실행됩니다.

데몬이랑 동일하게 동작하구요

Quote:

malloc하는 곳과 free하는 곳을 다 꼼꼼히 체크해 보세요.

메모리 문제는 큐를 미리 할당하기 때문에...더이상 체크할 곳이 없습니다.
큐에서 읽어올때 아래와 같이 읽어오는 방식이거든요

void * thread_start_func(void *arg)
{
       struct _tag  elem ;// statck에 메모리가 할당되구
       deque(&elem);      // 큐에서 읽은값을 elem 주소에  복사해줍니다.
}

아무튼 관심과 답변 갑사합니다.

@.@

익명 사용자의 이미지

Quote:
Makefile에 옵션설정으로 데몬방식으로 컴파일할수도 있고
그냥 일반 애플리케이션으로 컴파일할 수도 있는데요

그렇다면 데몬으로 컴파일 할때와 일반 애플로 컴파일 할때의
차이를 보시는 게 좋을 것 같습니다.

shamlock의 이미지

#ifdef _DAEMON
daemon_init();// 포크하고 시그널 무시하는 등 데몬의 기본 동작수행
#endif

데몬으로 컴파일할때는 위의 코드가 컴파일이 됩니다.
그리구 차이점이라면 데몬일때 로그내용이 파일에 저장되는 것인데요
(일반 애플리케이션일때는 그냥 stdout으로)

그렇다면 로그내용이 파일에 저장될때 메모리 누수가 발생하는가 싶은 생각이 들어서
로그를 아예 남기지 않도록 테스트를 해보았습니다.
그래도 마찬가지였습니다.

Quote:

그렇다면 데몬으로 컴파일 할때와 일반 애플로 컴파일 할때의
차이를 보시는 게 좋을 것 같습니다.

제가 아는 차이점에 대해서는 다 테스트해보았는데 더 이상은 모르겠네요
위의 경우에 발생가능한... 실수 또는 오류가 어떤것이 있는지 궁금합니다.

답변 감사드립니다.

사족) 이런거 보면 자바가 얼마나 편한건지..새삼 느낍니다.
메모리가 부족하면 램하나 추가하면 되거든요
안전한 프로그램이 램값보다 중요하니까.. 이휴

@.@

shamlock의 이미지

top 명령으로 해보면..
SIZE RES 필드가 늘어나는데 왜 그런지 알수가 없네요

@.@

최종호의 이미지

shamlock wrote:

Quote:

malloc하는 곳과 free하는 곳을 다 꼼꼼히 체크해 보세요.

메모리 문제는 큐를 미리 할당하기 때문에...더이상 체크할 곳이 없습니다.
큐에서 읽어올때 아래와 같이 읽어오는 방식이거든요

void * thread_start_func(void *arg)
{
       struct _tag  elem ;// statck에 메모리가 할당되구
       deque(&elem);      // 큐에서 읽은값을 elem 주소에  복사해줍니다.
}

아무튼 관심과 답변 갑사합니다.

일단, '있다'에 삼선짬뽕 하나 걸겠습니다!

아마 이 경우는 아니라고 생각되지만서도, 혹시 외부 라이브러리 사용하시는 것이 있다면 해당 라이브러리에
메모리 누수 문제 버그보고가 올라와 있는지 확인해 보세요.

sangwoo의 이미지

IPC나 regex 도 내부적으로 malloc을 사용하니, leak의 원인이 됩니다.
semdt() 이라든지, reg_free() 등을 잊으신 건 아닌지도 확인해 보세요.

----
Let's shut up and code.

shamlock의 이미지

이 긴글을 읽어주실분이 계실런지..

일단............. ipc,regex는 사용하지 않습니다.
바로 윗글을 읽고...혹시 시간관련..라이브러리에서
내부적으로 메모리 누수의 문제가 있지는 않나 궁금해지는군요

원하신다면 전체소스도 공개해드립니다. ㅠㅠ

최종호 wrote:
shamlock wrote:

Quote:

malloc하는 곳과 free하는 곳을 다 꼼꼼히 체크해 보세요.

메모리 문제는 큐를 미리 할당하기 때문에...더이상 체크할 곳이 없습니다.
큐에서 읽어올때 아래와 같이 읽어오는 방식이거든요

void * thread_start_func(void *arg)
{
       struct _tag  elem ;// statck에 메모리가 할당되구
       deque(&elem);      // 큐에서 읽은값을 elem 주소에  복사해줍니다.
}

아무튼 관심과 답변 갑사합니다.

일단, '있다'에 삼선짬뽕 하나 걸겠습니다!

아마 이 경우는 아니라고 생각되지만서도, 혹시 외부 라이브러리 사용하시는 것이 있다면 해당 라이브러리에
메모리 누수 문제 버그보고가 올라와 있는지 확인해 보세요.

버그를 찾기만 하면 삼선짬뽕은 제가 사드릴께요.. 진짜 사드림 ㅎㅎ (단, 버그찾아주시면 ㅋㅋ)

일단 시스템 초기화시에 큐 메모리를 할당해 놓으므로 더이상의 malloc은 없습니다. (적어도 제가 직접 malloc 하지는 않습니다)

그리고 네트워크 관련 함수들은 스티븐스아저씨의 소스를 그대로 카피해왔으므로 버그의 가능성에서 제외시켜보겠습니다.(하지만 맨밑에 첨부는 했음)

그 다음... 시간관련 라이브러리에 문제가 있지 않을까 생각이 듭니다. 시간관련 라이브러리에 대해 제가 정확히 그 내부동작을 이해하는 것은 아니라서.. 조금 의심이 가는군요.

디버깅을 위해 아래와 같은 매크로를 선언했습니다.

////////////////////////////////////////////////////////////
// DUMP LEVEL
#define DBG_DUMP(fmt,args...)    \
do{ \
    if(__global_log_level & LOG_LEVEL_DUMP) \
        log_msg(__global_dump_file,"DUMP : %u %s (%s@%d) "fmt,\
                time(NULL),kidi_formattime(timebuf),__FILE__,__LINE__,##args); \
}while(0)


참고로 변수 timebuf는 모든 함수마다 시작부분에서 
char timebuf[128]; 이라고 선언해두고 사용합니다.

위의 매크로에서는 아래의 format_time()을 사용합니다.

char *kidi_formattime(char *logtime)
{
    return format_time(logtime, 128, "%Y년%m월%d일 %H시%M분%S초");
}

char *format_time(char *buf, int size, const char *fmt)
{
    struct tm   st_tm ;
    time_t       tim;
    tim   = time(NULL);
    localtime_r(&tim, &st_tm);
    strftime(buf, size, fmt , &st_tm);
    return buf;
}

로그관련 함수들을 정의해놓은 파일 log.c는 아래와 같습니다.
로그파일 이름은 예를 들면 a.20050228.2644.0 인데
a.날짜.ProcessId.일련번호 입니다.
일련번호는 20메가 이상이면 일련번호가 증가합니다.

#include "libcomm.h"


char g_today[20]="00000000";
int  g_today_index = 0;

static void log_doit(int , const char *, va_list , char *);


void log_err(char *logfile, const char *fmt, ...)
{
    va_list ap;
    va_start(ap,fmt);
    log_doit(1,fmt,ap,logfile);
    va_end(ap);
    return ;
}


void log_msg(char *logfile, const char *fmt, ... )
{
    va_list ap;
    va_start(ap,fmt);
    log_doit(0,fmt,ap,logfile);
    va_end(ap);
    return ;
}


static void log_doit ( int errnoflag, 
        const char *fmt, va_list ap, char *logfile)
{
    int errno_save;
    char buf[ LOGMSG_MAX ];
    char filename_buf[ 256 ];
    char timebuf[128];
    int fd;

    errno_save = errno;
    buf[0]=0;
    sprintf(buf,"[%6ld] ",getpid());
    vsprintf(buf + strlen(buf), fmt, ap);
    if(errnoflag)
        sprintf(buf+strlen(buf),": %s", strerror(errno_save));
    strcat(buf,"\n");
    fd = open_log_file(logfile,filename_buf,128);
    if( fd >= 0)
    {
        errno = 0;
        if( write(fd, buf, strlen(buf)) < 0)
        {
            syslog(LOG_ERR,"write(%d,size:%d, %s): %s\n", fd,strlen(buf),filename_buf,strerror(errno));
        }
    }
    else
        syslog(LOG_ERR,"open(%s,O_APPEND|O_CREAT,0644) fail:%s\n", filename_buf,strerror(errno));

    close(fd);

    return ;
}



int open_log_file(char *filename_prefix,char *filename_buf, int bufsize)
{
    time_t       tim;
    tim = time(NULL);
    char timebuf[128];
    struct stat stat_buf;
    int fd;

    do
    {
        log_yyyymmdd(timebuf, sizeof(timebuf));
        if( strncmp( g_today, timebuf, 8) != 0)
        {   
            // 날자가 변동되었음
            g_today_index = 0;
            strncpy(g_today, timebuf,8);
        }


        sprintf(filename_buf,"%s.%s.%ld.%d",filename_prefix, g_today,getpid(),g_today_index);
        
        if( stat(filename_buf,&stat_buf) == 0)// 성공
        {
            if( stat_buf.st_size > 20000000) // 로그 파일의 최대크기는 20메가로 정했음
            {
                g_today_index++;
                continue;
            }
            else
            {
                fd = open(filename_buf,O_WRONLY|O_APPEND|O_CREAT,0664);
                break;
            }
        }
        else
        {
            g_today_index = 0;
            fd = open(filename_buf,O_WRONLY|O_APPEND|O_CREAT,0664);
            break;
        }
    } while(1);

    return fd;
}

char *log_yyyymmdd(char *buf, int bufsize)
{
    struct tm   st_tm ;
    time_t       tim;
    tim   = time(NULL);
    localtime_r(&tim, &st_tm);
    strftime(buf,bufsize,"%Y%m%d", &st_tm);
    return buf;
}



참고로
스티븐스아저씨의 소스코드를 긁어서 나름대로 만든 소스도
첨부시켜드립니다.

#include "libcomm.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/file.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>


// 안전한 방법으로 disconnect 한다.
// 리턴값은 마지막으로 읽은 데이타의 바이트수이다
int safe_disconnect( int sock )
{
    if( sock < 0) 
        return -1;
    close(sock);
}   
    


int tcp_connect_timeo(const char *hostname, const char *service,int nsec)
{
    struct addrinfo hints, *res, *ressave;
    int  sock,n; 

    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if( (n=getaddrinfo(hostname,service,&hints,&res)) != 0) 
        return -1;
    ressave = res;
    do
    {   // 소켓 생성
    	struct  sockaddr_in *ts;
        sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if(sock < 0)
            continue;

        ts = (struct sockaddr_in *) res->ai_addr;

        // 연결요청
        if(connect_nonb(sock, (struct sockaddr *)res->ai_addr, res->ai_addrlen,nsec) == 0)
            break;
        close(sock);
    }while( (res=res->ai_next) !=NULL);
    if( res == NULL)
        return -1;
    freeaddrinfo(ressave);
    return sock;
}


struct addrinfo * 
host_serv(const char *hostname, const char *service, int family, int socktype)
{
    int     n;
    struct addrinfo hints, *res;
    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_flags = AI_CANONNAME;
    hints.ai_family = family;
    hints.ai_socktype = socktype;

    if( (n=getaddrinfo(hostname,service,&hints,&res)) != 0)
        return NULL;
    return res;
}


const char *num2ipstr(const u_char *src, char *dst, size_t size)
{
    static const char fmt[] = "%u.%u.%u.%u";
    char tmp[sizeof "255.255.255.255"];

    sprintf(tmp, fmt, src[0], src[1], src[2], src[3]);
    if (strlen(tmp) > size) {
        errno = ENOSPC;
        return (NULL);
    }
    strcpy(dst, tmp);
    return (dst);
}


int tcp_connect(const char *hostname, const char *service)
{
    struct addrinfo hints, *res, *ressave;
    int  sock,n; 

    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if( (n=getaddrinfo(hostname,service,&hints,&res)) != 0) 
        return -1;
    ressave = res;
    do
    {   // 소켓 생성
    	struct  sockaddr_in *ts;
        sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if(sock < 0)
            continue;

        ts = (struct sockaddr_in *) res->ai_addr;

        // 연결요청
        if(connect(sock, res->ai_addr, res->ai_addrlen) == 0)
            break;
        close(sock);
    }while( (res=res->ai_next) !=NULL);
    if( res == NULL)
        return -1;
    freeaddrinfo(ressave);
    return sock;
}


// listen 소켓 생성 및 listen
int tcp_listen(const char *host, const char *serv, socklen_t *addrlenp)
{
    int             listenfd, n;
    const int       on = 1;
    struct addrinfo hints, *res, *ressave;

    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_flags = AI_PASSIVE;
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0)
    {
        return -1;
    }

    ressave = res;
    do 
    {
        listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if (listenfd < 0)
            continue;       /* error, try next one */

        if( setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)))
        {
            err_sys("setsockopt fail,for %s, %s: %s", 
                    host, serv, gai_strerror(n));
        }

        if (bind(listenfd, res->ai_addr, res->ai_addrlen) == 0)
            break;          /* success */

        close(listenfd);    /* bind error, close and try next one */
    } while ( (res = res->ai_next) != NULL);

    if (res == NULL)    /* errno from final socket() or bind() */
    {
        err_sys("tcp_listen error for %s, %s", host, serv);
        freeaddrinfo(ressave);
        return -1;
    }
    listen(listenfd, 1024);
    if (addrlenp)/* return size of protocol address */
        *addrlenp = res->ai_addrlen;    

    freeaddrinfo(ressave);
    return(listenfd);
}

int connect_nonb(int sockfd, const struct sockaddr *saptr, int salen, int nsec)
{
	int				flags, n, error;
	socklen_t		len;
	fd_set			rset, wset;
	struct timeval	tval;
    if( sockfd < 0) 
        return -1;

	flags = fcntl(sockfd, F_GETFL, 0);
	fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

	error = 0;
	if ( (n = connect(sockfd, (struct sockaddr *) saptr, salen)) < 0)
		if (errno != EINPROGRESS)
			return(-1);

	/* Do whatever we want while the connect is taking place. */

	if (n == 0)
		goto done;	/* connect completed immediately */

	FD_ZERO(&rset);
	FD_SET(sockfd, &rset);
	wset = rset;
	tval.tv_sec = nsec;
	tval.tv_usec = 0;

	if ( (n = select(sockfd+1, &rset, &wset, NULL,
					 nsec ? &tval : NULL)) == 0) {
		close(sockfd);		/* timeout */
		errno = ETIMEDOUT;
		return(-1);
	}

	if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
		len = sizeof(error);
		if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0)
			return(-1);			/* Solaris pending error */
	} else
		err_quit("select error: sockfd not set");

done:
	fcntl(sockfd, F_SETFL, flags);	/* restore file status flags */

	if (error) {
		close(sockfd);		/* just in case */
		errno = error;
		return(-1);
	}
	return(0);
}

int readable_timeo(int fd, int sec)
{
    fd_set rset;
    struct timeval tv;
    if( fd < 0) 
        return -1;
    FD_ZERO(&rset);
    FD_SET(fd, &rset);
    tv.tv_sec = sec;
    tv.tv_usec = 0;
    return (select (fd+1, &rset, NULL, NULL, &tv) );
}

int tcp_timedread(int fd,char *buf,int bufsize, int sec)
{
    int   ret;

    if( fd < 0) 
        return -1;
    if( (ret=readable_timeo(fd, sec)) > 0)
    {
        return read(fd, buf,bufsize);
    }
    else if( ret == 0 ) // timeout
    {
        return 0;
    }
    else //if( ret < 0 ) // error (interrupt or other signal)
        return -1;
}

int tcp_timedreadn(int fd,char *buf,int nbyte, int sec)
{
    int   ret;
    if( fd < 0) 
        return -1;
    if( (ret=readable_timeo(fd, sec)) > 0)
    {
        return readn(fd, buf,nbyte);
    }
    else if( ret == 0 ) // timeout
    {
        return 0;
    }
    else
        return -1;
}


@.@

죠커의 이미지

shamlock wrote:
사족) 이런거 보면 자바가 얼마나 편한건지..새삼 느낍니다.
메모리가 부족하면 램하나 추가하면 되거든요
안전한 프로그램이 램값보다 중요하니까.. 이휴

자바라고 무결할까요? 70년대 부터 GC를 쓴 환경에서도 버그는 발견됩니다. :twisted:

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.