Namespaces 14 - TIME namespace
Linux Namespaces Learning · Previous: CGROUP namespace · Next: From namespaces to a sandbox
TIME (monotonic and boot-time clocks) namespaces in Linux
1 Introduction
The TIME namespace is the youngest namespace; namespaces(7) lists its /proc/PID/ns/time link since Linux 5.6. It virtualises two clocks of the system: CLOCK_MONOTONIC (with its variants CLOCK_MONOTONIC_COARSE and CLOCK_MONOTONIC_RAW), which counts from an unspecified point in the past, in practice the boot, and CLOCK_BOOTTIME (with CLOCK_BOOTTIME_ALARM), which is the same but also counts the time the machine spent suspended. Each time namespace has an offset for each of the two clocks, relative to the initial namespace, and every process in it sees the clocks shifted by that offset. This affects everything that measures against them: clock_gettime(), the sleep and timer calls, and /proc/uptime.
The wall clock, CLOCK_REALTIME, is not virtualised. The date and time of day stay global for the whole machine; the man page says this was left out because of the complexity and overhead it would bring into the kernel.
The examples were run as an ordinary user on a 6.12 kernel with util-linux 2.40. As in the previous articles, --user --map-root-user is there only so that no root is needed; as root unshare --time alone works.
2 Working with TIME namespaces
2.1 The clocks in the default namespace
Look at the uptime, which is derived from the boot-time clock, at the wall clock, and at the time namespace of the shell. The first number in /proc/uptime is the uptime in seconds.
$ cat /proc/uptime $ 41858.36 129381.21 $ uptime -p $ up 11 hours, 37 minutes $ uptime -s $ 2026-10-04 09:28:55 $ date +%F\ %T $ 2026-10-04 21:06:33 $ readlink /proc/self/ns/time $ time:[4026531834]
2.2 Creating a TIME namespace with an offset
unshare --time creates a time namespace, and --boottime N sets the offset of the boot-time clock in seconds. With 86400 the system appears to have been up one day longer, and to have booted one day earlier. The date is the same inside and outside.
$ unshare --user --map-root-user --time --boottime 86400 cat /proc/uptime $ 128258.38 129381.24 $ unshare --user --map-root-user --time --boottime 86400 sh -c "uptime -p; uptime -s; date +%F\ %T" $ up 1 day, 11 hours, 37 minutes $ 2026-10-03 09:28:55 $ 2026-10-04 21:06:33 $ date +%F\ %T $ 2026-10-04 21:06:33
Without a user namespace an ordinary user may not create it:
$ unshare --time true $ unshare: unshare failed: Operation not permitted
2.3 All three clocks side by side
Read the three clocks directly: monotonic, boot-time and realtime, in whole seconds. In the default namespace the first two agree here, because this machine has not been suspended.
$ python3 -c "import time; print(round(time.clock_gettime(time.CLOCK_MONOTONIC)), round(time.clock_gettime(time.CLOCK_BOOTTIME)), round(time.clock_gettime(time.CLOCK_REALTIME)))" $ 41858 41858 1791140794
In a time namespace with --monotonic 3600 and --boottime 86400, the monotonic clock is one hour ahead, the boot-time clock one day ahead, and the realtime clock is untouched.
$ unshare --user --map-root-user --time --boottime 86400 --monotonic 3600 python3 -c "import time; print(round(time.clock_gettime(time.CLOCK_MONOTONIC)), round(time.clock_gettime(time.CLOCK_BOOTTIME)), round(time.clock_gettime(time.CLOCK_REALTIME)))" $ 45458 128258 1791140794
2.4 The offsets: /proc/PID/timens_offsets
The offsets of the namespace a process is in are in /proc/PID/timens_offsets. Each line has three fields: the clock (monotonic or boottime), the offset in seconds and the offset in nanoseconds. In the initial namespace both are zero.
$ cat /proc/self/timens_offsets $ monotonic 0 0 $ boottime 0 0 $ unshare --user --map-root-user --time --boottime 86400 --monotonic 3600 cat /proc/self/timens_offsets $ monotonic 3600 0 $ boottime 86400 0
The offsets are set by writing such lines to this file, which is what unshare does for you. That is possible only while the namespace has no process in it. Once the first process has entered, the offsets are fixed, and a write fails:
$ unshare --user --map-root-user --time --boottime 86400 sh -c "echo \"boottime 5 0\" > /proc/self/timens_offsets" $ sh: line 1: echo: write error: Permission denied
An offset that would make the clock negative is refused as well:
$ unshare --user --map-root-user --time --boottime -999999999 cat /proc/uptime $ unshare: failed to write to /proc/self/timens_offsets: Numerical result out of range
2.5 time and time_for_children
Because the offsets have to be written before anybody is inside, the process that calls unshare(CLONE_NEWTIME) does not move into the new namespace itself. Only the children it creates afterwards are placed there. This is the same pattern as with the PID namespace in article 04, and /proc/PID/ns shows it with two links: time is the namespace the process is in, time_for_children the one its children will get. A raw call of the system call makes it visible:
$ python3 -u -c 'import os $ print("before:", os.readlink("/proc/self/ns/time"), os.readlink("/proc/self/ns/time_for_children")) $ os.unshare(os.CLONE_NEWUSER | os.CLONE_NEWTIME) $ print("after: ", os.readlink("/proc/self/ns/time"), os.readlink("/proc/self/ns/time_for_children")) $ os.system("echo child: $(readlink /proc/self/ns/time)")' $ before: time:[4026531834] time:[4026531834] $ after: time:[4026531834] time:[4026534336] $ child: time:[4026534336]
After the call the process itself is still in the initial namespace (4026531834); its child is in the new one (4026534336). The unshare command hides this step: the program it starts is already inside, with both links pointing to the new namespace.
$ unshare --user --map-root-user --time sh -c "readlink /proc/self/ns/time /proc/self/ns/time_for_children" $ time:[4026534337] $ time:[4026534337]
2.6 Checking and entering a TIME namespace
In terminal 1, leave a process running in a time namespace with an offset of one day.
$ unshare --user --map-root-user --time --boottime 86400 sleep 644
In terminal 2, in the default namespace, find the namespace and read its offsets from outside.
$ lsns -t time $ NS TYPE NPROCS PID USER COMMAND $ 4026531834 time 30 4085 user catatonit -P $ 4026534339 time 1 1243042 user sleep 644 $ cat /proc/1243042/timens_offsets $ monotonic 0 0 $ boottime 86400 0
nsenter -T enters the time namespace of the process (and -U its user namespace). The command run there sees the shifted uptime; the same command on the host does not.
$ nsenter -t 1243042 -U -T --preserve-credentials uptime -p $ up 1 day, 11 hours, 37 minutes $ uptime -p $ up 11 hours, 37 minutes
2.7 What it is for, and what it is not
The time namespace was added for checkpoint/restore and container migration. A program that is frozen on one machine and continued on another, or on the same one after a reboot, has timers and stored timestamps based on the monotonic clock. If that clock suddenly jumped back to a small value, the timers would misfire. The restoring tool creates a time namespace with the offsets that make both clocks continue where they stopped.
It is not a way to give a program a different date. The wall clock is global, and the root of a user namespace has no capability over it:
$ unshare --user --map-root-user --time --boottime 86400 date -s "2030-01-01" $ date: cannot set date: Operation not permitted $ Tue Jan 1 12:00:00 AM CET 2030
The second line is only date printing the value it was asked to set; the clock did not change. A different time zone is not a matter of namespaces either. It is the TZ variable or the /etc/localtime file the program reads:
$ TZ=UTC date +%T\ %Z $ 19:06:50 UTC $ date +%T\ %Z $ 21:06:50 CEST
3 Check yourself
Answer these before you look at the answers:
- A program in a time namespace with
--boottime 86400callsdate. Is the result one day ahead? No.datereadsCLOCK_REALTIME, which is not virtualised. Only the monotonic and boot-time clocks, and with them the uptime, are shifted. - You want to change the offset of a time namespace in which a process is already running. Can you? No.
timens_offsetscan be written only before the first process enters the namespace; afterwards the write fails. - After
unshare(CLONE_NEWTIME), which of the two links in/proc/self/nschanges for the caller? Onlytime_for_children. The caller stays in its namespace and its children are created in the new one, as with a PID namespace. - What is the namespace for? Keeping the monotonic and boot-time clocks consistent for a container that is checkpointed and restored or migrated, so that its timers keep working.
Sources: