고급 Linux 드라이버(4) - 내부 및 외부 메모리 액세스


머리말

드라이버를 성공적으로 로드하기 위한 핵심 요소는 커널이 드라이버에 충분한 메모리 공간을 할당할 수 있다는 것입니다. 이러한 컨트롤 중 일부는 드라이버의 필수 데이터 구조에 사용되고 다른 일부는 데이터 교환에 사용됩니다. 동시에 커널은 외부 장치 포트에 액세스할 수 있어야 합니다. 일반적으로 외부 장치는 메모리 공간이나 I/O 공간에 연결됩니다. 이 장에서는 내부 및 외부 메모리 장치 액세스에 대해 자세히 소개합니다.

메모리 할당

kmalloc()이 섹션에서는 함수 및 함수를 포함하여 메모리 할당의 일부 기능을 주로 소개합니다 vmalloc(). 이 두 가지 중요한 기능을 소개한 후에는 백업 캐시의 내용에 대해 집중적으로 설명하겠습니다.이 지식은 드라이버 개발에 매우 ​​중요하며 주의가 필요합니다.

kmalloc 함수

C 언어에서는 malloc()和free()이 두 함수 "적"이 자주 발생합니다. malloc()함수는 메모리를 할당하는 데 사용되고 free()함수는 메모리를 해제하는 데 사용됩니다. 함수는 커널 모드에서 메모리 할당에 사용된다는 점을 제외하면 함수 kmalloc()와 유사합니다 . 함수는 강력한 함수이며 메모리가 충분하면 이 함수는 매우 빠르게 실행됩니다. 이 기능은 실제 메모리에서 프로그램에 대한 연속 저장 공간을 할당합니다. 이 저장 공간의 데이터는 지워지지 않습니다. 즉, 메모리의 원본 데이터가 저장되므로 사용 시 주의가 필요합니다. 기능이 빠르게 실행됩니다. 메모리를 할당하는 동안 블록을 허용하지 않는 플래그를 전달할 수 있습니다. 함수 프로토타입은 다음과 같습니다.malloc()kmalloc()kmalloc()
kmalloc()kmalloc()kmalloc()

static inline void *kmalloc(size_t size, gfp_t flags)

kmalloc()함수의 첫 번째 매개변수는 할당된 메모리의 크기를 나타내는 크기입니다. 두 번째 매개변수는 할당 플래그로, kmalloc()이 플래그를 통해 함수의 다양한 할당 방법을 제어할 수 있습니다. 다른 함수와 달리 kmalloc()함수의 이 두 매개변수는 매우 중요하며 아래에서 자세히 설명합니다.
1.size参数
크기 매개변수에는 Linux 하위 시스템의 매우 중요한 부분인 메모리 관리가 포함됩니다. 리눅스의 메모리 관리 방식은 페이지 크기에 따라 할당할 메모리를 제한한다. 일반적으로 페이지 크기는 4K입니다. kmalloc()특정 드라이버에 대해 4바이트의 메모리 공간을 할당하는 함수를 사용하면 Linux는 4K 메모리 공간의 페이지를 반환하며 이는 분명히 메모리 낭비입니다 .
기능은 공간 낭비로 인해 사용자 공간 기능 kmalloc()과 완전히 다르게 구현됩니다 . 함수는 메모리 공간을 힙에 할당하며 할당된 공간의 크기는 매우 유동적이지만 함수가 메모리 공간을 할당하는 방법은 상당히 특수한데 이 방법에 대해 간략히 설명하면 다음과 같다. Linux 커널이 기능을 처리하는 방식은 먼저 크기가 다른 일련의 메모리 풀을 할당하는 것이며 각 풀의 메모리 크기는 고정되어 있습니다. 메모리를 할당할 때 충분히 큰 메모리 풀에서 함수로 메모리를 전달합니다. 메모리를 할당할 때 Linux 커널은 미리 정의된 고정 크기 바이트 수만 할당할 수 있습니다. 요청한 메모리 크기가 2의 정수배가 아닌 경우 더 많은 메모리를 요청하고 요청한 메모리보다 큰 메모리 블록을 요청자에게 반환합니다. Linux 커널은 32바이트, 64바이트, 128바이트, 256바이트, 512바이트, 1024바이트, 2048바이트, 4096바이트, 8KB, 16KB, 32KB, 64KB, 128KB 메모리 풀 크기의 기능을 제공합니다. 따라서 프로그래머는 함수가 최소 32바이트의 메모리를 할당할 수 있으며 요청된 메모리가 32바이트 미만인 경우 32바이트도 반환한다는 점에 유의해야 합니다.malloc()malloc()kmalloc()
kmalloc()kmalloc()
kmalloc()kmalloc()kmalloc()함수가 할당할 수 있는 메모리 블록의 크기에도 상한선이 있습니다. 코드 이식성을 위해 이 상한선은 일반적으로 128KB입니다. 더 많은 메모리를 할당하려면 다른 메모리 할당 방법을 사용하는 것이 좋습니다.
2.flags参数
flags매개변수는 다양한 방식으로 함수의 동작을 제어할 수 있습니다 kmalloc(). 메모리를 적용하기 위해 가장 일반적으로 사용되는 매개변수는 입니다 GFP_KERNEL. 이 매개변수를 사용하여 메모리가 부족할 때 절전 모드로 호출하는 프로세스를 실행하고 메모리가 충분할 때 페이지를 할당하십시오. 따라서 GFP_KERNEL플래그를 사용하면 차단이 발생할 수 있으며 차단을 허용하지 않는 응용 프로그램의 경우 다른 메모리 플래그를 사용해야 합니다. 프로세스가 휴면 상태일 때 커널 하위 시스템은 버퍼의 내용을 디스크에 기록하여 휴면 프로세스를 위한 더 많은 공간을 남겨둡니다.
플래그는 인터럽트 핸들러, 대기 큐 등과 같은 함수에서 사용할 수 없습니다 GFP_KERNEL. 이 플래그로 인해 호출자가 잠들 수 있기 때문입니다. 자고 일어나면 많은 프로그램이 잘못됩니다. 이 경우 GFP_ATOMIC플래그를 사용하여 메모리의 원자적 할당을 나타낼 수 있습니다. 즉, 메모리를 할당하는 동안 휴면이 허용되지 않습니다. 플래그가 수면을 유발하지 않는 이유는 무엇입니까 GFP_ATOMIC? 이는 커널이 이 할당 방법을 위해 일부 메모리 공간을 예약하고 이러한 메모리 공간은 함수kmalloc() 가 플래그를 . GFP_ATOMIC플래그가 할당되는 방식은 대부분의 경우 성공 GFP_ATOMIC하고 즉시 반환됩니다. 및 로고
외에도 다른 플래그가 있지만 나머지 플래그는 일반적으로 사용되지 않습니다. 이러한 기호의 의미와 사용법은 다음과 같습니다.GFP_KERNELGFP_ATOMIC
여기에 이미지 설명 삽입

