超详细的2440中断机制分析!

一直在看2440的中断处理部分,不懂的实在太多了,百度到这篇文章,实在有听君一席话,胜养十年猪的感觉啊,下面上文章:

中断向量

b HandlerIRQ ;handler for IRQ interrupt

很自然,因为所有的单片机都是那样,中断向量一般放在开头,用过单片机的人都会很熟悉,那就不多说了。

异常服务程序

这里不用中断(interrupt)而用异常(exception),毕竟中断只是异常的一种情况,

下面主要分析的是“中断异常”说白了,就是我们平时单片机里面用的中断!!!所有由器件引起的中断,例如TIMER中断,UART中断,外部中断等等,都有一个统一的入口,那就是中断异常 IRQ ! 然后从IRQ的服务函数里面分辨出,当前究竟是什么中断,再跳转到相应的中断服务程序。这样看来,ARM比单片机要复杂一些了,不过原理是不变的。

上面说的就是思路,跟着这个思路来接着分析。

HandlerIRQ 很明显是一个标号,我们找到了

HandlerIRQ HANDLER HandleIRQ

这里是一个宏定义,我们再找到这个宏,看他是怎么定义的:

MACRO

$HandlerLabel HANDLER $HandleLabel

$HandlerLabel

sub sp,sp,#4 ;decrement sp(to store jump address)

stmfd sp!,{r0} ;PUSH the work register to stack(lr does not push because it return to original

address)

ldr r0,=$HandleLabel ;load the address of HandleXXX to r0

ldr r0,[r0] ;load the contents(service routine start address) of HandleXXX

