Linux驱动进阶(四)——内外存访问

news2024/12/26 9:27:34

文章目录

  • 前言
  • 内存分配
    • kmalloc函数
    • vmalloc()函数
    • 后备高速缓存
  • 页面分配
    • 内存分配
    • 物理地址和虚拟地址之间的转换
  • 设备I/O端口的访问
    • Linux I/O端口读写函数
    • I/O内存读写
    • 使用I/O端口
  • 小结


前言

驱动程序加载成功的一个关键因素,就是内核能够为驱动程序分配足够的内存空间。这些控件一部分用于驱动程序必要的数据结构,另一部分用于数据交换。同时,内核也应该具有访问外部设备端口的能力。一般来说,外部设备被连接到内存空间或者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()函数的第一个参数是size,表示分配内存的大小。第2个参数是分配标志,可以通过这个标志控制kmalloc()函数的多种分配方式。和其他函数不同,kmalloc()函数的这两个参数非常重要,下面将对这两个参数详细的解释。
1.size参数
size参数涉及内存管理的问题,内存管理是Linux子系统中非常重要的一部分。Linux的内存管理方式限定了内存只能按照页面的大小进行内存分配。通常,页面大小为4K。如果使用kmalloc()函数为某个驱动程序分配4字节的内存空间,则Linux会返回一个页面4K的内存空间,这显然是一种内存浪费。
因为空间浪费的原因,kmalloc()函数与用户空间malloc()函数的实现完全不同。malloc()函数在堆中分配内存空间,分配的空间大小非常灵活,而kmalloc()函数分配内存空间的方法比较特殊,下面对这种方法进行简要的解释。
Linux内核对kmalloc()函数的处理方式是,先分配一系列不同大小的内存池,每个池中的内存大小是固定的。当分配内存时,将包含足够大的内存池中的内存传递给kmalloc()函数。在分配内存时,Linux内核只能分配预定义、固定大小的字节数。如果申请的内存大小不是2的整数倍,则会多申请一些内存,将大于申请内存的内存区块返回给请求者。
Linux内核为kmalloc()函数提供了大小为32字节、64字节、128字节、256字节、512字节、1024字节、2048字节、4096字节、8KB、16KB、32KB、64KB和128KB的内存池。所以程序员应该注意,kmalloc()函数最小能够分配32字节的内存,如果请求的内存小于32字节,那么也会返回32字节。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是分配连续内存的大小。如果函数执行成功,则返回虚拟地址连续的一块内存区域。为了释放内存,Linux内核也提供了一个释放由vmalloc()函数分配的内存,这个函数是vfree()函数,其代码如下:

void vfree(const void *addr)

2.vmalloc()函数举例
vmalloc()函数在功能上与kmalloc()函数不同,但在使用上基本相同。首先使用vmalloc()函数分配一个内存空间,并返回一个虚拟地址。内存分配是一项要求严格的任务,无论什么时候,都应该对返回值进行检测。当分配内存后,可以使用copy_from_user()对内存进行访问。也可以将返回的内存空间转换为一个结构体,像下面代码的12~15行一样使用vmalloc()分配的内存空间。在不需要使用内存时,可以使用20行的vfree()函数释放内存。在驱动程序中,使用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;
}

后备高速缓存

在驱动程序中,会经常反复地分配很多同一大小的内存块,也会频繁的将这些内存块给释放掉。如果频繁的申请和释放内存,很容易产生内存碎片,使用内存池很好地解决了这个问题。在Linux中,为一些反复分配和释放的结构体预留了一些内存空间,使用内存池来管理,管理这种内存池的技术叫做slab分配器。这种内存叫做后备高速缓存。
slab分配器的相关函数定义在<linux/slab.h>文件中,使用后备高速缓存前,需要创建一个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表示该后备高速缓存对象的名字,以后可以使用name来表示使用那个后备高速缓存。
kmem_cache_create()函数的第3个参数align是后备高速缓存中第一个对象的偏移值,这个值一般情况下被置为0。第4个参数flage是一个位掩码,表示控制如何完成分配工作。第5个参数ctor是一个可选的函数,用来对加入后备高速缓存中的内存块进行初始化。

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)

