Windows가 fork ()에 가장 가까운 것은 무엇입니까?


124

나는 질문이 모든 것을 말한다고 생각한다.

Windows에서 포크하고 싶습니다. 가장 유사한 작업은 무엇이며 어떻게 사용합니까?

답변:


86

Cygwin 은 Windows에서 완전한 기능을 갖춘 fork ()를 가지고 있습니다. 따라서 Cygwin을 사용하는 것이 허용되는 경우 성능이 문제가되지 않는 경우 문제가 해결됩니다.

그렇지 않으면 Cygwin이 fork ()를 구현하는 방법을 살펴볼 수 있습니다. 꽤 오래된 Cygwin의 아키텍처 문서에서 :

5.6. 프로세스 생성 Cygwin의 포크 호출은 Win32 API 위에 잘 매핑되지 않기 때문에 특히 흥미 롭습니다. 이로 인해 올바르게 구현하기가 매우 어렵습니다. 현재 Cygwin 포크는 초기 UNIX 버전에 있었던 것과 유사한 비-기록 중 복사 구현입니다.

부모 프로세스가 자식 프로세스를 분기 할 때 가장 먼저 발생하는 것은 부모가 자식에 대한 Cygwin 프로세스 테이블의 공간을 초기화하는 것입니다. 그런 다음 Win32 CreateProcess 호출을 사용하여 일시 중단 된 자식 프로세스를 만듭니다. 다음으로 부모 프로세스는 setjmp를 호출하여 자체 컨텍스트를 저장하고 Cygwin 공유 메모리 영역 (모든 Cygwin 작업간에 공유 됨)에 이에 대한 포인터를 설정합니다. 그런 다음 자체 주소 공간에서 일시 중단 된 하위 주소 공간으로 복사하여 하위의 .data 및 .bss 섹션을 채 웁니다. 자식의 주소 공간이 초기화 된 후 부모가 뮤텍스를 기다리는 동안 자식이 실행됩니다. 아이는 그것이 분기되고 저장된 점프 버퍼를 사용하여 longjump되었다는 것을 발견합니다. 그런 다음 자식은 부모가 대기중인 뮤텍스를 설정하고 다른 뮤텍스에서 차단합니다. 이것은 부모가 스택과 힙을 자식에 복사하라는 신호입니다. 그 후 자식이 기다리고있는 뮤텍스를 해제하고 포크 호출에서 반환합니다. 마지막으로 자식은 마지막 뮤텍스에서 차단에서 깨어나 공유 영역을 통해 전달 된 메모리 매핑 영역을 다시 만들고 포크 자체에서 반환합니다.

부모 프로세스와 자식 프로세스 간의 컨텍스트 전환 수를 줄여서 포크 구현 속도를 높이는 방법에 대한 아이디어가 있지만, Fork는 Win32에서 거의 항상 비효율적입니다. 다행히도 대부분의 경우 Cygwin에서 제공하는 spawn 호출 패밀리는 약간의 노력으로 fork / exec 쌍으로 대체 할 수 있습니다. 이러한 호출은 Win32 API 위에 깔끔하게 매핑됩니다. 결과적으로 훨씬 더 효율적입니다. 포크 대신 spawn을 호출하도록 컴파일러의 드라이버 프로그램을 변경하는 것은 사소한 변경이며 테스트에서 컴파일 속도가 20 ~ 30 % 증가했습니다.

그러나 spawn과 exec는 자체적으로 어려움을 겪습니다. Win32에서 실제 실행을 수행 할 방법이 없기 때문에 Cygwin은 자체 PID (프로세스 ID)를 발명해야합니다. 결과적으로 프로세스가 여러 exec 호출을 수행 할 때 단일 Cygwin PID와 연관된 여러 Windows PID가 있습니다. 경우에 따라 이러한 Win32 프로세스 각각의 스텁이 남아있을 수 있으며 실행 된 Cygwin 프로세스가 종료 될 때까지 기다립니다.

많은 일처럼 들리지 않습니까? 그리고 예, 그것은 slooooow입니다.

편집 : 문서가 오래되었습니다.이 훌륭한 답변 을 업데이트하십시오.


