转自:https://www.cnblogs.com/blueclue/archive/2011/10/24/linux_system_program_ac.html
ac命令是什么?
man ac
的解释是:“打印用户连接时间的统计数据”。
附带的两个常用的命令参数:
-d 日统计
-p 每个用户的统计
ac命令运行结果
进入命令行,依次敲下命令,输出结果比较了然:
$ ac
total 312.64
$ ac -d
Oct 8 total 48.11
Oct 9 total 74.71
Oct 12 total 86.85
Oct 14 total 58.57
Today total 44.82
$ ac -p
lizh 313.09
total 313.09
统计结果单位为小时。
要统计出这些系统信息,需要查询哪些文档呢?把我们的问题列出个清单来对症下药即可:
- ac的登陆时间和注销时间日志记录在哪个文件?
- 何时会写入登陆和注销日志记录?
- 日志记录的格式是什么?
- 统计时间的算法是什么?
姑且只有这些问题吧,由此查阅我们需要的文档。
我们需要什么?
首先要做的是分析实现过程,我们需要以下工具作为参考:
ac文档手册 : man ac
wtmp文档手册 : man 5 wtmp
wtmp查看工具 : dump-utmp
login(3) : 写utmp和wtmp入口
logout(3) : 写utmp和wtmp入口
编程我们借助《Unix\Linux编程实践教程》这本书第二章51页的 utmplib.c 代码文件,实现对wtmp文件的操作,主要是实现wtmp文件的打开、关闭、读取、定位。
ac的登陆时间和注销时间日志记录在哪个文件?
在ac文档手册中标出了数据来源:
相关文件
/var/log/wtmp 系统范围的登录记录文件。进一步的细节参见 wtmp(5)。
ac的统计结果是基于这个文件的,之前一篇文章里也做过说明,可以查看这里。
也可参考wtmp文件的在线文档,跟文档手册是一样的内容,地址为:http://www.kernel.org/doc/man-pages/online/pages/man5/wtmp.5.html。
The wtmp file records all logins and logouts. Its format is exactly like utmp except that a null username indicates a logout on the associated
terminal. Furthermore, the terminal name ~ with username shutdown or reboot indicates a system shutdown or reboot and the pair of terminal names
|/} logs the old/new system time when date(1) changes it. wtmp is maintained by login(1), init(8), and some versions of getty(8) (e.g.,
mingetty(8) or agetty(8)). None of these programs creates the file, so if it is removed, record-keeping is turned off.
wtmp文件记录了所有的登陆和注销。它的格式与utmp很相似,除了它用空用户名表示某个相关联终端的注销。此外,终端名称为~,同时,用户名为shutdown或reboot的记录表示系统关闭或者重启;当使用date(1)修改系统时间时,终端名称里为|/}的记录了原来的/新的系统时间。wtmp文件记录由login(1)、init(8)和一些版本的getty(8)(例如:mingetty(i)或者agetty(8))来维护。这些程序并不创建该文件,所以如果文件被移除,保存记录的功能就被关闭了。
何时会写入登陆和注销记录?
查阅login(3)文档,里面有段话,大体意思是讲:login()的时候以struct utmp* ut为参数,用USER_PROCESS值填写ut->ut_type,并且填写ut->ut_pid等等。在logout()的时候,会将ut->ut_type写入DEAD_PROCESS;除此之外我们还得考虑reboot和shutdown的情况,在这种情况下,所有用户基本都在同一时间退出,这时只有一条退出记录,这条记录的用户名被记录为reboot和shutdown,终端名称记录为~。
目前,我们只考虑这两种情况。
日志记录的格式是什么?
有个工具:dump-utmp,我们利用这个工具查看wtmp文件,我们先用一个用户名在控制台1登陆再注销,运行命令:
$dump-utmp /var/log/wtmp
在wtmp文件尾部有4条新产生的记录为:
lizh |tty1 |7| | 1433| |Sat Oct 15 10:45:11 2011
lizh |tty1 |7|1 | 8251| |Sat Oct 15 10:45:11 2011
|tty1 |8| | 1433| |Sat Oct 15 10:45:56 2011
LOGIN |tty1 |6|1 | 8302| |Sat Oct 15 10:45:56 2011
这个输出格式在dump-utmp文档中有说明,是以’|’分隔, 每行以user name, tty, type, id, pid, hostaddr, host, time的顺序排列的,我们关心的是第一列user name、第二列tty、第三列type、最后一列time,除了type外都比较了然,type的定义在wtmp的文档中有说明,如下:
#define EMPTY 0 /* Record does not contain valid info
(formerly known as UT_UNKNOWN on Linux) */
#define RUN_LVL 1 /* Change in system run-level (see
init(8)) */
#define BOOT_TIME 2 /* Time of system boot (in ut_tv) */
#define NEW_TIME 3 /* Time after system clock change
(in ut_tv) */
#define OLD_TIME 4 /* Time before system clock change
(in ut_tv) */
#define INIT_PROCESS 5 /* Process spawned by init(8) */
#define LOGIN_PROCESS 6 /* Session leader process for user login */
#define USER_PROCESS 7 /* Normal process */
#define DEAD_PROCESS 8 /* Terminated process */
#define ACCOUNTING 9 /* Not implemented */
原来,USER_PROCESS值为7,DEAD_PROCESS值为8。
我们可以自己写个读取wtmp文件的代码,与dump-utmp打印出来的日志记录是一样的,这里参考《Unix\Linux编程实践教程》第2章50页who3.c的源码改写而成,采用缓冲技术,原书中有个图例,看起来比较明了,如下图:
将原来读取utmp文件改为读取wtmp,并略微修改打印项即可,源码如下:
/* utmplib.c - functions to buffer reads from utmp file
*
* functions are
* utmp_open( filename ) - open file
* returns -1 on error
* utmp_next( ) - return pointer to next struct
* returns NULL on eof
* utmp_close() - close file
*
* reads NRECS per read and then doles them out from the buffer
*/
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <utmp.h>
#define NRECS 16
#define NULLUT ((struct utmp *)NULL)
#define UTSIZE (sizeof(struct utmp))
static char utmpbuf[NRECS * UTSIZE]; /* storage */
static int num_recs; /* num stored */
static int cur_rec; /* next to go */
static int fd_utmp = -1; /* read from */
utmp_open( char *filename )
{
fd_utmp = open( filename, O_RDONLY ); /* open it */
cur_rec = num_recs = 0; /* no recs yet */
return fd_utmp; /* report */
}
struct utmp *utmp_next()
{
struct utmp *recp;
if ( fd_utmp == -1 ) /* error ? */
return NULLUT;
if ( cur_rec==num_recs && utmp_reload()==0 ) /* any more ? */
return NULLUT;
/* get address of next record */
recp = ( struct utmp *) &utmpbuf[cur_rec * UTSIZE];
cur_rec++;
return recp;
}
int utmp_seek(int rec_offset, int base)
{
off_t newpos;
int rv;
if ( base == SEEK_CUR )
rec_offset += ( cur_rec - num_recs );
newpos = lseek(fd_utmp, (off_t) rec_offset*UTSIZE, base);
if( newpos == -1 )
return -1;
cur_rec = num_recs ; /* will force reload */
return newpos/UTSIZE;
}
int utmp_reload()
/*
* read next bunch of records into buffer
*/
{
int amt_read;/* read them in */
amt_read = read( fd_utmp , utmpbuf, NRECS * UTSIZE );
/* how many did we get? */
num_recs = amt_read/UTSIZE;
/* reset pointer */
cur_rec = 0;
return num_recs;
}
utmp_close()
{
if ( fd_utmp != -1 ) /* don't close if not */
close( fd_utmp ); /* open */
}
#include <stdio.h>
#include <sys/types.h>
#include <utmp.h>
#include <fcntl.h>
#include <time.h>
void show_info(struct utmp*);
void showtime(long);
int main(int ac, char* av[])
{
int wtmpfd;
struct utmp* utbuf = utmp_next();
if (utmp_open(WTMP_FILE) == -1)
{
perror(WTMP_FILE);
return -1;
}
while ((utbuf=utmp_next()) != ((struct wtmp *)NULL))
show_info(utbuf);
utmp_close();
return 0;
}
/*
* show info()
* displays the contents of the utmp struct
* in human readable form
* * displays nothing if record has no user name
*/
void show_info( struct utmp *utbufp )
{
// if ( utbufp->ut_type != USER_PROCESS )
// return;
printf("%-8.8s", utbufp->ut_name); /* the logname */
printf(""); /* a space */
printf("|%-8.8s", utbufp->ut_line); /* the tty */
printf(""); /* a space */
printf("|%d", utbufp->ut_type);
printf("");
showtime( utbufp->ut_time ); /* display time */
#ifdef SHOWHOST
if ( utbufp->ut_host[0] != '\0' )
printf(" (%s)", utbufp->ut_host); /* the host */
#endif
printf("\n"); /* newline */
}
void showtime( long timeval )
/*
* displays time in a format fit for human consumption
* uses ctime to build a string then picks parts out of it
* Note: %12.12s prints a string 12 chars wide and LIMITS
* it to 12chars.
*/
{
char *cp; /* to hold address of time */
cp = ctime(&timeval); /* convert time to string */
/* string looks like */
/* Mon Feb 4 00:46:40 EST 1991 */
/* 0123456789012345. */
printf("|%12.12s", cp+4 ); /* pick 12 chars from pos 4 */
}
统计时间的算法
正常情况下,同一用户名、同一tty,用户login将ut_type置为USER_PROCESS,这算是时间的起点;用户logout时,将ut_type置为DEAD_PROCESS,或者是shutdown或是reboot下,这两种情况算是时间的终点,这都算是一次完整的登陆。
那么,我们可以设计一个结构来保存初步日志记录的分析结果:
struct utmp_record
{
short ut_type; // type
char ut_user[UT_NAMESIZE]; // user name
char ut_line[UT_LINESIZE]; // tty
int32_t login_sec; // 登陆时间
int32_t logout_sec; // 注销时间
double diff; // 登陆时间与注销时间的时间差
};
我们,在遍历wtmp文件时,将相关数据整理和计算填入此结构。
#include <stdio.h>
#include <sys/types.h>
#include <utmp.h>
#include <fcntl.h>
#include <time.h>
#define RECORDSIZE 2048
struct utmp_record
{
short ut_type; // type
char ut_user[UT_NAMESIZE]; // user name
char ut_line[UT_LINESIZE]; // tty
int32_t login_sec; // 登陆时间
int32_t logout_sec; // 注销时间
double diff; // 登陆时间与注销时间的时间差
};
struct utmp_record record[RECORDSIZE];
int num_rd = 0; // 整理日志后的记录数量
void showtime(long); // 打印时间(按照时间格式)
void show_totaltime(); // 打印总连接时间
double sec2hour(double); // 秒换算成小时
int main(int ac, char * av[])
{
int wtmpfd;
struct utmp *utbuf = utmp_next();
int i = 0;
if (utmp_open(WTMP_FILE) == -1)
{
perror(WTMP_FILE);
return - 1;
}
memset(record, 0, sizeof(struct utmp_record) * RECORDSIZE);
while ((utbuf = utmp_next()) != ((struct utmp *) NULL))
{
if ((utbuf->ut_type == USER_PROCESS))
{
for (i = 0; i < num_rd; i++)
{
if (strcmp(record[i].ut_user, utbuf->ut_user) == 0 &&
record[i].login_sec == utbuf->ut_time)
break;
}
if (i == num_rd)
{
record[num_rd].ut_type = utbuf->ut_type;
memcpy(record[num_rd].ut_user, utbuf->ut_user, UT_NAMESIZE);
memcpy(record[num_rd].ut_line, utbuf->ut_line, UT_LINESIZE);
record[num_rd].login_sec = utbuf->ut_time;
num_rd++;
}
}
else if (utbuf->ut_type == DEAD_PROCESS)
{
for (i = 0; i < num_rd; i++)
{
if (record[i].ut_type == USER_PROCESS &&
strcmp(record[i].ut_line, utbuf->ut_line) == 0 &&
utbuf->ut_time > record[i].login_sec)
{
record[i].logout_sec = utbuf->ut_time;
record[i].ut_type = DEAD_PROCESS;
record[i].diff = difftime(record[i].logout_sec, record[i].login_sec); //计算时间差
}
}
}
/* shudown和reboot时,将之前所有已经登录的用户登记为退出 */
else if ((utbuf->ut_type == RUN_LVL &&
strncmp(utbuf->ut_user, "shutdown", 8) == 0) ||
(utbuf->ut_type == BOOT_TIME &&
strncmp(utbuf->ut_user, "reboot", 6) == 0))
{
for (i = 0; i < num_rd; i++)
{
if (record[i].ut_type == USER_PROCESS &&
utbuf->ut_time > record[i].login_sec)
{
record[i].logout_sec = utbuf->ut_time;
record[i].ut_type = utbuf->ut_type;
record[i].diff = difftime(record[i].logout_sec, record[i].login_sec);
}
}
}
}
utmp_close();
if (ac == 1)
{ // 不带参数的ac令命
show_totaltime();
}
else if (ac == 2)
{
// TODO
// ..带参数的ac命令,例如:
// ac -p 按照用户名统计
// ac -d 按照日期统计
}
return 0;
}
void show_totaltime()
{
int i = 0;
double total = 0.0;
time_t now_t = time(NULL);
for (i = 0; i < num_rd; i++)
{
if (record[i].ut_type != USER_PROCESS)
{
total += record[i].diff;
}
else
{
record[i].diff = difftime(now_t, record[i].login_sec);
total += record[i].diff;
}
}
printf(" total %.2f\n", sec2hour(total));
}
double sec2hour(double secs)
{
return (double)secs / 3600;
}
对于当前用户的登陆时间计算,是采用执行acc时取当前时间然后与登陆时间做差的方法。
该程序只是一定程度上实现了ac工具,并不完美:
- 只考虑了简单场景,只能处理简单的wtmp文件,处理真实wtmp文件时,还是有较大的误差;
- 为了便于编程,整理后的数据结构数组个数,用的宏RECORDSIZE直接定义;
- 带参数执行未实现,像-p、-d参数,可在目前程序基础上再做扩展;
- 用户更改系统时间时对执行结果的影响,未作作相应处理。