现在的位置: 首页 > 综合 > 正文

深入linux设备驱动程序内核机制(第八章) 读书笔记

2013年10月28日 ⁄ 综合 ⁄ 共 5779字 ⁄ 字号 评论关闭

第八章 时间管理

    驱动对时间进行操作, 典型的可以分为两大类:延时与定时. 前者是两个连续的动作A与B之间插入一段时间空
    白, 即在A执行后需要等待若干时间才能执行B, 这段时间空白内, 处理哭也许是进入忙等待, 也许是切换到一
    个新进程上. 后者是在一个指定的时间点到后执行某此动作, 轮询是最典型的应用.

     欢迎转载本文, 请标明出处

     本文出处:http://blog.csdn.net/dyron

8.1 jiffies

    内核源码中针对32位和64位系统分别定义了jiffies和jiffies_64:

        #deifne __jiffy_data __attribute__((section(".data")))
        extern u64 __jiffy_data jiffies_64;
        extern unsigned long volatile __jiffy_data jiffies;

    __jiffy_data表明这两个对象最终出现在.data区中. extern 表明可能定义在某个别的文件中, 事实上它们
    出现在内核的链接脚本vmlinux.lds中. 除了位宽不一样, 其它都相同.

    jiffies在linux系统启动引导阶段初始化为0, 当系统完成了时钟的初始化之后, 每个时钟中断处理例程中该
    值都会被加1. 除了时钟中断对jiffies进行更新为, 其它任何人都只是读取该值.

    在实际使用中, linux用宏HZ来表示系统中时钟中断发生的频率.

    #ifdef __KERNEL__
    #define HZ CONFIG_HZ
    #define USER_HZ     100
    #define CLOCKS_PER_SEC(USER_HZ)
    #endif

    从定义中可以看出, 内核在配置阶段通过CONFIG_HZ修改HZ数值. HZ值为1000表明1秒内将发生1000次时钟中
    断. jiffies只能局限在毫秒级别上, 更高精度的时间管理jiffies无法满足.

    jiffies在内核中作为一个全局变量被导出, 如果驱动要读取当前jiffies的值, 只需要在源码中包含linux/
    jiffies.h即可

    8.1.1 时间比较

    time_after(a,b): 如果时间点A在B之后, 宏返回true.
    time_defore(a,b): 如果时间点A在B之前, 宏返回true.
    time_after_eq(a,b): 类似于time_after, 如果A与B相等时, 宏也返回true.
    time_before_eq(a,b): 类似于time_before, 在A和B相等时, 宏也返回true.
    time_in_range(a,b,c): 检查A是否包在B,C内, 当A等于B或C时, 宏也返回true.

    使用以上宏时, 参数都应该是unsigned long型, 如果是针对jiffies_64类型来判断, 只需要调用上述宏后缀
    加64的宏即可.

    8.1.2 时间转换

    unsigned int jiffies_to_msecs(const unsigned long j);
    unsigned int jiffies_to_usecs(const unsigned long j);
    unsigned long msecs_to_jiffies(const unsigned int m);
    unsigned long usecs_to_jiffies(const unsigned int u);

    内核定义了struct timeval和struct timespec两种数据结构.

    struct timespec {
        __kernel_time_t tv_sec;
        long tv_nsec;
    }

    struct timeval {
        __kernel_time_t tv_sec;
        __kernel_suseconds_t tv_usec;
    };

    timespec用秒和纳秒来描述时间, timeval用秒和毫秒的形式. 内核提供了jiffies变量和这两个数据结构的
    实例间相互转换的函数:

    unsigned long timespec_to_jiffies(const struct timespec *value);
    void jiffies_to_timespec(const unsinged long jiffies, struct timespec *value);
    unsigned long timeval_to_jiffies(const struct timeval *value);
    void jiffies_to_timeval(const unsigned long jiffies, struc ttimeval *value);

