以qcom8核armv8的SOC为例,把cpu本地定时器称为arch_timer,全局计数器Systemcounter不需要专门的dts节点,配置如下:
arch_timer:timer{compatible="arm,armv8-timer";interrupts=
4种定时器与dts中的对应关系如下:
软件中断号13:ARCH_TIMER_PHYS_SECURE_PPI,安全世界物理定时器私有中断,SecureEL1physicaltimer
软件中断号14:ARCH_TIMER_PHYS_NONSECURE_PPI,非安全世界物理定时器私有中断,Non-secureEL1physicaltimer
软件中断号11:ARCH_TIMER_VIRT_PPI,虚拟定时器私有中断,vitualtimer
软件中断号12:ARCH_TIMER_HYP_PPI,hypervisor定时器私有中断,Non-secureEL2physicaltimer
虚拟定时器和物理定时器的差别在于,虚拟定时器的Systemcounter计数输入会在实际Systemcounter基础上添加一个偏移量,这个偏移量由host控制,来实现不同guest的无感切换。
一般外设中断都是dts配置硬件中断号,然后软件映射到一个软件中断号,最后用软件中断号向系统注册中断,这里dts配置的直接就是软件中断号,因为linux中断系统中为SGI和PPI预留有专用软件中断号。
上面配置的定时器,cpu可以通过cp15协处理器来访问定时器寄存器,称为cp15类型定时器。arm还提供一种内存映射方式访问的定时器,称为内存映射定时器(MemoryMapTimer),是将定时器寄存器映射到io区域,注册在系统中的名称为“arch_mem_timer”,主要为了没有协处理器的版本使用。
TIMER_OF_DECLARE(armv8_arch_timer,“arm,armv8-timer”,arch_timer_of_init);
在内核初始化流中路径如下:
从开机0s的log中看到arch_timer的初始化情况,初始化了cp15和mmio2种定时器,时钟频率都是19.2MHz,都是虚拟的timer。系统启动早期只初始化cpu0的arch_timer,然后通过cpuhp_setup_state()设置了热插拔cpu时的注册和注销定时器函数,随着后续多核的启动以及下线,其他cpu的arch_timer也会陆续初始化注册或注销。
[0.000000]arch_timer:cp15andmmiotimer(s)runningat19.20MHz(virt/virt).运行时查看当前系统的ClockEventdevice,一个内存映射定时器arch_mem_timer和8个本地定时器arch_sys_timer
adbshellhead/sys/devices/system/clockevents/*/current_device==>/sys/devices/system/clockevents/broadcast/current_device<==arch_mem_timer==>/sys/devices/system/clockevents/clockevent0/current_device<==arch_sys_timer==>/sys/devices/system/clockevents/clockevent1/current_device<==arch_sys_timer==>/sys/devices/system/clockevents/clockevent2/current_device<==arch_sys_timer==>/sys/devices/system/clockevents/clockevent3/current_device<==arch_sys_timer==>/sys/devices/system/clockevents/clockevent4/current_device<==arch_sys_timer==>/sys/devices/system/clockevents/clockevent5/current_device<==arch_sys_timer==>/sys/devices/system/clockevents/clockevent6/current_device<==arch_sys_timer==>/sys/devices/system/clockevents/clockevent7/current_device<==arch_sys_timer通过cat/proc/interrupts查看中断计数,arch_timer中断触发频率非常高,仅次于2个高频的IPI中断,可见其重要性。软件IRQ号11,对应dts中的第3个ARCH_TIMER_VIRT_PPI,硬件中断号27。
IRQCPU0CPU1CPU2CPU3CPU4CPU5CPU6CPU711:741381292651385122237327418698511742GICv327Levelarch_timerIPI0:2848814481304062944032710200242350830199ReschedulinginterruptsIPI1:578917668334417521605160916651262FunctioncallinterruptsIPI2:00000000CPUstopinterruptsIPI3:00000000CPUstop(forcrashdump)interruptsIPI4:00000000TimerbroadcastinterruptsIPI5:362899561139931380919441147371569117867IRQworkinterruptsIPI6:00000000CPUwake-upinterrupts2.3时钟源clockSource初始化初始化完定时器arch_timer,接下来就会初始化时钟源,arm上称为arch_counter,通过clocksource结构体描述,成员初值如下:
staticstructclocksourceclocksource_counter={.name="arch_sys_counter",.rating=400,.read=arch_counter_read,.mask=CLOCKSOURCE_MASK(56),.flags=CLOCK_SOURCE_IS_CONTINUOUS,};.name=“arch_sys_counter”,时钟源名称为”arch_sys_counter”.rating=400表示其精度很高,是理想的时钟源.mask=CLOCKSOURCE_MASK(56),56位有效bit.flags=CLOCK_SOURCE_IS_CONTINUOUS,连续计数
软件初始化过程如下
u64(*arch_timer_read_counter)(void)__ro_after_init=arch_counter_get_cntvct;static__always_inlineu64__arch_counter_get_cntvct(void){u64cnt;asmvolatile(ALTERNATIVE("isb\nmrs%0,cntvct_el0","nop\n"__mrs_s("%0",SYS_CNTVCTSS_EL0),ARM64_HAS_ECV):"=r"(cnt));arch_counter_enforce_ordering(cnt);returncnt;}从开机log看出,创建了一个clocksource,名为arch_sys_counter,mask:0xffffffffffffff表示56位有效位数,然后注册了sched_clock,56bit有效位,19MHz频率,分辨率52ns。系统还有一个jiffies时钟源,但是精度太低了,最后系统选择arch_sys_counter作为clocksourcedevice
[0.000000]clocksource:arch_sys_counter:mask:0xffffffffffffffmax_cycles:0x46d987e47,max_idle_ns:440795202767ns[0.000000]sched_clock:56bitsat19MHz,resolution52ns,wrapsevery4398046511078ns[0.044957]clocksource:jiffies:mask:0xffffffffmax_cycles:0xffffffff,max_idle_ns:7645041785100000ns[0.075704]clocksource:Switchedtoclocksourcearch_sys_counter运行时查看当前时钟源为arch_sys_counter
高精度版本:高精度版本会读取硬件计数器值来保证纳秒级精度,访问临界区时加顺序锁,精度最高,速度相对其他版本较慢;
低精度版本:低精度版本直接返回上次tick更新的base值,不涉及硬件操作,精度低,速度快;
高精度快速版本:相对于高精度版本区别在于访问临界区时加顺序锁类型不同,精度和速度都介于上两者。
下面是提供的内核接口函数,根据函数名和参数类型大致可以顾名思义,函数名以_ts64结尾的表示获取timespec64格式,函数名带_coarse表示低精度版本,带_fast_ns的表示高精度快速版本。
printk()->_printk()->vprintk()->vprintk_default()->vprintk_emit()->vprintk_store()->local_clock()->sched_clock()->read_sched_clock()->arch_counter_read()
vdso整个执行过程代码调用流程如下:
static__always_inlineu64__arch_get_hw_counter(s32clock_mode,70conststructvdso_data*vd){u64res;*IfFEAT_ECVisavailable,usetheself-synchronizingcounter.*Otherwisetheisbisrequiredtopreventthatthecountervalue*isspeculated.*/asmvolatile(ALTERNATIVE("isb\n""mrs%0,cntvct_el0","nop\n"__mrs_s("%0",SYS_CNTVCTSS_EL0),ARM64_HAS_ECV):"=r"(res)::"memory");arch_counter_enforce_ordering(res);returnres;}linux-vdso.so.1这个库不依赖其他库,并且会被系统绝大部分so依赖,包括libc.so,非常有牌面。所有用户进程都会映射vdso到自己的地址空间。
从最早的嵌入式设备到大型计算机系统,定时器都是必备的模块。随着各种硬件的升级,linux上的定时器软件也在不断变化,这里不去过多追溯历史版本和兼容情况,以当前linux6.1版本arm64安卓设备为例做介绍。
在定时器中断到来时进入硬中断处理函数hrtimer_interrupt(),如果最近到期的任务是硬timer,则继续在当前中断环境下处理。如果是软timer,则挂起软中断HRTIMER_SOFTIRQ,软中断在hrtimer_run_softirq()中处理软timer任务。
staticstructcpuhp_stepcpuhp_hp_states[]={[CPUHP_HRTIMERS_PREPARE]={.name="hrtimers:prepare",.startup.single=hrtimers_prepare_cpu,.teardown.single=hrtimers_dead_cpu,},}4.2hrtimer的使用在内核中直接使用hrtimer示例代码如下,每8.3ms周期性触发并打印log。
其初始化入口为
start_kernel()|-->init_timers()|-->init_timer_cpus()|-->posix_cputimers_init_work()|-->open_softirq(TIMER_SOFTIRQ,run_timer_softirq)hrtimer切换到高精度模式时,如下
add_timer(structtimer_list*timer);|-->__mod_timer(timer,timer->expires,MOD_TIMER_NOTPENDING);|-->internal_add_timer(base,timer);|-->idx=calc_wheel_index(timer->expires,base->clk,&bucket_expiry);|-->returncalc_index(expires,lvl,bucket_expiry);|-->enqueue_timer(base,timer,idx,bucket_expiry);staticinlineunsignedcalc_index(unsignedlongexpires,unsignedlvl,unsignedlong*bucket_expiry){unsignedidx;trace_android_vh_timer_calc_index(lvl,&expires);//*expires-=1expires=(expires>>LVL_SHIFT(lvl))+1;*bucket_expiry=expires staticintcopy_signal(unsignedlongclone_flags,structtask_struct*tsk){#ifdefCONFIG_POSIX_TIMERSINIT_LIST_HEAD(&sig->posix_timers);hrtimer_init(&sig->real_timer,CLOCK_MONOTONIC,HRTIMER_MODE_REL);sig->real_timer.function=it_real_fn;#endif}enumhrtimer_restartit_real_fn(structhrtimer*timer){structsignal_struct*sig=container_of(timer,structsignal_struct,real_timer);structpid*leader_pid=sig->pids[PIDTYPE_TGID];trace_itimer_expire(ITIMER_REAL,leader_pid,0);kill_pid_info(SIGALRM,SEND_SIG_PRIV,leader_pid);returnHRTIMER_NORESTART;}ITIMER_REAL使用示例 SYSCALL_DEFINE1(alarm,unsignedint,seconds){returnalarm_setitimer(seconds);}staticunsignedintalarm_setitimer(unsignedintseconds){structitimerspec64it_new,it_old;#ifBITS_PER_LONG<64if(seconds>INT_MAX)seconds=INT_MAX;#endifit_new.it_value.tv_sec=seconds;it_new.it_value.tv_nsec=0;it_new.it_interval.tv_sec=it_new.it_interval.tv_nsec=0;do_setitimer(ITIMER_REAL,&it_new,&it_old);/**Wecan'treturn0ifwehaveanalarmpending...Andwe'd*betterreturntoomuchthantoolittleanyway*/if((!it_old.it_value.tv_sec&&it_old.it_value.tv_nsec)||it_old.it_value.tv_nsec>=(NSEC_PER_SEC/2))it_old.it_value.tv_sec++;returnit_old.it_value.tv_sec;}进程在调用alarm定时之前,需要设置SIGALRM信号的处理函数。参数很简单,只有一个秒,使用代码示例如下 #include#include#includevoidsig_handler(intsignum){printf("ReceivedSIGALRM,timerexpired!\n");}intmain(){signal(SIGALRM,sig_handler);alarm(5);printf("Waitingforalarm...\n");pause();//Suspendtheprocessuntilasignalisreceivedprintf("Exiting...\n");return0;}6.4PosixtimerPosixtimer大大扩展了itimer的功能,一个进程可以同时创建任意个timer,并且可以指定到期信号。Posixtimer封装了多个syscall接口: