OS를 만듭시다. 어때요~ 참 쉽죠? (11)

나빌레라의 이미지

OS. 영어로 풀어서 Operating System. 한자어로 번역해서 운영체제. 이것을 만든다고 하면 사람들은 굉장히 어렵게 생각한다. 리눅스나 윈도우같은 OS를 비교대상으로 본다면 OS를 만드는것은 정말 어렵고 힘들고, 개인이 만들기엔 어쩌면 불가능에 가까운 도전일지도 모른다. 하지만 OS의 기본 개념들은 대학교 학부과정에서 가르칠 정도로 이미 보편화 되어 있고 그 개념 자체들은 그다지 어렵지 않다. 개념 구현을 중심으로 동작하는 것 자체에 의미를 둔 OS를 만드는 것은 어쩌면 도전 해 볼만 한 가치가 있는 시도가 아닐까.

목차


1회
2회
3회
4회
5회
6회
7회
8회
9회
10회

11. 컨텍스트 스위칭

싱글테스크 펌웨어와 멀티테스크 OS 의 차이는 무엇일까. 많은 차이가 있겠지만, 가장 큰 차이는 바로 컨텍스트 스위칭의 존재 유무이다. 컨텍스트 스위칭이란 무엇이냐... 스위칭. switching. 쉬운 단어다. 바꾸다. 전환하다. 정도의 뜻이 되겠다. 컨텍스트를 바꾼다. 컨텍스트는 쉽게 말해 현재 cpu가 수행중인 테스크의 상태라고 볼 수 있다. 현재 cpu에서 수행중인 테스크의 상태를 전환한다는 말이다. 더 쉽게 설명하면 A, B, C 테스크가 OS안에 존재 하는데, A테스크가 수행중이다가 B테스크로 전환 하기 위한 테크닉을 컨텍스트 스위칭이라고 한다.

자! 다시 한번 나빌눅스의 TCB 구조체를 살펴 보자.

typedef struct _navil_free_task {
    unsigned int context_spsr;          
    unsigned int context[CONTEXTNUM]; 
    unsigned int context_sp;          
    unsigned int context_lr;         
    unsigned int context_pc;        
} Navil_free_task;

cpsr을 저장하기 위한 spsr 변수와 r0~r12 까지의 데이터레지스터를 저장하는 context변수. r13(sp)와 r14(lr), r15(pc) 를 각각 저장하기 위한 sp, lr, pc 변수가 있다. 컨텍스트 스위칭을 쉽게 설명자면 위에서 설명한 cpsr과 r0~r15까지의 범용레지스터를 위 변수영역(메모리)에 저장하고, 메모리에 저장되어 있는 해당 변수의 값들을 cpu의 레지스터로 옮기는 작업이다.

염두해 두어야 할것은 컨텍스트 스위칭 작업 자체는 cpu의 특권영역인 SVC 모드나 IRQ모드에서 일어난다는 점이다. 하지만 컨택스트스위칭의 대상이 되는 레지스터는 USER영역의 레지스터이다. 앞서강좌에서 간단하게 설명한 주소지정방식을 사용하여야 한다. 이후 나오는 소스코드를 보며 설명하겠다. 일단은 swi 핸들러의 컨텍스트 스위칭 코드를 보자.

