Linux系统编程之实现who命令

我们需要了解到一个事实就是,在Linux练习实现系统编程的各个程序时候,我们完全可以对现有命令进行改进,也就是说可以使用自己所写的更适合自己习惯的命令(当然一般linux的命令更加全面,觉得参数麻烦也可以通过快捷命名使用),具体可以查看Linux环境变量的讨论

了解who

who命令是显示目前登录系统的用户信息。执行who命令可列出登入帐号,使用的终端机,登入时间以及从何处登入。

想写任何命令的程序编写,必须先了解它,最好的方法当然是查看手册,$man who一下,参数什么的先不必详看,我们需要了解命令是怎么实现的,需要注意到的是这句:

If FILE is not specified, use /var/run/utmp. /var/log/wtmp as FILE is common.

也就是说,如果未指定文件,则从/var/run/utmp文件中读取,wtmp共同帮助完成who命令,来搜索下utmp与wtmp文件到底是什么,通过搜索可以看到有一条符合我们的条件。

xxx@xxx-ThinkPad-X230-Tablet:/var/run$ man -k utmp
utmp (5)             - login records

打开此文件$man 5 utmp我们可以很清晰的了解到utmp文件的作用了,选取我们需要了解的部分如下:

DESCRIPTION
       The  utmp file allows one to discover information about who is currently using the system.  There may be more users currently using the system, because not all programs use utmp logging.
       Warning: utmp must not be writable by the user class "other", because many system programs (foolishly)  depend  on  its  integrity.
       You  risk  faked  system logfiles and modifications of system files if you leave utmp writable to any user other than the owner and group owner of the file.
       The file is a sequence of utmp structures, declared as follows in <utmp.h> (note that this  is  only  one  of  several  definitions around; details depend on the version of libc)
       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.  

此段说明中可以看出utmp允许从这里读出当前在使用系统的信息,而wtmp文件用于记载所有的登录与登出信息。

有个小插曲是作者还在这用foolishly表达了对很多系统程序依赖诚信的愚蠢让我觉得这就是作者和用户交流表达自己不爽的一种方式吧哈哈哈。

这里代码实现的方法是直接在utmp文件里按顺序定义了数据结构,而告诉我们在include中的utmp.h中得到声明,查看了utmp.h文件后发现里面只包含了所有的声明,而在比较以前的版本中,则是在utmp.h中定义所有的数据结构,而umpt只保存结构数组,数据元素是utmp类型的结构,而utmp类型定义则需要去utmp.h中查询,相当于utmp文件仅仅是一个查询入口,较新的版本改成目前这样的原因我觉得是:

  • 直接定义,保存的时候就直接以这样类型的数据结构保存,这样不需要编译器再去utmp.h文件编译出需要什么类型的结构体再返回保存,节省了编译器的时间,提高效率。

此外还发现,utmp.h文件就是汇总一些将内核函数通过封装引入到用户态那些函数的声明。utmp会保存登陆信息,而他保存的信息,就是通过utmp.h中经过封装的内核函数调用取得,再把它反馈给utmp。我们执行who命令时,再从utmp中取得登陆信息。

 struct utmp {
               short   ut_type;              /* Type of record */
               pid_t   ut_pid;               /* PID of login process */
               char    ut_line[UT_LINESIZE]; /* Device name of tty - "/dev/" */
               char    ut_id[4];             /* Terminal name suffix,
                                                or inittab(5) ID */
               char    ut_user[UT_NAMESIZE]; /* Username */
               char    ut_host[UT_HOSTSIZE]; /* Hostname for remote login, or
                                                kernel version for run-level
                                                messages */
               struct  exit_status ut_exit;  /* Exit status of a process
                                                marked as DEAD_PROCESS; not
                                                used by Linux init (1 */
               /* The ut_session and ut_tv fields must be the same size when
                  compiled 32- and 64-bit.  This allows data files and shared
                  memory to be shared between 32- and 64-bit applications. */
           #if __WORDSIZE == 64 && defined __WORDSIZE_COMPAT32
               int32_t ut_session;           /* Session ID (getsid(2)),
                                                used for windowing */
               struct {
                   int32_t tv_sec;           /* Seconds */
                   int32_t tv_usec;          /* Microseconds */
               } ut_tv;                      /* Time entry was made */
           #else
                long   ut_session;           /* Session ID */
                struct timeval ut_tv;        /* Time entry was made */
           #endif

               int32_t ut_addr_v6[4];        /* Internet address of remote
                                                host; IPv4 address uses
                                                just ut_addr_v6[0] */
               char __unused[20];            /* Reserved for future use */
           };

这是utmp结构体的定义,从后面的注释中也可以很清楚的了解到此结构体中保存啦哪些信息,包括登陆类型,登陆id,登录pid等。

扫描二维码关注公众号,回复: 9934470 查看本文章

所以我们想实现who命令,接下来要做的就是把记录的登录用户一个个读出来并显示出来。

编写who

需要解决的第一个问题就是,如何读出文件中的数据结构,想到了两种方法:

  • 用getc和fgets读出字符与字符串然后直接打印

    但这种方法明显效率非常低

这时候就想到了文件操作,即open、read、close。所以我们的思路是把文件中数据结构写进缓冲区保存,再把保存的结构体中的数据打印出来即可。

