第 40 章 登录记账 (Login Accounting)
核心结论
-
两个核心文件:Linux 把登录信息分别写到
/var/run/utmp(当前登录态)与/var/log/wtmp(追加式审计日志,包括所有登录登出);/var/log/lastlog是 per-UID 的「上次登录」索引——三文件是 SUSv3 + 多个发行版的惯例。 -
utmpx 头文件与结构:utmpx API 起源于 System V,Linux 用 hybrid (BSD + SysV);
utmpx结构含ut_type/ut_pid/ut_line/ut_id/ut_user/ut_host/ut_exit/ut_session/ut_tv/ut_addr_v6;定义 ut_type 用 8 个常量 (EMPTY/RUN_LVL/BOOT_TIME/NEW_TIME/OLD_TIME/INIT_PROCESS/LOGIN_PROCESS/USER_PROCESS/DEAD_PROCESS)。 -
getutxent/getutxid/getutxline 三函数:
getutxent()顺序读;getutxid(ut)按 type+id 查 (RUN_LVL/BOOT_TIME 精确查;其他按ut_id);getutxline(ut)按 LOGIN_PROCESS/USER_PROCESS 的 line 字段查。用setutxent()倒带、endutxent()关闭文件。 -
path 常量与 SUSv3:
/var/run/utmp、/var/log/wtmp、/var/log/lastlog编译进 glibc;应用应使用<paths.h>的_PATH_UTMP/_PATH_WTMP/_PATH_LASTLOG常量,不要硬编码。SUSv3 不标准化这些 path。 -
登录/登出协议:登录时
pututxline()写USER_PROCESS记录;登出时改ut_type=DEAD_PROCESS、清空ut_user覆盖原记录;pututxline 自动去找旧记录做覆盖,找不到才追加。wtmp 通过updwtmpx()追加。 -
getlogin():用 utmp 找「当前控制终端上对应的登录名」——比
LOGNAME环境变量可信,因为后者可被用户改;通过ttyname(STDIN)找终端,然后查 utmp 中匹配ut_line的记录。
|
本章主旨
本章是「系统怎么看谁登录了」——三个文件 (utmp/wtmp/lastlog) + 一个 API (utmpx) 回答这个问题。 |
一、核心概念
本章围绕 6 个核心概念展开:核心文件、utmpx 结构和类型常量、getutx* 系列函数、getlogin、登录登出协议、lastlog 文件索引。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
utmp / wtmp 双文件 |
|
§40.1;用 |
utmpx 结构与 ut_type 9 类型 |
|
§40.3-40.4;运行级/换日志由 init/NTP 写; |
getutxent / getutxid / getutxline 检索 |
|
§40.4;返回静态区指针——非 reentrant;glibc 的 getutmpx_*_r 不存在,reentrant 走传统 utmp 的 |
getlogin() 与控制终端反查 |
|
§40.5;getlogin 失败 ENOTTY(无终端)或 ENXIO(utmp 无记录);reentrant 版本 |
pututxline / updwtmpx 更新协议 |
登录:构造 USER_PROCESS + ut_user/ut_pid/ut_line/ut_id/ut_tv,调 |
§40.6;getutx* 与 pututxline 共享「current file position」;登出时若 write 失败可调 atexit handler 清 utmp;init 在启动时也会清理 DEAD |
lastlog per-UID 索引 |
|
§40.7;用 |
二、详细笔记
40.1 utmp + wtmp 概述
What:UNIX 用两个文件记录登录态:utmp 当前登录用户(who 用),wtmp 历史(last 用)。Linux 在 /var/run/utmp 和 /var/log/wtmp。
Why:who/last/finger/write/talk 都依赖;写网络登录服务(sshd/ftpd)必须更新。
How:
-
path 常量:
<paths.h>的_PATH_UTMP、_PATH_WTMP、_PATH_LASTLOG,不要硬编码。 -
权限:utmp/wtmp 通常 0664 root:utmp 或 0664 root:adm;普通用户仅能读。
-
utmpx 名字起源:SysV 用
utmpx/wtmpx区别 BSD 的utmp/wtmp;Linux 沿用 SysV 头部结构但只一个 utmp 文件。
When:写自定义登录服务、终端分配器、who/last 等价查询工具。
40.2 utmpx API 与 utmp 的关系
What:Linux 提供 utmpx 和 utmp 两套 API 返回相同信息——utmpx 由 SUSv3 标准化;二者都映射到 /var/run/utmp。
Why:可移植性需要 umtx;utmp API 提供 _r reentrant 版本。
How:
-
utmpx:所有
x()函数(setutxent/getutxent/getutxid/getutxline/pututxline/endutxent/utmpxname)。 -
utmp:传统名字(同函数少 x:setutent/getutent/getutid/getutline/pututline/endutent/utmpname);提供 reentrant 的
getut*_r()。 -
Linux 上两者*等价*:
utmp.h也定义ut_type等常量(除_GNU_SOURCE之外一般默认)。
When:默认用 umtx(更好的可移植性);在 multithreaded 程序中必须用 getut*_r()。
40.3 utmpx 结构与 ut_type
What:struct utmpx 含 13 个字段;ut_type 是核心字段,决定记录语义。
Why:所有「登录事件」都编码成一条记录 + ut_type 标签。
How——struct utmpx 字段(来自 §40.3 Listing 40-1):
// 摘自《The Linux Programming Interface》 第 40 章
struct utmpx {
short ut_type; /* 类型 (1..8) */
pid_t ut_pid; /* login 进程 PID */
char ut_line[32]; /* 终端完整名(去 /dev/) */
char ut_id[4]; /* 终端名后缀 (tty1 -> "1") */
char ut_user[32]; /* 用户名 */
char ut_host[256]; /* 远端主机;内核版用于 RUN_LVL */
struct exit_status ut_exit; /* 仅 DEAD_PROCESS 有意义 */
long ut_session; /* session ID */
struct timeval ut_tv; /* 事件时间 */
int32_t ut_addr_v6[4]; /* IPv4 在 [0];IPv6 用全部 */
char __unused[20];
};
ut_type 常量(来自 §40.3,必须按整数值顺序——agetty 等程序用 >= INIT && ⇐ DEAD 判断):
EMPTY 0 /* 无效槽位 */
RUN_LVL 1 /* 系统启动/关机;ut_user="run-level" */
BOOT_TIME 2 /* 系统启动时间 */
NEW_TIME 3 /* 时钟调整后 */
OLD_TIME 4 /* 时钟调整前 */
INIT_PROCESS 5 /* init spawn 的子进程(如 getty) */
LOGIN_PROCESS 6 /* session leader(login(1)) */
USER_PROCESS 7 /* 真实用户登录 */
DEAD_PROCESS 8 /* 用户进程退出 */
When:每条记录必填 ut_type/ut_user/ut_pid/ut_line/ut_id/ut_tv;ut_line 通常 devname + 5 (去掉 /dev/);ut_id 通常 devname + 8。
40.4 getutxent/getutxid/getutxline 检索
What:三个顺序/索引读取函数;用 setutxent()/endutxent() 管理打开的 fd 和「当前游标」。
Why:who/last/lslogins 等工具都基于此。
How——getutxid 的「类型 + id」分支(来自 §40.4):
// 摘自《The Linux Programming Interface》 第 40 章
/* getutxid(&ut):分两支 */
/* (1) ut_type ∈ {RUN_LVL, BOOT_TIME, NEW_TIME, OLD_TIME} → 找下一个同 type 的 */
/* (2) ut_type ∈ {INIT, LOGIN, USER, DEAD} → 找下一个同 type 且同 ut_id 的 */
getutxline(&ut):找下一个 ut_type==LOGIN_PROCESS || USER_PROCESS 且 ut_line == ut.ut_line 的记录。
陷阱:getutx 函数返回「静态缓存区」的指针——glibc 不缓存,但 SUSv3 允许其他实现缓存导致同一记录反复返回。安全写法:
struct utmpx *res = NULL;
while ((res = getutxline(&ut)) != NULL) {
/* 使用 res */
memset(res, 0, sizeof(*res)); /* 清静态区,否则下次循环同一个对象 */
}
When:scanning 整个文件用 getutxent();找特定终端用 getutxline();找特定 type 用 getutxid()。
40.5 getlogin() 与控制终端反查
What:getlogin() 返回「控制终端 utmp 中的 username」,比 LOGNAME/getpwuid(getuid()) 更可靠。
Why:LOGNAME 用户可篡改;getpwuid() 在多 username 同 UID 时只返回第一个匹配;只有 utmp 反映「实际登录用的名字」。
How:
// 摘自《The Linux Programming Interface》 第 40 章
#include <unistd.h>
char *getlogin(void); /* 返回静态区指针;reentrant 用 getlogin_r */
实现:ttyname(STDIN_FILENO) 找 /dev/pts/0 → 在 utmp 中找 ut_line=="pts/0" 且 ut_type==LOGIN_PROCESS || USER_PROCESS 的记录 → 返回 ut_user。
错误:ENOTTY(无 stdin)、ENXIO(terminal 不在 utmp 中,如某些终端模拟器)。
When:确定用户实际登录名(如 audit/sudo);写「报告上次登录」功能。
40.6 登录登出协议(重点)
What:登录时 USER_PROCESS upsert utmp + 追加 wtmp;登出时 DEAD_PROCESS 清 username 覆盖 utmp + 追加 wtmp。
Why:所有登录服务(login/ssh/telnetd/ftp)的标准做法——否则 who/last 看到的就不是真实状态。
How——完整序列(来自 §40.6 Listing 40-3):
// 摘自《The Linux Programming Interface》 第 40 章
struct utmpx ut;
memset(&ut, 0, sizeof(ut));
ut.ut_type = USER_PROCESS;
strncpy(ut.ut_user, argv[1], sizeof(ut.ut_user));
ut.ut_pid = getpid();
strncpy(ut.ut_line, devname + 5, sizeof(ut.ut_line)); /* skip "/dev/" */
strncpy(ut.ut_id, devname + 8, sizeof(ut.ut_id)); /* skip "tty" */
time((time_t *)&ut.ut_tv.tv_sec);
setutxent();
if (pututxline(&ut) == NULL) errExit("pututxline");
updwtmpx(_PATH_WTMP, &ut); /* 同步追加 */
/* ... 工作 ... 登出时 */
ut.ut_type = DEAD_PROCESS;
ut.ut_user[0] = '\0'; /* 清空 */
time((time_t *)&ut.ut_tv.tv_sec);
setutxent();
pututxline(&ut);
updwtmpx(_PATH_WTMP, &ut);
endutxent();
pututxline 行为:先 getutxid 扫描找匹配的现有记录(按 ut_type + ut_id),找到就覆盖;找不到追加。所以同一个 tty 上多次登录会*改写*而不是叠加。
boot/time 事件:init 写 RUN_LVL/BOOT_TIME;NTP 写 NEW_TIME/OLD_TIME——这些由 init/NTP 完成,登录服务不需管。
When:任何自己实现的 login 服务(控制台 locker、串口 server、telnetd)都要按此序列更新。
40.7 lastlog per-UID 索引
What:/var/log/lastlog 按 UID 直接索引——第 N 个 UID 的记录在 N * sizeof(struct lastlog) 字节偏移。
Why:login(1) 在用户登录时给「Last login: …」提示。
How:
// 摘自《The Linux Programming Interface》 第 40 章 Listing 40-4
struct lastlog llog;
int fd = open(_PATH_LASTLOG, O_RDONLY);
for (j = 1; j < argc; j++) {
uid_t uid = userIdFromName(argv[j]);
if (lseek(fd, uid * sizeof(struct lastlog), SEEK_SET) == -1) ...
if (read(fd, &llog, sizeof(llog)) > 0)
printf("%-8s %-6s %s\n", argv[j], llog.ll_line, ctime(&llog.ll_time));
}
close(fd);
更新:同样 open + lseek + write(如果是 root 或 daemon 身份)。
限制:相同 UID 多登录名无法区分(每个 UID 只有一个槽位)。
When:login 服务更新 lastlog;自定义 LSP(log inspection)工具读 lastlog。
三、关键图表
|
非可视化条目(三个文件 + 9 个 ut_type)
|
|
登录登出事件序列
|
四、思维导图
mindmap
root((第 40 章 登录记账))
三个核心文件
utmp 当前态
wtmp 历史追加
lastlog per-UID 索引
utmpx 结构与常量
13 字段
ut_type 9 常量
无 SUSv3 字段
API 检索
getutxent 顺序
getutxid type+id
getutxline ut_line
setutxent endutxent
getlogin 反查
ttyname 找终端
utmp 反查 user
非 reentrant
登录登出协议
pututxline upsert
updwtmpx 追加
DEAD_PROCESS 覆盖
lastlog 索引
lseek UID 寻址
struct lastlog ll_time
ll_line ll_host
五、重点与易错点
-
三个文件各有分工:utmp 是「当前」集合(一写一覆盖),wtmp 是「历史」日志(追加),lastlog 是「上次」索引(直接 lseek)。混用就会出错。
-
ut_type 9 个常量顺序固定——agetty 等程序用
>= INIT_PROCESS && ⇐ DEAD_PROCESS范围检查;写新程序时按这个顺序枚举。 -
path 必须用
_PATH_UTMP等常量——硬编码/var/run/utmp在容器、SUSv3 移植场景会爆。 -
pututxline 不追加——它先找同 type+id 的旧记录做覆盖,找不到才追加。多次登录同一终端会 改写,并非多条 USER_PROCESS。
-
updwtmpx 不在 glibc 早期版本里——老 Unix 习惯用
login/logout/logwtmp三个不同函数;现代 Linux 代码应封装在updwtmpx()内。 -
getutx 系列返回静态区指针——在循环中要么
memset清静态区,要么使用 copy;多线程首选getut*_r()(但只有传统 utmp API 提供)。 -
getlogin() 失败是常态:daemon 进程(无 stdin)、xterm 没注册 utmp 条目、容器无 tty 等都是 ENOTTY/ENXIO。demangler 是常见 bug。
-
登录/登出必须写两次——utmp upsert/dead + wtmp 追加;只写一个会导致
who看到但last看不到,反之亦然。 -
init 在重启时清理残留 USER_PROCESS——不用担忧自己忘
DEAD_PROCESS的孤魂,但正常运行期间不写会污染。 -
lastlog 按 UID 寻址,UID 大的系统 lastlog 文件会稀疏但允许——
lseek到远超过文件末尾的位置再读会得到 0 字节(EOF)。 -
不要混淆
ut_line和ut_id:ut_line 是终端完整名(如pts/0),ut_id 是后缀 4 字符(如/0或0)。 -
wtmp 文件只会增长——实现一个 logrotate 配置定期 rotate 或清空,否则磁盘爆掉。
-
权限约束:普通用户只能读 utmp/wtmp;写入只能 root 或特定 group(通常
utmp)。错误处理要检查 EACCES。 -
atime/mtime:utmp/wtmp 每次 I/O 都更新——可能成为「高 page dirty rate」的来源之一。
-
跨章衔接:第 6 章 users/groups(getlogin/getlogin_r 配合)、第 8 章 密码 hash、第 9 章 用户 ID 概念;第 11 章 limits/limits.h;第 12 章 系统标识;第 30 章 线程 (utmp 静态区非 reentrant)。