linux内核之内核同步(一):内核同步基础

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_34258344/article/details/100592236

前言

1、内核同步缘起何处?

提到内核同步,这还要从操作系统的发展说起。操作系统在进程未出现之前,只是单任务在单处理器cpu上运行,只是系统资源利用率低,并不存在进程同步的问题。后来,随着操作系统的发展,多进程多任务的出现,系统资源利用率大幅度提升,但面临的问题就是进程之间抢夺资源,导致系统紊乱。因此,进程们需要通过进程通信一起坐下来聊一聊了进程同步的问题了,在linux系统中内核同步由此诞生。
在这里插入图片描述
实际上,内核同步的问题还是相对较复杂的,有人说,既然同步问题那么复杂,我们为什么还要去解决同步问题,简简单单不要并发不就好了吗?凡事都有两面性,我们要想获得更短的等待时间,就必须要去处理复杂的同步问题,而并发给我们带来的好处已经足够吸引我们去处理很复杂的同步问题。先提两个概念:
临界资源:

各进程采取互斥的方式,实现共享的资源称作临界资源。属于临界资源的硬件有打印机、磁带机等,软件有消息缓冲队列、变量、数组、缓冲区等。 诸进程间应采取互斥方式,实现对这种资源的共享。

临界区:

每个进程中访问临界资源的那段代码称为临界区。显然,若能保证诸进程互斥地进入自己的临界区,便可实现诸进程对临界资源的互斥访问。为此,每个进程在进入临界区之前,应先对欲访问的临界资源进行检查,看它是否正被访问。如果此刻该临界资源未被访问,进程便可进入临界区对该资源进行访问,并设置它正被访问的标志;如果此刻该临界资源正被某进程访问,则本进程不能进入临界区。

说到此处,内核同步实际上就是进程间通过一系列同步机制,并发执行程序,不但提高了资源利用率和系统吞吐量,而且进程之间不会随意抢占资源造成系统紊乱。
在这里插入图片描述
为了防止并发程序对我们的数据造成破坏,我们可以通过锁来保护数据,同时还要避免死锁。这里给出一些简单的规则来避免死锁的发生:

  • 注意加锁的顺序
  • 防止发生饥饿
  • 不要重复请求同一个锁
  • 设计锁力求简单

我们知道了可以用锁来保护我们的数据,但我们更需要知道,哪些数据容易被竞争,需要被保护,这就要求我们能够辨认出需要共享的数据和相应的临界区。实际上,我们需要在编写代码之前就设计好锁,所以我们需要知道内核中造成并发的原因,以便更好的识别出需要保护的数据和临界区。内核中造成并发的原因:

  • 中断
  • 内核抢占
  • 睡眠
  • 对称多处理

2、如何实现内核同步

为了避免并发,防止竞争,内核提供了一些方法来实现对内核共享数据的保护。下面将介绍内核中的原子操作、自旋锁和信号量三种同步措施。

(1)原子操作

原子操作可以保证指令以原子的方式被执行,执行过程不会被打断。Linux内核提供了一个专门的atomic_t类型(一个原子访问计数器),其定义如下:

typedef struct{
	int counter;
}atomic_t;

atomic_t类型定义在/include/linux/types.h中, 这样定义的原因是可以让GCC在编译的时候加以更加严格的类型检查,防止原子类型变量被误操作。随着64位体系结构的普及,64位原子操作被定义为atomic64_t。

typedef struct{
	long counter;
}atomic64_t;

对于atomic_t类型的变量,linux内核提供了一些专门的函数和宏来进行原子操作,如下表:

原子整数操作 说明
ATOMIC_INIT(int i) 声明atomic_t变量,并初始化为i
atomic_read(v) 原子操作读取整数变量v,返回*v
atomic_set(v,i) 原子操作把*v置成i
atomic_add(i,v) 原子操作*v加i
atomic_add_return(i,v) 原子操作v加i,返回v
atomic_add_negative(i,v) 原子操作*v加i,为负返1,否则返0
atomic_sub(i,v) 原子操作*v减i
atomic_sub_return(i,v) 原子操作v减i,返回v
atomic_sub_and_test(i,v) 原子操作*v减i,为0返1,否则返0
atomic_inc(v) 原子操作*v自加1
atomic_inc_and_test(v) 原子操作*v自加1,为0返1,为1返0
atomic_dec(v) 原子操作*v自减1
atomic_dec_and_test(v) 原子操作*v自减1,为0返1,为1返0

原子操作用法举例

\\定义原子变量
atomic_t u;//定义原子变量u
atomic_t v= ATIMIC_INIT(0);//定义原子变量v,并初始化为0
\\操作原子变量
atomic_set(&v,10);\\原子操作v=10
atomic_add(5,&v);\\原子操作v=v+5=15
atomic_dec(2,&v);\\原子操作v=v-2=13
atomic_sub_and_text(13,&v);\\原子操作v=v-13=0,并返回1
printk("v=%d\n",atomic_read(&v));\\会打印出v的值,即打印v=0

在内核实现计数器时,用锁机制来保护显得有些笨拙,而轻量级地保护内核计数器就可以使用原子操作。
Linux内核除了原子整数操作外,也提供了一组针对位原子操作的函数,如下表所示,它被定义在/include/asm-generic/bitops/atomic.h中,它常被应用于内存管理、设备驱动。

原子位操作 说明
set_bit(nr,addr) 原子操作设置地址为addr的第nr位
clear_bit(nr,addr) 原子操作清除地址为addr的第nr位
change_bit(nr,addr) 原子操作,取反地址为addr的第nr位
test_and_set_bit(nr,addr) 原子操作设置地址为addr的第nr位,并返回原先的值
test_and_clean_bit(nr,addr) 原子操作清除地址为addr的第nr位,并返回原先的值
test_and_clean_bit(nr,addr) 原子操作取反地址为addr的第nr位,并返回原先的值
test_bit(nr,addr) 原子操作,返回地址为addr的第nr位
find_first_zero_bit(addr,size) addr为内存的起始地址,size为要查找的最大长度,返回第一个为0的位号
find_next_zero_bit(addr,size,offset) addr为内存的起始地址,size为要查找的最大长度,offset
为开始搜索的起始位号,返回第一个为0的位号

注:

  • clear_bit(nr,addr)是原子操作,但不具备加锁功能,若要用于加锁目的,应当调用smp_mb__before_clear_bit 或smp_mb__after_clear_bit函数,以确保任何改变在其他的处理器上是可见的。
  • test_bit(nr,addr)定义如下
#define test_bit(nr, addr)			\
	(__builtin_constant_p((nr))		\
	? constant_test_bit((nr), (addr))	\
	: variable_test_bit((nr), (addr)))

根据nr是否为编译时常数来调用不同的函数

  • 若编译时为常数,则调用constant_test_bit
static __always_inline int constant_test_bit(unsigned int nr, const volatile unsigned long *addr) 
  • 若编译时为非常数,则调用variable_test_bit
static inline int variable_test_bit(int nr, volatile const unsigned long *addr)

(2)自旋锁

待续…

(3)信号量

待续…

猜你喜欢

转载自blog.csdn.net/qq_34258344/article/details/100592236