vmalloc() 함수

vmalloc()이 기능은 연속적인 가상 주소와 불연속적인 물리 주소로 메모리를 할당하는 데 사용됩니다. 즉, vmalloc()함수에 의해 할당된 페이지는 가상 주소 공간에서는 연속적이지만 물리적 주소 공간에서는 연속적이지 않습니다. 200M 메모리 공간을 할당해야 하는데 실제 물리적 메모리에 연속 200M 메모리 공간이 없지만 메모리에 메모리 조각이 많고 용량이 200M보다 큰 경우 사용할 수 있기 때문입니다. 불연속적인 물리적 메모리 공간을 변환하는 기능 vmalloc()주소 공간 매핑 레이어 연속 가상 주소 공간.
실행 효율성 측면에서 vmalloc()함수의 실행 오버헤드는 함수보다 훨씬 큽니다 __get_free_pages(). vmalloc()이 함수는 새 페이지 테이블을 만들고 비연속적인 물리적 메모리를 연속적인 가상 메모리에 매핑하기 때문에 오버헤드가 상대적으로 높습니다 . vmalloc()또한 이 기능은 새로운 페이지 테이블 설정으로 인해 더 많은 CPU 시간을 낭비하고 페이지 테이블을 저장하기 위해 더 많은 메모리가 필요합니다. 일반적으로 vmalloc()함수는 메모리가 큰 경우에 적용하고, 메모리가 적은 경우에는 __get_free_pages()함수를 사용하여 적용하는 것이 가장 좋습니다.
1.vmalloc()函数申请和释放
vmalloc()함수는 mm\vmalloc.c파일에 정의되어 있으며 함수의 프로토타입은 다음과 같습니다.

void *vmalloc(unsigned long size)

vmalloc()size이 함수는 할당된 연속 메모리의 크기인 하나의 매개변수를 받습니다 . 함수가 성공적으로 실행되면 연속 가상 주소가 있는 메모리 영역을 반환합니다. 메모리를 해제하기 위해 리눅스 커널에서도 vmalloc()함수에 의해 할당된 메모리를 해제하는 기능을 제공하는데 이 함수는 vfree()함수이고 그 코드는 다음과 같다.

void vfree(const void *addr)

2.vmalloc()함수 예제
vmalloc()함수는 function 의 함수와 다르지만 kmalloc()기본적으로 사용 방법은 동일합니다. 먼저 vmalloc()함수를 사용하여 메모리 공간을 할당하고 가상 주소를 반환합니다. 메모리 할당은 까다로운 작업이며 반환 값은 가능할 때마다 확인해야 합니다. 메모리가 할당된 후 copy_from_user()메모리를 사용하여 액세스할 수 있습니다. vmalloc()또한 반환된 메모리 공간을 구조체로 변환하고 할당된 메모리 공간을 다음 코드의 12-15행과 같이 사용할 수 있습니다. 메모리를 사용할 필요가 없으면 vfree()20행의 함수를 사용하여 메모리를 해제할 수 있습니다. 드라이버에서 함수에 표시된 vmalloc()대로 함수의 인스턴스를 사용합니다.xxx()

static int xxx(...)
{
    
    
	...  /*省略代码*/
	cpuid_entires - vmalloc(sizeof(struct kvm_cpuid_entry) * cpuid->nent );
	if(!cpuid_entries)
		goto out;
	if(copy_from_user(cpuid_entries, entries, cpuid->nent * sizeof(struct kvm_cpuid_entry)))
		goto out_free;
	for(i=0; i< cpuid->nent; i++){
    
    
		vcpu->arch.cpuid_emtries[i].eax = cpuid_entries[i].eax;
		vcpu->arch.cpuid_emtries[i].ebx= cpuid_entries[i].ebx;
		vcpu->arch.cpuid_emtries[i].ecx= cpuid_entries[i].ecx;
		vcpu->arch.cpuid_emtries[i].edx= cpuid_entries[i].edx;
		vcpu->arch.cpuid_emtries[i].index= 0;
	}
	.... /*省略代码*/
out_free:
	vfree(cpuid_entries);
out:
	return r;
}

룩어사이드 캐시

드라이버 프로그램에서 같은 크기의 많은 메모리 블록이 반복적으로 할당되는 경우가 많으며 이러한 메모리 블록도 자주 해제됩니다. 메모리를 자주 신청하고 해제하면 메모리 조각화가 발생하기 쉽고 메모리 풀을 사용하면 이러한 문제를 잘 해결할 수 있습니다. 리눅스에서는 할당과 해제를 반복하는 일부 구조체를 위해 일정량의 메모리 공간을 확보하고 메모리 풀을 이용해 관리하는데, 이 메모리 풀을 관리하는 기술을 할당자(allocator)라고 한다 slab. 이 메모리를 백업 캐시라고 합니다.
slab할당자의 관련 기능은 <linux/slab.h>파일에 정의되어 있으며 Backing Cache를 사용하기 전에 kernel_cache구조체를 생성해야 합니다.
1.创建slab缓存函数
캐시를 사용하기 전에 캐시를 생성하는 함수를 slab호출해야 하는데 이 함수의 코드는 다음과 같습니다.kmem_cache_create()slab

