kvm随笔系列一: kvm中的同步控制

来源:互联网 发布:崔雪莉在sm的地位 知乎 编辑:程序博客网 时间:2024/05/16 00:31
Linux 内核提供的管理同步的措施有:
原子操作(automic_t)
内存屏障
自旋锁
信号量
顺序锁
completion
RCU
下面分别看看每种的基本原理以及在KVM中是如何应用的:
(1) automic_t使用特殊cpu指令(x86采用lock)保证指令不会被其它cpu或环境切换打断。
例子(kvm\i8254.c): pit_timer_fn ==> 
if (ps->reinject || !atomic_read(&ps->pending)) {
atomic_inc(&ps->pending);
queue_kthread_work(&pt->worker, &pt->expired);
}
kvm_pit_ack_irq ==> value = atomic_dec_return(&ps->pending);

(2) 内存屏障
在的CPU一般采用流水线来执行指令。一个指令的执行被分成:取指、译码、访存、执行、写回、等若干个阶段。然后,多条指令可以同时存在于流水线中,同时被执行。
指令流水线并行的,多个指令可以同时处于同一个阶段,只要CPU内部相应的处理部件未被占满即可。比如说CPU有一个加法器和一个除法器,那么一条加法指令和一条除法指令就可能同时处于“执行”阶段, 而两条加法指令在“执行”阶段就只能串行工作。
相比于串行+阻塞的方式,流水线像这样并行的工作,效率是非常高的。然而,这样一来,乱序可能就产生了。
  在多处理器下,除了每个处理器要独自面对上面讨论的问题之外,当处理器之间存在交互的时候,同样要面对乱序的问题。
一 个处理器(记为a)对内存的写操作并不是直接就在内存上生效的,而是要先经过自身的cache。另一个处理器(记为b)如果要读取相应内存上的新值,先得 等a的cache同步到内存,然后b的cache再从内存同步这个新值。而如果需要同步的值不止一个的话,就会存在顺序问题
 __loaded_vmcs_clear ==>
crash_disable_local_vmclear(cpu);
list_del(&loaded_vmcs->loaded_vmcss_on_cpu_link);

/*
* we should ensure updating loaded_vmcs->loaded_vmcss_on_cpu_link
* is before setting loaded_vmcs->vcpu to -1 which is done in
* loaded_vmcs_init. Otherwise, other cpu can see vcpu = -1 fist
* then adds the vmcs into percpu list before it is deleted.
*/
smp_wmb();


loaded_vmcs_init(loaded_vmcs);
crash_enable_local_vmclear(cpu);


vmx_vcpu_load==>
local_irq_disable();
crash_disable_local_vmclear(cpu);


/*
* Read loaded_vmcs->cpu should be before fetching
* loaded_vmcs->loaded_vmcss_on_cpu_link.
* See the comments in __loaded_vmcs_clear().
*/
smp_rmb();


list_add(&vmx->loaded_vmcs->loaded_vmcss_on_cpu_link,
&per_cpu(loaded_vmcss_on_cpu, cpu));
crash_enable_local_vmclear(cpu);
local_irq_enable();

(3)自旋锁
自旋锁使用原则:
任何拥有自旋锁的代码,都不能休眠,不能因为任何原因放弃CPU。除了服务中断!
自旋锁会禁止当前CPU上的抢占,即使在单处理器架构上。
拥有锁的时间越短越好!
在中断中使用自旋锁,那么这个自旋锁必须是通过禁止中断形式上锁的。
在软件中断中使用自旋锁,那么这个自旋锁必须是通过关闭软件中断形式上锁的
static void allocate_vpid(struct vcpu_vmx *vmx)
{
int vpid;


vmx->vpid = 0;
if (!enable_vpid)
return;
spin_lock(&vmx_vpid_lock);
vpid = find_first_zero_bit(vmx_vpid_bitmap, VMX_NR_VPIDS);
if (vpid < VMX_NR_VPIDS) {
vmx->vpid = vpid;
__set_bit(vpid, vmx_vpid_bitmap);
}
spin_unlock(&vmx_vpid_lock);
}

