Linux中延时/暂停函数(sleep/usleep/nanosleep/select)的比较、底层实现说明
Linux中延时/暂停函数(sleep/usleep/nanosleep/select)的比较、底层实现说明
本来只是要搞清楚Linux下如何实现延时和暂停,但无意中看到一篇文章介绍了其实现,帮自己窥得一点底层原理。 知其然还要知其所以然,但自己没有这个储备和能力来研究Linux内核实现,特地转载留存。
1、sleep的精度是秒
2、usleep的精度是微妙,不精确
3、select的精度是微妙,精确
struct timevaldelay;
delay.tv_sec =0;
delay.tv_usec =20 * 1000; // 20 ms
select(0, NULL,NULL, NULL, &delay);
4、nanosleep的精度是纳秒,不精确
unix、linux系统尽量不要使用usleep和sleep而应该使用nanosleep,使用nanosleep应注意判断返回值和错误代码,否则容易造成cpu占用率100%。
无论是WinCE还是Linux操作系统,应用线程的运行总是涉及到两个基本的参数:一个是系统分配给线程的时间片,一个是系统调度的时间间隔。Linux和WinCE下这两个参数有所不同,如下表所示:
WinCE
嵌入式Linux
线程的运行时间片
100ms
10ms
系统调度间隔
1ms
10ms
方式一、 (sleep, usleep,nanosleep)本质上都是系统调用,但是精确程度不一样,依次递增。
方式二、nice函数
功能描述
改变进程优先级,也就是改变进程执行的优先顺序。
函数定义
int nice(int inc);
返回值
成功执行时,返回新的nice值。失败返回-1
参数介绍
inc数值越大则优先级越低(进程执行慢),超级用户可以使用负的inc 值,使优先顺序靠前,进程执行较快。nice的取值范围可参考getpriority的描述。
方式三、
排程或译排班,是将任务分配至资源的过程,在计算机或生产处理中尤为重要。
排班首要面对的就是效率问题。以数学而言,排班问题通常就是最佳化问题。以航空公司为例,使用机场每个登机口皆需计时付费,「分配登机口」就是一项任务,而「登机口」就是可供利用的资源,若将登机口使用数量及时间压到最低,亦即能节省最多的成本。
将排班多元程式规划系统的主要目的,是随时保有一个行程在执行,藉以提高cpu使用率。事实上,行程就是一种任务,可利用的资源即是cpu。若能最有效率完成运算,对使用者而言就不必久候。
具体实现就是,使用死循环,消耗cpu;或者其他的方式使cpu按序使用。
--------------------------------我是分割线------------------------------------------
nanosleep函数
int nanosleep(const struct timespec *req, struct timespec *rem);
struct timespec
{
time_t tv_sec; /* seconds /
long tv_nsec; / nanoseconds */
};
这个函数功能是暂停某个进程直到你规定的时间后恢复,参数req就是你要暂停的时间,其中req->tv_sec是以秒为单位,而tv_nsec以毫微秒为单位(10的-9次方秒)。由于调用nanosleep是是进程进入TASK_INTERRUPTIBLE,这种状态是会相应信号而进入TASK_RUNNING状态的,这就意味着有可能会没有等到你规定的时间就因为其它信号而唤醒,此时函数返回-1,切还剩余的时间会被记录在rem中。
看到这里刚刚看到他的实现是:将其状态设置成TASK_INTERRUPTIBLE,脱离就绪队列,然后进行一次进程调度再由内核在规定的时间后发送信号来唤醒这个进程。
在我刚开始学习编程时候,那时候我也曾试图使上下2条指令相隔一定时间来运行,那时我的做法是在这2条指令之间加上了一个400次的循环。这也算一种实现方式,我管它叫作延迟,但没有利用进程休眠来实现的好。但有一种特殊情况,使用休眠就无法实现了。
我们知道这里肯定脱离不了时钟中断,没有时钟中断的计时我们是无法实现这一功能的。那么假设时钟种中断是10毫秒一次(这种CPU还是有的),那么我们可以看到在函数调用的时候我们可以以毫微秒来暂停,如果我tv_sec = 0, tv_nsec = 2,那么时钟中断一定是在10微秒后来唤醒这个进程的,如果非实时性任务差个8微秒估计没什么大不了,不幸的是Linux支持实时性任务SCHED_FIFO和SCHED_RR.(我们以前谈到过)。
这时8微秒的差距就是不能容忍了,这是就不能靠休眠和时钟中断来实现了,这是linux采用就是延迟办法,执行一个循环来达到暂停的目的。
这2种实现的差别就是休眠实现的话,进程会进入休眠状态,而延迟实现的话,CPU是在执行循环不会进入休眠态。所以可以说虽然名为nanosleep,但它不一定会使进程进入sleep状态,当然不进入sleep 态的条件太苛刻(没多少人会写实时任务,且还是暂停要小于CPU时钟频率,加上现在CPU的频率是如此之高,这种情况一般发生在要求外设中断不小于某个特定值,而且应该是采用比较老的CPU或者嵌入式中)。
----------------------------------我是分割线------------------------------------
首先, 我会说不保证你在使用者模式 (user-mode) 中执行的行程 (process) 能够精确地控制时序因为 Linux 是个多工的作业环境. 你在执行中的行程 (process) 随时会因为各种原因被暂停大约 10 毫秒到数秒 (在系统负荷非常高的时候). 然而, 对於大多数使用 I/O 埠的应用而言, 这个延迟时间实际上算不了什麽. 要缩短延迟时间, 你得使用函式 nice 将你在执行中的行程 (process ) 设定成高优先权(请参考 nice(2) 使用说明文件) 或使用即时排程法 (real-time scheduling) (请看下面).
如果你想获得比在一般使用者模式 (user-mode) 中执行的行程 (process) 还要精确的时序, 有一些方法可以让你在使用者模式 (user-mode) 中做到 `即时’ 排程的支援. Linux 2.x 版本的核心中有软体方式的即时排程支援; 详细的说明请参考 sched_setscheduler(2) 使用说明文件. 有一个特殊的核心支援硬体的即时排程;
(Sleeping) : sleep() 与 usleep()
现在, 让我们开始较简单的时序函式呼叫. 想要延迟数秒的时间, 最佳的方法大概 是使用函式 sleep() . 想要延迟至少数十毫秒的时间 (10 ms 似乎已是最短的 延迟时间了), 函式 usleep() 应该可以使用. 这些函式是让出 CPU 的使用权 给其他想要执行的行程 (processes) (``自己休息去了’’), 所以没有浪费掉 CPU 的时间. 细节请参考 sleep(3) 与 usleep(3) 的说明文件.
如果让出 CPU 的使用权因而使得时间延迟了大约 50 毫秒 (这取决於处理器与机器的速度, 以及系统的负荷), 就浪费掉 CPU 太多的时间, 因为 Linux 的排程器 (scheduler) (单就 x86 架构而言) 在将控制权发还给你的行程 (process) 之前通常至少要花费 10-30 毫秒的时间. 因此, 短时间的延迟, 使用函式 usleep(3) 所得到的延迟结果通常会大於你在参数所指定的值, 大约至少有 10 ms.
nanosleep()
在 Linux 2.0.x 一系列的核心发行版本中, 有一个新的系统呼叫 (system call), nanosleep() (请参考 nanosleep(2) 的说明文件), 他让你能够 休息或延迟一个短的时间 (数微秒或更多).
如果延迟的时间 <= 2 ms, 若(且唯若)你执行中的行程 (process) 设定了软体的即时 排程 (就是使用函式 tt/sched_setscheduler()/), 呼叫函式 nanosleep() 时 不是使用一个忙碌回圈来延迟时间; 就是会像函式 usleep() 一样让出 CPU 的使用权休息去了.
这个忙碌回圈使用函式 udelay() (一个驱动程式常会用到的核心内部的函式) 来达成, 并且使用 BogoMips 值 (BogoMips 可以准确量测这类忙碌回圈的速度) 来计算回圈延迟的时间长度. 其如何动作的细节请参考 /usr/include/asm/delay.h).
使用 I/O 埠来延迟时间—针对不同的架构,有不同的系统调用
另一个延迟数微秒的方法是使用 I/O 埠. 就是从埠位址 0x80 输入或输出任何 byte 的资料 (请参考前面) 等待的时间应该几乎只要 1 微秒这要看你的处理器的型别与速度. 如果要延迟数微秒的时间你可以将这个动作多做几次. 在任何标准的机器上输出资料到该 埠位址应该不会有不良的後果□对 (而且有些核心的设备驱动程式也在使用他). {in|out}[bw]_p() 等函式就是使用这个方法来产生时间延迟的 (请参考档案 asm/io.h).
实际上, 一个使用到埠位址□围为 0-0x3ff 的 I/O 埠指令几乎只要 1 微秒的时间, 所以如果你要如此做, 例如, 直接使用并列埠, 只要加上几个 inb() 函式从该 埠位址□围读入 byte 的资料即可.
使用组合语言来延迟时间----cpu不释放控制权,即进入死循环,实现实时延迟
如果你知道执行程式所在机器的处理器型别与时钟速度, 你可以执行某些组合语言指令以便获得较短的延迟时间 (但是记住, 你在执行中的行程 (process) 随时会被暂停, 所以有时延迟的时间会比实际长). 如下面的表格所示, 内部处理器的速度决定了所要使用的时钟周期数; 如, 一个 50 MHz 的处理器 (486DX-50 或 486DX2-50), 一个时钟周期要花费 1/50000000 秒 (=200 奈秒).
指令 i386 时钟周期数 i486 时钟周期数nop 3 1xchg %ax,%ax 3 3or %ax,%ax 2 1mov %ax,%ax 2 1add %ax,0 2 1
(对不起, 我不知道 Pentiums 的资料, 或许与 i486 接近吧. 我无法在 i386 的资料上找到只花费一个时钟周期的指令. 如果能够就请使用花费一个时钟周期的指令, 要不然就使用管线技术的新式处理器也是可以缩短时间的.)
上面的表格中指令 nop 与 xchg 应该不会有不良的後果. 指令最後可能会 改变旗号暂存器的内容, 但是这没关系因为 gcc 会处理. 指令 nop 是个好的选择.
想要在你的程式中使用到这些指令, 你得使用 asm(“instruction”). 指令的语法就如同上面表格的用法; 如果你想要在单一的 asm() 叙述中使用多个指令, 可以使用分号将他们隔开. 例如, asm(“nop ; nop ; nop ; nop”) 会执行四个 nop 指令, 在 i486 或 Pentium 处理器中会延迟四个时钟周期 (或是 i386 会延迟 12 个时钟周期).
gcc 会将 asm() 翻译成单行组合语言程式码, 所以不会有呼叫函式的负荷.
在 Intel x86 架构中不可能有比一个时钟周期还短的时间延迟.
在 Pentiums 处理器上使用函式 rdtsc
对於 Pentiums 处理器而言, 你可以使用下面的 C 语言程式码来取得自从上次重新开机 到现在经过了多少个时钟周期:
--------------------------------------------------------------------------------
extern inline unsigned long long int rdtsc() { unsigned long long int x; asm volatile (".byte 0x0f, 0x31" : “=A” (x)); return x; }
--------------------------------------------------------------------------------
你可以询问参考此值以便延迟你想要的时钟周期数.
想要时间精确到一秒钟, 使用函式 time() 或许是最简单的方法. 想要时间更精确, 函式 gettimeofday() 大约可以精确到微秒 (但是如前所述会受到 CPU 排程的影响). 至於 Pentiums 处理器, 使用上面的程式码片断就可以精确到一个时钟周期.
如果你要你执行中的行程 (process) 在一段时间到了之後能够被通知 (get a signal), 你得使用函式 setitimer() 或 alarm() . 细节请参考函式的使用说明文件.
更多推荐
所有评论(0)