同步(二) —— 操作系统wait,signal原句实现分析

本文详细介绍了Linux操作系统中的Wait & Signal机制,包括基本原理、实现方式及常见问题的解决方案,特别关注如何避免丢失唤醒等问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一、概述

二、wait & signal基本原理

2.1  Lost Wake-Up Problem

三、linux中的实现

3.1 等待队列

3.2 wait_event_interruptible

3.3 wake_up_interruptible

3.3.1 Thundering Herd Problem

四、参考


一、概述

在OS中,一种同步的手段是wait/signal,用于请求资源的延迟可能较长的情形

二、wait & signal基本原理

一个wait的实现应该像下面这样,依赖于操作系统的调度:

sleeping_task = current;
set_current_state(TASK_INTERRUPTIBLE);
schedule();
func1();

先保存current,然后设置当前进程的状态为TASK_INTERRUPTIBLE,这样在schedule的时候该进程就会从运行队列中移出。

signal的实现就是将进程的状态重新置为TASK_RUNNING

一般的,在实际应用中,进程都是等待资源条件不满足时进行睡眠。这样在实现时需要注意几种情况:

2.1  Lost Wake-Up Problem

错过唤醒的问题,如下代码:

Process A:
1  spin_lock(&list_lock);
2  if(list_empty(&list_head)) {
3      spin_unlock(&list_lock);
4      set_current_state(TASK_INTERRUPTIBLE);
5      schedule();
6      spin_lock(&list_lock);
7  }
8
9  /* Rest of the code ... */
10 spin_unlock(&list_lock);

Process B:
100  spin_lock(&list_lock);
101  list_add_tail(&list_head, new_node);
102  spin_unlock(&list_lock);
103  wake_up_process(processa_task);

如上图所示,先不看问题,先做以下几点说明:

  1. 如果条件判断涉及到共享资源,要加锁避免race condition,如上图 1 spin_lock
  2. 如果条件不满足,在主动让出cpu之前,要先释放锁,在这种条件下,持有锁睡眠会导致死锁。

下面说明一下代码存在的问题,我们知道,在释放锁的那一刻,一定要分析之后产生的窗口,看看有没有造成问题。如在3-4之间切换出去了,完全执行Process B,将进程状态变成TASK_RUNNING,此时切换回Process A,注意,此时A认为conditon还没有满足,但实际上B执行过后条件已经满足了!如果A仍将进程状态设置为TASK_INTERRUPTIBLE,调用schedule,就会出现一直睡眠无法被唤醒的情况!

  • TIPS:在if/while语句块内,出现窗口切换,很容易出现条件与假设不一致的情况,这时一定要重点分析。

上述问题可以通过如下的代码片段解决:

Process A:

1  set_current_state(TASK_INTERRUPTIBLE);
2  spin_lock(&list_lock);
3  if(list_empty(&list_head)) {
4         spin_unlock(&list_lock);
5         schedule();
6         spin_lock(&list_lock);
7  }
8  set_current_state(TASK_RUNNING);
9
10 /* Rest of the code ... */
11 spin_unlock(&list_lock);

可以看到,将set_current_state(TASK_INTERRUPTIBLE) 提前了

下面说明一下为什么这样的改动可以避免漏掉wake-up的问题,假设在4-5出现了调度,执行Process B调用wake_up,注意此时进程状态是TASK_INTERRUPTIBLE,waku-up在检测到进程是TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE,会将其状态改变成TASK_RUNNING. B执行完毕切换回A,此时进程状态为TASK_RUNNING,这时候调用schedule(),schedule检测到进程状态为TASK_RUNNING会直接返回而不是陷入睡眠。这样就避免了上述问题。

三、linux中的实现

3.1 等待队列

等待队列是内核的数据结构,当内核请求的资源需要等待时,需要进入睡眠状态,此时进程通过进入等待队列保存,实现上内核提供了一个等待队列头和等待队列节点,二者构成一个链表。

先看一下等待队列头:

struct wait_queue_head {
	spinlock_t		lock;
	struct list_head	head;
};
typedef struct wait_queue_head wait_queue_head_t;

队列头初始化可以是静态的或动态的

[include/linux/wait.h]

  • DECLARE_WAIT_QUEUE_HEAD
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) {					\
	.lock		= __SPIN_LOCK_UNLOCKED(name.lock),			\
	.head		= { &(name).head, &(name).head } }

#define DECLARE_WAIT_QUEUE_HEAD(name) \
	struct wait_queue_head name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
  • init_waitqueue_head

接下来看一下等待队列节点:

struct wait_queue_entry {
	unsigned int		flags;
	void			*private;
	wait_queue_func_t	func;
	struct list_head	entry;
};

typedef struct wait_queue_entry wait_queue_entry_t;

其初始化也分为两种:

  • DECLARE_WAITQUEUE
#define __WAITQUEUE_INITIALIZER(name, tsk) {					\
	.private	= tsk,							\
	.func		= default_wake_function,				\
	.entry		= { NULL, NULL } }

