Linux系统编程第一课: CentOS7下who命令的实现

  今天是学Linux系统编程的第一天,然而明天六级考试我却在这里写代码。。。。不管怎么样先记录下这次实验再说。
  这一次的内容是手工实现who命令。who命令在Linux系统内用来查看各个用户的登入情况,可以显示在哪个终端登陆以及登陆时间等信息。自己实现的话自然是需要和系统互动啦,所以我们需要去找出来用什么头文件可以让我们和系统交互并且获取用户的登陆信息。代码先放在下面。

#include    //standard input and output stream
#include   //function exit
#include     //user login records relating
#include    //file controal relating, open function
#include   //function read and close
#include 

#define SHOWHOST

void showtime(long timeval);
void show_info(struct utmp *);
int main(){
    struct utmp utbuf;
    int utmpfd;
    if((utmpfd = open(UTMP_FILE, O_RDONLY))==-1){
        printf("error\n");
        perror(UTMP_FILE);
        exit(1);
    }
    while(read(utmpfd, &utbuf, sizeof(utbuf)) == sizeof(utbuf))
        show_info(&utbuf);
    close(utmpfd);
    return 0;
}

void show_info(struct utmp * utbufp){
    if(utbufp->ut_type != USER_PROCESS)
        return ;
    printf("% -8.8s ", utbufp->ut_user);
    printf("% -8.8s ", utbufp->ut_line);
    showtime(utbufp->ut_time);
#ifdef SHOWHOST
    if(utbufp->ut_host[0] != '\0')
        printf("(%s)", utbufp->ut_host);
#endif
    puts("");
}

void showtime(long timeval){
    char *cp;
    cp = ctime(&timeval);
    printf("%12.12s", cp);
}

  我在CentOS下编译通过,实现功能和who基本一样。
跟着课本实现很轻松。man who可以查看who的信息,在man手册靠后的地方我们发现了who命令需要和一个文件交互:/var/run/utmp(默认),那么我们来看看utmp是干什么的吧。
  man utmp之后可以基本知道这东西就是系统用来处理登陆信息的了.头文件bits/utmp.h

