Both the utmp and wtmp files are binary files made up of fixed-size records. Each record is one struct utmpx. Because the size is fixed, the kernel and C library can quickly seek to any record by position.
Each record captures a snapshot of a login event: who logged in, from where, on which terminal, at what time, and what type of event it was.
This structure is defined in <utmpx.h>. Every record in the utmp and wtmp files has exactly this layout:
#define _GNU_SOURCE /* Exposes ut_session without __ prefix */
#include <utmpx.h>
/* Helper struct for exit status of a dead process */
struct exit_status {
short e_termination; /* Signal that terminated the process */
short e_exit; /* Exit status of the process */
};
/* Fixed-size string buffers */
#define __UT_LINESIZE 32 /* max terminal name length */
#define __UT_NAMESIZE 32 /* max username length */
#define __UT_HOSTSIZE 256 /* max hostname length */
struct utmpx {
short ut_type; /* Type of record (see constants below) */
pid_t ut_pid; /* PID of the login process */
char ut_line[__UT_LINESIZE]; /* Terminal device name (e.g., "tty1", "pts/0") */
char ut_id[4]; /* Terminal suffix (e.g., "1", "/0") */
char ut_user[__UT_NAMESIZE]; /* Login 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 DEAD_PROCESS
(NOT filled in by init on Linux) */
long ut_session; /* Session ID (for windowing systems) */
struct timeval ut_tv; /* Timestamp when this record was created */
int32_t ut_addr_v6[4]; /* IP address of remote host:
IPv4 uses ut_addr_v6[0] only;
IPv6 uses all 4 elements */
char __unused[20]; /* Reserved for future use */
};
๐ก String fields: All string fields are null-terminated unless the content fills the entire array. For example, a 32-character username with no room for a null terminator won’t have one โ be careful when printing these fields. Use %.32s format or similar.
| Field | Type | Meaning | Example Value |
|---|---|---|---|
| ut_type | short | Type of record (login, logout, boot, etc.) | USER_PROCESS (7) |
| ut_pid | pid_t | PID of the login process (e.g., login, sshd) | 1471 |
| ut_line | char[32] | Terminal device name without “/dev/” prefix | “tty1”, “pts/0” |
| ut_id | char[4] | Suffix of terminal name after “tty”, “pts”, “pty” | “1”, “/0” |
| ut_user | char[32] | Login name of user. Zeroed out on logout. | “ravi” |
| ut_host | char[256] | Remote hostname for SSH/telnet; kernel version for run-level messages | “192.168.1.5” |
| ut_exit | exit_status | Exit status for DEAD_PROCESS records (not filled by init on Linux) | โ |
| ut_session | long | Session ID โ used by windowing systems for terminal windows | 1234 |
| ut_tv | struct timeval | Timestamp of this event (seconds + microseconds) | tv_sec=1717569000 |
| ut_addr_v6 | int32_t[4] | IP of remote host. IPv4 โ [0] only. IPv6 โ all 4. Linux-specific. | {0xC0A80105,0,0,0} |
The ut_line and ut_id fields are derived from the terminal device name. Here is how they relate:
| Full Device Path | ut_line (after /dev/) | ut_id (suffix after tty/pts/pty) | Login Type |
|---|---|---|---|
| /dev/tty1 | tty1 | 1 | Virtual console |
| /dev/tty2 | tty2 | 2 | Virtual console |
| /dev/pts/0 | pts/0 | /0 | Pseudoterminal (SSH/xterm) |
| /dev/pts/7 | pts/7 | /7 | Pseudoterminal (SSH/xterm) |
The ut_id is used as a unique key to find and overwrite a specific record in the utmp file when a user logs out from that terminal.
The ut_type field tells you what kind of event this record represents. These constants are defined in <utmpx.h>:
| Constant | Value | Meaning | Written by |
|---|---|---|---|
| EMPTY | 0 | Slot is empty, no valid data | โ |
| RUN_LVL | 1 | System run-level change (boot/shutdown) | init |
| BOOT_TIME | 2 | System boot time stored in ut_tv | init |
| NEW_TIME | 3 | New system time after clock adjustment | NTP daemon |
| OLD_TIME | 4 | Old system time before clock adjustment | NTP daemon |
| INIT_PROCESS | 5 | Process spawned by init (e.g., getty) | init |
| LOGIN_PROCESS | 6 | Session leader for a login (e.g., login program) | getty/login |
| USER_PROCESS | 7 | Active user login session โ most common | login/sshd |
| DEAD_PROCESS | 8 | Process has exited (logout marker) | init/login |
โ ๏ธ Important: The numeric values (0โ8) are significant. Some programs in the Linux source (like agetty) write range checks like ut_type >= INIT_PROCESS && ut_type <= DEAD_PROCESS to identify process-related records (types 5โ8).
When a user logs into a virtual console, four records appear in the wtmp file in this order:
init when it spawns the getty processgetty after it opens the terminal and prompts for usernamelogin after validating credentials; ut_user is set to usernameinit when it detects the login shell exited (user logged out)๐ก Race condition note: Some versions of init spawn getty before updating wtmp. This can cause the INIT_PROCESS and LOGIN_PROCESS records to appear in reverse order in the wtmp file.
This program reads the utmp file and prints every field of every record โ great for understanding what data is actually stored.
/* print_utmpx_fields.c
* Prints all fields of every record in utmp file
* Compile: gcc print_utmpx_fields.c -o print_utmpx_fields
* Note: run as root if permission denied
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <utmpx.h>
#include <time.h>
#include <arpa/inet.h> /* inet_ntop */
#include <string.h>
const char *ut_type_str(short type)
{
switch (type) {
case EMPTY: return "EMPTY";
case RUN_LVL: return "RUN_LVL";
case BOOT_TIME: return "BOOT_TIME";
case NEW_TIME: return "NEW_TIME";
case OLD_TIME: return "OLD_TIME";
case INIT_PROCESS: return "INIT_PROCESS";
case LOGIN_PROCESS: return "LOGIN_PROCESS";
case USER_PROCESS: return "USER_PROCESS";
case DEAD_PROCESS: return "DEAD_PROCESS";
default: return "UNKNOWN";
}
}
void print_record(const struct utmpx *ut, int num)
{
char timebuf[64];
char ipbuf[INET6_ADDRSTRLEN];
struct tm *tm_info;
printf("--- Record #%d ---\n", num);
printf(" ut_type : %d (%s)\n", ut->ut_type, ut_type_str(ut->ut_type));
printf(" ut_pid : %d\n", (int)ut->ut_pid);
printf(" ut_line : %.32s\n", ut->ut_line);
printf(" ut_id : %.4s\n", ut->ut_id);
printf(" ut_user : %.32s\n", ut->ut_user);
printf(" ut_host : %.256s\n", ut->ut_host);
/* Format timestamp */
tm_info = localtime((time_t *)&ut->ut_tv.tv_sec);
strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", tm_info);
printf(" ut_tv : %s (+ %ldus)\n", timebuf, (long)ut->ut_tv.tv_usec);
/* Show IP if present (IPv4 only check) */
if (ut->ut_addr_v6[0] != 0) {
struct in_addr addr;
addr.s_addr = ut->ut_addr_v6[0];
printf(" ut_addr_v6 : %s (IPv4)\n",
inet_ntop(AF_INET, &addr, ipbuf, sizeof(ipbuf)));
}
printf(" ut_session : %ld\n\n", ut->ut_session);
}
int main(void)
{
struct utmpx *ut;
int count = 0;
printf("sizeof(struct utmpx) = %zu bytes\n\n", sizeof(struct utmpx));
setutxent();
while ((ut = getutxent()) != NULL)
print_record(ut, ++count);
endutxent();
printf("Total records: %d\n", count);
return 0;
}
A practical program that filters utmpx records to show only records of a specific type โ useful for monitoring system boot time or user logins.
/* filter_by_type.c
* Filter utmpx records by type
* Compile: gcc filter_by_type.c -o filter_by_type
* Usage: ./filter_by_type [type_number]
* type 2 = BOOT_TIME, type 7 = USER_PROCESS, type 8 = DEAD_PROCESS
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <utmpx.h>
#include <time.h>
#include <paths.h>
int main(int argc, char *argv[])
{
struct utmpx *ut;
int target_type = USER_PROCESS; /* Default: show logged-in users */
int found = 0;
char timebuf[64];
if (argc > 1)
target_type = atoi(argv[1]);
printf("Filtering records with ut_type = %d\n\n", target_type);
/* Read from wtmp to see historical data too */
if (utmpxname(_PATH_WTMP) == -1) {
perror("utmpxname");
exit(EXIT_FAILURE);
}
setutxent();
while ((ut = getutxent()) != NULL) {
if (ut->ut_type != target_type)
continue;
strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S",
localtime((time_t *)&ut->ut_tv.tv_sec));
printf(" user=%-12.12s line=%-10.10s pid=%5d time=%s\n",
ut->ut_user, ut->ut_line, (int)ut->ut_pid, timebuf);
found++;
}
endutxent();
if (found == 0)
printf(" No records found with ut_type = %d\n", target_type);
else
printf("\nFound %d record(s).\n", found);
return 0;
}
Run examples:
./filter_by_type 2 # Show BOOT_TIME records (when system was booted)
./filter_by_type 7 # Show USER_PROCESS records (who is/was logged in)
./filter_by_type 8 # Show DEAD_PROCESS records (logout events)
Reads the wtmp file and searches for the most recent BOOT_TIME record to find when the system was last rebooted โ similar to the uptime command.
/* find_boot_time.c
* Find last system boot time from wtmp
* Compile: gcc find_boot_time.c -o find_boot_time
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <utmpx.h>
#include <time.h>
#include <paths.h>
#include <string.h>
int main(void)
{
struct utmpx *ut;
struct utmpx last_boot;
int found = 0;
char timebuf[64];
double uptime_seconds;
time_t now;
/* We need to scan wtmp for BOOT_TIME records */
if (utmpxname(_PATH_WTMP) == -1) {
perror("utmpxname");
exit(EXIT_FAILURE);
}
memset(&last_boot, 0, sizeof(last_boot));
setutxent();
/* Scan all records โ keep the latest BOOT_TIME */
while ((ut = getutxent()) != NULL) {
if (ut->ut_type == BOOT_TIME) {
last_boot = *ut; /* Copy the struct */
found = 1;
}
}
endutxent();
if (!found) {
printf("No BOOT_TIME record found in %s\n", _PATH_WTMP);
return 1;
}
strftime(timebuf, sizeof(timebuf), "%A, %d %B %Y %H:%M:%S",
localtime((time_t *)&last_boot.ut_tv.tv_sec));
printf("Last system boot: %s\n", timebuf);
/* Calculate approximate uptime */
time(&now);
uptime_seconds = difftime(now, (time_t)last_boot.ut_tv.tv_sec);
long days = (long)(uptime_seconds / 86400);
long hours = (long)((uptime_seconds - days * 86400) / 3600);
long minutes = (long)((uptime_seconds - days * 86400
- hours * 3600) / 60);
printf("System uptime : %ld days, %ld hours, %ld minutes\n",
days, hours, minutes);
return 0;
}
Answer: ut_line contains the full terminal device filename without the “/dev/” prefix (e.g., “tty1”, “pts/0”). ut_id contains only the suffix after “tty”, “pts”, or “pty” (e.g., “1”, “/0”). ut_id is used as a compact unique key to find a specific record in the utmp file.
Answer: On logout, the existing utmp record for that terminal is overwritten with a new record where ut_type is set to DEAD_PROCESS and ut_user is zeroed out. The ut_line and ut_id remain the same to identify which terminal it belongs to.
Answer: BOOT_TIME records store the system boot time in the ut_tv field. Tools like uptime and who -b read the BOOT_TIME record from the utmp file to determine when the system was last started.
Answer: Some programs use range checks like ut_type >= INIT_PROCESS && ut_type <= DEAD_PROCESS (i.e., values 5โ8) to identify all process-related records. This works correctly because the constants are assigned values in ascending order. Changing these numeric values would break such programs.
Answer: For a remote SSH login, ut_host contains the hostname or IP address of the remote machine. For a local console login, ut_host is typically empty. For run-level records, it contains the kernel version string.