struct kmem_cache *kmem_cache_create(const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void))

이 함수는 지정된 수의 메모리 블록을 보유할 수 있는 새로운 백업 캐시 객체를 생성합니다. 메모리 블록의 수는 size매개변수로 지정됩니다. 이 매개변수는 나중에 사용할 백업 캐시를 나타내는 데 name사용할 수 있는 백업 캐시 개체의 이름을 나타냅니다 . 함수의 세 번째 매개변수는 백업 캐시에 있는 첫 번째 개체의 오프셋 값이며 일반적으로 0으로 설정됩니다. 네 번째 매개변수는 할당이 수행되는 방식을 제어하는 ​​비트마스크입니다. 다섯 번째 매개변수는 lookaside 캐시에 추가된 메모리 블록을 초기화하는 데 사용되는 선택적 함수입니다.name
kmem_cache_create()alignflagector

unsigned int sz = sizeof(struct bio) + extra_size;
slab = kmem_cache_create("DRIVER_NAME", sz, 0, SLAB_HWCACHE_ALIGN, NULL);

2.分配slab缓存函数
호출 kmem_cache_create()함수가 백업 캐시를 생성하면 함수를 호출하여 kmem_cache_alloc()메모리 블록 객체를 생성할 수 있습니다. kmem_cache_alloc()함수 프로토타입은 다음과 같습니다.

void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)

이 함수의 첫 번째 매개변수는 cachep할당을 시작할 lookaside 캐시입니다. 두 번째 인수는 함수 flags에 전달된 인수와 동일하며 일반적으로 . 함수 에 해당하는 해제 함수는 메모리 블록 객체를 해제하는 함수이다. 함수 프로토타입은 다음과 같습니다.kmalloc()GFP_KERNEL
kmem_cache_alloc()kmem_cache_free()

void kmem_cache_free(struct kmem_cache *cachep, void *objp)

3.销毁slab缓存函数
함수 에 kmem_cache_create()해당하는 자유 함수는 kmem_cache_destroy()백업 캐시를 해제하는 함수이다. 함수의 프로토타입은 다음과 같습니다.

void  kmem_cache_destroy(struct kmem_cache *c)

이 함수는 함수를 호출하여 백업 캐시 영역의 메모리 블록 개체를 모두 해제한 후에야 kmem_cache_free()백업 캐시를 삭제할 수 있습니다 . lookaside 캐시를 사용하는 예는 스레드 구조를 나타내는 구조를 보유하는
4.slab缓存举例
lookaside 캐시를 만드는 다음 코드에 표시됩니다 . struct thread_info리눅스에서는 많은 쓰레드의 생성과 소멸을 수반하는데 __get_free_pages()함수를 사용하면 메모리 낭비가 많이 발생하고 효율이 상대적으로 낮다. thread_info스레드 구조의 경우 커널 초기화 단계에서 이름이 지정된 백업 캐시가 생성되며 코드는 다음과 같습니다.

/*以下两行创建slab缓存*/
static struct kmem_cache *thread_info_cache; 
		/*声明一个struct kmem_cache的指针*/
thread_info_cache = kmem_cache_create("thread_info", THREAD_SIZE,
					THREAD_SIZE, 0, NULL); /*创建一个后备高速缓存区*/
/*以下两行分配slab缓存*/
struct thread_info *ti;   /*线程结构体指针*/
ti = kmem_cache_alloc(thread_info_cache, GFP_KERNEL); /*分配一个结构体*/
/*省略了使用slab缓存的函数*/
/*以下两行释放slab缓存*/
kmem_cache_free(thread_info_cache, ti);  /*释放一个结构体*/
kmem_cache_destroy(thread_info_cache);   /*销毁一个结构体*/

페이지 할당

페이지를 할당하고 해제하기 위해 Linux에서 일련의 기능이 제공됩니다. 드라이버 프로세스가 메모리를 신청해야 하는 경우 커널은 필요에 따라 신청자에게 요청된 페이지 수를 할당합니다. 드라이버가 메모리를 신청할 필요가 없으면 메모리 누수를 방지하기 위해 요청된 페이지 수를 해제해야 합니다. 이 섹션에서는 드라이버 개발에 매우 ​​중요한 페이지 할당 방법에 대해 자세히 설명합니다.

메모리 할당

Linux 커널 메모리 관리 하위 시스템은 메모리 할당 및 해제를 위한 일련의 기능을 제공합니다. Linux에서는 관리의 편의를 위해 메모리를 페이지 단위로 할당합니다. 32비트 시스템에서 페이지 크기는 일반적으로 4KB이고 46비트 시스템에서 페이지 크기는 플랫폼에 따라 일반적으로 8KB입니다. 드라이버 프로그램의 프로세스가 공간을 요청하면 메모리 관리 하위 시스템은 요청된 페이지 수를 드라이버 프로그램에 할당합니다. 드라이버가 메모리를 필요로 하지 않는 경우 메모리를 해제할 수도 있으며, 메모리가 커널로 반환되면 다른 프로그램에서 사용할 수 있습니다. 다음은 메모리 할당 및 해제를 위해 이러한 기능을 제공하는 메모리 관리 하위 시스템에 대해 설명합니다. 메모리 관리 서브시스템이 제공하는 메모리 관리 함수의 반환 값은 함수를 두 가지 유형으로 나눕니다. 첫 번째 유형의 함수는 커널이 신청자에게 할당한 페이지를 가리키는
1.内存分配函数的分类
메모리 신청자에게 구조에 대한 포인터를 반환합니다. struct page두 번째 유형의 함수는 할당된 페이지의 첫 번째 가상 주소인 32비트 가상 주소를 반환합니다. 가상 주소와 물리 주소는 대부분의 컴퓨터 구성 및 원리 과정에서 설명되어 독자의 관심을 끌기를 바랍니다.
둘째, 함수가 반환하는 페이지 수에 따라 함수를 분류할 수 있습니다. 첫 번째 유형의 함수는 한 페이지만 반환하고 두 번째 유형의 함수는 여러 페이지를 반환할 수 있으며 페이지 수는 드라이버 개발자가 직접 지정할 수 있습니다. . 메모리 할당 함수의 분류는 아래 그림에 나와 있습니다.
여기에 이미지 설명 삽입
2.alloc_page()和alloc_pages()함수 구조를
반환하는 두 가지 주요 함수는 함수와 함수입니다. 이 두 함수는 파일에 정의되어 있습니다. 기능은 하나의 페이지를 할당하고 기능은 사용자의 필요에 따라 여러 페이지를 할당합니다. 두 함수 모두 구조체에 대한 포인터를 반환합니다 . 이 두 함수의 코드는 다음과 같습니다.struct pagealloc_page()alloc_pages()/include/linux/gfp.halloc_page()alloc_pages()struct page

