"Linux设备驱动开发详解" 笔记

《Linux设备驱动开发详解》第2版 宋宝华 编著

Bought on Dec 1, 2010, Noted on 2015.6

【声明】本文大部分内容摘自《Linux设备驱动开发详解》第2版,或者网上搜索,故不单独注明内容出处

第一篇 Linux设备驱动入门

 

设备的分类

字符设备;块设备;网络设备

其中网络设备不会映射到文件系统中的文件和目录,而是面向数据包的接受和发送

 

处理器分类

CPU的体系结构:冯诺依曼机构和哈弗结构(程序和数据分开存储,包括独立的总线)

从指令集角度分:RISC(ARM,MIPS,PowerPC)和CISC(IA x86)

按应用领域区分:通用处理器(GPP:general-purpose preprocessor),DSP,ASP/ASIC(Application specific integriated circuit)

 

Linux 2.6内核特点

1. 新的调度器,在高负载情况下执行极其出色
2. 内核任务可抢占,提高系统的实时性,使得鼠标和键盘事件得到更快的响应
3. 改进的线程模型,线程操作速度得以提高,可以处理任意数量的线程
4. 虚拟内存,增加r-map(方向映射),显著改善虚拟内存在一定程度负载下的性能
5. 音频。弃用OSS,改用ALSA
 

Linux Kernel's components

SCHED(进程调度)MM(内存管理)VFS(虚拟文件系统)NET(网络接口)IPC(进程间通信)
其中网络接口分为网络协议和网络驱动程序
 

Coding style

TAB 8 characters
if/for one line, without { }
switch and case aligning
for (i = 0; i < 10; i++) {
.......................
}
 

GNU C & ANSI C

GNU 是ANSI C的扩展语法
1. 零长度和变量长度数组
char data[0]; ....... then data[i]=......
int main(int argc, char *argv[])
{
int i, n = argc;
double x[n];
}
2. case的范围
like
switch (ch) {
case '0' ... '9': xxx
case 'a' ... 'f': xxx
}
3. typeof 获取变量的type
e.g. const typeof(x) _x = (x);
4. 可变参数宏
e.g. #define pr_debug(fmt, arg...) \
printk(fmt, ##arg)
其中##是为了处理参数为零的情况,去除掉fmt后面的逗号
5. 标号元素
通过指定索引或者结构体成员名,允许初始化值以任意顺序出现
struct file_operations ext2_file_operations = {
llseek: genernic_file_llseek,
ioctl: ext2_ioctl,
.....
}
6. built-in function in GCC
For example, __builtin_return_address(LEVEL)
Checking out GNU GCC manual

第二篇 Linux设备驱动核心理论

Linux File System

System calls of FS

int create(const char *filename, mode_t mode);
int umask(int newmask);
int open(const char *filename, int flags, mode_t mode);
int read(int fd, const void *buf, size_t length);
int write(int fd, const void *buf, size_t length);
int lseek(int fd, offset_t offset, int whence);
int close(int fd);
 

File operation API of C library

dependence on different OS

FILE *fopen(const char *path, const char *mode);
int fgetc(FILE *stream);
int fputc(int c, FILE *stream);
char *fgets(char *s, int n, FILE *stream);
int fprintf(FILE *stream, const char *format, ... );
int fscanf(FILE *steam, const char *format, ... );
size_t fread(void *ptr, size_t size, size_t n, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t n, FILE *stream);
int fseek(FILE *stream, long offset, int whence);
int fclose(FILE *stream);
 

struct file & inode

file结构体代表一个打开的文件或者设备。
file->f_mode标识文件的读写权限,file->f_flags标识可反映阻塞和非阻塞

inode结构体是linux管理文件系统的最基本单位,记录文件各类属性信息,其中i_rdev字段包含设备编号,包含major and minor number。一般major对应驱动,minor对应该驱动的设备序号
 

sysfs file system

这个VFS提供了包含所有系统硬件的层级视图,展示设备驱动模型各组件的层次关系,大致分三类:bus,devices,class。
三个重要结构体分别描述bus,driver,device: bus_type,device_driver,device.
device and driver's registration is separately. when any one registration, it will try to match another one via match() routine of bus_type();

Linux Character device

Main function of cdev

cdev struct describes character device information.

MAJOR(dev_t dev)
MINOR(dev_t dev)
MKDEV(int major, int minor)

void cdev_init(struct cdev *, struct file_operations *);
struct cdev *cdev_alloc(void);

int register_chrdev_region(dev_t from, unsigned count, const char *name);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

void cdev_put(strcut cdev *p);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev*);
 

procedure of character device initialization

1. application device number
2. registration cdev
 

data transmit

copy_from_user()
copy_to_user()
put_user()
get_user()
 

ioctl command format

Device type + no + direction + size
(Device type is magic number, see more in ioctl-number.txt)

_IO()
_IOR()
_IOW()
_IOWR()

 

private_data


struct file {
....
void *private_data;
}
private_data存储驱动的私有数据,一般在驱动probe时动态开辟内存,以便多个设备共用一个驱动使用;不过随着device——tree的应用,越来越多的驱动数据放在device_tree中。

 

Concurrency & race condition

并发和竞态


critical sections临界资源包含:HW,static/global variables
竞态发生的情况:SMP,多线程,包括可抢占式内核,中断
避免竞态的手段:中断屏蔽,原子操作,自旋锁,信号量
 

中断屏蔽

可以避免新的中断到来,也可以避免内核抢占的发生(进程调度依赖于中断来实现)

local_irq_disable()
local_irq_enable()
local_irq_save(flags)
local_irq_restore(flags)
local_bh_disable() //disable 中断底半部
local_bh_enable()
 

原子操作

For integer operand

atomic_set
atomic_read
atomic_add
atomic_sub
atomic_inc_and_test
atomic_dec_and_test
atomic_sub_and_test
atomic_inc_return
atomic_sub_return
atomic_dec_return
atomic_add_return
 

For bit operand

set_bit
clear_bit
change_bit
test_bit
test_and_set_bit
test_and_clear_bit
test_and_change_bit
 

自旋锁

Spin lock

spinlock_t lock;
spin_lock_init(lock);
spin_lock(lock)
spin_trylock(lock) return immediately even lock failure
spin_unlock(lock)

NOTE: 在自旋锁持有期间,内核抢占被禁止,主要针对SMP和单CPU可抢占内核,但是依然受到中断和底半部的影响。所以自旋锁往往结合中断使能函数一同使用,如下:
spin_lock_irq
spin_unlock_irq
spin_lock_irqsave
spin_unlock_irqrestore
spin_lock_bh
spin_unlock_bh

