接下来记录下看ldd3中时间管理的相关内容,结合 深入linux设备驱动程序内核机制 来阅读会好些。(其实 深入linux设备驱动程序内核机制 就是参考 ldd3 来写的,也可以看成是读ldd3的笔记吧)
概念
系统的时钟频率 时钟频率详解和编程 ,一般系统都会有个默认的时钟频率HZ,其实就是1秒内系统时钟发生多少次中断。假如:系统的HZ为1000,则表示1秒内系统会产生1000次时钟中断;
滴答数: 系统内核会有个统计时钟中断发生的次数的计数器,该计数器从系统启动引导被初始化为0后,只要发生一次时钟中断,该计数器就自增1;所以该计数器就是表示从开机起发生多少次中断。该计数器是个64位的变量(不管你系统或者cpu架构 是32位还是64位,该变量都是64位的),称为 jiffies_64。但我们一般使用的就是unsigned long jiffies变量,在64位平台上jiffies和jiffies_64应该是相同的(不过还是以各个平台为标准);在32位的平台上jiffies 就是 jiffies_64的低32位。为什么会有两个滴答计数器?是因为并不是在所有平台对jiffies_64访问都是原子的,而且我们一般用它的低32位就能达到所需要的精度了(如果执意要用jiffies_64,那么可以用 u64 get_jiffies_64(void)来原子的获取到jiffies_64)。
jiffies和jiffies_64都应该看成是只读变量,因为他们的值都是内核来修改的。下面看下使用:
#include<linux/jiffies.h>
unsigned long j, stamp_1s, stamp_halfs, stamp_nn;
j = jiffies; //当前值
stamp_1s = jiffies + HZ; //1秒后
stamp_halfs = jiffies + HZ / 2; //版秒后
stamp_nn = jiffies + n * HZ / 1000; // n毫米后
再说个溢出问题,假设HZ值为1000,则1秒jiffies就要增加1000, 而jiffies是unsigned long类型;那么, sizeof(unsigned long) / (24 * 60 * 60 * 100) 大概就是50天左右。
比较和转换
jiffies操作函数:
#include<linux/jiffies.h>
int time_after(unsigned long a, unsigned long b); // a > b ? 1:0
int time_before(unsigned long a, unsigned long b); // a < b ? 1:0
int time_after_eq(unsigned long a, unsigned long b); // a >= b ? 1:0
int time_before_eq(unsigned long a, unsigned long b); // a =< b ? 1:0
其实上面的函数基本是通过: diff = (long)a - (long)b;
jiffies和用户空间的时间转换
};
struct timespec{
time_ttv_sec; //秒
long inttv_nsec; //纳秒
};
#include<linux/time.h>
unsigned long timespec_to_jiffies(struct timespec *value);
void jiffies_to_timespec(unsigned long jiffies, struct timespec *value);
unsigned long timeval_to_jiffies(struct timeval *value);
void jiffies_to_timeval(unsigned long jiffies, struct timeval *value);
还有个转换函数我们应该会经常使用到,就是将我们日常使用的墙钟时间转换为jiffies:
unsigned long mktime (unsigned int year, unsigned int mon, unsigned int day, unsigned int hour, unsigned min, unsinged int sec);
延时操作
延时可以分为几种:1、使用系统时钟的延时,这种延时大于时钟滴答;2、以时钟滴答的延时,比如延时5个时钟滴答;3、延时的单位比滴答数还小,一般是ns为单位活在更短,一般就用软件方式实现忙等待就能达到效果;这里谈论后面2种延时;
长延时
长延时:主要分为两种,忙等待和让出处理器;
最简单的就是忙等待:while ( time_before(jiffies, j1) )nop; 忙等待到jiffies为j1;这种方法很多漏洞,比如忙等待浪费cpu资源,如果忙等待时禁止了中断,那系统就会死掉。。。总之,这种方式在发行的软件中是不能使用的(自己测试的可以简单下)
让出处理器的延时:
while ( time_before(jiffies, ji)){
schedule();
}
当时间没有到时,系统会通过schedule()函数来调度其他进程来运行,但调用schedule()进程会被放入到就绪队列,只要有机会该进程就会再次被调用运行;所以如果系统在空闲状态(只有一个进程),那么调用schedule()就会无效了。这里涉及到idle状态,该状态下类似休眠,如果就绪队列中没有进程在排队那么就会进入idle状态。很显然这里不会进入idle状态,而是放弃cpu然后又被调度,然后放弃cpu,然后又被调度....
让出处理器的延时还有个问题,就是不一定能按时被再次调度。因为进程的调度涉及到各种因素,比如优先级,时间片等。其实这种方法很有可能是超时后再调度回来,那究竟超时多久就不得而知了。
更好的延时方法:
set_current_state(TASK_UNINTERRUPTIBLE);
schedule_timeout(jiffies + HZ);
首先是设置当前进程状态为不可中断,这时候系统就会把该进程放到不可中断睡眠队列中,所以可以解决上面提到的调度--放弃--调度--放弃的循环中;这里会调用schedule_timeout(jiffies + HZ)函数,只有到了预定的时间后才会把该进程从不可中断的睡眠队列中移到就绪队列中。但是这还是没有解决何时调度该进程的问题,当到了时间后,该进程虽然被调度到就绪队列中,但什么时候调度该进程去运行却是不能由我们决定的。
上面的方法和: set_current_state(TASK_UNINTERRUPTIBLE); schedule(); 并不是一样的,这是因为schedule_timeout()函数会在时间到期时把进程从睡眠状态调度到就绪状态 ,而schedule()函数不负责唤醒睡眠进程;
这里有两个类似的函数:
#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);
上面的两个函数和schedule_timeout()类似,只是schedule_timeout()函数没有等待条件。上面的两个函数如果被其他事物唤醒,就是等待的条件已经满足。那么返回的是到预定时间为止还剩下的时间。
上面的延时方法其实还没有解决精确度的问题,如果一定要精确的话,那么可以试试这种方法:假设延时5s,那么可以先用:set_current_state(TASK_UNINTERRUPTIBLE), schedule_timeout(4s); 这样一定是4s后该进程被再次调用,那么比较下jiffies,看看是否到5s了,如果还差点那么可以用忙等待来处理。如果还有比较长时间,那么可以再次调用schedule_timeout(xx),这就可以自己设置了。
短延时
短延时一般是硬件延时,数量级都是毫秒和纳秒级的,所以不能用jiffies,不过内核提供了几个函数:
#include<linux/delay.h>
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
这些延时都是忙等待实现的;
下面延时方法不用忙等待实现:
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds);
前两个函数将调用进程休眠以给定的millisecs,msleep的调用是不可中断的。可以确定进程将至少睡眠给定的毫秒数。如果驱动程序正在某个等待队列上等待,而又希望其他进程能够打断这个休眠,则可以使用 msleep_interruptible(), 该函数在进程被打断唤醒时会返回剩余的睡眠时间。ssleep函数是以秒计算的不可中断休眠。