• 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读取时,如果文件中包含的字符数不够,系统会自动在后面补'',可能这也不利于对数据的处理。

      选用的读取函数是read函数,它是把缓冲区内容当二进制数据的形式来处理的,适用于读取数据块,如数组或结构体。函数原型如下:

    size_t numread = read(int fd,void *buf,size_t qty)

      这个函数位于头文件<unistd.h>中,是系统调用,用法自己查即可,要注意的一点是read返回的实际读取的字节数,如果文件包含的字符数不够,系统不会自动在后面补''。

      另外一个我认为很重要的一点是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("
    ");
    }

      虽然书上后续会对它做优化,但这个版本至少应该能运行的,然而我实际操作时却掉入了大坑,如下图所示:

      首先是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("
    ");
    }

    这次代码成功运行了,但结果。。。看下图吧:

    这结果莫名其妙,为了解决这个问题,我们去看一下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("
    ");
    }
    
    //将时间戳转换为可读形式并输出
    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("
    ");
    }
    
    //将时间戳转换为可读形式并输出
    void showtime(long timeval)
    {
        char *p;
        p = ctime(&timeval);
        //从第4个字符开始输出,屏蔽星期几的信息
        printf("%12.12s", p+4);
    }

    现在的输出结果如下:

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

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

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

  • 相关阅读:
    Smarty简介
    简易调用及实例化视图
    简易调用及实例化模型
    简易调用及实例化控制器
    MVC错误(一)
    单一入口及MVC目录规范
    MVC各个层的作用
    MVC工作流程
    【学习笔记】字符串—马拉车(Manacher)
    【题解】邻值查找 [CH1301]
  • 原文地址:https://www.cnblogs.com/czw52460183/p/10999434.html
Copyright © 2020-2023  润新知