鸣涧 发表于 2023-8-15 16:53:40

Linux中的延时休眠函数sleep、usleep、udelay、mdelay、ssleep、jiff...

Linux中的延时休眠函数sleep、usleep、udelay、mdelay、ssleep、jiffies详解


一、简介
      Linux中应用层和驱动层编写代码时都会用到延时,本文主要介绍两种情况下延时功能的实现。

1.应用层:sleep、usleep;

2.驱动层:udelay、mdelay、ssleep、通过jiffies 、定时器、中断底半部;

二、应用层延时

1.sleep


头文件:#include <unistd.h>
原    型:unsigned int sleep(unsigned int seconds);
参    数:seconds:延时时长,单位s;
返回值:0 - 表示成功休眠seconds时长;
            other - 表示在延时过程中因信号导致缺少的时长;

注   意:1.sleep的底层逻辑使用nanosleep实现,并且在一些系统中,sleep可以通过alarm和SIGALRM信号实现,但是不要混合调用alarm和sleep;
            2.当进程运行到sleep时,进程将进入到可中断休眠态,唤醒休眠的方式:到达延时时间 和 信号中断休眠;
            3.在运行sleep的时候,将会释放CPU的占有率,占用的资源较少;
            4.在sleep的时候,如果使用信号中断,将会运行sleep下一指令;

         关于注意中第三点相关的测试代码及效果:
#include <stdio.h>                     
#include <unistd.h>

int main(int argc,const char argv[])
{
      printf("strat\n");
      sleep(10);
      printf("stop\n");
      return 0;
}
2.usleep

头文件:#include <unistd.h>

原    型:int usleep(useconds_t usec);

参    数:usec:延时时长,单位us;

返回值:0 - 表示成功休眠usec时长;

            -1 - 表示失败,重置errno;

                        errno为EINTR 由于信号中断导致;

                        errno为EINVAL设置的休眠时间超出范围;

注   意:1.运行usleep将使线程暂停(至少)usec微秒,睡眠时间可能会因任何系统活动或处理调用所花费的时间或系统计时器的粒度而略微延长;

            2.休眠时长范围为0 ~ 1000000,超过范围将报错;

            3.在运行usleep的时候,将会释放CPU的占有率,占用的资源较少;

三、驱动层延时

         在了解延时方法之前,先了解一下忙等待的概念:一种进程执行状态。进程执行到一段循环程序的时候,由于循环判断条件不能满足而导致处理器反复循环,处于繁忙状态,该进程虽然繁忙但无法前进。忙等待通常效率低下,并且可能导致意外死锁,因为忙等待线程不会释放锁定的资源。

(一)delay函数

1.udelay

头文件:#include <linux/delay.h>

原    型:void udelay(unsigned long usecs);         //基本所有架构都包含有此函数,一般作为内联函数编译;

参    数:usec:延时时长,单位us;

注   意:1.只能处理时间较短的延时,参数usec最大只能设置为2000,超出2000编译会出现__bad_udela错误提示;

            2.udelay属于忙等待,延时的时间内不能运行其他的任务;udelay是通过计算处理器速度和使用整数变量loops_per_second循环实现;

            3.由于loops_per_second的精度值只有8位,所以当控制的延时较长时,累积的误差将会比较大;

2.mdelay、ndelay

头文件:#include <linux/delay.h>

原    型:void mdelay(unsigned long msecs); //使用 udelay 做循环

               void ndelay(unsigned long nsecs); //使用 udelay 做循环

注   意:1.本质都是由udelay实现,都是忙等待函数;

            2.编译时遇到 “implicit declaration of function 'udelay' ”问题,一般是由于头文件的问题;

(二)sleep函数

1.msleep、ssleep

头文件:#include <linux/delay.h>

原    型:void msleep(unsigned int millisecs);
               void ssleep(unsigned int seconds);

注   意:1.获取指定(或略长)的延时时间,但不会忙等待;

            2.调用时进入不可中断休眠态,不能被信号唤醒;

2.msleep_interruptible

头文件:#include <linux/delay.h>
原    型:unsigned long msleep_interruptible(unsigned int millisecs);

注   意:1.获取指定(或略长)的延时时间,但不会忙等待;
            2.调用时可以被信号唤醒,并返回初始请求睡眠周期中剩余的毫秒数,;一般用于等待队列并需要打断休眠唤醒进程时;

(三)jiffes

      如果想把执行延迟若干个时钟滴答,或者对延迟的精度要求不高(比如,想延迟整数数目的秒数),最简单的也是最笨的实现如下,也就是忙等待。

unsigned long j = jiffies + jit_delay * HZ;

while (jiffies < j)
    /* nothing */;
      jiffies在内核的头文件中声明为volatile类型变量,每次C代码访问都会重新读取,通过循环可以实现延迟作用,但是在忙等待期间处理器被占用,调度器不断运行在内核空间的进程。如果在进入循环之前正好关闭了中断,jiffies 值就不会得到更新,那么 while 循环的条件就永远为真,只能通过断电重启结束循环。

