系统中有很多与时间相关的程序(比如定期执行的任务,某一时间执行的任务,推迟一段时间执行的任务),因此,时间的管理对于 linux 来说非常重要。系统中管理的时间有 2 种:实际时间和定时器

1 实际时间

实际时间就是现实中钟表上显示的时间,其实内核中并不常用这个时间,主要是用户空间的程序有时需要获取当前时间,所以内核中也管理着这个时间。
实际时间的获取是在开机后,内核初始化时从 RTC 读取的。内核读取这个时间后就将其放入内核中的 xtime 变量中,并且在系统的运行中不断更新这个值。

注:RTC 就是实时时钟的缩写,它是用来存放系统时间的设备。一般和 BIOS 一样,由主板上的电池供电的,所以即使关机也可将时间保存。

实际时间存放的变量 xtime 在文件 kernel/time/timekeeping.c中。

  1. /* 按照16位对齐,其实就是2个long型的数据 */
  2. struct timespec xtime __attribute__ ((aligned (16)));
  3. /* timespec结构体的定义如下, 参考 <linux/time.h> */
  4. struct timespec {
  5. __kernel_time_t tv_sec; /* seconds */
  6. long tv_nsec; /* nanoseconds */
  7. };
  8. /* _kernel_time_t 定义如下 */
  9. typedef long __kernel_time_t;

系统读写 xtime 时用的就是顺序锁。

  1. /* 写入 xtime 参考 do_sometimeofday 方法 */
  2. int do_settimeofday(struct timespec *tv)
  3. {
  4. /* 省略 。。。。 */
  5. write_seqlock_irqsave(&xtime_lock, flags); /* 获取写锁 */
  6. /* 更新 xtime */
  7. write_sequnlock_irqrestore(&xtime_lock, flags); /* 释放写锁 */
  8. /* 省略 。。。。 */
  9. return 0;
  10. }
  11. /* 读取 xtime 参考 do_gettimeofday 方法 */
  12. void do_gettimeofday(struct timeval *tv)
  13. {
  14. struct timespec now;
  15. getnstimeofday(&now); /* 就是在这个方法中获取读锁,并读取 xtime */
  16. tv->tv_sec = now.tv_sec;
  17. tv->tv_usec = now.tv_nsec/1000;
  18. }
  19. void getnstimeofday(struct timespec *ts)
  20. {
  21. /* 省略 。。。。 */
  22. /* 顺序锁中读锁来循环获取 xtime,直至读取过程中 xtime 没有被改变过 */
  23. do {
  24. seq = read_seqbegin(&xtime_lock);
  25. *ts = xtime;
  26. nsecs = timekeeping_get_ns();
  27. /* If arch requires, add in gettimeoffset() */
  28. nsecs += arch_gettimeoffset();
  29. } while (read_seqretry(&xtime_lock, seq));
  30. /* 省略 。。。。 */
  31. }

上述场景中,写锁必须要优先于读锁 (因为 xtime 必须及时更新),而且写锁的使用者很少 (一般只有系统定期更新 xtime 的线程需要持有这个锁)。这正是顺序锁的应用场景。

2 定时器

定时器是内核中主要使用的时间管理方法,通过定时器,可以有效的调度程序的执行。动态定时器是内核中使用比较多的定时器,下面重点讨论的也是动态定时器。内核中的定时器有 2 种,静态定时器和动态定时器
静态定时器一般执行了一些周期性的固定工作:

  • 更新系统运行时间
  • 更新实际时间
  • 在 SMP 系统上,平衡各个处理器上的运行队列
  • 检查当前进程是否用尽了自己的时间片,如果用尽,需要重新调度。
  • 更新资源消耗和处理器时间统计值

动态定时器顾名思义,是在需要时(一般是推迟程序执行)动态创建的定时器,使用后销毁(一般都是只用一次)。一般我们在内核代码中使用的定时器基本都是动态定时器,下面重点讨论动态定时器相关的概念和使用方法。

2.1 定时器相关概念

定时器的使用中,下面 3 个概念非常重要:

  1. HZ
  2. jiffies
  3. 时间中断处理程序

    HZ

    节拍率 (HZ) 是时钟中断的频率,表示的一秒内时钟中断的次数。比如 HZ=100 表示一秒内触发 100 次时钟中断程序。HZ 的值一般与体系结构有关,x86 体系结构一般定义为 100,参考文件 include/asm-generic/param.h。HZ 值的大小的设置过程其实就是平衡精度和性能的过程,并不是 HZ 值越高越好。
