Linux 메모리 관리 소개

青灯夜游
풀어 주다: 2019-03-28 13:54:54
앞으로
3619명이 탐색했습니다.

이 글의 내용은 리눅스 메모리 관리 관련 지식을 이해할 수 있도록 리눅스 메모리 관리를 소개하는 것입니다. 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.

Linux에서 시스템이나 프로세스의 메모리 사용량을 확인하기 위해 top, vmstat, free 등의 명령을 사용하면 buff/cache memeory, swap, avail Mem 등이 자주 표시됩니다. 무슨 뜻인가요? 이 기사에서는 Linux에서의 메모리 관리에 대해 설명하고 이 질문에 답할 것입니다.

리눅스에서의 메모리 관리에 대한 논의는 사실 리눅스에서의 가상 메모리 구현에 대한 논의입니다. 저는 커널 전문가가 아니기 때문에 이 글에서는 몇 가지 개념적인 내용만 소개하고 구현 세부 사항에 대해서는 자세히 설명하지 않을 수도 있습니다. . 정확한.

초창기에는 물리적 메모리가 상대적으로 제한되어 있었는데, 사람들은 프로그램이 사용할 수 있는 메모리 공간이 실제 물리적 메모리보다 클 수 있기를 바랐기 때문에 가상 메모리라는 개념이 등장했습니다. 초기 아이디어를 훨씬 뛰어넘었습니다.

1. 가상 메모리

가상 메모리는 Linux에서 메모리를 관리하는 기술입니다. 이는 각 애플리케이션이 독립적이고 연속적인 사용 가능한 메모리 공간(연속적이고 완전한 주소 공간)을 가지고 있다고 생각하게 하지만 실제로는 일반적으로 여러 물리적 메모리 세그먼트에 매핑되고 일부는 임시로 디스크 스토리지에 저장되었다가 로드됩니다. 필요할 때 메모리에 저장됩니다.

각 프로세스가 사용할 수 있는 가상 주소의 크기는 CPU 비트 수와 관련이 있습니다. 32비트 시스템에서 가상 주소 공간 크기는 64비트 시스템에서 2^64=입니다. ? (그것을 알아낼 수 없습니다). 실제 물리적 메모리는 가상 주소 공간의 크기보다 훨씬 작을 수 있습니다.

가상 주소는 프로세스와 밀접하게 관련되어 있습니다. 서로 다른 프로세스의 동일한 가상 주소가 반드시 동일한 물리적 주소를 가리킬 수는 없으므로 프로세스를 떠나지 않고 가상 주소에 대해 이야기하는 것은 의미가 없습니다.

참고: 인터넷의 많은 기사에서는 가상 메모리를 스왑 공간과 동일시합니다. 실제로 스왑 공간은 가상 메모리의 큰 청사진의 일부일 뿐이므로 설명이 엄격하지 않습니다. ㅋㅋㅋ 가상 주소는 명령어를 얻을 때 사용됩니다. 이 주소는 프로그램이 링크될 때 결정됩니다(동적 라이브러리의 주소 범위는 커널이 프로세스를 로드하고 초기화할 때 조정됩니다). 실제 For 데이터를 얻기 위해서는 CPU가 가상 주소를 물리적 주소로 변환해야 하며, CPU가 주소를 변환할 때 프로세스의 페이지 테이블을 사용해야 하며, 페이지 테이블의 데이터는 유지됩니다. 운영 체제에 의해.

참고: Linux 커널 코드는 메모리에 액세스할 때 실제 물리적 주소를 사용하므로 가상 주소에서 물리적 주소로의 변환이 없으며 응용 프로그램 계층 프로그램에만 필요합니다. 변환을 용이하게 하기 위해 Linux는 가상 메모리와 물리적 메모리를 모두 고정 크기 페이지로 분할합니다. x86 시스템의 일반적인 메모리 페이지 크기는 4K이며 각 페이지에는 페이지 번호(PFN)인 고유 번호가 할당됩니다. ).