该函数的第1个参数cachep是开始分配的后备高速缓存。第二个参数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()函数释放后,才能销毁后备高速缓存。
4.slab缓存举例
一个使用后备高速缓存的例子如下代码所示,这段代码创建了一个存放struct thread_info结构体的后备高速缓存,这个结构体表示线程结构体。在Linux中,涉及大量线程的创建和销毁,如果使用__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 page结构体函数主要有两个:alloc_page()函数和alloc_pages()函数。这两个函数定义在/include/linux/gfp.h文件中。alloc_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的order次方来表示,例如如果只分配一个页面,order的值应该为0。
  • alloc_pages()函数调用如果成功,会返回指向第一个页面的struct page结构体的指针;如果分配失败,则返回一个NULL值。任何时候内存分配都有可能失败,所以应该在内存分配之后检查其返回值是否合法。
  • alloc_page()函数定义在2行,其只分配一个页面。这个宏只接收一个gfp_mask参数,表示内存分配的标志。默认情况下order被设置为0,表示函数将分配2的0次方等于1个物理页面给申请进程。
    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的order次方。如果要分配一个页面,那么只需要order等于0就可以了。
  • 3行,定义了一个struct page的指针。
  • 4行,调用了alloc_pages()函数分配了2的order次方页的内存空间。
  • 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宏,__pa宏会将虚拟地址address减去PAGE_OFFSET,通常在32位平台上定义为3GB
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宏,__va宏会将物理地址address加上PAGE_OFFSET,通常在32为平台上定义为3GB。
Linux中,物理地址和虚拟地址的关系如下图。在32位计算机中,最大的虚拟地址空间大小是4GB0~3GB表示用户空间,PAGE_OFFSET被定义为3GB,就是用户空间和内核空间的分界点。Linux内核中,使用3GB~4GB的内核空间来映射实际的物理地址。物理内存可能大于1GB的内核空间,甚至可能大很多。目前,主流的计算机物理内存在4GB左右。这种情况下,Linux使用一种非线性的映射方法,用1GB大小的内核空间来映射有可能大于1GB的物理内存。
在这里插入图片描述

设备I/O端口的访问

设备有一组外部寄存器用来存储和控制设备的状态。存储设备状态的寄存器叫做数据寄存器;控制设备状态的寄存器叫做控制寄存器。这些寄存器可能位于内存空间,也可能位于I/O空间,本节将介绍这些空间的寄存器访问方法。

Linux I/O端口读写函数

设备内部集成了一些寄存器,程序可以通过寄存器来控制设备。大部分外部设备都有多个寄存器,例如看门狗控制寄存器(WTCON)、数据寄存器(WTDAT)和计数寄存器(WTCNT)等;有如IIC设备也有4个寄存器来完成所有IIC操作,这些寄存器是IICCONIICSTATIICADDIICCDS
根据设备需要完成的功能,可以将外部设备连接到内存地址空间上或者连接到I/O地址空间。无论是内存地址空间还是I/O地址空间,这些寄存器的访问都是连续的。一般台式机在设计时,因为内存地址空间比较紧张,所以一般将外部设备连接到I/O地址空间上。而对于嵌入式设备,内存一般是64M或者128M,大多数嵌入式处理器支持1G的内存空间,所以可以将外部设备连接到多余的内存空间上。
在硬件设计上,内存地址空间和I/O地址空间的区别不是很大,都是由地址总线、控制总线和数据总线连接到CPU上的。对于非嵌入式产品的大型设备使用的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_region带3个参数,第一个参数start是物理地址的开始区域,第2个参数n是需要分配内存的字节长度,第3个参数name是这个资源的名字。如果函数成功,返回一个资源指针;如果函数失败,则返回一个NULL值。在模块卸载函数中,如果不再使用内存资源,可以使用release_region()宏释放内存资源,该函数的原型如下:

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

