当前位置: 首页 > news >正文

Linux时间子系统7:sleep timer接口定时实现

1、前言

        之前的文章中介绍了Linux时间相关的内容,包括用户态/内核态的时间获取,时间的种类,时钟源等,本篇开始的后续几篇文章将介绍Linux系统关于定时相关的服务,这与之前的内容是高度相关的,本篇还是从应用的角度出发,先看Linux用户态和内核态的操作访问接口。

        本篇文章主要是对下面两篇文章的整理,同时增加了部分个人的理解:
Linux时间子系统之(三):用户空间接口函数

Linux内核时钟系统和定时器实现 - 知乎

2、用户态定时器相关API接口

2.1 sleep系列

我们经常遇到过具有睡眠功能的库函数API接口:

sleep()
usleep()
nanosleep()
clock_nanosleep

sleep:

调用该函数会导致当前进程sleep,seconds之后(基于CLOCK_REALTIME)会返回继续执行程序。该函数的返回值说明了进程没有进入睡眠的时间。例如如果我们想要睡眠8秒,但是由于siganl中断了睡眠,只是sleep了5秒,那么返回值就是3,表示有3秒还没有睡。

sleep在musl1.2.3中的实现如下,可以看到sleep调用了nanosleep

unsigned sleep(unsigned seconds)
{struct timespec tv = { .tv_sec = seconds, .tv_nsec = 0 };if (nanosleep(&tv, &tv))return tv.tv_sec;return 0;
}

usleep:

usleep支持精度更高的微妙级别的定时操作,同样usleep也调用了nanosleep

int usleep(unsigned useconds)
{struct timespec tv = {.tv_sec = useconds/1000000,.tv_nsec = (useconds%1000000)*1000};return nanosleep(&tv, &tv);
}

nanosleep:

#include <time.h>int nanosleep(const struct timespec *req, struct timespec *rem);

usleep函数已经是过去式,不建议使用,取而代之的是nanosleep函数。req中设定你要sleep的秒以及纳秒值,然后调用该函数让当前进程sleep。返回0表示执行成功,返回-1说明执行失败,错误码在errno中获取。EINTR表示该函数被signal打断。rem参数是remaining time的意思,也就是说还有多少时间没有睡完。

linux kernel并没有提供sleep和usleep对应的系统调用,sleep和usleep的实现位于c lib。在有些系统中,这些实现是依赖信号的,也有的系统使用timer来实现的,对于GNU系统,sleep和usleep和nanosleep函数一样,都是通过kernel的sys_nanosleep的系统调用实现的(底层是基于hrtimer)。

nanosleep在musl中的实现,调用了clock_nanosleep系统调用

int nanosleep(const struct timespec *req, struct timespec *rem)
{return __syscall_ret(-__clock_nanosleep(CLOCK_REALTIME, 0, req, rem));
}

clock_nanosleep:
 

int clock_nanosleep(clockid_t clock_id, int flags,const struct timespec *request,struct timespec *remain);

clock_nanosleep接口函数需要传递更多的参数,当然也就是意味着它功能更强大。clock_id说明该接口函数不仅能基于real time clock睡眠,还可以基于其他的系统时钟睡眠,可以翻看以下之前对于系统时间种类的介绍。flag等于0或者1,分别指明request参数设定的时间值是相对时间还是绝对时间。

OK,到这里我们可以看到,对于musl而言,sleep的相关函数实际上都是基于clock_nanosleep系统调用,我们看一下clock_nanosleep的内核实现,它是这样的:

SYSCALL_DEFINE4(clock_nanosleep, const clockid_t, which_clock, int, flags,const struct __kernel_timespec __user *, rqtp,struct __kernel_timespec __user *, rmtp)
{const struct k_clock *kc = clockid_to_kclock(which_clock);struct timespec64 t;if (!kc)return -EINVAL;if (!kc->nsleep)return -EOPNOTSUPP;if (get_timespec64(&t, rqtp))return -EFAULT;if (!timespec64_valid(&t))return -EINVAL;if (flags & TIMER_ABSTIME)rmtp = NULL;current->restart_block.fn = do_no_restart_syscall;current->restart_block.nanosleep.type = rmtp ? TT_NATIVE : TT_NONE;current->restart_block.nanosleep.rmtp = rmtp;return kc->nsleep(which_clock, flags, &t);
}

可以看到,在内核,clock_nanosleep最终调用了posix clocks注册的nsleep函数,我们以clock_realtime为例,可以看到注册的nsleep函数是common_nsleep,而common_nsleep最终调用到了hrtimer_nanosleep。好的,我们先记住这个hrtimer。

