Unix/Linux编程实践教程阅读笔记-who指令的实现(Mac下的实现)-来自第二章P25-P44的笔记
实现who命令前要先了解其功能:
who命令可以查看当前已经登录的用户的信息,包括其用户名,终端名和登录时间,先在自己电脑上试一下:
书上查阅了联机帮助文档后明确了一点:who展示的信息来自于/var/adm/utmp 这个文件,书上通过进一步查阅得知,utmp这个文件存放的是一个结构体数组,此结构体被定义在/usr/include/utmp.h这个头文件中,以下是我的电脑上的utmp.h:
其中,ut_name保存的是用户名,ut_line保存的是终端名,ut_time保存登录时间,ut_host保存用于登录的远程计算机的名字。
那么我们怎么读取这个文件?
之前第一章我们用过fgets,它可以读取指定数量的字符,但它不适用于当前情况,至于具体为什么,书上没有说。我查了不少资料,感觉都讲得模棱两可,我说下我理解的吧,因为当前要读的是一个数据结构,而fgets又是以文本方式来处理读取到的内容,所以可能会把数据当文本处理,这一点不是很确定,只能说fgets一般用于读取文本内容吧。另外一点就是fgets读取时,如果文件中包含的字符数不够,系统会自动在后面补'\0',可能这也不利于对数据的处理。
选用的读取函数是read函数,它是把缓冲区内容当二进制数据的形式来处理的,适用于读取数据块,如数组或结构体。函数原型如下:
size_t numread = read(int fd,void *buf,size_t qty)
这个函数位于头文件<unistd.h>中,是系统调用,用法自己查即可,要注意的一点是read返回的实际读取的字节数,如果文件包含的字符数不够,系统不会自动在后面补'\0'。
另外一个我认为很重要的一点是read的缓冲基于内核完成的,这一点与fgetc不同,后者是标准函数,缓冲是基于用户空间实现的。具体可以参考这篇文章:https://traxexer.iteye.com/blog/1725145
放上几个在我查阅fgets和read区别时的参考链接:
https://blog.csdn.net/yanglianzhuang/article/details/83546696
https://zhidao.baidu.com/question/141607019.html
https://bbs.csdn.net/topics/90053721
http://blog.chinaunix.net/uid-21377953-id-443333.html
说实话,我还是没懂为什么不用fgets而用read,难道fgets用文本形式处理读取的数据会导致乱码?先不管了,我先记住读取一串文本时用fgets,读取数据块时用read吧。
好,用了read来读取数据,但read函数要用到文件描述符,因此我们相应的打开文件的函数就选取open,它包含于头文件<fcntl.h>中,具体的介绍在第一章笔记中记录过了,就不写了。注意,即使对于同一文件打开多次,对应的文件描述符也是不同的。
接下来就很简单了,我们简要叙述一下流程:
1.先打开utmp这个文件,打开的时候记得要考虑到打开失败的情况。
2.循环读取此文件,注意,读取的字节数正好等于结构体的大小。每读出一个结构体,就将它的信息解析出来并输出在屏幕上。什么时候结束呢?因为read返回的是实际读取的字节数,因此当某一次读不够指定的字节数时就可以退出了。
3.退出时别忘了关闭文件。
现在展示书上的代码(我做了点小小的改动):
#include <stdio.h> #include <fcntl.h> #include <utmp.h> #include <unistd.h> #define SHOWHOST void show_info(struct utmp *utbufp); int main(int argc,char *argv[]) { int fd; //打开utmp文件,UTMP_FILE定义在utmp.h中,指示了文件路径 if((fd = open(UTMP_FILE,O_RDONLY)) == -1) { exit(1); } struct utmp current_record; int reclen = sizeof(current_record); //循环读取utmp文件中结构体数组中的每一个结构体并解析处理 while (read(fd,¤t_record,reclen) == reclen) { show_info(¤t_record); } close(fd); return 0; } //展示读取到的utmp结构体 void show_info(struct utmp *utbufp) { //用户名 printf("% -8.8s",utbufp->ut_name); printf(" "); //终端名 printf("% -8.8s",utbufp->ut_line); printf(" "); //登录时间 printf("% 10ld",utbufp->ut_time); printf(" "); #ifdef SHOWHOST //远程主机名 printf("( %s)",utbufp->ut_host); #endif printf("\n"); }
虽然书上后续会对它做优化,但这个版本至少应该能运行的,然而我实际操作时却掉入了大坑,如下图所示:
首先是UTMP_FILE这个宏定义不存在,看了一下utmp.h文件,发现确实没有这个定义,看样子基于Unix系统的Mac和Linux下的utmp文件配置还是有点不同,那我们看一下这个文件被放到哪去了:
这里需要注意一下这个grep指令,它可以在指定目录下查找包含匹配字符串的文件内容,-n选项代表显示的内容中包含行号,比如上图,可以看到上面显示的每一个条中间都有个数字,33,18,20,12这些都是在此文件中的行号,-R代表递归地对目录下的所有文件(包括子目录)进行 grep。
为什么要在usr/include文件夹下查找呢?因为库函数一般存这个文件夹下,而且utmp.h在这个文件夹下,想来与它相关的宏定义也会在这里吧。
上图中可以看到UTMP_FILE被定义在netbsd.h和freebsd.h中,它与PATH_UTMP这个宏定义被关联起来了,而_PATH_UTMP在utmp.h中被定义为:#define _PATH_UTMP "/var/run/utmp",因此很简单,我们直接把UTMP_FILE改为PATH_UTMP就行。
问题解决了吗?
并没有,“Definition of 'utmp' must be imported from module 'Darwin.C.util' before it is required”这个错误仍然在那里,我看了许多网上的帖子也没有找到靠谱的办法,后来偶尔发现直接运行居然就不报错了。。
然而仍有问题,我的程序什么都没有输出。。。
又是一番调试,我发现读取到的结构体根本就是空的,于是我去了/var/run/utmp目录下找utmp这个文件,发现没有!只有个叫utmpx的。
咋办呢?我又去看了下utmp.h,发现上面有这么一段话:
看样子在Mac下utmp.h已经被弃用了,现在用的是utmpx,我们去/usr/include下找到utmpx.h,打开看下:
这个文件内容比较多,我们就挑有用的看就行,现在问题是_PATH_UTMP对应路径下没有utmp这个文件,显然我们要把路径改为指向utmpx这个文件,刚才我们已经看到它在/var/run下,找一下utmpx.h中有没有对应路径的宏定义,发现有个_PATH_UTMPX,显然需要在代码中把路径改为这个。
我们把包含的头文件utmp.h改为utmpx.h,仔细看发现utmpx.h中结构体定义名为utmpx,且对应的结构体子项和utmp.h中定义的的名字不大相同,因此还要修改结构体的定义,好在看它们的名字,还是很容易联系上的。有一点要注意,登录时间这一项,原先是个long,而现在是个结构体struct timeval ut_tv。
怎么从这个结构体中提取出对应的long呢?我们在utmpx.h中没发现struct timeval的定义,在xcode中跳转一下,我们找到如下内容:
看起来tv_sec的可能性比较大,再跳转一下:
很好,看样子这个tv_sec就对应原先的ut_time了。
因此修改后的代码如下:
#include <stdio.h> #include <fcntl.h> #include <utmpx.h> #include <unistd.h> #define SHOWHOST void show_info(struct utmpx *utbufp); int main(int argc,char *argv[]) { int fd; //打开utmpx文件,UTMP_FILEX定义在utmpx.h中,指示了文件路径 if((fd = open(_PATH_UTMPX,O_RDONLY)) == -1) { exit(1); } struct utmpx current_record; int reclen = sizeof(current_record); //循环读取utmpx文件中结构体数组中的每一个结构体并解析处理 while (read(fd,¤t_record,reclen) == reclen) { show_info(¤t_record); } close(fd); return 0; } //展示读取到的utmpx结构体 void show_info(struct utmpx *utbufp) { //用户名 printf("% -8.8s",utbufp->ut_user); printf(" "); //终端名 printf("% -8.8s",utbufp->ut_line); printf(" "); //登录时间 printf("% 10ld",utbufp->ut_tv.tv_sec); printf(" "); #ifdef SHOWHOST //远程主机名 printf("( %s)",utbufp->ut_host); #endif printf("\n"); }
这次代码成功运行了,但结果。。。看下图吧:
这结果莫名其妙,为了解决这个问题,我们去看一下utmpx这个文件的内容,结果发现编辑器打不开,这似乎不是UTF-8编码的,我们把后缀改为.txt再看一下:
看样子我们打印出的就是这个文件的第一行了,后面也能看到对应的登录名czw52460183和终端名ttys,但这个文件显然编码方式不对,出现了乱码,这什么情况?
网上也没有解释,找了半天打算放弃了,结果偶然在utmpx.h中发现这段话:
意思是我们不能直接去读取utmpx这个文件了,底层统一封装了获取utmpx结构体的方法,名为getutxent,我们看一下它的联机帮助文档:
这方法厉害了,按描述看,它就和读取文本一样,读完后应该有个类似记录当前位置的机制,再次调用时会读取数组中下一个utmpx结构体,而且它不需要文件事先被打开,如果没打开,它会自动打开文件。
真是日了狗了,那之前搞那么多打开文件和读取文件的步骤就都不需要了,其实实现这个who指令目的就是要练习文件的打开啊,现在你强行给我做了个封装,我尝试去跳转找getutxent的源码,然而并没找到。。。算了,那就直接用这个吧,修改后的代码如下:
#include <stdio.h> #include <fcntl.h> #include <utmpx.h> #include <unistd.h> #include <time.h> //#define SHOWHOST void show_info(struct utmpx *utbufp); void showtime( long timeval); int main(int argc,char *argv[]) { //int fd; //打开utmpx文件,UTMP_FILEX定义在utmpx.h中,指示了文件路径 // if((fd = open(_PATH_UTMPX,O_RDONLY)) == -1) // { // exit(1); // } // struct utmpx current_record; // int reclen = sizeof(current_record); //循环读取utmpx文件中结构体数组中的每一个结构体并解析处理 //while (read(fd,¤t_record,reclen) == reclen) while (getutxent() != NULL) { //show_info(¤t_record); show_info(getutxent()); } //close(fd); return 0; } //展示读取到的utmpx结构体 void show_info(struct utmpx *utbufp) { //只显示已登录用户 if(utbufp->ut_type != USER_PROCESS) { return; } //用户名 printf("% -12.12s",utbufp->ut_user); printf(" "); //终端名 printf("% -8.8s",utbufp->ut_line); printf(" "); //登录时间 //printf("% 10ld",utbufp->ut_tv.tv_sec); showtime(utbufp->ut_tv.tv_sec); printf(" "); #ifdef SHOWHOST //远程主机名 printf("( %s)",utbufp->ut_host); #endif printf("\n"); } //将时间戳转换为可读形式并输出 void showtime(long timeval) { char *p; p = ctime(&timeval); //从第4个字符开始输出,屏蔽星期几的信息 printf("%12.12s", p+4); }
结果如下:
已经很接近自带的who输出结果了,现在还有四个问题:
一是和书里不一样,我们自带的who没有输出远程主机名,对应的是图中的(),所以要注释掉#define SHOWHOST。
二是我的用户名比书里的要长,因此输出的字符不能这样截断,要多留点空间,可以把用户名输出那一行改为:
printf("% -12.12s",utbufp->ut_user);
补充一下, printf动态控制宽度的知识: printf("%-N.Ms",str); 它的作用是输出指定长度N的字符串,超长M时截断,不足时左对齐,右边补空格。
可以参考:https://blog.csdn.net/w332530494/article/details/8731921
三是我们自带的who命令只输出了两项,而这里为什么输出了三项呢?
这是因为utmpx结构体数组包含了所有终端的信息,即使某个终端没有用到,也会存放进去,所以要过滤一下,只显示已登录的用户。
utmpx结构体中有一个成员ut_type,当它的值为7(USER_PROCESS,定义在utmpx.h头文件中)时,表示这是一个已登录的用户,因此只要在show_info中判断出不是7就返回。
四是我们时间显示目前仍是时间戳的形式,需要将它转换为可读的形式,可以使用ctime函数,函数原型如下:
char *ctime(const time_t *timep);
其中,time_t定义在time.h头文件中: typedef long int time_t;
因此传入的是一个long *,返回的是该时间戳的可读形式,注意返回的可读形式会包含4个字符的星期几的信息,比如:Tue Jun 4 2,我们自带的who输出中没有星期几的信息,所以输出时要从第四个字符开始输出。
综合以上四点,改进代码如下:
#include <stdio.h> #include <fcntl.h> #include <utmpx.h> #include <unistd.h> #include <time.h> //#define SHOWHOST void show_info(struct utmpx *utbufp); void showtime( long timeval); int main(int argc,char *argv[]) { //int fd; //打开utmpx文件,UTMP_FILEX定义在utmpx.h中,指示了文件路径 // if((fd = open(_PATH_UTMPX,O_RDONLY)) == -1) // { // exit(1); // } // struct utmpx current_record; // int reclen = sizeof(current_record); //循环读取utmpx文件中结构体数组中的每一个结构体并解析处理 //while (read(fd,¤t_record,reclen) == reclen) while (getutxent() != NULL) { //show_info(¤t_record); show_info(getutxent()); } //close(fd); return 0; } //展示读取到的utmpx结构体 void show_info(struct utmpx *utbufp) { //只显示已登录用户 if(utbufp->ut_type != USER_PROCESS) { return; } //用户名 printf("% -12.12s",utbufp->ut_user); printf(" "); //终端名 printf("% -8.8s",utbufp->ut_line); printf(" "); //登录时间 //printf("% 10ld",utbufp->ut_tv.tv_sec); showtime(utbufp->ut_tv.tv_sec); printf(" "); #ifdef SHOWHOST //远程主机名 printf("( %s)",utbufp->ut_host); #endif printf("\n"); } //将时间戳转换为可读形式并输出 void showtime(long timeval) { char *p; p = ctime(&timeval); //从第4个字符开始输出,屏蔽星期几的信息 printf("%12.12s", p+4); }
现在的输出结果如下:
现在来看,这个程序很简单,重要的是这次经历让我明白,一定要好好看头文件的说明!!!
很多改动,比如某个文件的弃用,已经明确说明在了头文件中,另外,联机帮助文档也很重要。
很多问题,网上是查不到解决方案的,这时就要静下心来,好好看头文件和联机帮助文档。