11
Windows에서 Cygwin 앱을 작성하려는 경우 좋은 대답입니다. 그러나 일반적으로 최선의 방법은 아닙니다. 기본적으로 * nix와 Windows 프로세스 및 스레드 모델은 상당히 다릅니다. CreateProcess를 () 및 CreateThread ()는 일반적으로 해당 API가 있습니다
Foredecker

2
개발자는 이것이 지원되지 않는 메커니즘이며 IIRC는 실제로 시스템의 다른 프로세스가 코드 삽입을 사용할 때마다 중단되는 경향이 있음을 명심해야합니다.
Harry Johnston

1
다른 구현 링크는 더 이상 유효하지 않습니다.
PythonNut 2014

단지 다른 대답 링크에서 탈퇴 편집
Laurynas Biveinis

@Foredecker, 실제로 "cygwin 앱"을 작성하려고해도 그렇게해서는 안됩니다 . 유닉스를 모방하려고 fork하지만 누출 된 솔루션으로 이를 달성 하고 예상치 못한 상황에 대비해야합니다.
Pacerier

66

나는 이것을 한 적이 없기 때문에 이것에 대한 세부 사항을 확실히 알지 못하지만 네이티브 NT API에는 프로세스를 포크하는 기능이 있습니다 (Windows의 POSIX 하위 시스템에는이 기능이 필요합니다. POSIX 하위 시스템이 있는지 확실하지 않습니다. 더 이상 지원됩니다).

ZwCreateProcess ()를 검색하면 좀 더 자세한 정보를 얻을 수 있습니다. 예를 들어 Maxim Shatskih의이 정보는 다음과 같습니다.

여기서 가장 중요한 매개 변수는 SectionHandle입니다. 이 매개 변수가 NULL이면 커널은 현재 프로세스를 포크합니다. 그렇지 않으면이 매개 변수는 ZwCreateProcess ()를 호출하기 전에 EXE 파일에 생성 된 SEC_IMAGE 섹션 객체의 핸들이어야합니다.

참고 비록 코리나 Vinschen Cygwin에서이 ZwCreateProcess () 여전히 신뢰할 수를 사용하여 발견을 나타냅니다 :

Iker Arizmendi는 다음과 같이 썼습니다.

> Because the Cygwin project relied solely on Win32 APIs its fork
> implementation is non-COW and inefficient in those cases where a fork
> is not followed by exec.  It's also rather complex. See here (section
> 5.6) for details:
>  
> http://www.redhat.com/support/wpapers/cygnus/cygnus_cygwin/architecture.html

이 문서는 10 년 정도의 오래된 것입니다. Fork를 에뮬레이트하기 위해 여전히 Win32 호출을 사용하는 동안 메서드가 눈에 띄게 변경되었습니다. 특히, 특정 데이터 구조체가 자식에게 복사되기 전에 부모에서 특별한 처리가 필요하지 않는 한 더 이상 일시 중단 된 상태에서 자식 프로세스를 만들지 않습니다. 현재 1.5.25 릴리스에서 일시 중단 된 자식에 대한 유일한 경우는 부모의 열린 소켓입니다. 다가오는 1.7.0 릴리스는 전혀 중단되지 않습니다.

ZwCreateProcess를 사용하지 않는 한 가지 이유는 최대 1.5.25 릴리스까지 Windows 9x 사용자를 계속 지원하기 때문입니다. 그러나 NT 기반 시스템에서 ZwCreateProcess를 사용하려는 두 번의 시도는 한 가지 이유로 실패했습니다.

이 물건이 더 좋거나 문서화되어 있다면 정말 좋을 것입니다. 특히 몇 가지 데이터 구조와 프로세스를 하위 시스템에 연결하는 방법이 있습니다. 포크가 Win32 개념은 아니지만 포크를 구현하기 쉽게 만드는 것이 나쁘다고 생각하지는 않습니다.


이것은 잘못된 대답입니다. CreateProcess () 및 CreateThread ()는 일반적인 동등 항목입니다.
Foredecker 2009-06-13

2
Interix는 Windows Vista Enterprise / Ultimate에서 "UNIX 응용 프로그램 용 하위 시스템"으로 제공됩니다. en.wikipedia.org/wiki/Interix
bk1e