.global Core_swiHandler
Core_swiHandler:
    ldr     sp,=navilnux_current
    ldr     sp, 
 
    add     sp,sp,#4
    stmia   sp!, {r0-r12}^
    stmia   sp!, {sp,lr}^
    stmia   sp!, {lr}
 
    sub     sp,sp,#68
    mrs     r1,spsr
    stmia   sp!, {r1}
 
    ldr     sp,=svc_stack
 
    ldr     r10,[lr,#-4]
    bic     r10,r10,#0xff000000
    mov     r0,r10
 
    bl      swiHandler
 
    ldr     sp,=navilnux_next
    ldr     sp, 
 
    ldmia   sp!, {r1}
    msr     spsr_cxsf, r1
    ldmia   sp!, {r0-r12}^
    ldmia   sp!, {r13,r14}^
    ldmia   sp!, {pc}^

전 강좌에 나왔던 swi 핸들러와 코드가 많이 바뀌었다. 첫줄에 나오는 navilnux_current 변수와 밑에 나오는 navilnux_next 변수는 스케줄러에 의해서 결정되는 현재 테스크와 다음 테스크의 TCB를 가지고 있는 커널 전역 포인터 변수이다. 포인터변수이기 때문에 변수 주소로부터 값을 받아서 스택포인터(sp)에 넣는다. USER모드와 SVC모드의 레지스터를 보면, sp, lr, pc, spsr 네개의 레지스터만 독립적으로 가진다. 그중에서 맘대로 쓸수 있는건 sp 뿐이다. 그래서 SVC모드의 sp 레지스터에 현재동작중인 테스크의 TCB 첫번째 주소값을 가진다.

sp에 현재 테스크의 TCB의 시작위치를 받아온다음 거기서 4바이트를 더한다. 위쪽에 나온 TCB의 소스를 보면 가장먼저 선언된 변수가 spsr 이다. 일단 그 변수의 영역을 4바이트를 더해서 건너뛴다. 그런다음 stmia 명령으로 r0~r12까지 레지스터의 값을 메모리에 저장한다. 그러면 context배열에 값이 들어가게 된다. 여기서 봐야 할것이 stmia sp!, {r0-r12}^ 이 문장 뒤에 있는 꺽쇠 ^ 이다. ARM Addressing Modes Quick Reference 를 보면, stm 명령 (Store multiple) 의 레지스터 지정 뒤에 ^ 를 붙이면 User Mode 레지스터의 값을 가져다가 메모리에 복사 한다.

그리고 곧장이어서 User 영역의 sp와 lr을 TCB에 저장한다. 여기까지 해서 USER영역의 r0~r14까지를 모두 백업했다. TCB 구조체를 보면 그 다음 변수는 pc 자리 이다. ARM 프로세서는 Exception Mode 를 전환할때, User Mode의 cpsr 이 전환되는 Exception Mode 의 spsr 에 자동으로 저장이되고 이어서 User Mode 의 pc 가 전환되는 Exception Mode 의 lr 에 자동으로 저장된다. 그러므로 ^ 를 붙이지 않고 lr 을 메모리에 백업하면 SVC모드의 lr 이 백업되는 것이고 이 값은 Exception 이 발생하기 직전의 pc 의 값과 일치 한다. 여기까지 작업으로 USER영역의 r0~r15까지의 레지스터가 모두 백업 되었다. 이제 남은건 spsr 이다.

TCB 의 내용을 보면 spsr 변수는 맨 위에 있다. sp의 위치를 TCB의 맨위로 옮기기 위해 68바이트를 다시 뺀다. 레지스터 r0~r15 까지 16개와 spsr까지 17개의 레지스터 공간을 위로 올라가야 하므로 4byte * 17 해서 68바이트를 빼야 한다. sp의 주소를 TCB의 맨 위로 올려 놓았으므로, r1레지스터에 spsr의 값을 mrs 명령으로 가져 온다. r1 레지스터는 앞서 이미 백업 했으므로 마음껏 사용해도 된다. 그런 다음 sp 주소에 r1의 값을 백업한다. 즉 spsr의 값을 메모리에 저장한다. 이러고 나면 sp 의 값은 어디에 있을까. 맞춰 보시라. 별 의미는 없다. 그래서 답은 쓰지 않겠다. :)

이제 백업 할건 다 했다. sp의 값은 원래 가져야 할 SVC모드의 스택 시작 주소가 아니기에 define 되어 있는 SVC모드의 스택 시작값으로 sp 값을 초기화 한다음 swi 명령뒤에 붙어오는 시스템콜번호를 분리하고서 비로소 C언어로 작성된 핸들러 함수로 진입한다.

여기까지가 컨택스트 백업 과정이다.

핸들러 함수를 수행하고 나면, 다시 sp 에 navilnux_next 포인터변수의 값을 가져온다. navilnux_next 포인터 변수에는 스케줄러에 의해 결정된 다음 수행될 테스크의 TCB 이다. 컨텍스트 복구 과정은 백업과정에 비해 상대적으로 라인수가 적다.

TCB의 맨위에는 spsr 이 있다. 이 값을 r1 에 넣고 이것을 msr명령으로 spsr 로 실제로 복구 한다. spsr을 먼저 복구하는 이유는 spsr을 복구하기 위해서는 범용레지스터 하나를 사용해야 하고, 범용레지스터는 이후에 복구되어야 하므로, 레지스터를 복구하기 전에 미리 사용해서 spsr을 백업하기 위함이다.