/*alloc_page()函数分配一个页面,调用alloc_pages()实现分配一个页面的功能*/
#define alloc_page(gfp_mask)  alloc_pages(gfp_mask, 0)
/*alloc_pages()函数分配多个页面,调用alloc_pages_node()实现分配一个页面的功能*/
#define alloc_pages(gfp_mask, order) \
alloc_pages_node(numa_node_id(), gfp_mask, order)
/*该函数是真正的内存分配函数*/
static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order)
{
    
    
	if(unlikely(order >= MAX_ORDER))
		return NULL;
	if(nid < 0)
		nid = numa_node_id();
	return __alloc_pages(gfp_mask, order, node_zonelist(nid, gfp_mask));
}

이러한 기능은 아래에서 자세히 설명합니다.

  • alloc_pages()기능 기능은 여러 페이지를 할당하는 것입니다. 첫 번째 매개변수는 메모리 할당을 위한 플래그를 나타냅니다. 이 플래그는 kmalloc()함수의 플래그와 동일합니다. 정확히 말하면 kmalloc()함수는 alloc_pages()함수에 의해 구현되므로 동일한 메모리 할당 플래그를 갖습니다. 두 번째 매개변수는 order연속적으로 할당된 페이지 수를 나타냅니다. 페이지 수는 2의 오더 제곱으로 표현됩니다. 예를 들어 한 페이지만 할당된 경우 오더 값은 0이어야 합니다.
  • alloc_pages()함수 호출이 성공하면 첫 번째 페이지의 struct page구조에 대한 포인터를 반환하고 할당에 실패하면 NULL 값을 반환합니다. 메모리 할당은 언제든지 실패할 수 있으므로 메모리 할당 후 반환 값이 올바른지 확인해야 합니다.
  • alloc_page()이 기능은 한 페이지만 할당하는 2줄로 정의됩니다. 이 매크로는 gfp_mask메모리 할당 플래그를 나타내는 하나의 매개변수만 허용합니다. 기본적으로 order0으로 설정되며, 이는 함수가 응용 프로그램 프로세스에 물리적 페이지 1과 동일한 0의 거듭제곱에 2를 할당함을 나타냅니다.
    3.__get_free_page()和__get_free_pages()함수
    두 번째 유형의 함수가 실행된 후 요청된 페이지의 첫 번째 페이지의 가상 주소를 반환합니다. 여러 페이지가 반환되면 첫 번째 페이지의 가상 주소만 반환됩니다. __get_free_page()함수와 __get_free_pages()함수는 페이지 가상 주소를 반환합니다. __get_free_page()함수가 한 페이지만 반환하는 경우 __get_free_pages()함수는 여러 페이지를 반환합니다. 이 두 함수 또는 매크로에 대한 코드는 다음과 같습니다.
#define __get_free_page(gfp_mask) \
		__get_free_pages((gfp_mask), 0)

__get_free_page매크로는 __get_free_pages()함수 호출로 최종 구현되며, __get_free_pages()함수 호출 시 order값이 직접 0으로 할당되어 한 페이지만 반환됩니다. __get_free_pages()이 함수는 연속된 여러 페이지를 할당할 수 있을 뿐만 아니라 한 페이지를 할당할 수도 있으며 코드는 다음과 같습니다.

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
    
    
	struct page *page;
	page = alloc_pages(gfp_mask, order);
	if(!page)
		return 0;
	return (unsigned long)page_address(page);
}

기능은 아래에 자세히 설명되어 있습니다.

  • __get_free_pages()이 함수는 두 개의 매개변수를 받습니다. 첫 번째 매개변수는 kmalloc()함수의 플래그와 같고 두 번째 함수는 적용할 메모리 페이지 수를 나타내는 데 사용됩니다 페이지 수 계산 공식은 페이지 수 = 2의 순서입니다. 페이지를 할당하려면 주문이 0이기만 하면 됩니다.
  • 3행은 struct page포인터를 정의합니다.
  • alloc_pages()4행에서 2의 주문 파워 페이지의 메모리 공간을 할당하기 위해 함수가 호출됩니다 .
  • 5행과 6행은 메모리 부족으로 할당에 실패하면 0을 반환합니다.
  • 7행, page_address()물리적 주소를 가상 주소로 변환하는 함수를 호출합니다.
    4.内存释放函数
    메모리가 더 이상 필요하지 않으면 메모리 관리 시스템으로 반환해야 합니다. 그렇지 않으면 리소스 누수가 발생할 수 있습니다. Linux는 메모리를 해제하는 기능을 제공합니다. 메모리를 해제할 때 해제 함수에 올바른 struct page포인터나 주소를 전달해야 합니다. 이렇게 하면 메모리가 잘못 해제되어 시스템 충돌이 발생합니다. 메모리 해제 기능 또는 매크로는 다음과 같이 정의됩니다.
#define __free_page(page)  __free_pages((page), 0)
#define __free_pages(addr) free_pages((addr), 0

이 두 함수의 코드는 다음과 같습니다.

void free_pages(unsigned long addr, unsigned int order)
{
    
    
	if(addr != 0){
    
    
		VM_BUG_ON(!virt_addr_valid((void *)addr));
		__free_pages(virt_to_page((void *)addr), order);
	}
}
void __free_pages(struct page *page, unsigned int order)
{
    
    
	if(put_page_testzero(page)){
    
    
		if(order == 0)
			free_hot_page(page);
		else
			__free_pages_ok(page, order);
	}
}