NOTE: spin lock实际上是忙等,CPU不做任何事情,非常消耗CPU,所以只能用于很短时间的等待,往往用于等待硬件的场景
spin lock可能导致系统死锁,比如递归使用同一个自旋锁,即拿到锁后,没有释放而再次拿锁
spin lock锁定期间,不能调用可能引起进程调度的函数。如果进程获得自旋锁后再阻塞,如调用了copy_from_user(), copy_to_user(),则可能导致内核的崩溃
 

读写自旋锁

rwlock

允许读的并发,禁止写的同时进行

rwlock_init
read_lock
read_lock_ireqsave
read_lock_irq

read_unlock
read_unlock_irqrestore
read_unlock_irq

write_lock
write_lock_irqsave
write_lock_irq
write_trylock

write_unlock
write_unlock_irqrestore
write_unlock_irq


For example,

rwlock_t lock;
rwlock_init(&lock);

read_lock(&lock);
.....
read_unlock(&lock);

write_lock_irqsave(&lock, flags);
....
write_unlock_irqrestore(&lock, flags);
 

循序锁

seqlock

对读写锁的一种优化,读执行单元不会被写执行单元阻塞,写执行单元也不需要等待读执行单元完成读操作后才进行写操作,即读写可以同时操作,只不过如果读的过程中发生了写操作,需要重新读取数据;写执行单元之间是互斥的。

因为顺序锁允许读写同时进行,大大提高了并发性,对于读写同时进行的概率比较小的情况下,性能非常好。

NOTE:顺序锁有个限制,要求被保护的共享资源不含有指针,因为写执行单元可能使得指针失效,但读执行单元如果正要访问该指针,将导致oops

For example,
//write operation
write_seqlock(&seqlock_a);
...
write_sequnlock(&seqlock_a);

//read operation
read_seqbegin(...); //返回顺序锁的当前顺序号
read_seqretry(...); //检查资源是否被复写,如是,则重读

e.g.
do {
   seqnum = read_seqbegin(&seqlock_a);
   /* execute read operations */
   ....
   
} while(read_seqretry(&seqlock_a, seqnum));
 

读-拷贝-更新

RCU(Read-Copy Update)

原理:写操作需要先拷贝一个副本,先对副本进行修改,然后再适当的时机拷贝(update)回原有数据。这个时机就是所有引用该数据的CPU都退出对共享数据的操作的时候。读执行单元没有任何同步开销,而写操作单元的同步开销则取决于使用的写执行单元间同步机制。

RCU可以看做读写锁的高性能版本,既允许多个读单元并发,又允许多个读执行单元和多个写执行单元同时并发。但是对于写比较多的并发情况,写执行单元之间的同步开销也随之加大,必定需要用某种锁机制来同步并行的多个写操作

RCU operation functions

rcu_read_lock()
rcu_read_unlock()
实际上它们只是禁止和使能内核的抢占调度
即,
#define rcu_read_lock()   preempt_disable()
#define rcu_read_unlock()   preempt_enable()

synchronize_rcu()
该函数由RCU写执行单元调用,它将阻塞执行单元,直到所有读执行单元完成。如果有多个CPU调用该函数,那么他们将在一个grace period之后全部被唤醒

synchorize_kernel()
内核代码使用该函数来等待所有CPU处于可抢占状态,目前功能等同于synchroize_rcu(),但现在已经不在使用,而是使用synchroize_sched();该函数用于等待所有的CPU处于可抢占状态,它能保证正在进行的中断处理函数处理完毕,但不能保证正在进行的软中断处理完毕

call_rcu(struct rcu_head *head, void (*fucn)(struct rct_head *rcu));
由写执行单元调用,不会使写执行单元阻塞,因而可以在中断上下文或者软中断使用;该函数把func挂接在RCU回调函数链上,然后立即返回。synchronize_rcu的实现使用了call_rcu函数

RCU的链表版本,略
 

信号量

semaphore

struct semaphore sem;
void sema_init(struct semaphore *sem, int val);
#define init_MUTEX(sem)   sema_init(sem, 1) //初始化互斥信号量
#define init_MUTEX_LOCKED(sem) sema_init(sme, 0)
DECLARE_MUTEX(name)
DECLEAR_MUTEX_LOCKED(name)

#获得信号量
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem); //一旦阻塞进入睡眠可以被信号打断,信号会导致该函数返回,返回值为非0
int down_trylock(struct semaphore *sem); //因为不会导致调用者睡眠,可以用在中断上下文

#释放信号量
void up(struct semaphore *sem);

读写信号量

读写信号量和信号量的关系如同读写自旋锁和自旋锁
读写信号量可能引起进程阻塞,但它允许N个读执行单元同时访问共享资源,最多一个写执行单元。
like
down_read
up_read
down_write
up_write

完成量

completion
strcut completion
init_completion
void wait_for_completion(struct completion *c);
void complete(struct completion *c);
void complete_all(struct completion *c);

 

互斥体

 

struct mutex
mutex_init
mutex_lock
mutex_unlock

 

总结比较

 

信号量是进程级的,用于多个进程间同步,使用信号量的开销是进程上下文切换的开销;因进程上下文切换的开销较大,所以只有当进程占用资源时间较长时,用信号量才是较好的选择;当临界资源访问时间很短,使用自旋锁较好。
信号量可以阻塞即睡眠,自旋锁保护的资源不能够进入睡眠。阻塞意味着进程的切换,一旦进程切换出去,另一个进程企图获取本自旋锁,死锁就会发生
信号量存在于进程上下文,如果共享(临界)资源在中断或者软中断下使用,则只能选择自旋锁或者down_trylock信号量。
 

Block & Unblock

Block: once not match certain condition, sleep to wait
Unblock: if not ready, return to abort
 

Wait queue

等待队列 实现阻塞进程的唤醒
NOTE:信号量的实现也依赖于等待队列

wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
OR DECLARE_WAITQUEUE(name, tsk);

add_wait_queue(...)
remove_wait_queue(...)

wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)

wake_up(wait_queue_head_t *queue)
wakt_up_interruptible(wait_queue_head_t *queue)

sleep_on(wait_queue_head_t *q)
interruptible_sleep_on(wait_queue_head_t *q)
两个函数主要工作:将当前进程的状态设置成TASK_INTERRUPTIBLE,并定义一个等待队列,把它附属到等待队列头q,直到资源获得,q引导的等待队列被唤醒或者进程收到信号

sleep_on <-> wake_up
interruptible_sleep_on <-> wake_up_interruptible

NOTE: 在很多驱动中,并不调用sleep_on或者interruptible_sleep_on,而是亲自进行进程的状态改变和切换,举例说明。

