Suspending to Disk in Ubuntu-based Distro with BTRFS filesystem

| 5 min read

Hibernation (also known as suspend to disk) is one of the many features which most Linux distributions do not officially support. Suspending to disk is not an easy feature to implement and involves certain risks. The process starts at the kernel.

For a system to enter the hibernation state, the kernel sleeps all active processes pausing the OS and dumping the RAM to non-volatile memory and finally goes into the power-off state where a complete power loss does not result in the loss of data. On restart, it might be slower to exit the state as the memory content is loaded back into RAM before the computer completely starts.

I am working on Pop OS 22.04 with a custom partition structure using BTRFS filesystem and no encrypted partition. While this entry is focused on Pop OS, other Linux-based OS, Ubuntu and Ubuntu-based distributions might find some of its content relevant.

This entry assumes the reader already has a BTRFS system with no swap partition or file.

Enabling suspend to disk in Pop OS requires the following steps:

  • Disable the default Zram
  • Create a swap sub-volume and swapfile
  • Update kernel stubs
  • Test and enable hibernation

There are some limitations to the implementations of Linux swap in BTRFS [1]. A few of these require that the filesystem must be only a single device and have only a single data profile, no copy-on-write, no compression, etc.

# Disable the default Zram

Pop OS now has Zram enabled by default. With this module enabled, the kernel can create a RAM device with active disk compression of files and or inactive memory pages. The RAM device can be used for swap or as a disk for storage of temporary files.

For hibernation, Zram is not a suitable option. The module may be used for swap in RAM to speed up operation, however, contents in the RAM are volatile and do not persist after a power loss.

# Remove Pop OS Zram settings
sudo apt purge pop-default-settings-zram

# Reboot the system
sudo reboot now

# Create swap sub-volume and swapfile

# The sub-volume

BTRFS sub-volumes are mountable file trees (not block devices or logical volumes) with support for more than a single file tree on different path levels. The filesystem has a top-level sub-volume with an ID of 5. For the swap sub-volume to be created on this level, the root sub-volume needs to be mounted with the target sub-volume ID 5.

# List block devices
NAME        UUID                                   SIZE TYPE MOUNTPOINTS
nvme0n1                                          953.9G disk 
├─nvme0n1p1 8132-6E65                                2G part /boot/efi
├─nvme0n1p2                                        128M part 
├─nvme0n1p3 4C747256747242AE                     736.2G part 
├─nvme0n1p4 5A900223900205EB                       990M part 
├─nvme0n1p5 DE40025D40023CB1                      17.8G part 
├─nvme0n1p6 42C6987BC6987141                       1.4G part 
├─nvme0n1p7 269C-2883                                4G part /recovery
└─nvme0n1p8 21d80ba7-c82e-44f6-9ad6-1c815922ac27 191.3G part /home

Your drive will have different names and layouts. The current device is NVME drive and uses a PCI driver, hence the device names begin with nvme. SCSI/PATA/SATA/USB devices may have different names similar to sda, sda1, et cetera.

The target partition is nvme0n1p8 which is the BTRFS partition with the root and home sub-volumes mounted.

# Mount the root level
sudo mount -o subvolid=5,ssd,noatime,space_cache=v2,commit=10,compress=zstd,autodefrag /dev/nvme0n1p8 /mnt

# Create a @swap sub-volume
sudo btrfs subvolume create /mnt/@swap

# List sub-volumes on the root level
sudo btrfs subvolume list /mnt

The mount options used above should be adjusted per use case. For instance, depending on the version of BTRFS progs, the ssd may not be required as BTRFS will enable or disable SSD optimisations based on the disk type [2]. The swap sub-volume will be created on the root level. Listing of the BTRFS sub-volumes will show the new @swap on level 5.

ID 256 gen 102182 top level 5 path @
ID 257 gen 102182 top level 5 path @home
+ ID 258 gen 100589 top level 5 path @swap

# The swapfile

The size of the swap file is proportional to the size of the system RAM. For suspending to disk, we need a swapfile bigger than the RAM. free -h shows the system memory information.

$ free -h
               total        used        free      shared  buff/cache   available
