背景
msleep 函数的核心逻辑是将毫秒数转换为内核的 jiffies 单位,并通过 schedule_timeout_uninterruptible 让当前进程进入不可中断的睡眠状态。
细节
/**
* msleep - sleep safely even with waitqueue interruptions
* @msecs: Time in milliseconds to sleep for
*/
void msleep(unsigned int msecs)
{
unsigned long timeout = msecs_to_jiffies(msecs) + 1;
while (timeout)
timeout = schedule_timeout_uninterruptible(timeout);
}
可以看到将等待的毫秒换算为jiffies 然后 + 1,这里+1 是一个工程约等于。为了确保睡眠时间不会少于指定的毫秒数。msecs_to_jiffies 函数将毫秒转换为 jiffies,但由于 jiffies 是一个离散的计时单位,可能会导致精度损失。也体现了msleep精度并不是准确的毫秒。
另外可以看到msleep使用了while循环,但是会让出调度器。所以msleep会阻塞在这里。然后调度器每次调度回来msleep所在的执行单元后,会判断timeout是否已经timeout。进程会持续以不可中断的方式睡眠,直到timeout超时或者被其他机制唤醒。并且每次唤醒后会更新timeout的值。
其中schedule_timeout_uninterruptible在给定的超时时间内让当前进程进入睡眠状态,并且在睡眠期间不会被信号中断。睡眠期间,进程不会响应信号,但可以被内核的定时器机制或其他内核机制唤醒。如果超时时间到达,进程会被内核调度器唤醒。
这里所谓的不可中断为什么又能被其他机制唤醒呢?
这里涉及到Linux 内核的调度机制和信号处理机制。
“不可中断”(TASK_UNINTERRUPTIBLE)状态主体是进程。调用msleep的进程不会被信号(如 SIGINT(比如ctrl + c中断)、SIGTERM 等)唤醒。当进程处于这种状态时,它不会响应用户空间的信号中断,必须等到它自己完成睡眠或者被其他机制唤醒。
这里的中断不是只芯片的中断,不是一个概念和层次,他是一个进程能否被“叫停”。
内核驱动例子
static int wait_fw_init(struct mlx5_core_dev *dev, u32 max_wait_mili,
u32 warn_time_mili)
{
unsigned long warn = jiffies + msecs_to_jiffies(warn_time_mili);
unsigned long end = jiffies + msecs_to_jiffies(max_wait_mili);
u32 fw_initializing;
int err = 0;
do {
fw_initializing = ioread32be(&dev->iseg->initializing);
if (!(fw_initializing >> 31))
break;
if (time_after(jiffies, end) ||
test_and_clear_bit(MLX5_BREAK_FW_WAIT, &dev->intf_state)) {
err = -EBUSY;
break;
}
if (warn_time_mili && time_after(jiffies, warn)) {
mlx5_core_warn(dev, "Waiting for FW initialization, timeout abort in %ds (0x%x)\n",
jiffies_to_msecs(end - warn) / 1000, fw_initializing);
warn = jiffies + msecs_to_jiffies(warn_time_mili);
}
msleep(mlx5_tout_ms(dev, FW_PRE_INIT_WAIT));
} while (true);
return err;
}
这里是网卡的驱动程序定义了一个等待行为,以阻塞方式,间歇性的读取硬件状态是否就绪,但是在等待期间会让出调度器,并且不希望被信号中断。
代码模板
基于前面提到的例子,我们可以实现一个基于msleep的间歇性check函数模板,类似如下(并未调试)
下面例子实现一个等待函数,指定一个最长等待时间T1后,就会退出,但是等待期间会被分成多个时间窗口,每个时间窗口检测一次,每个时间窗口间隔T2;并且会在每间隔T3时间打印一次告警。
比如最长等待120s,每间隔1ms检测一次,每间隔1s打印一次。那就会在每次检测的期间,判断是否和上次间隔1s,如果间隔1s打印告警。如果和最开始间隔超过最长时间直接退出。
这里通过总分的方式再加状态check方式来查看硬件状态,既避免不停查询,也避免等待过久,并且对外还提供了一个总体时间。是一种不错的对外交付接口行为的表现。
#define MY_DRIVER_OK 0
int get_driver_status()
{
//读取硬件状态比如ioremap的地址用ioread或者其他
}
static int my_wait_pending(u32 max_wait_msec, u32 check_interfal, u32 warning_inter_msec)
{
//# max_wait_msec T1
//# check_interfal T2
//# warning_inter_msec T3
unsigned long warn = jiffies + msecs_to_jiffies(warning_inter_msec);
unsigned long end = jiffies + msecs_to_jiffies(max_wait_msec);
u32 flag = 0;
int err = 0;
do {
//check status for driver
// if check ok break
flag = get_driver_status(); //醒来后马上确认状态
if (flag == MY_DRIVER_OK) {
break;
}
if (time_after(jiffies, end)) { //已经超出最长时间了
printk(KERN_ERR "timeout...");
err = -EBUSY;
break;
}
if (warning_inter_msec && time_after(jiffies, warn)) { //固定时间对外提示告警
printk(KERN_WARN "timeout abort in %d seconds. Current turn check status is 0x%x\n", jiffies_to_msecs(end - warn) / 1000, flag); //打印还将等待多长时间后就会退出
warn = jiffies + msecs_to_jiffies(warn_time_msec);
}
//进入睡眠让出cpu
msleep(check_interfal); //固定check_interfal时间 醒来进行确认
} while (true);
return error; //最后返回异常状态
}
调用方式:
my_wait_pending(120 * 1000, 1, 1 * 1000); //入参单位是毫秒,分别最长等待120s,每间隔1ms检测一次,每间隔1s打印一次
所以该模板实现了最长等待120s,每间隔1ms检测一次,每间隔1s打印一次。那就会在每次检测的期间,判断是否和上次间隔1s,如果间隔1s打印告警。如果和最开始间隔超过最长时间直接退出。
当然还有一种实现方式比较原始的行为大体伪代码类似:
#define MAX_WAIT_TIME (120 * 1000) //ms
#define CHECK_INTERNAL 1 //ms
#define WARNING_INTERNAL (1 * 1000) //ms
for (int window = 0, warning_cnt = 0; window < (MAX_WAIT_TIME / CHECK_INTERNAL); windows ++)\
{
//check
if checkflag ok : break;
//max timeout 这里不用判断,for循环会限制
//warning打印
warning_cnt ++;
if warning_cnt == WARNING_INTERNAL / CHECK_INTERNAL:
warning_cnt = 0;
printk waning
msleep(CHECK_INTERNAL);
}