第 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) 回答这个问题。who(1)/last(1)/lastlog(1) 都依赖这套记账。读者应掌握 (1) 三个文件的语义分工(持久/追加/索引);(2) utmpx 结构的字段(特别是 ut_type 9 个常量);(3) 登录 / 登出序列——写 utmp + 追加 wtmp;(4) getlogin() 如何从 utmp 反查 user。理解了这些,写自己的 sshd/ftp server/console locker 时就能正确更新登录记账。

      一、核心概念

      本章围绕 6 个核心概念展开:核心文件、utmpx 结构和类型常量、getutx* 系列函数、getlogin、登录登出协议、lastlog 文件索引。

      概念 定义 + 重要性 实现提示

      utmp / wtmp 双文件

      /var/run/utmp 当前登录态(一行一用户,登录时 upsert、登出时 DEAD_PROCESS 覆盖);/var/log/wtmp 追加式历史(login/logout/init/boot 各写一行)

      §40.1;用 _PATH_UTMP / _PATH_WTMP 常量;建议 0644 root:utmp

      utmpx 结构与 ut_type 9 类型

      struct utmpx 含 ut_type (1-9 整数) + 终端名 + PID + 用户名 + 远程主机 + 退出状态 + session + 地址 + 时间戳;9 类常量: EMPTY=0/RUN_LVL=1/BOOT_TIME=2/NEW_TIME=3/OLD_TIME=4/INIT_PROCESS=5/LOGIN_PROCESS=6/USER_PROCESS=7/DEAD_PROCESS=8

      §40.3-40.4;运行级/换日志由 init/NTP 写;ut_addr_v6[4] IPv4 用 [0];无 SUSv3 标准化字段:ut_host/ut_exit/ut_session/ut_addr_v6

      getutxent / getutxid / getutxline 检索

      getutxent() 顺序读;getutxid(&ut) 按 ut_type+ut_id 查(RUN_LVL/BOOT_TIME 走 type 分支,INIT/LOGIN/USER/DEAD 走 type+id 分支);getutxline(&ut) 按 ut_line 找 LOGIN/USER 记录;setutxent() 倒带;endutxent() 关闭

      §40.4;返回静态区指针——非 reentrant;glibc 的 getutmpx_*_r 不存在,reentrant 走传统 utmp 的 getutent_r()/getutid_r()/getutline_r()

      getlogin() 与控制终端反查

      getlogin() 通过 ttyname(STDIN_FILENO) 找终端,再到 utmp 查对应 ut_user 字段;比 LOGNAME 环境变量可信

      §40.5;getlogin 失败 ENOTTY(无终端)或 ENXIO(utmp 无记录);reentrant 版本 getlogin_r() 在 glibc

      pututxline / updwtmpx 更新协议

      登录:构造 USER_PROCESS + ut_user/ut_pid/ut_line/ut_id/ut_tv,调 pututxline() 写到 utmp(先 getutxid 找旧记录再覆盖,找不到追加),再调 updwtmpx(_PATH_WTMP, &ut) 追加到 wtmp。登出:把 ut_type 改 DEAD_PROCESS、清 ut_user,再次 pututxline + updwtmpx

      §40.6;getutx* 与 pututxline 共享「current file position」;登出时若 write 失败可调 atexit handler 清 utmp;init 在启动时也会清理 DEAD

      lastlog per-UID 索引

      /var/log/lastloglseek(uid * sizeof(struct lastlog)) 索引;记录 ll_time/lLine/ll_host;login 程序用来向用户报「上次登录时间是 X」

      §40.7;用 _PATH_LASTLOG 常量;同 UID 多用户名无法区分;应用服务器(login/sshd/telnetd)应更新 lastlog

      二、详细笔记

      40.1 utmp + wtmp 概述

      What:UNIX 用两个文件记录登录态:utmp 当前登录用户(who 用),wtmp 历史(last 用)。Linux 在 /var/run/utmp/var/log/wtmp

      Whywho/last/finger/write/talk 都依赖;写网络登录服务(sshd/ftpd)必须更新。

      How

      1. path 常量<paths.h>_PATH_UTMP_PATH_WTMP_PATH_LASTLOG,不要硬编码。

      2. 权限:utmp/wtmp 通常 0664 root:utmp 或 0664 root:adm;普通用户仅能读。

      3. utmpx 名字起源:SysV 用 utmpx/wtmpx 区别 BSD 的 utmp/wtmp;Linux 沿用 SysV 头部结构但只一个 utmp 文件。

      When:写自定义登录服务、终端分配器、who/last 等价查询工具。

      40.2 utmpx API 与 utmp 的关系

      What:Linux 提供 utmpxutmp 两套 API 返回相同信息——utmpx 由 SUSv3 标准化;二者都映射到 /var/run/utmp

      Why:可移植性需要 umtx;utmp API 提供 _r reentrant 版本。

      How

      1. utmpx:所有 x() 函数(setutxent/getutxent/getutxid/getutxline/pututxline/endutxent/utmpxname)。

      2. utmp:传统名字(同函数少 x:setutent/getutent/getutid/getutline/pututline/endutent/utmpname);提供 reentrant 的 getut*_r()

      3. Linux 上两者*等价*:utmp.h 也定义 ut_type 等常量(除 _GNU_SOURCE 之外一般默认)。

      When:默认用 umtx(更好的可移植性);在 multithreaded 程序中必须用 getut*_r()

      40.3 utmpx 结构与 ut_type

      Whatstruct 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_tvut_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_PROCESSut_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() 与控制终端反查

      Whatgetlogin() 返回「控制终端 utmp 中的 username」,比 LOGNAME/getpwuid(getuid()) 更可靠。

      WhyLOGNAME 用户可篡改;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) 字节偏移。

      Whylogin(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)
      文件 / 类型 作用

      /var/run/utmp (_PATH_UTMP)

      当前登录态;登录 upsert、登出覆盖

      /var/log/wtmp (_PATH_WTMP)

      追加历史(login/logout/run-level);用 last(1)

      /var/log/lastlog (_PATH_LASTLOG)

      按 UID 索引的「上次登录」记录;用 lastlog(1)

      struct utmpx

      13 字段:type/pid/line/id/user/host/exit/session/tv/addr/v6

      ut_type==0..8

      9 个常量:EMPTY/RUN_LVL/BOOT_TIME/NEW_TIME/OLD_TIME/INIT_PROCESS/LOGIN_PROCESS/USER_PROCESS/DEAD_PROCESS

      getutxent()

      顺序读

      getutxid(&ut)

      按 type+id 查

      getutxline(&ut)

      按 ut_line 找 LOGIN/USER 记录

      pututxline(&ut)

      upsert utmp(先 getutxid 找旧记录再覆盖)

      updwtmpx(path, &ut)

      追加到 wtmp

      setutxent() / endutxent()

      倒带 / 关闭 utmp fd

      utmpxname(path)

      切换默认文件(配合 /proc/PID/comm 等)

      getlogin()

      从 utmp 反查「控制终端登录名」

      登录登出事件序列
      时刻 utmp 行为 wtmp 行为

      init spawn getty

      INIT_PROCESS 写入(init 写)

      INIT_PROCESS 写入

      用户输 login 名 + 密码

      getty → LOGIN_PROCESS 记录写(getty 写)

      LOGIN_PROCESS 写入

      login(1) 验证成功

      login → USER_PROCESS 记录 upsert + wtmp 追加

      USER_PROCESS 写入

      用户登出 / shell 退出

      init 检测 SIGCHLD → DEAD_PROCESS 覆盖 + wtmp 追加

      DEAD_PROCESS 写入

      重启后还活着 utmp 记录

      init 看到没 logout → DEAD_PROCESS 清理

      —(init 已隐式清理)

      时钟调整(NTP/adjtimex)

      NEW_TIME / OLD_TIME(NTP 写)

      NEW_TIME + OLD_TIME 两条追加

      四、思维导图

      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

      五、重点与易错点

      1. 三个文件各有分工:utmp 是「当前」集合(一写一覆盖),wtmp 是「历史」日志(追加),lastlog 是「上次」索引(直接 lseek)。混用就会出错。

      2. ut_type 9 个常量顺序固定——agetty 等程序用 >= INIT_PROCESS && ⇐ DEAD_PROCESS 范围检查;写新程序时按这个顺序枚举。

      3. path 必须用 _PATH_UTMP 等常量——硬编码 /var/run/utmp 在容器、SUSv3 移植场景会爆。

      4. pututxline 不追加——它先找同 type+id 的旧记录做覆盖,找不到才追加。多次登录同一终端会 改写,并非多条 USER_PROCESS。

      5. updwtmpx 不在 glibc 早期版本里——老 Unix 习惯用 login/logout/logwtmp 三个不同函数;现代 Linux 代码应封装在 updwtmpx() 内。

      6. getutx 系列返回静态区指针——在循环中要么 memset 清静态区,要么使用 copy;多线程首选 getut*_r()(但只有传统 utmp API 提供)。

      7. getlogin() 失败是常态:daemon 进程(无 stdin)、xterm 没注册 utmp 条目、容器无 tty 等都是 ENOTTY/ENXIO。demangler 是常见 bug。

      8. 登录/登出必须写两次——utmp upsert/dead + wtmp 追加;只写一个会导致 who 看到但 last 看不到,反之亦然。

      9. init 在重启时清理残留 USER_PROCESS——不用担忧自己忘 DEAD_PROCESS 的孤魂,但正常运行期间不写会污染。

      10. lastlog 按 UID 寻址,UID 大的系统 lastlog 文件会稀疏但允许——lseek 到远超过文件末尾的位置再读会得到 0 字节(EOF)。

      11. 不要混淆 ut_lineut_id:ut_line 是终端完整名(如 pts/0),ut_id 是后缀 4 字符(如 /00)。

      12. wtmp 文件只会增长——实现一个 logrotate 配置定期 rotate 或清空,否则磁盘爆掉。

      13. 权限约束:普通用户只能读 utmp/wtmp;写入只能 root 或特定 group(通常 utmp)。错误处理要检查 EACCES。

      14. atime/mtime:utmp/wtmp 每次 I/O 都更新——可能成为「高 page dirty rate」的来源之一。

      15. 跨章衔接:第 6 章 users/groups(getlogin/getlogin_r 配合)、第 8 章 密码 hash、第 9 章 用户 ID 概念;第 11 章 limits/limits.h;第 12 章 系统标识;第 30 章 线程 (utmp 静态区非 reentrant)。

      Asciidoc lint check

      asciidoctor: 无警告。