2.物理地址到虚拟地址的映射函数
在使用读写I/O内存的函数之前,需要使用ioremap()函数,将外部设备的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中,这样就可以通过这个变量访问rtc寄存器的值了。s3c_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端口使用的资源。如下图所示,在设备驱动模块的加载函数或者open()函数中可以调用request_region()函数请求I/O端口资源;然后使用inb()、outb()、intw()、outw()等函数来读写外部设备的I/O端口;最后,在设备驱动程序的模块卸载函数或者release()函数中,释放申请的I/O内存资源。
在这里插入图片描述
1.申请和释放I/O端口
如果要访问I/O端口,那么就需要先申请一个内存资源来对应I/O端口,在此之前,不能对I/O端口进行操作。Linux内核提供了一个函数来申请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端口的地址,第2个参数表示从start开始的n个端口,第3个参数是设备的名字。如果分配成功,那么request_region()函数会返回一个非NULL值;如果失败,则返回NULL值,此时,不能使用这些端口。
__request_region()函数用来申请资源,这个函数有5个参数。第1个参数是资源的父资源,这样所有系统的资源被连接成一棵资源树,方便内核的管理。第2个参数是I/O端口的开始地址。第3个参数表示需要映射多少个I/O端口。第4个参数是设备的名字。第5个参数是资源的标志。
如果不再使用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端口的地址,第2个参数表示从start开始的n个端口。
2.读写I/O端口
当驱动程序申请了I/O相关的资源后,可以对这些端口进行数据的读取或写入。对不同功能的寄存器写入不同的值,就能够使外部设备完成相应的工作。一般来说,大多数外部设备将端口的大小设为8位、16位或者32位。不同大小的端口需要使用不同的读取和写入函数,不能将这些函数混淆使用。如果用一个读取8位端口的函数读一个16位的端口,会导致错误。读取端口数据的函数如下所示。

  • inb()和outb()函数是读写8位端口的函数。inb()函数第一个参数是端口号,其是一个无符号的16位端口号。outb()函数用来向端口写入一个8位的数据,第1个参数是要写入的8位数据,第2个参数是I/O端口。
static inline u8 inb(u16 port)
static inline void outb(u8 v, u16 port)
  • inw()和outw()函数是读写16位端口的函数。inb()函数第一个参数是端口号,其是一个无符号的16位端口号。outw()函数用来向端口写入一个16位的数据,第1个参数是要写入的16位数据,第2个参数是I/O端口。
static inline u6 inw(u16 port)
static inline void outw(u16 v, u16 port)
  • inl()和outl()函数是读写32位端口的函数。inl()函数第一个参数是端口号,其是一个无符号的32位端口号。outl()函数用来向端口写入一个32位的数据,第1个参数是要写入的32位数据,第2个参数是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地址读取count个字节,outsb()函数从addr地址向src地址写入count个字节。

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

insw()函数从addr地址读取countx2个字节,outsw()函数从addr地址向dst地址写入countx2个字节。

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

insw()函数从addr地址读取countx4个字节,outsw()函数从addr地址向dst地址写入countx4个字节。
需要注意的是,串传输函数直接从端口中读出或者写入指定长度的数据。因此,如果当外部设备和主机之间有不同的字节序时,则会导致意外的错误。例如主机使用小端字节序,外部设备使用大端字节序,在进行数据读写时,应该交换字节序,使彼此互相理解。

小结

外部设备可以处于内存空间或者I/O空间中,对于嵌入式产品来说,一般外部设备处于内存空间中。本章对外部设备处于内存空间和I/O空间的情况分别进行了讲解。在Linux中,为了方便编写驱动程序,对内存空间和I/O空间的访问提供了一套统一的方法,这个方法是“申请资源->映射内存空间->访问内存->取消映射->释放资源”。
Linux中使用了后备高速缓存来频繁地分配和释放同一种对象,这样不但减少了内存碎片的出现,而且还提高了系统的性能,为驱动程序的高效性打下基础。
Linux中提供了一套分配和释放页面的函数,这些函数可以根据需要分配物理连续或者不连续的内存,并将其映射到虚拟地址空间中,然后对其进行访问。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/757568.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