struct utmp
{
    short int ut_type;
    pid_t ut_pid;
    char ut_line[UT_LIENSIZE[;
    char ut_id[4];
    char ut_user[UT_NAMESIZE];
    char ut_host[UT_HOSTSIZE];
    strcut exit_status ut_exit;
#ifdef __WORDSIZE_TIME64_COMPAT32
    int32_t ut_session;
    struct
    {
        int32_t tv_sec;
        int32_t tv_usec;
    } ut_tv;
#else
    long int ut_session;
    struct timeval tu_tv;
#endif
#define ut_name ut_user
#ifndef _NO_UT_TIME
#define ut_time ut_tv.tv_sec
#endif
#define ut_xtime ut_tv.tv.sec
#define ut_addr ut_addr_v6[0]    
}

好的, 我们注意到了这个结构,里面有和用户登陆相关的所有信息. 我们的问题就变得简单了, 只要写一个小程序打开/var/run/utmp文件, 按照定义的utmp结构把文件里面的内容统统读取出来然后打印到屏幕上就OK啦.
但是有一个问题: 用什么打开?用什么读取? 也许使用fgets之类的函数? 我相信用过的人都是对这些函数充满了嫌弃的. 我们现在有更好的选择.read, open, close函数.
三个函数定义在不同的地方, open在头文件fcntl.h里面,而read和close在unistd.h里面. 三个文件各自负责不同的部分. open函数接受一个字符串指针和一个flag, flag包括O_RDONLY, O_WRONLY, O_RDWR, 我们这里不希望修改utmp的内容, 所以使用O_RDONLY, 同时,open返回一个文件描述符(整型), 如果是0, 代表标准输入, 1代表标准输出, 2表示标准错误输出. -1表示错误. 其他的正数表示文件成功打开而且已经建立了联系,可以通过这个文件描述符来访问文件.
所以代码里,我们用utmpfd是否等于-1来判断是否能够进行输出. 当不为-1的时候, 我们的程序可以顺序执行, 通过文件描述符来访问文件utmp, while循环调用read函数来进行读取.
read函数接受三个参数,文件描述符,一个指针, 以及读取的字符数量.原型是


ssizt_t read(int fd, void* buf, size_t count)

那么接受open返回值的utmpfd就可以作read的第一个参数, 而中间的指针, 考虑到utmp在utmp中已经定义是结构体, 那么我们就用struct utmp来处理它, 至于最后一个, 由于文件内数据是按序排布的, 我们只有一直读取和结构等大字节的数据出来就可以了. 而利用read的返回值是成功读取的字符数量, 那么利用utmp结构的大小就可以控制读取合适结束, 如代码已经展示的那样.

代码接下来的部分就是一些格式化输出啦, 但是在依照课本修改代码的时候, 我发现一个问题, 用来格式化输出时间, 让时间readable的函数ctime在man手册内接受的是一个time_t指针, 但是在utmp.h内并没有定义time_t类型,而且, 也没有代码中出现的ut_time变量, 代码是怎么正常工作的呢?
返回去看utmp.h, 在靠近结构定义的结尾部分,我们看到了一串宏定义, 前面已经打出来了.有一对看不明白的关于字宽的检查和一个很关键的typedef ut_time ut_tv.tv_sec, 很好, 我们顺利找到了ut_time的定义, 但是再返回去一看, 我们用的ut_tv.tv_sec是哪一个?这会影响到我们具体在showtime里面用long还是int还是别的什么类型来作参数. 图国条件编译里面条件为真, 那么类型是int32_t, 我们写一个小程序int32.c, 如下:

#include     
#include 
int main(){
    int32_t num;
    return 0;
}      

然后用命令gcc -E int32.c | grep int32_t可以查看到最终的一个typedef, 是这样的typedef signed int int32_t, 那么我们自然就可以用signed int来读取时间, 但是如果条件为假呢? timeval结构体是什么? 为了解决这个问题, 我开始仿照书上照read等一系列的函数的方法开始了寻找timeval.

先看一看, timeval是和时间相关的, 那么去和时间有关的头文件里面看看好了. 到/usr/inclue里面, 查看了time.h文件, 发现里面第118行有一个定义

 struct timeval
{
        __time_t tv_sec;
        __susecond_t tv_usec;
 };

很好, 我们找到了timaval的原型, 不过问题来了, __time_t和time_t是什么关系?继续找好了, 很好, 75行出现了typedef __time_t time_t, 感觉到这里大功告成了. 然而我不知为何脑袋有洞, 之前提到关于字宽的定义, 为真还是假? 于是我又开始了寻找, 回到utmp.h里面, 我发现几乎都是extern声明, 显然没什么用, 但是随之我发现出现了一个头文件sys/types.h, 这个是干嘛的? 好奇打开看了看, 没有收获. 但是又发现了另外一个宏: bits/utmp.h, 打开一看

#include 
#include 
#include 
#include < bits/wordsize.h>

!!!!!wordsize不正是我要找的吗?!赶紧打开

#if defined __x86_64__ && !defined __ILP32__
# define __WORDSIZE 64
#else
# define __WORDSIZE 32
#endif
#ifdef __x86_64__
# define __WORDSIZE_TIME64_COMPAT32 1
# define __SYSCALL_WORDSIZE 64
#endif

虽然没有发现十分有头绪的地方, 但是到目前位置, 为了utmp, 我们已经知道utmp.h同时和time.h, sys/types.h, sys/times.h所关联, 他们联合在一起为timeval提供了定义, 同时还要避免重复定义和重复包含的问题, 然后utmp.h还和bits/utmp.h关联, 而这个文件又依靠和bits/wordsize.h的关联为utmp.h和sys/utmp.h提供了WORDSIZE方面的定义, 于是整个系统的用户信息才能够被完整而且准确的记录, 同时由于int32_t和long的大小关系, who命令制作时优先选用long便可以避免32位系统和64位系统不兼容的情况.而事实上, 还有更复杂的地方, time.h里面还定义了一个叫timespec的时间结构, 提供纳秒级的时间, 而sys/time.h里面还提供了一个宏定义以来进行timeval和timespec的转换以保证程序正确.

我实际找timeval的定义是花了比写这篇博客还久得多的时间, 因为没有看到time.h里面对ut_time的宏定义, 所以满世界搜索头文件来匹配. 虽然花了很多时间, 但是也是第一次那么认真地把各个头文件之间相互包含关联的内容查看一遍.

不得不感慨系统制作之不易, 443个头文件, 静态库和动态库被保存在/usr/include目录下, 彼此之间相互关联之复杂, 我仅查看了8个头文件就已经被震撼. 尤其在无数的复杂的宏呈现, 然而其目的却只是保证粗心的用户和程序员能够正常使用,不用担心各类库的调用会带来严重的后果. 工程师代替用户做好了一切的防护措施, 以后再也不敢甩锅给系统…不过最佩服的还是Linus居然能够只靠一个人就完成Linux内核的构建工作, 实在不敢想象在当时没有各类强大IDE支持的时代他是如何完成如此复杂异常的工作的.

你可能感兴趣的:(Linux学习)