0%

linux内核同步机制总结

0x00 前言

学习内核,遇到最多的问题就是条件竞争,然后,再复杂一点的条件竞争就要利用到内核的同步机制,之前一直很迷,现在,总结一下。

0x01 条件竞争

对于用户态的程序一般不会有条件竞争的情况出现,因为,进程和进程之间总是独立的,都有自己独立的空间,用户态的程序在计算机系统层面一般扮演的是客户端的角色,而内核扮演的是服务端的角色,内核提供着多种服务,每一个用户态的进程都可以请求某一种的服务,而每一种服务,不可避免的需要声明一些全局变量来维持服务的正常运行,如果多个进程同时对某一全局变量进行直接或者间接的操作,就可能损坏全局变量,轻则服务崩溃,重则导致非预期程序流,内核提供了多种同步机制来解决条件竞争的问题

0x02 同步机制

主要介绍以下几种内核同步机制:

  • 原子操作
  • 自旋锁
  • RCU
  • 读写锁

0x00 原子操作

具体用法参考linux源码 linux-master/Documentation/atomic_t.txt 的详细描述

具体实现在 linux-master/arch/architecture/include/asm/atomic.h** architecture为具体的架构,如x86

Documentation这个目录真香,之前就知道这么里面有内核的描述,没想到这么详细

我们一般对一个变量操作分这么几步:

  • 在内存中取出变量放到寄存器
  • 操作变量
  • 写回内存

在这三步中间,中断的话,就可能出问题,所以,原子操作就是解决这种问题,保证完成之前这些步骤不被中断,(形象点就是说把这三个步骤封装成一个步骤,原子操作不可再分

列出常用的函数,还有一些没写。。。。

atomic_read(atomic_t *v) 读取原子变量的值

atomic_set(atomic_t *v, int i) 将v设置为i

atomic_add(int i, atomic_t *v) 将i加到v

atomic_add_return(int i, atomic_t *v) 将i加到v,并返回结果

atomic_sub(int i, atomic_t *v) 从v减去i

atomic_sub_return(int i, atomic_t *v) 从v减去i,并返回结果

atomic_sub_and_test(int i, atomic_t v) 从v减去v。如果结果为0则返回true,否则返回false atomic_inc(atomic_t v) 将v加1

atomic_inc_and_test(atomic_t *v) 将v加1。如果结果为0则返回true,否则返回false

atomic_dec(atomic_t *v) 从v减去1

atomic_dec_and_test(atomic_t v) 从v减去1。如果结果为0则返回true,否则返回false atomic_add_negative(int i, atomic_t v) 将i加到v。如果结果小于0则返回true,否则返回false atomic_add_negative(int i, atomic_t *v) 将i加到v。如果结果为负则返回true,否则返回false

0x01 自旋锁

0x00 临界区

说到锁,就不得不提 临界区 这个概念,就是说规划一段代码区域,在这段代码区域内,在同一时间内只允许一个进程运行这段代码(可能不太准确,大概是这个意思),其他进程会以各种方式阻挡在临界区之外,我个人理解临界区只是一个抽象化的概念,实际的实现,就是各种锁的机制,如果你细节的去想内核的各种同步机制,其实他们都是实现了临界区这个概念,包括原子操作,从而实现,在同一时间只有一个进程去操作同一变量

0x01 自旋锁的思想

每个进程访问临界区时都要获取自旋锁,自旋锁只能由一个进程持有,当进程离开临界区的时候,就要释放自旋锁,如果,获得不到锁,此进程就会一直进行空操作

具体参考:linux-master/Documentation/spinlocks.rst

0x02 RCU(Read-Copy Update)

这种机制的思想是可以有多个读者,但是,更新数据的时候,需要先复制一个副本,在副本上完成修改,在所有 进行读访问的使用者结束对旧副本的读取之后,指针可以替换为指向新的、修改后副本的指针,我们可以发现这种机制有写滞后效果,单单靠这一种同步机制,很难来避免条件竞争,有的时候反而给漏洞利用提供便利,由于这种同步机制在某些情况下优异的性能,这种机制在内核用的很广泛,这种机制主要用于对共享资源的访问在大部分时间应该是只读的,写访问应该相对很少的情况,接下来,主要介绍一下用法

rcu_read_lock() 函数标志着临界区的开始,rcu_read_unlock() 函数标志着临界区的结束,在临界区内不允许中断、睡眠,禁止和启用抢占,在临界区内不能直接访问一个指针,需要用到 rcu_dereference(ptr) 来获取一个受保护指针,修改一个受保护的指针也是不能直接修改的需要 rcu_assign_pointer(ptr,new_ptr) 函数来修改,那么什么时候写回到原来的指针呢,内核提供了两个函数

  • synchronize_rcu() 等待所有现存的读访问完成,在该函数返回之后,释放原指针 ,更新指针为新指针
  • call_rcu() 可用于注册一个函数,在所有针对共享资源的读访问完成之后调用,执行释放和更新的操作

0x03 读写锁

读写锁的理念是一个进程写的时候,其他进程不能读写,一个进程读的时候,允许其他进程读,但是不允许写 具体实现是在进入临界区的时候,会加相应的锁(加锁的那一行代码标志临界区的开始。。。),离开临界区时,会释放相应的锁(释放锁的那一行代码标志临界区的结束),但是理想很美好,现实很骨感,如果一个内核驱动,两处引用了同一全局变量,一处有读写锁,另一处没有,同样会引起条件竞争,只是,减少了发生的概率

具体参考:linux-master/Documentation/spinlocks.rst

0x03 总结

本来打算好好写写,但是,这周事情有点多,想写的东西有点大,弄巧成拙,写着写着发现没有什么新意(而且还发现写不完2333),只能作为一种总结,如有错误,还请联系我

参考资料: