原文出处:http://developers.sun.com/solaris/articles/kstatc.html
Solaris 内核统计信息 - 使用C访问libkstat
引言
为了向外界导出特定模块的统计信息,Solaris内核向设备驱动程序和其它内核模块提供了很多数据结构和函数集。这个底层结构被称为kstat,它向Solaris软件开发者提供:
供设备驱动程序和其它内核模块提交统计信息的C语言函数。
供应用程序从Solaris取得统计信息数据而不需要直接访问内核内存的C语言函数。
使用基于Perl的命令行程序/usr/bin/kstat重复访问统计信息数据或者使用shell脚本(在solaris 8里介绍)。
Solaris的libkstat库包含用于从应用程序访问kstat的C语言函数。这些函数利用伪设备/dev/kstat提供一个到内核数据的安全接口,避免需要程序设置UID为root。
因为很多开发者对通过C程序访问内核统计信息很感兴趣,这篇文章关注使用libkstat。它解释数据结构和函数,并提供代码例子使你能够轻松开始使用这个程序库。
数据结构概述
Solaris的内核统计信息由一个被称为kstat链的链接表结构维护。每一个kstat都有一个通用头部和类型专属数据段。这个链在系统启动的时候被初始化,但是因为Solaris是一个动态操作系统,这个链会随时改变。如果内核需要可以向系统添加或者从系统里删除kstat的入口。例如,当使用动态配置向一个正在运行的系统添加插件板和它的所有附加组件,相应的新硬件驱动程序和内核模块kstat入口就被插入到链中。
图一:kstat链
kstat结构中的ks_data成员指向kstat的数据段。很多数据类型被支持:raw,
named,timer,interrupt和I/O。这些将在数据类型小节解释。
例1:kstat头结构,来自/usr/include/kstat.h
/*
* 与内核和用户都相关的域
*/
hrtime_t ks_crtime; /* 创建时间 */
struct kstat * ks_next; /* kstat链的下一个链接 */
kid_t ks_kid; /* kstat独一无二的ID */
char ks_module[KSTAT_STRLEN]; /* 模块名 */
uchar_t ks_resv; /* 保留的 */
int ks_instance; /* 模块实例 */
char ks_name[KSTAT_STRLEN]; /* kstat名称 */
uchar_t ks_type; /* kstat数据类型 */
char ks_class[KSTAT_STRLEN]; /* kstat类型 */
uchar_t ks_flags; /* kstat标志 */
void * ks_data; /* kstat类型专属的数据 */
uint_t ks_ndata; /* 数据记录的个数 */
size_t ks_data_size; /* kstat数据大小 */
hrtime_t ks_snaptime; /* 上一次拍数据快照的时间 */
/*
* 仅与内核相关的域
*/
int ( * ks_update)( struct kstat * , int );
void * ks_private;
int ( * ks_snapshot)( struct kstat * , void * , int );
void * ks_lock;
} kstat_t;
有意义的成员包括:
ks_crtime:反映kstat被创建的时间,允许通过各种计数器计算从kstat被创建以后的等级(“从启动以后的等级”被更通用的概念“从kstat被创建以后的级级”替代)。
所有的与时间相关的kstat成员,例如创建时间,上一次拍快照的时间,kstat_timer_t,kstat_io_t时间戳和类型的值都是64位纳秒值。
kstat时间戳的精确性依赖于机器,但是精确(位数)在所有平台上都是相同的。参看gethrtime(3C)的man手册页获得与高分辨率时间戳相关的所有信息。
ks_next:kstat被保存在一个以NULL结束的链表或者链里。ks_next指向链中的下一个kstat。
ks_kid:kstat独一无二的标识符。
ks_module和ks_instance:这包含创建kstat的模块名和实例。假如只有一个实例,ks_instance就是0。参看kstat的手册页的Names小节获得更多的信息。
ks_name:给kstat一个有意义的名称。参看kstat手册页的Names小节获得更多的kstat名称空间信息。
ks_type:kstat里的数据类型。在数据类型小节介绍kstat包含的数据类型。
ks_class:每一个kstat都可以被描述成某些主要类型的统计信息,例如总线(bus),磁盘(disk),网络(net),虚拟内存(vm)或者杂项(misc)。这个域可以用作提取相关kstat的过滤器。
Solaris当前使用的有下列值:
bus controller device_error disk hat
kmem_cache kstat misc net nfs
pages partition rpc ufs vm
vmem
ks_data,ks_ndata和ks_data_size:ks_data指向kstat的数据段。这里保存的数据类型取决于ks_type。ks_ndata表示数据记录数。仅有一些kstat类型支持多记录数据。
下列kstat支持多记录数据:
KSTAT_TYPE_RAW
KSTAT_TYPE_NAMED
KSTAT_TYPE_TIMER
下列
kstat
仅支持一个记录的数据:
KSTAT_TYPE_INTR KSTAT_TYPE_IO
ks_data_size
:是数据段的总大小,以字节为单位。
ks_snaptime
:这是最后一次拍数据快照的时间戳。可以使用下面的计算方法来计算活动级别:
rate = (new_count – old_count) / (new_snaptime – old_snaptime)
现在开始
使用
kstat
,程序必须先调用
kstat_open()
函数,它反回一个指向
kstat
控制结构的指针。例
2
展示了该结构的成员。
例
2
:
kstat
链控制结构
typedef struct kstat_ctl {
kid_t kc_chain_id; /* 当前kstat链ID */
kstat_t * kc_chain; /* 指向kstat链的指针 */
int kc_kd; /* /dev/kstat文件描述符 */
} kstat_ctl_t;
kc_chain
指向
kstat
链拷贝的头部。一般通过遍历或者使用
kstat_lookup()
函数来查找和处理特定类型的
kstat
。
kc_chain_id
是
kstat
链的标识符,或者是你的
kstat
链拷贝的
KCID
。它的使用方法在
kstat
手册页的
Names
小节说明。
为了避免访问
kstat
数据不必要的开销,程序首先查找
kstat
链里感兴趣的数据类型,然后使用
kstat_read()
和
kstat_data_lookup()
函数从内核里取得统计信息。
例
3
是一个展示怎样打印出所有的与磁盘
I/O
信息相关的
kstat
入口。它遍历整个链查找
ks_type
值为
KSTAT_TYPE_IO
的中
kstat
,调用
kstat_read()
获取数据,然后用
my_io_display()
处理数据。
例
9
展示了怎么实现这个示例函数。
例
3
:通过磁盘
I/O
信息打印
kstat
入口
kstat_ctl_t
*
kc;
kstat_t
*
ksp;
kstat_io_t kio;
kc
=
kstat_open();
for
(ksp
=
kc
->
kc_chain; ksp
!=
NULL; ksp
=
ksp
->
ks_next) {
if
(ksp
->
ks_type
==
KSTAT_TYPE_IO) {
kstat_read(kc, ksp,
&
kio);
my_io_display(kio);
}
}
数据类型
kstat的数据段可以保存五种类型中的一种,用ks_type域来标识。下面的kstat类型可以保存多条记录。记录数保存在ks_ndata里。
KSTAT_TYPE_RAW KSTAT_TYPE_NAMED KSTAT_TYPE_TIMER
ks_data_size域整个数据段的大小,以字节为单位。 KSTAT_TYPE_RAW “raw”类型的kstat被当做一个字节数组对待,通常被导出到一些众所周知的数据结构,例如vminfo(在/usr/include/sys/sysinfo.h里定义)。例4展示了打印这些信息的一种方法。
例4:倾倒出一个Raw kstat
static void print_vminfo(kstat_t * kp)
{
vminfo_t * vminfop;
vminfop = (vminfo_t * )(kp -> ks_data);
printf( " Free memory: %dn " , vminfop -> freemem);
printf( " Swap reserved: %dn " , vminfop -> swap_resv);
printf( " Swap allocated: %dn " , vminfop -> swap_alloc);
printf( " Swap available: %dn " , vminfop -> swap_avail);
printf( " Swap free: %dn " , vminfop -> swap_free);
}
KSTAT_TYPE_NAMED
这一类型的kstat包含一个随机的name=value统计信息列表。例5展示了用于保存例命名的kstat数据结构。
例5:命名的 kstat 定义在/usr/include/kstat.h
typedef
struct
kstat_named {
char
name[KSTAT_STRLEN];
/*
计数器名字
*/
uchar_t data_type;
/*
数据类型
*/
union {
char
c[
16
];
/*
足够128位整数
*/
int32_t i32;
uint32_t ui32;
int64_t i64;
uint64_t ui64;
/*
这些数据成员是被废弃的
*/
int32_t l;
uint32_t ul;
int64_t ll;
uint64_t ull;
} value;
/*
计数器值
*/
} kstat_named_t;
#define
KSTAT_DATA_CHAR 0
#define
KSTAT_DATA_INT32 1
#define
KSTAT_DATA_UINT32 2
#define
KSTAT_DATA_INT64 3
#define
KSTAT_DATA_UINT64 4
/*
这些类型是被废弃的
*/
#define
KSTAT_DATA_LONG 1
#define
KSTAT_DATA_ULONG 2
#define
KSTAT_DATA_LONGLONG 3
#define
KSTAT_DATA_ULONGLONG 4
#define
KSTAT_DATA_FLOAT 5
#define
KSTAT_DATA_DOUBLE 6
例9中的程序使用一个函数my_named_display()展示怎么显示命名的kstat. 注意到如果类型是KSTAT_DATA_CHAR,不能保证那个16字节的值域是NULL结尾的。当你使用类似printf()的函数来打印这个值时,记住这个是很重要的。
KSTAT_TYPE_TIMER
这一类型的kstat保存有事件计时器的统计信息。它们为任何类型的事件提供计数与计时的基础信息。
例6:计时器kstat 定义在/usr/include/kstat.h
typedef
struct
kstat_timer {
char
name[KSTAT_STRLEN];
/*
事件名
*/
uchar_t resv;
/*
保留
*/
u_longlong_t num_events;
/*
事件数
*/
hrtime_t elapsed_time;
/*
累积的逝去时间
*/
hrtime_t min_time;
/*
最短的事件持续时间
*/
hrtime_t max_time;
/*
最长的事件持续时间
*/
hrtime_t start_time;
/*
前一事件开始时间
*/
hrtime_t stop_time;
/*
前一事件停止时间
*/
} kstat_timer_t;
KSTAT_TYPE_INTR 这一类型的kstat保存中断的统计信息。中断分类如下:
-
中断类型
定义
硬中断
来源于硬件设备自身的中断
软中断
一些系统中断源引起的中断
看门狗
周期性计时器调用引起的中断
伪中断
进入一个没有中断服务程序的中断入口点
多服务
检测到一个中断并且中断服务正在处理其它类型的中断
例7:中断kstat,定义在/usr/include/kstat.h
#define KSTAT_INTR_HARD 0
#define KSTAT_INTR_SOFT 1
#define KSTAT_INTR_WATCHDOG 2
#define KSTAT_INTR_SPURIOUS 3
#define KSTAT_INTR_MULTSVC 4
#define KSTAT_NUM_INTRS 5
typedef struct kstat_intr {
uint_t intrs[KSTAT_NUM_INTRS]; /* 中断计数器 */
} kstat_intr_t;
KSTAT_TYPE_IO
例8:I/O kstat, 定义在/usr/include/kstat.h
typedef struct kstat_io {
/*
* 基础计数器。
*/
u_longlong_t nread; /* 己读字节数 */
u_longlong_t nwritten; /* 己写字节数 */
uint_t reads; /* 读操作的次数 */
uint_t writes; /* 写操作的次数 */
hrtime_t wtime; /* 累积的等待(前一服务)时间 */
hrtime_t wlentime; /* 累积等待 length * time 积 */
hrtime_t wlastupdate; /* 上一次等待队列改变 */
hrtime_t rtime; /* 累积的运行(服务)时间 */
hrtime_t rlentime; /* 累积的运行 length *time 积 */
hrtime_t rlastupdate; /* 上一次运行队列改变时间 */
uint_t wcnt; /* 处于等待状态的元素个数 */
uint_t rcnt; /* 处于运行状态的元素个数 */
} kstat_io_t;
累积时间和队列长度统计信息
时间的统计信息为活跃任务的时间总和,队列长的统计信息为所有各队列长与此队列消逝所用时间的乘积之和,一个队列长的黎曼和包括了时间。图2对对队列/时间作了简单的描述。
在每一个状态改变点(任一个队列入口和出口),如果在这期间队列的长度不为0,从上一次状态改变逝去的时间将被加入到活动时间。
逝去时间和队列长度的积被加到长度(wlentime或者rlentime域)乘以时间的运行总和上。
图2:队列长度示例 状态方案:
if (队列长度 != 0) {
时间 += 自上一次状态改变逝去的时间;
时间长度 += (自上一次状态改变逝去的时间 * 队列长度);
}
广义上这种方法可以测量任何规定的系统。用未完成的到X服务器的RPC调用分别代替队列长度。
许多I/O子系统有至少两个基础“链表”管理它们的事务。
1、一个链表用于那些已经被接收,但还没开始处理的事务。
2、一个链表用于那些正在处理,但还没有结束的事务。
为此,定义有两个累各时间统计信息:
1、预服务(等待)时间
2、服务(运行)时间
累积的繁忙时间按纳秒累计。
kstat 名称
kstat的名字空间由kstat结构的三个域定义:
ks_module ks_instance ks_name
这三个域的组合被确保是唯一的。
例如,设想一个系统有4块高速以太网接口。Sun的高速以太网控制器设备驱动模块被叫做“hme”。第一个以太网接口常常是实例0,第二个是1,以此类推。“hme”驱动程序提供两种类型的kstat给每一个接口。第一种包含命名的kstat和性能统计信息。第二个包含中断统计信息。
在ks_module == “hme”, ks_instance == 0和ks_name == “hme0”下找到kstat数据中第一个接口的网络统计信息。中断的统计信息包含在以ks_module == “hme”, ks_instance == 0和ks_name == “hmec0”标识的kstat中。
在这个例子中,模块名和实例数的组合使得ks_name域(“hme0”和“hmec0”)仅仅是这个驱动的一个规约。其它驱动程序可能使用相似的命名规约来公布复合的kstat数据类型,但是不要求这样;内核模块保证这个组合是唯一的。
如何检测内核提供了些什么kstat。在Solaris 8里一个最容易的办法就是不带参数运行/usr/bin/kstat。它将打印几乎所有的当前kstat数据。Solaris的kstat命令可以倾泄出绝大多数己知的类型为KSTAT_TYPE_RAW的kstat。
函数
下面的函数非常有用对于从用户空间的C程序访问kstat数据:
kstat_ctl_t *kstat_open(void);
初始化一个kstat控制结构,提供到内核统计信息库的入口。它返回一个指向这一结构的指针,这个指针必须供给随后的libkstat函数调用的kc参数。
kstat_t *kstat_lookup(kstat_ctl_t *kc, char *ks_module, int ks_instance, char *ks_name);
遍历kstat链用给定的ks_module,ks_instance和ks_name域查找kstat。如果ks_module是NULL,ks_instance是-1或者ks_name是NULL,那么这些域在查找的时候将被忽略。例如kstat_lookup(kc, NULL, -1, “foo”)将仅仅查找名字为“foo”的第一个kstat。
void *kstat_data_lookup(kstat_t *ksp, char *name);
查找给定名字的kstat数据段记录。仅当kstat类型带有命名的数据记录时这个操作才有效。目前,仅有KSTAT_TYPE_NAMED和KSTAT_TYPE_TIMER类型的kstat带有命名的数据记录。必须先调用一次kstat_read()从内核取得数据。然后用这个子函数在数据段里查找特定的数据记录。
kid_t kstat_read(kstat_ctl_t *kc, kstat_t *ksp, void *buf);
从内核中取特定kstat的数据。
kid_t kstat_write(kstat_ctl_t *kc, kstat_t *ksp, void *buf);
写数据到内核中特定的kstat。只有超级用户可以使用kstat_write()。
kid_t kstat_chain_update(kstat_ctl_t *kc);
促使用户的kstat链头与内核的同步。
int kstat_close(kstat_ctl_t *kc);
释放与kstat控制结构相关的所有资源。当调用exit(2)和execve()时会自动做这个。(查看exec(2)的man手册页了解关于exit(2)和execve()的更多的信息)
链更新处理
数据结构概述里提到kstat链的动态性。libkstat库的kstat_open()函数返回一个内核kstat链的拷贝。因此,内核kstat链的内容有可能会改变,程序应该在适当的时间调用kstat_chain_update()函数来查看kstat链的私有拷贝是否与内核的相同。这就是KCID的目的(保存在kstat控制结构的kc_chain_id)。
每一次内核模块向系统链里添加或者从系统链里删除一个kstat,KCID都被增加。当你的程序调用kstat_chain_update()时,函数检查你的程序的控制结构中的kc_chain_id是否与内核的相匹配。如果不匹配,kc_chain_update()重建程序的本地kstat链并且返回:
新的KCID,如果链已经被改变
0,如果没有改变
-1,如果检测到某些错误
如果程序从前一次调用kstat库缓冲某些本地数据,则新的KCID作为一个标志表示你已经更新过信息。你可以再次搜索kstat链查看你的程序感兴趣的数据是否已经被添加或者删除。
一个实际的例子是iostat系统命令。它缓冲系统磁盘的一些内部数据并且辨别一个磁盘是在线或者下线。如果带着一个间隔时间参数调用iostat,它每隔一个间隔时间打印一次I/O统计信息。每一次循环,它都调用一次kstat_chain_update()查看是否某些东西已经被改变。如果发生改变,它判断它感兴趣的设备是已经被添加或者删除。
所有的汇总到一起
你的C程序必须包含:
#include <kstat.h>
当链接你的程序时,编译命令行必须包含参数-lkstat。
cc -o print_some_kstats -lkstat print_some_kstats.c
下面是一个短小和示例程序。首先,它用kstat_lookup()和kstat_read来查找系统的CPU速率。然后进入一个无限循环,打印所有关于KSTAT_TYPE_IO类型的kstat的信息总计。注意到循环的顶部,它调用kstat_chain_update()来检查你拥有的是当前数据。如果kstat链已经被改变,它在标准错误上给出一个简短的消息。
例9:打印不同类型kstat的示例程序
/* print_some_kstats.c:
* print out a couple of interesting things
*/
#include < kstat.h >
#include < stdio.h >
#include < inttypes.h >
#define SLEEPTIME 10
void my_named_display( char * , char * , kstat_named_t * );
void my_io_display( char * , char * , kstat_io_t);
main( int argc, char ** argv)
{
kstat_ctl_t * kc;
kstat_t * ksp;
kstat_io_t kio;
kstat_named_t * knp;
kc = kstat_open();
/*
* Print out the CPU speed. We make two assumptions here:
* 1) All CPUs are the same speed, so we'll just search for the
* first one;
* 2) At least one CPU is online, so our search will always
* find something. :)
*/
ksp = kstat_lookup(kc, " cpu_info " , - 1 , NULL);
kstat_read(kc, ksp, NULL);
/* lookup the CPU speed data record */
knp = kstat_data_lookup(ksp, " clock_MHz " );
printf( " CPU speed of system is " );
my_named_display(ksp -> ks_name, ksp -> ks_class, knp);
printf( " n " );
/* dump some info about all I/O kstats every
SLEEPTIME seconds */
while ( 1 ) {
/* make sure we have current data */
if (kstat_chain_update(kc))
fprintf(stderr, " <<State Changed>>n " );
for (ksp = kc -> kc_chain; ksp != NULL; ksp = ksp -> ks_next) {
if (ksp -> ks_type == KSTAT_TYPE_IO) {
kstat_read(kc, ksp, & kio);
my_io_display(ksp -> ks_name, ksp -> ks_class, kio);
}
}
sleep(SLEEPTIME);
} /* while(1) */
}
void my_io_display( char * devname, char * class , kstat_io_t k)
{
printf( " Name: %s Class: %sn " ,devname, class );
printf( " tnumber of bytes read %lldn " , k.nread);
printf( " tnumber of bytes written %lldn " , k.nwritten);
printf( " tnumber of read operations %dn " , k.reads);
printf( " tnumber of write operations %dnn " , k.writes);
}
void
my_named_display( char * devname, char * class , kstat_named_t * knp)
{
switch (knp -> data_type) {
case KSTAT_DATA_CHAR:
printf( " %.16s " ,knp -> value.c);
break ;
case KSTAT_DATA_INT32:
printf( " % " PRId32,knp -> value.i32);
break ;
case KSTAT_DATA_UINT32:
printf( " % " PRIu32,knp -> value.ui32);
break ;
case KSTAT_DATA_INT64:
printf( " % " PRId64,knp -> value.i64);
break ;
case KSTAT_DATA_UINT64:
printf( " % " PRIu64,knp -> value.ui64);
}
}
附加信息
这篇文章的大多数信息来自各种各样的Sun解决方案文档,Solaris白皮书和Solaris man手册页(第3小节 KSTAT)。详细的API信息,参看《Solaris 8的参考手册》和《编写设备驱动程序》。两者都可以在docs.sun.com获得。