C语言 sleep()背后的算法是什么?

pnwntuvh  于 2023-01-29  发布在  其他
关注(0)|答案(8)|浏览(131)

现在有件事我一直想知道:sleep()是如何实现的?
如果这一切都是关于使用操作系统中的API,那么API是如何制作的呢?
这一切都归结为在CPU上使用特殊的机器码吗?CPU是否需要一个特殊的协处理器或其他小发明,没有它们就不能使用sleep()?
sleep()最著名的体现是在C语言中(更准确地说,是在C编译器附带的库中,比如GNU的libc),尽管今天几乎每种语言都有它的等价物,但在某些语言中(想想Bash),sleep的实现并不是我们在这个问题中所关注的...
编辑:在阅读了一些答案之后,我看到流程被放置在等待队列中。从那里,我可以猜测两种选择
1.设置计时器,以便内核在到期时间唤醒进程,或者
1.每当内核被允许一个时间片时,它轮询时钟以检查是否到了唤醒进程的时间。
答复只提到备选方案1,因此,我要问:这个计时器是如何工作的?如果它是一个简单的中断,让内核唤醒进程,内核怎么能要求计时器“在140毫秒内唤醒我,这样我就可以把进程置于运行状态”?

isr3a4wc

isr3a4wc1#

问题的“更新”显示了对现代操作系统如何工作的一些误解。
内核是不被“允许”有时间片的。内核是给用户进程分配时间片的东西。“计时器”不是用来唤醒休眠进程的,而是用来停止当前正在运行的进程的。
本质上,内核试图通过停止占用CPU时间过长的进程来公平地分配CPU时间。简单来说,我们假设不允许任何进程使用CPU超过2毫秒。因此,内核会将计时器设置为2毫秒,并让进程运行。当计时器触发中断时,内核获得控制权。它保存运行进程的当前状态(寄存器、指令指针等),并且控制权不返回给它。相反,从等待被分配CPU的进程列表中选择另一个进程,并且被中断的进程转到队列的后面。
休眠进程只是简单地“不”在等待CPU的队列中。相反,它被存储在休眠队列中。每当内核收到定时器中断时,就会检查休眠队列,时间已到的进程被转移到“等待CPU”队列中。
当然,这是一个粗略的简化,它需要非常复杂的算法来确保安全性、公平性、平衡性、优先级、防止饥饿,并以最小的内存量快速完成这一切。

yvt65v4c

yvt65v4c2#