8.2 延时操作

    从延时精度的角度出发, 延迟函数分成两大类:1 是基于时钟滴答jiffies实现的延时. 这种延时一般在ms级
    别, 称为长延时. 另一类延时精度超越了时钟滴答的边界, 称为短延时.

    8.2.1 长延时

    . 忙等待
        简单的忙等待是while或者什么循环:

        unsigned long t = oxffffffff;
        while (t--);

        unsigned long j = jiffies + HZ;
        while(time_before(jiffies, j))
            cpu_relax();

        cpu_relax不会导致当前代码上出处理哭.

    1. 这种忙等待在进入等待CPU后不做任何事, 空转CPU, 这种资源的浪费很可鸡肉. 对于单CPU来说, 如果不
    幸在进入忙等待前并闭了CPU的中断, 那么jiffies的值将不会被更新. while循环的条件将一直满足.

????????关掉CPU的中断, 时钟中断也不会再发生, 所以jiffies就不会被更新.

    2. 对抢占系统而言, 忙等待可能被中断抢占, 抢占后调度到进程B, 而后返回A, A的延时可能已超过了预期
    时间.

    . 让出处理器

    这里不再调用cpu_relax而是调用schedule().  schedule可以让出处理器, 但还在当前CPU的运行队列中,这
    使系统无法进入idle状态, 如果只有当前进程,那样还是会被执行. 影响CPU的自己降频等功能.

    这里有一变种: schedule_timeout, 在调用schedule_timeout之前先调用set_current_state宏将当前进程的
    状态设置为TASK_UNINTERRUPTIBLE, 后边再调用schedule()时, 因为当前进程是TASK_UNINTERRUPTIBLE, 所以
    当前进程会被移出处理哭的运行队列.

    while (time_before(jiffies, j)) {

        set_current_state(TASK_UNINTERRUPTIBLE);
        schedule();
    }

    上面的代码很糟糕,它这么设置当前进程为UNINTERRUPTIBLE后, 不会再有别的代码去更改其状态,使其可以再
    次进入运行队列. 如果使用schedule_timeout, 定时器到期后, 将会唤醒进程并使其重新进入进行队列.

    事实上, linux内核还提供了一个基于schedule_timeout版本实现的毫秒级睡眠的函数msleep;
    内核还提供了msleep的一个变体msleep_interruptible; 进程可以因为收到信号而被唤醒,那么将会返回原先
    指定的休眠时间msecs的剩余时间值.

    8.2.2 短延时

    1. 假如HZ=1000, jiffies增加的数量为HZ, 也就是说jiffies每隔1ms才会增加1. 想要实现微秒级的延迟,单
    纯通过jiffies是不可能完成的.

    2. 在微秒级的水平上, 必须考虑进程切换所带来的时间开销. 因为进行切换所耗费几个us到上百个us之间,
    基于之上论述, 短处暑一般都是苦于忙等待来完成的.

    void mdelay(unsigned long msecs);
    void udelay(unsigned long usecs);
    void ndelay(unsigned long nsecs);

8.3 内核定时器

    驱动实现使用一个定时哭, 首先应该定义一个定时器类型的变量. struct timer_list.

    struct timer_list {
    struct list_head entry;
    unsigned long expries; //指定定时器到期时间
    struct tvec_base *base;

    void (*function)(unsigned long); //定时器函数. 当expires到期时, 该函数将被触发.
    unsigned long data; //定时器对象携带的数据.
    int slack;
    }