15
@Foredecker-이것은 잘못된 대답 일 수 있지만 CreateProcess () / CreateThread ()도 잘못되었을 수 있습니다. '일을 수행하는 Win32 방식'또는 '가능한 한 fork () 의미론에 가까운'을 찾고 있는지 여부에 따라 다릅니다. CreateProcess ()는 fork ()와 크게 다르게 동작하는데, 이것이 cygwin이이를 지원하기 위해 많은 작업을해야하는 이유입니다.
Michael Burr

1
@jon : 링크를 수정하고 관련 텍스트를 답변에 복사하려고했습니다 (따라서 향후 깨진 링크는 문제가되지 않습니다). 그러나이 대답은 충분히 전 100 % 아니에요 출신 특정 오늘 발견 견적 내가 2009 년에 언급 된 것입니다
마이클 버

4
사람들이 " fork즉시 exec"를 원하면 아마도 CreateProcess가 후보가 될 것입니다. 하지만 fork없이exec 것은 종종 바람직하며 이것이 사람들이 진짜를 요구하는 원동력 fork입니다.
Aaron McDaid

37

글쎄요, 창문에는 그와 비슷한 것이 없습니다. 특히 fork는 * nix에서 스레드 또는 프로세스를 개념적으로 생성하는 데 사용할 수 있기 때문입니다.

그래서 저는 다음과 같이 말해야합니다.

CreateProcess()/CreateProcessEx()

CreateThread()(C 애플리케이션의 경우 _beginthreadex()더 좋다고 들었습니다 ).


17

사람들은 Windows에서 포크를 구현하려고 시도했습니다. 이것이 내가 찾을 수있는 가장 가까운 것입니다.

출처 : http://doxygen.scilab.org/5.3/d0/d8f/forkWindows_8c_source.html#l00216

static BOOL haveLoadedFunctionsForFork(void);

