linux下常用性能分析的指令(Linux时间子系统之POSIXClock)

clock是timer的基础,任何一个timer都需要运作在一个指定的clock上来内核中维护了若干的clock根据计时的特点,clock分成两种:一种是真实世界的时间概念,另外一个是仅仅计算CPU执行时间 从clock的生命周期来看,可以分成静态和动态的posix clock,静态是一直存在于内核中的,而动态clock有创建和销毁的概念,今天小编就来聊一聊关于linux下常用性能分析的指令?接下来我们就一起去研究一下吧!

linux下常用性能分析的指令(Linux时间子系统之POSIXClock)

linux下常用性能分析的指令

一、前言

clock是timer的基础,任何一个timer都需要运作在一个指定的clock上来。内核中维护了若干的clock。根据计时的特点,clock分成两种:一种是真实世界的时间概念,另外一个是仅仅计算CPU执行时间 。从clock的生命周期来看,可以分成静态和动态的posix clock,静态是一直存在于内核中的,而动态clock有创建和销毁的概念。

二、基本概念

1、核心数据结构

所谓clock,实际上就是一种计时工具,可能是硬件,也可能是软件,当然对于POSIX clock而言,当然是指软件抽象了。clock能够记录一段时间的流逝,这段时间可能是真实的墙上时间,也可能是虚拟的时间,例如基于某个进程或者线程的CPU执行时间。在linux kernel中,用struct k_clock来抽象,具体定义如下:

struct k_clock {

int (*clock_getres) (const clockid_t which_clock, struct timespec *tp);

int (*clock_set) (const clockid_t which_clock, const struct timespec *tp);

int (*clock_get) (const clockid_t which_clock, struct timespec * tp);

int (*clock_adj) (const clockid_t which_clock, struct timex *tx);

int (*timer_create) (struct k_itimer *timer);

int (*nsleep) (const clockid_t which_clock, int flags, struct timespec *, struct timespec __user *);

long (*nsleep_restart) (struct restart_block *restart_block);

int (*timer_set) (struct k_itimer * timr, int flags, struct itimerspec * new_setting,

struct itimerspec * old_setting);

int (*timer_del) (struct k_itimer * timr);

void (*timer_get) (struct k_itimer * timr, struct itimerspec * cur_setting);

};

clock作为一个计时工具当然有计时精度,通过clock_getres函数可以获取该clock的时间精度,需要说明的是这个精度是和timer相关的,用于将用户设定的timer超时时间规整到clock精度允许的数值上。clock_get和clock_set函数可以分别获取和设定当前的时间,这个时间值是一个绝对时间值(对于时间轴而言,这个绝对时间也是相对的,是相对于该timeline的epoch而言),标记了当前时间点。clock计时有可能是不准确的,例如基于系统晶振的clock。一方面本身晶振的精度有限,时间累积长了会出现较大误差。另外,晶振也会随着使用时间的推移、温度的变化等等因素而导致误差。clock_adj函数允许系统根据外部的精确时间信息对本clock进行调整。nsleep和nsleep_restart这两个成员函数可以让进程sleep一段时间。timer_xxx系列函数是和POSIX interval timer相关,具体会在POSIX timer文档中描述。

2、静态定义的clock

static struct k_clock posix_clocks[MAX_CLOCKS];

posix_clocks数组定义了系统支持的所有的clock,相关的定义如下:

#define CLOCK_REALTIME 0

#define CLOCK_MONOTONIC 1

#define CLOCK_PROCESS_CPUTIME_ID 2

#define CLOCK_THREAD_CPUTIME_ID 3

#define CLOCK_MONOTONIC_RAW 4

#define CLOCK_REALTIME_COARSE 5

#define CLOCK_MONOTONIC_COARSE 6

#define CLOCK_BOOTTIME 7

#define CLOCK_REALTIME_ALARM 8

#define CLOCK_BOOTTIME_ALARM 9

#define CLOCK_SGI_CYCLE 10 /* Hardware specific */