HZ 值 优势 劣势
高 HZ 时钟中断程序运行的更加频繁,依赖时间执行的程序更加精确,对资源消耗和系统运行时间的统计更加精确。 时钟中断执行的频繁,增加系统负担时钟中断占用的 CPU 时间过多

此外,有一点需要注意,内核中使用的 HZ 可能和用户空间中定义的 HZ 值不一致,为了避免用户空间取得错误的时间,内核中也定义了 USER_HZ,即用户空间使用的 HZ 值。
一般来说,USER_HZ 和 HZ 都是相差整数倍,内核中通过函数 jiffies_to_clock_t来将内核来将内核中的 jiffies 转为 用户空间 jiffies

  1. /* 参见文件: kernel/time.c *
  2. //*
  3. * Convert jiffies/jiffies_64 to clock_t and back.
  4. */
  5. clock_t jiffies_to_clock_t(unsigned long x)
  6. {
  7. #if (TICK_NSEC % (NSEC_PER_SEC / USER_HZ)) == 0
  8. if HZ < USER_HZ
  9. return x * (USER_HZ / HZ);
  10. else
  11. return x / (HZ / USER_HZ);
  12. endif
  13. #else
  14. return div_u64((u64)x * TICK_NSEC, NSEC_PER_SEC / USER_HZ);
  15. #endif
  16. }
  17. EXPORT_SYMBOL(jiffies_to_clock_t);

jiffies

jiffies 用来记录自系统启动以来产生的总节拍数。比如系统启动了 N 秒,那么 jiffies 就为 N×HZ。jiffies 的相关定义参考头文件 _

  1. /* 64bit和32bit的jiffies定义如下 */
  2. extern u64 __jiffy_data jiffies_64;
  3. extern unsigned long volatile __jiffy_data jiffies;

Linux 内核提供了几个 jiffies 和 ms、 us、 ns 之间的转换函数:
image.png

使用定时器时一般都是以 jiffies 为单位来延迟程序执行的,比如延迟 5 个节拍后执行的话,执行时间就是 jiffies+532 位的 jiffies 的最大值为 2^32-1,在使用时有可能会出现回绕的问题。比如下面的代码:

  1. unsigned long timeout = jiffies + HZ/2; /* 设置超时时间为 0.5秒 */
  2. while (timeout < jiffies)
  3. {
  4. /* 还没有超时,继续执行任务 */
  5. }
  6. /* 执行超时后的任务 */

正常情况下,上面的代码没有问题。当 jiffies 接近最大值的时候,就会出现回绕问题。由于是 unsinged long 类型,所以 jiffies 达到最大值后会变成 0 然后再逐渐变大,如下图所示:
1558679735535.png
所以在上述的循环代码中,会出现如下情况:
1558679772456.png

  1. 循环中第一次比较时,jiffies = J1,没有超时
  2. 循环中第二次比较时,jiffies = J2,实际已经超时了,但是由于 jiffies 超过的最大值后又从 0 开始,所以 J2 远远小于 timeout
  3. while 循环会执行很长时间 (> 2^32-1 个节拍) 不会结束,几乎相当于死循环了

为了回避回扰的问题,可以使用 头文件中提供的 time_aftertime_before 等宏

  1. #define time_after(a,b) \
  2. (typecheck(unsigned long, a) && \
  3. typecheck(unsigned long, b) && \
  4. ((long)(b) - (long)(a) < 0))
  5. #define time_before(a,b) time_after(b,a)
  6. #define time_after_eq(a,b) \
  7. (typecheck(unsigned long, a) && \
  8. typecheck(unsigned long, b) && \
  9. ((long)(a) - (long)(b) >= 0))
  10. #define time_before_eq(a,b) time_after_eq(b,a)

上述代码的原理其实就是将 unsigned long 类型转换为 long 类型来避免回扰带来的错误,利用 time_after 宏就可以巧妙的避免回绕带来的超时判断问题,将之前的代码改成如下代码即可:

  1. unsigned long timeout = jiffies + HZ/2; /* 设置超时时间为 0.5秒 */
  2. while (time_after(jiffies, timeout))
  3. {
  4. /* 还没有超时,继续执行任务 */
  5. }
  6. /* 执行超时后的任务 */

