我们还是以在餐厅等位作为例子,如果餐厅提供一个通知客人的设备,当有空位时,服务员可以通过这个设备通知等待中的客人。那么,客人就不需要不断去询问服务员空位的状况,可以利用等待这段时间小瞌一会。
类似的,服务端程序在等待客户端请求到来这段时间,操作系统可以先把服务端进程挂起(睡眠),然后执行其他可运行的进程。当有客户端请求到来时,操作系统唤醒服务端进程来处理客户端的请求。
如下图所示:
当进程M在等待系统中某些资源变为就绪状态时,操作系统会把进程M的从 CPU 中切换出去,然后把进程M防止到睡眠队列中。接着操作系统会从可运行队列中,选择一个最合适的进程(如图中的进程1)调度到 CPU 中运行。
当进程M等待的资源变为就绪状态后,操作系统操作系统便会把进程M放置回可运行队列中。这样,进程M就可以在下一个调度周期中争夺 CPU 的运行时间。如下图所示:
在上图中,当客户端请求到来后,操作系统便会唤醒等待客户端请求的进程M,然后把其放回到可运行队列中。
这个就是操作系统中的睡眠与唤醒机制。
Linux 的睡眠与唤醒机制实现
在 Linux 内核中,很多系统调用和内核函数都可能会导致进程睡眠,如 I/O 相关的系统调用、sleep类内核函数、内存分配函数等。
下面我们以sleep类内核函数来分析 Linux 是如何实现睡眠与唤醒机制的。
1. 睡眠函数的使用
在 Linux 内核中,如果想让一个进程进入睡眠状态,可以调用schedule_timeout_interruptible内核函数。其原型如下:
signedlong__schedschedule_timeout_interruptible(signedlongtimeout);
参数timeout表示希望进程睡眠多长时间,此函数会让进程睡眠timeout个时钟节拍(tick)的时间。例如,如果希望进程睡眠 1 秒,可以使用如下代码实现:
...
schedule_timeout_interruptible(1*HZ);
//1秒后进程被唤醒,继续执行下面代码
...
2. 睡眠函数的实现
接下来,我们来分析一下schedule_timeout_interruptible内核函数是如何让进程进入睡眠状态的。其代码如下所示:
signedlong__schedschedule_timeout_interruptible(signedlongtimeout)
{
__set_current_state(TASK_INTERRUPTIBLE);
returnschedule_timeout(timeout);
}
schedule_timeout_interruptible内核函数主要做了如下两件事情:
调用 __set_current_state函数将进程设置为可 中断睡眠状态。需要注意的是,这个步骤只是将进程的状态设置为可中断睡眠状态,但此时进程还没有被内核调度程序移出 CPU。
调用 schedule_timeout函数使进程真正进入睡眠状态(放弃 CPU 的使用权限)。
调用 __set_current_state函数将进程设置为可 中断睡眠状态。需要注意的是,这个步骤只是将进程的状态设置为可中断睡眠状态,但此时进程还没有被内核调度程序移出 CPU。
调用 schedule_timeout函数使进程真正进入睡眠状态(放弃 CPU 的使用权限)。
从上面代码可以看出,schedule_timeout函数才是使进程进入睡眠的主体。那么,我们继续来分析schedule_timeout函数的实现:
signedlong__schedschedule_timeout(signedlongtimeout)
{
structtimer_listtimer;
unsignedlongexpire;
...
expire=timeout+jiffies;
//1.将当前进程添加到定时器中,定时器的超时时间为expire,回调函数为process_timeout
setup_timer_on_stack(&timer,process_timeout,(unsignedlong)current);
__mod_timer(&timer,expire,false,TIMER_NOT_PINNED);
//2.主动触发内核进行进程调度
schedule;
//3.将进程从定时器中删除
del_singleshot_timer_sync(&timer);
destroy_timer_on_stack(&timer);
timeout=expire-jiffies;
out:
returntimeout<0?0:timeout;
}
schedule_timeout函数的逻辑主要分为以下三个步骤:
将当前进程添加到定时器中,定时器的超时时间设置为 expire,回调函数为 process_timeout。那么当定时器超时时,便会触发调用 process_timeout函数。
调用 schedule函数触发内核进行进程调度。由于当前进程在 schedule_timeout_interruptible函数中被设置为 可中断睡眠状态,所以当调度器发现当前进程是 可中断睡眠状态时,将会把当前进程移出可运行队列,并且让出 CPU 的使用权限。
当进程被唤醒后,将会把进程从定时器中删除。
将当前进程添加到定时器中,定时器的超时时间设置为 expire,回调函数为 process_timeout。那么当定时器超时时,便会触发调用 process_timeout函数。
调用 schedule函数触发内核进行进程调度。由于当前进程在 schedule_timeout_interruptible函数中被设置为 可中断睡眠状态,所以当调度器发现当前进程是 可中断睡眠状态时,将会把当前进程移出可运行队列,并且让出 CPU 的使用权限。
当进程被唤醒后,将会把进程从定时器中删除。
从上面的分析可知,在调用schedule_timeout函数时,内核会为当前进程创建一个定时器,其超时时间被设置为schedule_timeout函数传入的参数加上当前时间。当定时器到期后,便会触发调用process_timeout函数,而process_timeout函数最终会调用try_to_wake_up函数来唤醒进程。
我们接着来分析下try_to_wake_up函数的实现,看看其如何唤醒进程的:
staticint
try_to_wake_up(structtask_struct*p,unsignedintstate,intwake_flags)
{
unsignedlongflags;
intcpu,success=0;
...
//1.为进程挑选一个最合适的CPU运行
cpu=select_task_rq(p,p->wake_cpu,SD_BALANCE_WAKE,wake_flags);
...
//2.把进程添加到CPU的可运行队列中
ttwu_queue(p,cpu);
...
returnsuccess;
}
在上面的代码中,我们只保留了核心的代码。可以看出try_to_wake_up函数主要完成 2 件事情:
调用 select_task_rq函数为进程挑选一个最合适的 CPU 运行。
调用 ttwu_queue函数把进程添加到 CPU 的可运行队列中。
调用 select_task_rq函数为进程挑选一个最合适的 CPU 运行。
调用 ttwu_queue函数把进程添加到 CPU 的可运行队列中。
被唤醒的进程添加到 CPU 的可运行队列后,并不会立即被执行。内核会在下一个调度周期中,选择合适的进程进行调度时,被唤醒的进程才有可能被选中运行。
总结
本文主要介绍了进程为什么需要有睡眠和唤醒功能,并且分析了进程睡眠与唤醒的实现原理。进程睡眠与唤醒功能主要为了解决进程在等待某些资源变为可用时,需要不断探测资源的状态。这种探测既白白浪费了宝贵的 CPU 时间,而且还影响了系统的吞吐量。
而进程睡眠可以在进程等待资源变为可用状态时,主动放弃 CPU 的使用权限,这时 CPU 便可运行其他可运行的进程,从而使 CPU 的利用率达到最优。当进程等待的资源变为可用时,内核主动唤醒等待中的进程,进程便可以继续运行。返回搜狐,查看更多