위의 코드에서 알 수 있듯이 free_pages()함수는 __free_pages()메모리 해제를 완료하기 위해 함수를 호출하는 것입니다. free_pages()함수의 첫 번째 매개변수는 메모리 페이지를 가리키는 가상 주소이고 두 번째 매개변수는 해제할 페이지 수로 할당된 페이지 수와 동일해야 합니다.

물리적 주소와 가상 주소 간의 변환

대부분의 메모리 할당 기능에는 기본적으로 물리 주소와 가상 주소 간의 변환이 포함됩니다. 함수를 사용하여 virt_to_phys()커널 가상 주소를 물리적 주소로 변환할 수 있습니다. virt_to_phys()함수 정의는 다음과 같습니다.

#define __pa(x)     ((x) - PAGE_OFFSET)
static inline unsigned long virt_to_phys(volatile void *address)
{
    
    
	return __pa((void *)address);
}

virt_to_phys()함수는 __pa매크로를 호출하고 매크로는 일반적으로 32비트 플랫폼에서 정의되는 __pa가상 주소를 address뺍니다 . 기능 에 해당하는 기능은 이 기능이 물리적 주소를 커널 가상 주소로 변환한다는 것입니다 . 함수는 다음과 같이 정의됩니다.PAGE_OFFSET3GB
virt_to_phys()phys_to_virt()phys_to_virt()

#define __va(x)   ((x) + PAGE_OFFSET)
static inline void * phys_to_virt(unsigned long address)
{
    
    
	return __va(address);
}

phys_to_virt()함수는 __va매크로를 호출하고 매크로는 일반적으로 32비트 플랫폼에서 3GB로 정의되는 __va물리적 주소를 address추가합니다 . Linux에서 물리적 주소와 가상 주소 간의 관계는 아래 그림과 같습니다. 32비트 컴퓨터에서 최대 가상 주소 공간 크기는 . 사용자 공간 과 커널 공간의 구분점인 3GB로 정의되는 사용자 공간을 나타냅니다. 리눅스 커널에서는 3GB~4GB의 커널 공간을 실제 물리적 주소 매핑에 사용한다. 실제 메모리는 커널 공간의 1GB보다 클 수 있으며 훨씬 더 클 수도 있습니다. 현재 주류 컴퓨터 물리적 메모리는 약 4GB입니다. 이 경우 Linux는 1GB보다 클 수 있는 물리적 메모리를 매핑하기 위해 1GB의 커널 공간을 사용하는 비선형 매핑 방법을 사용합니다.PAGE_OFFSET
4GB0~3GBPAGE_OFFSET
여기에 이미지 설명 삽입

장치 I/O 포트에 대한 액세스

장치에는 장치의 상태를 저장하고 제어하는 ​​데 사용되는 외부 레지스터 세트가 있습니다. 장치의 상태를 저장하는 레지스터를 데이터 레지스터라고 하고 장치의 상태를 제어하는 ​​레지스터를 제어 레지스터라고 합니다. 이러한 레지스터는 메모리 공간 또는 I/O 공간에 위치할 수 있으며 이 섹션에서는 이러한 공간의 레지스터 액세스 방법을 소개합니다.

Linux I/O 포트 읽기 및 쓰기 기능

장치는 일부 레지스터를 통합하고 프로그램은 레지스터를 통해 장치를 제어할 수 있습니다. WTCON대부분의 외부 장치에는 감시 제어 레지스터( ), 데이터 레지스터( WTDAT) 및 카운팅 레지스터( ) 등과 같은 여러 레지스터가 있으며 WTCNT일부 IIC 장치에는 모든 IIC 작업을 완료하기 위해 4개의 레지스터가 있으며 이러한 레지스터는 IICCON, IICSTAT, IICADD, 입니다 IICCDS.
장치가 완료해야 하는 기능에 따라 외부 장치는 메모리 주소 공간 또는 I/O 주소 공간에 연결할 수 있습니다. 메모리 주소 공간이든 I/O 주소 공간이든 이러한 레지스터에 대한 액세스는 계속됩니다. 일반적으로 데스크톱 컴퓨터를 설계할 때 메모리 주소 공간이 상대적으로 부족하기 때문에 외부 장치는 일반적으로 I/O 주소 공간에 연결됩니다. 임베디드 장치의 경우 메모리는 일반적으로 64M 또는 128M이며 대부분의 임베디드 프로세서는 1G 메모리 공간을 지원하므로 추가 메모리 공간에 외부 장치를 연결할 수 있습니다.
하드웨어 설계 측면에서 I/O메모리 주소 공간과 주소 공간의 차이는 그다지 크지 않으며 모두 주소 버스, 제어 버스 및 데이터 버스를 통해 CPU에 연결됩니다. Non-embedded 제품의 대형 장비에 사용되는 CPU의 경우 일반적으로 메모리 주소와 I/O 주소 공간이 분리되어 별도로 액세스되며 해당 읽기 및 쓰기 명령이 제공됩니다. 예를 들어 x86 플랫폼에서 I/O 주소 공간에 대한 액세스는 in 및 out 명령어를 사용하는 것입니다.
단순 임베디드 장치의 CPU의 경우 I/O 주소 공간은 일반적으로 메모리 주소 공간에 병합됩니다. ARM 프로세서는 1G의 메모리 주소 공간에 액세스할 수 있고, 낮은 주소 공간에 메모리를 마운트하고, 사용하지 않는 메모리 주소 공간에 외부 장치를 마운트할 수 있습니다. 외부 장치는 메모리와 같은 방식으로 액세스할 수 있습니다.

I/O 메모리 읽기 및 쓰기

