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,&current_record,reclen) == reclen)
    {
        show_info(&current_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,&current_record,reclen) == reclen)
    {
        show_info(&current_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,&current_record,reclen) == reclen)
    while (getutxent() != NULL)
    {
        //show_info(&current_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,&current_record,reclen) == reclen)
    while (getutxent() != NULL)
    {
        //show_info(&current_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);
}

现在的输出结果如下:

 

现在来看,这个程序很简单,重要的是这次经历让我明白,一定要好好看头文件的说明!!!

很多改动,比如某个文件的弃用,已经明确说明在了头文件中,另外,联机帮助文档也很重要。

很多问题,网上是查不到解决方案的,这时就要静下心来,好好看头文件和联机帮助文档。

 

posted on 2019-06-11 12:37  暴躁法师  阅读(862)  评论(0编辑  收藏  举报