2.2 时钟中断处理程序

时钟中断处理程序作为系统定时器而注册到内核中,体系结构的不同,可能时钟中断处理程序中处理的内容不同。但是以下这些基本的工作都会执行:

  • 获得 xtime_lock 锁,以便对访问 jiffies_64 和墙上时间 xtime 进行保护
  • 需要时应答或重新设置系统时钟
  • 周期性的使用墙上时间更新实时时钟
  • 调用 tick_periodic()

tick_periodic 函数位于: kernel/time/tick-common.c

  1. static void tick_periodic(int cpu)
  2. {
  3. if (tick_do_timer_cpu == cpu) {
  4. write_seqlock(&xtime_lock);
  5. /* Keep track of the next tick event */
  6. tick_next_period = ktime_add(tick_next_period, tick_period);
  7. do_timer(1);
  8. write_sequnlock(&xtime_lock);
  9. }
  10. update_process_times(user_mode(get_irq_regs()));
  11. profile_tick(CPU_PROFILING);
  12. }
  13. void do_timer(unsigned long ticks)
  14. {
  15. /* jiffies_64 增加指定ticks */
  16. jiffies_64 += ticks;
  17. /* 更新实际时间 */
  18. update_wall_time();
  19. /* 更新系统的平均负载值 */
  20. calc_global_load();
  21. }
  22. void update_process_times(int user_tick)
  23. {
  24. struct task_struct *p = current;
  25. int cpu = smp_processor_id();
  26. /* 更新当前进程占用CPU的时间 */
  27. account_process_tick(p, user_tick);
  28. /* 同时触发软中断,处理所有到期的定时器 */
  29. run_local_timers();
  30. rcu_check_callbacks(cpu, user_tick);
  31. printk_tick();
  32. /* 减少当前进程的时间片数 */
  33. scheduler_tick();
  34. run_posix_cpu_timers(p);
  35. }

2.3 定时器的定义

定时器在内核中用一个链表来保存的,链表的每个节点都是一个定时器。参见头文件

  1. struct timer_list {
  2. struct list_head entry;
  3. unsigned long expires; //超时时间
  4. void (*function)(unsigned long); //超时后的回调函数
  5. unsigned long data;
  6. struct tvec_base *base;
  7. #ifdef CONFIG_TIMER_STATS
  8. void *start_site;
  9. char start_comm[16];
  10. int start_pid;
  11. #endif
  12. #ifdef CONFIG_LOCKDEP
  13. struct lockdep_map lockdep_map;
  14. #endif
  15. };

通过加入条件编译的参数,可以追加一些调试信息。

2.4 定时器的生命周期

一个动态定时器的生命周期中,一般会经过下面的几个步骤:
1558680678950.png

1 初始化定时器:

  1. struct timer_list my_timer; /* 定义定时器 */
  2. init_timer(&my_timer); /* 初始化定时器 */

2 填充定时器:

  1. my_timer.expires = jiffies + delay; /* 定义超时的节拍数 */
  2. my_timer.data = 0; /* 给定时器函数传入的参数 */
  3. my_timer.function = my_function; /* 定时器超时时,执行的自定义函数 */
  4. /* 从定时器结构体中,我们可以看出这个函数的原型应该如下所示: */
  5. void my_function(unsigned long data);

3 激活定时器和修改定时器:激活定时器之后才会被触发,否则定时器不会执行。修改定时器主要是修改定时器的延迟时间,修改定时器后,不管原先定时器有没有被激活,都会处于激活状态。
填充定时器结构之后,可以只激活定时器,也可以只修改定时器,也可以激活定时器后再修改定时器。所以填充定时器结构和触发定时器之间的步骤,也就是虚线框中的步骤是不确定的。

  1. add_timer(&my_timer); /* 注册到内核,激活定时器 */
  2. mod_timer(&my_timer, jiffies + new_delay); /* 修改定时器,设置新的延迟时间 */

4 触发定时器:每次时钟中断处理程序会检查已经激活的定时器是否超时,如果超时就执行定时器结构中的自定义函数。