static int common_nsleep(const clockid_t which_clock, int flags,const struct timespec64 *rqtp)
{ktime_t texp = timespec64_to_ktime(*rqtp);return hrtimer_nanosleep(texp, flags & TIMER_ABSTIME ?HRTIMER_MODE_ABS : HRTIMER_MODE_REL,which_clock);
}

2.2 timer系列

alarm:

alarm()函数可以设置一个定时器,在特定时间超时,并产生SIGALRM信号,如果不忽略或不捕捉该信号,该进程会被终止。

alarm在musl1.2.3中的实现如下:

unsigned alarm(unsigned seconds)
{struct itimerval it = { .it_value.tv_sec = seconds }, old = { 0 };setitimer(ITIMER_REAL, &it, &old);return old.it_value.tv_sec + !!old.it_value.tv_usec;
}

可以看到alarm是基于间隔定时器itimer来实现的,调用了setitimer系统调用,至于setitimer后面会讲到,是基于hrtimer实现的。

Interval timer:

#include <sys/time.h>int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);struct itimerval {struct timeval it_interval; /* next value */struct timeval it_value;    /* current value */
};

Interval timer函数的行为和alarm函数类似,不过功能更强大。每个进程支持3种timer,不同的timer定义了如何计时以及发送什么样的信号给进程,which参数指明使用哪个timer:

(1)ITIMER_REAL。基于CLOCK_REALTIME计时,超时后发送SIGALRM信号,和alarm函数一样。

(2)ITIMER_VIRTUAL。只有当该进程的用户空间代码执行的时候才计时,超时后发送SIGVTALRM信号。

(3)ITIMER_PROF。只有该进程执行的时候才计时,不论是执行用户空间代码还是陷入内核执行(例如系统调用),超时后发送SIGPROF信号。

两个成员分别指明了本次和下次(超期后如何设定)的时间值。通过这样的定义,interval timer可以实现one shot类型的timer和periodic的timer。例如current value设定为5秒,next value设定为3秒,设定这样的timer后,it_value值会不断递减,直到5秒后触发,而随后it_value的值会被重新加载(使用it_interval的值),也就是等于3秒,之后会按照3为周期不断的触发。

old_value返回上次setitimer函数的设定值。getitimer函数获取当前的Interval timer的状态,其中的it_value成员可以得到当前时刻到下一次触发点的世时间信息,it_interval成员保持不变,除非你重新调用setitimer重新设定。

虽然interval timer函数也是POSIX标准的一部分,不过在新的POSIX标准中,interval timer接口函数被标注为obsolescent,取而代之的是POSIX timer接口函数。

在Linux 2.6.16 之前,itimer的实现是基于内核定时器timer wheel封装成的定时器接口。内核封装的定时器接口是提供给其他内核模块使用,也是其他定时器的基础。itimer通过内核定时器的封装,生成提供给用户层使用的接口setitimer和getitimer。内核定时器timer wheel提供的内核态调用接口可参考

add_timer() 
del_timer() 
init_timer()

当然现在的setitimer和getitimer是基于hrtimer的,我们以musl为例看一下:

int setitimer(int which, const struct itimerval *restrict new, struct itimerval *restrict old)
{if (sizeof(time_t) > \sizeof(long)) {time_t is = new->it_interval.tv_sec, vs = new->it_value.tv_sec;long ius = new->it_interval.tv_usec, vus = new->it_value.tv_usec;if (!IS32BIT(is) || !IS32BIT(vs))return __syscall_ret(-ENOTSUP);long old32[4];int r = __syscall(SYS_setitimer, which,((long[]){is, ius, vs, vus}), old32);if (!r && old) {old->it_interval.tv_sec = old32[0];old->it_interval.tv_usec = old32[1];old->it_value.tv_sec = old32[2];old->it_value.tv_usec = old32[3];}return __syscall_ret(r);}return syscall(SYS_setitimer, which, new, old);
}

内核对应的实现:

SYSCALL_DEFINE3(setitimer, int, which, struct __kernel_old_itimerval __user *, value,struct __kernel_old_itimerval __user *, ovalue)
{struct itimerspec64 set_buffer, get_buffer;int error;if (value) {error = get_itimerval(&set_buffer, value);if (error)return error;} else {memset(&set_buffer, 0, sizeof(set_buffer));printk_once(KERN_WARNING "%s calls setitimer() with new_value NULL pointer."" Misfeature support will be removed\n",current->comm);}error = do_setitimer(which, &set_buffer, ovalue ? &get_buffer : NULL);if (error || !ovalue)return error;if (put_itimerval(ovalue, &get_buffer))return -EFAULT;return 0;
}