str r0,[sp,#4] ;store the contents(ISR) of HandleXXX to stack

ldmfd sp!,{r0,pc} ;POP the work register and pc(jump to ISR)

MEND

用 HandlerIRQ 将这个宏展开之后得到的结果实际是这样的

HandlerIRQ

sub sp,sp,#4 ;decrement sp(to store jump address)

stmfd sp!,{r0} ;PUSH the work register to stack(lr does not push because it return to original

address)

ldr r0,=HandleIRQ ;load the address of HandleXXX to r0

ldr r0,[r0] ;load the contents(service routine start address) of HandleXXX

str r0,[sp,#4] ;store the contents(ISR) of HandleXXX to stack

ldmfd sp!,{r0,pc} ;POP the work register and pc(jump to ISR)

至于具体的跳转原理下面再说,

好了,这样的话就容易看的多了,很明显, HandlerIRQ 还是一个标号,IRQ异常向量就是跳转到这里执行的,这里粗略看一下,应该是保存现场,然后跳转到真正的处理函数,那么很容易发现了这么一句 ldr r0,=HandleIRQ ,没错,我们又找到了一个标号 HandleIRQ ,看来真正的处理函数应该是这个 HandleIRQ ,继续寻找

AREA RamData, DATA, READWRITE

^ _ISR_STARTADDRESS ; _ISR_STARTADDRESS=0x33FF_FF00

HandleReset # 4

HandleUndef # 4

HandleSWI # 4

HandlePabort # 4

HandleDabort # 4

HandleReserved # 4

HandleIRQ # 4

最后我们发现在这里找到了 HandleIRQ ,^ 其实就是 MAP ,这段程序的意思是,从 _ISR_STARTADDRESS 开始,预留一个变量,每个变量一个标号,预留的空间为 4个字节,也就是 32BIT,其实这里放的是真正的C写的处理函数的地址,说白了,就是函数指针 - - 这样做的话就很灵活了

接着,我们需要安装IRQ处理句柄,说白了,就是设置处理函数的地址,让PC指针可以正确的跳转。

于是我们在接着的找到安装句柄的语句

; Setup IRQ handler

ldr r0,=HandleIRQ ;This routine is needed

ldr r1,=IsrIRQ ;if there is not 'subs pc,lr,#4' at 0x18, 0x1c

str r1,[r0]

说白了就是将 IsrIRQ 的地址填到 HandleIRQ对应的地址里面,前面说了 HandleIRQ 放的是中断处理的函数的入口地址,我们继续找 IsrIRQ

IsrIRQ

sub sp,sp,#4 ;reserved for PC

stmfd sp!,{r8-r9}

ldr r9,=INTOFFSET

ldr r9,[r9]

ldr r8,=HandleEINT0

add r8,r8,r9,lsl #2

ldr r8,[r8]

str r8,[sp,#8]

ldmfd sp!,{r8-r9,pc}

要理解这个代码,得先学学2440的中断系统了,INTOFFSET存放的是当前中断的偏移号,根据偏移就知道当前是哪个中断源发生的中断。

注意了,我们说的是中断,而不是异常,看看原来的表是啥样子的

^ _ISR_STARTADDRESS ; _ISR_STARTADDRESS=0x33FF_FF00

HandleReset # 4

HandleUndef # 4

HandleSWI # 4

HandlePabort # 4

HandleDabort # 4

HandleReserved # 4

HandleIRQ # 4

HandleFIQ # 4

HandleEINT0 # 4

HandleEINT1 # 4

HandleEINT2 # 4

HandleEINT3 # 4

.......

可以看到,前面几个是异常,从 HandleEINT0 就是 IRQ异常的向量存放的地方了,这样就可以理解为什么上面 IsrIRQ 里面里面要执行那条指令

ldr r8,=HandleEINT0

add r8,r8,r9,lsl #2

道理很简单, HandleEINT0 就是所有IRQ中断向量表的入口,在这个地址上面,加上一个适当的偏移量,INTOFFSET ,那么我们知道现在,到底是哪个IRQ在申请中断了。

至于具体怎么跳转的?

首先,我们说了,HandleEINT0 开始的一段内存里面,存放的就是中断服务函数的函数指针,ARM的体系的话,每个指针变量就是占4个字节,这里就解释了,为什么这里为每个标号分配了4个字节的空间,里面放的就是函数指针!!!下面再看看怎么跳转,继续看 IsrIRQ 里面就实现了跳转了

str r8,[sp,#8]

ldmfd sp!,{r8-r9,pc}

其实最核心就是这两句了,先查找到当前中断服务程序的地址,将他放到 R8 里面,然后出栈,弹出给PC那么PC很自然就跳到中断服务程序了。至于这里的堆栈问题又是一个非常棘手的,需要好好的参透ARM的中断架构,需要了解的可以自己仔细的阅读 《ARM体系结构与编程》里面说的很详细。我们这里的重点是研究怎么跳转。

最后,我们看看在C代码中是怎么安装终端向量的,例如看 按键的外部中断,是怎么具体设置的,参看/src/keyscan.c 里面的代码很简单,里面只有3个函数

KeyScan_Test 是按键测试的主函数

Key_ISR 是按键中断服务函数

在 KeyScan_Test里面,我们发现了有这么一句

pISR_EINT0 = pISR_EINT2 = pISR_EINT8_23 = (U32)Key_ISR;

可以理解否? Key_ISR就是上面提到的按键中断服务函数,函数的名字,代表的就是函数的地址!!!!

将中断服务函数的地址,注意了,是地址,这是一个 U32型的变量。送到几个变量,我们以pISR_EINT0 作为例子,查看头文件定义,在 2440addr.h 里面找到

// Interrupt vector

#define pISR_EINT0 (*(unsigned *)(_ISR_STARTADDRESS+0x20))

_ISR_STARTADDRESS有没有似曾相识的感觉?没错,刚才分析的汇编代码里面就提到了

^ _ISR_STARTADDRESS ; _ISR_STARTADDRESS=0x33FF_FF00

HandleReset # 4

HandleUndef # 4

......

对,地址就是这里,然后 _ISR_STARTADDRESS+0x20 就是跳过前面的异常向量,进入IRQ中断向量的入口,所以说到尾

pISR_EINT0 = (U32)Key_ISR;

完成的操作就是,将 Key_ISR 的地址存放到

HandleEINT0 # 4

这个IRQ向量表里面!!!!

当按键中断发生的时候,发生IRQ异常中断当前PC值-4 保存到LR_IRQ里面,然后执行

b HandlerIRQ

然后是执行

HandlerIRQ

sub sp,sp,#4 ; 预留一个用来存放PC地址

stmfd sp!,{r0} ; 保存R0,因为下面使用了

ldr r0,=HandleIRQ ; 将HandleIRQ(服务程序)的地址装载到R0

ldr r0,[r0]

str r0,[sp,#4] ; 保存到刚才预留的地方

ldmfd sp!,{r0,pc} ; 弹出堆栈,恢复R0,并且将刚才计算好的 HandleIRQ 地址弹出到 PC堆栈是向下生长的,所以 SUB SP,SP,#4 就相当于 PUSH XX,但是这个XX这个时候并没有用,因为这里用的是强制移动 SP 指针实现的。然后得到服务程序的地址,再将这个值放回刚才预留的栈的空位上面,最后就是POP出R0恢复,并且将刚才得到的服务程序的地址送到 PC,那么实现的效果就是跳转到 HandleIRQ 里面了。

接着看刚才是怎么安装的HandleIRQ 的

; Setup IRQ handler

ldr r0,=HandleIRQ ;This routine is needed

ldr r1,=IsrIRQ ;if there is not 'subs pc,lr,#4' at 0x18, 0x1c

str r1,[r0]

可以看出,这里将 IsrIRQ 的地址的值保存到 HandleIRQ 中,也就是说,上面的 IRQ 服务程序,这个时候实际上就是指 IsrIRQ !

所以接着的事情就是转移到 IsrIRQ 中执行:

IsrIRQ

sub sp,sp,#4 ; 预留一个值来保存PC

stmfd sp!,{r8-r9}

ldr r9,=INTOFFSET ; 计算偏移量,下面解释

ldr r9,[r9]

ldr r8,=HandleEINT0

add r8,r8,r9,lsl #2

ldr r8,[r8]

str r8,[sp,#8] ; 因为保存了2个寄存器R8 R9 ,所以SP下移了8位

ldmfd sp!,{r8-r9,pc} ; 恢复寄存器,弹出到PC,同上面的一样怎么保存,操作SP,跟最后弹出到PC的部分和上面的例子一样,下面说说中间的计算部分计算偏移量,其实原理很简单,首先 INTOFFSET 保存着当前是哪个IRQ中断,例如 0代表着 HandleEINT0,1代表HandleEINT1 ..... 等等,这不是乱来,有一个表的,这个是由 S3C2440 的datasheet说的,自己可以去查看。

然后得到 中断处理函数的向量表,这个表的首地址就是 HandleEINT0,那么很自然的想到,怎么查表?那还不简单?HandleEINT0 + INTOFFSET 不就完了?基地址加偏移量就得到表中某项了,当然,因为这里是中断处理向量

每一项占用4个字节,所以用lsl #2处理一下,左移2位相当于乘以4,偏移量乘以4,这应该很好理解的。

我们这个例子找到的就是 HandleEINT0 ,将里面的值读出来,里面放的是 HandleEINT0 服务函数的地址,这个地址怎么来的?是在C程序里面设置的。我们看 keyscan.c 程序,找到一个 void KeyScan_Test(void) 函数,

里面有这么一句:

pISR_EINT0 = pISR_EINT2 = pISR_EINT8_23 = (U32)Key_ISR;

这里是安装了3个按键中断服务程序,我们只关注 0号中断,也就是

pISR_EINT0 = (U32)Key_ISR;

这句话什么意思?先看看pISR_EINT0的定义,在 2440addr.h 中定义

#define pISR_EINT0 (*(unsigned *)(_ISR_STARTADDRESS+0x20))

看到没有?_ISR_STARTADDRESS 不就是刚才说的那个异常向量的入口地址?加上一个 0x20 之后实际上指向的,就是 HandleEINT0 !!!这么说来,上面的意思就是,将 Key_ISR 处理函数的入口地址,送到 HandleEINT0 中。

再来看 Key_ISR ,这是一个典型的服务程序,加了_irq 作为编译关键字,告诉编译器,这个函数是中断服务程序

得保存需要的寄存器,免得被破坏。具体可以参考 《ARM体系结构与编程》P283 页的描述。

static void __irq Key_ISR(void)

{

.......

}

加上 _irq 关键字之后,编译器就会处理好所有的保存动作了,并不需要多关心。但是这个是 ARM-CC 编译器的关键字,GCC中并没有这个东西,所以GCC处理中断的时候最好还是自己保存一下。

到这里为止,整个中断的过程就解释完毕。分析的过程中确实学习了很多。

<think>好的,我现在需要详细分析Java中常见锁机制的底层实现。首先,用户之前已经询问过AQS,所以我应该以AQS为基础展开,但这次要更深入,涵盖更多类型的锁,并详细讲解它们的实现细节。 首先,我需要回顾Java中的锁类型,比如ReentrantLock、ReentrantReadWriteLock、StampedLock、Synchronized关键字等。然后,针对每一种锁,分析它们是如何利用AQS或者其他机制实现的。 ReentrantLock是基于AQS的独占锁,分为公平和非公平模式。需要详细说明这两种模式在获取锁时的区别,比如非公平锁在尝试获取锁时直接CAS,而公平锁会先检查队列是否有等待线程。同时,要解释可重入性的实现,即通过AQS的state字段记录持有次数,以及如何释放。 接下来是ReentrantReadWriteLock,它使用AQS的共享和独占模式来实现读写分离。这里需要拆解读写锁的状态划分,高16位表示读锁,低16位表示写锁,以及如何通过CAS维护这些状态。此外,读锁的共享获取和写锁的独占获取机制,以及可能出现的锁降级情况。 StampedLock是JDK8引入的,它并不是基于AQS,而是使用类似CAS和自旋的方式,结合状态变量和队列管理。需要分析其乐观读、悲观读、写锁的实现,以及如何通过戳记(stamp)来验证数据的一致性。这里可能涉及到CLH队列的变种或者不同的同步机制。 Synchronized关键字底层依赖于对象头和监视器锁(Monitor),需要说明偏向锁、轻量级锁、重量级锁的升级过程,以及它们与操作系统互斥量(Mutex)的关系。这部分可能涉及JVM源码中的ObjectMonitor结构,以及不同锁状态在对象头中的编码方式。 此外,还需要提到其他锁机制,如Condition条件变量是如何基于AQS的ConditionObject实现的,Semaphore、CountDownLatch等同步器的底层AQS结构。 在分析过程中,需要注意术语的准确性,比如CLH队列、CAS操作、自旋优化、锁膨胀等。同时,要结合具体的代码示例或伪代码,帮助用户理解。例如,ReentrantLock的非公平锁实现中,lock()方法如何直接尝试CAS获取,失败后进入acquire方法。 还需要考虑不同锁的性能特点和适用场景,比如自旋锁在短等待时的优势,重量级锁在竞争激烈时的适应性。同时,可能存在的死锁问题及如何通过锁机制本身或工具(如线程转储)来排查。 最后,要确保内容结构清晰,分点详述每个锁的实现,并比较它们的异同。可能需要使用图表或状态转换图来辅助说明,但在纯文本回答中需要用文字描述清楚。 在写作过程中,要避免过于技术化的术语堆砌,尽量用通俗的语言解释复杂概念,同时保持专业性。需要检查每个知识点的准确性,比如AQS的tryAcquire方法在子类中的具体实现差异,以及不同锁如何利用这些模板方法。 总结来说,我需要系统性地拆解每个常见锁的底层实现,结合AQS、对象头、CAS等机制,详细分析其工作原理、代码结构、状态管理以及优缺点,确保用户能够全面理解Java锁机制的内部运作。</think>### Java中常见锁机制的底层实现分析 Java锁机制是并发编程的核心,不同锁的实现方式直接影响性能和适用场景。以下是主要锁机制的底层实现原理分析: --- #### **1. `synchronized`关键字** **底层实现**:依赖JVM的对象头与Monitor机制。 ##### **对象头结构** - **Mark Word**(32/64位):存储哈希码、GC分代年龄、**锁状态标志**。 - 锁状态包括:无锁、偏向锁、轻量级锁、重量级锁。 - **Monitor(监视器锁)**:每个对象关联一个`ObjectMonitor`(C++实现),包含: - `_owner`:持有锁的线程。 - `_EntryList`:阻塞等待锁的线程队列。 - `_WaitSet`:调用`wait()`后进入等待状态的线程队列。 ##### **锁升级过程** 1. **偏向锁**(Biased Locking): - **场景**:单线程无竞争。 - **实现**:通过CAS将Mark Word中的线程ID设置为当前线程。 - **优势**:仅一次CAS,无后续同步开销。 2. **轻量级锁**(Lightweight Locking): - **场景**:多线程竞争,但线程交替执行。 - **实现**: - 线程栈中创建`Lock Record`,复制对象头的Mark Word。 - 通过CAS将Mark Word指向`Lock Record`,成功则获取锁。 - 失败则膨胀为重量级锁。 - **优势**:避免线程阻塞,通过自旋尝试获取锁。 3. **重量级锁**: - **场景**:高竞争或长时间等待。 - **实现**:依赖操作系统互斥量(Mutex)和条件变量,线程进入阻塞状态。 - **缺点**:涉及用户态到内核态切换,开销大。 ##### **锁膨胀流程** ``` 无锁 → 偏向锁 → 轻量级锁 → 重量级锁 ``` --- #### **2. `ReentrantLock`** **底层实现**:基于AQS(AbstractQueuedSynchronizer)。 ##### **核心组件** - **AQS的`state`字段**:表示锁的持有次数(可重入)。 - **CLH队列**:管理等待线程的FIFO队列。 ##### **非公平锁实现** ```java final void lock() { if (compareAndSetState(0, 1)) // 直接尝试CAS获取锁 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); // 进入AQS排队逻辑 } protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // 可重入 setState(c + acquires); return true; } return false; } ``` ##### **公平锁 vs 非公平锁** - **公平锁**:`hasQueuedPredecessors()`检查队列是否有等待线程。 - **非公平锁**:允许插队,直接尝试CAS获取锁。 --- #### **3. `ReentrantReadWriteLock`** **底层实现**:AQS的共享模式(读锁) + 独占模式(写锁)。 ##### **状态分割** - **高16位**:读锁持有次数(`sharedCount`)。 - **低16位**:写锁重入次数(`exclusiveCount`)。 ##### **写锁获取** ```java protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c); // 取低16位 if (c != 0) { if (w == 0 || current != getExclusiveOwnerThread()) return false; // 存在读锁或其他线程持有写锁 if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); setState(c + acquires); // 写锁重入 return true; } if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; setExclusiveOwnerThread(current); return true; } ``` ##### **读锁获取** ```java protected final int tryAcquireShared(int unused) { for (;;) { int c = getState(); if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; // 写锁被其他线程持有 int r = sharedCount(c); // 取高16位 if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { // 更新读锁计数 return 1; } } } ``` ##### **锁降级** - **场景**:持有写锁 → 获取读锁 → 释放写锁。 - **目的**:保证数据可见性,防止其他写线程修改数据。 --- #### **4. `StampedLock`(JDK8+)** **底层实现**:基于CLH变种队列 + CAS操作,非AQS实现。 ##### **核心状态** - **`state`字段**:分为模式位和版本号。 - 写锁:最低8位表示写锁状态。 - 读锁:高24位表示读锁数量。 ##### **锁类型** 1. **写锁(独占)**: - 通过CAS设置`state`的低8位。 - 若失败,进入队列等待。 2. **悲观读锁(共享)**: - 检查是否有写锁,无则增加读计数。 3. **乐观读**: - 不修改`state`,仅获取版本戳(stamp)。 - 后续通过`validate(stamp)`检查是否发生写操作。 ##### **示例** ```java public double read() { long stamp = stampedLock.tryOptimisticRead(); // 乐观读 double currentX = x, currentY = y; if (!stampedLock.validate(stamp)) { // 检查是否被修改 stamp = stampedLock.readLock(); // 升级为悲观读 try { currentX = x; currentY = y; } finally { stampedLock.unlockRead(stamp); } } return Math.sqrt(currentX * currentX + currentY * currentY); } ``` --- #### **5. `Semaphore`与`CountDownLatch`** **底层实现**:基于AQS的共享模式。 ##### **`Semaphore`** - **`state`字段**:表示可用许可数量。 - **获取许可**:`acquire()`减少`state`,若不足则阻塞。 - **释放许可**:`release()`增加`state`,唤醒等待线程。 ##### **`CountDownLatch`** - **`state`字段**:表示剩余计数。 - **`await()`**:阻塞直到`state`为0。 - **`countDown()`**:递减`state`,触发唤醒。 --- ### **锁机制对比与适用场景** | 锁类型 | 实现机制 | 适用场景 | 特点 | |----------------------|----------------------|-----------------------------------|-------------------------------| | `synchronized` | JVM Monitor | 简单同步、低竞争环境 | 自动锁升级,无需手动释放 | | `ReentrantLock` | AQS独占模式 | 需要公平性、超时、可中断的同步 | 灵活控制,需手动释放 | | `ReentrantReadWriteLock` | AQS共享+独占 | 读多写少场景 | 读写分离,避免写饥饿 | | `StampedLock` | 自定义CLH队列+CAS | 极高读吞吐量,乐观读优化 | 无重入,可能导致线程饥饿 | | `Semaphore` | AQS共享模式 | 限流、资源池管理 | 控制并发线程数 | --- ### **底层优化技术** 1. **CAS(Compare-And-Swap)**: - 通过硬件指令(如x86的`CMPXCHG`)实现无锁更新。 - 关键方法:`Unsafe.compareAndSwapInt()`。 2. **自旋锁(Spin Lock)**: - 轻量级锁中线程通过循环尝试获取锁,避免直接阻塞。 - 适用场景:锁持有时间极短。 3. **锁消除(Lock Elision)**: - JIT编译器检测不可能存在竞争的锁(如局部对象),直接移除同步代码。 4. **锁粗化(Lock Coarsening)**: - 合并相邻的同步块,减少锁获取/释放次数。 --- ### **总结** Java锁机制的底层实现围绕**线程竞争管理**和**资源状态同步**展开。从JVM内置的`synchronized`到基于AQS的显式锁,再到`StampedLock`的创新设计,不同锁机制在性能、灵活性和复杂度之间权衡。理解其实现原理有助于合理选择锁类型,优化高并发场景下的程序性能。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值