Mem:            31Gi        11Gi        14Gi       1.6Gi       5.0Gi        16Gi

Based on the output, the total memory on our system is 31Gi, hence a swapfile of 33Gi is enough to accommodate the contents of the RAM at hibernation.

This step of the process requires an updated version of BTRFS, at least version 6.1 and above. The version provides easy commands to make a swapfile and to retrieve the resume_offset value[1:1].

sudo btrfs filesystem mkswapfile --size 33G /mnt/@swap/swapfile
Or manually create the swapfile.
sudo truncate -s 0 /mnt/@swap/swapfile
sudo chattr +C /mnt/@swap/swapfile
sudo fallocate -l 33G /mnt/@swap/swapfile
sudo chmod 0600 /mnt/@swap/swapfile
sudo mkswap /mnt/@swap/swapfile

# Enable swap

Update mount entries in /etc/fstab to include the swap sub-volume and the swap file.

# Unmount the root level sub-volume
sudo umount /mnt

# Set a variable to hold the target partition UUID
UUID="$(lsblk --noheadings -o UUID /dev/nvme0n1p8)"

# Update fstab entries
sudo tee -a /etc/fstab << EOF
UUID=$UUID /swap btrfs defaults,subvol=@swap,nodatacow,noatime,nospace_cache,compress=no 0 0
/swap/swapfile none swap defaults 0 0

# Mount all entries including the @swap sub-volume
sudo mount -av

# Activate swapfile
sudo swapon /swap/swapfile

# Show system memory and swap
free -h

The new output of free should include a row with the swap information.

$ free -h
               total        used        free      shared  buff/cache   available
Mem:            31Gi        11Gi        13Gi       1.6Gi       5.8Gi        16Gi
Swap:           32Gi          0B        32Gi

# Update kernel stubs

With the swapfile now available, it only serves to store inactive memory pages when the system is running out of memory. Adding options which can be passed to the kernel at boot time will ensure that the system is resumed after hibernation.

The resume option specifies with disk partition and resume_offset points to the exact location of the swapfile. The resume_offset is required when using a swapfile, otherwise, only the resume is required for a swap partition.

The following command shows the swapfile offset.

$ sudo btrfs inspect-internal map-swapfile /swap/swapfile 
Physical start: 112232574976
Resume offset:      27400531
# Set a variable to hold the offset value
OFFSET="$(sudo btrfs inspect-internal map-swapfile -r /swap/swapfile)"

# Ensure the target partition UUID is still set
echo $UUID

# Optional: Remove any resume and resume_offset options if they already exist
# Replace <UUID> and <OFFSET> with their values
sudo kernelstub -d 'resume=UUID=<UUID>' 'resume_offset=<OFFSET>'

# Add options if they are not present
sudo kernelstub -a "resume=UUID=$UUID resume_offset=$OFFSET"

# Show current configuration
sudo kernelstub -p

# Update initramfs config
echo "resume=UUID=$UUID resume_offset=$OFFSET" | sudo tee /etc/initramfs-tools/conf.d/resume
sudo echo $OFFSET > /sys/power/resume_offset

# Update existing initramfs
sudo update-initramfs -u

# Test and enable hibernation

Ensure hibernation works by executing the following command.

systemctl hibernate

And if there was an issue.

Check the system logs for error messages. Any of the following commands should provide additional information about the problem.

sudo journalctl -r -u systemd-hibernate.service

sudo journalctl -r -u

sudo systemctl list-dependencies -a

Common error messages include:

  • No such device: Check that you have the correct kernel options. Ensure the resume and resume_offset options are correct.
  • No space left on device: The swapfile or swap partition does not have enough space. Your swap space may be less than the RAM or there are multiple swap spaces with at least one not having enough space.

Add Hibernate and Hybrid Sleep options to the power menu, using Hibernate Status Button GNOME-Shell extension.

Ultimately, add the following policy kit.

sudo tee /etc/polkit-1/localauthority/10-vendor.d/com.ubuntu.desktop.pkla << EOF
[Enable hibernate in upower]

[Enable hibernate in logind]

  1. BTRFS Swapfile ↩︎ ↩︎

  2. BTRFS Mount Options ↩︎