5 删除定时器:激活和未被激活的定时器都可以被删除,已经超时的定时器会自动删除,不用特意去删除。

  1. /*
  2. * 删除激活的定时器时,此函数返回1
  3. * 删除未激活的定时器时,此函数返回0
  4. */
  5. del_timer(&my_timer);

在多核处理器上用 del_timer 函数删除定时器时,可能在删除时正好另一个 CPU 核上的时钟中断处理程序正在执行这个定时器,于是就形成了竞争条件。为了避免竞争条件,建议使用 del_timer_sync 函数来删除定时器

del_timer_sync 函数会等待其他处理器上的定时器处理程序全部结束后,才删除指定的定时器。

  1. /*
  2. * 和del_timer 不同,del_timer_sync 不能在中断上下文中执行
  3. */
  4. del_timer_sync(&my_timer);

3 delayed_work

对于周期性的任务,除了定时器以外,在Linux内核中还可以利用一套封装得很好的快捷机制,其本质是利用工作队列和定时器实现,这套快捷机制就是delayed_work:

  1. #include <linux/workqueue.h>
  2. struct delayed_work {
  3. struct work_struct work;//work_func_t类型成员函数func()会在延时后被执行
  4. struct timer_list timer;
  5. /* target workqueue and CPU ->timer uses to queue ->work */
  6. struct workqueue_struct *wq;
  7. int cpu;
  8. };
  9. //函数,用于指定内核延时任务
  10. static inline bool schedule_delayed_work(struct delayed_work *dwork,unsigned long delay);
  11. //取消函数
  12. int cancel_delayed_work(struct delayed_work *work);
  13. int cancel_delayed_work_sync(struct delayed_work *work);

4 内核延时与唤醒

内核对于那些短暂,精确的延迟要求也提供了相应的宏。

4.1 短延迟

Linux内核中提供了下列3个函数以分别进行纳秒、微秒和毫秒延迟:

  1. #include <include/asm/delay.h>
  2. #include <include/delay.h>
  3. void ndelay(unsigned long nsecs);
  4. void udelay(unsigned long usecs);
  5. void mdelay(unsigned long msecs);//忙等待,时间太长不建议使用

毫秒时延(以及更大的秒时延)已经比较大了,在内核中,最好不要直接使用mdelay()函数,这将耗费CPU资源,对于毫秒级以上的时延,内核提供了下述函数:

  1. void msleep(unsigned int millisecs);
  2. unsigned long msleep_interruptible(unsigned int millisecs);
  3. void ssleep(unsigned int seconds);

通过这些宏,可以简单的实现延迟,比如延迟 5ns,只需 ndelay(5); 即可。这些短延迟的实现原理并不复杂:

  • 首先,内核在启动时就计算出了当前处理器 1 秒能执行多少次循环,即 loops_per_jiffy (loops_per_jiffy 的计算方法参见 init/main.c文件中的 calibrate_delay 方法)。
  • 然后算出延迟 5ns 需要循环多少次,执行那么多次空循环即可达到延迟的效果

loops_per_jiffy的值可以在启动信息中看到:

  1. [root@vbox ~] dmesg | grep delay
  2. Calibrating delay loop (skipped), value calculated using timer frequency.. 6387.58 BogoMIPS (lpj=3193792)

4.2 长延迟

在内核中进行延迟的一个很直观的方法是比较当前的jiffies和目标jiffies(设置为当前jiffies加上时间间隔的jiffies),直到未来的jiffies达到目标jiffies。
内核提供的方法如下:

  1. #define time_after(a,b) \
  2. (typecheck(unsigned long, a) && \
  3. typecheck(unsigned long, b) && \
  4. ((long)(b) - (long)(a) < 0))
  5. #define time_before(a,b) time_after(b,a)

使用示例如下:

  1. /* 延迟 100 个 jiffies */
  2. unsigned long delay = jiffies + 100;
  3. while(time_before(jiffies, delay)); //等待期间一直在循环,忙等待
  4. /* 再延迟 2s */
  5. unsigned long delay = jiffies + 2*Hz;
  6. while(time_before(jiffies, delay));

4.3 睡着延迟