위 그림에서 볼 수 있듯이 가상 메모리와 물리 메모리 페이지는 페이지 테이블을 통해 매핑됩니다. 프로세스 X와 Y의 가상 메모리는 서로 독립적이며 페이지 테이블도 두 프로세스 간에 공유됩니다. 프로세스는 자신의 가상 주소 공간에 마음대로 접근할 수 있으며, 페이지 테이블과 물리적 메모리는 커널에 의해 유지됩니다. 프로세스가 메모리에 액세스해야 할 때 CPU는 가상 주소를 프로세스의 페이지 테이블을 기반으로 하는 물리적 주소로 변환한 다음 액세스합니다.

참고: 가상 주소 공간의 모든 페이지가 해당 페이지 테이블과 연결되어 있는 것은 아닙니다. 가상 주소가 프로세스에 할당된 후에만, 즉 프로세스가 유사한 malloc 함수를 호출한 후에만 시스템이 해당 페이지 테이블을 설정합니다. 페이지 테이블에 레코드를 추가합니다. 프로세스가 페이지 테이블과 연결되지 않은 가상 주소에 액세스하면 시스템은 SIGSEGV 신호를 발생시켜 프로세스가 종료되는 경우가 많습니다. 와일드 포인터에 액세스합니다. 즉, 각 프로세스는 4G(32비트 시스템) 가상 주소 공간을 갖고 있지만 시스템에 적용된 주소 공간만 사용할 수 있으며, 할당되지 않은 주소 공간에 접근하면 세그먼트폴트 오류가 발생하게 된다. Linux는 가상 주소 0을 어디에도 매핑하지 않으므로 널 포인터에 액세스하면 세그먼트 오류 오류가 확실히 보고됩니다.

3. 가상 메모리의 장점

● 더 큰 주소 공간: 연속적이므로 프로그램 작성 및 링크가 더 쉬워집니다.

● 프로세스 격리: 서로 다른 프로세스의 가상 주소 간에는 관계가 없습니다. , 한 프로세스의 작업은 다른 프로세스에 영향을 미치지 않습니다

● 데이터 보호: 가상 메모리의 각 부분에는 해당 읽기 및 쓰기 속성이 있어 프로그램의 코드 세그먼트 수정, 데이터 블록 실행 등을 방지할 수 있습니다. 시스템 보안 향상

● 메모리 매핑: 가상 메모리를 사용하면 디스크의 파일(실행 파일 또는 동적 라이브러리)을 가상 주소 공간에 직접 매핑할 수 있습니다. 이렇게 하면 물리적 메모리를 읽어야 하는 경우에만 지연 할당이 가능합니다. 해당 파일이 로드되면 실제로 디스크에서 메모리로 로드됩니다. 메모리가 부족한 경우 메모리의 이 부분을 지워서 물리적 메모리 활용 효율성을 향상시킬 수 있으며 이 모든 것이 애플리케이션에 투명합니다. 의

● 공유 메모리: 예를 들어 동적 라이브러리는 메모리에 복사본을 저장한 다음 이를 다른 프로세스의 가상 주소 공간에 매핑하기만 하면 됩니다. 파일에 대한 독점 소유권을 갖습니다. 프로세스 간 메모리 공유는 동일한 물리적 메모리를 프로세스의 서로 다른 가상 주소 공간에 매핑하여 달성할 수도 있습니다

● 물리적 메모리 관리: 물리적 주소 공간은 모두 운영 체제에 의해 관리되며 프로세스는 직접 할당 및 재활용이 불가능하여 시스템이 메모리를 더 잘 활용하고 프로세스 간 메모리 요구 사항의 균형을 맞출 수 있습니다

● 기타: 가상 주소 공간을 사용하여 스왑 공간 및 COW(쓰기 시 복사)와 같은 기능 간편 구현 가능

4. 페이지 테이블

페이지 테이블은 간단히 메모리로 이해 가능 매핑(mapping) 연결된 목록(물론 실제 구조는 매우 복잡함), 여기에 포함된 각 메모리 매핑은 가상 주소를 특정 리소스(물리적 메모리 또는 외부 저장 공간)에 매핑합니다. 각 프로세스에는 다른 프로세스의 페이지 테이블과 아무 관련이 없는 자체 페이지 테이블이 있습니다.

5. 메모리 매핑