跟踪do_setitimer可以看到又是调用了hrtimer

alrm和itimer

函数alarm本质上设置的是低精确、非重载的ITIMER_REAL类定时器,它只能精确到秒,并且每次设置只能产生一次定时。函数setitimer 设置的定时器则不同,它们不但可以计时到微妙(理论上),还能自动循环定时。在一个Unix进程中,不能同时使用alarm和ITIMER_REAL类定时器。

2.3 posix timer

POSIX定时器的是为了解决间隔定时器itimer的以下问题:

  • 一个进程同一时刻只能有一个同一种类型(ITIMER_REAL, ITIMER_PROF, ITIMER_VIRT)的itimer。POSIX定时器在一个进程中可以创建任意多个timer。
  • itimer定时器到期后,只能通过信号(SIGALRM,SIGVTALRM,SIGPROF)的方式通知进程,POSIX定时器到期后不仅可以通过信号进行通知,还可以使用自定义信号,还可以通过启动一个线程来进行通知。
  • itimer支持us级别,POSIX定时器支持ns级别。

POSIX定时器提供的定时器API如下:

int timer_create(clockid_t clock_id, struct sigevent *evp, timer_t *timerid);
int timer_settime(timer_t timerid, int flags, const struct itimerspec *value, struct itimerspect *ovalue);
int timer_gettime(timer_t timerid,struct itimerspec *value);
int timer_getoverrun(timer_t timerid);
int timer_delete (timer_t timerid);

(1)创建timer

从timer_create我们就可以看到,这和posix 时间接口就很像了,首先它需要用户指定clock id,这就意味着它可以指定clock创建定时器。

struct sigevent设置了定时器到期时的通知方式和处理方式等,结构的定义如下:

struct sigevent
{int sigev_notify;   //设置定时器到期后的行为int sigev_signo;    //设置产生信号的信号码union sigval   sigev_value; //设置产生信号的值void (*sigev_notify_function)(union sigval);//定时器到期,从该地址启动一个线程pthread_attr_t *sigev_notify_attributes;    //创建线程的属性
}union sigval
{int sival_int;  //integer valuevoid *sival_ptr; //pointer value
}

如果sigevent传入NULL,那么定时器到期会产生默认的信号,对CLOCK_REALTIMER来说,默认信号就是SIGALRM,如果要产生除默认信号之外的其他信号,程序必须将evp->sigev_signo设置为期望的信号码。

如果几个定时器产生了同一个信号,处理程序可以用 sigev_value来区分是哪个定时器产生了信号。要实现这种功能,程序必须在为信号安装处理程序时,使用struct sigaction的成员sa_flags中的标志符SA_SIGINFO。

sigev_notify定义了当timer超期后如何通知该进程,可以设定:

(a)SIGEV_NONE。不需要异步通知,程序自己调用timer_gettime来轮询timer的当前状态

(b)SIGEV_SIGNAL。使用sinal这样的异步通知方式。发送的信号由sigev_signo定义。如果发送的是realtime signal,该信号的附加数据由sigev_value定义。

(c)SIGEV_THREAD。创建一个线程执行timer超期callback函数,_attribute定义了该线程的属性。

(d)SIGEV_THREAD_ID。行为和SIGEV_SIGNAL类似,不过发送的信号被送达进程内的一个指定的thread,这个thread由_tid标识。

(2)设定timer

timerid就是上一节中通过timer_create创建的timer。new_value和old_value这两个参数类似setitimer函数,这里就不再细述了。flag等于0或者1,分别指明new_value参数设定的时间值是相对时间还是绝对时间。如果new_value.it_value是一个非0值,那么调用timer_settime可以启动该timer。如果new_value.it_value是一个0值,那么调用timer_settime可以stop该timer。

timer_gettime函数和getitimer类似,可以参考上面的描述。

(3)删除timer

有创建就有删除,timer_delete用来删除指定的timer,释放资源。

我们照例来看一下musl和对应内核的实现