I/O 포트는 액세스를 위해 I/O 메모리 공간에 매핑될 수 있습니다. 다음 그림은 I/O 메모리 접근 과정을 나타낸 것으로, open()디바이스 드라이버 모듈의 로딩 함수나 함수에서 호출하여 request_mem_region()리소스를 신청할 수 있습니다. 함수를 이용하여 ioremap()I/O 포트가 위치한 물리 주소를 가상 주소로 매핑한 후 readb()、readw()、readl()레지스터의 내용을 읽고 쓰는 등의 함수를 호출할 수 있습니다. I/O 메모리가 더 이상 사용되지 않으면 ioummap()기능을 사용하여 물리적-가상 주소 매핑을 해제할 수 있습니다. 마지막으로 release_mem_region()함수를 사용하여 요청된 리소스를 해제합니다.
여기에 이미지 설명 삽입
1.申请I/O内存
와 같은 기능을 사용하여 readb()、readw()、readl()I/O 메모리에 액세스하기 전에 먼저 I/O 메모리 영역을 할당해야 합니다. 이 함수를 완성하기 위한 함수는 request_mem_region()함수 프로토타입이 다음과 같다는 것입니다.

#define   request_mem_region(start, n , name)
__request_region(&iomem_resource, (start), (n), (name), 0)

request_mem_region__request_region()함수를 내부적으로 호출하는 매크로로 정의됩니다 . 매크로는 request_mem_region3개의 매개변수를 취하는데, 첫 번째 매개변수 start는 물리적 주소의 시작 영역, 두 번째 매개변수 n은 할당할 메모리의 바이트 길이, 세 번째 매개변수는 name리소스 이름입니다. 함수가 성공하면 리소스 포인터가 반환되고 함수가 실패하면 NULL 값이 반환됩니다. 모듈 언로딩 기능에서 메모리 리소스가 더 이상 사용되지 않으면 release_region()매크로를 사용하여 메모리 리소스를 해제할 수 있습니다. 이 함수의 프로토타입은 다음과 같습니다.

#define release_region(start, n)   __release_region(&ioport_resource, (start), (n))

2.物理地址到虚拟地址的映射函数
ioremap()I/O 메모리 읽기 및 쓰기 기능을 사용하기 전에 외부 장치의 I/O 포트의 물리적 주소를 가상 주소로 매핑하는 기능을 사용해야 합니다 . ioremap()함수의 프로토타입은 다음과 같습니다.

void __iomem *ioremap(unsigned long phys_addr, unsigned long size)

ioremap()이 함수는 물리적 주소와 전체 I/O 포트의 크기를 수신하고 size크기의 물리적 주소 공간에 해당하는 가상 주소를 반환합니다. 기능을 사용한 후 ioremap()물리적 주소는 가상 주소 공간에 매핑되므로 I/O 포트에서 데이터를 읽고 쓰는 것은 메모리에서 데이터를 읽는 것처럼 간단합니다. 함수 가 요청한 ioremap()가상 주소는 iounmap()함수를 사용하여 해제해야 하며 함수의 프로토타입은 다음과 같습니다.

void iounmap(volatile void __iomem *addr)

iounmap()함수는 ioremap()함수가 요청한 가상 주소를 매개변수로 받아 물리 주소에서 가상 주소로의 매핑을 취소합니다. 함수가 반환된 가상 주소 이지만 ioremap()포인터로 직접 사용할 수는 없습니다.
3.I/O内存的读写
커널 개발자들은 가상 주소를 읽고 쓰는 일련의 함수를 준비했습니다.이 함수는 다음과 같습니다.

  • ioread()함수와 iowrite8()함수는 8비트 I/O 메모리를 읽고 쓰는 데 사용됩니다.
unsigned int ioread8(void __iomem *addr)
void iowrite8(u8 b, void __iomem *addr)
  • ioread16()함수와 iowrite16()함수는 16비트 I/O 메모리를 읽고 쓰는 데 사용됩니다.
unsigned int ioread16(void __iomem *addr)
void iowrite16(u16 b, void __iomem *addr)
  • ioread32()함수와 iowrite()32함수는 32비트 I/O 메모리를 읽고 쓰는 데 사용됩니다.
unsigned int ioread32(void __iomem *addr)
void iowrite32(u32 b, void __iomem *addr)
  • 대용량 저장 장치의 경우 위의 기능을 사용하여 읽기 및 쓰기를 여러 번 반복하여 대량의 데이터 전송을 완료할 수 있습니다. Linux 커널은 또한 일련의 값을 읽고 쓰는 일련의 함수를 제공합니다.이 함수는 위 함수의 반복 호출입니다.함수 프로토타입은 다음과 같습니다.
/*以下3个函数读取一串I/O内存的值*/
#define ioread8_rep(p,d,c)  _raw_readsb(p,d,c)
#define ioread16_rep(p,d,c)  _raw_readsw(p,d,c)
#define ioread32_rep(p,d,c)  _raw_readsl(p,d,c)
/*以下3个函数写入一串I/O内存的值*/
#define iowrite8_rep(p,s,c) __raw_writesb(p,s,c)
#define iowrite16_rep(p,s,c) __raw_writesw(p,s,c)
#define iowrite32_rep(p,s,c) __raw_writesl(p,s,c)

Linux 커널 소스 코드를 읽을 때 일부 드라이버가 readb()、readw()、readl(). 드라이버 호환성을 보장하기 위해 커널은 여전히 ​​이러한 함수를 사용하지만 앞서 언급한 함수의 사용은 새 드라이버 코드에서 권장됩니다. 주된 이유는 새 기능이 실행 중일 때 유형 검사를 수행하여 드라이버의 안전을 보장하기 때문입니다. 이전 함수 또는 매크로 프로토타입은 다음과 같습니다.

u8 readb(const volatile void __iomem *addr)
void writeb(u8 b, volatile void __iomem *addr)
u8 readw(const volatile void __iomem *addr)
void writew(u16 b, volatile void __iomem *addr)
u8 readl(const volatile void __iomem *addr)
void writel(u32 b, volatile void __iomem *addr)

