Linux系统编程(四):utmp文件和who命令的实现

本篇文章将会介绍who命令的工作原理,介绍了utmp文件和相应结构体,以及我们可以如何用简单代码将其实现,重点在如何查询和了解相关调用的过程。

当然最后实现的是不带任何参数的版本,否则过于复杂就偏离了学习系统编程的初心,而完全变成了几乎没有实用意义的造轮子。相较于上篇文章中ls -l的实现,who的实现更为简单。

1 who 的作用

who可以知道正在使用系统的用户信息

1
2
$ who
choupin tty7 2020-01-23 12:30 (:0)

每一行代表一个已经登录的用户,第一列是用用户名,第二列是终端名,第三列是登录时间,第四列是用户登录地址。

2 who 如何工作

可以使用man来查看相关文档

1
man who

可以看到类似

1
If FILE is not specified, use /var/run/utmp.  /var/log/wtmp as FILE is common.

可知who是从utmp文件中读取用户登录的信息。

然后使用

1
man -k utmp

查看相关帮助

1
2
3
4
5
6
7
8
9
10
11
$ man -k utmp
endutent (3) - access utmp file entries
endutxent (3) - access utmp file entries
getutent (3) - access utmp file entries
getutent_r (3) - access utmp file entries
...
utmp (5) - login records
utmpdump (1) - dump UTMP and WTMP files in raw format
utmpname (3) - access utmp file entries
utmpx (5) - login records
utmpxname (3) - access utmp file entries

找到utmp(5)这一行,可知utmp与之相关,然后输入

1
man 5 utmp

可以看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
NAME
utmp, wtmp - login records

SYNOPSIS
#include <utmp.h>
...
struct utmp {
short ut_type; /* Type of record */
pid_t ut_pid; /* PID of login process */
char ut_line[UT_LINESIZE]; /* Device name of tty - "/dev/" */
char ut_id[4]; /* Terminal name suffix, or inittab(5) ID */
char ut_user[UT_NAMESIZE]; /* Username */
char ut_host[UT_HOSTSIZE]; /* Hostname for remote login, or
kernel version for run-level
messages */
struct exit_status ut_exit; /* Exit status of a process
marked as DEAD_PROCESS; not
used by Linux init (1 */
/* The ut_session and ut_tv fields must be the same size when
compiled 32- and 64-bit. This allows data files and shared
memory to be shared between 32- and 64-bit applications. */
#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

int32_t ut_addr_v6[4]; /* Internet address of remote
host; IPv4 address uses
just ut_addr_v6[0] */
char __unused[20]; /* Reserved for future use */
};
...

故而可知可以使用struct utmp来存放从utmp文件中读取的数据,且其成员就是who要展示的信息。

3 who 的代码实现

3.1 who的1.0版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <utmp.h>

#include "helper.h"

#define SHOWHOST

void show_info(struct utmp*);
int main() {
struct utmp cur_ut;
int utmpfd;
int reclen = sizeof(cur_ut);

if ((utmpfd = open(UTMP_FILE, O_RDONLY)) == -1) ERR_EXIT("open error");
while (read(utmpfd, &cur_ut, reclen)) show_info(&cur_ut);
close(utmpfd);
return 0;
}

void show_info(struct utmp* ut_ptr) {
printf("%-8.8s ", ut_ptr->ut_name);
printf("%-8.8s ", ut_ptr->ut_line);
printf("%-u ", ut_ptr->ut_time);
#ifdef SHOWHOST
printf("%s", ut_ptr->ut_host);
#endif
printf("\n");
}

其中ERR_EXIT(m)为一个宏,定义如下

1
2
3
4
5
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(1); \
} while (0)

编译执行后结果如下

1
2
3
4
5
$ ./who1 
reboot ~ 1580634912 5.3.0-26-generic
runlevel ~ 1580622998 5.3.0-26-generic
LOGIN tty1 1580373750
choupin tty7 1580442826 :0

系统的who命令执行结果如下

1
2
$ who
choupin tty7 2020-01-23 12:30 (:0)

可见其存在两个问题

  • 时间显示不正确
  • 显示了多余的非当前用户的信息

在2.0版的实现中可以解决该问题

3.2 who的2.0版本

