46

现在有一件我一直想知道的事情:sleep() 是如何实现的?

如果都是关于使用操作系统中的 API,那么 API 是如何制作的?

这一切都归结为在 CPU 上使用特殊的机器代码吗?该 CPU 是否需要一个特殊的协处理器或其他小玩意儿,没有它们你就不能拥有 sleep() 吗?

sleep() 最著名的化身是在 C 中(更准确地说,在 C 编译器附带的库中,例如 GNU 的 libc),尽管今天几乎每种语言都有其等价物,但在某些语言中 sleep 的实现(想想 Bash) 不是我们在这个问题中看到的......

编辑:在阅读了一些答案后,我看到该进程被放置在等待队列中。从那里,我可以猜到两种选择,要么

  1. 设置了一个计时器,以便内核在适当的时间唤醒进程,或者
  2. 每当内核被允许一个时间片时,它都会轮询时钟以检查是否该唤醒进程。

答案只提到备选方案 1。因此,我问:这个计时器的行为如何?如果只是一个简单的中断让内核唤醒进程,那么内核如何要求计时器“在 140 毫秒内唤醒我,以便我可以将进程置于运行状态”?

4

8 回答 8

43

问题的“更新”显示了对现代操作系统如何工作的一些误解。

内核不是“允许的”时间片。内核是给用户进程分配时间片的东西。“计时器”未设置为唤醒睡眠进程 - 它设置为停止当前正在运行的进程。

本质上,内核试图通过停止在 CPU 上运行时间过长的进程来公平分配 CPU 时间。为简化图,假设任何进程使用 CPU 的时间不得超过 2 毫秒。因此,内核会将计时器设置为 2 毫秒,然后让进程运行。当定时器触发中断时,内核获得控制权。它保存正在运行的进程的当前状态(寄存器、指令指针等),并且控制权不返回给它。取而代之的是,从等待分配 CPU 的进程列表中选择另一个进程,而被中断的进程将排到队列的后面。

睡眠进程根本不在等待 CPU 的队列中。相反,它存储在睡眠队列中。每当内核收到定时器中断时,就会检查睡眠队列,并将时间到的进程转移到“等待CPU”队列中。

当然,这是一个粗略的简化。它需要非常复杂的算法来确保安全性、公平性、平衡、优先级、防止饥饿,并以最少的内存用于内核数据来快速完成所有操作。

于 2008-10-07T15:22:14.877 回答
36

有一个称为睡眠队列的内核数据结构。这是一个优先队列。每当一个进程加入到睡眠队列中时,都会计算最快被唤醒的进程的过期时间,并设置一个定时器。此时,过期的作业从队列中取出,进程恢复执行。

(有趣的琐事:在较旧的 unix 实现中,有一个进程队列,其中 fork() 已被调用,但尚未为其创建子进程。它当然被称为fork queue。)

于 2008-10-06T19:51:49.917 回答
16

也许操作系统的主要工作是向应用程序编写者隐藏真实硬件的复杂性。因此,任何关于操作系统如何工作的描述都有变得非常复杂、非常快的风险。因此,我不会处理真正的操作系统需要处理的所有“假设”和“但是”。我只是要在较高的概念级别上描述一个过程是什么,什么是调度器,定时器队列是如何工作的。希望这会有所帮助。

什么是流程:

将一个进程——让我们只讨论进程,稍后再讨论线程——想象为“操作系统调度的东西”。一个进程有一个 ID——想想一个整数——你可以把这个整数看作是一个包含该进程所有上下文的表的索引。

上下文是硬件信息——寄存器、内存管理单元内容、其他硬件状态——当加载到机器中时,将允许进程“运行”。上下文还有其他组件——打开文件的列表、信号处理程序的状态,以及最重要的是进程正在等待的东西

进程花费大量时间休眠(又名等待)

进程花费大量时间等待。例如,读取或写入磁盘的进程将花费大量时间等待数据到达或确认已在磁盘上输出。操作系统人员使用术语“等待”和“睡眠”(和“阻塞”)在某种程度上可以互换 - 所有这一切都意味着该进程正在等待某些事情发生,然后才能继续以愉快的方式继续。令人困惑的是,操作系统 API sleep() 恰好使用底层操作系统机制来休眠进程。

进程可能正在等待其他事情:例如,网络数据包到达、窗口选择事件或计时器到期。

流程和调度