(四)内核定时器

1.定义

      用来调度函数在指定的时间(基于内部时钟滴答)执行任务,内核定时器本质是一个数据结构,相关函数定义在<linux/timer.h>。
struct timer_list {
    struct list_head entry;                  //内核定时器链表
    unsigned long expires;                //定时的时间
    void (*function)(unsigned long);//定时器处理函数(当定时时间到了之后,执行的函数)
    unsigned long data;/*传递给函数的参数,若需要在参数中传递多个数据项,可以将它们捆绑成单个数据结构并且将它的指针强制转换为 unsiged long 的指针传入。*/
    struct tvec_t_base_s *base;
#ifdef CONFIG_TIMER_STATS
    void *start_site;
    char start_comm;
    int start_pid;
#endif
};

      当在进程上下文之外(即在中断上下文)中运行程序时, 必须遵守下列规则:
      (1)不允许访问用户空间;
      (2)current 指针在原子态没有意义;
      (3)不能进行睡眠或者调度.;

2.应用

1.分配内核定时器的对象

struct timer_list mytimer;
(1)通过jiffies获取当前的时间
                jiffies:它是内核时钟节拍数,从内核启动开始,这个值就一直在增加。               
(2)定时器的频率在内核中可以设置,当前内核设置的频率的值在内核目录下.config中保存着。
                如:443 CONFIG_HZ=100 ===>它就是内核定时器的频率,即定时器每增加1走的时间是10ms
2.定时器的初始化

mytimer.expires = jiffies + 100;
timer_setup(&mytimer, 定时器处理函数, 0);

3.注册定时器

void add_timer(struct timer_list *timer)

//注册定时器,在注册定时器的时候,定时器就开始启动了,并且只会执行一次,

//定时器注册只能注册一次,如果再次注册定时器内核会崩溃

int mod_timer(struct timer_list *timer, unsigned long expires)

//功能:当定时器注册完之后,如果想再次启动定时器,需要借助这个函数

4.注销定时器

int del_timer(struct timer_list *timer)

//删除定时器

3.发展现状

      因为受到 jitter 、硬件中断,还有其他定时器和其他异步任务的影响,内核定时器暂时处理单任务具有优势,但是不适合在工业环境中生产系统,如果注册之后再次注册,将会导致内核的崩溃。

(五)中断底半部

      在中断处理函数中是不能够做延时,耗时,甚至休眠的操作,即中断处理函数只能够做简短,不耗时的,紧急的事情。但是有的时候在中断到来的时候又希望做耗时的操作,所以就产生了矛盾。内核为了解决这一矛盾推出了中断底半部的机制。例如在网卡产生中断的时候,需要去网络上读取数据,这个读取数据的过程就是相对耗时操作,所以可以在中断底半部中完成读取网络数据的过程。中断底半部有软中断,tasklet,工作队列这个三个机制。

1.软中断

      软中断在内核中一共有32个,都是给内核使用的,所以驱动工程师一般不能够使用这个软中断来处理相对耗时的操作。

2.tasklet:

      tasklet是基于软中断实现的,tasklet没有个数限制,tasklet工作在中断上下文,里面可以做相对耗时的操作,但是不能够做休眠的操作。tasklet是中断的一个部分不能脱离中断单独执行。在中断顶半部执行即将结束的时候给开启底半部标志位置位即可。

1.分配对象

    struct tasklet_struct{

      struct tasklet_struct *next;//构成内核链表
      unsigned long state; //是否触发的状态
      atomic_t count; //触发的次数
      bool use_callback; //设置为真表示使用新版的函数

      union {

            void (*func)(unsigned long data);
            void (*callback)(struct tasklet_struct *t);//新版的底半部处理函数

      };

      unsigned long data; //向底半部传递的参数

    };

2.对象初始化

    void tasklet_setup(struct tasklet_struct *t,

    void (*callback)(struct tasklet_struct *));

3.调用执行

    void tasklet_schedule(struct tasklet_struct *t);

3.工作队列

      在内核启动的时候默认会启动一个events线程,这个线程维护了一个工作队列,如果你向这个工作队列中提交work,并唤醒这个休眠的进程,此时就可以回调工作队列的底半部处理函数。工作队列工作于进程上下文,可以脱离中断单独执行。工作队列中以做延时,耗时,甚至休眠的操作。

1.分配对象
    struct work_struct {

      /* atomic_long_t data; */

      //向底半部处理函数中传递的参数

      struct list_head entry;//构成队列

      work_func_t func;          //工作队列的底半部处理函数

    };

    typedef void (*work_func_t)(struct work_struct *work);
2.对象初始化
    INIT_WORK(_work, _func)

3对象的调用
    bool schedule_work(struct work_struct *work);


页: [1]
查看完整版本: Linux中的延时休眠函数sleep、usleep、udelay、mdelay、ssleep、jiff...