int timer_create(clockid_t clk, struct sigevent *restrict evp, timer_t *restrict res)
{volatile static int init = 0;pthread_t td;pthread_attr_t attr;int r;struct start_args args;struct ksigevent ksev, *ksevp=0;int timerid;sigset_t set;switch (evp ? evp->sigev_notify : SIGEV_SIGNAL) {case SIGEV_NONE:case SIGEV_SIGNAL:case SIGEV_THREAD_ID:if (evp) {ksev.sigev_value = evp->sigev_value;ksev.sigev_signo = evp->sigev_signo;ksev.sigev_notify = evp->sigev_notify;if (evp->sigev_notify == SIGEV_THREAD_ID)ksev.sigev_tid = evp->sigev_notify_thread_id;elseksev.sigev_tid = 0;ksevp = &ksev;}if (syscall(SYS_timer_create, clk, ksevp, &timerid) < 0)return -1;*res = (void *)(intptr_t)timerid;break;case SIGEV_THREAD:if (!init) {struct sigaction sa = { .sa_handler = SIG_DFL };__libc_sigaction(SIGTIMER, &sa, 0);a_store(&init, 1);}if (evp->sigev_notify_attributes)attr = *evp->sigev_notify_attributes;elsepthread_attr_init(&attr);pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);pthread_barrier_init(&args.b, 0, 2);args.sev = evp;__block_app_sigs(&set);__syscall(SYS_rt_sigprocmask, SIG_BLOCK, SIGTIMER_SET, 0, _NSIG/8);r = pthread_create(&td, &attr, start, &args);__restore_sigs(&set);if (r) {errno = r;return -1;}ksev.sigev_value.sival_ptr = 0;ksev.sigev_signo = SIGTIMER;ksev.sigev_notify = SIGEV_THREAD_ID;ksev.sigev_tid = td->tid;if (syscall(SYS_timer_create, clk, &ksev, &timerid) < 0)timerid = -1;td->timer_id = timerid;pthread_barrier_wait(&args.b);if (timerid < 0) return -1;*res = (void *)(INTPTR_MIN | (uintptr_t)td>>1);break;default:errno = EINVAL;return -1;}return 0;
}

可以看到POSIX定时器通过调用内核的posix_timer系统调用进行实现,但c库对POSIX timer进行了一定的封装,例如如果POSIX timer到期通知方式被设置为 SIGEV_THREAD 时,glibc 需要自己完成一些辅助工作,因为内核无法在 Timer 到期时启动一个新的线程。

我们再看内核对应的实现:

SYSCALL_DEFINE3(timer_create, const clockid_t, which_clock,struct sigevent __user *, timer_event_spec,timer_t __user *, created_timer_id)
{if (timer_event_spec) {sigevent_t event;if (copy_from_user(&event, timer_event_spec, sizeof (event)))return -EFAULT;return do_timer_create(which_clock, &event, created_timer_id);}return do_timer_create(which_clock, NULL, created_timer_id);
}

跟踪do_timer_create可以看到,同样是调用了注册的posix clocks对应的timer create函数,我们仍然以clock_realtime为例,可以看到实际调用的是hrtimer_init。

3、内核态睡眠、定时器相关API接口

睡眠相关接口:

usleep_range(umin,umax);  //推荐使用
msleep(ms);
msleep_interruptible(ms);
ssleep(s);

低精度定时器相关接口:

1. 初始化:

#define timer_setup(timer, callback, flags)			\__init_timer((timer), (callback), (flags))#define DEFINE_TIMER(_name, _function)				\struct timer_list _name =				\__TIMER_INITIALIZER(_function, 0)

2. mod_timer

内核通过函数mod_timer来修改已经激活的定时器超时时间,也可以用来激活已经初始化但是没有激活的定时器。

int mod_timer(struct timer_list *timer, unsigned long expires)
{return __mod_timer(timer, expires, 0);
}
EXPORT_SYMBOL(mod_timer);

3. add_timer

void add_timer(struct timer_list *timer)
{BUG_ON(timer_pending(timer));__mod_timer(timer, timer->expires, MOD_TIMER_NOTPENDING);
}
EXPORT_SYMBOL(add_timer);

如果要指定运行定时器的cpu,可以使用

void add_timer_on(struct timer_list *timer, int cpu)

4. 删除定时器

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

同add_timer函数相反,del_timer类的函数负责从系统的定时器管理队列中摘除一个定时器对象。

对于del_timer函数要摘除的定时器对象timer,函数会首先判断该对象是否是一个pending的定时器,一个处于pending状态的定时器是处在处理器的定时器管理队列中正等待被调度执行的定时器对象。如果一个要被del_timer函数删除的timer对象已经被调度执行(内核源码称这种定时器状态为inactive),函数将直接返回0,否则函数将通过detach_timer将该定时器对象从队列中删除。

在多处理器的SMP系统中,del_timer_sync函数要完成的任务除了同del_timer一样从定时器队列中删除一个定时器对象外,还会确保当函数返回时系统中没有任何处理器正在执行定时器对象上的定时器函数(这里没有提到,如果有,就会一直等待完成),而如果只是调用del_timer,那么当函数返回时,被删除的定时器对象的定时器函数可能正在其他处理器上运行。

  • 内核定时器会在超时后主动删除,del_timer主要用来删除那些没有超时的定时器。
  • 对于多处理器,使用del_timer_sync要比del_timer安全,del_timer_sync会等待定时器执行完成。
  • del_timer_sync不能在中断上下文使用(会引起死锁),只能在进程上下文使用

