标题:Linux内核锁与中断处理


Linux内核锁

在Linux内核里面,一般采用了如下几种锁的机制,来保证多线程的同步与互斥:

 

(1)原子操作

atomic_t v
void atomic_set(atomic_t *v, int i);
atomic_t v = ATOMIC_INIT(0);
int atomic_read(atomic_t *v);
void atomic_add(int i, atomic_t *v);
void atomic_sub(int i, atomic_t *v);
void atomic_inc(atomic_t *v);
void atomic_dec(atomic_t *v);
void set_bit(nr, void *addr);
void clear_bit(nr, void *addr);

 

(2)自旋锁

不可递归

不可睡眠

中断上下文可使用

短期持有

spinlock_t my_lock = SPIN_LOCK_UNLOCKED;
void spin_lock_init(spinlock_t *lock);
void spin_lock(spinlock_t *lock);
void spin_unlock(spinlock_t *lock);

 

(3)信号量

允许睡眠

适合长期占有

只能在进程上下文使用

void sema_init(struct semaphore *sem, int val);
DEFINE_SEMAPHORE(name)
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
void up(struct semaphore *sem);

 

(4)互斥体
void mutex_init(struct mutex *mutex);
static DEFINE_MUTEX(mymutex);  
mutex_lock(&mymutex);
/* Critical Section code ... */  
g_i++;
mutex_unlock(&mymutex);  

int mutex_lock_interruptible(struct mutex *lock);
int mutex_trylock(struct mutex *lock);  //never sleeps

(5)完成变量

//定义和初始化
struct completion my_completion; init_completion(&my_completion);
//等待
void wait_for_completion(struct completion *c);
//完成
void complete(struct completion *c);
void complete_all(struct completion *c); 

一个任务通知另外一个任务发生了某个特定的事件。同步两个任务。

 


(6)读写锁

rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* Static way */
rwlock_t my_rwlock;
rwlock_init(&my_rwlock); /* Dynamic way */
void read_lock(rwlock_t *lock);
void read_unlock(rwlock_t *lock);
void write_lock(rwlock_t *lock);
void write_unlock(rwlock_t *lock);

 

Linux软中断和工作队列

(1)中断处理程序局限:

异步方式执行并且可能会打断其他重要代码,甚至是其他中断处理程序

往往对硬件进行操作,需要很高的时限要求,效率要高,时间要少

中断上下文不能阻塞

一般只完成必要的数据拷贝

(2)下半部机制

下半部的任务是执行与中断处理密切相关但中断处理程序本身不执行的工作。在下半部运行时,可以响应所有的中断。

a.软中断

b.tasklet

c.工作队列

大多数时候选择tasklet;需要睡眠选择工作队列;执行频率和连续性高时选择软中断。

 

 软中断:

a)产生后并不是马上可以执行,必须要等待内核的调度才能执行。软中断不能被自己打断,只能被硬件中断打断(上半部)。

 b)可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保护其数据结构。

 

 tasklet:

由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那么软中断其实是没有必要的。因此诞生了弥补以上两个要求的tasklet。它具有以下特性:

 a)一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。

 b)多个不同类型的tasklet可以并行在多个CPU上。

c)软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。

 

workqueue:

软中断和tasklet运行在中断上下文中,于是导致了一些问题:不能睡眠、不能阻塞。由于中断上下文处于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。但可阻塞函数不能用在中断上下文中实现,必须要运行在进程上下文中,例如访问磁盘数据块的函数。因此,可阻塞函数不能用软中断来实现。但是它们往往又具有可延迟的特性。

 因此在2.6版的内核中出现了在内核态运行的工作队列(替代了2.4内核中的任务队列)。它也具有一些可延迟函数的特点(需要被激活和延后执行),但是能够能够在不同的进程间切换,以完成不同的工作。

 

 

上半部和下半部的区别
上半部指的是中断处理程序,下半部则指的是一些虽然与中断有相关性但是可以延后执行的任务。举个例子:在网络传输中,网卡接收到数据包这个事件不一定需要马上被处理,适合用下半部去实现;但是用户敲击键盘这样的事件就必须马上被响应,应该用中断实现。
两者的主要区别在于:中断不能被相同类型的中断打断,而下半部依然可以被中断打断;中断对于时间非常敏感,而下半部基本上都是一些可以延迟的工作。由于二者的这种区别,所以对于一个工作是放在上半部还是放在下半部去执行,可以参考下面四条:
a
)如果一个任务对时间非常敏感,将其放在中断处理程序中执行。
b
)如果一个任务和硬件相关,将其放在中断处理程序中执行。
c
)如果一个任务要保证不被其他中断(特别是相同的中断)打断,将其放在中断处理程序中执行。

d)其他所有任务,考虑放在下半部去执行。

 

Linux内核工作在进程上下文或者中断上下文。提供系统调用服务的内核代码代表发起系统调用的应用程序运行在进程上下文,代表进程运行;另一方面,中断处理程序,异步运行在中断上下文,代表硬件运行,中断上下文和特定进程无关。运行在中断上下文的代码就要受一些限制,不能做下面的事情:

1、睡眠或者放弃CPU(硬件与人睡眠例子)

    因为内核在进入中断之前会关闭进程调度,一旦睡眠或者放弃CPU,这时内核无法调度别的进程来执行,系统就会死掉

2、尝试获得信号量

    如果获得不到信号量,代码就会睡眠,效果同1

3、执行耗时的任务

    中断处理应该尽可能快,因为内核要响应大量服务和请求,中断上下文占用CPU时间太长会严重影响系统功能。

4、访问用户空间的虚拟地址



看文字不过瘾?点击我,进入周哥教IT视频教学
麦洛科菲长期致力于IT安全技术的推广与普及,我们更专业!我们的学员已经广泛就职于BAT360等各大IT互联网公司。详情请参考我们的 业界反馈 《周哥教IT.C语言深学活用》视频

我们的微信公众号,敬请关注