睡着延迟无疑是比忙等待更好的方式,睡着延迟是在等待的时间到来之前进程处于睡眠状态,CPU资源被其他
进程使用。内核中有个利用定时器实现延迟的函数schedule_timeout。这个函数会将当前的任务睡眠到指定时间后唤醒,所以等待时不会占用 CPU 时间。

  1. /* 将任务设置为可中断睡眠状态 */
  2. set_current_state(TASK_INTERRUPTIBLE);
  3. /* 小睡一会儿,“s“秒后唤醒 */
  4. schedule_timeout(s*HZ);

查看 schedule_timeout 函数的实现方法,可以看出是如何使用定时器的。

  1. signed long __sched schedule_timeout(signed long timeout)
  2. {
  3. /* 定义一个定时器 */
  4. struct timer_list timer;
  5. unsigned long expire;
  6. switch (timeout)
  7. {
  8. case MAX_SCHEDULE_TIMEOUT:
  9. /*
  10. * These two special cases are useful to be comfortable
  11. * in the caller. Nothing more. We could take
  12. * MAX_SCHEDULE_TIMEOUT from one of the negative value
  13. * but I' d like to return a valid offset (>=0) to allow
  14. * the caller to do everything it want with the retval.
  15. */
  16. schedule();
  17. goto out;
  18. default:
  19. /*
  20. * Another bit of PARANOID. Note that the retval will be
  21. * 0 since no piece of kernel is supposed to do a check
  22. * for a negative retval of schedule_timeout() (since it
  23. * should never happens anyway). You just have the printk()
  24. * that will tell you if something is gone wrong and where.
  25. */
  26. if (timeout < 0) {
  27. printk(KERN_ERR "schedule_timeout: wrong timeout "
  28. "value %lx\n", timeout);
  29. dump_stack();
  30. current->state = TASK_RUNNING;
  31. goto out;
  32. }
  33. }
  34. /* 设置超时时间 */
  35. expire = timeout + jiffies;
  36. /* 初始化定时器,超时处理函数是 process_timeout,后面再补充说明一下这个函数 */
  37. setup_timer_on_stack(&timer, process_timeout, (unsigned long)current);
  38. /* 修改定时器,同时会激活定时器 */
  39. __mod_timer(&timer, expire, false, TIMER_NOT_PINNED);
  40. /* 将本任务睡眠,调度其他任务 */
  41. schedule();
  42. /* 删除定时器,其实就是 del_timer_sync 的宏
  43. del_singleshot_timer_sync(&timer);
  44. /* Remove the timer from the object tracker */
  45. destroy_timer_on_stack(&timer);
  46. timeout = expire - jiffies;
  47. out:
  48. return timeout < 0 ? 0 : timeout;
  49. }
  50. EXPORT_SYMBOL(schedule_timeout);
  51. /*
  52. * 超时处理函数 process_timeout 里面只有一步操作,唤醒当前任务。
  53. * process_timeout 的参数其实就是 当前任务的地址
  54. */
  55. static void process_timeout(unsigned long __data)
  56. {
  57. wake_up_process((struct task_struct *)__data);
  58. }

schedule_timeout一般用于延迟时间较长的程序。这里的延迟时间较长是对于计算机而言的,其实也就是延迟大于 1 个节拍 (jiffies)。对于某些极其短暂的延迟,比如只有 1ms,甚至 1us,1ns 的延迟,必须使用上面的短延迟方法。

4.3 定时器和延迟的例子