컨택스트백업 부분에서 설명했던 대로 LDM(Load multiple) 명령의 주소지정에 ^ 를 사용하면 메모리의 값을 USER영역의 레지스터에 복구하게 된다. 이어서 r13(sp) , r14(lr) 을 User 영역에 복구한다. 다시 ARM Addressing Modes Quick Reference를 보면 LDM 의 주소지정 방식에 ^ 를 사용하고, 지정 레지스터에 pc 가 있을 경우에는 pc 에 저장되는 주소 번지로 브렌치하고 spsr 을 cpsr 에 저장한다고 나와 있다. 즉, USER 모드로 전환하고 직전에 수행되었던 위치로 되돌아 가게 된다.

여기까지 해서 컨텍스트를 복구한다. 여기까지 수행하면 스케줄러에 의해 선택된 다음 테스크로 실행이 넘어가게 된다. 이 과정은 매우 빠르게 진행되므로 동시에 여러개의 테스크가 돌아가는 것 처럼 보인다.

이어서 IRQ 모드에서의 컨텍스트 스위칭 코드를 보겠다. 실상 SVC 모드에서의 컨텍스트 스위칭 코드와 전혀 다를바 없다. 다만 앞뒤에 약간의 코드가 붙어 있다. 일단 코드를 보자.

.global Core_irqHandler
Core_irqHandler:    
    mrs     r13,cpsr
    orr     r13,r13,#0x80
    msr     cpsr,r13
 
    ldr     sp,=navilnux_current
    ldr     sp, 
 
    sub     lr,lr,#4
    add     sp,sp,#4
    stmia   sp!, {r0-r12}^
    stmia   sp!, {sp,lr}^
    stmia   sp!, {lr}
 
    sub     sp,sp,#68
    mrs     r1,spsr
    stmia   sp!, {r1}
 
    ldr     sp,=irq_stack
 
    bl      irqHandler
 
    ldr     sp,=navilnux_next
    ldr     sp, 
 
    ldmia   sp!, {r1}
    msr     spsr_cxsf, r1
    ldmia   sp!, {r0-r12}^
    ldmia   sp!, {r13,r14}^
 
    mrs     r14,cpsr
    and     r14,r14,#0x7f
    msr     cpsr,r14
 
    ldmia   sp!, {pc}^

전체적인 컨텍스트 스위칭 코드는 swi 핸들러에 있는 컨텍스트 스위칭 코드와 같다. 이전 강좌에서 설명한 pipe line 과 인지위치때문에 lr 에서 4바이트를 빼서 TCB에 저장하는것 외에는 완전히 같은 코드이다. 그리고 그 앞뒤에 코드가 있다. 앞뒤에 코드는 IRQ 를 활성화/비활성화 하는 코드이다.

    mrs     r13,cpsr
    orr     r13,r13,#0x80
    msr     cpsr,r13
 
            :
            :
 
    mrs     r14,cpsr
    and     r14,r14,#0x7f
    msr     cpsr,r14

필자는 나빌눅스를 설계할때 외부 인터럽트 중첩을 허용하지 않기로 결정했다. 그래서 IRQ핸들러에 들어가기 전에 IRQ를 비활성화 한다. cpsr의 7번 비트를 1로 하면 ARM 프로세서는 IRQ를 받아들이지 않는다. 그 상태에서 컨텍스트 스위칭 코드를 수행하고 IRQ 핸들러 함수에 들어 갔다가 나오고 다시 cpsr의 7번 비트를 0으로 하면서 IRQ를 활성화 한다.

굉장히 길고 어렵게 설명할 줄 알았는데 의외로 간단하게 컨텍스트 스위칭에 대한 설명이 끝났다. 이어서 약간 수정된 커널 함수들과 간단하게 라운드로빈으로 구현된 스케줄러 코드를 보겠다.

#include <navilnux.h>
 
extern Navil_mem_mng memmng;
extern Navil_task_mng taskmng;
 
Navil_free_task *navilnux_current;
Navil_free_task *navilnux_next;
Navil_free_task dummyTCB;
int navilnux_current_index;
 
void scheduler(void)
{
    navilnux_current_index++;
    navilnux_current_index %= (taskmng.max_task_id + 1);
 
    navilnux_next = &taskmng.free_task_pool[navilnux_current_index];
    navilnux_current = navilnux_next;
}
 
void swiHandler(unsigned int syscallnum)
{
    printf("system call %d\n", syscallnum);
}
 