有一个内核数据结构叫做睡眠队列,它是一个优先级队列,每当有进程加入睡眠队列时,就会计算最快被唤醒的进程的到期时间,并设置一个计时器,到那时,到期的作业就会从队列中取出,进程会继续执行。
(有趣的琐事:在较早的unix实现中,有一个队列用于已经被调用fork()的进程,但是子进程还没有被创建。2它当然被称为***fork队列***。
啊!

dfuffjeb

dfuffjeb3#

也许操作系统的主要工作是向应用程序编写者隐藏真实的硬件的复杂性。因此,任何关于操作系统如何工作的描述都有可能变得非常复杂,非常快。因此,我不打算讨论真实操作系统需要处理的所有“如果”和“是的但是”。我只打算从概念的高度来描述:什么是进程,调度程序做什么,计时器队列如何工作。希望这对你有帮助。

什么是流程:

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

  • 上下文 * 是硬件信息--寄存器、内存管理单元内容、其他硬件状态--当加载到机器中时,将允许进程“运行”。上下文还有其他组件--打开文件的列表、信号处理程序的状态,以及这里最重要的,* 进程正在等待的东西 *。
    进程花费大量时间休眠(即等待)

进程花费大量时间等待。例如,读或写磁盘的进程将花费大量时间等待数据到达或确认数据已离开磁盘。2操作系统人员使用术语“等待”和“休眠”(和“阻塞”)有些可互换--所有这些都意味着进程在继续它的快乐之路之前正在等待某些事情发生。令人困惑的是,OS API sleep()碰巧使用底层OS机制来休眠进程。
进程可以等待其他事情:例如,要到达的网络分组、窗口选择事件或要期满的定时器。

流程和计划

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

日程安排:

操作系统以固定的时间间隔决定哪些进程应该运行。操作系统决定运行的算法称为调度算法,这并不奇怪。调度算法的范围从非常简单的(“每个人都可以跑10毫秒,然后队列中的下一个人可以跑”)变得复杂得多(考虑进程优先级、执行频率、运行时截止期、进程间依赖性、连锁锁和各种其他复杂主题)。

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

每个滴答声都会导致操作系统中断。
当操作系统处理此定时器中断时,它会将系统时间的概念再增加10 ms。然后,它会查看定时器队列,并决定需要处理该队列中的哪些事件。
计时器队列实际上是一个“需要处理的事情”的队列,我们称之为事件。这个队列是按照到期时间排序的,最快的事件排在最前面。
“事件”可以是“唤醒进程X”,或者“去踢那边的磁盘I/O,因为它可能卡住了”,或者“在那边的光纤通道链路上发送一个保活数据包”,无论操作系统需要做什么。
当你有一个以这种方式排序的队列时,很容易管理出队。操作系统简单地查看队列的头部,并在每个滴答声中将事件的“到期时间”减少10毫秒。当到期时间变为零时,操作系统将该事件出队,并执行任何调用的操作。
在进程处于休眠状态的情况下,它只是使进程再次可运行。
很简单,是吧?

nzrxty8p

nzrxty8p4#

这个问题至少有两个不同的层次来回答。(还有很多其他与此混淆的东西,我就不提了)
1.这是一个应用程序级的,这就是C库所做的。这是一个简单的操作系统调用,它只是告诉操作系统不要给予这个进程CPU时间,直到时间过去。操作系统有一个暂停的应用程序队列,以及一些关于它们在等待什么的信息(通常是时间,或者一些数据出现在某处)。
1.内核级。当操作系统现在没有任何事情要做时,它会执行一条'hlt'指令。这条指令什么也不做,但它永远不会自己完成。当然,硬件中断是正常服务的。简单地说,操作系统的主循环看起来像这样(从非常非常远的地方看):

allow_interrupts ();
while (true) {
  hlt;
  check_todo_queues ();
}

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

rxztt3cl

rxztt3cl5#

多任务操作系统有一个叫做调度器的组件,这个组件负责给线程分配CPU时间,调用sleep告诉操作系统在一段时间内不要给这个线程给予CPU时间。
有关完整详细信息,请参见http://en.wikipedia.org/wiki/Process_states

sqxo8psd

sqxo8psd6#

我对Linux一无所知,但我可以告诉你Windows上发生了什么。
Sleep()使进程的时间片立即结束,将控制权交还给操作系统。然后操作系统设置一个计时器内核对象,在时间过去后收到信号。操作系统将不再给予该进程任何时间,直到内核对象收到信号。即使这样,如果其他进程具有更高或相同的优先级,它仍可能等待一段时间才让进程继续。
操作系统使用特殊的CPU机器码来进行进程切换。这些函数不能被用户态代码访问,所以它们只能通过API调用进入操作系统。

ua4mk5z4

ua4mk5z47#

从本质上说,是的,有一个“特殊的小发明”-它的重要性远远超过睡眠()。
传统上,在x86上,这是一个Intel 8253或8254“可编程间隔定时器”。在早期的PC上,这是主板上的一个单独的芯片,可以由CPU编程来Assert中断(通过“可编程中断控制器”,另一个分立芯片)。功能仍然存在,尽管它现在是主板电路的大得多的块中的极小部分。
今天的操作系统仍然对PIT进行编程,使其定期唤醒(在Linux的最新版本中,默认为每毫秒一次),这就是内核能够实现抢占式多任务的原因。

cczfrluj

cczfrluj8#

    • glibc 2.21 Linux操作系统**

转发到nanosleep系统调用。
glibc是大多数Linux桌面发行版上C stdlib的默认实现。
如何找到它:第一React是:

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选项。
#define的参数如下:

#define SYSCALL_NAME nanosleep

然后将其用于:

#include <syscall-template.S>

好的,这就是我现在要继续的宏观扩展游戏。
我认为这会生成posix/nanosleep.o文件,该文件必须与所有内容链接在一起。

    • Linux 4.2 x86_64纳米休眠系统调用**

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

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()并允许其他进程运行
  • x1米20英寸1x
  • hrtimer_start_range_ns
  • TODO:达到arch/x86计时级别
  • TODO:以上步骤是直接在syscal调用中断处理程序中完成的,还是在常规内核线程中完成的?

关于它的几篇文章:

相关问题