正在等待的进程被称为non-runnable。它们不会进入操作系统的运行队列。但是当进程正在等待的事件发生时,它会导致操作系统将进程从不可运行状态移动到可运行状态。同时,操作系统将进程放在运行队列上,这实际上不是一个队列——它更像是所有进程的一堆,如果操作系统决定这样做,它们可以运行。

调度:

操作系统定期决定应该运行哪些进程。操作系统决定这样做的算法被称为调度算法,有点不足为奇。调度算法的范围从非常简单(“每个人运行 10 毫秒,然后队列中的下一个人开始运行”)到更复杂的(考虑到进程优先级、执行频率、运行时截止日期、进程间依赖、链锁和各种其他复杂的主题)。

定时器队列 一台计算机内部有一个定时器。有很多方法可以实现,但经典的方法称为周期性计时器。定期计时器以固定的时间间隔滴答作响——在今天的大多数操作系统中,我相信这个速率是每秒 100 次——100 Hz——每 10 毫秒。我将在后面使用该值作为具体速率,但要知道大多数值得一提的操作系统都可以配置不同的滴答声——而且许多操作系统不使用这种机制,并且可以提供更好的计时器精度。但我离题了。

每个滴答都会导致操作系统中断。

当操作系统处理这个定时器中断时,它会将其系统时间的概念再增加 10 毫秒。然后,它查看计时器队列并决定需要处理该队列上的哪些事件。

定时器队列实际上一个“需要处理的事情”的队列,我们​​称之为事件。此队列按到期时间排序,最早的事件在前。

“事件”可以是“唤醒进程 X”,或者“去那里踢磁盘 I/O,因为它可能已经卡住了”,或者“在那边的光纤通道链路上发送一个 keepalive 数据包”。无论操作系统需要做什么。

当你有一个以这种方式排序的队列时,很容易管理出队。操作系统只是查看队列的头部,并在每个滴答声中将事件的“到期时间”递减 10 毫秒。当到期时间变为零时,操作系统会将该事件出列,并执行任何需要的操作。

在休眠进程的情况下,它只是使进程再次可运行。

很简单吧?

于 2008-10-07T01:34:44.733 回答
10

至少有两个不同的层次来回答这个问题。(还有很多其他会混淆的东西,我不会碰它们)

  1. 应用程序级别,这就是 C 库所做的。这是一个简单的操作系统调用,它只是告诉操作系统在时间过去之前不要给这个进程提供 CPU 时间。操作系统有一个挂起的应用程序队列,以及一些关于它们在等待什么的信息(通常是时间,或者某些数据出现在某处)。

  2. 内核级别。当操作系统现在无事可做时,它会执行“hlt”指令。该指令不执行任何操作,但它永远不会自行完成。当然,硬件中断是正常服务的。简而言之,操作系统的主循环看起来像这样(从非常非常远的地方):

    允许中断();
    而(真){
      hlt;
      check_todo_queues ();
    }
    

    中断处理程序简单地将东西添加到待办事项队列中。实时时钟被编程为周期性地(以固定的速率)产生中断,或者在下一个进程想要被唤醒时在未来的某个固定时间产生中断。

于 2008-10-06T20:05:35.180 回答
9

多任务操作系统有一个称为调度程序的组件,该组件负责将 CPU 时间分配给线程,调用 sleep 告诉操作系统在一段时间内不要将 CPU 时间分配给该线程。

有关完整的详细信息,请参阅http://en.wikipedia.org/wiki/Process_states

于 2008-10-06T19:53:57.417 回答
8

我对 Linux 一无所知,但我可以告诉你在 Windows 上会发生什么。

Sleep() 导致进程的时间片立即结束以将控制权返回给操作系统。操作系统然后设置一个计时器内核对象,该对象在时间过去后收到信号。在内核对象收到信号之前,操作系统将不再给该进程时间。即使这样,如果其他进程具有更高或相同的优先级,它可能仍会等待一段时间才能让进程继续。

操作系统使用特殊的 CPU 机器代码来进行进程切换。这些函数不能被用户模式代码访问,因此它们只能通过对操作系统的 API 调用来访问。

于 2008-10-06T19:56:22.187 回答
5

本质上,是的,有一个“特殊的小玩意儿”——它的重要性远不止 sleep()。

传统上,在 x86 上,这是一个 Intel 8253 或 8254“可编程间隔定时器”。在早期的 PC 中,这是主板上的一个独立芯片,可由 CPU 编程以在预设时间间隔后断言中断(通过“可编程中断控制器”,另一个独立芯片)。该功能仍然存在,尽管它现在只是更大的主板电路的一小部分。