각 메모리 매핑은 가상 메모리 조각에 대한 설명입니다. 가상 메모리 주소의 시작 위치, 길이, 권한(예: 이 메모리의 데이터를 읽고, 쓰고, 실행할 수 있는지 여부) 및 관련 리소스(예: 물리적 메모리 페이지, 스왑 공간의 페이지, 디스크의 파일 콘텐츠) , 등.).

프로세스가 메모리를 적용하면 시스템은 가상 메모리 주소를 반환하고 해당 가상 메모리에 대한 메모리 매핑을 생성하여 페이지 테이블에 넣지만 시스템이 반드시 해당 가상 메모리를 할당하지는 않습니다. 이때 시스템은 일반적으로 물리적 메모리를 할당하고 프로세스가 실제로 이 메모리에 액세스할 때 이를 해당 메모리 매핑과 연결합니다. 이를 지연 할당/요청 시 할당이라고 합니다.

각 메모리 매핑에는 연관된 물리적 리소스 유형을 나타내는 태그가 있습니다. 일반적으로 익명 및 파일 백업이라는 두 가지 범주로 구분됩니다. 익명의 경우 더 구체적인 공유 및 쓰기 시 복사 유형이 있고, 파일 백업에는 더 구체적인 장치 지원 유형이 있습니다. 다음은 각 유형이 나타내는 내용입니다.

file backed

이 유형은 메모리 매핑에 해당하는 물리적 리소스가 디스크 파일에 포함된 정보에는 파일 위치, 오프셋, rwx 권한 등이 포함됩니다.

프로세스가 해당 가상 페이지에 처음 액세스할 때 메모리 매핑에서 해당 물리적 ​​메모리를 찾을 수 없기 때문에 CPU는 페이지 오류 인터럽트를 보고한 다음 운영 체제에서 이를 처리합니다. 인터럽트를 실행하고 파일 내용을 물리적 메모리에 로드한 다음 CPU가 다음에 이 가상 주소에 액세스할 수 있도록 메모리 매핑을 업데이트합니다. 이렇게 메모리에 로드된 데이터는 일반적으로 페이지 캐시에 배치됩니다. 페이지 캐시는 나중에 소개하겠습니다.

일반 프로그램 실행 파일과 동적 라이브러리는 이러한 방식으로 프로세스의 가상 주소 공간에 매핑됩니다.

device backed

은 백엔드가 디스크의 물리적 주소에 매핑된다는 점을 제외하면 파일 백업과 유사합니다. 실제 메모리가 교체되면 장치 백업으로 표시됩니다.

anonymous

프로그램 자체에서 사용하는 데이터 세그먼트와 스택 공간은 물론 mmap을 통해 할당된 공유 메모리도 찾을 수 없습니다. 해당 파일에 저장되므로 메모리 페이지의 이 부분을 익명 페이지라고 합니다. 익명 페이지와 파일 백업의 가장 큰 차이점은 다음에 필요할 때 디스크에서 메모리로 로드할 수 있기 때문에 메모리가 부족할 때 시스템이 백업된 파일에 해당하는 물리적 메모리를 직접 삭제하지만 익명 페이지는 삭제할 수 없으며 교체만 가능합니다.

shared

여러 프로세스의 페이지 테이블에 있는 여러 메모리 매핑은 가상 주소(다른 프로세스)를 통해 동일한 물리적 주소에 매핑될 수 있습니다. 메모리의 가상 주소는 다를 수 있습니다) 동일한 내용에 액세스할 수 있습니다. 한 프로세스에서 메모리 내용이 수정되면 다른 프로세스에서 즉시 읽을 수 있습니다. 이 방법은 일반적으로 프로세스(예: mmap) 간에 고속 공유 데이터를 달성하는 데 사용됩니다. 공유로 표시된 메모리 매핑을 삭제하고 재활용할 때 물리 페이지의 참조 카운트를 업데이트하여 물리 페이지 카운트가 0이 되도록 업데이트한 후 재활용해야 합니다.

쓰기 시 복사

쓰기 시 복사는 공유 기술을 기반으로 하여 이러한 유형의 메모리를 읽을 때 시스템에서 수행할 필요가 없습니다. 특별한 작업이 필요하며 이 메모리에 쓸 때 시스템은 새 메모리를 생성하고 원래 메모리의 데이터를 새 메모리에 복사한 다음 새 메모리를 해당 메모리 매핑과 연결한 다음 쓰기 작업을 수행합니다. Linux의 많은 기능은 포크 등과 같은 성능 향상을 위해 쓰기 시 복사 기술을 사용합니다.