xxx_write(...)
{
do {
    avail = device_writable(...);
    if (avail < 0)
        _ _set_current_state(TASK_INTERRUPTIBLE); //change state of process
    if (avail < 0) {
        if (file->f_flags & O_NONBLOCK)
            return - EAGAIN;
        schedule(); //switch to other process
        if (singal_pending(current)) //if wake up by signal
            return - ERESTARTSYS;
}while(avail < 0);
}

NOTE: 以上代码不完整,并且存在错误,仅仅展示如何改变进程状态和schedule来简单展示如何让当前process进行sleep的,而没有用到sleep_on
 

Polling轮询

当读取非阻塞的设备时,需要应用不断检查设备是否就绪。除了忙等外,即不停的判断,使用select和poll系统调用来查询设备是否就绪

select

int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set exceptfds, struct timeval *timeout)
readfds,writefds,exceptfds分别是被select监视的读,写和异常处理的文件描述符集合

FD_ZERO
FD_SET
FD_CLR
FD_ISSET
 

Poll

unsigned int (*poll) (struct file *filp, struct poll_table *wait);
poll_wait //把当前进程添加到wait参数指定的等待列表(poll_table)中
驱动程序poll应该返回设备资源的可获取状态,即POLLIN,POLLOUT,POLLPRI等

驱动中poll函数的典型模板为:

statci unsigned int xxx_poll(struct file *filp, poll_state *wait)
{
....
poll_wait(filp, &dev->r_wait, wait);
poll_wait(filp, &dev->w_wait, wait);

if (...) /*readble*/
    mask |= POLLIN | POLLRDNORM;
if (...) /*writable*/
    mask |= POLLOUT | POLLWRNORM;
...
return mask;
}

 

非阻塞I/O编程

设置非阻塞I/O的两种方式:open(O_NONBLOCK);fcntl(O_NONBLOCK)

对于慢速I/O的读写设计思想:

如果阻塞式操作,需要创建多线程来读写I/O,但是需要线程同步开销,例如线程A为主线程,负责控制和状态逻辑等操作,线程B专门来负责读写数据,A和B通过buffer和锁机制来同步;

如果非阻塞方式,要么定周期轮询(polling)浪费CPU,要么采用I/O多路,显然后者是最好的。后者可以采用epoll,或者select方式;前者基本很少使用,因为效率低下并且实时性不如epoll/ select

 

异步通知

 

概述

一旦设备就绪,则通过信号主动通知应用程序,类似于硬件上的中断概念

可见除了阻塞I/O,非阻塞I/O+polling以外,还可以用异步通知来实现异步I/O

进程间的信号有几十个,一个信号被捕获意思是当信号到达时有相应的代码处理它,如果没有被这个进程所捕获,内核将采用默认行为处理。注意:SIGKILL和SIGSTOP两个信号不能被捕获或者忽略
 

信号的接收

应用程序中,为了捕获信号,可以使用singal函数来设置相应信号的处理函数
typedef void (*sighandler_t) (int);
sighandler_t signal(int signum, sighandler_t handler);
handler为处理函数,
若为SIG_IGN表示忽略该信号;
若为SIG_DFL表示采用系统默认方式处理;
另一个改变进程接收特定信号后行为的函数为sigaction函数

ctrl+c send SIGINT
kill send SIGTERM

通常用户空间处理一个设备释放的信号需要完成三个工作
1. F_SETOWN IO控制命令设置设备文件的拥有者为本进程,这样设备驱动发出的信号才能被本进程接收到
e.g. fcntl(fd, F_SETOWN, getpid())
2. F_SETFL IO控制命令设置设备文件支持FASYNC,即异步通知模式
e.g. fcntl(fd, F_GETFL)
3. signal函数连接信号和信号处理函数
 

信号的释放

设备驱动中释放一个信号需要的三项工作
1. 支持F_SETOWN命令,设置filp->f_owner为对应进程ID,有内核完成,驱动无需关注
2. 支持F_SETFL命令的处理,当FASYNC标志改变时,驱动程序的fasync函数将得以执行。所以需要驱动实现fasync函数
3. 在设备资源ready时,调用kill_fasync()函数激发相应的信号

设备驱动中异步通知编程涉及的主要数据结构和函数
struct fasync_struct
fasync_helper //处理FASYNC标志变更的
kill_fasync //释放信号用的函数
 

Linux2.6异步I/O

处理同步I/O以外,POSIX的异步I/O(AIO)的基本思想是允许进程发起很多I/O操作,而不用阻塞或者等待任何操作完成,稍后再接收I/O操作完成的通知时,在检索I/O操作的结果

select提供的功能(异步阻塞I/O)与AIO类似,它对通知事件进行阻塞,而不是对I/O调用进行阻塞。JC:这里注意一下,I/O调用依然是非阻塞(即非阻塞I/O)的,但是select相当于通知事件进行拦截和封装,呈献给应用程序的,或者说是在应用程序视角看来,select是异步阻塞I/O操作。

对于AIO来说,同时存在多个对设备的操作,用aiocb结构体来区分各个操作,以便标识I/O通知针对的操作项目,主要函数如下:
aio_read
aio_write
aio_error
aio_return //只有在aio_error调用确定请求已经完成后,再调用aio_return
aio_suspend //挂起进程,直到异步请求完成为止
aio_cancel
lio_listio //在一个系统调用内,启动大量的I/O操作,发起多个传输
以上都是用户空间如何利用AIO
 

AIO机制中内核通知用户空间的方式

1. 信号;
2. 回调函数
应用程序提供一个回调函数给内核,以便AIO的请求完成后内核调用这个函数。
其中回调函数最终是请求了一个线程回调函数,具体请参考page 190

总结下,块设备和网络设备本身是异步的,字符设备必须明确表明支持AIO,一般字符设备无需支持AIO。

 

中断

概述

中断分为顶半部(top half)和底半部(bottom half)

top half的工作主要是清除中断标记,挂载bottom half执行。

top half往往设计为不可中断,bottom half则正好相反

 

中断共享
多个设备共用一根中短线的情况,在申请中断时使用IRQF_SHARED标志;每个中断例程都快速判断是否本设备中断。

中断分类

内部中断(软中断,溢出,除法错误等)和外部中断(外设)
可屏蔽中断和不可屏蔽中断
向量中断和非向量中断(前者由硬件提供ISR入口地址;后者有软件根据中断标志选择ISR入口)

 

中断详解 - 摘录中断的知识

本节内容 Quote from: http://blog.csdn.net/droidphone/article/details/7445825
特别感谢:http://blog.csdn.net/droidphone

