linux 内核长延时方法

2023-12-24 23:55:36

? ?忙等待

如果你想延时执行多个时钟嘀哒, 允许在值中某些疏忽, 最容易的( 尽管不推荐 ) 的实
现是一个监视 jiffy 计数器的循环. 这种忙等待实现常常看来象下面的代码, 这里 j1
是 jiffies 的在延时超时的值:
while (time_before(jiffies, j1))
cpu_relax();
对 cpu_relex 的调用使用了一个特定于体系的方式来说, 你此时没有在用处理器做事情.
在许多系统中它根本不做任何事; 在对称多线程(" 超线程" ) 系统中, 可能让出核心给
其他线程. 在如何情况下, 无论何时有可能, 这个方法应当明确地避免. 我们展示它是因
为偶尔你可能想运行这个代码来更好理解其他代码的内幕.
我们来看一下这个代码如何工作. 这个循环被保证能工作因为 jiffies 被内核头文件声
明做易失性的, 并且因此, 在任何时候 C 代码寻址它时都从内存中获取. 尽管技术上正
确( 它如同设计的一样工作 ), 这种忙等待严重地降低了系统性能. 如果你不配置你的内
核为抢占操作, 这个循环在延时期间完全锁住了处理器; 调度器永远不会抢占一个在内核
中运行的进程, 并且计算机看起来完全死掉直到时间 j1 到时. 这个问题如果你运行一个
可抢占的内核时会改善一点, 因为, 除非这个代码正持有一个锁, 处理器的一些时间可以
被其他用途获得. 但是, 忙等待在可抢占系统中仍然是昂贵的.
更坏的是, 当你进入循环时如果中断碰巧被禁止, jiffies 将不会被更新, 并且 while
条件永远保持真. 运行一个抢占的内核也不会有帮助, 并且你将被迫去击打大红按钮.

这个延时代码的实现可拿到, 如同下列的, 在 jit 模块中. 模块创建的这些 /proc/jit*
文件每次你读取一行文本就延时一整秒, 并且这些行保证是每个 20 字节. 如果你想测试
忙等待代码, 你可以读取 /proc/jitbusy, 每当它返回一行它忙-循环一秒.
为确保读, 最多, 一行( 或者几行 ) 一次从 /proc/jitbusy. 简化的注册 /proc 文件的
内核机制反复调用 read 方法来填充用户请求的数据缓存. 因此, 一个命令, 例如 cat
/proc/jitbusy, 如果它一次读取 4KB, 会冻住计算机 205 秒.
推荐的读 /proc/jitbusy 的命令是 dd bs=200 < /proc/jitbusy, 可选地同时指定块数
目. 文件返回的每 20-字节 的行表示 jiffy 计数器已有的值, 在延时之前和延时之后.
这是一个例子运行在一个其他方面无负担的计算机上:
phon% dd bs=20 count=5 < /proc/jitbusy
1686518 1687518
1687519 1688519
1688520 1689520
1689520 1690520
1690521 1691521
看来都挺好: 延时精确地是 1 秒 ( 1000 jiffies ), 并且下一个 read 系统调用在上一
个结束后立刻开始. 但是让我们看看在一个有大量 CPU-密集型进程在运行(并且是非抢占
内核)的系统上会发生什么:
phon% dd bs=20 count=5 < /proc/jitbusy
1911226 1912226
1913323 1914323
1919529 1920529
1925632 1926632
1931835 1932835
这里, 每个 read 系统调用精确地延时 1 秒, 但是内核耗费多过 5 秒在调度 dd 进程以
便它可以发出下一个系统调用之前. 在一个多任务系统就期望是这样; CPU 时间在所有运
行的进程间共享, 并且一个 CPU-密集型 进程有它的动态减少的优先级. ( 调度策略的讨
论在本书范围之外).
上面所示的在负载下的测试已经在运行 load50 例子程序中进行了. 这个程序派生出许多
什么都不做的进程, 但是以一种 CPU-密集的方式来做. 这个程序是伴随本书的例子文件
的一部分, 并且缺省是派生 50 个进程, 尽管这个数字可以在命令行指定. 在本章, 以及
在本书其他部分, 使用一个有负载的系统的测试已经用 load50 在一个其他方面空闲的计
算机上运行来进行了.
如果你在运行一个可抢占内核时重复这个命令, 你会发现没有显著差别在一个其他方面空
闲的 CPU 上以及下面的在负载下的行为:
phon% dd bs=20 count=5 < /proc/jitbusy
14940680 14942777
14942778 14945430

14945431 14948491
14948492 14951960
14951961 14955840
这里, 没有显著的延时在一个系统调用的末尾和下一个的开始之间, 但是单独的延时远远
比 1 秒长: 直到 3.8 秒在展示的例子中并且随时间上升. 这些值显示了进程在它的延时
当中被中断, 调度其他的进程. 系统调用之间的间隙不是唯一的这个进程的调度选项, 因
此没有特别的延时在那里可以看到.

?让出处理器