#define CLOCK_TAI 11

#define MAX_CLOCKS 16

POSIX标准定义了4种类型的clock,CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_PROCESS_CPUTIME_ID和CLOCK_THREAD_CPUTIME_ID,其他是linux specific。如果一个clock的timeline是基于CPU运行时间的,那么我们称之CPU-time clock。CPU-time clock主要是用来为某个进程或者线程的执行时间进行计时的,一旦线程(进程)被切换,那么该clock就停掉了,直到下次调度器切换回该线程(进程)执行。

各个具体的操作系统实现可以定义自己特有的clock,对于Linux kernel,我们定义了若干种clock。CLOCK_MONOTONIC_RAW启动时间点被设成0,此后一直不断累加,而且能设定,不会随NTP调整。CLOCK_REALTIME_COARSE、CLOCK_MONOTONIC_COARSE的概念和CLOCK_REALTIME、CLOCK_MONOTONIC的概念是类似的,只不过是精度是比较粗的版本。有时候,timer没有必要要求那么高的精度,那么我们可以使用这种clock,从而可以获取更好的性能。CLOCK_BOOTTIME和CLOCK_MONOTONIC类似,也是单调上述,在系统初始化的时候设定的基准数值是0,不过CLOCK_BOOTTIME计算系统suspend的时间,也就是说,不论是running还是suspend(这些都算是启动时间),CLOCK_BOOTTIME都会累积计时,直到系统reset或者shutdown。

CLOCK_REALTIME_ALARM和CLOCK_BOOTTIME_ALARM主要用于Alarmtimer,这种timer是基于RTC的,更详细的内容请参考本站Alarmtimer的文档。CLOCK_TAI是原子钟的时间,和基于UTC的CLOCK_REALTIME类似,不过没有leap second。

用户空间的clock_xxx函数会传递clock id的参数,在内核态,根据id作为index在posix_clocks数组中可以索引到对应的clock,然后调用clock对应的callback函数就OK了。当然基本意思就是这样,具体实现如下:

static struct k_clock *clockid_to_kclock(const clockid_t id)

{

if (id < 0)

return (id & CLOCKFD_MASK) == CLOCKFD ?

&clock_posix_dynamic : &clock_posix_cpu;

if (id >= MAX_CLOCKS || !posix_clocks[id].clock_getres)

return NULL;

return &posix_clocks[id];

}

clockid_to_kclock这个函数用来将clock id和具体的posix clock的k_clock 数据结构对应起来。在linux平台上,clockid是int类型的数据,共32个bit,高29个bit用来保存一个pid(用于CPU-time clock)或者fd(动态分配的clock),bit 2用来说明该CPU-time clock是一个进程clock还是线程clock。bit 1和bit 0用来说明该clock id的类型:PROF=0, VIRT=1, SCHED=2, or FD=3。

当clock id小于0的时候,要么是CPU-time clock,要么是动态分配的clock,可以根据clock id的类型来判断。CPU-time clock和动态分配的clock后面会具体介绍。

三、各种real timeclock的定义

系统初始化的时候会调用init_posix_timers函数对各种静态定义的real time clock进行注册。注:monotonic clock也是real time clock的一种,全称是monotonic real time clock。

1、real time clock的定义如下(timer相关内容不在本文描述):

struct k_clock clock_realtime = {

.clock_getres = hrtimer_get_res,

.clock_get = posix_clock_realtime_get,

.clock_set = posix_clock_realtime_set,

.clock_adj = posix_clock_realtime_adj,

.nsleep = common_nsleep,

.nsleep_restart = hrtimer_nanosleep_restart,

};

real time clock需要调用timekeeping模块的接口来获取和设定当前时间值。对于获取当前时间值的函数posix_clock_realtime_get而言,是调用ktime_get_real_ts函数,该函数是timekeeping模块的接口函数,以timespec的格式回了real time clock的当前值。posix_clock_realtime_set函数主要是调用do_settimeofday这个timekeeping模块的接口函数。posix_clock_realtime_adj是调用do_adjtimex接口函数来实现具体的功能。