readb()기능은 ioread8()기능과 동일합니다. writeb()기능은 iowrite8()기능과 동일합니다. 기타 유사점.
4.I/O内存的读写举例
I/O 메모리를 사용하여 완료하는 예는 rtc실시간 클럭 드라이버에서 볼 수 있습니다. rtc실시간 클럭 드라이버의 프로브 기능 에서 s3c_rtc_probe(). platform_get_resource()먼저 플랫폼 장치에서 I/O 포트의 정의, 주로 I/O 포트의 물리적 주소를 얻기 위해 함수가 호출됩니다 . 그런 다음 request_mem_region()ioremap()함수를 사용하여 I/O 포트의 물리적 주소를 가상 주소로 변환하고 전역 변수에 저장하여 이 변수를 통해 레지스터 값에 액세스할 수 있도록 합니다 s3c_rtc_base. 함수의 코드는 다음과 같습니다.rtcs3c_rtc_probe()

static void __iomem *s3c_rtc_base; /*定义一个全局变量,存放虚拟内存地址*/
static int __devint s3c_rtc_probe(struct platform_device *pdev)
{
    
    
	... /*省略部分代码*/
	/*从平台设备中获得I/O端口的定义*/
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if(res == NULL){
    
    
		dev_err(&pdev->dev, "failed to get memory region resource\n")
		return -ENPENT;
	}
	s3c_rtc_mem = request_mem_region(res->start,
					res->end - res->start+1,
					pdev->name); /*申请内存资源*/
	if(s3c_rtc_mem == NULL){
    
      /*如果有错误,则跳转到错误处理代码中*/
		dev_err(&pdev->dev, "failed to reserve memory region\n");
		ret = -ENOENT;
		goto err_nores;
	}
	s3c_rtc_base = ioremap(res->start, res->end - res->start + 1);
	/*申请物理地址映射成虚拟地址*/
	if(s3c_rtc_base == NULL){
    
      /*如果错误,则跳转到错误处理代码中*/
		dev_err(&pdev->dev, "failed ioremap()\n");
		ret = -EINVAL;
		goto err_nomap;
	}
	...   /*省略部分代码*/

err_nomap:
	iounmap(s3c_rtc_base);   /*如果错误,则取消映射*/
err_nortc:
	release_resource(s3c_rtc_mem); /*如果错误,则释放资源*/
err_norse:
	return ret;
}

request_mem_region()및 함수를 사용하여 ioremap()I/O 포트의 물리적 주소를 가상 주소로 변환하여 readb()writeb()함수를 사용하여 I/O 메모리에 데이터를 쓸 수 있습니다. rtc실시간 시계의 기능은 s3c_rtc_setpie()두 가지 기능을 사용하여 S3C2410_TICNT에 데이터를 씁니다. s3c_rtc_setpie()함수의 소스 코드는 다음과 같습니다.

static int s3c_rtc_setpie(struct device *dev, int enable)
{
    
    
	unsigned int tmp;
	pr_debug("%s:pie=%d\n",__func__, enabled);
	spin_lock_irq(&s3c_rtc_pie_lock);
	tmp = readb(s3c_rtc_base + S3Cs410_TICNT) & ~S3C2410_TICNT_ENABLE;
	if(enabled)
		tmp |= S3C2410_TICNT_ENABLE;
	writeb(tmp, s3c_rtc_base + S3C2410_TICNT);
	spin_unlock_irq(&s3c_rtc_pie_lock);
	return 0;
}

I/O 메모리가 더 이상 사용되지 않으면 iounmap()기능을 사용하여 물리적-가상 주소 매핑을 해제할 수 있습니다. 마지막으로 release_mem_region()함수를 사용하여 요청된 리소스를 해제합니다. rtc실시간 시계의 기능은 요청된 메모리 공간을 s3c_rtc_remove()사용 iounmap()하고 해당 기능은 해제합니다. 기능 코드는 다음과 같습니다.release_mem_region()s3c_rtc_remove()

static int __devexit s3c_rtc_remove(struct platform_device *dev)
{
    
    
	struct rtc_device *rtc = platform_get_drvdata(dev);
	platform_set_drvdata(dev, NULL);
	rtc_device_unregister(&dev->dev, 0);
	s3c_rtc_setpie(0);
	iounmap(s3c_rtc_base);
	release_resource(s3c_rtc_mem);
	kfree(s3c_rtc_mem);
	return 0;
}

I/O 포트 사용

I/O 주소 공간을 사용하는 외부 장치의 경우 I/O 포트 및 장치를 통해 데이터를 전송해야 합니다. I/O 포트에 접근하기 전에 I/O 포트에서 사용하는 리소스를 커널에 적용해야 합니다. 아래 그림과 같이 Loading 함수나 Device Driver 모듈의 함수에서 I/O 포트 리소스를 요청하는 함수 를 open()호출하거나 함수에서 요청한 I/O 메모리 리소스를 해제할 수 있습니다. I/O 포트에 접근하려면 I/O 포트에 해당하는 메모리 리소스를 신청해야 하며, 그 전에는 I/O 포트에서 작업을 할 수 없습니다. 리눅스 커널은 I/O 포트의 자원을 신청하는 기능을 제공하며, 포트의 자원을 신청해야만 해당 포트를 사용할 수 있다.이 기능은 다음과 같다 .request_region()inb()、outb()、intw()、outw()release()
여기에 이미지 설명 삽입
1.申请和释放I/O端口
request_region()

#define request_region(start,n,name)  __request_region(&ioport_resource,
(start), (n), (name), 0)
struct resource * __request_region(struct resource *parent, 
					resource_size_t start, resource_size_t n,
					const char *name, int flags)

request_region()__request_region()함수는 함수에 의해 구현되는 매크로입니다 . 이 매크로는 3개의 매개변수를 받는데 첫 번째 매개변수는 start사용할 I/O 포트의 주소이고 두 번째 매개변수는 start처음부터 n개의 포트를 나타내며 세 번째 매개변수는 장치 이름입니다. 할당에 성공하면 request_region()NULL이 아닌 값을 반환하고, 실패하면 NULL 값을 반환하는데, 이때 이러한 포트는 사용할 수 없다.
__request_region()이 함수는 리소스를 신청하는 데 사용되며 이 함수에는 5개의 매개변수가 있습니다. 첫 번째 매개변수는 리소스의 상위 리소스이므로 모든 시스템 리소스가 리소스 트리로 연결되어 커널 관리에 편리합니다. 두 번째 매개변수는 I/O 포트의 시작 주소입니다. 세 번째 매개변수는 매핑해야 하는 I/O 포트 수를 나타냅니다. 네 번째 매개변수는 장치의 이름입니다. 다섯 번째 매개변수는 리소스의 플래그입니다.
I/O 포트를 더 이상 사용하지 않을 경우 적절한 시기에 I/O 포트를 해제해야 하는데, 이 과정은 일반적으로 모듈의 언로딩 기능에 있습니다. I/O 포트를 해제하는 매크로는 다음과 같습니다 release_region().