定时器的回调函数是在中断上下文中执行,所以使用中断上下文的注意事项依然适用于定时器回调函数。

  • 定时器回调函数需要保护共享数据。
  • 定时器回调函数中不能触发进程调度。
  • 定时器回调函数中不能睡眠。
  • 定时器回调函数中不能使用信号量。

高精度定时器相关接口:
初始化:

void hrtimer_init(struct hrtimer *timer, clockid_t clock_id,enum hrtimer_mode mode)EXPORT_SYMBOL_GPL(hrtimer_init);

启动:

static inline void hrtimer_start(struct hrtimer *timer, ktime_t tim,const enum hrtimer_mode mode)

取消/删除

int hrtimer_cancel(struct hrtimer *timer)

以上内核定时器详细的内容后面再补充。

相关文章:

  • Anti-human IL-10 mAb (12G8), biotin:Mabtech热销品
  • vue大作业-实现学校官网
  • 【杂记-浅谈Sequence Number/序列号】
  • 第三方美颜SDK开发详解:直播美颜工具的功能与技术实现
  • upload-labs第十二关教程
  • 【Redis】基于Redission实现分布式锁(代码实现)
  • macOS聚集搜索功能开启与关闭
  • Excel/WPS《超级处理器》功能介绍与安装下载
  • 判断单链表是否带环且返回节点
  • 云原生巡检监控报告
  • newtonsoft.json动态读取json以及动态生成
  • vue2 + element-ui,前端配置化表单封装(2024-06-14)
  • 对象的扩展
  • Golang 并发编程(Goroutine、Channels、Select、Sync、原子操作函数、Context、gpool)
  • 深入探索面向对象编程(OOP):封装、继承和多态的实际应用
  • python3.6+scrapy+mysql 爬虫实战
  • CODING 缺陷管理功能正式开始公测
  • Debian下无root权限使用Python访问Oracle
  • ES6系列(二)变量的解构赋值
  • Java 9 被无情抛弃,Java 8 直接升级到 Java 10!!
  • JS正则表达式精简教程(JavaScript RegExp 对象)
  • Mac转Windows的拯救指南
  • Redis在Web项目中的应用与实践
  • SegmentFault 技术周刊 Vol.27 - Git 学习宝典:程序员走江湖必备
  • Spring Cloud(3) - 服务治理: Spring Cloud Eureka
  • 初识MongoDB分片
  • 大主子表关联的性能优化方法
  • 基于MaxCompute打造轻盈的人人车移动端数据平台
  • 计算机在识别图像时“看到”了什么?
  • 将 Measurements 和 Units 应用到物理学
  • 可能是历史上最全的CC0版权可以免费商用的图片网站
  • 目录与文件属性:编写ls
  • 如何设计一个比特币钱包服务
  • 算法---两个栈实现一个队列
  • 腾讯优测优分享 | 你是否体验过Android手机插入耳机后仍外放的尴尬?
  • 一个6年java程序员的工作感悟,写给还在迷茫的你
  • 好程序员大数据教程Hadoop全分布安装(非HA)
  • ​卜东波研究员:高观点下的少儿计算思维
  • ‌[AI问答] Auto-sklearn‌ 与 scikit-learn 区别
  • # Apache SeaTunnel 究竟是什么?
  • # 睡眠3秒_床上这样睡觉的人,睡眠质量多半不好
  • #Linux(make工具和makefile文件以及makefile语法)
  • #控制台大学课堂点名问题_课堂随机点名
  • #数学建模# 线性规划问题的Matlab求解
  • $(this) 和 this 关键字在 jQuery 中有何不同?
  • (C#)获取字符编码的类
  • (C语言)编写程序将一个4×4的数组进行顺时针旋转90度后输出。
  • (pycharm)安装python库函数Matplotlib步骤
  • (Pytorch框架)神经网络输出维度调试,做出我们自己的网络来!!(详细教程~)
  • (Redis使用系列) SpirngBoot中关于Redis的值的各种方式的存储与取出 三
  • (附源码)spring boot球鞋文化交流论坛 毕业设计 141436
  • (附源码)springboot家庭装修管理系统 毕业设计 613205
  • (附源码)计算机毕业设计SSM基于java的云顶博客系统
  • (附源码)计算机毕业设计SSM疫情社区管理系统
  • (学习日记)2024.03.12:UCOSIII第十四节:时基列表