void irqHandler(void)
{
    if( (ICIP&(1<<27)) != 0 ){
        OSSR = OSSR_M1;
        OSMR1 = OSCR + 3686400;
 
        scheduler();
    } 
}
 
void os_timer_init(void)
{
    ICCR = 0x00;
 
    ICMR |= (1 << 27);      
    ICLR &= ~(1 << 27);    
 
    OSCR = 0;
    OSMR1 = OSCR + 3686400; 
 
    OSSR = OSSR_M1;
}
 
void os_timer_start(void)
{
    OIER |= (1<<1);
    OSSR = OSSR_M1;
}
 
void irq_enable(void)
{
    __asm__("msr    cpsr_c,#0x40|0x13");
}
 
void irq_disable(void)
{
    __asm__("msr    cpsr_c,#0xc0|0x13");
}
 
int sched_init(void)
{
    if(taskmng.max_task_id < 0){
        return -1;
    }
 
    navilnux_current = &dummyTCB;
    navilnux_next = &taskmng.free_task_pool[0];
    navilnux_current_index = -1;
 
    return 0;
}
 
void navilnux_init(void)
{
    mem_init();
    task_init();
 
    os_timer_init();
    os_timer_start();
}
 
int main(void)
{
    navilnux_init();
    navilnux_user();
 
    if(sched_init() < 0){
        printf("Kernel Pannic!\n");
        return -1;
    }
 
    int i;
    for(i = 0 ; i <= taskmng.max_task_id ; i++){
        printf("TCB : TASK%d - init PC(%p) \t init SP(%p)\n", i+1,
                    taskmng.free_task_pool[i].context_pc,
                    taskmng.free_task_pool[i].context_sp);
    }
 
    printf("REAL func TASK1 : %p\n", user_task_1);
    printf("REAL func TASK2 : %p\n", user_task_2);
    printf("REAL func TASK3 : %p\n", user_task_3);
 
    irq_enable();
 
    while(1){
        msleep(1000);
    }
 
    return 0;
}	

navilnux.c 함수의 내용이다. 이전 강좌의 내용에 비해 수정된 부분이 있다. 몇개의 전역변수가 추가 되었고, main() 함수의 내용이 살짝 수정 되었다. 찬찬히 살펴 보자.

Navil_free_task *navilnux_current;
Navil_free_task *navilnux_next;
Navil_free_task dummyTCB;
int navilnux_current_index;

파일의 윗부분에 추가로 커널전역변수가 선언되어 있다. 위쪽에서 설명한 컨텍스트 스위칭 코드에서 눈에 익은 navilnux_current 포인터 변수와 navilnux_next 포인터 변수가 선언되어 있다. 이어서 dummyTCB 변수가 선언되어 있다. 이 변수는 OS 가 시작하면서 첫번째 컨텍스트 스위칭이 돌때 최초로 레지스터안에 들어 있는 값은 쓰레기 값이다. 이 값은 어딘가에 버려져야 한다. 그 버려져야 하는 일종의 쓰레기통 역할을 하는 TCB 가 바로 dummyTCB 전역변수 이다. 그리고 navilnux_current_index 변수는 taskmng.free_task_pool 배열의 인덱스로 사용되는데, 현재 돌고 있는 테스크의 자유테스크블럭 배열의 인덱스를 가지고 있다. 라운드로빈 스케줄링을 돌기 위해서 이 인덱스를 0 ~ max_task_id 까지의 인덱스를 순환하는데 사용한다. 이런식으로 라운드로빈 스케줄링을 구현했다. 링크드리스트를 이용해서 환형링크드리스트로 포인터회전을 이용해 구현할 수도 있으나, 그럼 코드가 살짝더 복잡해지고 설명할것이 많아진다. 이 강좌의 최초 컨셉이 최대한 쉽게 코드를 작성하는 것이기 때문에 이런식으로 조금은 수준낮아 보이지만 인덱스를 순회하는 식으로 라운드 로빈 스케줄링을 구현했다.

그 스케줄러 코드이다.

void scheduler(void)
{
    navilnux_current_index++;
    navilnux_current_index %= (taskmng.max_task_id + 1);
 
    navilnux_next = &taskmng.free_task_pool[navilnux_current_index];
    navilnux_current = navilnux_next;
}