위의 소개를 통해 메모리 사용 프로세스를 다음과 같이 간단히 요약할 수 있습니다.

1 이 프로세스는 시스템에 메모리 적용 요청을 보냅니다

2. 시스템은 프로세스의 가상 주소 공간이 모두 사용되었는지 확인합니다. 남은 경우 프로세스

3에 가상 주소를 할당합니다. 이를 프로세스

4의 페이지 테이블에 저장합니다. 시스템은 가상 주소를 프로세스에 반환하고 프로세스는 가상 주소

#에 액세스하기 시작합니다. 🎜🎜#5. CPU는 가상 주소를 기반으로 프로세스의 페이지 테이블에 가상 주소를 배치합니다. 해당 메모리 매핑을 찾았지만 매핑이 물리적 메모리와 연결되지 않아 페이지 누락 인터럽트가 발생했습니다. 🎜🎜#

6 운영 체제는 페이지 누락 인터럽트를 수신한 후 실제 물리적 메모리를 할당하고 해당 메모리 매핑

7으로 이동합니다. 완료되면 CPU가 메모리에 액세스할 수 있습니다

물론 페이지 폴트 인터럽트는 매번 발생하는 것이 아니라 시스템이 필요하다고 느낄 때만 발생합니다. 메모리 할당을 지연할 때만 사용됩니다. 이 경우 위의 3단계에서 시스템은 실제 물리적 메모리를 할당하고 이를 메모리 매핑과 연결합니다.

6. 기타 개념 운영 체제가 가상 메모리와 물리적 메모리 간의 매핑을 구현하는 한 메모리 관계에서는 정상적으로 작동하지만 메모리 액세스를 보다 효율적으로 만들기 위해 고려해야 할 사항이 여전히 많습니다. 여기서는 메모리 및 해당 기능과 관련된 몇 가지 다른 개념을 살펴볼 수 있습니다.

MMU(Memory Management Unit) MMU는 프로세스를 할당하는 데 사용되는 CPU의 가상 주소입니다. 물리 주소를 변환하는 모듈입니다. 간단히 말하면 이 모듈의 입력은 프로세스의 페이지 테이블과 가상 주소이고, 출력은 물리 주소입니다. 가상 주소를 물리 주소로 변환하는 속도는 시스템 속도에 직접적인 영향을 미치므로 CPU에는 가속을 위해 이 모듈이 포함되어 있습니다.

TLB (Translation Lookaside Buffer) 위에서 소개했듯이 MMU의 입력은 페이지 테이블입니다. 그리고 페이지 테이블은 메모리에 저장되는데 CPU의 캐시에 비해 메모리가 매우 느리므로 가상 주소를 물리적 주소로 변환하는 속도를 더욱 높이기 위해 Linux는 CPU에 존재하는 TLB를 발명했습니다. L1 캐시는 캐싱에 사용되며, 다음 변환 전에 TLB를 확인할 수 있도록 가상 주소를 찾은 물리적 주소에 매핑합니다.

물리적 페이지를 할당해야 합니다물리적 메모리는 가상 메모리보다 훨씬 적기 때문에 실제 상황에서 운영 체제는 메모리 사용량을 최대화하기 위해 물리적 메모리를 할당하는 데 매우 주의해야 합니다. 실제 메모리를 절약하는 방법 중 하나는 현재 사용 중인 가상 페이지에 해당하는 데이터만 메모리에 로드하는 것입니다. 예를 들어, 대규모 데이터베이스 프로그램에서 쿼리 작업만 사용하면 삽입, 삭제 등을 담당하는 코드 세그먼트를 메모리에 로드할 필요가 없습니다. 이 방법은 많은 물리적 메모리를 절약할 수 있습니다. 물리적 메모리 페이지라고 불리는 주문형 할당은 지연된 로딩이라고도 합니다.