#define release_region(start, n)   __release_region(&ioport_resource, (start), (n))
void __release_region(struct resource *parent, resource_size_t start, reosurce_size_t n)

release_region__release_region()함수에 의해 구현되는 매크로입니다 . 첫 번째 매개변수는 start사용할 I/O 포트의 주소이고 두 번째 매개변수는 start처음부터 n개의 포트를 나타냅니다.
2.读写I/O端口
드라이버가 I/O 관련 리소스를 적용한 후 이러한 포트에서 데이터를 읽거나 쓸 수 있습니다. 기능이 다른 레지스터에 다른 값을 쓰면 외부 장비가 해당 작업을 완료할 수 있습니다. 일반적으로 대부분의 주변 장치는 포트 크기를 8비트, 16비트 또는 32비트로 설정합니다. 크기가 다른 포트는 서로 다른 읽기 및 쓰기 기능이 필요하며 이러한 기능을 혼합해서는 안 됩니다. 8비트 포트를 읽는 함수로 16비트 포트를 읽으면 오류가 발생합니다. 포트 데이터를 읽는 기능은 아래와 같습니다.

  • inb()和outb()함수는 8비트 포트를 읽고 쓰는 함수입니다. inb()함수의 첫 번째 매개변수는 부호 없는 16비트 포트 번호인 포트 번호입니다. outb()이 함수는 8비트 데이터를 포트에 쓰는 데 사용되며 첫 번째 매개변수는 쓸 8비트 데이터이고 두 번째 매개변수는 I/O 포트입니다.
static inline u8 inb(u16 port)
static inline void outb(u8 v, u16 port)
  • inw()和outw()함수는 16비트 포트를 읽고 쓰는 함수입니다. inb()함수의 첫 번째 매개변수는 부호 없는 16비트 포트 번호인 포트 번호입니다. outw()이 함수는 16비트 데이터를 포트에 쓰는 데 사용되며 첫 번째 매개변수는 쓸 16비트 데이터이고 두 번째 매개변수는 I/O 포트입니다.
static inline u6 inw(u16 port)
static inline void outw(u16 v, u16 port)
  • inl()和outl()함수는 32비트 포트를 읽고 쓰는 함수입니다. inl()함수의 첫 번째 매개변수는 부호 없는 32비트 포트 번호인 포트 번호입니다. outl()이 함수는 32비트 데이터를 포트에 쓰는 데 사용되며 첫 번째 매개변수는 쓸 32비트 데이터이고 두 번째 매개변수는 I/O 포트입니다.
static inline u8 inl(u32 port)
static inline void outl(u32 v, u16 port)

위의 함수는 기본적으로 한번에 1, 2, 4바이트를 전송합니다. 한 번에 데이터 문자열을 전송하는 기능도 일부 프로세서에서 실현되며 문자열의 기본 단위는 바이트, 워드 및 더블 워드가 될 수 있습니다. 문자열 전송은 개별 바이트 전송보다 훨씬 빠르므로 대량의 데이터를 전송할 때 유용합니다. 문자열 전송을 위한 일부 I/O 함수 프로토타입은 다음과 같습니다.

void insb(unsigned long addr, void *dst, unsigned long count)
void outsb(unsigned long addr, void *src, unsigned long count)

insb()함수는 주소 에서 주소 addr바이트를 dst읽고 함수는 주소 에서 주소 바이트를 씁니다 .countoutsb()addrsrccount

void insw(unsigned long addr, void *dst, unsigned long count)
void outsw(unsigned long addr, void *src, unsigned long count)

insw()함수는 주소 에서 바이트를 addr읽고 함수는 주소 에서 주소 바이트를 씁니다 .countx2outsw()addrdstcountx2

void insl(unsigned long addr, void *dst, unsigned long count)
void outsl(unsigned long addr, void *src, unsigned long count)

insw()함수는 주소 에서 바이트를 addr읽고 함수는 주소 에서 주소 바이트를 씁니다 . 문자열 전송 기능은 포트에서 지정된 길이의 데이터를 직접 읽거나 씁니다. 따라서 외부 장치와 호스트 간에 다른 엔디안이 있으면 예기치 않은 오류가 발생합니다. 예를 들어 호스트는 리틀 엔디안 바이트 순서를 사용하고 외부 장치는 빅 엔디안 바이트 순서를 사용하는데, 데이터를 읽고 쓸 때 서로 이해할 수 있도록 바이트 순서를 바꿔야 합니다.countx4outsw()addrdstcountx4

요약

외부 장치는 메모리 공간 또는 I/O 공간에 있을 수 있으며 임베디드 제품의 경우 일반적으로 외부 장치는 메모리 공간에 있습니다. 이 장에서는 외부 장치가 메모리 공간과 I/O 공간에 각각 있는 상황을 설명합니다. Linux에서는 드라이버 작성을 용이하게 하기 위해 메모리 공간과 I/O 공간에 접근하기 위한 통일된 방법을 제공합니다. ".
Linux에서는 백업 캐시를 사용하여 동일한 개체를 자주 할당하고 해제하므로 메모리 조각화 발생을 줄일 수 있을 뿐만 아니라 시스템의 성능을 향상시키고 드라이버의 효율성을 위한 기반을 마련합니다.
Linux는 페이지 할당 및 해제를 위한 일련의 기능을 제공하며 이러한 기능은 필요에 따라 물리적으로 연속적이거나 비연속적인 메모리를 할당하고 가상 주소 공간에 매핑한 다음 액세스할 수 있습니다.

추천

출처blog.csdn.net/m0_56145255/article/details/131708990