0%

UULP-Chapter02-1-编写自己的`who`程序

简介

第二章的第一部分主要集中介绍了命令who和实现一个自己的who,其中比较有收货的是man文档的使用,文件读写操作,Linux时间表示方法,以及printf的格式符。

who命令是一个比较老的命令,而且现如今也用的不多,有一些终端模拟器甚至不再支持这些命令以至于这些命令会出现bug。

文件读写

文件读写主要是介绍并利用了open, read, write, close这四个系统调用,以及who命令需要的存储的登入信息是通过文件utmp格式来存储的,这是个二进制的文件,里面是一个个的struct utmp类型的二进制表示,读出的时候要使用

1
read(utmp_fileno, &record, sizeof(struct utmp));

Linux时间表示

Linux的时间有多种表示方法:

  1. time_t 类型,其实这个类型就是一个long的整数,记录了从Epoch (1970-01-01)以来的秒数

  2. struct timeval类型,这个类型如下

    1
    2
    3
    4
    struct timeval {
    time_t tv_sec; // seconds
    suseconds_t tv_usec; // microseconds
    }

    它也是表示从Epoch以来的时间秒数,只不过更加精确,加入了毫秒的域

  3. struct tm 类型,这个类型如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct tm {
    int tm_sec; /* Seconds (0-60) */
    int tm_min; /* Minutes (0-59) */
    int tm_hour; /* Hours (0-23) */
    int tm_mday; /* Day of the month (1-31) */
    int tm_mon; /* Month (0-11) */
    int tm_year; /* Year - 1900 */
    int tm_wday; /* Day of the week (0-6, Sunday = 0) */
    int tm_yday; /* Day in the year (0-365, 1 Jan = 0) */
    int tm_isdst; /* Daylight saving time */
    };

    这是一个分拆了的时间,存储有年月日时分秒等等详细信息。

几个时间类型的转换

  • struct timeval $\leftrightarrow$ time_t: 由于这两个类型的关系是前一个更精确一些,于是我们可以直接转换:

    1
    2
    3
    4
    5
    6
    struct timeval tv;
    time_t t;
    ......
    t = tv.tv_sec;
    ......
    tv.tv_sec = t;
  • time_t $\leftrightarrow$ struct tm: 对于这两个类型的函数,我们有直接的库函数可以使用:

    1
    2
    3
    struct tm *gmtime(const time_t *timep);
    struct tm *localtime(const time_t *timep);
    time_t mktime(struct tm *tm);

    其中gmtimelocaltime的区别在于,gmtime转化为UTC时间,而localtime转化为当地时区时间。

  • struct timeval $\leftrightarrow$ struct tm: 通过复合上述两种变换达到目的。

printf的格式符

在标准代码中,有这样的格式化输出语句

1
printf("%-8.8s", record->ut_user);

其中%-8.8f是一个不常见的格式符,其中的负号表示靠左对齐,也就是补空格补在右端,整数部分的8表示最小长度,缺了补空格,小数部分的8表示最大长度,多了截去。

印象深刻的bug

时间问题

标准代码中用ctimetime_t类型的时间直接转化为一个人类友好可读的字符串,但是其实现是如下:

1
2
3
4
5
6
7
8
9
10
void show_info(struct utmp *record) {
...
show_time(record->ut_time);
...
}

void show_time(time t) {
char cp = ctime(&t);
printf("%12.12s", cp + 4);
}

在我实现时,我直接就用如下代码实现:

1
2
3
4
5
void show_info(struct utmp *record) {
...
printf("%12.12s", ctime(&(record->ut_time)) + 4);
...
}

看上去没有什么问题,但是打印出来显示我在几百万年以后登入了系统,思考了一会并查阅了手册我发现,ut_time的定义其实是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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

#ifndef _NO_UT_TIME
#define ut_time ut_tv.tv_sec
#endif

显然在我的系统中,ut_time绝不是else分支中的定义,因为那样定义是没有错的,但是在前一个分支的定义中,tv_secint32_t类型而不是struct timeval中的time_t类型,最终的结果就导致,我调用ctime(&(record->ut_time))的时候,将一个指向32位整形的指针强制转化为指向64位整形的指针,其效果就相当于是tv_sec | ((uint64_t)tv_usec << 32),结果自然是一个非常大的整数。实现了穿越

另:其实我在编译的时候加入了-Wall选项,编译器也报了个incompatible pointer casting,结果我还是忽略了。