也许是先入为主的缘故,在看linux设备驱动的并发控制,阻塞相关章节时,我更愿意看宋宝华老师的那本设备驱动开发详解。
linux设备驱动中的并发控制
并发与竞态
解决方法:保证对共享资源的互斥访问!
访问共享资源的代码区称为临界区(critical sections),临界区需要以某种互斥机制加以保护。
linux设备驱动的互斥机制:中断屏蔽、原子操作、自旋锁、信号量。
中断屏蔽
使用方法:
---------------------------
local_irq_disable(); // 屏蔽中断
---------------------------
critical sections // 临界区
---------------------------
local_irq_enable(); // 开中断
---------------------------
缺点:
(1)linux系统的异步I/O、进程调度等很多重要操作都依赖于中断,长时间屏蔽中断是不可取的;
(2)并不能解决SMP问题
因此单独使用中断屏蔽通常不是一种值得推荐的避免竞态的方法,它适宜与自旋锁联合使用。
原子操作
原子操作是指在执行过程中不会被别的代码路径所中断的操作。
原子操作分为:整型原子操作、位原子操作。
自旋锁
自旋锁(spin lock)是一种对临界资源进行互斥访问的典型手段,其名称来源于它的工作方式。
一般这样使用
spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock); //获取自旋锁,保护临界区
----// 临界区
spin_unlock(&lock); // 解锁
自旋锁主要针对SMP或者单CPU但内核可抢占的情况,对于单CPU且内核不支持抢占的系统,自旋锁退化为空操作。
应当谨慎使用自旋锁,使用中还要特别注意如下几个问题:
1,自旋锁实际上是忙等待,当锁不可用时,CPU一直循环执行“测试并设置”该锁直到可用而取得该锁。因此只有在占用锁的时间极
短的情况下,使用自旋锁才是合理的。
2,自旋锁可能导致系统死锁。引发这个问题最常见的情况就是递归使用一个自旋锁;此外,如果进程获得自旋锁之后再阻塞,也有
可能导致死锁的发生。copy_from_user()、copy_to_user()、kmalloc()等函数都有可能引起阻塞,因此在自旋锁的占用期间不能调
用这些函数。
读写自旋锁
自旋锁对读或者写一视同仁,不支持并发访问,于是出现了自旋锁的衍生锁读写自旋锁(rwlock)可允许读的并发,但是读和写也能同
时进行。
顺序锁
顺序锁(seqlock)是对读写锁的一种优化,若使用顺序锁,读执行单元绝不会被写执行单元阻塞。但是写执行单元与写执行单元之间
仍然是排斥的。
顺序锁有一个限制,它必须要求被保护的共享资源不含有指针,因为写执行单元可能使得指针失效,但读执行单元如果正要访问该指
针,将导致OOPS。
读--拷贝--更新
RCU(Read-Copy Update),它是基于其原理命名的。可看作读写锁的高性能版本,相比读写锁,RCU的优点在于既允许多个读执行单元
访问被保护的数据,又允许多个读执行单元与多个写执行单元同时访问被保护的数据。
但是RCU布恩那个替代读写锁,因为如果写比较多时,对读执行单元的性能提高不能弥补写执行单元导致的损失。因为使用RCU时,写
执行单元之间的同步开销会比较大,它需要延迟数据结构的释放,复制被修改的数据结构,它也必须使用某种锁机制同步并行的其他
写执行单元的修改操作。
信号量
信号量(semaphore)是用于保护临界区的一种常用方法,它的是用方式和自旋锁类似,与自旋锁不同的是当获取不到信号量时,进程
不会原地打转而是进入休眠等待状态。
如果信号量被初始化为0,则它可以用于同步。不过linux提供了一种比信号量更好的同步机制-----completion,它用于一个执行单
元等待另外一个执行单元完成某事。
自旋锁与信号量PK
自旋锁与信号量都是解决互斥问题的基本手段,面对特定的情况,应该如何进行选择呢?选择的依据是临界区的性质和系统
的特点。
从严格意义上来说,信号量和自旋锁属于不同层次的互斥手段,前者的实现依赖于后者。
信号量是进程级的。因此只有当进程占用资源时间较长时,用信号量才是较好的选择。当所要保护的临界区访问时间比较短
时,使用自旋锁是非常方便的。
linux设备驱动中的阻塞与非阻塞I/O
阻塞操作是指在执行设备操作时若不能获得资源则刮起进程,直到满足可操作的条件后再进行操作。 非阻塞操作的进程进行设备操作时并不挂起,它或者放弃,或者不停地查询,直至可以进行操作为止。
等待队列 在linux驱动程序中,可以使用等待队列(wait queue)来实现阻塞进程的唤醒。 linux2.6提供如下关于等待队列的操作。 1,定义“等待队列头” wait_queue_head_t my_queue; 2,初始化“等待队列头” init_waitqueue_head(&my_queue); 而下面的DECLARE_WAIT_QUEUE_HEAD()宏可以作为定义并初始化等待队列头的快捷方式。 DECLARE_WAIT_QUEUE_HEAD(name) 3,定义等待队列 DECLARE_WAITQUEUE(name,tsk) 该宏用于定义并初始化一个名为name的等待队列。 4,添加/移除等待队列 void fastcall add_wait_queue(wait_queue_head_t *q,wait_queue_t *wait);用于将等待队列wait添加到等待队列头q指向的等待
队列链表中。 void fastcall remove_wait_queue(wait_queue_head_t *q,wait_queue_t *wait);用于将等待队列wait从附属的等待队列头q指向的
等待队列链表中移除。 5,等待事件 wait_event(queue,contion) wait_event_interruptible(queue,contion) wait_event_timeout(queue,contion,timeout) wait_event_interruptible_timeout(queue,contion,timeout) 这4个从函数名字上就可以理解了。 6,唤醒队列 void wake_up(wait_queue_head_t *queue);与wait_event(queue,contion),wait_event_timeout(queue,contion,timeout)配合使
用。可以唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE的进程; void wake_up_interruptible(wait_queue_head_t *queue);与wait_event_interruptible(queue,contion),
wait_event_interruptible_timeout(queue,contion,timeout)配合使用。只能唤醒处于TASK_INTERRUPTIBLE的进程。 7,在等待队列上睡眠 sleep_on(wait_queue_head_t *queue);将目前进程的状态置成TASK_UNINTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队
列头q,知道资源可获得,q引导的等待队列被唤醒。 interruptible_sleep_on(wait_queue_head_t *queue);将目前进程的状态置成TASK_INTERRUPTIBLE,并定义一个等待队列,之后把它
附属到等待队列头q,知道资源可获得,q引导的等待队列被唤醒或者进程收到信号。
它们的流程如下所示: (1)定义并初始化一个等待队列,将进程状态改变为TASK_UNINTERRUPTIBLE或TASK_INTERRUPTIBLE,并将等待队列添加到等待队列头
。 (2)通过schedule()放弃CPU,调度其他进程执行。 (3)进程被其他地方唤醒,将等待队列移除等待队列头。
总结 在设备驱动中阻塞IO一般基于等待队列来实现,等待队列可用于同步驱动中事件发生的先后顺序。使用非阻塞IO的应用程序也可借助
轮询函数来查询设备是否能立即被访问,用户空间调用select()和poll()接口,设备驱动提供poll()函数。设备驱动中的poll()本身
不会阻塞,但是poll()和select()系统调用则会阻塞地等待文件描述符集合中的至少一个可访问或超时。 |
Linux设备驱动中的异步通知与异步I/O
异步通知的概念与作用
阻塞与非阻塞访问、poll()函数提供了较好的解决设备访问的机制,若有了异步通知机制就更加完整了。
异步通知的意思是:一旦设备就绪,则主动通知应用程序,貌似硬件中“中断”的概念,比较完整的称谓是“信号驱动的异步
I/O”。信号是软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。
信号是异步的,一个进程不必通过任何操作来等待信号的到达。
阻塞I/O意味着一直等待设备可访问后再访问,非阻塞I/O中使用poll()意味着查询设备是否可以访问,而异步通知则意味着设备
通知自身可访问,实现了异步I/O。由此可见,这几种方式I/O可以互为补充。
Linux信号
在linux系统中,异步通知使用信号来实现。除了SIGSTOP和SIGKILL两个信号外,进程能够忽略或捕获其他的全部信号。一个信号
被捕获的意思是当一个信号到达时有相应的代码处理它。如果一个信号没有被这个进程锁捕获,内核将采用默认行为处理。
信号的接收
在用户程序中,为了捕获信号,可以使用signal()函数来设置对应信号的处理函数,如下所示:
void(*signal(int signum,void (*handler))(int))(int);
可以分解如下:
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
第一个参数指定信号的值,第二个参数指定针对前面信号值的处理函数,若为SIG_IGN,表示忽略该信号;若为SIG_DFL,表示采用系统默认方式处理信号;若为用户自定义的函数,则信号被捕获后,该函数将被执行。
除了signal()函数外,sigaction函数可用于改变进程接收到特定信号后的行为,它的原型如下
sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
该函数的第一个参数为信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号。第二个参数是指向结构体sigaction的一个实例指针,在结构体sigaction的实例中,指定了对特定信号的处理函数,若为空,则进程会以默认方式对信号处理。第三个参数oldact指向的对象用来保存原来对相应信号的处理函数,可指定oldact为NULL。如果把第二个和第三个参数都设为NULL,那么该函数可用于检查信号的有效性。
为了在用户空间中能处理一个设备释放的信号,必须完成以下3项工作:
(1)通过F_SETOWN IO控制命令设置设备文件的拥有者为本进程,这样从设备驱动发出的信号才能被本进程接收到。
(2)通过F_SETFL IO控制命令设置设备文件支持FASYNC,即异步通知模式。
(3)通过signal函数连接信号和信号处理函数。
信号的释放
为了使设备支持异步通知机制,驱动程序中涉及以下3项工作。
(1)支持F_SETOWN命令;
(2)支持F_SETFL命令的处理,每当FASYNC标志改变时,驱动程序中的fasync()函数将得以执行。因此驱动中应实现fasync()函数;
(3)在设备资源可获得时,调用kill_fasync()函数获得激发相应的信号;
很明显,驱动程序的这3项与应用程序中的3项工作是一一对应的。
linux2.6异步IO
在某些情况下,IO请求可能需要与其他进程产生重叠。异步I/O(AIO)应用程序接口就提供了这种功能。
linux异步IO是2.6内核版本的一个标准特性。AIO的基本思想是允许进程发起很多IO操作,而不用阻塞或等待任何操作完成。稍后或在接收到IO操作完成的通知时,进程就可以检索IO操作的结果。
关于AIO我暂时还没有用到,就先不研究了。
中断与时钟
在linux中断中引入了顶半部和底半部分离的机制。顶半部完成尽可能少的比较紧急的功能。它往往只是简单读取寄存器中的中断状态并清除中断标志后就进行“登记中断”的工作。“登记中断”意味着将底半部处理程序挂到该设备的底半部执行队列去。这样,底半部执行的速度就会很快,可以服务更多的中断请求。以前在写单片机和arm程序时,也经常用到这种思想。
现在,中断工作的处理重心就落到了底半部的头上,它来完成中断事件的绝大多数任务。底半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断,这也是底半部和顶半部最大的不同。
Linux中断编程
申请和释放中断
1,申请IRQ
int request_irq(unsigned int irq,
void (*handler)(int irq,void *dev_id,struct pt_regs *regs),
unsigned long irqflags,
const char *devname,
void *dev_id);
irq是要申请的中断号。
handler是向系统登记的中断处理函数,是一个回调函数,中断发生时,系统调用这个函数,dev_id参数将被传递给它。
irqflags是中断处理属性,若设置了SA_INTERRUPT,则表示中断处理程序是快速处理程序;若设置了SA_SHIRQ,则表示多个设备共享中断,dev_id在中断共享时会用到,一般设置为这个设备的设备结构体或者NULL。
request_irq()返回0表示成功,返回-INVAL表示中断号无效或处理函数指针为NULL,返回-EBUSY表示中断已经被占用且不能共享。
2,释放IRQ
与request_irq()对应的函数为free_irq()
void free_irq(unsigned int irq,void *dev_id);
3,使能和屏蔽中断
下列3个函数用于操作一个中断源,对系统内所有CPU生效。
void disable_irq(int irq);// 等待目前的中断处理完成后再使用
void disable_irq_nosynv(int irq);//立即返回
void enable_irq(int irq);
下列2个函数将屏蔽本CPU内的所有中断。
void local_irq_save(unsigned long flags);
void local_irq_disable(void);
前者是将目前的中断状态保留在flags中,后者直接禁止中断。
与上述2个禁止中断对应的恢复中断的方法:
void local_irq_restore(unsigned long flags);
void local_irq_enable(void);
底半部机制
linux系统实现底半部的机制主要有tasklet、工作队列和软中断。
1,tasklet
使用比较简单,只需要定义tasklet及其处理函数,并将2者关联即可,例如:
void my_tasklet_fun(unsigned long); // 定义一个处理函数
DECLARE_TASKLET(my_tasklet,my_tasklet_fun,data);// 实现了定义名称为my_tasklet的tasklet并将其与my_tasklet_fun()这个函数绑定,而传入这个函数的参数为data。
在需要调度tasklet的时候引用一个tasklet_schedule()函数就能使系统在适当的时候进行调度运行,如下所示:
tasklet_schedule(&my_tasklet);
tasklet使用模板
// 定义tasklet和底半部函数相关联
void xxx_do_tasklet(unsigned long);
DECLARE_TASKLET(xxx_tasklet,xxx_do_tasklet,data);
// 中断处理底半部
void xxx_do_tasklet(unsigned long)
{
----
}
// 中断处理顶半部
irqreturn_t xxx_interrupt(int ira,void *dev_id,struct pt_regs *regs)
{
----
tasklet_schedule(&xxx_tasklet);
----
}
//设备驱动模块加载函数
int __init xxx_init(void)
{
---
//申请中断
result = request_irq(xxx_irq,xxx_interrupt,SA_INTERRUPT,"xxx",NULL);
---
}
//设备驱动模块卸载函数
void __exit xxx_exit(void)
{
---
//释放中断
free_irq(xxx_irq,NULL);
---
}
2,工作队列
工作队列的使用方法和tasklet非常相似。
3,软中断
软中断使用软件方式模拟硬件中断的概念,实现宏观上的异步执行效果,tasklet也是基于软中断实现的。
软中断和tasklet仍然运行于中断上下文,而工作队列则运行于进程上下文,因此软中断和tasklet处理函数不能睡眠,而工作队列处理函数允许睡眠。
前面说的异步通知所基于的信号也类似于中断。
硬中断,软中断和信号的区别:
硬中断是外部设备对CPU的中断;
软中断通常是硬中断服务程序对内核的中断;
信号是由内核(或其他进程)对某个进程的中断。
内核定时器 内核定时器编程软件意义上的定时器最终依赖硬件定时器来实现,内核在时钟中断发生后检测各定时器是否到期,到期后的定时器处理函数将作为软中断在底半部执行。实质上,时钟中断处理程序执行update_process_timers()函数,该函数调用run_local_timers()函数,这个函数处理TIMER_SOFTIRQ软中断,运行当前处理器上到期的所有定时器。 在Linux设备驱动编程中,可以利用linux内核中提供的一组函数和数据结构来完成定时触发工作或者完成某周期性的事务。 linux内核所提供的用于操作定时器的数据结构和函数如下。 1,time_list struct time_list{ struct list_head entry; // 定时器列表 unsigned long expires; // 定时器到期时间 void (*function)(unsigned long); // 定时器处理函数 unsigned long data; // 作为参数被传入定时器处理函数 struct timer_base_s *base; }; 2,初始化定时器 void init_timer(struct time_list *timer); 该函数初始化timer_list的entry的next为NULL,并给base指针赋值。
TIMER_INITIALIZER(_function,_expires,_data)宏用于赋值定时器结构体的function、expires、data、base成员。 DEFINE_TIMER(_name,_function,_expires,_data)宏是定义并初始化定时器成员的“快捷方式”。
此外,setup_timer()也可用于初始化定时器并赋值其成员。
3,增加定时器 void add_timer(struct time_list *timer); 4,删除定时器 void del_timer(struct time_list *timer); 5,修改定时器的expire void mod_timer(struct time_list *timer,unsigned long expires);
内核定时器使用模板 // xxx设备结构体 struct xxx_dev { struct cdev cdev; --- timer_list xxx_timer;// 设备要使用的定时器 }
//xxx驱动中的某函数 xxx_func1(---) { struct xxx_dev *dev = filep->private_data;
--- //初始化定时器 init_timer(&dev->xxx_timer); dev->xxx_timer.function = &xxx_do_timer; dev->xxx_timer.data = (unsigned long)dev; //设备结构体指针作为定时器处理函数参数 dev->xxx_timer.expires = jiffies + delay;
// 添加注册定时器 add_timer(&dev->xxx_timer);
--- }
//xxx驱动中的某函数 xxx_func2(---) { struct xxx_dev *dev = filep->private_data;
---
// 删除定时器 del_timer(&dev->xxx_timer);
--- }
// 定时器处理函数 static void xxx_do_timer(unsigned long arg) { struct xxx_devices *dev = (struct xxx_devices *)(arg); ---
// 调度定时器再执行 dev->xxx_timer.expires = jiffies + delay; add_timer(&dev->xxx_timer); --- }
内核延时 linux内核提供如下3个函数分别进行纳秒、微妙和毫秒延迟。实际上都是忙等待! void ndelay(unsigned long nsecs); void udelay(unsigned long usecs); void mdelay(unsigned long msecs);
在内核中最好不要直接使用mdelay()函数,对于毫秒级以上的时延,内核提供了如下函数 void msleep(unsigned int millisecs); unsigned long msleep_interruptible(unsigned int millisecs); // 可以被打断 void ssleep(unsigned int secdonds);
长延迟 比较当前的jiffies和目标jiffies
睡着延迟 schedule_timeout()可以使当前任务睡眠指定的jiffies之后重新被调度执行。msleep()和msleep_interruptible在本质上都是依靠包含了schedule_timeout()的schedule_timeout_uninterruptible()和schedule_timeout_interruptible()实现的。 |
内存与IO访问
内存空间和IO空间
目前大多数嵌入式微处理器如ARM、POWERPC等中并不提供IO空间,而仅存在内存空间。内存空间可以直接通过地址、指针来访问,程序和程序中使用的变量和其他数据都存在于内存空间中。
内存管理单元MMU
为了理解基本的MMU操作原理,需先明晰几个概念。
TLB:Translation Lookaside Buffer,即转换旁路缓存,TLB是MMU的核心旗舰,它缓存少量的虚拟地址与物理地址的转换关系,是转换表的Cache,因此也经常被称为“快表”。
TTW:Translation Table walk,即转换表漫游,当TLB中没有缓存对应的地址转换关系时,需要通过对内存中转换表的访问来获得虚拟地址和物理地址的对应关系。TTW成功后,结果应写入TLB.
当ARM要访问存储器时,MMU先查找TLB中的虚拟地址表。如果ARM的结构支持分开的数据TLB(DTLB)和指令TLB(ITLB),则除取指令使用ITLB外,其他的都使用DTLB。
若TLB中没有虚拟地址的入口,则转换表遍历硬件从存放于主存储器中的转换表中获取地址转换信息和访问权限,同时将这些信息放入TLB,它或者被放在一个没有使用的入口或者替换一个已经存在的入口。之后,在TLB条目中控制信息的控制下,当访问权限允许时,对真实物理地址的访问就爱那个在Cache或者在内存中发生。
ARM中的TLB条目中的控制信息用于控制对对应地址的高速缓存和写缓冲,并决定是否高速缓存。
(1)C(高速缓存)和B(缓冲)位被用来控制对应地址的高速缓存和写缓冲,并决定是否采用高速缓存。
(2)访问权限和域位用来控制读写访问是否被允许。
Linux内存管理
对于包含MMU的处理器而言,Linux系统提供了复杂的存储管理系统,使得进程所能访问的内存达到4GB。
在linux系统中,进程的4GB内存空间被分为2个部分,用户空间和内核空间。用户空间一般分布在0-3GB,剩下的3-4GB为内核空间。用户进程通常情况下只能访问用户空间的虚拟地址。
每个进程的用户空间都是完全独立、互不相干的,用户进程各有不同的页表。而内核空间是由内核负责映射,它不会跟着进程改变,是固定的。内核空间地址有自己对应的页表,内核的虚拟空间独立于其他程序。
linux中1GB的内核空间又被分为物理内存映射区、虚拟内存分配区、高端页面映射区、专用页面映射区和系统保留映射区。如下:
4GB-----------------
保留区
--------------------
专用页面映射区
--------------------
高端内存映射区
--------------------
--------------------
vmalloc分配器区
--------------------
--------------------
物理内存映射区
3GB-----------------
一般情况下,物理内存映射区最大长度为896M,系统的物理内存被顺序映射到内核空间的这个区域中。当系统物理内存大于896MB时,超过物理内存映射区的那部分内存称为高端内存(未超过的称为常规内存),内核在存取高端内存时必须将他们映射到高端页面映射区。
linux保留内核空间最顶部FIXADDR_TOP---4GB的区域作为保留区。
紧接着最顶端的保留区以下的一段区域为专用页面映射区(FIXADDR_START---FIXADDR_TOP)。它的总尺寸和每一页的用途由fixed_address枚举结构在编译的时候预定义,用__fix_to_virt(index)可获取专用区内预定义页面的逻辑地址。其开始地址和结束地址宏定义如下:
#define FIXADDR_START (FIXADDR_TOP - __FIXADDR_SIZE)
#define FIXADDR_TOP ((unsigned long)__FIXADDR_TOP)
#define __FIXADDR_TOP 0xfffff000
接下来,如果系统配置了高端内存,则位于专用页面映射区之下的就是一段高端内存映射区,其起始地址为PKMAP_BASE,定义如下:
#defined PKMAP_BASE ((FIXADDR_BOOT_START - PAGE_SIZE * (LAST_PKMAP + 1)) & PMD_MASK)
其中涉及到的宏定义如下:
#define LAST_PKMAP PTRS_PER_PTE
#define PTRS_PER_PTE 512
#define PMD_MASK (~(PMD_SIZE - 1))
#define PAGE_SIZE (1UL << PMD_SHIFT)
#define PMD_SHIFT 21
在物理区和高端映射区之间为虚存内存分配区(VMALLOC_START---VMALLOC_END),用于malloc()函数,它的前部与物理内存映射区
有一个隔离带,后部与高端映射区也有一个隔离带。vmalloc区域定义如下:
#define VMALLOC_OFFSET (8*1024*1024)
#define VMALLOC_START (((unsigned long)high_memory \
+ vmalloc_earlyreserve \
+ 2*VMALLOC_OFFSET - 1) \
& ~(VMALLOC_OFFSET - 1))
#ifdef CONFIG_HIGHMEM
#define VMALLOC_END (PKMAP_BASE - 2*PAGE_SIZE)
#else
#define VMALLOC_END (FIXADDR - 2*PAGE_SIZE)
内存存取
用户空间内存动态申请和释放。理想情况下,malloc()和free()应成对出现,即谁申请,就由谁释放。
内核空间内存动态申请
在linux内核空间申请内存涉及到的函数主要包括kmalloc(),__get_free_pages()和vmalloc()等。kmalloc()和__get_free_pages()(及其类似函数)申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系。而vmallox()则是在虚拟内存空间给出的一块连续的内存区,实质上,这片连续的虚拟内存在物理内存上并不一定连续,也没有简单的换算关系。
虚拟地址与物理地址关系
对于内核物理内存映射区的虚拟内存,使用virt_to_phys()可以实现内核虚拟地址转化为物理地址,定义如下:
#define __pa(x) ((unsigned long)x - PAGE_OFFSET)
extern inline unsigned long virt_to_phys(volatile void *address)
{
return __pa(address);
}
上面转换过程中调用的__pa()会将虚拟地址减去PAGE_OFFSET,通常为3GB。
与之对应的函数为phys_to_virt()
#define __va(x) ((unsigned long)x + PAGE_OFFSET)
extern inline unsigned long phys_to_virt(volatile void *address)
{
return __va(address);
}
上述方法仅适用于常规内存。
IO内存静态映射
在将linux移植到目标电路板的过程中,通常会建立外设IO内存物理地址到虚拟地址的静态映射,这个映射通过在电路板对应的
map_desc结构体添加新成员来完成。
struct map_desc
{
unsigned long virtual; // 虚拟地址
unsigned long pfn; // __phys_to_pfn(phy_addr)
unsigned long length; // 大小
unsigned long type; // 类型
};
阅读(1701) | 评论(0) | 转发(0) |