论文解读:Inpaint Anything: Segment Anything Meets Image Inpainting

论文&#xff1a;https://arxiv.org/pdf/2304.06790.pdf 代码&#xff1a;https://github.com/geekyutao/Inpaint-Anything 图1&#xff1a;Inpaint Anything示意图。用户可以通过点击图像中的任何对象来选择它。借助强大的视觉模型&#xff0c;例如SAM[7]、LaMa [13]和稳定扩散…

我叫李明,我是一名开发人员

目录 一、这是一个故事 二、不屈不挠的李明 三、化解于无形 四、总结 一、这是一个故事 这个故事的主人公是一个年轻的程序员&#xff0c;他叫做李明。李明是一名技术过硬、工作认真负责的程序员&#xff0c;他的工作是开发一款新的软件产品。这款软件是一款在线购物平…

【Java基础】第四章 Object 类应用

系列文章目录 [Java基础] 第一章 String类应用及分析 [Java基础] 第二章 数组应用及源码分析 [Java基础] 第三章 StringBuffer 和 StringBuilder 类应用及源码分析 [Java基础] 第四章 Object 类应用 文章目录 系列文章目录前言一、如何使用Object&#xff1f;1.1、显式继承1.2…

c++内存映射文件

概念 将一个文件直接映射到进程的进程空间中&#xff08;“映射”就是建立一种对应关系,这里指硬盘上文件的位置与进程逻辑地址空间中一块相同区域之间一 一对应,这种关系纯属是逻辑上的概念&#xff0c;物理上是不存在的&#xff09;&#xff0c;这样可以通过内存指针用读写内…

Web-登录功能实现(含JWT令牌)

登录功能 这个登陆功能先不返回JWT令牌 登陆会返回JWT令牌 一会在登陆验证时讲解JWT令牌&#xff08;返回的data就是它&#xff09; 登录校验 概述 就是你比如复制一个url 用一个未曾登陆对应url系统的浏览器访问 他会先进入登陆页面 登陆校验就是实现这个功能 简而言之…

基于EasyExcel的单元格合并自定义算法处理

基于EasyExcel导出Excel后&#xff0c;通过对合并单元格的简单规则配置&#xff0c;实现如下图所示的单元格合并效果&#xff1a; 效果截图 原表格数据如下&#xff1a; 通过配置单元格合并规则后&#xff0c;生成的合并后的表格如下&#xff1a; 注&#xff1a;其中第三列&a…

Android Studio连接安卓手机

1. 创建项目 2. 下载Google USB Driver 点击右上角红框的【SDK Manager】->【SDK Tools】。 也可以在 【tools】->【SDK Manager】->【SDK Tools】下进入。 点击Google USB Driver&#xff0c;下载后点ok。 3. 环境变量 右键【我的电脑】->【高级系统设置】-&g…

基于微信小程序的高校新生自助报道系统设计与实现(Java+spring boot+MySQL+小程序)

获取源码或者论文请私信博主 演示视频&#xff1a; 基于微信小程序的高校新生自助报道系统设计与实现&#xff08;Javaspring bootMySQL微信小程序&#xff09; 使用技术&#xff1a; 前端&#xff1a;html css javascript jQuery ajax thymeleaf 微信小程序 后端&#xff1…

123、仿真-基于51单片机的电流控制仿真系统设计(Proteus仿真+程序+原理图+参考论文+配套资料等)

方案选择 单片机的选择 方案一&#xff1a;STM32系列单片机控制&#xff0c;该型号单片机为LQFP44封装&#xff0c;内部资源足够用于本次设计。STM32F103系列芯片最高工作频率可达72MHZ&#xff0c;在存储器的01等等待周期仿真时可达到1.25Mip/MHZ(Dhrystone2.1)。内部128k字节…

java报错- 类文件具有错误的版本 61.0, 应为 52.0 请删除该文件或确保该文件位于正确的类路径子目录中。