int fork(void) 
{
    HANDLE hProcess = 0, hThread = 0;
    OBJECT_ATTRIBUTES oa = { sizeof(oa) };
    MEMORY_BASIC_INFORMATION mbi;
    CLIENT_ID cid;
    USER_STACK stack;
    PNT_TIB tib;
    THREAD_BASIC_INFORMATION tbi;

    CONTEXT context = {
        CONTEXT_FULL | 
        CONTEXT_DEBUG_REGISTERS | 
        CONTEXT_FLOATING_POINT
    };

    if (setjmp(jenv) != 0) return 0; /* return as a child */

    /* check whether the entry points are 
       initilized and get them if necessary */
    if (!ZwCreateProcess && !haveLoadedFunctionsForFork()) return -1;

    /* create forked process */
    ZwCreateProcess(&hProcess, PROCESS_ALL_ACCESS, &oa,
        NtCurrentProcess(), TRUE, 0, 0, 0);

    /* set the Eip for the child process to our child function */
    ZwGetContextThread(NtCurrentThread(), &context);

    /* In x64 the Eip and Esp are not present, 
       their x64 counterparts are Rip and Rsp respectively. */
#if _WIN64
    context.Rip = (ULONG)child_entry;
#else
    context.Eip = (ULONG)child_entry;
#endif

#if _WIN64
    ZwQueryVirtualMemory(NtCurrentProcess(), (PVOID)context.Rsp,
        MemoryBasicInformation, &mbi, sizeof mbi, 0);
#else
    ZwQueryVirtualMemory(NtCurrentProcess(), (PVOID)context.Esp,
        MemoryBasicInformation, &mbi, sizeof mbi, 0);
#endif

    stack.FixedStackBase = 0;
    stack.FixedStackLimit = 0;
    stack.ExpandableStackBase = (PCHAR)mbi.BaseAddress + mbi.RegionSize;
    stack.ExpandableStackLimit = mbi.BaseAddress;
    stack.ExpandableStackBottom = mbi.AllocationBase;

    /* create thread using the modified context and stack */
    ZwCreateThread(&hThread, THREAD_ALL_ACCESS, &oa, hProcess,
        &cid, &context, &stack, TRUE);

    /* copy exception table */
    ZwQueryInformationThread(NtCurrentThread(), ThreadBasicInformation,
        &tbi, sizeof tbi, 0);
    tib = (PNT_TIB)tbi.TebBaseAddress;
    ZwQueryInformationThread(hThread, ThreadBasicInformation,
        &tbi, sizeof tbi, 0);
    ZwWriteVirtualMemory(hProcess, tbi.TebBaseAddress, 
        &tib->ExceptionList, sizeof tib->ExceptionList, 0);

    /* start (resume really) the child */
    ZwResumeThread(hThread, 0);

    /* clean up */
    ZwClose(hThread);
    ZwClose(hProcess);

    /* exit with child's pid */
    return (int)cid.UniqueProcess;
}
static BOOL haveLoadedFunctionsForFork(void)
{
    HANDLE ntdll = GetModuleHandle("ntdll");
    if (ntdll == NULL) return FALSE;

    if (ZwCreateProcess && ZwQuerySystemInformation && ZwQueryVirtualMemory &&
        ZwCreateThread && ZwGetContextThread && ZwResumeThread &&
        ZwQueryInformationThread && ZwWriteVirtualMemory && ZwClose)
    {
        return TRUE;
    }

    ZwCreateProcess = (ZwCreateProcess_t) GetProcAddress(ntdll,
        "ZwCreateProcess");
    ZwQuerySystemInformation = (ZwQuerySystemInformation_t)
        GetProcAddress(ntdll, "ZwQuerySystemInformation");
    ZwQueryVirtualMemory = (ZwQueryVirtualMemory_t)
        GetProcAddress(ntdll, "ZwQueryVirtualMemory");
    ZwCreateThread = (ZwCreateThread_t)
        GetProcAddress(ntdll, "ZwCreateThread");
    ZwGetContextThread = (ZwGetContextThread_t)
        GetProcAddress(ntdll, "ZwGetContextThread");
    ZwResumeThread = (ZwResumeThread_t)
        GetProcAddress(ntdll, "ZwResumeThread");
    ZwQueryInformationThread = (ZwQueryInformationThread_t)
        GetProcAddress(ntdll, "ZwQueryInformationThread");
    ZwWriteVirtualMemory = (ZwWriteVirtualMemory_t)
        GetProcAddress(ntdll, "ZwWriteVirtualMemory");
    ZwClose = (ZwClose_t) GetProcAddress(ntdll, "ZwClose");

    if (ZwCreateProcess && ZwQuerySystemInformation && ZwQueryVirtualMemory &&
        ZwCreateThread && ZwGetContextThread && ZwResumeThread &&
        ZwQueryInformationThread && ZwWriteVirtualMemory && ZwClose)
    {
        return TRUE;
    }
    else
    {
        ZwCreateProcess = NULL;
        ZwQuerySystemInformation = NULL;
        ZwQueryVirtualMemory = NULL;
        ZwCreateThread = NULL;
        ZwGetContextThread = NULL;
        ZwResumeThread = NULL;
        ZwQueryInformationThread = NULL;
        ZwWriteVirtualMemory = NULL;
        ZwClose = NULL;
    }
    return FALSE;
}

4
대부분의 오류 검사가 누락되었습니다. 예를 들어 ZwCreateThread는 SUCCEEDED 및 FAILED 매크로를 사용하여 검사 할 수있는 NTSTATUS 값을 반환합니다.
BCran

1
하면 어떻게됩니까 fork충돌, 그것은 프로그램을 충돌 않거나, 스레드는 충돌합니까? 프로그램이 충돌하면 실제로 포크가 아닙니다. 나는 실제 해결책을 찾고 있기 때문에 호기심이 많으며 이것이 적절한 대안이 될 수 있기를 바랍니다.
leetNightshade 2014

1
제공된 코드에 버그가 있음을 알고 싶습니다. haveLoadedFunctionsForFork는 헤더의 전역 함수이지만 c 파일의 정적 함수입니다. 둘 다 글로벌이어야합니다. 그리고 현재 포크가 충돌하여 오류 검사를 추가합니다.
leetNightshade 2014

사이트가 죽었고 내 시스템에서 예제를 컴파일 할 수있는 방법을 모르겠습니다. 일부 헤더가 누락되었거나 잘못된 헤더가 포함되어 있다고 가정합니다. (예 그들을 표시되지 않습니다.)
폴 스텔리에게

6

Microsoft가 새로운 "Windows 용 Linux 하위 시스템"옵션을 도입하기 전에는 CreateProcess()Windows가fork() 었지만 Windows에서는 해당 프로세스에서 실행할 실행 파일을 지정해야합니다.