主要做两点改进

  • 使用ctime()来将time_t格式的时间转换为正常时间
  • 通过utmp结构中的ut_type是否定义为USER_PROCESS来判断是否是用户登录信息

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <fcntl.h>   // open()
#include <stdio.h> // printf()
#include <time.h> // ctime()
#include <unistd.h> // read() close()
#include <utmp.h> // struct utmp

#include "helper.h"

#define SHOWHOST

void show_info(struct utmp* utptr);

int main() {
struct utmp cur_ut;
int utfd;
if ((utfd = open(UTMP_FILE, O_RDONLY)) == -1) ERR_EXIT("error open");
while (read(utfd, &cur_ut, sizeof(cur_ut)))
if (cur_ut.ut_type == USER_PROCESS) show_info(&cur_ut);
close(utfd);
return 0;
}

void show_info(struct utmp* utptr) {
time_t tv = utptr->ut_time;
printf("%-8.8s ", utptr->ut_name);
printf("%-8.8s ", utptr->ut_line);
printf("%-24.24s", ctime(&tv));
#ifdef SHOWHOST
if (utptr->ut_host[0] != '\0') printf(" (%-s)", utptr->ut_host);
#endif // SHOWHOST
printf("\n");
}

执行结果为

1
2
 ./who2
choupin tty7 Thu Jan 23 12:30:08 2020 (:0)

可以看见此时结果与系统命令who输出的结果相当接近了。

3.3 who的实现3.0版本

上述版本实现简单明了,但每次只从utmp文件中使用read()读取一个用户的数据,如果该主机同时有多个用户使用,就需要调用许多次read(),从而导致程序性能低下。

可以使用一个数组,然后用read()一次性读取的n个用户的信息,然后每次返回一个用户供输出信息,当将数组中存放的数据都输出完以后,再重新读取n个用户信息到数组中,直到输出完所有信息。

各个功能的函数实现如下,文件utmplib.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <fcntl.h>
#include <stdio.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <utmp.h>

#include "helper.h"

#define SHOWHOST

#define NRECS 16
#define NULLUT ((struct utmp*)NULL)
#define UTSIZE (sizeof(struct utmp))

static char utmpbuf[NRECS * UTSIZE];
static int num_recs;
static int cur_rec;
static int utmpfd = -1;

int utmp_open() {
utmpfd = open(UTMP_FILE, O_RDONLY);
num_recs = cur_rec = 0;
return utmpfd;
}

int utmp_reload() {
int n_chars = read(utmpfd, utmpbuf, NRECS * UTSIZE);
if (n_chars == -1) ERR_EXIT("read error");
num_recs = n_chars / UTSIZE;
cur_rec = 0;
return num_recs;
}

struct utmp* utmp_next() {
struct utmp* recp;
if (utmpfd == -1) ERR_EXIT("open utmp file error");
if (cur_rec == num_recs && utmp_reload() == 0) return NULLUT;
recp = (struct utmp*)&utmpbuf[cur_rec * UTSIZE];
cur_rec += 1;
return recp;
}

void utmp_close() {
if (utmpfd != -1) close(utmpfd);
}

void show_info(struct utmp* utptr) {
time_t tv = utptr->ut_time;
printf("%-8.8s ", utptr->ut_name);
printf("%-8.8s ", utptr->ut_line);
printf("%-24.24s", ctime(&tv));
#ifdef SHOWHOST
if (utptr->ut_host[0] != '\0') printf(" %-s", utptr->ut_host);
#endif // SHOWHOST
printf("\n");
}

主函数实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

#include "utmplib.c"

int main() {
struct utmp cur_ut;
struct utmp* utptr = &cur_ut;
utmp_open();
while ((utptr = utmp_next()) != NULLUT)
if (utptr->ut_type == USER_PROCESS) show_info(utptr);
utmp_close();
return 0;
}

4 结束语

在本篇文章中,主要记录了who命令的工作原理,了解了utmp文件的作用,以及struct utmp。在具体的实现过程中,循序渐进,从易到难实现了多个版本,其实这是因为who的实现比上篇文章中ls -l更简单,也在书中更前面的位置。因此这里重在学会如何查询和学习命令的方法。

-------- 本文结束 感谢阅读 --------
给我加块红烧肉吧
  • 本文标题: Linux系统编程(四):utmp文件和who命令的实现
  • 本文作者: Chou Bin
  • 创建时间: 2020年02月23日 - 23时02分
  • 修改时间: 2020年02月23日 - 23时02分
  • 本文链接: http://yoursite.com/2020/02/23/apue-hm04/
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!