SpringBoot使用了3.0或者3.0以上&#xff0c;因为Spring官方发布从Spring6以及SprinBoot3.0开始最低支持JDK17&#xff0c;所以仅需将SpringBoot版本降低为3.0以下即可。

ES6类-继承-Symbol-模版字符串

目录 类 继承 ES5 如何继承 ES6继承 Symbol 用途 可以产生唯一的值&#xff0c;独一无二的值 解决命名冲突 getOwnPropertySymbols() 作为全局注册表 缓存 Symbol.for() 消除魔术字符串 模版字符串 类 在javascript语言中&#xff0c;生成实例对象使用构造函数&#xf…

数据库基本操作-----数据库用户管理和授权

目录 一、数据库用户管理 1&#xff0e;新建用户 2&#xff0e;查看用户信息 3&#xff0e;重命名用户 4&#xff0e;删除用户 ​编辑5&#xff0e;修改当前登录用户密码 6&#xff0e;修改其他用户密码 7&#xff0e;忘记 root 密码的解决办法 &#xff08;1&#xff09;修…

Redis数据类型 — List

List 列表是简单的字符串列表&#xff0c;按照插入顺序排序&#xff0c;可以从头部或尾部向 List 列表添加元素。 List内部实现 List 类型的底层数据结构是由双向链表或压缩列表实现的&#xff1a; 如果列表的元素个数小于 512 个&#xff08;默认值&#xff0c;可由 list-m…

详解Single-Shot Alignment Network (S2A-Net) 基于遥感图像的特征对齐旋转目标检测

引言 目标检测&#xff1a;把图像中的物体使用方框标记起来&#xff0c;不同类别物体应使用不同颜色 目标检测其实是寻找物体边界框(bounding box)回归问题(regression)和对物体分类问题(classification)的统一 遥感目标检测&#xff1a;普通的目标检测是日常生活中的横向的图…

2.4 线性表的插入删除

1. 链表的插入删除 1. 单链表插入删除 图1. 单链表插入结点 图2. 单链表删除结点 #include <iostream>typedef struct LNode {int data;struct LNode* next; }LNode;/// <summary> /// 判断链表是否非空 /// </summary> /// <param name"p">…

常见关于数组的函数的介绍

关于字符串函数的介绍 求字符串长度 strlen函数 用于计算字符串的长度的函数&#xff0c;需要使用的库函数是string.h 函数声明 size_t strlen(const char *str)函数模拟实现 #include<stdio.h> #include<assert.h> size_t my_strlen(const char* arr) {asse…

review回文子串

给你一个字符串 s&#xff0c;请你将 s 分割成一些子串&#xff0c;使每个子串都是 回文串 。返回 s 所有可能的分割方案。 回文串 是正着读和反着读都一样的字符串。 class Solution {List<List<String>> lists new ArrayList<>(); // 用于存储所有可能…

阿里瓴羊One推出背后,零售企业迎数字化新解

配图来自Canva可画 近年来随着数字经济的高速发展&#xff0c;各式各样的SaaS应用服务更是层出不穷&#xff0c;但本质上SaaS大多局限于单一业务流层面&#xff0c;对用户核心关切的增长问题等则没有提供更好的解法。在SaaS赛道日渐拥挤、企业增长焦虑愈演愈烈之下&#xff0c…

Midjourney助力交互设计师设计网站主页

Midjourney的一大核心优势是提供创意设计&#xff0c;这个功能也可以用在网站主页设计上&#xff0c;使用Midjourney prompt 应尽量简单&#xff0c;只需要以"web design for..." or "modern web design for..."开头即可 比如设计一个通用SAAS服务的初创企…

单片机第一季:零基础5——LED点阵

1&#xff0c;第八章-LED点阵 如何驱动LED点阵&#xff1a; (1)单片机端口直接驱动。要驱动8*8的点阵需要2个IO端口&#xff08;16个IO口&#xff09;、要驱动16*16的点阵需要4个IO端口&#xff08;32个IO口&#xff09;。 (2)使用串转并移位锁存器驱动。要驱动16*16点阵只需要…