UNIX 프로세스 생성은 Windows와 매우 다릅니다. 이 fork()호출은 기본적으로 현재 프로세스를 거의 전체적으로 각각 자체 주소 공간에 복제하고 계속해서 별도로 실행합니다. 프로세스 자체는 다르지만 여전히 동일한 프로그램을 실행하고 있습니다. 모델 에 대한 좋은 개요는 여기 를 참조 하십시오fork/exec .

다른 방법으로 돌아가서, 윈도우의 상당 CreateProcess()는 IS fork()/exec() UNIX의 기능.

소프트웨어를 Windows로 포팅하고 번역 레이어를 신경 쓰지 않는다면 Cygwin은 원하는 기능을 제공했지만 다소 까다로 웠습니다.

물론, 함께 새로운 리눅스 서브 시스템 , 윈도우가 가지고있는 가장 가까운 것은 fork()입니다 실제로 fork() :-)


2
그렇다면 WSL이 주어지면 fork평균적인 비 WSL 응용 프로그램에서 사용할 수 있습니까?
Caesar

6

다음 문서에서는 UNIX에서 Win32로 코드를 이식하는 방법에 대한 몇 가지 정보를 제공합니다. https://msdn.microsoft.com/en-us/library/y23kc048.aspx

무엇보다도 두 시스템간에 프로세스 모델이 상당히 다르다는 것을 나타내며 fork ()와 같은 동작이 필요한 경우 CreateProcess와 CreateThread를 고려할 것을 권장합니다.


4

"파일 액세스 또는 printf를 원하면 io가 거부됩니다."

  • 당신은 당신의 케이크를 먹을 수 없습니다. msvcrt.dll에서 printf ()는 콘솔 API를 기반으로하며, 그 자체로 lpc를 사용하여 콘솔 서브 시스템 (csrss.exe)과 통신합니다. csrss와의 연결은 프로세스 시작시 시작됩니다. 즉, "중간"에서 실행을 시작하는 모든 프로세스는 해당 단계를 건너 뛰게됩니다. 운영 체제의 소스 코드에 액세스 할 수없는 경우 수동으로 csrss에 연결을 시도 할 필요가 없습니다. 대신 자신 만의 서브 시스템을 만들어야하며, 따라서 fork ()를 사용하는 애플리케이션에서 콘솔 기능을 피해야합니다.

  • 자체 하위 시스템을 구현 한 후에는 하위 프로세스에 대한 모든 상위 핸들을 복제하는 것을 잊지 마십시오 .-)

"또한 커널 모드에 있지 않으면 Zw * 함수를 사용해서는 안됩니다. 대신 Nt * 함수를 사용해야합니다."

  • 이것은 올바르지 않습니다. 사용자 모드에서 액세스 할 때 Zw *** Nt *** 사이에는 전혀 차이가 없습니다. 이들은 동일한 (상대적) 가상 주소를 참조하는 두 개의 다른 (ntdll.dll) 내 보낸 이름입니다.

ZwGetContextThread (NtCurrentThread (), & context);

  • ZwGetContextThread를 호출하여 현재 (실행중인) 스레드의 컨텍스트를 얻는 것은 잘못되고 충돌 할 가능성이 있으며 (추가 시스템 호출로 인해) 작업을 수행하는 가장 빠른 방법이 아닙니다.

2
이것은 주요 질문에 대한 답변이 아니라 다른 몇 가지 다른 답변에 답변하는 것으로 보이며 명확성을 위해 각각에 직접 답변하고 진행 상황을 더 쉽게 따르도록하는 것이 좋습니다.
Leigh

printf가 항상 콘솔에 기록한다고 가정하는 것 같습니다.
Jasen

3

fork () 시맨틱은 fork ()가 호출 될 때 자식이 부모의 실제 메모리 상태에 액세스해야하는 경우에 필요합니다. 인스턴트 fork ()가 호출 될 때 메모리 복사의 암시 적 뮤텍스에 의존하는 소프트웨어가있어 스레드를 사용할 수 없습니다. (이것은 copy-on-write / update-memory-table 의미론을 통해 최신 * nix 플랫폼에서 에뮬레이션됩니다.)