下面的例子测试了短延迟,自定义定时器以及 schedule_timeout 的使用:

  1. #include <linux/sched.h>
  2. #include <linux/timer.h>
  3. #include <linux/jiffies.h>
  4. #include <asm/param.h>
  5. #include <linux/delay.h>
  6. #include "kn_common.h"
  7. MODULE_LICENSE("Dual BSD/GPL");
  8. static void test_short_delay(void);
  9. static void test_delay(void);
  10. static void test_schedule_timeout(void);
  11. static void my_delay_function(unsigned long);
  12. static int testdelay_init(void)
  13. {
  14. printk(KERN_ALERT "HZ in current system: %dHz\n", HZ);
  15. /* test short delay */
  16. test_short_delay();
  17. /* test delay */
  18. test_delay();
  19. /* test schedule timeout */
  20. test_schedule_timeout();
  21. return 0;
  22. }
  23. static void testdelay_exit(void)
  24. {
  25. printk(KERN_ALERT "*************************\n");
  26. print_current_time(0);
  27. printk(KERN_ALERT "testdelay is exited!\n");
  28. printk(KERN_ALERT "*************************\n");
  29. }
  30. static void test_short_delay()
  31. {
  32. printk(KERN_ALERT "jiffies [b e f o r e] short delay: %lu", jiffies);
  33. ndelay(5);
  34. printk(KERN_ALERT "jiffies [a f t e r] short delay: %lu", jiffies);
  35. }
  36. static void test_delay()
  37. {
  38. /* 初始化定时器 */
  39. struct timer_list my_timer;
  40. init_timer(&my_timer);
  41. /* 填充定时器 */
  42. my_timer.expires = jiffies + 1*HZ; /* 2秒后超时函数执行 */
  43. my_timer.data = jiffies;
  44. my_timer.function = my_delay_function;
  45. /* 激活定时器 */
  46. add_timer(&my_timer);
  47. }
  48. static void my_delay_function(unsigned long data)
  49. {
  50. printk(KERN_ALERT "This is my delay function start......\n");
  51. printk(KERN_ALERT "The jiffies when init timer: %lu\n", data);
  52. printk(KERN_ALERT "The jiffies when timer is running: %lu\n", jiffies);
  53. printk(KERN_ALERT "This is my delay function end........\n");
  54. }
  55. static void test_schedule_timeout()
  56. {
  57. printk(KERN_ALERT "This sample start at : %lu", jiffies);
  58. /* 睡眠2秒 */
  59. set_current_state(TASK_INTERRUPTIBLE);
  60. printk(KERN_ALERT "sleep 2s ....\n");
  61. schedule_timeout(2*HZ);
  62. printk(KERN_ALERT "This sample end at : %lu", jiffies);
  63. }
  64. module_init(testdelay_init);
  65. module_exit(testdelay_exit);

Makefile 如下:

  1. obj-m := mydelay.o
  2. mydelay-objs := testdelay.o kn_common.o
  3. #generate the path
  4. CURRENT_PATH:=$(shell pwd)
  5. #the absolute path
  6. LINUX_KERNEL_PATH:=/usr/src/linux-source-4.15.0 #直接用发行版中的linux源码,不用再下载linux内核源码。注意,每个linux发行版的目录不一定一样
  7. #complie object
  8. all:
  9. make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
  10. clean:
  11. make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean

执行测试命令及查看结果的方法如下:(Ubuntu 18.04编译不过,kernel已经改变)

  1. [root@vbox chap11] make
  2. [root@vbox chap11] insmod mydelay.ko
  3. [root@vbox chap11] rmmod mydelay.ko
  4. [root@vbox chap11] dmesg | tail -14
  5. HZ in current system: 1000Hz
  6. jiffies [b e f o r e] short delay: 4296079617
  7. jiffies [a f t e r] short delay: 4296079617
  8. This sample start at : 4296079619
  9. sleep 2s ....
  10. This is my delay function start......
  11. The jiffies when init timer: 4296079619
  12. The jiffies when timer is running: 4296080621
  13. This is my delay function end........
  14. This sample end at : 4296081622
  15. *************************
  16. 2013-5-9 23:7:20
  17. testdelay is exited!
  18. *************************

结果说明:
1 短延迟只延迟了 5ns,所以执行前后的 jiffies 是一样的。

  1. jiffies [b e f o r e] short delay: 4296079617
  2. jiffies [a f t e r] short delay: 4296079617

2 自定义定时器延迟了 1 秒后执行自定义函数,由于我的系统 HZ=1000,所以 jiffies 应该相差 1000

  1. The jiffies when init timer: 4296079619
  2. The jiffies when timer is running: 4296080621

实际上 jiffies 相差了 1002,多了 2 个节拍

3 schedule_timeout 延迟了 2 秒,jiffies 应该相差 2000

  1. This sample start at : 4296079619
  2. This sample end at : 4296081622

实际上 jiffies 相差了 2003,多了 3 个节拍

以上结果也说明了定时器的延迟并不是那么精确,差了 2,3 个节拍其实就是误差 2,3 毫秒 (因为 HZ=1000)
如果 HZ=100 的话,一个节拍是 10 毫秒,那么定时器的误差可能就发现不了了 (误差只有 2,3 毫秒,没有超多 1 个节拍)。