?????????定时器函数将在中断上下文中执行, 非当前进程的地址空间中.

    8.3.1 init_timer

    init_timer函数内部会调用__init_timer, init_timer主要实始化定时器对象中与内核实现相关的成员, 所以
    驱动在使用定时器前, 应该调用init_timer.

    static void __init_timer(struct timer_list *timer,
        const char *name,
        struct lock_class_key *key)
    {
    timer->entry.next = NULL;
    timer->base = __raw_get_cpu_var(tvec_bases);
    timer->slack = -1;
    #ifdef CONFIG_TIMER_STATS
    timer->start_site = NULL;
    timer->start_pid = -1;
    memset(timer->start_comm, 0, TASK_COMM_LEN);
    #endif
    lockdep_init_map(&timer->lockdep_map, name, key, 0);
    }

    8.3.2 add_timer

    当程序定义一个定时哭后, init_timer及相应代码初始化定时器对象中的成员后, 程序需要调用add_timer将
    定时器加入到系统中, 这样在expires表示的时间点到期后才会被触发.

    内核中定义了一个struct tvec_base来管理系统中添加的所有定时器.

    struct tvec_base {
    spinlock_t lock;
    struct timer_list *running_timer;
    unsigned long timer_jiffies;
    unsigned long next_timer;
    struct tvec_root tv1;
    struct tvec tv2;
    struct tvec tv3;
    struct tvec tv4;
    struct tvec tv5;
    } ____cacheline_aligned;

    tv1 - tv5被内核用来对系统中注册的定时器进行散列式管理, 内核为系统中的每个CPU都定义了一个struct
     tvec_base类型的变量.

     static DEFINE_PER_CPU(struct tvec_base *, tvec_bases) = &boot_tvec_bases;

     tvec_base用来将系统中加入的第个定时器组织管理起来, 用简单的单一链表结构当然可以实现这一目标,然
     尔系统会在每个时钟中断中扫描该链表要分辨出哪些定时器已到期或即将到期.

     每当驱动通过add_timer向系统添加一个定时器时, 系统都会对该定时器的到期时间expires进行分类, 根据
     到期时间的长短将当前定时器对象放到struct tvec_base对象的成员tv1, tv2, tv3, tv4, tv5的定时器链
     表中.

     在每个时钟中断处理函数中, 严格地说是时钟中断处理的下半部softirq部分, 会对tvec_bases管理的定时器
     队列进行扫描, 以确定当前队列中有哪些定时器已经到期.

     对tvec_bases管理的定时器队列进行扫描, 如果发现有定时器到期, 则调用该定时器对象的fn函数, 这里需
     要注意, 当一个定时器对象中的定时函数被调用时, 该定时器对象已经从系统的定时器 队列中删除了, 所以
     如果要让该定时器对象在以后能继续被系统所调用, 则需要再次调用add_timer或者mod_timer来将定时器重
     新加入到系统中去.

     8.3.3 del_timer和del_timer_sync

     int del_timer(struct timer_list *timer);
     int del_timer_sync(struct timer_list *timer);

     del_timer与del_timer_sync只在smp系统上才有区别, 在单处理器系统中, 两者相同.
     del_timer要摘除定时器对象timer, 会首先判断该对象是否是一个pending的定时器, 一个处理pending状态
     的定时器是处在处理器的定时器管理队列中正等待被调度执行的定时器对象. 如果一个要被del_timer函数
     删除的timer对象已经被调用执行, 将直接返回0, 否则函数将通过detach_timer将该定时器对象从队列中删
     除. 在SMP系统中, del_timer_sync要完成的任务除了del_timer一样的删一个定时器外, 还会确保当函数返
     回时系统中没有任何处理器 正在招待定时器对象上的定时器函数.

8.4 本章小结

     欢迎转载本文, 请标明出处

     本文出处:http://blog.csdn.net/dyron

    时间管理相关的任务从总体上可以分成两大类, 一类是延迟当前处理器的执行, 另一类是设定一个延迟时间
    点, 当该延迟时间点到期后执行特定的动作.

    对于延迟的实现, 又可分为忙等待和让出处理器. 前者是让CUP进入到不断循环中以实现延迟. 后者在当前进
    程延迟期间让出处理器, 让处理器 来执行别的进程. 如果延迟时间非常短, 后者就不适合.

    定时器在驱动中最常见的场景是实现轮询机制, 实现只需要定义一个定时器对象并指定其到期时间及实现一个
    定时器函数, 通过add_timer或者mod_timer将该定时器对象加入系统中即可.

抱歉!评论已关闭.