纳秒级别的sleep是通过高精度timer实现的,real time clock的精度和hrtimer相关,具体可以参考hrtimer相关文档。

2、monotonic clock的定义如下:

struct k_clock clock_monotonic = {

.clock_getres = hrtimer_get_res,

.clock_get = posix_ktime_get_ts,

.nsleep = common_nsleep,

.nsleep_restart = hrtimer_nanosleep_restart,

};

monotonic clock没有clock_set函数,不能被设定。通过ktime_get_ts这个timekeeping模块的接口可以获得monotonic clock的当前值。纳秒级别的sleep以及精度的获取函数和real time clock一样。

3、monotonic raw clock的定义如下:

struct k_clock clock_monotonic_raw = {

.clock_getres = hrtimer_get_res,

.clock_get = posix_get_monotonic_raw,

};

posix_get_monotonic_raw函数是调用timekeeping模块getrawmonotonic接口函数实现获取monotonic raw clock当前时间数值的。和monotonic clock一样,不能设定。和monotonic clock不同的是该clock没有timer相关的callback函数。

4、coarse clock

struct k_clock clock_realtime_coarse = {

.clock_getres = posix_get_coarse_res,

.clock_get = posix_get_realtime_coarse,

};

struct k_clock clock_monotonic_coarse = {

.clock_getres = posix_get_coarse_res,

.clock_get = posix_get_monotonic_coarse,

};

这两个clock的精度都是和tick相关的,KTIME_LOW_RES定义就是tick的纳秒数值。clock_get函数分别调用current_kernel_time和get_monotonic_coarse获取当前时间点的值。

CLOCK_BOOTTIME和CLOCK_TAI的clock实现非常简单,大家自行阅读代码就OK了。

四、CPU-time clock

1、概述

从用户空间的角度看,有两种CPU-time clock的应用场景:

(1)调用clock_xxx函数并传递CLOCK_PROCESS_CPUTIME_ID或者CLOCK_THREAD_CPUTIME_ID给该函数

(2)调用clock_getcpuclockid或者pthread_getcpuclockid函数来获取指定进程或者线程的clock id,之后调用clock_xxx函数并传递该clock id参数

应对第一种场景,系统初始化的时候会调用init_posix_cpu_timers函数对静态定义的CPU-time clock进行注册。对于第二种场景,内核静态定义了一个clock_posix_cpu的clock来应对这种需求。

2、指定进程或者线程的CPU-time clock

内核静态定义了一个clock如下(去掉了timer的callback函数):

struct k_clock clock_posix_cpu = {

.clock_getres = posix_cpu_clock_getres,

.clock_set = posix_cpu_clock_set,

.clock_get = posix_cpu_clock_get,

.nsleep = posix_cpu_nsleep,

.nsleep_restart = posix_cpu_nsleep_restart,

};

(1)获取精度信息

static int posix_cpu_clock_getres(const clockid_t which_clock, struct timespec *tp)

{

int error = check_clock(which_clock);--------参数校验

if (!error) {

tp->tv_sec = 0;

tp->tv_nsec = ((NSEC_PER_SEC HZ - 1) / HZ);

if (CPUCLOCK_WHICH(which_clock) == CPUCLOCK_SCHED) {

tp->tv_nsec = 1;

}

}

return error;

}

该函数的执行逻辑分成两个部分,一部分是参数校验,一部分是返回精度。参数校验需要检查的包括:

(a)clock id中的高29个bit包含了pid,获取pid的代码如下:

#define CPUCLOCK_PID(clock) ((pid_t) ~((clock) >> 3))

从代码可知,实际上并不是将pid放到高29个bit,而是将反码保存到了高29个bit。为何保存反码?这样做为了确保clock id是一个负数(MSB是1),还记得clockid_to_kclock的实现吗?要获取该clock id的精度,要确保该pid的task存在