#define DECLARE_WAITQUEUE(name, tsk)						\
	struct wait_queue_entry name = __WAITQUEUE_INITIALIZER(name, tsk)
  • init_waitqueue_entry
static inline void init_waitqueue_entry(struct wait_queue_entry *wq_entry, struct task_struct *p)
{
	wq_entry->flags		= 0;
	wq_entry->private	= p;
	wq_entry->func		= default_wake_function;
}

3.2 wait_event_interruptible

#define wait_event_interruptible(wq_head, condition)				\
({										\
	int __ret = 0;								\
	might_sleep();								\
	if (!(condition))							\
		__ret = __wait_event_interruptible(wq_head, condition);		\
	__ret;									\
})

#define __wait_event_interruptible(wq_head, condition)				\
	___wait_event(wq_head, condition, TASK_INTERRUPTIBLE, 0, 0,		\
		      schedule())

重点看一下__wait_event

#define ___wait_event(wq_head, condition, state, exclusive, ret, cmd)		\
({										\
	__label__ __out;							\
	struct wait_queue_entry __wq_entry;					\
	long __ret = ret;	/* explicit shadow */				\
										\
	init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0);	\
	for (;;) {								\
		long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state);\
										\
		if (condition)							\
			break;							\
										\
		if (___wait_is_interruptible(state) && __int) {			\
			__ret = __int;						\
			goto __out;						\
		}								\
										\
		cmd;								\
	}									\
	finish_wait(&wq_head, &__wq_entry);					\
__out:	__ret;									\
})

上述代码实际上就是执行以下逻辑

 DEFINE_WAIT(wait);

    while (1) {
    	prepare_to_wait(&queue, &wait, state);
    	if (condition)
	    break;
	schedule();
    }
    finish_wait(&queue, &wait);

prepare_to_wait

void prepare_to_wait(struct wait_queue_head *wq_head, 
                     struct wait_queue_entry *wq_entry, int state)
{
	unsigned long flags;

	wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
	spin_lock_irqsave(&wq_head->lock, flags);
	if (list_empty(&wq_entry->entry))
		__add_wait_queue(wq_head, wq_entry);
	set_current_state(state);
	spin_unlock_irqrestore(&wq_head->lock, flags);
}

从上面我们看到,conditon在某些情况下是共享资源,是要加锁保护的,但是一般内核解锁的位置相对于我们开始的描述提前了,向下面这样:

  1.  lock                   
  2.  while (!conditon) {
  3.           unlock
  4.          DEFINE_WAIT(wait);
  5.           while (1) {
  6.                prepare_to_wait(&queue, &wait, state);
  7.                    if (condition)
  8.                        break;
  9.                    schedule();
  10.           }
  11.           finish_wait(&queue, &wait);
  12.           lock
  13. }
  14. statements
  15. unlock
  • 当然如果condition不是共享资源,或者判断condition自带保护,可以不用锁。
  • 蓝色部分是手工休眠,内核实现对应wait_event
  • 对应标准教材中的wait(enqueue,unlock,sleep,lock)顺序不同,相当于3,4互换,这样是不是有问题呢?而且我们在并发一节中说过,如果在调度之前解锁,产生一个窗口(8,9)条件成立情况下schedule就会产生丢失event情况,可能导致无法唤醒。

可以看到内核一般的使用和上面有所不同——其解决丢失唤醒事件的方法并不是将set_current_state 提前,而是在其中加了一个if条件对从condition的判断, 在此就分析一下linux内核这样实现为何能不丢失唤醒事件,我用下面的表格进行分析,在解锁之后,调度之前产生的每个窗口造成对condition的影响都列在下表,其中3-5意味着在上图3和4之间产生一个窗口,列举了可能的切换之后conditon的变化。记住,我们进入while时条件是不满足的。

3-66-77-88-9结论
真(x)

不调度,我们看到,只要在6-7之间条件成立,

重新获取锁尝试,这是没有问题的。这同时也说明

了,在linux这种写法下,解锁操作在入队prepare_to_wait操作之前是没有问题的。实际上因为linux上加入等待队列并不意味着发生了调度。

 

假(x)
真(x)
假(x)
这个说明参考了LDD3,这种情况会调用schedule函数,却不会发生调度,因为此时进程被唤醒时检测到其是TASK_INTERRUPTIBLE状态,唤醒进程会将其状态重新置为RUNNING,这样schedule就直接返回了。
这个是我们预期的操作,在解锁和调度之前条件都不成立,进程正常睡眠即可,当进程被唤醒条件可能是真也可能是假,即不假设任何条件,此时老老实实获取锁在去进行尝试即可。

3.3 wake_up_interruptible

#define wake_up_interruptible(x)    __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)

留待以后分析。

3.3.1 Thundering Herd Problem

惊群现象,wake_up_all

四、参考

【1】Kernel Korner - Sleeping in the Kernel

【2】The problem with nested sleeping primitives

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值