C언어를 저번달에 공부하기 시작한 사람이라도 이해 할 수 있을 만한 수준의 명쾌한 코드이다. navilnux_current_index 를 하나 증가시켜서 일단 다음에 동작해야 할 테스크의 자유컨트롤 블록 인덱스를 지정하고, 최대 인덱스 값으로 나누어 navilnux_current_index 의 증가값이 임계값을 넘어갔을 경우 다시 0으로 돌려 놓아 인덱스를 순환하게 한다.

이렇게 인덱스를 지정하고 나면, 해당인덱스를 가지는 자유컨트롤블록으로 부터 TCB의 포인터를 받아서 navilnux_next 포인터 변수에 주소를 저장한다. 이렇게 해서 스케줄러는 다음에 수행될 테스크를 지정하는 것이다. TCB에 우선순위를 표시하는 변수를 하나 두고, 스케줄러에서 우선순위큐를 이용한다면 테스크우선순위를 가지는 OS가 되는 것이다.

스케줄러가 새로 추가 되었으니, 스케줄러를 초기화 하는 코드가 당연히 필요하다. 스케줄러 초기화는 스케줄러에 관여하는 navilnux_current 와 navilnux_next, navilnux_current_index 세개의 변수를 초기화 한다.

int sched_init(void)
{
    if(taskmng.max_task_id < 0){
        return -1;
    }
 
    navilnux_current = &dummyTCB;
    navilnux_next = &taskmng.free_task_pool[0];
    navilnux_current_index = -1;
 
    return 0;
}

스케줄러의 초기화이기 때문에 커널에 등록된 테스크가 없을때는 당연히 에러를 내야 한다. 등록된 테스크의 갯수는 taskmng.max_task_id 의 값을 확인해서 알 수 있다. 그래서 max_task_id 의 값이 0 보다 작으면 테스크가 등록되지 않았다는 뜻이기 때문에 초기화를 더이상 진행하지 않고 에러(-1)을 반환한다. 만약 테스크가 하나이상 등록되어 있다면, 초기값의 navilnux_current 에는 dummyTCB 를 지정하고, navilnux_next 에는 첫번째 테스크 (taskmng.free_task_pool[0])을 지정한다. 그래야 컨텍스트 스위칭이 돌고, 스케줄링을 받지 않고 그대로 swi 핸들러나 IRQ 핸들러가 돌더라고 무사히 첫번째 테스크부터 돌게 된다. navilnux_current_index 의 초기값은 -1 로 지정해야 스케줄러함수의 코드상으로 첫번째 스케줄링이후에 0번 인덱스(첫번째 테스크)가 스케줄링을 받을 수 있다.

    navilnux_init();
    navilnux_user();
 
    if(sched_init() < 0){
        printf("Kernel Pannic!\n");
        return -1;
    }

스케줄러를 초기화하는 함수는 반환값이 있다. 스케줄러초기화의 반환값이 음수이면 테스크가하나도 등록되어 있지 않은 상태이다. 테스크가 하나도 등록되어 있지않으면 OS가 동작해 봤자 움직일 대상이 없으므로 부팅에러로 간주한다. 그래서 에러메시지를 출력하면서 부팅을 거부 한다. 메시지는 아주 익숙한 "Kernel Pannic" 을 뿌려준다. 왠지 반갑다. :)

나빌눅스에서 스케줄러가 호출되는 시점은 딱 두가지로만 나누었다. 먼저 swi 에서 특정 시스템콜을 사용했을 때와 IRQ 로 들어오는 OS timer 에 의해서만 스케줄러가 호출된다. 아직 시스템콜을 구현하는 강좌 순서가 오지 않았으므로 기존에 구현했던 OS timer 부분에서 스케줄러를 호출 하는 코드를 추가 하였다.

void irqHandler(void)
{
    if( (ICIP&(1<<27)) != 0 ){
        OSSR = OSSR_M1;
        OSMR1 = OSCR + 3686400;
 
        scheduler();
    } 
}

1초에 한번씩, 스케줄러를 호출한다. 순서상으로 따져 보면 irqHandler() 함수에 진입하기 전에 컨텍스트 백업을 수행하고, OS timer 처리루틴으로 들어가 스케줄러를 호출해서 navilnux_next 를 지정하고, 다시 irqHandler() 함수를 빠져 나가서 컨텍스트를 복구 한다. IRQ 종류에 따라서 스케줄러를 호출하지 않고 IRQ가 발생했던 테스크로 바로 돌아가는 경우도 있기 때문에 스케줄러의 호출을 컨텍스트 스위칭에 직접 연결하지 않고, 따로 분리 했다.

그리고 컨텍스트스위칭과 스케줄러를 통해 사용자테스크들은 순서대로 계속 cpu를 받아 돌고 있으므로, 사용자테스크는 종료되면 안된다. 그래서 사용자테스크를 무한루프로 바꾸어 줘야 한다.

void user_task_1(void)
{
    int a, b, c;
 
    a = 1;
    b = 2;
    c = a + b;
 
    while(1){
        printf("TASK1 - a:%p\tb:%p\tc:%p\n", &a, &b, &c);
        msleep(1000);
    }
}
 
void user_task_2(void)
{
    int a, b, c;
 
    a = 1;
    b = 2;
    c = a + b;
 
    while(1){
        printf("TASK2 - a:%p\tb:%p\tc:%p\n", &a, &b, &c);
        msleep(1000);
    }
}
 
void user_task_3(void)
{
    int a, b, c;
 
    a = 1;
    b = 2;
    c = a + b;
 
    while(1){
        printf("TASK3 - a:%p\tb:%p\tc:%p\n", &a, &b, &c);
        msleep(1000);
    }
}


이번 회에서는 따로 추가된 파일이 없으므로 Makefile 은 수정된 바 없다. 역시나 가벼운 마음으로 make 해서 빌드 해 보자. 그리고 minicom 을 통해 보드에 다운로드 해 부팅해 보자.

TCB : TASK1 - init PC(a000bbe4)          init SP(a04ffffc)                      
TCB : TASK2 - init PC(a000bc40)          init SP(a05ffffc)                      
TCB : TASK3 - init PC(a000bc9c)          init SP(a06ffffc)                      
REAL func TASK1 : a000bbe4                                                      
REAL func TASK2 : a000bc40                                                      
REAL func TASK3 : a000bc9c                                                      
TASK1 - a:a04fffe8      b:a04fffe4      c:a04fffe0                              
TASK2 - a:a05fffe8      b:a05fffe4      c:a05fffe0                              
TASK3 - a:a06fffe8      b:a06fffe4      c:a06fffe0                              
TASK1 - a:a04fffe8      b:a04fffe4      c:a04fffe0                              
TASK2 - a:a05fffe8      b:a05fffe4      c:a05fffe0                              
TASK3 - a:a06fffe8      b:a06fffe4      c:a06fffe0                              
TASK1 - a:a04fffe8      b:a04fffe4      c:a04fffe0                              
TASK2 - a:a05fffe8      b:a05fffe4      c:a05fffe0                              
TASK3 - a:a06fffe8      b:a06fffe4      c:a06fffe0                              
TASK1 - a:a04fffe8      b:a04fffe4      c:a04fffe0                              
TASK2 - a:a05fffe8      b:a05fffe4      c:a05fffe0                              
TASK3 - a:a06fffe8      b:a06fffe4      c:a06fffe0  
                         :
                         :          


세개의 테스크가 순서대로 도는 것을 학인 할 수 있다. 테스크의 출력값에 대해서 더 설명할것이 남아 있지만, 이번 회차 강좌의 길이가 계속 길어 지고 있어서, 다음 회차로 넘기겠다. 다음회차는 그래서 조금 짧고 간단하게 끝날 것 같다.

이번 회차강좌는 정말 중요한 내용을 담고 있다. 다소 이해가 안되었더라도, 다시 한번 읽어보고 첨부 한 코드를 천천히 살펴 보기 바란다.

이 글은 http://raonlife.com/navilera/blog/view/85/에 동시 연재 됩니다.

File attachments: 
첨부파일 크기
파일 navilnux_chap11.tgz73.73 KB

댓글

tazanboy의 이미지

개발보드도 없고 해서 화면을 보고 노트에 적어가면서 공부중인데 어려운 부분도 많지만 전체적으로 설명을 참 잘하셔서 도움이 많이 되네요.

verimuch의 이미지

글 항상 잘보고 있습니다.

나빌레라님 땜에 ez보드를 사기로 맘을 먹었어요..하핫!

---------------------------------
Life is not fair, Get used to it.

paeksj98의 이미지

정말 수고 많으십니다.

나빌레라의 이미지

감사합니다.
오늘은 12편이 올라갈겁니다..^^;
--
얇은 사 하이얀 고깔은 고이 접어서 나빌레라

----------------------
얇은 사 하이얀 고깔은 고이 접어서 나빌레라

익명 사용자의 이미지

좋은 내용이네요

댓글 달기

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
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.