(b)如果该clock id是一个进程相关的(调用clock_getcpuclockid获得),那么这个进程id应该是一个实实在在的进程id。在linux kernel中,pid实际上是线程ID,POSIX标准的进程ID,也就是PID在内核中被成为线程组ID。因此,所谓一个“实实在在的进程id”就是说该线程的id(pid)和tgid一样,该pid标识的线程是线程组leader。当然,就是获取精度而已,实际上要求并不要那么严格,也许该pid标识的线程leader会退出,因此实际上要求该pid标识的task有thread group leader就OK了。(这里有可能理解有误,TODO)

(c)如果该clock id是一个线程相关的(调用pthread_getcpuclockid获得),那么调用者必须和该线程(clock id中指明的那个线程)属于一个进程(线程组)。

返回精度部分的代码逻辑很简单,对于PROF和VIRT类型的CPU-time clock,其精度是tick,对于SCHED类型,精度是1ns。

(2)获取当前时间值

同样的,首先需要从clock id中获取pid的值,然后根据pid的值获取对应的task sturct,如果pid等于0,那么不需要费劲去寻找。得到task struct之后,可以调用posix_cpu_clock_get_task函数获取时间值:

static int posix_cpu_clock_get_task(struct task_struct *tsk, const clockid_t which_clock,

struct timespec *tp)

{

int err = -EINVAL;

unsigned long long rtn;

if (CPUCLOCK_PERTHREAD(which_clock)) {---per 线程的cpu clock

if (same_thread_group(tsk, current))---必须和调用者是同一个线程组,也就是同一个进程

err = cpu_clock_sample(which_clock, tsk, &rtn);

} else {

if (tsk == current || thread_group_leader(tsk))---进程的cpu clock

err = cpu_clock_sample_group(which_clock, tsk, &rtn);

}

if (!err)

sample_to_timespec(which_clock, rtn, tp); ---给返回值赋值

return err;

}

这里仍然存在校验问题,也就是说是否允许调用者获取该task的CPU-time clock。对于进程,只允许调用者进程获取自己的CPU-time clock,在多线程环境下,主线程(线程组leader)可以获取整个进程的CPU-time clock信息。对于per线程的操作,必须和调用者是同一个线程组,也就是同一个进程。

(a)获取线程的clock信息

static int cpu_clock_sample(const clockid_t which_clock, struct task_struct *p,

unsigned long long *sample)

{

switch (CPUCLOCK_WHICH(which_clock)) {

default:

return -EINVAL;

case CPUCLOCK_PROF:

*sample = prof_ticks(p);----获取该task在用户空间加上在kernel space的执行时间

break;

case CPUCLOCK_VIRT:

*sample = virt_ticks(p);----获取该task在用户空间的执行时间

break;

case CPUCLOCK_SCHED:

*sample = task_sched_runtime(p);----和调度器相关的cpu clock

break;

}

return 0;

}

计算进程或者线程在cpu上的执行时间是一个挺烦人的事,一方面想要精度高,另外一方面又不想计算量大。因此,实际上CPU-time clock有三种,CPUCLOCK_PROF和CPUCLOCK_VIRT这两种都是比较粗略估计CPU执行时间的clock,它的工作原理就是在周期性tick中进行进程cpu time的统计,如果该tick是用户态(timer中断了用户态程序的执行),那么整个tick的时间都是该进程的用户态执行时间。如果该tick是内核态,并且是用户程序进行系统调用而陷入内核,那么整个tick的时间都是该进程的系统态执行时间。

CPUCLOCK_SCHED clock和上面的方法不一样,它的精度是纳秒级别的,是在调度器上进行计算进程时间。具体的计算方法还是留到调度器文章中再描述吧。

(b)cpu_clock_sample_group函数概念类似,不过是统计一个进程上所有线程的时间而已。

3、CLOCK_PROCESS_CPUTIME_ID 类型的clock

