Linux 竞态解决方案之原子操作
原子操作可以解决任何情况下的竞态问题。
Linux的原子操作主要分为位原子操作以及整型原子操作
位原子操作
eg
int g_data = 0x10; // 共享资源
g_data |= (1<< 3);
g_data ~= ( 1>>2);
以上的两个位操作并不是原子操作。
如果内核编程中,操作共享资源时,需要解决竞态问题,除了之前博客中所说的那三种方法,原子操作也是一个很好的方案。
内核也提供了一些位原子操作的API,如下:(示例定义,也许是宏,或者某个头文件中的static inline函数,根据不同的平台内核代码会进行选择)
test_bit(nr, addr); // 获取某个bit值。
整型原子操作
最典型的 g_data ++; g_data --; 都不是原子操作,内核为整型提供了原子操作的方法,如下:
内核提供了很多atomic的接口。
整型原子操作示例
atomic_t g_data = ATOMIC_INIT(1);
atomic_set(&g_data, 2);
atomic_read(&g_data);
Linux 内核等待队列
1. 等待分为忙等待和休眠等待,等待队列的等待,是休眠等待(不可用于中断上下文)
2. 休眠等待是指等去进程释放CPU资源,将CPU资源让出给到其他任务使用
3. 等待的原因:等待事件的触发,比如socket上收到数据;或者等待某个条件得到满足等等
4. Linux 系统下休眠等待的方法:
在用户空间调用 usleep,sleep 等
在内核空间调用msleep,ssleep,schedule,schedule_timeout等
这些函数,有个相对而言共同的特点,就是必须休眠指定的时间,即便在这个时间内,某个条件得到满足,也无法被唤醒,除非发送信号。(休眠只允许适用于进程上下文,进程上下文可接收信号)
对应休眠时间不固定,事件触发时刻不确定,条件被满足时机不确定的场景,以上的等待显然是不靠谱的。
5. 既然休眠等待在进程上下文中,那么对进程的状态是有影响的:
进程的运行状态: TASK_RUNNING
进程的准备就绪状态: TASK_READY
进程的休眠状态:
可中断的休眠状态和不可中断的休眠状态(TASK_UNINTERRUPTBLE, TASK_INTERRUPTIBLE)
6. 等待队列的实质是有多个休眠的进程放在一组,形成一个队列。
7. 同一个等待队列中的每一个节点,每一个休眠的进程都等待同一个事件,如果等待不同的事件,那么需要将进程添加入不同的队列中去。
总结:linux内核等待队列机制的本质目的就是让进程在内核空间在不满足条件的情况下进行休眠,在满足了条件的情况下进行唤醒,而不是固定间隔的休眠,无法及时的被唤醒去响应事件。
编程步骤
需要确认:需要休眠的进程,等待队列头,进程的处理者即调度器。作为驱动开发,调度不需要处理,由Linux内核完成。
1. 定义并初始化等待队列头
wait_queue_head_t wq; // 定义一个等待队列头
init_waitqueue_head(&wq);// 初始化等待队列头
2. 定义并初始化装载休眠进程的容器
注意:当前系统也即Ubuntu 64bit 版本的改数据结构过大,这个用的是其他架构下的,old版本,作为示例。pid的类型都不是 pid_t。comm 表示进程名,也只给了17个字符,也是最长16个单字节字符的c-style的字符串。
wait_queue_t wait; // 定义一个装载进程的容器
init_waitqueue_entry(&wait, current); // 将当前进程添加到容器中
其中 current 表示当前进程,是一个宏,内部通过 get_current(); 获取到当前进程的 stask_struct 指针。
3. 添加休眠进程到等待队列(就是等待队列头)中
add_wait_queue(&wq, &wait);
添加进去,但是此时并没有我们期望的休眠。
4. 设置当前进程的休眠状态:可中断或者不可中断的休眠状态
5. 让进程进入休眠状态
schedule(); //永久性休眠
注意,此时代码在这里将不再继续执行了,进入休眠等待,直到条件被满足后,被唤醒。如果是可中断的休眠状态,那么也有可能被信号唤醒。
6. 一旦进程被唤醒,进程将从schedule函数返回,代码继续执行,然后要设置状态为运行状态,并且将当前进程从休眠队列中移除。
set_current_state(TASK_RUNNING);
remove_wait_queue(&wq, &wait);
7. 接下来,要做一件重要的事儿,就是判断进程被唤醒的原因,是驱动主动唤醒(某个事件得到满足),还是信号引起的唤醒。
注意:这个接口返回的是 true 跟 false,如果是 true 表示,表示是信号唤醒的,否则,是事件得到满足
if ( signal_pending(current) )
{
printk("由于信号引起的唤醒!\n");
return -ERESTARTSYS;
}
else
{
printk("事件得到满足,驱动主动唤醒!\n");
// 处理事件
// ....
}
8. 当事件得到满足,驱动主动唤醒休眠进程的方法
wake_up(&wq); // 唤醒wq等待队列中所有的休眠进程
// 或者
wake_up_interruptible(&wq); // 唤醒wq等待队列中所有的的睡眠类型为可中断的休眠进程
操蛋:唤醒这个等待队列中所有的进程,是不是很操蛋,确实非常操蛋,所以编程上不控制,那就可能造成共享资源抢占,造成竞态,而且同时唤醒所有,就是“惊群”。非常操蛋!消费者一群都嗷嗷待哺,只有一只小虫子,你把小崽子全喊到餐桌上,是不是很操蛋!
优化后的等待队列
1. 初始化一个队列头
2. 实现条件判断以及休眠
注意: wait_event(wq, condition);
第一: wq head 对象,而不是指针
第二: condition 如果为 true 会被直接 break;也就是说,直接往下走,不会休眠,否则进入休眠
以上是进入的是不可中断的休眠,如果需要进入可中断的休眠,如下:
注意,wait_event 无返回值,但是 wait_event_interruptible 有返回值。
conditon:
如果为真,进程不休眠,立即返回0
如果为假,进程将在此休眠,休眠状态为可中断,并且等待驱动主动唤醒或者信号来唤醒,驱动主动唤醒时,驱动一定要将conditon置真
如果是驱动主动唤醒,此宏返回,并且之后驱动还要将condtion置假,为下一次休眠做准备
这句话是什么意思呢?一段编程示例: