版权声明:本文为博主原创文章,未经博主允许不得转载。
https://blog.csdn.net/huangweiqing80/article/details/83028651
现代操作系统有三大特性:中断处理、多任务处理和多核处理器(SMP)。这些特性导致当多个进程、线程或者CPU同时访问一个资源时,可能会发生错误,这些错误是操作系统运行所不允许的,这就是并发。
并发是指在操作系统中,一个时间段中有几个程序正在运行,且这几个程序都是在同一个处理机上运行,但任一时刻点上只有一个程序在处理机上运行。
并发容易导致竞争问题。竞争就是两个或者两个以上的新城同时访问同一个资源,从而引起资源的错误。
为了避免并发对系统资源的影响,Linux中提出了一下并发控制机制。这些控制机制有原子变量操作、锁机制。
一、原则变量操作
所谓原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断,也就说,它是最小的执行单位,不可能有比它更小的执行单位,因此这里的原子实际是使用了物理学里的物质微粒的概念。
原子操作需要硬件的支持,因此是架构相关的,其API和原子类型的定义都定义在内核源码树的include/asm/atomic.h文件中,它们都使用汇编语言实现,因为C语言并不能实现这样的操作。
原子操作主要用于实现资源计数,很多引用计数(refcnt)就是通过原子操作实现的。
1.1. 原子类型定义
Typedef struct {
volatile int counter;
} atomic_t;
volatile修饰字段告诉gcc不要对该类型的数据做优化处理,对它的访问都是对内存的访问,而不是对寄存器的访问。精确地说就是,优化器在用到这个变量时必须每次都小心地从内存重新读取这个变量的值,而不是使用保存在寄存器里的备份。
关于volatile可以看volatile关键字的解析
在Linux中,定义了两种原子变量操作,一种是原子整型操作,另一种是原子位操作。
1.2. 原子整型操作
有时候需要共享的资源可能只是一个简单的整型数值。这时候就可以使用原子整型操作。
1.申明定义atomic_t
ATOMIC_INIT宏的功能是定义一个atomic_t类型的变量,红参数是需要给该变量初始化的值。该宏定义如下:
#define ATOMIC_INIT(i) { (i) }
我们知道atomic_t类型的变量是一个结构图类型,所以对其进行定义和初始化应该用结构体的方法来定义和初始化。
例如我们要声明定义一个名为section的atomic_t类型的变量:
atomic_t section = ATOMIC_INIT(0);
这句代码展开后,就是
atomic_t section = { (0) };
1.3. 原子操作API
-
atomic_set(atomic_t* v, int i);
该函数设置原子类型的变量v的值为i。 -
atomic_read(atomic_t* v);
该函数对原子类型的变量进行原子读操作,它返回原子类型的变量v的值。 -
atomic_add(int i, atomic_t *v);
该函数给原子类型的变量v增加值i。 -
atomic_sub(inti, atomic_t *v);
该函数从原子类型的变量v中减去i。 -
atomic_inc(atomic_t*v);
该函数对原子类型变量v原子地增加1。 -
Int atomic_sub_and_test(inti, atomic_t *v);
该函数从原子类型的变量v中减去i,并判断结果是否为0,如果为0,返回真,否则返回假。 -
Void atomic_dec(atomic_t*v);
该函数对原子类型的变量v原子地减1。 -
Int atomic_dec_and_test(atomic_t*v);
该函数对原子类型的变量v原子地减1,并判断结果是否为0,如果为0,返回真,否则返回假。 -
Int atomic_inc_and_test(atomic_t*v);
该函数对原子类型的变量v原子地增加1,并判断结果是否为0,如果为0,返回真,否则返回假。 -
Int atomic_add_negative(inti, atomic_t*v);
该函数对原子类型的变量v原子地增加I,并判断结果是否为负数,如果是,返回真,否则返回假。 -
Int atomic_add_return(inti, atomic_t *v);
该函数对原子类型的变量v原子地增加i,并且返回指向v的指针。 -
Int atomic_sub_return(inti, atomic_t *v);
该函数从原子类型的变量v中减去i,并且返回指向v的指针。 -
Int atomic_inc_return(atomic_t* v);
该函数对原子类型的变量v原子地增加1并且返回指向v的指针。 -
Int atomic_dec_return(atomic_t* v);
该函数对原子类型的变量v原子地减1并且返回指向v的指针。
原子操作通常用于实现资源的引用计数,在TCP/IP协议栈的IP碎片处理中,就使用了引用计数,碎片队列结构struct ipq描述了一个IP碎片,字段refcnt就是引用计数器,它的类型为atomic_t,当创建IP碎片时(在函数ip_frag_create中),使用atomic_set函数把它设置为1,当引用该IP碎片时,就使用函数atomic_inc把引用计数加1,当不需要引用该IP碎片时,就使用函数ipq_put来释放该IP碎片,ipq_put使用函数atomic_dec_and_test把引用计数减1并判断引用计数是否为0,如果是就释放Ip碎片。函数ipq_kill把IP碎片从ipq队列中删除,并把该删除的IP碎片的引用计数减1(通过使用函数atomic_dec实现)。
1.4 原子位操作
原子位操作是根据数据的每一位单独进行操作。根据体系结构的不同,原子位操作函数的实现也不同。原子位操作和原子整数操作是不同的。原子位操作不需要专门定义一个类似atomic_t类型的变量,只需要一个普通的变量指针就可以了。
原子位操作API有
-
static inline void set_bit(unsigned nr, volatile unsigned long *addr)
将addr变量的第nr位设置为1 -
static inline void clear_bit(unsigned nr, volatile unsigned long *addr)
将addr变量的第nr位设置为0 -
static inline void change_bit(unsigned nr, volatile unsigned long *addr)
将addr变量的第nr位设置为相反的数 -
static inline int test_and_set_bit(unsigned nr, volatile unsigned long *addr)
将addr变量的第nr位设置为1,并返回没有修改之前的值 -
static inline int test_and_clear_bit(unsigned nr, volatile unsigned long *addr)
将addr变量的第nr位设置为0,并返回没有修改之前的值 -
static inline int test_and_change_bit(unsigned nr,volatile unsigned long *addr)
将addr变量的第nr位设置为相反的数,并返回没有修改之前的值
1.5 非原子位操作
在Linux中,还定义了一组与原子位操作功能相同但非原子位的操作。这些函数的命名是在原子位操作的函数前加两个下划线。例如与原子位操作set_bit()函数想对应的是__set_bit(),这个函数不会保证是一个原子操作。与此类似的函数原型如下:
static inline void __set_bit(int nr, volatile unsigned long *addr)
static inline void __clear_bit(int nr, volatile unsigned long *addr)
static inline void __change_bit(int nr, volatile unsigned long *addr)
static inline int __test_and_set_bit(int nr, volatile unsigned long *addr)
static inline int __test_and_clear_bit(int nr, volatile unsigned long *addr)
static inline int __test_and_change_bit(int nr,volatile unsigned long *addr)
二、锁机制
Linux中提供了一些锁机制来避免竞争条件,引入锁机制,是因为单独的原子操作不能满足复杂的内核设计需要。例如,当一个临界区域要在多个函数之间来回运行时,原子操作就显得无能为力了。
一般可以认为有两种锁,一种是自旋锁,另一种是信号量。当然还有其他的一下锁。
锁机制的实现理念是:
去获得锁(自旋锁/信号量),如果获取失败,则进程会等待或者休眠,这样获取锁这条代码后面的代码将暂时不会执行,只有获取成功返回之后才会开始执行下面的代码,我们就把临界资源放到获取锁这条代码后面,这样就达到了锁定临界资源的目的了
自旋锁总结:
Spanlock(自旋锁-调用进程不会睡眠)
自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。
信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用(_trylock的变种能够在中断上下文使用),而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共巷资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。
自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。
互斥锁是计数器值为1的信号量
跟互斥锁一样,一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。
无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。
(3)在单处理机器上,自旋锁是无意义的。因为在编译时不会加入自旋锁,仅仅被当作一个设置内核抢占机制是否被启用的开关。如果禁止内核抢占,那么在编译时自旋锁会被完全剔除出内核。
(4)Linux内核中,自旋锁是不可递归的。如果试图得到一个你正在持有的锁,你必须去自旋,等待你自己释放这个锁。但这时你处于自旋忙等待中,所以永远不会释放锁,就会造成死锁现象。
(5)在中断处理程序中,获取锁之前一定要先禁止本地中断(当前处理器的中断),否则,中断程序就会打断正持有锁的内核代码,有可能会试图去争用这个已经被持有的自旋锁。这样就会造成双重请求死锁(中断处理程序会自旋,等待该锁重新可用,但锁的持有者在这个处理程序执行完之前是不可能运行的)
(6)锁真正保护的是数据(共享数据),而不是代码。对于BLK(大内核锁)保护的是代码。