中断控制器

所有中断达到CPU之前,都会经过中断控制器汇集,符合要求的中断请求才能通知CPU

中断控制器的工作:对irq的优先级进行控制,提供给CPU中断源(irq编号),使能(enable)或者屏蔽(mask)irq,清除中断请求(ack),这里注意,ack和enable和mask的区别。有些中断还需要CPU在处理完irq后对控制器发出eoi指令(end of interrupt),在smp系统中,控制各个irq与cpu之间的亲和性(affinity)

中断控制器的软件抽象为irq_chip,其中的一堆函数操作正是中断控制器所能做的事情

 

中断子系统框架

#硬件封装层

中断控制器被封装起来,形成了中断子系统的硬件封装层

linux 中断向量表在arch/arm/kernel/entry-armv.S中定义

vector_stub irq, IRQ_MODE, 4 //这一句把宏展开后实际上就是定义了vector_irq,根据进入中断前的cpu模式,分别跳转到__irq_usr或__irq_svc。

vector_stub dabt, ABT_MODE, 8  // 这一句把宏展开后实际上就是定义了vector_dabt,根据进入中断前的cpu模式,分别跳转到__dabt_usr或__dabt_svc。

 

#中断流控层

电平触发中断(level type)
边缘触发中断(edge type)
简易的中断(simple type)

fast eoi type(针对需要回应eoi(end of interrupt)的中断控制器)
per cpu type(smp)
以上不同类型的中断被抽象出来,成为了中断子系统的流控层



流控层细节:
进入C代码的第一个函数是asm_do_IRQ,在ARM体系中,这个函数只是简单地调用handle_IRQ,对应代码如下:
arch/arm/kernel/entry-armv.S include “arch/arm/include/asm/entry-macro-multi.S”
在entry-macro-multi.S中,bne     asm_do_IRQ。
其中asm_do_IRQ最终调用了generic_handle_irq。

以上是启动时的流程,不过对于任何一个中断的处理流程也是这样的。CPU一旦响应IRQ中断后,ARM会自动把CPSR中的I位置位,表明禁止新的IRQ请求,直到中断控制转到相应的流控层后才通过local_irq_enable()打开。这里说的流控层处理就是指generic_handle_irq被调用,最终调用到irq注册的流控层处理回调中,如下:
    static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)  
    {  
        desc->handle_irq(irq, desc);  
    }  

通用中断子系统把几种常用的流控类型进行了抽象,并为它们实现了相应的标准函数,我们只要选择相应的函数,赋值给irq所对应的irq_desc结构的handle_irq字段中即可。

这些标准的回调函数都是irq_flow_handler_t类型:
    typedef void (*irq_flow_handler_t)(unsigned int irq,  
                            struct irq_desc *desc); 
 

#通用逻辑层

中断通用逻辑层通过标准的封装接口(实际上就是struct irq_chip定义的接口)访问并控制中断控制器的行为。

本层将作为硬件封装层和中断流控层以及驱动程序API层之间的桥梁,驱动程序和板级代码可以通过以下几个API设置irq的流控函数,实际上就是初始化irq_chip.

    irq_set_handler();
    irq_set_chip_and_handler();
    irq_set_chip_and_handler_name();

 

目前的通用中断子系统实现了以下这些标准流控回调函数,这些函数都定义在:kernel/irq/chip.c中,
    handle_simple_irq  用于简易流控处理;
    handle_level_irq  用于电平触发中断的流控处理;
    handle_edge_irq  用于边沿触发中断的流控处理;
     handle_fasteoi_irq  用于需要响应eoi的中断控制器;
    handle_percpu_irq  用于只在单一cpu响应的中断;
    handle_nested_irq  用于处理使用线程的嵌套中断;

#驱动程序API

该部分向驱动程序提供了一系列的API,用于向系统申请/释放中断,打开/关闭中断,设置中断类型和中断唤醒系统的特性等操作。其中的一些API如下:

申请和释放中断
request_irq
free_irq

使能和屏蔽中断
disable_irq
disable_irq_nosync(同disable_irq区别在于,它部等待当前中断处理完成便立即返回)
enable_irq

屏蔽/恢复本CPU内所有的中断
local_irq_save
local_irq_disable
local_irq_restore
local_irq_enable

    irq_set_affinity(); // 亲和性
    request_threaded_irq();  //JC:先找到这个irq对应的irq_desc,然后把这个irq和对应的action注册到irq_desc中去
其中,
    irq是要申请的IRQ编号,
    handler是中断处理服务函数,该函数工作在中断上下文中,如果不需要,可以传入NULL,但是不可以和thread_fn同时为NULL;
    thread_fn是中断线程的回调函数,工作在内核进程上下文中,如果不需要,可以传入NULL,但是不可以和handler同时为NULL;
    
    irqflags是该中断的一些标志,可以指定该中断的电气类型,是否共享等信息;
    devname指定该中断的名称;
    dev_id用于共享中断时的cookie data,通常用于区分共享中断具体由哪个设备发起;

NOTE:beside request_threaded_irq, there is request _any_context_irq, request_percpu_irq.....

IRQ的描述方式

基于结构体struct irq_desc,其组织形式有数组方式和基数树(radix tree)动态分配方式。(后者宏开关:CONFIG_SPARSE_IRQ,一般都用动态分配的方式radix tree)

系统中每一个注册的中断源,都会分配一个唯一的编号用于识别该中断,我们称之为IRQ编号。
例如arch/mach-xxx/include/irqs.h

IRQ系统的启动和工作流程

IRQ系统的初始化
(1) start_kernel -> setup_arch -> early_trap_init //完成中断向量拷贝和重定位工作(armv8-64和armv7可能都不太一样,要具体看代码

位于arch/arm/kernel/traps.c中的early_trap_init()被调用,其中两个memcpy会把__vectors_start开始的代码拷贝到0xffff0000处,把__stubs_start开始的代码拷贝到0xFFFF0000+0x200处,这样,异常中断到来时,CPU就可以正确地跳转到相应中断向量入口并执行他们。

(2) start_kernel -> early_irq_init -> alloc_desc //完成于硬件平台无关的代码,开辟内存给irq_desc
(3) start_kernel -> init_IRQ //完成中断控制器的初始化,为每个irq_desc安装flow_handler and irq_chip,当然后面可以通过irq_set_chip_and_handler来安装

 

    系统启动阶段,取决于内核的配置,内核会通过数组或基数树分配好足够多的irq_desc结构;
    根据不同的体系结构,初始化中断相关的硬件,尤其是中断控制器;
    为每个必要irq的irq_desc结构填充默认的字段,例如irq编号,irq_chip指针,根据不同的中断类型配置流控handler;
    设备驱动程序在初始化阶段,利用request_threaded_irq() api申请中断服务,两个重要的参数是handler和thread_fn;
    当设备触发一个中断后,cpu会进入事先设定好的中断入口,它属于底层体系相关的代码,它通过中断控制器获得irq编号,在对irq_data结构中的某些字段进行处理后,会将控制权传递到中断流控层(通过irq_desc->handle_irq);
    中断流控处理代码在作出必要的流控处理后,通过irq_desc->action链表,取出驱动程序申请中断时注册的handler和thread_fn,根据它们的赋值情况,或者只是调用handler回调,或者启动一个线程执行thread_fn,又或者两者都执行;
    至此,中断最终由驱动程序进行了响应和处理。

 

NOTE: 每个irq都会属于某一个irq_desc, 简单地说,逻辑关系是这样的,
最顶层是一系列的irq_desc,每个irq_desc都有irq_data,对应的irq_chip(中断控制器的一堆操作指针函数),
irq_desc里面还有个action list,其中每个action是一个特定irq的处理方式,上面包含handler和thread_fn

中断响应过程的整个流程:
H/W INT -> asm_do_IRQ() ->handle_IRQ
handle_IRQ() <1>-> [soft-irq]irq_enter
handle_IRQ() <2>-> [flow-handle流控层]generic_handle_irq ->action->handle -> irq_wake_thread() [desc->thread_fn]
handle_IRQ() <3>-> [soft-irq]irq_exit -> invoke_softirq & __do_softirq
at last, in irq-thread, run desc->thread_fn()
NOTE: 特别注意:irq_desc的action结构体list,其中包含handler & thread_fn

IRQ调试接口

在/proc目录下面,有两个与中断子系统相关的文件和子目录,它们是:

    /proc/interrupts:文件
    /proc/irq:子目录

读取interrupts会依次显示irq编号,每个cpu对该irq的处理次数,中断控制器的名字,irq的名字,以及驱动程序注册该irq时使用的名字

/proc/irq目录下面会为每个注册的irq创建一个以irq编号为名字的子目录,每个子目录下分别有以下条目:

  • smp_affinity            irq和cpu之间的亲缘绑定关系;
  • smp_affinity_hint   只读条目,用于用户空间做irq平衡只用;
  • spurious                  可以获得该irq被处理和未被处理的次数的统计信息;
  • handler_name       驱动程序注册该irq时传入的处理程序的名字;

 

中断线程化(Interrupt Threads)

只要产生中断事件,内核将立即执行相应的中断处理程序,等到所有挂起的中断和软中断处理完毕后才能执行正常的任务,因此有可能造成实时任务得不到及时的处理。中断线程化之后,中断将作为内核线程运行而且被赋予不同的实时优先级,实时任务可以有比中断线程更高的优先级。这样,具有最高优先级的实时任务就能得到优先处理,即使在严重负载下仍有实时性保证。

 

MISC

以下是判断在中断上下文还是进程上下文的标志

#definein_irq()                 (hardirq_count())  //硬中断上下文的判断
#definein_softirq()          (softirq_count())  //软中断上下文的判断

由上面的mark可以searching 代码从而看出真正在哪里设置的mark

 

软中断也属于中断上下文,不能调用可以睡眠的函数,内核不允许,会panic

 

如果普通软中断在执行完前,就是没有local_bh_enable之前,又来了硬中断,要来设相同的softirq的pending标志位,这是硬中断能否设置成功?如果设置成功,那么软中断不知道在它执行过程中,硬中断到底多少次设置pending bit位(插一句,看tasklet是个list,就可以避免这个问题)

 

底半部机制

tasklet,work queue,softirq
实际上解决的问题,或者说要达到的目的是延时操作

tasklet

Tasklet概述

在硬中断isr中调用tasklet_schedule(t),并且只会注册一个tasklet实例(类似单例),所以不必考虑SMP的并行问题。这样就避免了使用softirq所要考虑的互斥的问题。如果tasklet在执行tasklet->func()前,被多次调度,即调用tasklet_schedule,它只能推后执行

Tasklet 实现细节:

tasklet实现基于软中断HI_SOFTIRQ和TASKLET_SOFTIRQ,实际上他就是软中断的两种类型,只不过可以给kernel或者driver来使用的通用软中断,而其他软中断类型都是专用的,比如定时器,net rx和net tx

Linux内核采用两个PER_CPU的数组tasklet_vec[]和tasklet_hi_vec[]维护系统种的所有tasklet(kernel/softirq.c),分别维护TASKLET_SOFTIRQ级别和HI_SOFTIRQ级别的tasklet。

 

在start_kernel中,调用softirq_init来初始化softirq,其中核心代码就下面两行:

    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);

解释下tasklet_action回调函数和大致工作:

 

关闭本地中断的前提下,移出当前cpu的待处理tasklet链表到一个临时链表后,清除当前cpu的tasklet链表,之所以这样处理,是为了处理当前tasklet链表的时候,允许新的tasklet被调度进待处理链表中。

遍历临时链表,用tasklet_trylock判断当前tasklet是否已经在其他cpu上运行,而且tasklet没有被禁止:

  • 如果没有运行,也没有禁止,则清除TASKLET_STATE_SCHED状态位,执行tasklet的回调函数。
  • 如果已经在运行,或者被禁止,则把该tasklet重新添加会当前cpu的待处理tasklet链表上,然后触发TASKLET_SOFTIRQ软中断,等待下一次软中断时再次执行。
  • tasklet有以下几个特征:
  • 同一个tasklet只能同时在一个cpu上执行,但不同的tasklet可以同时在不同的cpu上执行;
  • 一旦tasklet_schedule被调用,内核会保证tasklet一定会在某个cpu上执行一次;
  • 如果tasklet_schedule被调用时,tasklet不是出于正在执行状态,则它只会执行一次;
  • 如果tasklet_schedule被调用时,tasklet已经正在执行,则它会在稍后被调度再次被执行;
  • 两个tasklet之间如果有资源冲突,应该要用自旋锁进行同步保护;

tasklet的API有:

tasklet_init()
tasklet_schedule()和tasklet_hi_schedule()
tasklet_disable_nosync()、tasklet_disable()、task_enable()
tasklet_action()和tasklet_hi_action() #具体执行函数
tasklet_kill

work queue

struct work_struct my_wq;
void my_wq_func(unsigned long);
INIT_WORK(&my_wq, (void (*)(void *)) my_wq_func, NULL);
schedule_work(&my_wq);
 

softirq