如我们已见到的, 忙等待强加了一个重负载给系统总体; 我们乐意找出一个更好的技术.
想到的第一个改变是明确地释放 CPU 当我们对其不感兴趣时. 这是通过调用调度函数而
实现地, 在 <linux/sched.h> 中声明:
while (time_before(jiffies, j1)) {
schedule();
}
这个循环可以通过读取 /proc/jitsched 如同我们上面读 /proc/jitbusy 一样来测试.
但是, 还是不够优化. 当前进程除了释放 CPU 不作任何事情, 但是它保留在运行队列中.
如果它是唯一的可运行进程, 实际上它运行( 它调用调度器来选择同一个进程, 进程又调
用调度器, 这样下去). 换句话说, 机器的负载( 在运行的进程的平均数 ) 最少是 1, 并
且空闲任务 ( 进程号 0, 也称为对换进程, 由于历史原因) 从不运行. 尽管这个问题可
能看来无关, 在计算机是空闲时运行空闲任务减轻了处理器工作负载, 降低它的温度以及
提高它的生命期, 同时电池的使用时间如果这个计算机是你的膝上机. 更多的, 因为进程
实际上在延时中执行, 它所耗费的时间都可以统计.
/proc/jitsched 的行为实际上类似于运行 /proc/jitbusy 在一个抢占的内核下. 这是一
个例子运行, 在一个无负载的系统:
phon% dd bs=20 count=5 < /proc/jitsched
1760205 1761207
1761209 1762211
1762212 1763212
1763213 1764213
1764214 1765217
有趣的是要注意每次 read 有时结束于等待比要求的多几个时钟嘀哒. 这个问题随着系统
变忙会变得越来越坏, 并且驱动可能结束于等待长于期望的时间. 一旦一个进程使用调度
来释放处理器, 无法保证进程将拿回处理器在任何时间之后. 因此, 以这种方式调用调度
器对于驱动的需求不是一个安全的解决方法, 另外对计算机系统整体是不好的. 如果你在
运行 load50 时测试 jitsched, 你可以见到关联到每一行的延时被扩充了几秒, 因为当
定时超时的时候其他进程在使用 CPU .

超时

到目前为止所展示的次优化的延时循环通过查看 jiffy 计数器而不告诉任何人来工作.
但是最好的实现一个延时的方法, 如你可能猜想的, 常常是请求内核为你做. 有 2 种方
法来建立一个基于 jiffy 的超时, 依赖于是否你的驱动在等待其他的事件.
如果你的驱动使用一个等待队列来等待某些其他事件, 但是你也想确保它在一个确定时间
段内运行, 可以使用 wait_event_timeout 或者 wait_event_interruptible_timeout:
#include <linux/wait.h>
long wait_event_timeout(wait_queue_head_t q, condition, long timeout);
long wait_event_interruptible_timeout(wait_queue_head_t q, condition, long
timeout);
这些函数在给定队列上睡眠, 但是它们在超时(以 jiffies 表示)到后返回. 因此, 它们
实现一个限定的睡眠不会一直睡下去. 注意超时值表示要等待的 jiffies 数, 不是一个
绝对时间值. 这个值由一个有符号的数表示, 因为它有时是一个相减运算的结果, 尽管这
些函数如果提供的超时值是负值通过一个 printk 语句抱怨. 如果超时到, 这些函数返回
0; 如果这个进程被其他事件唤醒, 它返回以 jiffies 表示的剩余超时值. 返回值从不会
是负值, 甚至如果延时由于系统负载而比期望的值大.
/proc/jitqueue 文件展示了一个基于 wait_event_interruptible_timeout 的延时, 结
果这个模块没有事件来等待, 并且使用 0 作为一个条件:
wait_queue_head_t wait;
init_waitqueue_head (&wait);
wait_event_interruptible_timeout(wait, 0, delay);
当读取 /proc/jitqueue 时, 观察到的行为近乎优化的, 即便在负载下:
phon% dd bs=20 count=5 < /proc/jitqueue
2027024 2028024
2028025 2029025
2029026 2030026
2030027 2031027
2031028 2032028
因为读进程当等待超时( 上面是 dd )不在运行队列中, 你看不到表现方面的差别, 无论
代码是否运行在一个抢占内核中.
wait_event_timeout 和 wait_event_interruptible_timeout 被设计为有硬件驱动存在,
这里可以用任何一种方法来恢复执行: 或者有人调用 wake_up 在等待队列上, 或者超时
到. 这不适用于 jitqueue, 因为没人在等待队列上调用 wake_up ( 毕竟, 没有其他代码
知道它 ), 因此这个进程当超时到时一直唤醒. 为适应这个特别的情况, 这里你想延后执
行不等待特定事件, 内核提供了 schedule_timeout 函数, 因此你可以避免声明和使用一
个多余的等待队列头:

#include <linux/sched.h>
signed long schedule_timeout(signed long timeout);
这里, timeout 是要延时的 jiffies 数. 返回值是 0 除非这个函数在给定的 timeout
流失前返回(响应一个信号). schedule_timeout 请求调用者首先设置当前的进程状态,
因此一个典型调用看来如此:
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout (delay);
前面的行( 来自 /proc/jitschedto ) 导致进程睡眠直到经过给定的时间. 因为
wait_event_interruptible_timeout 在内部依赖 schedule_timeout, 我们不会费劲显示
jitschedto 返回的数, 因为它们和 jitqueue 的相同. 再一次, 不值得有一个额外的时
间间隔在超时到和你的进程实际被调度来执行之间.
在刚刚展示的例子中, 第一行调用 set_current_state 来设定一些东西以便调度器不会
再次运行当前进程, 直到超时将它置回 TASK_RUNNING 状态. 为获得一个不可中断的延时,
使用 TASK_UNINTERRUPTIBLE 代替. 如果你忘记改变当前进程的状态, 调用
schedule_time 如同调用 shcedule( 即, jitsched 的行为), 建立一个不用的定时器.
如果你想使用这 4 个 jit 文件在不同的系统情况下或者不同的内核, 或者尝试其他的方
式来延后执行, 你可能想配置延时量当加载模块时通过设定延时模块参数.

文章来源:https://blog.csdn.net/liu1250836704/article/details/135188054
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。