Windows에서 syscall로 존재하는 가장 가까운 것은 CreateProcess입니다. 할 수있는 최선의 방법은 부모가 메모리를 새 프로세스의 메모리 공간으로 복사하는 동안 다른 모든 스레드를 고정한 다음이를 해제하는 것입니다. Cygwin frok [sic] 클래스 나 Eric des Courtis가 게시 한 Scilab 코드는 스레드 고정을 수행하지 않습니다.

또한 커널 모드에 있지 않으면 Zw * 함수를 사용해서는 안되며 대신 Nt * 함수를 사용해야합니다. 커널 모드에 있는지 여부를 확인하고 그렇지 않은 경우 Nt *가 항상 수행하는 모든 경계 검사 및 매개 변수 확인을 수행하는 추가 분기가 있습니다. 따라서 사용자 모드에서 호출하는 것이 약간 덜 효율적입니다.


Zw * 내보내기 기호에 관한 매우 흥미로운 정보입니다. 감사합니다.
Andon M. Coleman

사용자 공간의 Zw * 함수는 안전을 위해 여전히 커널 공간의 Nt * 함수에 매핑됩니다. 아니면 적어도 그래야합니다.
Paul Stelian 2019 년


2

Windows에서 fork ()를 쉽게 에뮬레이트하는 방법은 없습니다.

대신 스레드를 사용하는 것이 좋습니다.


공평하게 구현 fork은 CygWin 이 한 것과 정확히 일치 했습니다. 당신이 이제까지 읽을 경우에, 어떻게 그들은 그것을했다,을 yor "쉬운 방법"는 총 misunderstatement :-)입니다
paxdiablo


2

다른 답변에서 언급했듯이 NT (최신 버전의 Windows 기반 커널)에는 Unix fork ()와 동등한 기능이 있습니다. 그것은 문제가 아닙니다.

문제는 프로세스의 전체 상태를 복제하는 것이 일반적으로 올바른 일이 아니라는 것입니다. 이것은 Windows와 마찬가지로 Unix 세계에서도 마찬가지이지만 Unix 세계에서는 항상 fork ()가 사용되며 라이브러리는이를 처리하도록 설계되었습니다. Windows 라이브러리는 그렇지 않습니다.

예를 들어 시스템 DLL kernel32.dll 및 user32.dll은 Win32 서버 프로세스 csrss.exe에 대한 개인 연결을 유지합니다. 포크 후에는 해당 연결의 클라이언트쪽에 두 개의 프로세스가있어 문제가 발생합니다. 자식 프로세스는 csrss.exe에 그 존재를 알리고 새 연결을 만들어야합니다. 그러나 이러한 라이브러리는 fork ()를 염두에두고 설계되지 않았기 때문에이를 수행 할 인터페이스가 없습니다.

따라서 두 가지 선택이 있습니다. 하나는 kernel32 및 user32 및 분기되도록 설계되지 않은 기타 라이브러리의 사용을 금지하는 것입니다. 실제로는 모두 인 kernel32 또는 user32에 직접 또는 간접적으로 연결되는 모든 라이브러리를 포함합니다. 즉, Windows 데스크톱과 전혀 상호 작용할 수 없으며 별도의 Unixy 세계에 갇혀 있습니다. 이것은 NT 용 다양한 Unix 서브 시스템이 취하는 접근 방식입니다.

다른 옵션은 fork ()와 함께 작동하도록 알지 못하는 라이브러리를 얻기 위해 일종의 끔찍한 해킹에 의존하는 것입니다. 그것이 Cygwin이하는 일입니다. 새 프로세스를 만들고 초기화 (csrss.exe에 자체 등록 포함) 한 다음 이전 프로세스에서 대부분의 동적 상태를 복사하고 최상의 결과를 기대합니다. 이것은 지금까지 작동합니다. 확실히 안정적으로 작동하지 않습니다. 주소 공간 충돌로 인해 임의로 실패하지 않더라도 사용중인 라이브러리가 자동으로 손상된 상태로 남아있을 수 있습니다. Cygwin이 "완전한 기능을 갖춘 fork ()"를 가지고 있다는 현재 받아 들여진 답변에 대한 주장은 ... 모호합니다.

요약 : Interix와 같은 환경에서는 fork ()를 호출하여 포크 할 수 있습니다. 그렇지 않으면 욕망에서 벗어나도록 노력하십시오. Cygwin을 대상으로하는 경우에도 꼭 필요한 경우가 아니면 fork ()를 사용하지 마십시오.


당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.