软件中断(softIRQ)是内核提供的一种延迟执行机制,它完全由软件触发,虽然说是延迟机制,实际上,在大多数情况下,它与普通进程相比,能得到更快的响应时间。

 

open_softirq 注册软中断对应的处理函数
raise_softirq函数触发一个软中断

其中softirq和tasklet运行于softirq上下文,属于"原子"上下文的一种,而工作队列运行于进程上下文。因此软中断和tasklet处理函数不能睡眠,而工作队列允许睡眠。

local_bh_disable() and local_bh_enable() 是内核中用于禁止和使能软中断和tasklet底半部机制的函数

多个软中断可以同时在多个cpu运行,就算是同一种软中断,也有可能同时在多个cpu上运行。内核为每个cpu都管理着一个待决软中断变量(pending),它就是irq_cpustat_t:



### softirq macro-definition

There're ten types of software interrupt defined in interrupt.h like HI_SOFTIRQ, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, ... SCHED_SOFT_IRQ ...

### softirq interrupt vector table
h = softirq_vec;
static struct softirq_action softirq_vec[NR_SOFTIRQS] = ...
NR_SOFTIRQS is also defined in interrupt.h

### softirq exectution core function and when wakeup softirqd
irq_exit() finally executes __do_softirq().
In __do_softirq(), kernel executes each softirq one by one as priority, if threshold of max round (MAX_SOFTIRQ_RESTART) reach, but softirq is also set because of some hardware interrupt comming to set softirq again, then we do softirq in softirqd in process context rather than INT context. This can let process could be scheduled.

### softirq avoid race
because __local_bh_disable_ip is executed during softirq; that's to say only one softirq routine could be running in same CPU core simultaneously

### instance of schedule() in ksoftirqd
Because it will execute for long time sometimes, so it call schedule() by itself. 中文解释一下:大多数情况下,软中断都会在irq_exit阶段被执行,在irq_exit阶段没有处理完的软中断才有可能会在守护进程中执行。

 

基于上面所说,软中断的执行既可以守护进程中执行,也可以在中断的退出阶段执行。实际上,软中断更多的是在中断的退出阶段执行(irq_exit),以便达到更快的响应,加入守护进程机制,只是担心一旦有大量的软中断等待执行,会使得内核过长地留在中断上下文中。

 

软中断有32位标记,每一位对应一种类型的软中断,硬中断ISR会设置其中某一个bit位而后触发软中断调度,软中断循环并依次判断每个bit位,若被置则执行相应的软中断程序,直到所有bit位都为空。在软中断执行期间,进程不会被调度执行,除非softirqd的触发门限到了,才会强制切换到进程上下文中

 

对于驱动程序的开发者来说,无需实现自己的软中断。但是,对于某些情况下,我们不希望一些操作直接在中断的handler中执行,但是又希望在稍后的时间里得到快速地处理,这就需要使用tasklet机制。

 

比较tasklet和softirq

tasklet较之于softirq,tasklet不需要考虑SMP下的并行问题,而又比workqueues有着更好的性能

