今天看内核发现
disable_preempt
这个函数,觉得挺有意思就看了下网上资料,以下我将之整理成了两个函数来加以理解。
一、
barrier
函数
内存屏障出现因为编译器或现在的处理器常会自作聪明地对指令序列进行一些处理,比如数据缓存,读写指令乱序执行等等。如果优化对象是普通内存,那么一般会提升性能而且不会产生逻辑错误。但如果对
I/O
操作进行类似优化很可能造成致命错误。所以要使用内存屏障,以强制该语句前后的指令以正确的次序完成。其实在指令序列中放一个
wmb
的效果是使得指令执行到该处时,把所有缓存的数据写到该写的地方,同时使得
wmb
前面的写指令一定会在
wmb
的写指令之前执行。
rmb
(读内存屏障)保证了屏障之前的读操作一定会在后来的读操作执行之前完成。
wmb
保证写操作不会乱序,
mb
指令保证了两者都不会。这些函数都是
barrier
函数的超集。
这些函数在已编译的指令流中插入硬件内存屏障;具体的插入方法是平台相关的。
关于
barrier()
宏实际上也是优化屏障:
#define barrier() __asm__ __volatile__(“”: : :”memory”)
CPU
越过内存屏障后,将刷新自己对存储器的缓冲状态。这条语句实际上不生成任何代码,但可使
gcc
在
barrier()
之后刷新寄存器对变量的分配。
1
)
set_mb(),mb(),barrier()
函数追踪到底,就是
__asm__ __volatile__(“”:::”memory”),
而这行代码就是内存屏障。
2
)
__asm__
用于指示编译器在此插入汇编语句
3
)
__volatile__
用于告诉编译器,严禁将此处的汇编语句与其它的语句重组合优化。即:原原本本按原来的样子处理这这里的汇编。
4
)
memory
强制
gcc
编译器假设
RAM
所有内存单元均被汇编指令修改,这样
cpu
中的
registers
和
cache
中已缓存的内存单元中的数据将作废。
cpu
将不得不在需要的时候重新读取内存中的数据。这就阻止了
cpu
又将
registers
,
cache
中的数据用于去优化指令,而避免去访问内存。
5
)
“”:::
表示这是个空指令。
barrier()
不用在此插入一条串行化汇编指令。
6
)
__asm__,__volatile__,memory
在前面已经解释
例
1
:
1 int a = 5, b = 6;
2 barrier();
3 a = b;
在
line 3
,
GCC
不会用存放
b
的寄存器给
a
赋值,而是重新读内存中的
b
值,赋值给
a
。
例
2
:
它在进程上下文中将一个元素插入一个单向链表:
new->next=i->next;
wmb();
i->next=new;
同时,如果不加锁地遍历这个单向链表。或者在遍历链表时已经可以看到
new
,或者
new
还不在该链表中。两个内存写
事件的顺序必须按照程序顺序进行。否则可能
new
的
next
指针将指向一个无效地址,就很可能出现
OOPS!
不论是
gcc
编译器的优化还是处理器本身采用的大量优化,如
Write buffer, Lock-up free, Non- blocking reading, Register allocation, Dynamic scheduling, Multiple issues
等,都可能使得实际执行可能违反程序顺序,因此,引入内存屏障来保证事件的执行次序严格按程序顺序来执行。
使用内存屏障强加的严格的
CPU
内存事件次序,保证程序的执行看上去象是遵循顺序一致性模型。在当前的实现中,
wmb()
实际上是一个空操作,这是因为目前
Intel
的
CPU
系列都遵循“处理机一致性”,所有的写操作是遵循程序顺序的,不会越过前面的读写操作。但是,由于
Intel CPU
系列可能会在将来采用更弱的内存一致性模型并且其他体系结构可能采用其他放松的一致性模型,仍然在内核里必须适当地插入
wmb()
保证内存事件的正确次序。
二、
disable_preempt
函数
先讲下
linux
的调度机制,
linux
下有两种调度方式:
1)
显式调度
,
进程自己因为缺少相应的所申请的资源
,
显式调用调度器
,
让出处理器
,
比如
:
内核申请的信号阻塞了
,
自旋锁锁住了。
2)
隐式调度
,
整个
linux
系统在运行过程中的非显示的调用调度器
,
这又分两种情况:
A)
用户态抢占调度
比如
:
在系统调用
,
中断处理
,
异常处理返回用户态时
,
该进程的时间片已经用完。
B)
内核态抢占调度
比如
:
当前内核态执行过程中事先没有禁止内核态抢占
,
有中断产生时
,
中断处理
又产生了更高级优先进程
,
那么就会直接抢占前面的内核态执行体。
常见的调度点
1)
进程被阻塞时比如申请资源时被阻塞
2)
调整参数时
比如通过
sched_setscheduler() ,nice()
等函数调整进程的调度策略
,
静态优先级时
3)
睡眠进程被唤醒时
比如
wake_up
唤醒等待队列中的进程时
,
如果该进程具有更高优先级则会设置当前
进程
TIF_NEED_RESCHED,
如果允许内核态抢占
,
则会调度一次
,
(
这是由等待队列中的默认的唤醒函数控制的
,
默认的唤醒函数为
:
int default_wake_function(wait_queue_t*,unisgned int mode,int sync,void* key)
EXPORT_SYMBOL(default_wake_function)
因为
EXPORT_SYMBOL
了
default_wake_function,
所以我们可以制作我们自己的唤醒函数
.
4)
中断处理完时
如果中断处理过程中设置了
TIF_NEED_SCHED
标志
,
中断返回时
,
不论是要返回内核态还是用户态
,
都会发生一次抢占
.
当然
,
在这也会检查有没有软中断需要处理
.
5)
执行了
preempt_enable()
函数
(
见前面说明
)
而我们在抢占式内核中,有三处地方需要显示的禁用抢占:
1.
操作
Per-CPU
变量的时候,比如
smp_processor_id()
就是这一类问题,但一个进程被抢占后重新调度,有可能调度到其他的
CPU
上去,这时定义的
Per-CPU
变量就会有问题。下面是一个例子:
struct this_needs_locking tux[NR_CPUS];
tux[smp_processor_id()] = some_value;
/* task is preempted here… */
something = tux[smp_processor_id()];
这里如果没有抢占保护的话
some_value
与
something
可能返回不同的值。当处理
CPU ID
时,可以考虑使用
get_pcu()/put_cpu()
接口,该函数对实现了禁用抢占,取得
CPU ID
,使能抢占的序列。算是
kernel
推荐的使用方法。
2.
必须保护
CPU
的状态。这类问题是体系结构依赖的。例如,在
x86
上,进入和退出
FPU
就是一种临界区,必须在禁抢占的情况下使用。
3.
获得和释放锁必须在一个进程中实现。也就是说一个锁被一个进程持有,也必须在这个进程中释放。
禁用
/
使能抢占的函数主要有:
spin_lock()/spin_unlock()
disable_preempt()/enable_preempt()
(禁止或使能内核抢占)调用下面的
inc_preempt_count()/dec_preempt_count()
,并且加入了
memory barrier
。
inc_preempt_count()/dec_preempt_count()
get_cpu()/put_cpu()
相关数据结构及函数如下
:
struct thread_info
中
{
unisgned int preempt_count;—–(PREEMPT 0-7
位表示内核态禁止抢占计数器
,SOFTIRQ 8-15
表示软中断禁止计数器
,HARDIRQ 16-27
表示中断嵌套的深度
)
}
只要
PREEMPT
为
0
时才允许内核态抢占
.
preempt_disable()————–
主要执行
inc_preempt_count()(
增加
PREEMPT,
从而禁止内核态抢占
)
preempt_enable()————–
主要执行
preempt_enable_no_resched()
和
preempt_check_resched()
preempt_enable_no_resched()
主要执行
dec_preempt_count()
preempt_check_resched()
主要执行
test_thread_flag(TIF_NEED_RESCHED)
(
是否设置了需要调度的标志
)
和
preempt_schedule()(
进行内核态抢占调度
)