(4)信号量
内核信号量类似于自旋锁,因为当锁关闭着时,它不允许内核控制路径继续进行。然而,当内核控制路径试图获取内核信号量锁保护的忙资源时,相应的进程就被挂起。只有在资源被释放时,进程才再次变为可运行。只有可以睡眠的函数才能获取内核信号量;中断处理程序和可延迟函数都不能使用内核信号量
hva_to_pfn ==>
down_read(&current->mm->mmap_sem);
exit:
up_read(&current->mm->mmap_sem);

(5) 顺序锁
适用情况:
1. 当要保护的资源很小,很简单
2. 会被频繁读取访问并且很少发生写入访问
3. 访问会很快
这种情况下就可以使用 seqlock

使用限制:通常,seqlock不能保护包含指针的数据结构。
        kvm中未使用该锁。
使用示例: unsigned int seq;
do {
seq = read_seqbegin(&the_lock);
} while read_seqretry(&the_lock, seq);

(6) Completion
虽然信号量可以用于实现同步,但往往可能会出现一些不好的结果。例如:当进程A分配了一个临时信号量变量,把它初始化为关闭的MUTEX,并把其地址传递给进程B,然后在A之上调用down(),进程A打算一旦被唤醒就撤销给信号量。随后,运行在不同CPU上的进程B在同一个信号量上调用up()。然而,up()down()的目前实现还允许这两个函数在同一个信号量上并发。因此,进程A可以被唤醒并撤销临时信号量,而进程B还在运行up()函数。结果up()可能试图访问一个不存在的数据结构。这样就会出现错误。为了防止发生这种错误就专门设计了completion机制专门用于同步。
 kvm中未使用该机制。
使用说明:
在A函数中,如果需要等待其他的处理,使用void wait_for_completion(struct completion * comp ); 则在这个位置上将处于非中断的sleep,进行等待,也就是相关的线程/进程,用户是无法kill的。 
•在B函数,如果已经处理完,可以交由A函数处理,有下面两种方式 
void complete(struct completion * comp ); 如果要执行A必须等待B先执行,B执行后,A可以继续执行。如果A需要再次执行,则需要确保下一次B执行完。如果连续执行两次B,则可以执行两次A,第三次A要等第三次B执行完。 
void complete_all(struct completion * comp ); 只要B执行完,A就可以执行,无论执行多少次。如果需要再等待B的直系个可以使用INIT_COMPLETION(struct completion * comp ) 。重新初始化completion即可。 
void complete_and_exit(struct completion * comp ,long retval ) ; 这个处理具有complete的功能外,还将调用它的线程/进程终止。可用于一些无限循环的场景,例如受到某个cleaned up的信息后,e通知用户程序终止,允许A函数执行。

(7) RCU
 RCU(Read-Copy Update)是数据同步的一种方式,在当前的Linux内核中发挥着重要的作用。RCU主要针对的数据对象是链表,目的是提高遍历读取数据的效率,为了达到目的使用RCU机制读取数据的时候不对链表进行耗时的加锁操作。这样在同一时间可以有多个线程同时读取该链表,并且允许一个线程对链表进行修改(修改的时候,需要加锁)。RCU适用于需要频繁的读取数据,而相应修改数据并不多的情景。在kvm中大量使用了该方式。
例1:
kvm_irq_delivery_to_apic_fast ==>
rcu_read_lock();
map = rcu_dereference(kvm->arch.apic_map);
        .........//对map指针内容的读操作
       rcu_read_unlock();

recalculate_apic_map ==>
new = kzalloc(sizeof(struct kvm_apic_map), GFP_KERNEL);
.....对new指针内容复制
old = rcu_dereference_protected(kvm->arch.apic_map,
lockdep_is_held(&kvm->arch.apic_map_lock));
rcu_assign_pointer(kvm->arch.apic_map, new);

对apic map的读取较频繁,而更新较少。

例2:
读保护:
idx = srcu_read_lock(&kvm->srcu);
......
srcu_read_unlock(&kvm->srcu, idx);

写:
install_new_memslots==>
static struct kvm_memslots *install_new_memslots(struct kvm *kvm,
struct kvm_memslots *slots, struct kvm_memory_slot *new)
{
struct kvm_memslots *old_memslots = kvm->memslots;

update_memslots(slots, new, kvm->memslots->generation);
rcu_assign_pointer(kvm->memslots, slots);
synchronize_srcu_expedited(&kvm->srcu);

kvm_arch_memslots_updated(kvm);

return old_memslots;
}



0 0
原创粉丝点击