tasklet和softirq区别(摘录:http://blog.csdn.net/wuxinyicomeon/article/details/5996695)

软中断支持SMP,同一个softirq可以在不同的CPU上同时运行,softirq必须是可重入的。软中断是在编译期间静态分配的,它不像tasklet那样能被动态的注册或去除。软中断不能进入硬中断部分,且同一个CPU上软中断的执行是串行的,即不允许嵌套。因此,do_softirq()函数一开始就检查当前CPU是否已经正出在中断服务中,如果是则 do_softirq()函数立即返回。这是由do_softirq()函数中的 if (in_interrupt()) return; 保证的。

 

引入tasklet,最主要的是考虑支持SMP,提高SMP多个cpu的利用率;不同的tasklet可以在不同的cpu上运行。tasklet可以理解为softirq的派生,所以它的调度时机和软中断一样。对于内核中需要延迟执行的多数任务都可以用tasklet来完成,由于同类tasklet本身已经进行了同步保护,所以使用tasklet比软中断要简单的多,而且效率也不错。tasklet把任务延迟到安全时间执行的一种方式,在中断期间运行,即使被调度多次,tasklet也只运行一次,不过tasklet可以在SMP系统上和其他不同的tasklet并行运行。在SMP系统上,tasklet还被确保在第一个调度它的CPU上运行,因为这样可以提供更好的高速缓存行为,从而提高性能。
与一般的软中断不同,某一段tasklet代码在某个时刻只能在一个CPU上运行,但不同的tasklet代码在同一时刻可以在多个CPU上并发地执行。Kernel/softirq.c中用tasklet_trylock()宏试图对当前要执行的tasklet(由指针t所指向)进行加锁,如果加锁成功(当前没有任何其他CPU正在执行这个tasklet),则用原子读函数atomic_read()进一步判断count成员的值。如果count为0,说明这个tasklet是允许执行的。如果tasklet_trylock()宏加锁不成功,或者因为当前tasklet的count值非0而不允许执行时,我们必须将这个tasklet重新放回到当前CPU的tasklet队列中,以留待这个CPU下次服务软中断向量TASKLET_SOFTIRQ时再执行。为此进行这样几步操作:(1)先关 CPU中断,以保证下面操作的原子性。(2)把这个tasklet重新放回到当前CPU的tasklet队列的首部;(3)调用__cpu_raise_softirq()函数在当前CPU上再触发一次软中断请求TASKLET_SOFTIRQ;(4)开中断。

软中断和tasklet都是运行在中断上下文中,它们与任一进程无关,没有支持的进程完成重新调度。所以软中断和tasklet不能睡眠、不能阻塞,它们的代码中不能含有导致睡眠的动作,如减少信号量、从用户空间拷贝数据或手工分配内存等。也正是由于它们运行在中断上下文中,所以它们在同一个CPU上的执行是串行的,这样就不利于实时多媒体任务的优先处理。


那么,什么情况下使用工作队列,什么情况下使用tasklet。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择tasklet。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。

总结硬中断,软中断和信号

硬中断是外部设备对CPU的中断,软中断是中断底半部的一种处理机制,而信号则是内核或者其他进程对某个进程的中断。注意:系统调用通过软中断(例如ARM为swi)陷入内核,此时软中断的概念是指由软件指令引发的中断,和我们这里说的softirq是两个不同的概念。


 

Timer

内核定时器

硬件RTC提供两种中断:1. tick定周期的时钟信号;2. alarm
软件定时器是基于硬件RTC来实现的,实现原理这里不做介绍
定时器的API
struct timer_list
void init_timer(struct timer_list * timer);
DECLARE_INITIALIZER(_function, _expires, _data)
DEFINE_TIMER(_name, _function, _expires, _data)
setup_timer
add_timer
del_timer
mod_timer

HZ为1s对应的tick数目,比如200
 

delayed_work

本质利用工作队列和定时器实现
schedule_delayed_work
cancel_delayed_work
 

内核延时

短延时

ndelay,udelay,mdelay 空转CPU
msleep和ssleep不能被打断
msleep_interruptiable
 

长延时

比较当前的jiffies和目标jiffies,直到未来的jiffies达到目标的jiffies。
time_before, time_after
 

带休眠的延迟

schedule_timeout() 使当前任务睡眠指定的jiffies之后重新被调度
msleep本质上包含了schedule_timeout

 

MMU

基本概念

MMU的作用:提供VA和PA的映射,内存访问权限保护和Cache缓存控制

Linux内核使用了三级页表 PGD,PMD and PTE

TLB Translation Lookaside Buffer  (cache 缓存va和pa的转换关系,俗称“快表”)

TTW Translation Table walk (转换表漫游,即通过多级页表的访问来找到va和pa的对应关系,TTW成功后,结果写入TLB)

注意:代码页和数据页往往是分开的,即数据TLB(DTLB)和指令TLB(ITLB)

"Kernel Memory Layout on ARM Linux" in kernel/Documentation/arm/memory.txt

Write-through VS Write-back/behind
Write-through: Write is done synchronously both to the cache and to the backing store.
Write-back (or Write-behind): Writing is done only to the cache. A modified cacheblock is written back to the store, just before it is replaced.

常用数值:2^12 = 4K = 0x1000; 0xC000_0000 = 3 * 1024 * 1024 * 1024

PAGE_OFFSET is different from PAGE_SHIFT. The former is alignment between kernel and user space, the latter is page size.

PAGE_SHIFT is 12 (2^12 =  4K page size)

PAGE_OFFSET is 0xC000_0000

 

#definePUD_SHIFT       PGDIR_SHIFT

#definePMD_SHIFT               21

#definePGDIR_SHIFT             21

 

pgd(Stage-2 page table) address:10-bit

pte(page table)address: 10-bit

 

274 enum zone_type {

 

294         ZONE_DMA,

309        ZONE_NORMAL,

319        ZONE_HIGHMEM,

321        ZONE_MOVABLE,

322        __MAX_NR_ZONES

323 };

 

虚拟地址和物理地址的关系

对于物理内存直接映射区的虚拟内存,使用virt_to_phys()来实现VA到PA的转化,对于ARM而言,virt_to_phys的定义如下:

#define __virt_to_phys(x)    (((phys_addr_t)(x) - PAGE_OFFSET + PHYS_OFFSET))

 

 

 

内存管理的各个区域

内核将每个内存区域作为一个单独的内存对象管理,相应的操作也都一致。

vm_area_struct是描述进程地址空间的基本管理单元,进程的虚拟地址空间由多个内存区域来描述,并分别由链表和红黑树组织起来,前者用于遍历所有节点,后者用于快速搜索

 

匿名映射,没有映射到文件的都可以叫匿名映射
匿名映射应该对应的就是匿名页面
相对应的是文件页面,即映射到文件结点或者真实文件的页面(mmap)
 
将物理地址与线性地址建立起对应关系。这种映射是由put_page(物理页地址,线性地址)来完成的,该函数通过参数所给的线性地址算出对应的页目录项和页表项,然后将物理页地址存入页表项,页表项地址存入目录项,以完成从线性地址到物理页地址的映射。
Linux 2.6 引入了基于对象的反向映射机制。这种方法也是为物理页面设置一个用于反向映射的链表,但是链表上的节点并不是引用了该物理页面的所有页表项,而是相应的虚拟内存区域(vm_area_struct结构),虚拟内存区域通过内存描述符(mm_struct结构)找到页全局目录,从而找到相应的页表项。相对于前一种方法来说,用于表示虚拟内存区域的描述符比用于表示页面的描述符要少得多,所以遍历后边这种反向映射链表所消耗的时间也会少很多。

 

每个用户进程的虚拟地址空间是独立的,页表也是独立的;

内核的地址空间也是独立和固定的

 

kernel地址空间由高到低依次划分为如下几个区域

系统保留内存为最顶部:FIXADDR_TOP ~ 4GB

专用页面映射区:FIXADDR_START ~ FIXADDRTOP (编译时预定义的)

高端内存映射区:起始地址为PKMAP_BASE

虚拟内存分配区(vmalloc):VMALLOC_START ~ VMALLOC_END

物理内存直接映射区:最大长度为896MB,大于896MB的物理内存都称之为高端内存

 

内存调试手段

/proc/meminfo
/proc/slabinfo

 

内核空间的内存申请方式

buddy system VS slab allocator

buddy system and slab allocator都是内存分配的算法,前者用于以页为单位分配较大内存,后者用于小块内存的情况

 

kmalloc, __get_free_pages and vmalloc.

the two former is used to allocate memory in physical memory map area and continuous. VA and PA just offset.

va vmalloc allocated is continuous but pa is maybe not continuous.

 

kmalloc通过调用__get_free_pages实现,如果调用kmalloc使用GFP_KERNEL则表示若暂时无法分配内存,则睡眠,所以中断上下文(包括软中断,tasklet,内和定时器)和自旋锁期间不能调用kmalloc with GFP_KERNEL flag,应该用GFP_ATOMIC flag来申请kmalloc,当时候GFP_ATOMIC分配时,若不满足条件直接返回。

__get_free_pages是linux最底层的用于获取空闲内存的方法,以page的2次幂的伙伴算法来管理和分配内存,所以用page为单位来分配

get_zeroed_page()  // clear page

__get_free_page()  //allocate one page

__get_free_pages(unsigned int flags, unsigned int order) ; //allocate 2 ^ order pages

以上函数最终调用了alloc_pages(), ......

以上函数的flag参数常用的是GFP_KERNEL and GFP_ATOMIC,区别就是阻塞和非阻塞。

 

vmalloc

一般用于较大的顺序缓冲区分配内存,开销远大于__get_free_pages(), 为了完成vmalloc,新的页表被建立

void *vmalloc(unsigned long size);

void vfree(void *addr);

vmalloc不能用于原子上下文中,其内部实现使用了标志为GFP_KERNEL的kmalloc()。

 

slab与内存池

(实际上kmalloc是使用slab机制来实现的)

slab多用于分配较小的内存并且重复内存的情况,比如task_struct等。

创建slab缓存:kmem_cache_create

分配slab缓存:kmem_cache_alloc

释放slab缓存:kmem_cache_free

回收slab缓存:kmem_cache_destroy

 

/proc/slabinfo

 

slab实现依然依赖于__get_free_pages(), 只不过申请页面后,再分隔这些页为更小的单元并进行管理,从而节省了内存,提高slab缓冲对象的访问效率。

 

除了slab以外,kernel还包含对内存池的支持。内存池技术是非常经典的用于分配大量小对象的后备缓存技术。

 

kernel内存池

创建内存池:mempool_create

分配和回收对象:mempool_alloc; mempool_free

回收内存池:mempool_destroy

 

SPRD ThreadX的内存池设计思想,这种思想也是普遍采用的

byte pool and block pool.

byte pool以字节为单位分配,可以连续分配任意大小的内存

block pool以多种预先设定的固定大小为单位分配,避免了碎片,但是也降低了灵活性

 

设备IO端口和I/O内存

 

通常来说,控制设备的寄存器如果位于I/O空间时,称为I/O端口;位于内存空间时,对应的内存空间为I/O内存

(ARM而言,不存在I/O端口的概念)

对于I/O端口是通过特殊的读取指令来访问的;

对于I/O内存,首先使用ioremap函数将设备所处的物理地址映射到虚拟地址空间。ioremap也需要建立新的页表。释放用ioumap。

I/O内存除了通过指针直接读写以外,还可以用下面的函数完成

ioread8,ioread16,ioread32

readb,readw,readl

等等

 

申请和释放设备的I/O端口和I/O内存

I/O端口申请:requeset_region, release_region

I/O内存申请:request_mem_region, release_mem_region

I/O内存访问流程:request_mem_region -> ioremap -> ioread8 ..... -> ioumap -> release_mem_region

 

设备地址映射到用户空间 mmap、munmap

mmap可以使用户空间直接访问设备的物理地址,它将用户空间的一段内存和设备内存进行关联,当用户访问用户空间的这段地址范围时,实际上会转化为对设备的访问。

这种映射以page为单位,系统调用mmap会最终调用到驱动中的mmap函数。

mmap大致的流程如下:

在进程的虚拟空间中找到一块VMA,将其进行映射;

如果设备驱动程序或者文件系统的file_operations定义了mmap操作,则调用它;

将这个VMA插入进程的VMA链表中

驱动的mmap函数会建立新的页表,并填充VMA结构体vm_operations_struct指针。VMA即vm_area_struct,用于描述一个虚拟内存区域。

 

创建页表有两个方式:nopage() and remap_pfn_range(),后者一般用于设备内存映射,前者还可以用于RAM映射,nopage掉用于缺页异常时。

 

DMA

DMA和cache一致性问题。cache是CPU内部的,针对内存的缓存。

DMA是内存和外设之间的高速传输通道。当DMA涉及的内存有被cached的部分,那么CPU不知道这部分cache已经和内存上的数据不一致了(脏了)。

往往看似非常离奇的想象,很可能是cache导致的

解决DMA导致的cache一致性问题的方法是禁止DMA目标地址范围内的内存的cache功能。

DMA的操作接口(略)

 

#表初始化

start_kernel -> setup_arch ->early_paging_init; sanity_check_meminfo; paging_init;

 

sanity_check_meminfo

检测判断是否需要创建highmem区,并且重建描述内存bank的数组. VMALLOC_MIN定义了vmalloc区的起始位置(通过VMALLOC_END和vmalloc_reserve计算得出),VMALLOC_END定义了vmalloc区的结束位置,vmalloc_reserve是系统预留给vmalloc区的大小。

 

paging_init

build_mem_type_table这个函数根据CPU类型,设置mem_types全局数组,mem_types数组保存了页目录和页表的属性,将来创建页目录和页表时,会用到mem_types。

 

prepare_page_table这个函数会请空页目录,有两块地址空间区域是不需要清除的,一个是kernelimage,另外一个是vmalloc+ 永久内核映射以及固定映射线性地址。 

其中memblock.memory.regions[0].base,memblock.memory.regions[0].size记录lowmem的物理地址地址和大小

 

map_lowmem建立低端内存的所有页目录和页表:遍历memorybank,映射那些没有highmem标记的内存bank

arch/arm/kernel/vmlinux.lds.S_stext

 

devicemaps_init这个函数创建device的映射,

1. 把machine vectors映射到0xffff0000处

2. 调用平台特定的map_io,对于mx51,这个函数主要是映射mx51功能寄存器区, AIPS1 AIPS2 和SPBA0,这三个寄存器区大小为1MB,映射后的虚拟地址分别为0xF7E00000,0xF7D00000,0xFB100000

 

kmap_init永久内核映射和固定映射的线性区域;创建pkmap的pgd和pte。并且让pkmap_page_table指向这个PTE page的linux p/t。一般来说kmap都是使用一个page的pte来映射高端内存到内核地址空间,对于arm来说,每个page可以存放512个pte_t,所以pkmap的地址空间为2M。

 

bootmem_init :

 

1. 调用check_initrd获取initrd所在的memory bank对应的node

 

2. 对每一个节点:

 

获取该node的 min(最小pfn), node_low(最大low memory pfn), node_hight(最大high memory pfn)

调用bootmem_init_node初始化node,bootmem_init_node会初始化bootmem bitmap

 

如果是node 0,那么调用reserve_node_zero为node 0 reserve的内存:内核text和data区,初始化页表区(16KB),以及swapper_pg_dir之前的

一块内存(在我的机器上是4个page)

如果initrd存放在当前的node上,那么调用bootmem_reserve_initrd保留initrd占用的内存。initrd_start是initrd在起始虚拟内存地址,

initrd_end是initrd结束的虚拟内存地址。

3. 对每一个node 调用bootmem_free_node

 

设置这个node内各个zone的大小

调用free_area_init_node:计算node的总pages数目,为这个node分配mem map,注意node内所有zone的memmap都分配在一起

调用free_area_init_core:对于node内的每一个zone,进行初始化。注意这个函数present_pages是total size减去了该分区对应的memmap占用

的pages,但是实际上memmap是放在node的开始位置,这里似乎不应该减去这个值

 

4.high_memory 是一个很奇怪的变量,high_memory应该是物理内存的概念,但是high_memory变量保存的确实一个内核地址。

 

Linux设备驱动

设备驱动模型:总线,设备和驱动

linux虚拟总线:platform总线,platform device and platform driver,比如I2C,RTC,LCD等控制器归纳为platform device

其中platform bus的数据结构为bus_type, 其对应的实例为plaftform_bus_type

 

第三篇 Linux设备驱动实例

 

第四篇 Linux设备驱动调试,移植

 

 

 

 

 

你可能感兴趣的:(_legacy)