구현 원리는 매우 간단합니다. 즉, CPU가 가상 메모리 페이지에 액세스할 때 가상 메모리 페이지에 해당하는 데이터가 물리 메모리에 로드되지 않은 경우 CPU는 이를 알려줍니다. 오류가 발생한 운영 체제에서는 페이지 오류가 발생한 경우 운영 체제가 데이터를 실제 메모리에 로드하는 역할을 담당합니다. 데이터를 메모리에 로드하는 데 시간이 많이 걸리기 때문에 CPU는 거기서 기다리지 않고 다음에 프로세스를 예약할 때 데이터가 이미 물리적 메모리에 있습니다.

Linux는 실행 파일과 동적 라이브러리를 로드하는 데 주로 이 방법을 사용합니다. 프로그램이 커널에 의해 실행되도록 예약되기 시작하면 커널은 프로세스의 실행 파일과 동적 라이브러리를 가상 주소에 매핑합니다. 그리고 즉시 사용될 데이터의 작은 부분만 물리적 메모리에 로드하고 다른 부분은 CPU가 액세스할 때만 로드됩니다.

스왑 공간프로세스에서 데이터를 물리적 메모리에 로드해야 하는데 실제 물리적 메모리가 로드되지 않는 경우 모두 사용되면 운영 체제는 현재 프로세스의 요구 사항을 충족하기 위해 실제 메모리의 일부 페이지를 회수해야 합니다.

파일 백업 메모리 데이터, 즉 물리적 메모리의 데이터가 디스크의 파일에서 가져온 경우 커널은 다음 번에 더 많은 메모리를 해제하기 위해 메모리에서 이 데이터 부분을 직접 제거합니다. 프로세스가 데이터의 이 부분에 액세스해야 하면 디스크에서 메모리로 로드됩니다. 그러나 데이터의 이 부분이 수정되고 파일에 기록되지 않은 경우 데이터의 이 부분은 더티 데이터가 됩니다. 더티 데이터는 직접 삭제할 수 없으며 스왑 공간으로만 이동할 수 있습니다. (실행 파일 및 동적 라이브러리 파일은 수정되지 않으나, mmap+private을 통해 메모리에 매핑된 디스크 파일은 수정될 수 있습니다. 이렇게 매핑된 메모리는 특별합니다. 수정 전에는 파일 백업이 되어 있습니다. 수정 후에는 수정되지 않습니다. 수정되었습니다. 디스크에 다시 기록되기 전에는 익명이 됩니다.)

对于anonymous的内存数据,在磁盘上没有对应的文件,这部分数据不能直接被删除,而是被系统移到交换空间上去。交换空间就是磁盘上预留的一块特殊空间,被系统用来临时存放内存中不常被访问的数据,当下次有进程需要访问交换空间上的数据时,系统再将数据加载到内存中。由于交换空间在磁盘上,所以访问速度要比内存慢很多,频繁的读写交换空间会带来性能问题。

关于swap空间的详细介绍请参考Linux交换空间

共享内存

有了虚拟内存之后,进程间共享内存变得特别的方便。进程所有的内存访问都通过虚拟地址来实现,而每个进程都有自己的page tables。当两个进程共享一块物理内存时,只要将物理内存的页号映射到两个进程的page table中就可以了,这样两个进程就可以通过不同的虚拟地址来访问同一块物理内存。

从上面的那个图中可以看出,进程X和进程Y共享了物理内存页PFN3,在进程X中,PFN3被映射到了VPFN3,而在进程Y中,PFN3被映射到了VPFN1,但两个进程通过不同的虚拟地址访问到的物理内存是同一块。

访问控制

page table里面的每条虚拟内存到物理内存的映射记录(memory mapping)都包含一份控制信息,当进程要访问一块虚拟内存时,系统可以根据这份控制信息来检查当前的操作是否是合法的。

为什么需要做这个检查呢?比如有些内存里面放的是程序的可执行代码,那么就不应该去修改它;有些内存里面存放的是程序运行时用到的数据,那么这部分内存只能被读写,不应该被执行;有些内存里面存放的是内核的代码,那么在用户态就不应该去执行它;有了这些检查之后会大大增强系统的安全性。

huge pages

由于CPU的cache有限,所以TLB里面缓存的数据也有限,而采用了huge page后,由于每页的内存变大(比如由原来的4K变成了4M),虽然TLB里面的纪录数没变,但这些纪录所能覆盖的地址空间变大,相当于同样大小的TLB里面能缓存的映射范围变大,从而减少了调用MMU的次数,加快了虚拟地址到物理地址的转换速度。

