Linux源码阅读网站:https://elixir.bootlin.com/linux/v5.15/source/include/linux/atomic/atomic-instrumented.h#L179
现代操作系统三大特性:中断处理,多任务处理,多处理器
多个线程,进程,或者多个CPU访问同一个资源时可以能会发生错误
内核需要提供一些控制机制,防止错误的发生,保护公用资源
以下实例来自正点原子
控制机制有:
原子变量操作
自旋锁
信号量
完成量
原子变量操作
1.原子变量操作,有原子的特性,以前人们觉得不可再分,最小。原子变量操作就是不会被中断的操作。
2.原子变量操作是需要硬件支持的,与架构相关。
3.使用汇编实现,C语言无法实现,定义在内核源码树/include/asm/drivers/atomic.h中
4.优点是编写简单,不被中断。缺点是只能实现简单的功能,如计数操作,保护的东西十分有限
如何使用
内核为原子类型特定地设计了一种结构,内核开发程序员可以使用此结构实例化出原子变量
为原子变量身上的原子操作也有一套特有的函数,用于对其进行设置,自增,自减等操作
内核中对原子结构的定义:/include/linux/types.h
//原子结构
typedef struct {
int counter;
} atomic_t;
//INIT宏 用于定义一个原子变量
//定义和初始化一个结构体
//使用方法:atomic_t count = ATOMIC_INIT(0);
//展开:atomic_t count = { (0) };
#define ATOMIC_INIT(i) { (i) }
#ifdef CONFIG_64BIT
typedef struct {
s64 counter;
} atomic64_t;
#endif
内核中对原子结构的使用案例:
对原子变量的操作函数接口都放在了/include/linux/atomic/atomic-instrumented.h
//定义变量
static atomic_t waiting_for_crash_ipi;
//.....
//变量使用
void machine_crash_nonpanic_core(void *unused)
{
struct pt_regs regs;
crash_setup_regs(®s, get_irq_regs());
printk(KERN_DEBUG "CPU %u will stop doing anything useful since another CPU has crashed\n",
smp_processor_id());
crash_save_cpu(®s, smp_processor_id());
flush_cache_all();
set_cpu_online(smp_processor_id(), false);
atomic_dec(&waiting_for_crash_ipi); //在此处使用了
while (1) {
cpu_relax();
wfe();
}
}
原子位操作与原子操作差不多,只不过这个操作粒度更细一些;
原子操作只能对整形变量或者位进行保护,能保护的东西太有限了,如果涉及到其他类型如结构体,就需要使用到锁机制。
自旋锁
自旋锁,应该说是让别人自旋的锁,QAQ
当你拿到这个锁,那么你的资源别人就无法访问了,只能在旁边眼睁睁地看着,打转儿!这就是自旋的意思
看得出来,如果你长期持有这个锁,等待这个锁释放的所有线程都会搁那儿打转,浪费时间,降低了系统性能
所以它不适合在长时间持有锁的场合
定义所在文件:/include/linux/spinlock_types.h
typedef struct spinlock {
struct rt_mutex_base lock;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
} spinlock_t;
使用案例:
static DEFINE_SPINLOCK(i8259_irq_lock);
inline void
i8259a_enable_irq(struct irq_data *d)
{
spin_lock(&i8259_irq_lock);
i8259_update_irq_hw(d->irq, cached_irq_mask &= ~(1 << d->irq));
spin_unlock(&i8259_irq_lock);
}
自旋锁API
死锁原因
1.在自旋锁保护的临界区不能使用任何能够引起睡眠和阻塞的API函数
自旋锁会自动禁止抢占,也就说当线程 A得到锁以后会暂时禁止内核抢占。如果线程 A 在持有锁期间进入了休眠状态,那么线程 A 会自动放弃 CPU 使用权。线程 B 开始运行,线程 B 也想要获取锁,但是此时锁被 A 线程持有,而且内核抢占还被禁止了!线程 B 无法被调度出去,那么线程 A 就无法运行,锁也就无法释放,好了,死锁发生了!
2.使用自旋锁的线程,被中断打断访问共享资源
线程 A 先运行,并且获取到了 lock 这个锁,当线程 A 运行 functionA 函数的时候中断发生了,中断抢走了 CPU 使用权。右边的中断服务函数也要获取 lock 这个锁,但是这个锁被线程 A 占有着,中断就会一直自旋,等待锁有效。但是在中断服务函数执行完之前,线程 A 是不可能执行的,线程 A 说“你先放手”,中断说“你先放手”,场面就这么僵持着,
死锁发生!
带中断管理的API
为了能够附加上管理本地中断的功能,可以使用如下的配套API
使用案例
/* 线程 A */
void functionA ()
{
unsigned long flags; /* 中断状态 */
spin_lock_irqsave(&lock, flags) /* 获取锁 */
/* 临界区 */
spin_unlock_irqrestore(&lock, flags) /* 释放锁 */
}
/* 中断服务函数 */
void irq()
{
spin_lock(&lock) /* 获取锁 */
/* 临界区 */
spin_unlock(&lock) /* 释放锁 */
}
读写自旋锁
写的时候不能读,读的时候也不能写
读的时候可以读
顺序锁
写的时候可以读,如果读的时候发生了写操作,最好重新读取,以保证数据完整性
不允许同时进行并发的写操作
信号量
信号量介绍
信号量是同步的一种方式
用于控制对共享资源的访问
正点原子的绝妙比喻:
某一天早上 A 去上厕所了,过了一会 B 也想用厕所,因为 A 在厕所里面,所以 B 只能等到 A 用来了才能进去。 B
要么就一直在厕所门口等着,等 A 出来,这个时候就相当于自旋锁。 B 也可以告诉 A,让 A 出来以后通知他一下,然后 B
继续回房间睡觉,这个时候相当于信号量。
由此看来:
信号量可以提高处理器的效率,线程不必像自旋锁那样傻等了–> 适合长时间占有资源的场所(对比自旋锁的特点)
信号量也带来了更大的开销,信号量让线程进入休眠状态以后会切换线程,这里会产生开销
–>无法用在中断里(因为中断是无法被休眠的)
–> 如果 共享资源的持有时间比较短 切换频繁 就不要用信号量 开销会很大
信号量数据结构
/* Please don't access any members of this structure directly */
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
信号量使用示例
struct semaphore sem; /* 定义信号量 */
sema_init(&sem, 1); /* 初始化信号量 */
down(&sem); /* 申请信号量 */
/* 临界区 */
up(&sem); /* 释放信号量 */
互斥锁
在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
互斥锁使用示例
1 struct mutex lock; /* 定义一个互斥体 */
2 mutex_init(&lock); /* 初始化互斥体 */
3 4
mutex_lock(&lock); /* 上锁 */
5 /* 临界区 */
6 mutex_unlock(&lock); /* 解锁 */