今天的操作系统仍然对 PIT 进行编程以定期唤醒它(在最新版本的 Linux 中,默认为每毫秒一次),这就是内核能够实现抢先式多任务处理的方式。

于 2009-07-21T04:05:50.767 回答
2

glibc 2.21 Linux

转发到nanosleep系统调用。

glibc 是大多数 Linux 桌面发行版上 C 标准库的默认实现。

如何找到它:第一反应是:

git ls-files | grep sleep

这包含:

sysdeps/unix/sysv/linux/sleep.c

我们知道:

sysdeps/unix/sysv/linux/

包含 Linux 细节。

在该文件的顶部,我们看到:

/* We are going to use the `nanosleep' syscall of the kernel.  But the
   kernel does not implement the stupid SysV SIGCHLD vs. SIG_IGN
   behaviour for this syscall.  Therefore we have to emulate it here.  */
unsigned int
__sleep (unsigned int seconds)

因此,如果您相信评论,我们基本上就完成了。

在底部:

 weak_alias (__sleep, sleep)

这基本上说__sleep== sleep。该函数nanosleep通过:

result = __nanosleep (&ts, &ts);

greppingg之后:

git grep nanosleep | grep -v abilist

我们得到了一小部分有趣的事件,我认为__nanosleep定义在:

sysdeps/unix/sysv/linux/syscalls.list 

在线上:

nanosleep   -   nanosleep   Ci:pp   __nanosleep nanosleep

这是一些由以下内容解析的超级 DRY 魔术格式:

sysdeps/unix/make-syscalls.sh

然后从构建目录:

grep -r __nanosleep

引导我们:/sysd-syscalls这是make-syscalls.sh生成和包含的内容:

#### CALL=nanosleep NUMBER=35 ARGS=i:pp SOURCE=-
ifeq (,$(filter nanosleep,$(unix-syscalls)))
unix-syscalls += nanosleep
$(foreach p,$(sysd-rules-targets),$(foreach o,$(object-suffixes),$(objpfx)$(patsubst %,$p,nanosleep)$o)): \
        $(..)sysdeps/unix/make-syscalls.sh
    $(make-target-directory)
    (echo '#define SYSCALL_NAME nanosleep'; \
     echo '#define SYSCALL_NARGS 2'; \
     echo '#define SYSCALL_SYMBOL __nanosleep'; \
     echo '#define SYSCALL_CANCELLABLE 1'; \
     echo '#include <syscall-template.S>'; \
     echo 'weak_alias (__nanosleep, nanosleep)'; \
     echo 'libc_hidden_weak (nanosleep)'; \
    ) | $(compile-syscall) $(foreach p,$(patsubst %nanosleep,%,$(basename $(@F))),$($(p)CPPFLAGS))
endif

它看起来像是 Makefile 的一部分。git grep sysd-syscalls表明它包含在:

sysdeps/unix/Makefile:23:-include $(common-objpfx)sysd-syscalls 

compile-syscall看起来像关键部分,所以我们发现:

# This is the end of the pipeline for compiling the syscall stubs.
# The stdin is assembler with cpp using sysdep.h macros.
compile-syscall = $(COMPILE.S) -o $@ -x assembler-with-cpp - \
                   $(compile-mkdep-flags)

请注意,这-x assembler-with-cpp是一个gcc选项。

#defines 参数如:

#define SYSCALL_NAME nanosleep

然后在以下位置使用它们:

#include <syscall-template.S>

好的,这就是我现在要进行的宏扩展游戏。

我认为这会生成posix/nanosleep.o必须与所有内容链接在一起的文件。

Linux 4.2 x86_64 nanosleep 系统调用

使用调度程序:这不是一个忙碌的睡眠。

搜索标签:

sys_nanosleep

引导我们kernel/time/hrtimer.c

SYSCALL_DEFINE2(nanosleep, struct timespec __user *, rqtp,

hrtimer代表高分辨率计时器。从那里主线看起来像:

  • hrtimer_nanosleep
  • do_nanosleep
    • set_current_state(TASK_INTERRUPTIBLE);这是可中断的睡眠
    • freezable_schedule();它调用schedule()并允许其他进程运行
  • hrtimer_start_expires
  • hrtimer_start_range_ns
  • TODO:达到arch/x86时序级别
  • TODO:上述步骤是直接在系统调用中断处理程序中完成的,还是在常规内核线程中完成的?

关于它的几篇文章:

于 2015-11-10T20:39:20.403 回答