Caches

为了提高系统性能,Linux使用了一些跟内存管理相关的cache,并且尽量将空闲的内存用于这些cache。这些cache都是系统全局共享的:

  • Buffer Cache
    用来缓冲块设备上的数据,比如磁盘,当读写块设备时,系统会将相应的数据存放到这个cache中,等下次再访问时,可以直接从cache中拿数据,从而提高系统效率。它里面的数据结构是一个块设备ID和block编号到具体数据的映射,只要根据块设备ID和块的编号,就能找到相应的数据。

  • Page Cache
    这个cache主要用来加快读写磁盘上文件的速度。它里面的数据结构是文件ID和offset到文件内容的映射,根据文件ID和offset就能找到相应的数据(这里文件ID可能是inode或者path,本人没有仔细去研究)。

从上面的定义可以看出,page cache和buffer cache有重叠的地方,不过实际情况是buffer cache只缓存page cache不缓存的那部分内容,比如磁盘上文件的元数据。所以一般情况下和page cache相比,Buffer Cache的大小基本可以忽略不计。

当然,使用cache也有一些不好的地方,比如需要时间和空间去维护cache,cache一旦出错,整个系统就挂了。

七、总结

有了上面介绍的知识,再来看看我们刚开始提出来的问题,以top命令的输出为例:

KiB Mem :   500192 total,   349264 free,    36328 used,   114600 buff/cache
KiB Swap:   524284 total,   524284 free,        0 used.   433732 avail Mem
로그인 후 복사

KiB Mem代表物理内存,KiB Swap代表交换空间,它们的单位都是KiB。

total、used和free没什么好介绍的,就是总共多少,然后用了多少,还剩多少。

buff/cached代表了buff和cache总共用了多少,buff代表buffer cache占了多少空间,由于它主要用来缓存磁盘上文件的元数据,所以一般都比较小,跟cache比可以忽略不计;cache代表page cache和其它一些占用空间比较小且大小比较固定的cache的总和,基本上cache就约等于page cache,page cache的准确值可以通过查看/proc/meminf中的Cached得到。由于page cache是用来缓存磁盘上文件内容的,所以占有空间很大,Linux一般会尽可能多的将空闲物理内存用于page cache。

avail Mem은 프로세스의 다음 할당에 사용할 수 있는 물리적 메모리의 양을 나타냅니다. 여유 공간 외에 시스템이 즉시 일부 공간을 해제할 수도 있기 때문에 이 크기는 일반적으로 여유 공간보다 약간 큽니다.

그럼 현재 메모리 사용량이 비정상적인지 어떻게 확인하나요? 참고 사항은 다음과 같습니다.

● Mem free 값은 상대적으로 작고, buff/cache 값도 작습니다. free 값이 상대적으로 작다고 해서 반드시 문제가 있는 것은 아닙니다. 페이지 캐시에 메모리를 최대한 많이 사용하지만, buff/cache 값도 작다면 이는 메모리가 부족하고 시스템에 캐시가 자주 필요한 애플리케이션인 경우를 의미합니다. FTP 서버와 같은 디스크 읽기 및 쓰기가 수행되면 성능에 미치는 영향은 매우 커집니다.

● 사용된 스왑의 값이 상대적으로 큽니다.

이 상황은 위의 상황보다 더 심각합니다. 일반적인 상황에서는 스왑을 거의 사용하지 않아야 합니다. 스왑 공간이 더 많이 사용된다는 의미입니다. vmstat 명령을 통해 in/out이 잦아진다면 시스템 메모리가 심각하게 부족하여 전체적인 성능에 심각한 영향을 미친다는 의미입니다

추천 관련 영상 튜토리얼: "

Linux Tutorial"

위 내용은 전체 내용입니다 이 기사가 모든 사람이 도움말을 배우는 데 도움이 되기를 바랍니다. 더 흥미로운 내용을 보려면 PHP 중국어 웹사이트의 관련 튜토리얼 열을 주의 깊게 살펴보세요! ! !

위 내용은 Linux 메모리 관리 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:segmentfault.com
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