编写who命令:文件操作,缓冲区与联机帮助
最近阅读UULP(Understanding Unix/Linux Programming),按照书中介绍对Unix/Linux系统编程进行学习梳理,总结如下。
1. who命令能做什么
who命令用于查看有谁在使用系统。
执行who命令。
who
其输出格式如下:
ustc tty7 Sept 1 08:34 (xxx.yy.com用户登陆地址,有些版本没有)
john lft/0 Sept 1 08:34
其显示包含用户名,终端名,和登录时间。
2. who命令是如何工作的
想知道如何自己编写这样一个命令,首先当然是要了解who是如何工作的?最好的方法自然是先查看一下联机帮助(man)。
man who
查看输出结果,在描述部分有这样一句话:
If FILE is not specified, use /var/run/utmp. /var/log/wtmp as FILE is common.
所以who的实现,跟utmp/wtmp文件应该有很大的联系。
继续对utmp查看联机帮助。
man utmp
果然在描述部分里有:
The utmp file allows one to discover information about who is currently using the system.
表明的确可以通过utmp查看谁在使用系统。
进一步查看其数据结构,发现与我们who命令需要相关的用户名,终端名,和登录时间。
char ut_user[UT_NAMESIZE]; /* Username */ char ut_line[UT_LINESIZE]; /* Device name of tty - "/dev/" */ char ut_host[UT_HOSTSIZE]; /* Hostname for remote login, or kernel version for run-level struct { int32_t tv_sec; /* Seconds */ int32_t tv_usec; /* Microseconds */ } ut_tv; /* Time entry was made */
他们都在struct utmp中。
3. 编写自己的who命令
3.1 who1.c
有了第二部分的了解之后,编写自己的who命令看起来就有眉目了,我们只需要利用unix文件操作,读取utmp文件中的相应项,并以固定格式输出在屏幕即可。
基础的文件操作函数接口复习一下:
#include <fcntl.h> int open( char *filename, int access, int permission ); int read( int handle, void *buffer, int nbyte ); int write( int handle, void *buffer, int nbyte ); int close( int handle );
所以对相关项以固定格式输出,利用#SHOWHOST控制是否显示第四项(用户登录地址),得到第一个版本的who命令.
who1.c
代码:
1 #include<stdio.h> 2 #include<utmp.h> 3 #include<fcntl.h> 4 #include<unistd.h> 5 #include<stdlib.h> 6 #define SHOWHOST 7 8 void show_info(struct utmp* utbufp) { 9 printf("%8.8s", utbufp -> ut_user); 10 printf(" "); 11 printf("%8.8s", utbufp -> ut_line); 12 printf(" "); 13 printf("%d", utbufp -> ut_tv.tv_sec); 14 printf(" "); 15 #ifdef SHOWHOST 16 printf("%s", utbufp -> ut_host); 17 #endif 18 printf("\n"); 19 } 20 21 int main() { 22 struct utmp current_record; 23 int utmpfd; 24 int reclen = sizeof(current_record); 25 if ((utmpfd = open(UTMP_FILE,O_RDONLY)) == -1) { 26 perror(UTMP_FILE); 27 exit(1); 28 } 29 while (read(utmpfd,¤t_record,reclen) == reclen) { 30 show_info(¤t_record); 31 } 32 close(utmpfd); 33 return 0; 34 }
查看一下输出:
reboot ~ 1472787188 4.4.0-34-generic
runlevel ~ 1472787197 4.4.0-34-generic
LOGIN tty1 1472787200
ustc tty7 1472787242 :0
对比系统who命令输出,多了一些项,而且时间显示好像也不是可读的时间,所以需要进一步修改。
3.2 who2.c
who1.c的输出中包含了所有终端的信息,甚至是尚未使用的,同时还有LOGIN这样的控制台信息(不是真正的用户)。
所以考虑如何去掉他们,继续查看utmp的联机帮助,在关于ut_type中有这样的定义。
#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 */
可以看出,ut_type的值为7时,表明其是一个user_process,所以加一句判断,即可消除其他行。
代码:
if (utbufp -> ut_type != USER_PROCESS) { return; }
再看时间显示问题,unix有库函数ctime可以将time_t类型转换为人常用的时间形式。
查看ctime的man文档,学习其使用方式:
char *ctime(const time_t *timep);
发现给定一个time_t类型(long int),即可转换为一个可读字符串,还是很简单的。
注: 如果不知道ctime,也可以通过
man time | grep transform之类的命令,查找内容中带有转换字样的与时间相关的命令,在显示结果中找到可能有用的,再细看其man文档,发现ctime。
这样,把上述两部分总结起来,就可以得到第二个版本的who。
who2.c
1 #include<stdio.h> 2 #include<utmp.h> 3 #include<fcntl.h> 4 #include<unistd.h> 5 #include<stdlib.h> 6 #include<time.h> 7 #define SHOWHOST 8 void show_time(long timeval) { 9 char* cp; 10 cp = ctime(&timeval); 11 printf("%12.12s",cp + 4); 12 } 13 14 void show_info(struct utmp* utbufp) { 15 if (utbufp -> ut_type != USER_PROCESS) { 16 return; 17 } 18 printf("%-8.8s", utbufp -> ut_user); 19 printf(" "); 20 printf("%-8.8s", utbufp -> ut_line); 21 printf(" "); 22 show_time(utbufp -> ut_tv.tv_sec); 23 printf(" "); 24 #ifdef SHOWHOST 25 printf("%s", utbufp -> ut_host); 26 #endif 27 printf("\n"); 28 } 29 30 int main() { 31 struct utmp current_record; 32 int utmpfd; 33 int reclen = sizeof(current_record); 34 if ((utmpfd = open(UTMP_FILE,O_RDONLY)) == -1) { 35 perror(UTMP_FILE); 36 exit(1); 37 } 38 while (read(utmpfd,¤t_record,reclen) == reclen) { 39 show_info(¤t_record); 40 } 41 close(utmpfd); 42 return 0; 43 }
查看其输出结果:
ustc tty7 Sep 1 20:34 :0
与系统自带的who没什么区别了。至此,一个能用的who命令应该是完成了。
3.3. 继续优化,who3.c
一个能用的who命令是编写好了,但是其有没有不足之处呢?显然是有的,一个最大的问题,就是文件的I/O效率太低了。
因为每次读写操作的系统调用CPU都要完成用户态与内核态之间的切换,假设结果中有大量用户信息要去读,则大量的资源消耗在了这里,所以这是优化的重点。
而优化的思路也很简单,就是使用缓冲区。每次从内核多读一些进来进缓冲区,然后从缓冲区读取utmp数据进行显示。
把缓冲区大小设置为16,开闭读取文件都用写好的接口从缓冲区读取和写入数据,有如下代码:
1 //utmplib.h 2 #ifndef UTMPLIB_H 3 #define UTMPLIB_H 4 5 #define NRECS 16 6 #define NULLUT (struct utmp *)NULL 7 #define UTSIZE (sizeof (struct utmp)) 8 9 static char utmpbuf[NRECS * UTSIZE]; //storage 10 static int num_recs; //nums of stored utmp data 11 static int cur_rec; //next pos to go 12 static int fd_utmp = -1; //read from 13 14 int utmp_open(char* filename); 15 16 struct utmp* utmp_next(); 17 18 int utmp_reload(); 19 20 void utmp_close(); 21 22 #endif
1 //utmplib.c 2 #include<stdio.h> 3 #include<fcntl.h> 4 #include<sys/types.h> 5 #include<unistd.h> 6 #include<utmp.h> 7 #include"utmplib.h" 8 9 int utmp_open(char* filename) { 10 fd_utmp = open(filename, O_RDONLY); 11 cur_rec = num_recs = 0; 12 return fd_utmp; 13 } 14 15 struct utmp* utmp_next() { 16 struct utmp* recp; 17 if (fd_utmp == -1) { 18 return NULLUT; 19 } 20 if (cur_rec == num_recs && utmp_reload() == 0) { 21 return NULLUT; 22 } 23 recp = (struct utmp*) &utmpbuf[cur_rec * UTSIZE]; 24 cur_rec ++; 25 return recp; 26 } 27 28 int utmp_reload() { 29 int aimt_read; 30 aimt_read = read(fd_utmp, utmpbuf, NRECS * UTSIZE); 31 num_recs = aimt_read / UTSIZE; 32 cur_rec = 0; 33 return num_recs; 34 } 35 36 void utmp_close() { 37 if (fd_utmp != -1) { 38 close(fd_utmp); 39 } 40 return; 41 }
1 //who3.c 2 #include<stdio.h> 3 #include<utmp.h> 4 #include<fcntl.h> 5 #include<unistd.h> 6 #include<stdlib.h> 7 #include<time.h> 8 #include"utmplib.h" 9 #define SHOWHOST 10 void show_time(long timeval) { 11 char* cp; 12 cp = ctime(&timeval); 13 printf("%12.12s",cp + 4); 14 } 15 16 void show_info(struct utmp* utbufp) { 17 if (utbufp -> ut_type != USER_PROCESS) { 18 return; 19 } 20 printf("%-8.8s", utbufp -> ut_user); 21 printf(" "); 22 printf("%-8.8s", utbufp -> ut_line); 23 printf(" "); 24 show_time(utbufp -> ut_tv.tv_sec); 25 printf(" "); 26 #ifdef SHOWHOST 27 printf("%s", utbufp -> ut_host); 28 #endif 29 printf("\n"); 30 } 31 32 int main() { 33 struct utmp* utbufp; 34 if (utmp_open(UTMP_FILE) == -1) { 35 perror(UTMP_FILE); 36 exit(1); 37 } 38 while ((utbufp = utmp_next()) != (struct utmp*)NULL) { 39 show_info(utbufp); 40 } 41 utmp_close(); 42 return 0; 43 }
至此一个who命令基本编写完毕。
我们在这个过程中大量使用了man文档学习和辅助编写,复习了基本的文件操作函数,学习了使用缓冲技术提高I/O效率。
参考文献:
Bruce Molay, Understanding Unix/Linux programming - A Guide to Theory and Practice.
代码也可以通过https://github.com/wangxiaobao1114/Unix-Linux_Programming查看。