简介
第二章的第一部分主要集中介绍了命令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的时间有多种表示方法:
time_t
类型,其实这个类型就是一个long
的整数,记录了从Epoch (1970-01-01)以来的秒数struct timeval
类型,这个类型如下1
2
3
4struct timeval {
time_t tv_sec; // seconds
suseconds_t tv_usec; // microseconds
}它也是表示从Epoch以来的时间秒数,只不过更加精确,加入了毫秒的域
struct tm
类型,这个类型如下:1
2
3
4
5
6
7
8
9
10
11struct 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
6struct timeval tv;
time_t t;
......
t = tv.tv_sec;
......
tv.tv_sec = t;time_t
$\leftrightarrow$struct tm
: 对于这两个类型的函数,我们有直接的库函数可以使用:1
2
3struct tm *gmtime(const time_t *timep);
struct tm *localtime(const time_t *timep);
time_t mktime(struct tm *tm);其中
gmtime
和localtime
的区别在于,gmtime
转化为UTC时间,而localtime
转化为当地时区时间。struct timeval
$\leftrightarrow$struct tm
: 通过复合上述两种变换达到目的。
printf
的格式符
在标准代码中,有这样的格式化输出语句
1 | printf("%-8.8s", record->ut_user); |
其中%-8.8f
是一个不常见的格式符,其中的负号表示靠左对齐,也就是补空格补在右端,整数部分的8表示最小长度,缺了补空格,小数部分的8表示最大长度,多了截去。
印象深刻的bug
时间问题
标准代码中用ctime
将time_t
类型的时间直接转化为一个人类友好可读的字符串,但是其实现是如下:
1 | void show_info(struct utmp *record) { |
在我实现时,我直接就用如下代码实现:
1 | void show_info(struct utmp *record) { |
看上去没有什么问题,但是打印出来显示我在几百万年以后登入了系统,思考了一会并查阅了手册我发现,ut_time
的定义其实是这样的:
1 |
|
显然在我的系统中,ut_time
绝不是else
分支中的定义,因为那样定义是没有错的,但是在前一个分支的定义中,tv_sec
是int32_t
类型而不是struct timeval
中的time_t
类型,最终的结果就导致,我调用ctime(&(record->ut_time))
的时候,将一个指向32位整形的指针强制转化为指向64位整形的指针,其效果就相当于是tv_sec | ((uint64_t)tv_usec << 32)
,结果自然是一个非常大的整数。实现了穿越
另:其实我在编译的时候加入了-Wall
选项,编译器也报了个incompatible pointer casting
,结果我还是忽略了。