struct k_clock process = {

.clock_getres = process_cpu_clock_getres,

.clock_get = process_cpu_clock_get,

.nsleep = process_cpu_nsleep,

.nsleep_restart = process_cpu_nsleep_restart,

};

process_cpu_clock_getres用来获取时间精度,该函数实际是调用posix_cpu_clock_getres(PROCESS_CLOCK, tp)来完成的。process_cpu_clock_get用来获取当前时间值,实际上是通过调用posix_cpu_clock_get完成。posix_cpu_clock_xxx函数在上一节中已经描述。

4、CLOCK_THREAD_CPUTIME_ID类型的clock

很简单,大家自行学习吧。

五、动态分配clock

1、源由

某些硬件提供了计时的能力,可以实现成一个posix clock,同时,这些硬件又类似USB设备那样可以热拔插,这也就意味着该posix clock不能静态定义。此外,除了标准的timer和clock相关的操作,这些提供计时能力的硬件还需要一些其他的类似字符设备界面的控制接口,在这样的需求推动下,内核提供了dynamic posix clock。

2、dynamic posix clock

系统中的每一个dynamic posix clock用struct posix_clock来抽象,如下:

struct posix_clock {

struct posix_clock_operations ops;--------------(1)

struct cdev cdev;----------------------(2)

struct kref kref;

struct rw_semaphore rwsem;

bool zombie;------------------------(3)

void (*release)(struct posix_clock *clk);-------------(4)

};

(1)ops是该dynamic posix clock的操作函数集,分成两个group,一个是timer(例如:timer_create、timer_delete等)以及clock操作相关(例如clock_gettime、clock_settime等),另外一个是普通字符设备的操作函数(例如:open、read、write等)。

(2)该dynamic posix clock对应的cdev数据结构。在struct posix_clock_operations中有一个owner,其实在cdev中也有一个指向moudle的owner成员,看起来似乎是重复定义了。同样的疑问也存在与kref成员,因为在cdev中有kobject成员,kobject抽象了内核最基础的对象类别,包括名字、引用计数等,因此,我觉得只要struct posix_clock包括了cdev成员,struct posix_clock_operations中的owner以及struct posix_clock中的kref应该没有存在的必要了。

(3)zombie记录了底层硬件的状态,对于hotplug的外设,有可能硬件被拔除。rwsem用来保护该状态信息

(4)当reference count等于0的时候会调用release函数释放dynamic posix clock占用的资源。

3、注册和注销

底层的有计时能力的硬件driver可以调用posix_clock_register和posix_clock_unregister来注册或者注销一个posix clock,注册代码如下:

int posix_clock_register(struct posix_clock *clk, dev_t devid)

{

int err;

kref_init(&clk->kref);

init_rwsem(&clk->rwsem);

cdev_init(&clk->cdev, &posix_clock_file_operations);-----VFS接口的操作函数集合

clk->cdev.owner = clk->ops.owner;

err = cdev_add(&clk->cdev, devid, 1);

return err;

}

VFS接口的操作函数集合都非常简单,基本上都是struct posix_clock_operations上的字符设备操作函数集合上。这样,用户空间的程序可以通过标准的文件描述符进行设备操作。

4、clock和timer接口

通过clock_xxx或者timer_xxx函数可以指定clock id,对于dynamic posix clock可以通过下面的操作来生成一个dynamic posix clock ID:

#define FD_TO_CLOCKID(fd) ((~(clockid_t) (fd) << 3) | CLOCKFD)

其中fd是通过设备节点打开的那个有计时能力的硬件。在内核态会通过clockid_to_kclock操作将clock id转换成

static struct k_clock *clockid_to_kclock(const clockid_t id)

{

if (id < 0)

return (id & CLOCKFD_MASK) == CLOCKFD ?

&clock_posix_dynamic : &clock_posix_cpu;

……

}

clock_posix_dynamic可以将dynamic posix clock ID转换成对应的posix_clock,然后调用struct posix_clock_operations上的time和clock相关的函数即可。

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页