来看下main函数

  1 #include <stdio.h>
  2 #include <utmp.h>
  3 #include <fcntl.h>
  4 #include <unistd.h>
  5 #include "show_info.c"
  6 
  7 #define SHOWOST
  8 
  9 int main()
 10 {
 11         struct utmp current_record;
 12         int utmpfd;
 13         int reclen = sizeof(current_record);
 14 
 15         if((utmpfd = open(UTMP_FILE,O_RDONLY))==-1)
 16         {
 17                 perror(UTMP_FILE);
 18                 exit(1);
 19         }
 20 
 21         while (read(utmpfd,&current_record,reclen)==reclen)
 22         show_info(&current_record);
 23         close(utmpfd);
 24         return 0;
 25 
 26 }

此处采用了utmp类型的结构体来保存读入的数据,并且每次读入一个utmp类型的数据,不断读入,最后调用show_info函数把登录信息显示出来。show_info的结构很简单,只需要把用来保存的结构体中数据不断读出就好。

  1 show_info(struct utmp *utbufp){
  2         printf("%-8.8s",utbufp->ut_name);
  3         printf(" ");
  4         printf("%-8.8s",utbufp->ut_line);
  5         printf(" ");
  6         printf("%10ld",utbufp->ut_time);
  7         printf(" ");
  8 #ifdef SHOWOST
  9         printf("(%s)",utbufp->ut_host);
 10 #endif  
 11         printf("\n");
 12 }       

代码本身比较简单,主要是搞清它的原理和过程,以及怎么想到考虑它的编写过程,我在此处选用了头文件与函数分离的办法,也出现了一些问题,好在最终得到了解决,具体请参见头文件与函数定义分离的处理

这样,运行它就得到了输出:

xxx@xxx-ThinkPad-X230-Tablet:~/code/unix_linux/who$ ./a.out 
reboot   ~        1540433259 (4.15.0-34-generic)
runlevel ~        1540433270 (4.15.0-34-generic)
xxx      :0       1540433375 (:0)
LOGIN    tty3     1540979878 ()

与系统的who对比下:

xxx@xxx-ThinkPad-X230-Tablet:~/code/unix_linux/who$ who
xxx      :0           2018-10-25 10:09 (:0)

有两点不足:

  • 没有消除已经登录的记录,而是全部打印出来。
  • 没有正确显示时间

解决问题

消除登录记录

要解决第一个问题,首先考虑到的是,系统是如何区分哪些终端对应活动的用户呢?我们再回到utmp中查看,发现系统对每种用户都给了标示符:

           #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 */

那么我们需要的就是标示符为7的正常用户,所以只需要让它在show_info函数的输出之前中有个条件判别就好了:

if(utbufp->type!=USER_PROCESS)
return;

接着运行一下,第一个问题就解决了。

xxx@xxx-ThinkPad-X230-Tablet:~/code/unix_linux/who$ ./a.out 
xxx      :0       1540433375 (:0)

显示时间

需要获取时间,首先要了解Linux下是如何存储时间的,Linux下分为两种时间存储方式:

  • 使用time_t类型存储从1970年到现在经过了多少秒
  • 使用tm结构体分别存储年月日时分秒,需要值得注意的是年份是从1900年起至今多少年,而不是直接存储当前年份。

还是老办法通过man -k time查找时间相关的文件,发现一个相关的通过man 3 ctime打开找到了我们要的答案:

 The call ctime(t) is equivalent to asctime(localtime(t)).  It converts the calendar time t into a  null-terminated  string  of  the form

          "Wed Jun 30 21:49:08 1993\n"

这里应该介绍的是使用第一种方法,用ctime函数把time_t的数值转换为人们日常使用时间格式。有关ctime函数使用也在此文件中有说明:

char *ctime(const time_t *timep);

那么就使用第一种方法吧,ctime会有一个指向time_t的指针,返回的时间字符串如上面所示,这里我们不需要星期,所以从第四个字符开始统计。

那么需要添加一个showtime函数:

  1 #include <stdio.h>
  2 #include <utmp.h>
  3 #include <fcntl.h>
  4 #include <unistd.h>
  5 #include "show_info.h"
  6 #include <time.h>
  7 void showtime(long timeval)
  8 {
  9         char *cp;
 10         cp = ctime(&timeval);
 11         printf("%12.12s",cp+4);
 12 }           

同时需要把show_info里面输出时间部分替换,这样最终修改完得到了最终结果,输出一下试试:

xxx@xxx-ThinkPad-X230-Tablet:~/code/unix_linux/who$ ./a.out 
xxx      :0       Oct 25 10:09(:0)

对比一下系统的:

xxx@xxx-ThinkPad-X230-Tablet:~/code/unix_linux/who$ who
xxx      :0           2018-10-25 10:09 (:0)

除了格式不同之外完全实现了!

编写命令我们需要从源头看,看手册对于它如何规定,调用了哪些函数进行实现,了解它的工作原理,再把它串起来就可以,在过程中可以学到很多细节部分深究还是十分有趣的。

发布了15 篇原创文章 · 获赞 13 · 访问量 9060

猜你喜欢

转载自blog.csdn.net/weixin_43122409/article/details/83963882
今日推荐