mirror of
https://github.com/checkpoint-restore/criu.git
synced 2026-07-17 16:47:50 +00:00
docs: improve formatting in 'under-the-hood' documentation
Signed-off-by: Andrei Vagin <avagin@gmail.com> Signed-off-by: Andrei Vagin <avagin@google.com>
This commit is contained in:
parent
d0e0ac4520
commit
5c6eaebf04
52 changed files with 1413 additions and 2313 deletions
|
|
@ -1,101 +1,99 @@
|
|||
# 32bit tasks C/R
|
||||
# 32-bit tasks C/R
|
||||
|
||||
## Compatible applications
|
||||
|
||||
On x86_64 there are two types of compatible applications:
|
||||
- ia32 - compiled to run on i686 target, can be executed on x86_64 with `IA32_EMULATION` config option set.
|
||||
- x32 - specially compiled binaries to run on x86_64 machine with `CONFIG_X86_X32` config option set.
|
||||
On x86_64, there are two types of compatibility mode applications:
|
||||
- ia32: Compiled to run on an i686 target, these can be executed on x86_64 if the `IA32_EMULATION` configuration option is enabled.
|
||||
- x32: Specially compiled binaries designed to run on x86_64 with the `CONFIG_X86_X32` configuration option enabled.
|
||||
|
||||
Both of them uses 4 byte pointers thus can address no more than 4Gb of virtual memory.<br />
|
||||
But x32 uses full 64-bit register set (and thus can't be launched on i686 host natively).<br />
|
||||
Both of them requires additional environment on x86_64 as Glibc, libraries, and compiler support.<br />
|
||||
x32 is rarely distributed (at this moment only [Debian x32 port can be easily found](https://wiki.debian.org/X32Port)).<br />
|
||||
So, CRIU will support ia32 C/R at this moment, x32 support may be quite easily added on top of ia32 as needed patches have already added in kernel with ia32 C/R support.<br />
|
||||
The following text uses *compatible* and *32-bit* in the meaning of ia32 applications unless otherwise specified.
|
||||
Both use 4-byte pointers and thus can address no more than 4 GB of virtual memory.
|
||||
However, x32 uses the full 64-bit register set and therefore cannot be launched natively on an i686 host.
|
||||
Both require an additional environment on x86_64, such as Glibc, libraries, and compiler support.
|
||||
x32 is rarely distributed; currently, only the [Debian x32 port](https://wiki.debian.org/X32Port) is easily found.
|
||||
Currently, CRIU supports ia32 C/R. Support for x32 can be added relatively easily, as the necessary kernel patches for ia32 C/R are already in place.
|
||||
In this document, the terms *compatible* and *32-bit* refer to ia32 applications unless otherwise specified.
|
||||
|
||||
## Difference between native and compat applications
|
||||
## Difference between native and compatibility mode applications
|
||||
|
||||
From the CPU's point of view, 32-bit compatibility mode applications differ to 64-bit application by current CS (code segment selector): if corresponding value of L-bit from flags of entry in descriptors table is set the CPU will be in 64-bit mode when this segment descriptor is being used. There are some other differences between 32 and 64-bit selectors, one can read about them [in the article "The 0x33 Segment Selector (Heavens Gate)"](https://www.malwaretech.com/2014/02/the-0x33-segment-selector-heavens-gate.html). Code selectors for both bits are defined in kernel headers as `__USER32_CS` and `__USER_CS` and corresponds to descriptors in GDT (Global Descriptors Table). One can change 64-bit mode to compatibility mode by swapping CS value (e.g., with longjump).
|
||||
From the CPU's point of view, 32-bit compatibility mode applications differ from 64-bit applications by the current Code Segment (CS) selector. If the L-bit (Long mode) in the segment descriptor is set, the CPU operates in 64-bit mode when that descriptor is used. There are other differences between 32-bit and 64-bit selectors; for more details, see [the article "The 0x33 Segment Selector (Heavens Gate)"](https://www.malwaretech.com/2014/02/the-0x33-segment-selector-heavens-gate.html). Code selectors for both modes are defined in kernel headers as `__USER32_CS` and `__USER_CS`, corresponding to descriptors in the Global Descriptor Table (GDT). The mode can be switched from 64-bit to compatibility mode by changing the CS value (e.g., using a long jump).
|
||||
|
||||
From the Linux kernel's point of view, applications differ by values set during exec of application such as `mmap_base` or thread info flags `TIF_ADDR32`/`TIF_IA32`/`TIF_X32`.
|
||||
Both native and compat applications can do 32 or 64-bit syscalls.
|
||||
From the Linux kernel's point of view, applications differ based on values set during `exec`, such as `mmap_base` or thread info flags like `TIF_ADDR32`, `TIF_IA32`, or `TIF_X32`.
|
||||
Both native and compatibility mode applications can perform either 32-bit or 64-bit syscalls.
|
||||
|
||||
## Mixed-bitness applications
|
||||
|
||||
That's entirely possible with current kernel ABI to create mixed-bitness applications, which may be *very* entangled.
|
||||
For example, one could set *both* 32-bit and 64-bit robust futex list pointers.
|
||||
Or one can create multi-threaded application where some threads are executing 32-bit code, some 64-bit code.
|
||||
The current kernel ABI allows for the creation of mixed-bitness applications, which can become quite complex.
|
||||
For instance, an application could set both 32-bit and 64-bit robust futex list pointers.
|
||||
Alternatively, a multi-threaded application could have some threads executing 32-bit code while others execute 64-bit code.
|
||||
|
||||
If we ever meet application of such mixed-bitness kind, the support may be added to CRIU quite easily, but it should be done under some compile-time config as it'll add more syscalls to usual C/R where they aren't needed.
|
||||
If support for such mixed-bitness applications is ever needed, it could be added to CRIU relatively easily. However, this should likely be a compile-time configuration option to avoid adding unnecessary syscalls to standard C/R operations.
|
||||
|
||||
At this moment there is no plans to add such support and it's quite unlikely that we'll find such application in real world (non-syntetic test).
|
||||
Currently, there are no plans to add this support, as such applications are unlikely to be encountered outside of synthetic tests.
|
||||
|
||||
## Approaches to C/R compatible applications
|
||||
## Approaches to C/R for compatibility mode applications
|
||||
|
||||
C/R of compatible applications can be done differently, this section describes cons/pros of each, to address decision why C/R of 32-bit tasks done *that* way and not some other.
|
||||
32-bit C/R can be implemented in several ways. This section describes the pros and cons of various approaches and explains why the current implementation was chosen.
|
||||
|
||||
### Restore with exec() of 32-bit dummy binary vs from 64-bit CRIU
|
||||
### Restore via exec() of a 32-bit dummy binary vs. from 64-bit CRIU
|
||||
|
||||
Restore of 32-bit application can be done with some daemon that runs in 32-bit mode and communicates with CRIU binary (or 32-bit CRIU subprocess).
|
||||
Restoring a 32-bit application could be done using a 32-bit daemon that communicates with the 64-bit CRIU binary or a 32-bit CRIU subprocess.
|
||||
|
||||
-*Pros**:
|
||||
- no kernel patches expected (not quite true: vDSO mremap() still needed support)
|
||||
**Pros**:
|
||||
- No kernel patches expected (though `vDSO mremap()` would still require support).
|
||||
|
||||
-*Cons**:
|
||||
- CRIU code base does not have special restore daemon to communicate with - code needs to be reworked
|
||||
- 64-bit app can have 32-bit child, which could be a parent to 64-bit and so on - need to re-exec native 64-bit CRIU from 32-bit dummy (or 32-bit CRIU)
|
||||
- need to send to the daemon properties of restoring processes, open fds to images, share memory with parsed ps_tree and so on... The number of IPC calls will slow down restore
|
||||
- restoring becomes more complicated, and if looking forward to restoring user/pid sub-namespaces, it will be too entangled
|
||||
- no optimized inheritance for task's properties those erase with exec()
|
||||
- will need also another daemon for x32
|
||||
**Cons**:
|
||||
- The CRIU codebase lacks a dedicated restore daemon, requiring significant rework.
|
||||
- A 64-bit application can have a 32-bit child, which in turn could parent a 64-bit process. This would require re-executing the native 64-bit CRIU from the 32-bit dummy or subprocess.
|
||||
- It would be necessary to send process properties, open image file descriptors, and shared memory containing the parsed `ps_tree` to the daemon. The volume of IPC calls would slow down the restoration process.
|
||||
- Restoration becomes more complex, especially when considering user and PID namespaces.
|
||||
- Task properties that are erased during `exec()` cannot benefit from optimized inheritance.
|
||||
- A separate daemon would also be needed for x32.
|
||||
|
||||
### Restore with a flag to sigreturn() or arch_prctl()
|
||||
|
||||
The initial attempt to do 32-bit C/R, was rejected by lkml community by many reasons. It should have swapped thread info flags (such as `TIF_ADDR32`/`TIF_IA32`/`TIF_X32`), unmap native 64-bit vDSO blob from process's address space and map compatible 32-bit vDSO - all according to some bit in sigframe in `rt_sigreturn()` call or some dedicated for it `arch_prctl()` call.
|
||||
The initial attempt to implement 32-bit C/R was rejected by the LKML community for several reasons. It involved swapping thread info flags (e.g., `TIF_ADDR32`, `TIF_IA32`, `TIF_X32`), unmapping the native 64-bit vDSO, and mapping the 32-bit vDSO based on a bit in the `rt_sigreturn()` sigframe or a dedicated `arch_prctl()` call.
|
||||
|
||||
-*Pros**:
|
||||
- Simple from the point of CRIU: just do sigreturn with a new bit set or call arch_prctl() and do sigreturn
|
||||
**Pros**:
|
||||
- Simple for CRIU: just perform a `sigreturn` with the new bit set or call `arch_prctl` before `sigreturn`.
|
||||
|
||||
-*Cons**:
|
||||
- If 32-bit vDSO image on restored host differ from dumped (in image), need to catch task after sigreturn and make jump trampolines separately - in case of arch_prctl() simpler ([that's why arch_prctl was in initial RFC](https://lkml.org/lkml/2016/6/1/425))
|
||||
- Too many points of failure for one syscall, too complicated
|
||||
- Just adding a way to swap those thread info flags from userspace would result in a new races/bugs (as e.g., TASK_SIZE macro depends on TIF_ADDR32, the mmap code may do unexpected things)
|
||||
**Cons**:
|
||||
- If the 32-bit vDSO on the restoration host differs from the dumped image, the task must be intercepted after `sigreturn` to create jump trampolines (this is simpler with `arch_prctl`).
|
||||
- Too many potential failure points for a single syscall; overly complex.
|
||||
- Allowing userspace to swap thread info flags could introduce new race conditions and bugs (e.g., since the `TASK_SIZE` macro depends on `TIF_ADDR32`, memory mapping behavior might become unpredictable).
|
||||
|
||||
After discussion in lkml, conclusion was: separate changing personality (like thread info flags) from API to map vDSO blobs, remove TIF_IA32 flag that differs 32 from 64-bit tasks and look on syscall's nature: compat syscall, x32 syscall or native syscall.
|
||||
Following LKML discussions, it was decided to separate personality changes from the vDSO mapping API, remove the `TIF_IA32` flag that distinguished 32-bit from 64-bit tasks, and instead rely on the nature of the syscall (compat, x32, or native).
|
||||
|
||||
### Seizing with two 32-bit and 64-bit parasites
|
||||
### Seizing with separate 32-bit and 64-bit parasites
|
||||
|
||||
-*Pros**:
|
||||
- no 32-bit calls in 64-bit parasite and vice-versa
|
||||
- no need in exit in parasite: ptrace code doesn't allow to set 32-bit regset to 64-bit task and the reverse, running parasite the same nature as task bereaves us from those limits
|
||||
**Pros**:
|
||||
- No 32-bit calls in the 64-bit parasite and vice-versa.
|
||||
- Since `ptrace` does not allow setting a 32-bit register set on a 64-bit task (and vice versa), using a parasite of the same nature as the task avoids these limitations.
|
||||
|
||||
-*Cons**:
|
||||
- need to have two/three (for x32 also) blobs for seizing
|
||||
- macros in makefiles to build two parasites
|
||||
- serialization of parasite's answers: arguments to parasite differ in size - serialize them, which added not nice-looking and less readable C macros
|
||||
**Cons**:
|
||||
- Requires maintaining two or three (for x32) separate parasite blobs.
|
||||
- Requires complex Makefile macros to build multiple parasites.
|
||||
- Serializing parasite responses is difficult because argument sizes differ between modes, leading to complex and less readable C macros.
|
||||
|
||||
### Current approach
|
||||
|
||||
FIXME
|
||||
|
||||
## Needs to be done (TODO)
|
||||
## To-Do
|
||||
|
||||
### Kernel patch for vsyscall page
|
||||
|
||||
That's emulated page, not a vma - affects only in /proc/<pid>/maps for restored process. Depends on !TIF_IA32 && !TIF_X32 - Andy got patches for disabling the emulation on per-pid basics, for now I ran tests with `vsyscall=none` boot parameter because zdtm.py checks maps before/after C/R.
|
||||
The `vsyscall` page is emulated and is not a standard VMA; it only appears in `/proc/<pid>/maps`. Its presence depends on `!TIF_IA32` and `!TIF_X32`. Andy Lutomirski has patches to disable this emulation on a per-PID basis. Currently, tests are run with the `vsyscall=none` boot parameter because `zdtm.py` verifies memory maps before and after C/R.
|
||||
|
||||
### Error dump on x32-bit app dumping
|
||||
### Error reporting on x32 binary dumping
|
||||
|
||||
At this moment we'll support only compat ia32 applications, attempt to dump x32 compat binary should result in error.
|
||||
Currently, only ia32 applications are supported. Attempting to dump an x32 binary should result in an error.
|
||||
|
||||
### Continue removing TIF_IA32 from uprobes & Oprofile
|
||||
|
||||
This flag should be gone as it's suggested by Andy & Oleg.
|
||||
There is quite lot of work to make kernel work without it, but small gain:
|
||||
the restored ia32 process will be traced by uprobes/oprofile and stuff like that.
|
||||
As suggested by Andy Lutomirski and Oleg Nesterov, this flag should be removed. While removing it requires significant kernel work, it enables restored ia32 processes to be traced by tools like uprobes and OProfile.
|
||||
|
||||
-*Updated**: Done by [patches](https://lore.kernel.org/all/20201004032536.1229030-1-krisman@collabora.com/T/#u) - that merged to v5.11
|
||||
**Update**: Completed; patches were merged into kernel v5.11.
|
||||
|
||||
## External links
|
||||
- [github issue](https://github.com/checkpoint-restore/criu/issues/43)
|
||||
- [GitHub issue](https://github.com/checkpoint-restore/criu/issues/43)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
# AIO
|
||||
|
||||
To C/R AIO we need to mess with tree things:
|
||||
To C/R AIO, we need to address three things:
|
||||
|
||||
1. AIO ring
|
||||
1. The AIO ring
|
||||
1. Completed events in the ring
|
||||
1. In-flight events out there
|
||||
|
||||
The latter thing is still not supported :(
|
||||
|
||||
|
||||
1. In-flight events
|
||||
|
||||
In-flight events are not yet supported. :(
|
||||
|
|
|
|||
|
|
@ -1,25 +1,24 @@
|
|||
# BPF Maps
|
||||
|
||||
BPF maps are kernel objects which store data (used by BPF programs) in the form of key-value pairs. Applications access BPF maps using file descriptors. C/R of BPF maps involves serializing their *meta-data* and *data*:
|
||||
BPF maps are kernel objects that store data (used by BPF programs) in the form of key-value pairs. Applications access BPF maps using file descriptors. C/R of BPF maps involves serializing their *metadata* and *data*:
|
||||
|
||||
**Meta-data** - This includes information such as map type, key size, value size, etc. CRIU obtains this information from the proc filesystem and using the `bpf` system call with argument `BPF_OBJ_GET_INFO_BY_FD`.
|
||||
**Metadata** - This includes information such as map type, key size, value size, etc. CRIU obtains this information from the `proc` filesystem and via the `bpf` system call with the `BPF_OBJ_GET_INFO_BY_FD` argument.
|
||||
|
||||
**Data** - This is the map's contents, i.e. the actual key-value pairs. CRIU relies on batch operations to read (`BPF_MAP_LOOKUP_BATCH`) key-value pairs from maps during the checkpoint stage and to write (`BPF_MAP_UPDATE_BATCH`) them during the restore phase.
|
||||
**Data** - This is the map's content, i.e., the actual key-value pairs. CRIU relies on batch operations to read (`BPF_MAP_LOOKUP_BATCH`) key-value pairs from maps during the checkpoint stage and to write (`BPF_MAP_UPDATE_BATCH`) them during the restore phase.
|
||||
|
||||
## Support for BPF Maps
|
||||
|
||||
CRIU currently supports C/R of the following BPF map types:
|
||||
|
||||
- BPF_MAP_TYPE_HASH
|
||||
- BPF_MAP_TYPE_ARRAY
|
||||
- `BPF_MAP_TYPE_HASH`
|
||||
- `BPF_MAP_TYPE_ARRAY`
|
||||
|
||||
## To-Do
|
||||
|
||||
- C/R of BTF (BPF Type Format) information
|
||||
- C/R of other kinds of BPF maps
|
||||
|
||||
|
||||
## External Links
|
||||
- [BPF Documentation](https://www.kernel.org/doc/html/latest/bpf/index.html)
|
||||
- [Notes on BPF](https://blogs.oracle.com/linux/notes-on-bpf-1)
|
||||
- [An eBPF Overview](https://www.collabora.com/news-and-blog/blog/2019/04/05/an-ebpf-overview-part-1-introduction/)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,64 +1,63 @@
|
|||
# CGroups
|
||||
|
||||
This page describes how CRIU manages CGroups.
|
||||
This page describes how CRIU manages control groups (cgroups).
|
||||
|
||||
## Overview
|
||||
|
||||
When talking about C/R of CGroups info, we mean three things:
|
||||
C/R of cgroup information involves three components:
|
||||
|
||||
1. The groups tasks live in
|
||||
1. The groups that exist and are visible by tasks
|
||||
1. Mountpoints of "cgroup" file system
|
||||
1. The groups where tasks reside.
|
||||
1. The groups that exist and are visible to tasks.
|
||||
1. Mountpoints of the "cgroup" filesystem.
|
||||
|
||||
CRIU started supporting this info since version 1.3-rc1. Here's how it works.
|
||||
CRIU has supported this information since version 1.3-rc1. Here is how it works.
|
||||
|
||||
## CGroups tasks live in
|
||||
|
||||
CRIU defines a "set" of cgroups. A set is a per-controller list of paths where a task lives. If paths to groups for two tasks differ at least for one controller, they are considered to live in different sets.
|
||||
CRIU defines a "set" of cgroups. A set is a per-controller list of paths where a task resides. If the paths to groups for two tasks differ by at least one controller, they are considered to reside in different sets.
|
||||
|
||||
For every set CRIU generates an ID, which is then stored in the task's `core.tc.cg_set` image. The set in which CRIU lives during dump is also generated and is saved in the inventory image. The set in which the root task lives in is also special -- every other set (except CRIU's one) is checked to contain only sub-dirs of the respective root task's set. Otherwise dump fails.
|
||||
For every set, CRIU generates an ID, which is then stored in the task's `core.tc.cg_set` image. The set in which CRIU resides during dump is also generated and saved in the inventory image. The set in which the root task resides is also special—every other set (except CRIU's own) is checked to ensure it contains only subdirectories of the respective root task's set. Otherwise, the dump fails.
|
||||
|
||||
On restore each task is moved into the respective set. If task's set coincide with CRIU's one task isn't moved anywhere and remains in whatever cgroups CRIU restore was started.
|
||||
On restore, each task is moved into its respective set. If a task's set coincides with CRIU's, the task is not moved and remains in whatever cgroups CRIU restore was started in.
|
||||
|
||||
## CGroups that are visible by tasks
|
||||
## CGroups that are visible to tasks
|
||||
|
||||
Other than CGroups collected with tasks there can be other groups in which no tasks live. To pick up those CRIU gets the root set and saves all the CGroups tree starting from it. This information is stored in the `cgroup.controllers` image. In the same place CRIU saves the properties of CGroups (i.e. values read from CGroup configuration files). Note that since CRIU starts from root set and scans the directories tree, all the paths in this section are also subdirs of the root set's.
|
||||
In addition to cgroups containing tasks, there may be other groups where no tasks reside. To capture these, CRIU identifies the root set and saves the entire cgroup tree starting from it. This information is stored in the `cgroup.controllers` image. In the same image, CRIU saves the properties of the cgroups (i.e., values read from cgroup configuration files). Note that since CRIU starts from the root set and scans the directory tree, all paths in this section are subdirectories of the root set.
|
||||
|
||||
In order to make CRIU handle this information on dump and restore one should specify the `--manage-cgroups` option.
|
||||
To have CRIU handle this information during dump and restore, specify the `--manage-cgroups` option.
|
||||
|
||||
## Dumping more cgroups than are visible
|
||||
|
||||
In some cases, it can be useful to dump a specific cgroup subtree, regardless of what cgroups the container's tasks are in. For example, systemd-based containers like Ubuntu 16.04 will put all of their tasks in one of `/init.scope`, `/system.slice/...`, or `/user.slice/...`. By default, then, CRIU's cgroup engine will not dump the root of the cgroup tree `/`. The problem is that systemd opens `/` as a directory FD and changes the permissions on it, resulting in errors like
|
||||
In some cases, it can be useful to dump a specific cgroup subtree, regardless of which cgroups the container's tasks are in. For example, systemd-based containers like Ubuntu 16.04 will put all of their tasks in one of `/init.scope`, `/system.slice/...`, or `/user.slice/...`. By default, CRIU's cgroup engine will not dump the root of the cgroup tree `/`. The problem is that systemd opens `/` as a directory file descriptor and changes the permissions on it, resulting in errors like:
|
||||
|
||||
`(00.361723) 1: Error (criu/files-reg.c:1487): File sys/fs/cgroup/systemd has bad mode 040755 (expect 040775)`
|
||||
|
||||
The solution is for the container engine to tell CRIU the root of the tree to start dumping at, via `--cgroup-root` on dump, so that these permissions are preserved when checkpointing the cgroup tree.
|
||||
The solution is for the container engine to tell CRIU the root of the tree at which to start dumping via `--cgroup-root` on dump, so that these permissions are preserved when checkpointing the cgroup tree.
|
||||
|
||||
## Mountpoints of "cgroup" file system
|
||||
## Mountpoints of "cgroup" filesystem
|
||||
|
||||
If found in the list of mounts, CRIU would dump one, but only the "root" mount will work. If you bind-mounted some subgroups into container, CRIU dump would fail.
|
||||
If found in the list of mounts, CRIU will dump one, but only the "root" mount will work. If you have bind-mounted subgroups into a container, the CRIU dump will fail.
|
||||
|
||||
## Restoring into different CGroups
|
||||
|
||||
The option syntax is `--cgroup-root [*controller*:]/*path*`. Without this option, CRIU restores tasks and groups that live in the subtrees starting from the root task's dirs. When this option is given, the respective `*controller*`s are restored under the given `*path*`s instead.
|
||||
The option syntax is `--cgroup-root [*controller*:]/*path*`. Without this option, CRIU restores tasks and groups that reside in the subtrees starting from the root task's directories. When this option is provided, the respective `*controller*`s are restored under the given `*path*`s instead.
|
||||
|
||||
## CGroups restoring strategy
|
||||
|
||||
When restoring cgroups CRIU may meet already existing cgroup controllers and as result it relies on user choice how to behave in such case: should it overwrite existing properties with values from the image or should ignore them? Or maybe it is unacceptable to modify any existing cgroup?
|
||||
When restoring cgroups, CRIU may encounter existing cgroup controllers. In such cases, it relies on the user to specify the desired behavior: should it overwrite existing properties with values from the image, or should it ignore them? Or perhaps it is unacceptable to modify any existing cgroup?
|
||||
|
||||
To break a tie CRIU supports that named restore modes, which should be specified as an addition to `--manage-cgroups=*mode*` option. The `*mode*` argument may be one of the following:
|
||||
To resolve this, CRIU supports named restore modes, which are specified via the `--manage-cgroups=*mode*` option. The `*mode*` argument can be one of the following:
|
||||
|
||||
- `none`. Do not restore cgroup properties but require cgroup to pre-exist at the moment of restore procedure.
|
||||
- `props`. Restore cgroup properties and require cgroup to pre-exist.
|
||||
- `soft`. Restore cgroup properties if only cgroup has been created by *criu*, otherwise do not restore properies.
|
||||
- `full`. Always restore all cgroups and their properties.
|
||||
- `strict`. Restore all cgroups and their properties from the scratch, requiring them to not present in the system.
|
||||
- `ignore`. Don't deal with cgroups and pretend that they don't exist.
|
||||
- `none`: Do not restore cgroup properties; require the cgroup to pre-exist at the time of restoration.
|
||||
- `props`: Restore cgroup properties; require the cgroup to pre-exist.
|
||||
- `soft`: Restore cgroup properties only if the cgroup was created by CRIU; otherwise, do not restore properties.
|
||||
- `full`: Always restore all cgroups and their properties.
|
||||
- `strict`: Restore all cgroups and their properties from scratch, requiring that they do not already exist in the system.
|
||||
- `ignore`: Do not manage cgroups and proceed as if they do not exist.
|
||||
|
||||
By default, `soft` is assigned if `--manage-cgroups` option passed without an argument (i.e. the same as `--manage-cgroups=soft`).
|
||||
By default, `soft` is assigned if the `--manage-cgroups` option is passed without an argument (i.e., the same as `--manage-cgroups=soft`).
|
||||
|
||||
## External CGroup yard
|
||||
The option syntax is `--cgroup-yard path`.
|
||||
|
||||
Instead of trying to mount cgroups in CRIU, provide a path to a directory with already created cgroup yard. Useful if you don't want to grant `CAP_SYS_ADMIN` to CRIU. For every cgroup mount there should be exactly one directory. If there is only one controller in this mount, the dir's name should be just the name of the controller. If there are multiple controllers co-mounted, the directory name should be a comma-separated list of controllers.
|
||||
|
||||
Instead of trying to mount cgroups within CRIU, provide a path to a directory containing a pre-created cgroup "yard." This is useful if you do not want to grant `CAP_SYS_ADMIN` to CRIU. For every cgroup mount, there should be exactly one directory. If there is only one controller in the mount, the directory name should simply be the name of the controller. If multiple controllers are co-mounted, the directory name should be a comma-separated list of those controllers.
|
||||
|
|
|
|||
|
|
@ -1,34 +1,31 @@
|
|||
# Change IP address
|
||||
|
||||
When doing a [live migration](live-migration.md) of a process from one host to another a common question is -- how to deal with the different IP address on the destination host. Although the correct answer would be to use containers, moving a service onto different IP address might make sense. This article describes how to do it.
|
||||
When performing a [live migration](live-migration.md) of a process from one host to another, a common question is how to handle a different IP address on the destination host. While the recommended approach is to use containers, moving a service to a different IP address may sometimes be necessary. This article describes how to achieve this.
|
||||
|
||||
> **Note:** This is not yet implemented in CRIU: [https://github.com/checkpoint-restore/criu/issues/211](https://github.com/checkpoint-restore/criu/issues/211)
|
||||
> **Note:** This feature is not yet implemented in CRIU: [https://github.com/checkpoint-restore/criu/issues/211](https://github.com/checkpoint-restore/criu/issues/211)
|
||||
|
||||
## Problem
|
||||
|
||||
Just changing the IP address and letting the things work as they used to is not possible not due to CRIU constraints, but due to how [TCP connection](tcp-connection.md) operates according to the protocol. One cannot proceed the packet flow with one IP address changed, the client would just ignore such packets.
|
||||
Simply changing the IP address and expecting things to work as before is not possible—not because of CRIU constraints, but due to how the [TCP protocol](tcp-connection.md) operates. Packet flow cannot continue if an IP address changes; the client would simply ignore such packets.
|
||||
|
||||
So when talking about migrating a server to some other place with some other IP three things are to be considered.
|
||||
When migrating a server to a host with a different IP address, three things must be considered.
|
||||
|
||||
### Listening sockets
|
||||
|
||||
If your server is bound to 0.0.0.0 (INADDR_ANY) then migration would "just work" there's no IP address that would mismatch. If your server is bound to some device, then you'll have to change the binding IP address. Right now this can be done by editing the [images](images.md), in particular, all PF_INET sockets sit in files.img image and [CRIT](crit.md) can be used to modify one.
|
||||
If your server is bound to `0.0.0.0` (`INADDR_ANY`), then migration will "just work" because no IP address mismatch will occur. If your server is bound to a specific device, you will need to change the binding IP address. Currently, this can be done by editing the [images](images.md); specifically, all `PF_INET` sockets are stored in the `files.img` image, and [CRIT](crit.md) can be used to modify them.
|
||||
|
||||
### In-flight connections
|
||||
|
||||
These are connect()-ed, but not yet accept()-ed. We have an option --skip-in-flight that makes criu ignore these guys.
|
||||
These are connections that have been `connect()`ed but not yet `accept()`ed. The `--skip-in-flight` option allows CRIU to ignore these connections.
|
||||
|
||||
### Established sockets
|
||||
|
||||
These guys are tough, as they do have some real IP address wired into their configuration. Technically it's possible to restore the socket with different IP address (by modifying the inetsk.img with [CRIT](crit.md)), but as was said -- the peer would not accept that. In the worst case the connection would get stuck till TCP timeout.
|
||||
These are challenging because they have a specific IP address baked into their configuration. While it is technically possible to restore a socket with a different IP address (by modifying `inetsk.img` with [CRIT](crit.md)), the peer would not accept it. In the worst case, the connection would remain stuck until a TCP timeout occurs.
|
||||
|
||||
## Possible solution
|
||||
|
||||
So if we're OK with just breaking these connections we need to teach criu to break them. There are two things to consider while doing this.
|
||||
|
||||
a) Dumping sockets. Since we don't really need the connection we'd need to teach criu to skip those guys. The code dumping PF_INET sockets is in criu/sk-inet.c, the code dumping IPPROTO_TCP stuff is in criu/sk-tcp.c
|
||||
|
||||
b) Restoring sockets. Just leaving the hole in the place where the connected socket was is not nice, the server would get wrong error codes from syscalls and, which is worse, the hole might become busy with some other file (when server does open/socket/accept/whatever) which will break server internal logic. So at restore time we'd need to put some stub into the descriptor. I would suggest addressing this dump-time and instead of dumping the established socket into image dump the socket that looks like closed one. In this case socket restoring code would just restore the closed socket into proper place.
|
||||
|
||||
If breaking these connections is acceptable, we need to enable CRIU to do so. There are two aspects to consider:
|
||||
|
||||
a) **Dumping sockets**: Since the connection is no longer needed, CRIU should be taught to skip these sockets. The code for dumping `PF_INET` sockets is in `criu/sk-inet.c`, and the code for dumping `IPPROTO_TCP` data is in `criu/sk-tcp.c`.
|
||||
|
||||
b) **Restoring sockets**: Leaving a "hole" where a connected socket once existed is problematic; the server would receive incorrect error codes from syscalls, and worse, the hole might be filled by another file (e.g., when the server calls `open`, `socket`, or `accept`), breaking the server's internal logic. Therefore, at restore time, a stub should be placed into the descriptor. I suggest addressing this at dump-time: instead of dumping the established socket, dump a socket that appears to be closed. The restoration code would then restore this "closed" socket into the proper place.
|
||||
|
|
|
|||
|
|
@ -1,68 +1,68 @@
|
|||
# Checkpoint/Restore
|
||||
|
||||
This page describes the overall design of how Checkpoint and Restore work in CRIU.
|
||||
This page describes the overall design of the Checkpoint and Restore mechanisms in CRIU.
|
||||
|
||||
## Checkpoint
|
||||
|
||||
The checkpoint procedure relies heavily on **/proc** file system (it's a general place where criu takes all the information it needs).
|
||||
The information gathered from /proc includes:
|
||||
The checkpoint procedure relies heavily on the **/proc** filesystem, which is the primary source of information for CRIU.
|
||||
The information gathered from `/proc` includes:
|
||||
|
||||
- Files descriptors information (via **/proc/$pid/fd** and **/proc/$pid/fdinfo**).
|
||||
- Pipes parameters.
|
||||
- File descriptor information (via **/proc/$pid/fd** and **/proc/$pid/fdinfo**).
|
||||
- Pipe parameters.
|
||||
- Memory maps (via **/proc/$pid/maps** and **/proc/$pid/map_files/**).
|
||||
- etc.
|
||||
- And more.
|
||||
|
||||
The process dumper (called a dumper below) does the following steps during the checkpoint stage.
|
||||
The process dumper (hereafter referred to as the "dumper") performs the following steps during the checkpoint stage.
|
||||
|
||||
### Collect process tree and freeze it
|
||||
The **$pid** of a process group leader is obtained from the command line (`--tree` option). By using this **$pid** the dumper walks though **/proc/$pid/task/** directory collecting threads and through the **/proc/$pid/task/$tid/children** to gathers children recursively. While walking tasks are stopped using the `ptrace`'s `PTRACE_SEIZE` command.
|
||||
### Collecting and Freezing the Process Tree
|
||||
The PID of the process group leader is obtained from the command line (`--tree` option). Using this PID, the dumper traverses the **/proc/$pid/task/** directory to collect threads and **/proc/$pid/task/$tid/children** to gather child processes recursively. During this traversal, tasks are stopped using the `ptrace` `PTRACE_SEIZE` command.
|
||||
|
||||
-See also: [Freezing the tree](freezing-the-tree.md)*
|
||||
*See also: [Freezing the tree](freezing-the-tree.md)*
|
||||
|
||||
### Collect tasks' resources and dump them
|
||||
At this step CRIU reads all the information (it knows) about collected tasks and writes them to dump files. The resources are obtained via
|
||||
1. VMAs areas are parsed from **/proc/$pid/smaps** and mapped files are read from **/proc/$pid/map_files** links
|
||||
1. File descriptor numbers are read via **/proc/$pid/fd**
|
||||
1. Core parameters of a task (such as registers and friends) are being dumped via ptrace interface and parsing **/proc/$pid/stat** entry.
|
||||
### Collecting and Dumping Task Resources
|
||||
In this step, CRIU reads all available information about the collected tasks and writes it to dump files. Resources are obtained as follows:
|
||||
|
||||
Then CRIU injects a [parasite code](parasite-code.md) into a task via ptrace interface. This is done in two steps -- at first we inject only a few bytes for *mmap* syscall at CS:IP the task has at moment of seizing. Then ptrace allow us to run an injected syscall and we allocate enough memory for a parasite code chunk we need for dumping. After that the parasite code is copied into new place inside dumpee address space and CS:IP set respectively to point to our parasite code.
|
||||
1. Virtual Memory Areas (VMAs) are parsed from **/proc/$pid/smaps**, and mapped files are identified via **/proc/$pid/map_files** links.
|
||||
1. File descriptor numbers are retrieved from **/proc/$pid/fd**.
|
||||
1. Core task parameters, such as registers, are dumped using the `ptrace` interface and by parsing the **/proc/$pid/stat** entry.
|
||||
|
||||
From parsite context CRIU does more information such as
|
||||
CRIU then injects [parasite code](parasite-code.md) into the task via the `ptrace` interface. This occurs in two stages: first, a few bytes are injected at the task's current `CS:IP` to execute an `mmap` syscall. Once `ptrace` runs this syscall, enough memory is allocated for the full parasite code. The parasite code is then copied into the dumpee's address space, and `CS:IP` is updated to point to it.
|
||||
|
||||
From the parasite's context, CRIU gathers additional information, such as:
|
||||
1. Credentials
|
||||
1. Contents of memory
|
||||
1. Memory contents
|
||||
|
||||
### Cleanup
|
||||
|
||||
After everything dumped (such as memory pages, which can be written out only from inside dumpee address space) we use ptrace facility again and cure dumpee by dropping out all our parasite code and restoring original code. Then CRIU detaches from tasks and they continue to operate.
|
||||
Once all resources (such as memory pages, which can only be written from within the dumpee's address space) have been dumped, CRIU uses the `ptrace` facility again to "cure" the dumpee by removing the parasite code and restoring the original instructions. CRIU then detaches from the tasks, allowing them to resume operation.
|
||||
|
||||
## Restore
|
||||
|
||||
The restore procedure (aka restorer) is done by CRIU morphing itself into the tasks it restores. On the top-level it consists of 4 steps
|
||||
The restoration procedure (the "restorer") involves CRIU morphing itself into the tasks it is restoring. This process consists of four high-level steps:
|
||||
|
||||
### Resolve shared resources
|
||||
### Resolving Shared Resources
|
||||
|
||||
At this step CRIU reads in image files and finds out which processes share which resources. Later shared resources are restored by some one process and all the others either inherit one on the 2nd stage (like session) or obtain in some other way. The latter is, for example, shared files which are sent with SCM_CREDS messages via unix sockets, or shared memory areas that are restoring via `memfd` file descriptor.
|
||||
CRIU reads the image files to identify which processes share specific resources. Shared resources are typically restored by one process; others either inherit them during the second stage (e.g., sessions) or obtain them through other means. Examples include shared files sent via UNIX sockets with `SCM_RIGHTS` messages or shared memory areas restored via `memfd` file descriptors.
|
||||
|
||||
### Fork the process tree
|
||||
### Forking the Process Tree
|
||||
|
||||
At this step CRIU calls fork() many times to re-created the processes needed to be restored. Note, that threads are not restored here, but on the 4th step.
|
||||
CRIU calls `fork()` repeatedly to recreate the process tree. Note that threads are not restored at this stage, but rather in the fourth step.
|
||||
|
||||
### Restore basic tasks resources
|
||||
### Restoring Basic Task Resources
|
||||
|
||||
Here CRIU restores all resources but
|
||||
In this stage, CRIU restores most resources, excluding:
|
||||
|
||||
1. memory mappings exact location
|
||||
1. timers
|
||||
1. credentials
|
||||
1. threads
|
||||
1. Exact memory mapping locations
|
||||
1. Timers
|
||||
1. Credentials
|
||||
1. Threads
|
||||
|
||||
The restoration of the above four types of resources are delayed till the last stage for the reasons described below. On this stage CRIU opens files, prepares [namespaces](namespaces.md), maps (and fills with data) private memory areas, creates sockets, calls chdir() and chroot() and doing some more.
|
||||
The restoration of these four resource types is delayed until the final stage for the reasons described below. During this stage, CRIU opens files, prepares [namespaces](namespaces.md), maps and populates private memory areas, creates sockets, and performs operations like `chdir()` and `chroot()`.
|
||||
|
||||
### Switch to restorer context, restore the rest and continue
|
||||
### Switching to the Restorer Context, Finalizing Restoration, and Resuming
|
||||
|
||||
The reason for restorer blob is simple. Since criu morphs into the target process, it will have to unmap all its memory and put back the target one. While doing so, some code should exist in memory (the code doing the munmap and mmap). Therefore, the restorer blob is introduced. It's a small piece of code, that doesn't intersect with criu mappings AND target mappings. At the end of stage 2 criu jumps into this blob and restores the memory maps.
|
||||
The restorer blob is necessary because as CRIU morphs into the target process, it must unmap its own memory and map the target's. Some code must remain in memory to perform these `munmap` and `mmap` operations. The restorer blob is a small piece of code positioned so that it does not intersect with either CRIU's or the target's memory mappings. At the end of the third stage, CRIU jumps into this blob to restore the memory maps.
|
||||
|
||||
At the same place we restore timers not to make them fire too early, here we restore credentials to let criu do privileged operations (like fork-with-pid) and threads not to make them suffer from sudden memory layout change.
|
||||
|
||||
-See also: [restorer context](restorer-context.md), [tree after restore](tree-after-restore.md).*
|
||||
In this context, timers are restored (to prevent them from firing prematurely), credentials are set (allowing privileged operations like `fork_with_pid`), and threads are recreated to avoid issues with memory layout changes.
|
||||
|
||||
*See also: [restorer context](restorer-context.md), [tree after restore](tree-after-restore.md).*
|
||||
|
|
|
|||
|
|
@ -2,51 +2,54 @@
|
|||
|
||||
## Summary
|
||||
|
||||
There are two moments in time where criu runs in a somewhat strange environment
|
||||
There are two scenarios where CRIU operates in a specialized environment:
|
||||
|
||||
- [Parasite code](parasite-code.md) execution
|
||||
- [Restore](restorer-context.md) of page dumped contents and yield rt-sigreturn to continue execution of the original program
|
||||
- [Parasite code](parasite-code.md) execution.
|
||||
- [Restoration](restorer-context.md) of dumped page contents and yielding `rt_sigreturn` to continue execution of the original program.
|
||||
|
||||
## Building PIE code blobs for criu
|
||||
## Building PIE Code Blobs for CRIU
|
||||
|
||||
Parasite code executes in the dumpee process context thus it needs to be [PIE](http://en.wikipedia.org/wiki/Position-independent_code)
|
||||
compiled and to have own stack. The same applies to restorer code, which takes place at the very end of restore procedure.
|
||||
Parasite code executes within the context of the dumpee process; therefore, it must be compiled as a Position-Independent Executable (PIE) and maintain its own stack. The same requirements apply to the restorer code used at the end of the restoration process.
|
||||
|
||||
Thus we need to reserve stack, place it statically somewhere in criu and use it at dump/checkpoint stages. To achieve
|
||||
this (and still have some human way to edit source code) we use the following tricks
|
||||
To accommodate this, we reserve a static stack within CRIU for use during the checkpoint and restoration stages. To keep the source code maintainable, we employ the following techniques:
|
||||
|
||||
- Parasite code has own bootstrap code laid in a pure assembler file (parasite_head.S)
|
||||
- Restorer bootstrap code is done in a simpler way in restorer.c.
|
||||
- The parasite code uses its own bootstrap logic, defined in a pure assembly file (`parasite_head.S`).
|
||||
- The restorer bootstrap code is implemented more simply within `restorer.c`.
|
||||
|
||||
For both cases we generate header files which consist of
|
||||
For both cases, we generate header files that include:
|
||||
|
||||
- Functions offsets for export
|
||||
- C array of binary data, for example
|
||||
- Function offsets for export.
|
||||
- A C array of binary data.
|
||||
|
||||
#define parasite_blob_offset____export_parasite_args 0x000000000000002c
|
||||
#define parasite_blob_offset____export_parasite_cmd 0x0000000000000028
|
||||
#define parasite_blob_offset____export_parasite_head_start 0x0000000000000000
|
||||
#define parasite_blob_offset____export_parasite_stack 0x0000000000006034
|
||||
|
||||
static char parasite_blob[] = {
|
||||
0x48, 0x8d, 0x25, 0x2d, 0x60, 0x00, 0x00, 0x48,
|
||||
0x83, 0xec, 0x10, 0x48, 0x83, 0xe4, 0xf0, 0x6a,
|
||||
0x00, 0x48, 0x89, 0xe5, 0x8b, 0x3d, 0x0e, 0x00,
|
||||
0x00, 0x00, 0x48, 0x8d, 0x35, 0x0b, 0x00, 0x00,
|
||||
...
|
||||
};
|
||||
Example:
|
||||
|
||||
These headers we include in the criu compiled file and then use them for checkpoint/restore.
|
||||
```c
|
||||
#define parasite_blob_offset____export_parasite_args 0x000000000000002c
|
||||
#define parasite_blob_offset____export_parasite_cmd 0x0000000000000028
|
||||
#define parasite_blob_offset____export_parasite_head_start 0x0000000000000000
|
||||
#define parasite_blob_offset____export_parasite_stack 0x0000000000006034
|
||||
|
||||
Generation of these files is done in several steps
|
||||
static char parasite_blob[] = {
|
||||
0x48, 0x8d, 0x25, 0x2d, 0x60, 0x00, 0x00, 0x48,
|
||||
0x83, 0xec, 0x10, 0x48, 0x83, 0xe4, 0xf0, 0x6a,
|
||||
0x00, 0x48, 0x89, 0xe5, 0x8b, 0x3d, 0x0e, 0x00,
|
||||
0x00, 0x00, 0x48, 0x8d, 0x35, 0x0b, 0x00, 0x00,
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
- All object files needed by are linked into 'built-in.o'
|
||||
- With help of ld script we move code and data to special layout, i.e. to sections with predefined names and addresses
|
||||
- With help of objcopy we move the section(s) we need to one binary file
|
||||
- With help of hexdump we generate C-styled array of data and put it into -blob.h header
|
||||
These headers are included in the CRIU source files and used during checkpoint/restore.
|
||||
|
||||
## Example of building procedure
|
||||
Generation of these files involves several steps:
|
||||
|
||||
1. All required object files are linked into `built-in.o`.
|
||||
1. Using a linker script, code and data are moved to a specialized layout (i.e., sections with predefined names and addresses).
|
||||
1. Using `objcopy`, the required section(s) are moved into a single binary file.
|
||||
1. Using `hexdump`, a C-style data array is generated and placed into a `-blob.h` header.
|
||||
|
||||
## Example Building Procedure
|
||||
|
||||
```
|
||||
LINK pie/parasite.built-in.o
|
||||
GEN pie/parasite.built-in.bin.o
|
||||
GEN pie/parasite.built-in.bin
|
||||
|
|
@ -56,5 +59,4 @@ Generation of these files is done in several steps
|
|||
GEN pie/restorer.built-in.bin.o
|
||||
GEN pie/restorer.built-in.bin
|
||||
GEN pie/restorer-blob.h
|
||||
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,321 +1,82 @@
|
|||
# Comparison to other CR projects
|
||||
# Comparison to Other C/R Projects
|
||||
|
||||
This pages tries to explain differences between CRIU and other C/R solutions.
|
||||
This page explains the differences between CRIU and other checkpoint/restore (C/R) solutions.
|
||||
|
||||
## DMTCP
|
||||
|
||||
{{:DMTCP}}
|
||||
See [DMTCP](dmtcp.md).
|
||||
|
||||
## BLCR
|
||||
|
||||
Berkeley Lab Checkpoint/Restart (BLCR) is a part of the Scalable Systems Software Suite ,
|
||||
developed by the Future Technologies Group at Lawrence Berkeley National Lab under SciDAC
|
||||
funding from the United States Department of Energy. It is an Open Source, system-level
|
||||
checkpointer designed with High Performance Computing (HPC) applications in mind: in particular
|
||||
CPU and memory intensive batch-scheduled MPI jobs. BLCR is implemented as a GPL-licensed
|
||||
loadable kernel module for Linux 2.4.x and 2.6.x kernels on the x86, x86_64, PPC/PPC64, ARM architectures, and a
|
||||
small LGPL-licensed library.
|
||||
Berkeley Lab Checkpoint/Restart (BLCR) is part of the Scalable Systems Software Suite, developed by the Future Technologies Group at Lawrence Berkeley National Lab with funding from the U.S. Department of Energy. It is an open-source, system-level checkpointer designed for High Performance Computing (HPC) applications, particularly CPU- and memory-intensive batch-scheduled MPI jobs. BLCR is implemented as a GPL-licensed loadable kernel module for Linux 2.4.x and 2.6.x kernels on x86, x86_64, PPC/PPC64, and ARM architectures, along with a small LGPL-licensed library.
|
||||
|
||||
## PinLIT / PinPlay
|
||||
|
||||
PinLIT (Pin-Long Instruction Trace) is a checkpointing tool built on top of Intel's proprietary [PIN binary instrumentation tool](https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool) described on page 48 of [Cristiano Pereira's PhD thesis](https://cseweb.ucsd.edu/~calder/papers/thesis-cristiano.pdf). It records the processor's (big) architectural register state and all pages of memory that contain application and shared library code, optimizing size by only storing memory used during a desired interval.
|
||||
PinLIT (Pin-Long Instruction Trace) is a checkpointing tool built on top of Intel's proprietary [PIN binary instrumentation tool](https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool), as described in [Cristiano Pereira's PhD thesis](https://cseweb.ucsd.edu/~calder/papers/thesis-cristiano.pdf). It records the processor's architectural register state and all memory pages containing application and shared library code, optimizing size by only storing memory used during a specific interval.
|
||||
|
||||
[PinPlay](https://software.intel.com/en-us/articles/program-recordreplay-toolkit) or the Program Record/Replay Toolkit appears to be the successor of or new name for PinLIT.
|
||||
[PinPlay](https://software.intel.com/en-us/articles/program-recordreplay-toolkit) (the Program Record/Replay Toolkit) is the successor to PinLIT. Both tools primarily focus on reducing benchmark runtime on computer architecture simulators using sampling algorithms like SimPoint.
|
||||
|
||||
Both tools appear primarily focused on reducing benchmark runtime on slow computer architecture simulators, leveraging sampling algorithms such as SimPoint.
|
||||
## OpenVZ (In-Kernel)
|
||||
|
||||
## OpenVZ (in-kernel)
|
||||
Legacy OpenVZ (based on RHEL4, RHEL5, and RHEL6 kernels) features in-kernel checkpoint/restore. The source code can be found in `kernel/cpt/`.
|
||||
|
||||
Legacy OpenVZ (RHEL4, RHEL5, RHEL6 based kernels) has in-kernel checkpoint/restore, sources can be found in kernel/cpt/.
|
||||
## CKPT (In-Kernel)
|
||||
|
||||
## CKPT (in-kernel)
|
||||
(In-kernel) [Linux Checkpoint/Restore](https://ckpt.wiki.kernel.org/index.php/Main_Page) was a project active from roughly 2008 to 2010 that aimed to implement checkpoint/restore for Linux processes directly within the kernel.
|
||||
|
||||
(In-kernel) [Linux Checkpoint/Restart](https://ckpt.wiki.kernel.org/index.php/Main_Page) was a project from around 2008 to around 2010 to implement checkpoint/restart of Linux processes.
|
||||
## CRIU, DMTCP, BLCR, and OpenVZ Comparison Table
|
||||
|
||||
## CRIU, DMTCP, BLCR, OpenVZ comparison table
|
||||
|
||||
“looks\seems like yes/no” - i found only unproved message(s) saying “yes”/“no”
|
||||
- **"Yes/No"**: Information based on unverified reports.
|
||||
- **"Not yet"**: Officially planned or no known technical barriers.
|
||||
|
||||
“not yet” - it is officially planned or i found no reasons, why it can’t be done.
|
||||
|
||||
|
||||
{| class="wikitable sortable"
|
||||
|-
|
||||
!
|
||||
! CRIU
|
||||
! DMTCP
|
||||
! BLCR
|
||||
! OpenVZ
|
||||
|
||||
|-
|
||||
| Arch
|
||||
| x86_64, ARM, AArch64, PPC64le
|
||||
| x86, x86_64, ARM
|
||||
| x86, x86_64, PPC/PPC64, ARM
|
||||
| x86, x86_64
|
||||
|
||||
|-
|
||||
| OS
|
||||
| Linux
|
||||
| Linux
|
||||
| Linux
|
||||
| Linux
|
||||
|
||||
|-
|
||||
| Uses standard kernel?
|
||||
| Yes, provided it's 3.11 or later
|
||||
| Yes
|
||||
| Yes, just needs to load module
|
||||
| No. OpenVZ kernel is required
|
||||
|
||||
|-
|
||||
| Can be used without preloading special libraries before app start?
|
||||
| Yes
|
||||
| No
|
||||
| No
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Can be used as non-root user?
|
||||
| Yes, but user can only manipulate tasks belonging to him
|
||||
| Yes
|
||||
| Yes
|
||||
| No
|
||||
|
||||
|-
|
||||
| Can run unmodified programs?
|
||||
| Yes
|
||||
| Yes
|
||||
| No. Statically linked and/or threaded apps are unsupported.
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Can run unprepared tasks?
|
||||
| Yes
|
||||
| No. It preloads the DMTCP library. That library runs before the routine main(). It creates a second thread. The checkpoint thread then creates a socket to the DMTCP coordinator and registers itself. The checkpoint thread also creates a signal handler.
|
||||
| No. CR shall notify processes when a checkpoint is to occur (before the kernel takes a checkpoint) to allow the processes to prepare itself accordingly.
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Retains behavior of the c/r-ed programs?
|
||||
| Yes (but see [What can change after C/R](what-can-change-after-cr.md))
|
||||
| No, because of wrappers on system calls
|
||||
| No, because of wrappers on system calls
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Live migration
|
||||
| Yes, even if kernel, libs, etc are newer. Can use [memory changes tracking](memory-changes-tracking.md) to decrease freeze time
|
||||
| Yes, if both kernels are recent
|
||||
| Yes, but if all components are the same. Even if prelinked addresses are different, it will not restore, but it can save the whole used libs and localization files to restore program on the different machine
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Containers
|
||||
| Yes, LXC and OpenVZ containers
|
||||
| No. It doesn't support namespaces, so it probably can’t dump containers
|
||||
| {{No|Looks like no}}
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Parallel/distributed computations libraries
|
||||
| No (planned)
|
||||
| Yes. OpenMPI, MPICH2, OpenMP, Cilk are alredy supported and Infiniband is in progress
|
||||
| Yes. Cray MPI, Intel MPI, LAM/MPI, MPICH-V, MPICH2, MVAPICH, Open MPI, SGI MPT
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Possible to C/R of gdb with debugged app?
|
||||
| No, because they are using the same interface
|
||||
| Yes
|
||||
| No
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| X Window apps (KDE, GNOME, etc)
|
||||
| Yes, via VNC
|
||||
| Yes, via VNC
|
||||
| {{No|Looks like no}}
|
||||
| Yes, via VNC
|
||||
|
||||
|
||||
|-
|
||||
| Solutions for invocation in the custom software
|
||||
| Yes, [RPC](rpc.md) and [C API](c-api.md)
|
||||
| Yes, plugins and API
|
||||
| {{No|Not yet}}
|
||||
| Yes, via ioctl calls
|
||||
|
||||
|-
|
||||
| colspan="4" |
|
||||
|
||||
|-
|
||||
| Unix sockets
|
||||
| Yes
|
||||
| Yes
|
||||
| No
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| UDP sockets
|
||||
| Yes, both ipv4 and ipv6
|
||||
| {{No|Not yet}}. Developers of dmtcp had no request for this
|
||||
| {{No|Not yet}}
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| TCP sockets
|
||||
| Yes
|
||||
| Yes
|
||||
| {{No|Not yet}}
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Established TCP connection
|
||||
| Yes
|
||||
| No, but you can write a simple DMTCP plugin that tells DMTCP how you want to reconnect on restart
|
||||
| No
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Infiniband
|
||||
| No
|
||||
| {{No|Not yet, developing is on the half-way}}
|
||||
| No
|
||||
| No
|
||||
|
||||
|-
|
||||
| Multithread support
|
||||
| Yes
|
||||
| Yes
|
||||
| Yes
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Multiprocess
|
||||
| Yes
|
||||
| Yes
|
||||
| Yes
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Process groups and sessions
|
||||
| Yes
|
||||
| Yes
|
||||
| {{No|Not yet}}
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Zombies
|
||||
| Yes
|
||||
| No
|
||||
| No
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Namespaces
|
||||
| Yes
|
||||
| No
|
||||
| No
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Ptraced programs
|
||||
| No
|
||||
| Yes
|
||||
| No
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| System V IPC
|
||||
| Yes
|
||||
| Yes
|
||||
| No
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Memory mappings
|
||||
| Yes, all kinds
|
||||
| Yes
|
||||
| Partial
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Pipes
|
||||
| Yes
|
||||
| Yes
|
||||
| {{No|Not yet}}
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Terminals
|
||||
| Yes, but only Unix98 PTYs
|
||||
| Yes
|
||||
| Yes
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Non-POSIX files (inotify, signalfd, eventfd, etc)
|
||||
| Yes, inotify, fanotify, epoll, signalfd, eventfd
|
||||
| Yes, epoll, eventfd, signalfd are already supported and inotify will be supported in future
|
||||
| {{No|Looks like no}}
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Timers
|
||||
| Yes
|
||||
| No. Any counter or timer active since the beginning of a process will consider the restarted process to be a new process.
|
||||
| Yes
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Shared resources (files, mm, etc.)
|
||||
| Yes. SysVIPC, files, fd table and memory
|
||||
| Yes. System V shared memory(shmget, etc.), mmap-based shared memory, shared sockets, pipes, file descriptors
|
||||
| No, but it is planned to support shared mmap regions
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Block devices
|
||||
| No
|
||||
| {{Yes|Looks like yes}}
|
||||
| No
|
||||
| No
|
||||
|
||||
|
||||
|-
|
||||
| Character devices
|
||||
| Yes, only /dev/null, /dev/zero, etc. are supported
|
||||
| Yes, looks like null and zero are supported
|
||||
| Yes, /dev/null and /dev/zero
|
||||
| Yes
|
||||
|
||||
|-
|
||||
| Capture the contents of open files
|
||||
| Yes, if file is unlinked
|
||||
| {{No|Looks like no}}
|
||||
| {{No|Not yet}}
|
||||
| Yes
|
||||
|
||||
|}
|
||||
| Feature | CRIU | DMTCP | BLCR | OpenVZ |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **Arch** | x86_64, ARM, AArch64, PPC64le | x86, x86_64, ARM | x86, x86_64, PPC/PPC64, ARM | x86, x86_64 |
|
||||
| **OS** | Linux | Linux | Linux | Linux |
|
||||
| **Uses standard kernel?** | Yes (3.11+) | Yes | Yes (requires module) | No (requires OpenVZ kernel) |
|
||||
| **No preloading required?** | Yes | No | No | Yes |
|
||||
| **Non-root support?** | Yes (own tasks only) | Yes | Yes | No |
|
||||
| **Unmodified programs?** | Yes | Yes | No (static/threaded unsupported) | Yes |
|
||||
| **Unprepared tasks?** | Yes | No (preloads library) | No (requires notification) | Yes |
|
||||
| **Retains behavior?** | Yes | No (syscall wrappers) | No (syscall wrappers) | Yes |
|
||||
| **Live migration** | Yes (with optimizations) | Yes | Yes (if identical environment) | Yes |
|
||||
| **Containers** | Yes (LXC, OpenVZ) | No | No | Yes |
|
||||
| **Parallel/Distributed libs** | Planned | Yes | Yes | Yes |
|
||||
| **C/R of gdb + app?** | No | Yes | No | Yes |
|
||||
| **X Window apps** | Yes (via VNC) | Yes (via VNC) | No | Yes (via VNC) |
|
||||
| **Custom software API** | Yes (RPC, C API) | Yes (Plugins, API) | Not yet | Yes (via ioctl) |
|
||||
| **Unix sockets** | Yes | Yes | No | Yes |
|
||||
| **UDP sockets** | Yes | Not yet | Not yet | Yes |
|
||||
| **TCP sockets** | Yes | Yes | Not yet | Yes |
|
||||
| **Established TCP** | Yes | No (requires plugin) | No | Yes |
|
||||
| **Infiniband** | No | In progress | No | No |
|
||||
| **Multithread support** | Yes | Yes | Yes | Yes |
|
||||
| **Multiprocess** | Yes | Yes | Yes | Yes |
|
||||
| **Groups and sessions** | Yes | Yes | Not yet | Yes |
|
||||
| **Zombies** | Yes | No | No | Yes |
|
||||
| **Namespaces** | Yes | No | No | Yes |
|
||||
| **Ptraced programs** | No | Yes | No | Yes |
|
||||
| **System V IPC** | Yes | Yes | No | Yes |
|
||||
| **Memory mappings** | Yes (all kinds) | Yes | Partial | Yes |
|
||||
| **Pipes** | Yes | Yes | Not yet | Yes |
|
||||
| **Terminals** | Yes (Unix98 PTYs) | Yes | Yes | Yes |
|
||||
| **Non-POSIX files** | Yes (inotify, epoll, etc.) | Yes | No | Yes |
|
||||
| **Timers** | Yes | No | Yes | Yes |
|
||||
| **Shared resources** | Yes | Yes | Planned | Yes |
|
||||
| **Block devices** | No | Yes | No | No |
|
||||
| **Character devices** | Partial (/dev/null, etc.) | Partial | Partial | Yes |
|
||||
| **Open file content** | Yes (if unlinked) | No | Not yet | Yes |
|
||||
|
||||
## Sources
|
||||
DMTCP:
|
||||
-http://dmtcp.sourceforge.net/
|
||||
-http://dmtcp.sourceforge.net/papers/dmtcp.pdf
|
||||
-http://www.ccs.neu.edu/home/gene/papers/ccgrid06.pdf
|
||||
-http://research.cs.wisc.edu/htcondor/CondorWeek2010/condor-presentations/cooperman-dmtcp.pdf
|
||||
-http://dmtcp.sourceforge.net/papers/mtcp.pdf
|
||||
|
||||
BLCR:
|
||||
-https://upc-bugs.lbl.gov/blcr/doc/html/
|
||||
-https://ftg.lbl.gov/assets/projects/CheckpointRestart/Pubs/LBNL-49659.pdf
|
||||
-https://ftg.lbl.gov/assets/projects/CheckpointRestart/Pubs/blcr.pdf
|
||||
-https://ftg.lbl.gov/assets/projects/CheckpointRestart/Pubs/checkpointSurvey-020724b.pdf
|
||||
-https://ftg.lbl.gov/assets/projects/CheckpointRestart/Pubs/lacsi-2003.pdf
|
||||
-https://ftg.lbl.gov/assets/projects/CheckpointRestart/Pubs/LBNL-60520.pdf
|
||||
**DMTCP:**
|
||||
- http://dmtcp.sourceforge.net/
|
||||
- http://dmtcp.sourceforge.net/papers/dmtcp.pdf
|
||||
|
||||
## External links
|
||||
**BLCR:**
|
||||
- https://upc-bugs.lbl.gov/blcr/doc/html/
|
||||
- https://ftg.lbl.gov/assets/projects/CheckpointRestart/Pubs/blcr.pdf
|
||||
|
||||
## External Links
|
||||
|
||||
- [How does DMTCP work?](http://dmtcp.sourceforge.net/FAQ.html#Internals)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,37 +1,30 @@
|
|||
# Copy-on-write memory
|
||||
# Copy-on-Write Memory
|
||||
|
||||
## Problem
|
||||
Private anonymous mappings are tricky. They are declared to belong to a single process only and contain *its* data, but the Linux kernel optimizes the case when task calls fork() and creates a copy of itself. In this case all private anonymous mappings are "shared" between the parent and the child, but when either of them tries to modify the memory, the respective page is duplicated and the changes occur in the modifier's copy only.
|
||||
Private anonymous mappings present unique challenges. While they are intended to belong to a single process, the Linux kernel optimizes `fork()` by sharing these mappings between the parent and child. When either process modifies the memory, the kernel duplicates the respective page so that changes only affect the modifier's copy. This is known as Copy-on-Write (COW).
|
||||
|
||||
When taking a dump of a process tree, it's totally correct to copy contents of all the anonymous private mappings independently and restore them in the same way -- just mmap and put the memory in there. But with this approach we effectively do the described memory duplication and thus increase memory usage by checkpointed and restore application.
|
||||
When dumping a process tree, it is technically correct to copy the contents of all private anonymous mappings independently and restore them—effectively just calling `mmap()` and populating the memory. However, this approach causes memory duplication, increasing the memory footprint of the restored application compared to the original.
|
||||
|
||||
To fix this, criu in version 0.3 and above does special tricks.
|
||||
To address this, CRIU (version 0.3 and above) employs specialized techniques to maintain COW integrity.
|
||||
|
||||
## How restore works to keep COW intact
|
||||
We have different ideas how to restore COW (Copy-on-write)<ref>[wikipedia:Copy-on-write](wikipediacopy-on-write.md)</ref> memory. In a moment we even thought to use KSM (Kernel Shared Memory)<ref>[wikipedia:Kernel SamePage Merging (KSM)](wikipediakernel-samepage-merging-ksm.md)</ref> for that. As result we found a good way for restoring COW memory (I guess). All VMA (Virtual Memory Area)s are restored in the same way as they were created. Here are two questions:
|
||||
## How Restoration Maintains COW Integrity
|
||||
We explored several ideas for restoring COW memory, including the use of Kernel SamePage Merging (KSM). Ultimately, we developed a robust method where Virtual Memory Areas (VMAs) are restored in the same way they were originally created. This involves addressing two key questions:
|
||||
|
||||
1. Which VMAs should be inherited?
|
||||
1. How to avoid intersections with criu VMAs?
|
||||
1. How can we avoid intersections with CRIU's own VMAs?
|
||||
|
||||
The first question is not resolved completely. Now a VMA is inherited if a parent has a VMA with the same start and end addresses. This covers 99% of cases, but it doesn't work if a VMA was moved.
|
||||
The first question is handled by inheriting a VMA if the parent has a VMA with identical start and end addresses. This covers the vast majority of cases, though it does not currently account for VMAs that have been moved.
|
||||
|
||||
The second question is more interesting. Currently criu reserves continuous space for all private VMAs, then restores all VMAs one by one in this space. Inherited VMAs are moved from a parent space. All VMAs are sorted by start addresses.
|
||||
To address the second question, CRIU reserves continuous space for all private VMAs and restores them one by one. Inherited VMAs are moved from the parent's address space. All VMAs are sorted by their start addresses.
|
||||
|
||||

|
||||
|
||||
In “restorer” all criu’s VMAs are unmapped and private VMAs are space apart. The complexity of this algorithm is linear. Now it looks simple, but I spent a few hours to find it.
|
||||
In the "restorer" phase, all of CRIU's own VMAs are unmapped, and private VMAs are correctly spaced. This algorithm has linear complexity. While it may seem simple now, arriving at this design took significant effort.
|
||||
|
||||
{{Out|“Complexity is easy; simplicity is difficult. -Georgy Shpagin”}}
|
||||
> "Complexity is easy; simplicity is difficult." — Georgy Shpagin
|
||||
|
||||
{{Out|“Everything should be made as simple as possible, but not more simpler. - Albert Einstein”}}
|
||||
|
||||
All VMAs and their contents are restored before forking children, so here is one more item. A parent can change some pages after forking a child, so such pages should be dropped from the child's VMA. For solving this problem bitmaps are used to mark touched pages and madvise() is used to remove extra pages.
|
||||
|
||||
One more case is not handled now. COW memory are not restored if a process is reparented to init.
|
||||
|
||||
## References
|
||||
|
||||
|
||||
{{Like}}
|
||||
> "Everything should be made as simple as possible, but not simpler." — Albert Einstein
|
||||
|
||||
Since all VMAs and their contents are restored before forking child processes, a parent might modify pages after a child is forked. To ensure correctness, these modified pages must be dropped from the child's VMA. CRIU uses bitmaps to track touched pages and employs `madvise()` to remove the redundant pages.
|
||||
|
||||
One case currently not handled is COW memory restoration when a process is reparented to `init`.
|
||||
|
|
|
|||
|
|
@ -1,21 +1,13 @@
|
|||
# DMTCP
|
||||
|
||||
<!--
|
||||
|
||||
WARNING! ACHTUNG! UWAGA! ВНИМАНИЕ! POZOR!
|
||||
|
||||
When editing this article, remember its contents is also being included into [Comparison to other CR projects](comparison-to-other-cr-projects.md).
|
||||
Make sure that one looks OK after you have edited this.
|
||||
|
||||
If you add something here that should not be included there, wrap it into <noinclude>.
|
||||
Pay attention to formatting issues such as amount of vertical whitespace. Thanks!
|
||||
|
||||
-->
|
||||
<noinclude>This article tries to explain the differences between CRIU and DMTCP.
|
||||
<noinclude>
|
||||
This article explains the differences between CRIU and DMTCP.
|
||||
</noinclude>
|
||||
DMTCP implements checkpoint/restore of a process on a library level. This means, that if you want to C/R some application you should launch one with DMTCP library (dynamically) linked from the very beginning. When launched like this, the DMTCP library intercepts a certain amount of library calls from the application, builds a shadow data-base of information about process' internals and then forwards the request down to glibc/kernel. The information gathered is to be used to create an image of the application. With this approach, one can only dump applications known to run successfully with the DMTCP libraries, but the latter doesn't provide proxies for all kernel APIs (for example, inotify() is known to be unsupported). Another implication of this approach is potential performance issues that arise due to proxying of requests.
|
||||
|
||||
Restoration of process set is also tricky, as it frequently requires restoring an object with the predefined ID and kernel is known to provide no APIs for several of them. For example, kernel cannot fork a process with the desired PID. To address that, DMTCP fools a process by intercepting the getpid() library call and providing fake PID value to the application. Such behavior is very dangerous, as application might see wrong files in the /proc filesystem if it will try to access one via its PID.
|
||||
DMTCP implements checkpoint/restore at the library level. This means that to checkpoint/restore an application, it must be launched with the DMTCP library dynamically linked from the start. When launched this way, the DMTCP library intercepts various library calls, builds a shadow database of information about the process's internals, and then forwards requests to `glibc` or the kernel. The gathered information is used to create an application image.
|
||||
|
||||
CRIU, on the other hand, doesn't require any libraries to be pre-loaded. It will checkpoint and restore any arbitrary application, as long as kernel provides all needed facilities. Kernel support for some of CRIU features were added recently, essentially meaning that a recent kernel version might be required.
|
||||
With this approach, only applications known to run successfully with the DMTCP libraries can be dumped. Furthermore, DMTCP does not provide proxies for all kernel APIs (for example, `inotify()` is currently unsupported). Another implication is the potential performance overhead caused by proxying requests.
|
||||
|
||||
Restoring a process set is also complex, as it often requires restoring objects with predefined IDs that the kernel may not allow userspace to set. For example, the kernel traditionally does not allow forking a process with a specific PID. To work around this, DMTCP "fools" the process by intercepting `getpid()` and providing a fake PID value. This behavior can be dangerous, as the application might see incorrect information in the `/proc` filesystem if it attempts to access files via its PID.
|
||||
|
||||
CRIU, by contrast, does not require any libraries to be pre-loaded. It can checkpoint and restore arbitrary applications as long as the kernel provides the necessary facilities. Since support for some CRIU features was added to the kernel relatively recently, a modern kernel version is typically required.
|
||||
|
|
|
|||
|
|
@ -1,92 +1,87 @@
|
|||
# Dumping files
|
||||
# Dumping Files
|
||||
|
||||
This is what CRIU does to dump information about opened files.
|
||||
This page describes how CRIU dumps information about open files.
|
||||
|
||||
## Files, descriptors and inodes in Linux
|
||||
## Files, Descriptors, and Inodes in Linux
|
||||
|
||||
When a task opens a file Linux kernel constructs a chain of 3 objects to serve this.
|
||||
When a task opens a file, the Linux kernel constructs a chain of three objects to manage it:
|
||||
|
||||
**Inode**
|
||||
This is an object which describes file as a couple of meta-data (owner, type, size) and data (the bytes themselves).
|
||||
The Inode describes the file itself, including metadata (owner, type, size) and the actual data (the bytes on disk).
|
||||
|
||||
**Dentry (directory entry)**
|
||||
A helper object that kernel uses to resolve file path into Inode object. If a file has hard-links, then one Inode will have several Dentries.
|
||||
**Dentry (Directory Entry)**
|
||||
A helper object the kernel uses to resolve a file path to an Inode. If a file has hard links, one Inode will have multiple Dentries.
|
||||
|
||||
**File**
|
||||
This one describes how a tasks works with an opened Dentry-Inode pair.
|
||||
The File object describes how a task interacts with an open Dentry-Inode pair (e.g., current offset, access mode).
|
||||
|
||||
**File descriptor**
|
||||
It's a number in task's table, that is used to reference the needed File object
|
||||
**File Descriptor**
|
||||
This is a number in the task's file descriptor table (FDT) that references a specific File object.
|
||||
|
||||
So after `open()` (or some other, see below) system call there will be this chain in the memory
|
||||
After an `open()` (or similar) system call, this chain exists in memory:
|
||||
|
||||

|
||||

|
||||
|
||||
The File object can be referenced by more than on FDT, e.g. when a task calls `fork()` the child one gets new FDT, but it references the same Files as the parent does, i.e. Files become shared objects.
|
||||
A File object can be referenced by more than one FDT. For example, when a task calls `fork()`, the child receives a new FDT that references the same File objects as the parent, making them shared objects.
|
||||
|
||||

|
||||

|
||||
|
||||
The Inode object is also interesting. First of all, remember that in Linux file descriptors can be obtained not only by the `open()` system call, but also by `pipe()` and `socket()` and a bunch of Linux-specific `epoll_create`, `signalfd` and others. So when serving *this* Linux would anyway create the mentioned above chain of File-Dentry-Inode objects, but the Inode one will be different for different calls. And CRIU knows this all and acts respectively :)
|
||||
Inodes are also noteworthy. In Linux, file descriptors can be obtained through `open()`, `pipe()`, `socket()`, and various Linux-specific calls like `epoll_create` or `signalfd`. In all these cases, the kernel creates the File-Dentry-Inode chain, but the type of Inode differs based on the call. CRIU understands these distinctions and handles them accordingly.
|
||||
|
||||
## How info about opened files is stored in CRIU
|
||||
## How Information About Opened Files is Stored in CRIU
|
||||
|
||||
Having said that CRIU introduces several images to keep info about what files are opened by task.
|
||||
CRIU uses several image files to store information about open files.
|
||||
|
||||
### File descriptors
|
||||
### File Descriptors
|
||||
|
||||
First image is the `fdinfo-$id.img` one. This image contains info about FDT of a process. The entries have two important fields -- `fd` and `id`.
|
||||
The first image is `fdinfo-$id.img`, which contains information about a process's FDT. Entries include two critical fields: `fd` (the descriptor number) and `id` (an identifier for the File-Inode pair).
|
||||
|
||||
The *fd* is the descriptor number under which the created on restore File should be put. The *id* is the identifier to the File-Inode object pair in other images.
|
||||
### Files and Inodes
|
||||
|
||||
### Files themselves
|
||||
To simplify matters, CRIU treats the File-Dentry-Inode triplet as a single object. Separate images are used for each Inode type:
|
||||
|
||||
For the sake of simplicity, CRIU doesn't introduce separate states for Files, Dentries and Inodes, leaving it to the kernel. Instead, each triplet is treated as one object and for every Inode type (file, pipe, socket, etc.) separate image is introduced. Thus CRIU has
|
||||
- `reg-files.img`: Regular files (created via `open()`).
|
||||
- `unixsk.img`: UNIX sockets.
|
||||
- `pipes.img`: Pipes.
|
||||
- `inetsk.img`: IP sockets (TCP and UDP).
|
||||
- `signalfd.img`: Signal file descriptors.
|
||||
- And others.
|
||||
|
||||
- reg-files.img for regular files, that are created by open() call
|
||||
- unixsk.img for unix sockets
|
||||
- pipes.img for pipes
|
||||
- inetsk for IP sockets (both TCP and UDP)
|
||||
- signalfd.img for signal fd
|
||||
- etc.
|
||||
A full list of generated image files is available in [Images](images.md).
|
||||
|
||||
A full list of image files generated by CRIU is available in [Images](images.md).
|
||||
Each image stores the appropriate state for the File and Inode. Dentry information, specifically the file path, is primarily preserved for regular files.
|
||||
|
||||
In each of these image files is preserved appropriate information about File and Inode of respective file. Dentry information is effectively stored for regular files only -- the file's *path*.
|
||||
## How CRIU Gathers Information for Dumping
|
||||
|
||||
## How CRIU gets the information to dump
|
||||
During the dump process, CRIU must determine:
|
||||
|
||||
So on dump CRIU needs to find out several things.
|
||||
1. The FD numbers owned by tasks.
|
||||
1. Which File objects are shared between task FDTs.
|
||||
1. The types of Inodes involved.
|
||||
1. The internal state of File and Inode objects.
|
||||
|
||||
1. The FD numbers owned by tasks
|
||||
1. How File-s are shared between tasks' FDTs
|
||||
1. What Inode types are there
|
||||
1. State of File and Inode objects
|
||||
### FD Numbers
|
||||
|
||||
### FD numbers
|
||||
This is straightforward: reading the `/proc/$pid/fd` or `/proc/$pid/fdinfo` directories reveals the required numbers.
|
||||
|
||||
This is simple. Reading the */proc/$pid/fd* or */proc/$pid/fdinfo* directory just shows the required numbers.
|
||||
### File Sharing
|
||||
|
||||
### Files sharing
|
||||
To determine if two FDs across different tasks refer to the same File object, CRIU uses the `kcmp` system call.
|
||||
|
||||
In order to find out whether two Files sitting beyond two FDs of two tasks are the same CRIU [uses](kcmp-trees.md) the `kcmp` system call.
|
||||
### Determining Inode Type
|
||||
|
||||
### Determining Inode type
|
||||
In most cases, the Inode type can be identified by calling `stat()` on the descriptor. CRIU achieves this by asking the [parasite code](parasite-code.md) to send the files back via a UNIX socket using `SCM_RIGHTS`. CRIU then calls `fstat()` on these files and checks the `st_mode` field.
|
||||
|
||||
The inode type in most of the cases can be found out by `stat()`-ing the descriptor. To do this CRIU asks the [parasite code](parasite-code.md) to send back the files via unix socket's `SCM_RIGHTS` message. After this, having the same Files at hands CRIU `fstat`s each and checks the `st_mode` field.
|
||||
|
||||
For some stupid files (signalfd, inotify, etc.) the mode field is zero and CRIU reads the */proc/$pid/fd/$fd/* link. For those the link target uniquely defines the Inode type.
|
||||
For certain specialized files (e.g., `signalfd`, `inotify`), the mode field may be zero. In these cases, CRIU reads the `/proc/$pid/fd/$fd/` link; the link target uniquely identifies the Inode type.
|
||||
|
||||
### State of File and Inode
|
||||
|
||||
From File CRIU needs only two things -- the mode and the position. Both can be read from */proc/$pid/fdinfo/$fd/* and the latter one can be requested via `lseek` call.
|
||||
|
||||
Getting the Inode state is specific to Inode. CRIU uses the following sources of information:
|
||||
|
||||
1. Data from */proc/$pid/fdinfo/$fd/*
|
||||
1. Link target of */proc/$pid/fd/$fd/* link
|
||||
1. Inode-specific ioctl()-s
|
||||
1. Fetch data directly from FD with recv + MSG_PEEK for socket queues and tee for pipes/fifos
|
||||
1. Info from sock_diag modules to get info about sockets
|
||||
|
||||
From the File object, CRIU primarily needs the access mode and the current position. Both are available in `/proc/$pid/fdinfo/$fd/`, and the position can also be retrieved via `lseek()`.
|
||||
|
||||
Gathering Inode-specific state depends on the type. CRIU uses several sources:
|
||||
|
||||
1. Data from `/proc/$pid/fdinfo/$fd/`.
|
||||
1. The target of the `/proc/$pid/fd/$fd/` link.
|
||||
1. Inode-specific `ioctl()` calls.
|
||||
1. Direct data retrieval (e.g., using `recv` with `MSG_PEEK` for socket queues or `tee` for pipes/FIFOs).
|
||||
1. `sock_diag` modules for detailed socket information.
|
||||
|
|
|
|||
|
|
@ -1,33 +1,26 @@
|
|||
# FAQ
|
||||
|
||||
The intention of this page is to hold answers to frequently (and no so frequently) asked questions as well as some random data spread among developers' heads ;-) Note, that a big portion of FAQ-s are about [why C/R fails](why-cr-fails.md).
|
||||
This page provides answers to frequently (and not so frequently) asked questions, along with technical details maintained by the developers. Note that many questions regarding why C/R might fail are addressed in [Why C/R fails](why-cr-fails.md).
|
||||
|
||||
- <b>Q</b>: Why CRIU dumps parts of read-only mappings that map the code section of a binary? For instance, there is a mapping at, say 0x400000, that maps the code of /usr/bin/something. After dump there will be at least a page at 0x400000 in the pagemap
|
||||
- <b>A</b>: The code section may have been COWed, for instance during dynamic load of the shared libraries.
|
||||
- **Q**: Why does CRIU dump parts of read-only mappings that contain the code section of a binary? For example, there is a mapping at `0x400000` for `/usr/bin/something`, and after the dump, there is at least one page at `0x400000` in the pagemap.
|
||||
- **A**: The code section may have been modified via Copy-on-Write (COW), for instance, during the dynamic loading of shared libraries.
|
||||
|
||||
- **Q**: Why can't my [test](zdtm-test-suite.md) perform privileged operations, even though I am running `zdtm.py` as root?
|
||||
- **A**: This is because `zdtm.py` runs sub-tests as a non-existent, non-root user. If your sub-test requires root privileges, add `'flags': 'suid'` to the test's `.desc` file.
|
||||
|
||||
- <b>Q</b>: Why my [test](zdtm-test-suite.md) cannot do privileged operation, though I run zdtm.py as root?
|
||||
- <b>A</b>: That's because zdtm.py runs all sub-tests from non-existing non-root user. If you need root prio in your sub-test add `'flags': 'suid'` into test's .desc file.
|
||||
- **Q**: Is it possible to perform [live migration](live-migration.md) between servers while changing the IP address?
|
||||
- **A**: The short answer is yes, provided that breaking existing connections is acceptable. More details are available in [this article](change-ip-address.md).
|
||||
|
||||
- **Q**: Why does a dump fail with the message "Cannot dump half of a stream unix connection" in the logs?
|
||||
- **A**: This usually occurs in configurations [where C/R fails](when-cr-fails.md). This specific error relates to [external UNIX sockets](external-unix-socket.md).
|
||||
|
||||
- <b>Q</b>: Is it possible to do [live migration](live-migration.md) from one server to another with changing the IP address?
|
||||
- <b>A</b>: Quick answer is -- if breaking the connections is OK, then yes. In details it's described [in this article](change-ip-address.md).
|
||||
- **Q**: Why does a restore fail with a "... pid mismatch ..." error in the logs?
|
||||
- **A**: The PID of a process or thread CRIU is trying to restore is already in use by another process. To resolve this, consider using [CR in namespaces](cr-in-namespace.md).
|
||||
|
||||
|
||||
- <b>Q</b>: Why does dump fail with "Cannot dump half of a stream unix connection" message in logs?
|
||||
- <b>A</b>: There are configurations [when C/R fails](when-cr-fails.md). This particular error is about [external UNIX socket](external-unix-socket.md).
|
||||
|
||||
|
||||
- <b>Q</b>: Why does restore fail with "... pid mismatch ..." error in logs?
|
||||
- <b>A</b>: The PID of the process of thread CRIU tries to restore is already busy. To overcome this, try [CR in namespace](cr-in-namespace.md).
|
||||
|
||||
|
||||
- <b>Q</b>: I've built CRIU, how can I make sure it works?
|
||||
- <b>A</b>: There are two minimal things to do. First is to [check the kernel](check-the-kernel.md) and the second is run the [ZDTM test suite](zdtm-test-suite.md).
|
||||
- **Q**: How can I verify that my CRIU build works correctly?
|
||||
- **A**: There are two primary steps: first, [check the kernel](check-the-kernel.md) compatibility, and second, run the [ZDTM test suite](zdtm-test-suite.md).
|
||||
|
||||
## Docker
|
||||
|
||||
- <b>Q</b>: Cannot restore from images onto freshly created container (E.g. [this](https://github.com/xemul/criu/issues/289) gihub issue)
|
||||
- <b>A</b>: CRIU doesn't checkpoint filesystem, so once you've checkpointed the container, the images contain path to files, that sit in the *modified* root tree. So you cannot restore from images on *another* root tree, you need to get the same tree back. You need to say `docker commit` after the checkpoint and restore only on this particular image.
|
||||
|
||||
|
||||
- **Q**: Why can't I restore from images onto a freshly created container? (See [this GitHub issue](https://github.com/checkpoint-restore/criu/issues/289))
|
||||
- **A**: CRIU does not checkpoint the filesystem itself. When you checkpoint a container, the images contain paths to files residing in the *modified* root filesystem. Therefore, you cannot restore from those images onto a *different* root filesystem; you must use the original filesystem state. You should `docker commit` after the checkpoint and only restore using that specific image.
|
||||
|
|
|
|||
|
|
@ -1,39 +1,35 @@
|
|||
# Fdinfo engine
|
||||
# FDInfo Engine
|
||||
|
||||
= Masters and slaves =
|
||||
1. A file may be referred to by several file descriptors. The descriptors may belong to a single process or to several processes.
|
||||
1. A group of descriptors referring to the same file is called shared. One of the descriptors is named master, others are slaves.
|
||||
1. Every descriptor is described via struct fdinfo_list_entry (fle).
|
||||
1. One process opens a master fle of a file, while other processes, sharing the file, obtain it using scm_rights. See send_fds() and receive_fds() for details.
|
||||
## Masters and Slaves
|
||||
1. A file may be referenced by multiple file descriptors, which may belong to a single process or several different processes.
|
||||
1. A group of descriptors referring to the same file is considered "shared." One descriptor is designated the **master**, while the others are **slaves**.
|
||||
1. Every descriptor is represented by a `struct fdinfo_list_entry` (fle).
|
||||
1. One process opens the master `fle` of a file, while other processes sharing the file obtain it using `SCM_RIGHTS`. See `send_fds()` and `receive_fds()` for details.
|
||||
|
||||
= Per-process file restore =
|
||||
Every file type is described via structure file_desc. We sequentially call file_desc::ops::open(struct file_desc *d, int *new_fd) method for every master file of a process until all masters are restored. The open methods may return three values:
|
||||
- 0 -- restore of the master file is successefuly finished;
|
||||
- 1 -- restore is in progress or it can't be started yet, because of it depends on another files, so the method should be called once again;
|
||||
- -1 -- restore failed.
|
||||
## Per-Process File Restore
|
||||
Every file type is described by a `struct file_desc`. We sequentially call the `file_desc::ops::open(struct file_desc *d, int *new_fd)` method for every master file of a process until all masters are restored. The `open` methods can return three values:
|
||||
- ` 0`: Restoration of the master file has successfully completed.
|
||||
- ` 1`: Restoration is in progress or cannot yet start because of dependencies on other files; the method will be called again.
|
||||
- `-1`: Restoration failed.
|
||||
|
||||
Right after a file is opened for the first time, the open method must return the fd value in the new_fd argument. This allows the common code to send this master to other processes to reopen the master as a slave as soon as possible. At the same time, returning a non-negative new_fd does not mean that the master is restored. The open() callback may return a non-negative new_fd and "1" as return value at the same time.
|
||||
When a file is first opened, the `open` method must return the file descriptor value in the `new_fd` argument. This allows the core code to send this master to other processes so they can reopen it as a slave as soon as possible. Note that returning a non-negative `new_fd` does not necessarily mean the master is fully restored. The `open()` callback may return a non-negative `new_fd` while still returning `1` to indicate that work remains.
|
||||
|
||||
Example. Restore of connected unix socket by open() method.
|
||||
-1)Open a socket, write its file descriptor to new_fd and return 1.
|
||||
-2)Check if peer socket is open and bound. If it's not so, then return 1 and repeat step "2" in next time.
|
||||
-3)Connect to the peer and return 0.
|
||||
**Example: Restoring a connected UNIX socket**
|
||||
1. Open a socket, write its file descriptor to `new_fd`, and return `1`.
|
||||
1. Check if the peer socket is open and bound. If not, return `1` and retry later.
|
||||
1. Connect to the peer and return `0`.
|
||||
|
||||
Note: it's also possible to go to step "2" right after new_fd is written.
|
||||
|
||||
The peer, which bind() the socket waits in "2", must notify the socket, when it is bound:
|
||||
-1)bind(<peer name>);
|
||||
-2)set_fds_event(<socket pid>);
|
||||
|
||||
= Dependencies =
|
||||
1. Slave TTY can only be created after respective master peer is restored. But now we wait even more -- till all masters are restored.
|
||||
1. CTTY must be created after all other TTYs are restored. For all tty dependencies see tty_deps_restored() for the details.
|
||||
1. Epoll can be created in any time, but it can add a fd in its polling list after the corresponding fle is completely restored. The only exception is a epoll listening other epoll. In this case we wait till listened fle is just created (not restored). See epoll_not_ready_tfd().
|
||||
1. Unix socket must wait a peer before connect to it. See peer_is_not_prepared() for the details.
|
||||
1. TCP sockets have a counter on address use.
|
||||
1. Implementing a new relationships between fle stages, check, that you are not introducing a circular dependence (with existing).
|
||||
|
||||
= Notes =
|
||||
1. Pipes (and fifos), unix sockets and TTYs generate two fds in their ->open callbacls, the 2nd one can conflict with some other fd the task restores and (!) this "2nd one" may require sending to some other task.
|
||||
The peer that performs the `bind()` must notify the waiting socket once it is ready:
|
||||
1. `bind(<peer name>)`
|
||||
1. `set_fds_event(<socket pid>)`
|
||||
|
||||
## Dependencies
|
||||
1. A slave TTY can only be created after its respective master peer is restored. Currently, we wait until all masters are restored.
|
||||
1. The controlling terminal (CTTY) must be created after all other TTYs are restored. See `tty_deps_restored()` for details on TTY dependencies.
|
||||
1. Epoll instances can be created at any time, but an FD can only be added to its polling list after the corresponding `fle` is completely restored. The exception is an epoll instance listening to another epoll instance; in this case, we only wait until the listened `fle` is created. See `epoll_not_ready_tfd()`.
|
||||
1. A UNIX socket must wait for its peer before connecting. See `peer_is_not_prepared()` for details.
|
||||
1. TCP sockets use a counter for address usage.
|
||||
1. When implementing new relationships between `fle` stages, ensure you do not introduce circular dependencies.
|
||||
|
||||
## Notes
|
||||
1. Pipes (and FIFOs), UNIX sockets, and TTYs generate two FDs in their `->open` callbacks. The second FD may conflict with another FD the task is restoring, and this second FD may need to be sent to another task.
|
||||
|
|
|
|||
|
|
@ -1,39 +1,34 @@
|
|||
# Filesystems pecularities
|
||||
# Filesystem Peculiarities
|
||||
|
||||
"All filesystems are equal, but some filesystems are more equal than others."
|
||||
|
||||
This page describes how different filesystems affect the CRIU dump (and restore) process.
|
||||
This page describes how different filesystems affect the CRIU dump and restore processes.
|
||||
|
||||
## BTRFS
|
||||
|
||||
When we `stat()` a file we can get on which device it resides by checking the `st_dev` value. However, kernel exposes the device value in some more places. In particular, the device is shown in the `/proc/$pid/[mounts|mountinfo]` files and in the `/proc/$pid/s?maps` ones. Moreover, the sock-diag subsystem recently added into the kernel, reveals the device (and inode) on which a unix socket is bound.
|
||||
When calling `stat()` on a file, we can determine the device it resides on by checking the `st_dev` value. However, the kernel exposes device values in several other places, such as `/proc/$pid/mounts`, `/proc/$pid/mountinfo`, and `/proc/$pid/smaps`. Additionally, the `sock-diag` subsystem reveals the device and inode for bound UNIX sockets.
|
||||
|
||||
The problem with btrfs is that it substitutes the real device number with a virtual one in the `stat()` system call. And once we get this value we cannot compare it to any other device number obtained from other sources, they will always differ (these virtual device numbers are unique).
|
||||
BTRFS is problematic because it replaces the real device number with a virtual one in the `stat()` system call. This virtual device number cannot be compared to device numbers obtained from other sources, as they will always differ.
|
||||
|
||||
In order to address this issue, CRIU performs path-to-device resolution in user-space by analysing the information obtained from the `/proc/$pid/mountinfo` files. The routine in question is `mount.c:phys_stat_resolve_dev()`.
|
||||
To address this, CRIU performs path-to-device resolution in userspace by analyzing information from `/proc/$pid/mountinfo`. This logic is implemented in `mount.c:phys_stat_resolve_dev()`.
|
||||
|
||||
### BTRFS Workaround
|
||||
|
||||
One possible workaround to use BTRFS in combination with CRIU is to disable copy on write (COW). During the discussion in https://github.com/containers/podman/issues/9318 one workaround to use Podman's checkpoint/restore support was: `chattr +C /var/lib/containers`
|
||||
One possible workaround for using BTRFS with CRIU is to disable Copy-on-Write (COW). For example, to use Podman's checkpoint/restore support on BTRFS, you can use: `chattr +C /var/lib/containers`.
|
||||
|
||||
## NFS
|
||||
|
||||
In Linux files have an attribute called `st_nlink` -- the amount of names the file has. When a file is removed (which is done with the `unlink` system call) this counter is decremented and, if it hits zero, the file itself can be removed from disk. Not "is removed", but "can be removed", since the file can be held opened while someone unlinks one. In the latter case the physical removal of the file is delayed till the file is closed.
|
||||
In Linux, files have an `st_nlink` attribute representing the number of names (hard links) pointing to the file. When a file is unlinked, this counter is decremented; if it reaches zero, the file can be physically removed from the disk. However, if a process still holds the file open, physical removal is delayed until the file is closed.
|
||||
|
||||
NFS does special handing in case the nlink value is about to hit zero. The thing is -- if NFS client would send the last unlink request to the server, the latter would just go and kill the file physically, since it doesn't "know" that someone holds this file opened (this information is owned by the client). Instead, client marks the file as "to be deleted on close" and doesn't perform the last nlink decrement immediately. And only when the file is closed and the mentioned flag is seen, the last unlink is sent to the server. And one more thing -- to prevent naming collisions (in simple words -- `open()` by the old name shouldn't file this old file) NFS also renames the file, giving it a special name ".nfsXXX" where XXX is some unique identifier. This trick is called "NFS silly rename".
|
||||
NFS handles this differently. If an NFS client sent the final `unlink` request, the server would immediately delete the file, unaware that the client still has it open. To prevent this, the client marks the file for deletion upon closing and renames it to a special name (e.g., `.nfsXXX`). This is known as "NFS silly rename."
|
||||
|
||||
How does this affect CRIU? In the article "[How hard is it to open a file](how-hard-is-it-to-open-a-file.md)?" it's said, that CRIU should be able to dump and restore files, that are unlinked, but opened. Briefly: if a file is such, CRIU cannot just save the file's path, as once dumped tasks are killed, the fill would stop existing. So CRIU takes these files into the images. But on NFS there's no such thing as "unlinked" file -- it prevents the nlink count from dropping to zero. For CRIU all NFS files look as alive ones.
|
||||
How does this affect CRIU? As discussed in [How hard is it to open a file?](how-hard-is-it-to-open-a-file.md), CRIU must be able to dump and restore open but unlinked files. Normally, CRIU identifies these and stores their content in images. On NFS, however, unlinked files do not appear to have an `nlink` count of zero because of the silly rename.
|
||||
|
||||
To handle this, CRIU checks that a file it dumps resides on NFS (this is simply by checking the `statfs`'s `fs_type` field). If the file is such CRIU then checks its name to be the silly-renamed one. If both checks succeed the file is treated as "opened and unlinked" one. The code in question is the `files-reg.c:nfs_silly_rename()`.
|
||||
To handle this, CRIU checks if a file resides on NFS (via `statfs`). If it does, CRIU checks if the filename follows the silly-rename pattern. If both are true, the file is treated as "open and unlinked." This logic is in `files-reg.c:nfs_silly_rename()`.
|
||||
|
||||
## AUFS
|
||||
|
||||
[AUFS](http://aufs.sourceforge.net) is not (yet) upstream, but Docker guys use one, so does CRIU.
|
||||
|
||||
This FS has a strange BUG -- when we `execv` some file on it, and then check for `/proc/$pid/maps` or `.../smaps` files or links in the `.../map_files/` directory, we would note, that those mappings, that correspond to the executed file are seen under "wrong" paths.
|
||||
|
||||
How wrong are these paths? AUFS joins several subdirectories into one, all these subdirs are called *branches*. And the file seen inside AUFS by one path "really" has some other one -- the path by the file is seen in the respective branch. So in proc in these strange cases we would see the path from branch, instead of the path, by which the file is seen in AUFS.
|
||||
|
||||
This is problematic, since CRIU needs to know the path by which the file was accessed inside AUFS in order to properly restore one. To fix this, CRIU checks that there's an AUFS mount in the game, reads the branches info from sysfs, and then, when meets an AUFS file in mappings, check for the path to belong to one of the branches and "fixes" one. This has sits in the `sysfs_parse.c:fixup_aufs_vma_fd`.
|
||||
AUFS is not in the upstream kernel, but it is used by Docker and supported by CRIU.
|
||||
|
||||
This filesystem has a known issue: when a file is executed (`execv`), the mappings in `/proc/$pid/maps` or `/proc/$pid/smaps` may show "wrong" paths. AUFS combines multiple subdirectories (branches) into one. A file accessed via an AUFS path actually resides in one of these branches. In certain cases, `/proc` shows the path within the branch rather than the AUFS path.
|
||||
|
||||
This is problematic because CRIU needs the AUFS path to properly restore the file. To fix this, CRIU identifies AUFS mounts, reads branch information from `sysfs`, and "fixes" the paths by mapping branch-specific paths back to their AUFS equivalents. This logic is in `sysfs_parse.c:fixup_aufs_vma_fd`.
|
||||
|
|
|
|||
|
|
@ -1,57 +1,35 @@
|
|||
# Final states
|
||||
# Final States
|
||||
|
||||
Final state is the state a process tree ends up in after CRIU dump or restore.
|
||||
The "final state" refers to the status of a process tree after a CRIU dump or restore operation.
|
||||
|
||||
There are 3 possible final states:
|
||||
**Running : processes are running as usual**
|
||||
**Stopped : processes are stopped using SIGSTOP**
|
||||
**Dead : processes are destroyed using SIGKILL**
|
||||
There are three possible final states:
|
||||
- **Running**: Processes continue execution as usual.
|
||||
- **Stopped**: Processes are halted using `SIGSTOP`.
|
||||
- **Dead**: Processes are terminated using `SIGKILL`.
|
||||
|
||||
## Changing the default final states
|
||||
## Changing the Default Final States
|
||||
|
||||
You can use the following command line options with CRIU dump and restore commands
|
||||
to change the default process tree final state:
|
||||
The following command-line options can be used with `criu dump` and `criu restore` to modify the final state:
|
||||
|
||||
- `--leave-stopped`
|
||||
- `--leave-running`
|
||||
|
||||
## criu dump
|
||||
|
||||
By default, the final state of a process tree after `criu dump` is *dead*,
|
||||
meaning CRIU kills the process tree right after dumping it.
|
||||
By default, the final state after a `criu dump` is **dead**, meaning CRIU terminates the process tree immediately after dumping it.
|
||||
|
||||
Such a default makes lot of sense. Suppose the process tree is left running after being dumped.
|
||||
If processes are left running, they will most probably change the filesystem state (for example,
|
||||
delete a file) and/or networking state (for example, close a TCP connection). After such changes
|
||||
CRIU won't be able to restore the process tree from the dump, as the system simply won't have
|
||||
resources that this process tree requires at the moment when it was captured.
|
||||
This default behavior is intentional. If processes were left running, they might change the filesystem (e.g., deleting a file) or networking state (e.g., closing a TCP connection). Such changes would make it impossible to restore the process tree from the dump later, as the required resources would no longer be in the state captured during the dump.
|
||||
|
||||
On the contrary, if a process tree does not destroy any resources it depended upon before dump,
|
||||
then it is possible to restore it after dump made with `--leave-running` option.
|
||||
Be aware, though, that the process tree left running after the dump may modify some state
|
||||
(for example, write to a file) in way that is not compatible on the application level with
|
||||
the process tree restored from this dump later.
|
||||
However, if a process tree does not destroy critical resources, it can be left running using the `--leave-running` option. Note that even in this case, the running processes might modify state (like file contents) in a way that is logically incompatible with a future restoration from that dump.
|
||||
|
||||
Now, let's consider leaving the process tree stopped after performing the dump.
|
||||
One possible use case for that is CRIU debugging. If CRIU was not quite accurate doing dump,
|
||||
then it would leave some traces in the dumped process tree. You can investigate such a process tree
|
||||
in its stopped state. Surely you might find some other cases to use `--leave-stopped`.
|
||||
Leaving a process tree **stopped** is often useful for debugging CRIU. If a dump was not entirely accurate, the traces can be investigated while the processes are in a stopped state.
|
||||
|
||||
Also, leaving the process tree running is naturally necessary for the `predump`
|
||||
command, so currently `--leave-stopped` and `--leave-running` options
|
||||
are ignored for `predump`.
|
||||
Note: Currently, the `--leave-stopped` and `--leave-running` options are ignored for the `predump` command, which naturally requires the process tree to remain running.
|
||||
|
||||
## criu restore
|
||||
|
||||
By default, the final state of a process tree after restore is *running*. That's because one usually
|
||||
wants to immediately resume execution of the process tree after restore. One can use `--leave-stopped`
|
||||
option to restore the process tree and leave it in ''stopped' state.
|
||||
|
||||
## Changing the state from stopped to running
|
||||
|
||||
After criu dumped or restored a process tree and left it in *stopped* state,
|
||||
we may need to continue its execution, changing the state to *running*.
|
||||
To do that, use the [pstree_cont.py](https://github.com/xemul/criu-scripts/blob/master/pstree_cont.py)
|
||||
script. Its sole argument is the PID of a process tree root process.
|
||||
By default, the final state after a restore is **running**, as users typically want to resume execution immediately. The `--leave-stopped` option can be used to leave the restored process tree in a stopped state.
|
||||
|
||||
## Resuming from a Stopped State
|
||||
|
||||
If a process tree was left in a *stopped* state after a dump or restore, you can resume its execution (changing its state to *running*) using the [pstree_cont.py](https://github.com/xemul/criu-scripts/blob/master/pstree_cont.py) script. Its only argument is the PID of the root process of the tree.
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
# Freezing the tree
|
||||
# Freezing the Tree
|
||||
|
||||
## Introduction
|
||||
|
||||
Before we can start checkpointing processes, we have to make sure that they will not change their state. The latter not only includes opening new files, sockets, changing session and other, but also producing new children processes which, in turn, can escape from dumping procedure. In other words, the process tree itself and processes in it must be "immobilized" while we are dumping it. While sounds trivial in theory, it is problematic in real life. The checkpoint is supposed to be transparent to the application we are dumping, thus it must not notice any change in process state transition. Traditionally, processes are stopped with the stop signal, but doing so would disturb the process state.
|
||||
Before we can begin checkpointing processes, we must ensure they cannot change their state. This includes not only preventing them from opening new files or sockets or changing sessions but also stopping them from producing new child processes that might escape the dumping procedure. In other words, the process tree and the individual processes within it must be "immobilized" during the dump. While this sounds trivial in theory, it is challenging in practice. The checkpoint is intended to be transparent to the application, meaning the application should not perceive any change in its state transitions. While processes are traditionally stopped using `SIGSTOP`, doing so can disturb the process state.
|
||||
|
||||
So there are two ways to transparently stop the process tree:
|
||||
There are two primary ways to transparently stop a process tree:
|
||||
|
||||
- Capturing them with ptrace
|
||||
- Freezing them using [freezer cgroup](https://www.kernel.org/doc/Documentation/cgroup-v1/freezer-subsystem.txt).
|
||||
- Capturing them with `ptrace`.
|
||||
- Freezing them using the [freezer cgroup](https://www.kernel.org/doc/Documentation/cgroup-v1/freezer-subsystem.txt).
|
||||
|
||||
## Capturing with ptrace
|
||||
|
||||
(Section content to be added)
|
||||
|
||||
## Using freezer cgroup
|
||||
|
||||
(Section content to be added)
|
||||
|
||||
## See also
|
||||
|
||||
- [Tree after restore](tree-after-restore.md)
|
||||
|
|
@ -21,4 +25,3 @@ So there are two ways to transparently stop the process tree:
|
|||
## External links
|
||||
|
||||
- https://www.kernel.org/doc/Documentation/cgroup-v1/freezer-subsystem.txt
|
||||
|
||||
|
|
|
|||
|
|
@ -1,51 +1,39 @@
|
|||
# Fsnotify
|
||||
|
||||
## Hardness in dumping and restoring of fsnotify
|
||||
## Challenges in Dumping and Restoring Fsnotify
|
||||
|
||||
Fsnotify are implemented quite straightforth -- we can fetch watchees by their handled from procfs output:
|
||||
The implementation of `fsnotify` is relatively straightforward—we can identify watched items by their handles from the `procfs` output:
|
||||
|
||||
pos: 0
|
||||
flags: 02000000
|
||||
inotify wd:3 ino:9e7e sdev:800013 mask:800afce ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:7e9e0000640d1b6d
|
||||
```
|
||||
pos: 0
|
||||
flags: 02000000
|
||||
inotify wd:3 ino:9e7e sdev:800013 mask:800afce ignored_mask:0 fhandle-bytes:8 fhandle-type:1 f_handle:7e9e0000640d1b6d
|
||||
```
|
||||
|
||||
so that on dump we can remember a watchee file handler and open it back on restore retrieving path from file descriptor
|
||||
link provided by procfs.
|
||||
During a dump, CRIU records the watched file handle and, during restoration, reopens it by retrieving the path from the file descriptor link provided by `procfs`.
|
||||
|
||||
This all works just fine until watchees are represented as children of another watch descriptor. Consider one has a
|
||||
directory **dir** and two files under it **a** and **b**:
|
||||
This works well until watched items are descendants of another watch descriptor. Consider a directory `dir` containing two files, `a` and `b`:
|
||||
|
||||
dir
|
||||
`- a
|
||||
`- b
|
||||
```
|
||||
dir
|
||||
`- a
|
||||
`- b
|
||||
```
|
||||
|
||||
and a program sets up fsnotify mark on every file entry, i.e. on **dir** itself and both files. Then imagine
|
||||
a program open both **a** and **b** and then unlink them. This action generates notify events which a program
|
||||
may or may not read yet (thus events queue is not empty) but a user start dumping procedure. Because kernel has
|
||||
not yet any API to peek events from queue (note the *peek* here means to read events without removing them
|
||||
from the queue) we either should ignore the events or refuse to dump.
|
||||
If a program sets up an `fsnotify` mark on `dir` and both files, and then opens and unlinks `a` and `b`, notify events are generated. If these events are still in the queue (i.e., the program has not read them yet) when a dump begins, a problem arises. Because the kernel currently lacks an API to "peek" at events in the queue (reading them without removing them), we must either ignore these events or refuse the dump.
|
||||
|
||||
Refusing dumping might be an option but due to current CRIU design it turns out that we might stuck in situation
|
||||
where any attempt to dump will force CRIU to generate events itself leading to endless cycle. This is mostly because
|
||||
of that named *ghost* files. The *ghost* files are the files which were removed by an application but its file
|
||||
descriptor is still alive. For such scenario we generate a hardlink to the deleted file at moment of dumping which
|
||||
of course generates notify events.
|
||||
Refusing the dump is sometimes necessary because of CRIU's current design, where a dump attempt might force CRIU to generate its own events, leading to an endless cycle. This is primarily due to "ghost" files—files that have been removed by the application but whose file descriptors remain open. For these, CRIU generates a hard link to the deleted file during dumping, which triggers notify events. A similar situation occurs during restoration when ghost files are unlinked, again causing the kernel to generate events.
|
||||
|
||||
Almost the same situation happens on restore procedure -- *ghost* files get unlinked which cause kernel to
|
||||
generate events.
|
||||
Until the `fsnotify` dump/restore procedure is redesigned, we must ignore non-empty notify queues during a dump and accept that CRIU will generate its own events during restoration.
|
||||
|
||||
So until redesign of the dumping/restore procedure for fsnotify system we have to ignore nonempty notify queues
|
||||
on dump and live with the fact that we're generating own events on restore.
|
||||
## Potential Solutions
|
||||
|
||||
## Chopping the knot
|
||||
Possible ways to resolve this include:
|
||||
|
||||
Here are possible ways to resolve the situation
|
||||
- **Late-stage processing**: When dumping, collect `fsnotify` and ghost file descriptors into separate lists and process them at the very end, reading out notify events from the `fsnotify` descriptors.
|
||||
- **Deferred restoration**: During restoration, collect `fsnotify` descriptors in the root CRIU task and defer their restoration until all other files from every child process are restored. Then, restore the notifies and read out all generated events.
|
||||
|
||||
- when dumping files gather fsnotify and ghost file descriptors into separate lists and dump them at the very late stage; then read out notify events from fsnotify descriptors
|
||||
- when restoring files collect fsnotify descriptors into a root criu task deferring theis restore until all other files (from every child process) are restored; then restore notifies and read out all generated events
|
||||
|
||||
both ways require significant rework of CRIU design so for a while we simply print out a warning if fsnotify queue is not empty and continue processing.
|
||||
Both approaches would require significant changes to CRIU's architecture. For now, CRIU simply prints a warning if an `fsnotify` queue is not empty and continues processing.
|
||||
|
||||
## See also
|
||||
- [Irmap](irmap.md)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,320 +1,237 @@
|
|||
# How hard is it to open a file
|
||||
# How Hard is it to Open a File?
|
||||
|
||||
This article outlines what CRIU restore needs to take care of when re-creating an open file descriptor.
|
||||
This article outlines what CRIU must handle when recreating an open file descriptor during restoration.
|
||||
|
||||
Let's imagine we have an information about a file we want to open.
|
||||
What should it contain? Apparently, access mode and path:
|
||||
|
||||
```C
|
||||
|
||||
struct file {
|
||||
char *path;
|
||||
unsigned mode;
|
||||
} *f;
|
||||
Suppose we have information about a file we want to open, such as its access mode and path:
|
||||
|
||||
```c
|
||||
struct file {
|
||||
char *path;
|
||||
unsigned mode;
|
||||
} *f;
|
||||
```
|
||||
|
||||
and we'd like to have that path being opened by a process. We would
|
||||
do it like below:
|
||||
|
||||
```C
|
||||
|
||||
int fd;
|
||||
|
||||
fd = open(f->path, f->mode);
|
||||
To have this path opened by a process, we might try:
|
||||
|
||||
```c
|
||||
int fd;
|
||||
fd = open(f->path, f->mode);
|
||||
```
|
||||
|
||||
Right? Right, but it's not all of it. We all know, that not only regular
|
||||
files might be opened via paths, but also such things as FIFOs. And
|
||||
plain `open()` with the flags we want it to have may just hang. So we need
|
||||
to change that code to look like this:
|
||||
However, this is insufficient. Regular files are not the only things opened via paths; FIFOs are another example. A standard `open()` on a FIFO with certain flags might hang indefinitely. To prevent this, the code must be modified:
|
||||
|
||||
```C
|
||||
```c
|
||||
int fd, tfd = -1;
|
||||
|
||||
int fd, tfd = -1;
|
||||
if (S_ISFIFO(f->mode))
|
||||
tfd = open(f->path, O_RDWR);
|
||||
|
||||
if (S_ISFIFO(f->mode))
|
||||
tfd = open(f->path, O_RDWR);
|
||||
|
||||
fd = open(f->path, f->mode);
|
||||
|
||||
if (tfd >= 0)
|
||||
close(tfd);
|
||||
fd = open(f->path, f->mode);
|
||||
|
||||
if (tfd >= 0)
|
||||
close(tfd);
|
||||
```
|
||||
|
||||
The tfd keeps FIFO read-write opened while we open it with any flags
|
||||
we want. Then we close it.
|
||||
Using `tfd` to keep the FIFO open for reading and writing ensures the subsequent `open()` succeeds regardless of the flags.
|
||||
|
||||
Now this seems to be OK, but it's actually not. In Linux, file can be
|
||||
unlinked while being opened (these [invisible files](invisible-files.md) are treated carefully
|
||||
on dump). In that case what was formerly pointed by
|
||||
path may be kept in some temporary location. We have to create a
|
||||
temporary name for it, and unlink it afterwards. So, we need to extend the
|
||||
info about a file:
|
||||
|
||||
```C
|
||||
|
||||
struct file {
|
||||
char *path;
|
||||
unsigned mode;
|
||||
char *temp_path;
|
||||
} *f;
|
||||
Even this revised approach has issues. In Linux, a file can be unlinked while it is still open (these [invisible files](invisible-files.md) require careful handling during a dump). If a file is unlinked, the path it once occupied may no longer exist or may point to something else. We must create a temporary name for it and unlink it after opening. Thus, we extend our file information and the opening logic:
|
||||
|
||||
```c
|
||||
struct file {
|
||||
char *path;
|
||||
unsigned mode;
|
||||
char *temp_path;
|
||||
} *f;
|
||||
```
|
||||
|
||||
and the opening code to take care of that temporary location
|
||||
```c
|
||||
int fd, tfd = -1;
|
||||
|
||||
```C
|
||||
if (f->temp_path)
|
||||
link(f->temp_path, f->path);
|
||||
|
||||
int fd, tfd = -1;
|
||||
if (S_ISFIFO(f->mode))
|
||||
tfd = open(f->path, O_RDWR);
|
||||
|
||||
if (f->temp_path)
|
||||
link(f->temp_path, f->path);
|
||||
fd = open(f->path, f->mode);
|
||||
|
||||
if (S_ISFIFO(f->mode))
|
||||
tfd = open(f->path, O_RDWR);
|
||||
|
||||
fd = open(f->path, f->mode);
|
||||
|
||||
if (tfd >= 0)
|
||||
close(tfd);
|
||||
|
||||
if (f->temp_path)
|
||||
unlink(f->path);
|
||||
if (tfd >= 0)
|
||||
close(tfd);
|
||||
|
||||
if (f->temp_path)
|
||||
unlink(f->path);
|
||||
```
|
||||
|
||||
And we haven't seen all the code we need to manage what is pointed by
|
||||
the `temp_path`, but let's proceed.
|
||||
Directories can also be open while removed. Since `link()` and `unlink()` do not work for directories, we must adjust the logic:
|
||||
|
||||
We have forgotten, that opened and <s>unlinked</s> removed can also be a
|
||||
directory. For directories, link and unlink do not work, and we have to
|
||||
append the code to at least try to make things work OK:
|
||||
```c
|
||||
int fd, tfd = -1;
|
||||
|
||||
```C
|
||||
if (f->temp_path) {
|
||||
if (S_ISDIR(f->mode))
|
||||
mkdir(f->path);
|
||||
else
|
||||
link(f->temp_path, f->path);
|
||||
}
|
||||
|
||||
int fd, tfd = -1;
|
||||
if (S_ISFIFO(f->mode))
|
||||
tfd = open(f->path, O_RDWR);
|
||||
|
||||
if (f->temp_path) {
|
||||
if (S_ISDIR(f->mode))
|
||||
mkdir(f->path);
|
||||
else
|
||||
link(f->temp_path, f->path);
|
||||
}
|
||||
fd = open(f->path, f->mode);
|
||||
|
||||
if (S_ISFIFO(f->mode))
|
||||
tfd = open(f->path, O_RDWR);
|
||||
|
||||
fd = open(f->path, f->mode);
|
||||
|
||||
if (tfd >= 0)
|
||||
close(tfd);
|
||||
|
||||
if (f->temp_path) {
|
||||
if (S_ISDIR(f->mode))
|
||||
rmdir(f->mode);
|
||||
else
|
||||
unlink(f->path);
|
||||
}
|
||||
if (tfd >= 0)
|
||||
close(tfd);
|
||||
|
||||
if (f->temp_path) {
|
||||
if (S_ISDIR(f->mode))
|
||||
rmdir(f->path);
|
||||
else
|
||||
unlink(f->path);
|
||||
}
|
||||
```
|
||||
|
||||
Done. Oh wait, we also should take care of hard links! If a file has any,
|
||||
and both were opened and removed, we cannot just go
|
||||
ahead and kill the `temp_path` after opening, as
|
||||
it can be waiting for some other
|
||||
`struct file` to open one. A little bit more information should be added
|
||||
to the `struct file`.
|
||||
Hard links introduce further complexity. If a file has multiple hard links that were all opened and then removed, we cannot simply delete the `temp_path` after opening it, as other `struct file` instances might still need it.
|
||||
|
||||
```C
|
||||
|
||||
struct temp_file {
|
||||
char *path;
|
||||
unsigned users;
|
||||
};
|
||||
|
||||
struct file {
|
||||
char *path;
|
||||
unsigned mode;
|
||||
struct temp_file *temp;
|
||||
} *f;
|
||||
```c
|
||||
struct temp_file {
|
||||
char *path;
|
||||
unsigned users;
|
||||
};
|
||||
|
||||
struct file {
|
||||
char *path;
|
||||
unsigned mode;
|
||||
struct temp_file *temp;
|
||||
} *f;
|
||||
```
|
||||
|
||||
and to the code that opens one now looks like this:
|
||||
The opening logic then becomes:
|
||||
|
||||
```C
|
||||
```c
|
||||
int fd, tfd = -1;
|
||||
|
||||
int fd, tfd = -1;
|
||||
if (f->temp) {
|
||||
if (S_ISDIR(f->mode))
|
||||
mkdir(f->path);
|
||||
else
|
||||
link(f->temp->path, f->path);
|
||||
}
|
||||
|
||||
if (f->temp) {
|
||||
if (S_ISDIR(f->mode))
|
||||
mkdir(f->path);
|
||||
else
|
||||
link(f->temp->path, f->path);
|
||||
}
|
||||
if (S_ISFIFO(f->mode))
|
||||
tfd = open(f->path, O_RDWR);
|
||||
|
||||
if (S_ISFIFO(f->mode))
|
||||
tfd = open(f->path, O_RDWR);
|
||||
fd = open(f->path, f->mode);
|
||||
|
||||
fd = open(f->path, f->mode);
|
||||
|
||||
if (tfd >= 0)
|
||||
close(tfd);
|
||||
|
||||
if (f->temp) {
|
||||
if (--f->temp->users == 0) {
|
||||
if (S_ISDIR(f->mode))
|
||||
rmdir(f->mode);
|
||||
else
|
||||
unlink(f->temp->path);
|
||||
}
|
||||
}
|
||||
if (tfd >= 0)
|
||||
close(tfd);
|
||||
|
||||
if (f->temp) {
|
||||
if (--f->temp->users == 0) {
|
||||
if (S_ISDIR(f->mode))
|
||||
rmdir(f->path);
|
||||
else
|
||||
unlink(f->temp->path);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By the way, we've left behind the scenes all the code required to make
|
||||
the `temp_file` data be shared between processes that need one and to
|
||||
make the decrementing of `f->temp->users` be SMP-safe.
|
||||
Note that making `temp_file` data sharable across processes and ensuring `f->temp->users` is updated safely in an SMP environment requires additional implementation details. We also do not currently handle the rare case where a file/directory is removed and replaced by another object of the same name.
|
||||
|
||||
Also note, that we don't handle the case when the file/directory is
|
||||
removed and some other file/directory is created under the same name.
|
||||
It's a rare case.
|
||||
|
||||
Now, is that all? No, sorry. A couple of things left. First, Linux has
|
||||
a thing called mount namespace. And two files with the same path may
|
||||
have been opened in different mount namespaces. So we also need the
|
||||
info about what mount point the file belongs to like this:
|
||||
|
||||
```C
|
||||
|
||||
struct file {
|
||||
char *path;
|
||||
unsigned mode;
|
||||
struct temp_file *temp;
|
||||
unsigned mnt_id;
|
||||
} *f;
|
||||
Mount namespaces add another layer of complexity. Two files with the same path may reside in different mount namespaces. We must track the mount point ID:
|
||||
|
||||
```c
|
||||
struct file {
|
||||
char *path;
|
||||
unsigned mode;
|
||||
struct temp_file *temp;
|
||||
unsigned mnt_id;
|
||||
} *f;
|
||||
```
|
||||
|
||||
and the code to open file would now look like
|
||||
The opening logic then uses `open_ns_root()` to access the correct mount namespace:
|
||||
|
||||
```C
|
||||
```c
|
||||
int fd, tfd = -1, ns_fd;
|
||||
char *rel_path = f->path + 1;
|
||||
|
||||
int fd, tfd = -1, ns_fd;
|
||||
char *rel_path = f->path + 1;
|
||||
ns_fd = open_ns_root(f->mnt_id);
|
||||
|
||||
ns_fd = open_ns_root(f->mnt_id);
|
||||
if (f->temp) {
|
||||
if (S_ISDIR(f->mode))
|
||||
mkdirat(ns_fd, rel_path);
|
||||
else
|
||||
linkat(ns_fd, f->temp->path, ns_fd, rel_path);
|
||||
}
|
||||
|
||||
if (f->temp) {
|
||||
if (S_ISDIR(f->mode))
|
||||
mkdirat(ns_fd, rel_path);
|
||||
else
|
||||
linkat(ns_fd, f->temp->path, ns_fd, rel_path);
|
||||
}
|
||||
if (S_ISFIFO(f->mode))
|
||||
tfd = openat(ns_fd, rel_path, O_RDWR);
|
||||
|
||||
if (S_ISFIFO(f->mode))
|
||||
tfd = openat(ns_fd, rel_path, O_RDWR);
|
||||
fd = openat(ns_fd, rel_path, f->mode);
|
||||
|
||||
fd = openat(ns_fd, rel_path, f->mode);
|
||||
if (tfd >= 0)
|
||||
close(tfd);
|
||||
|
||||
if (tfd >= 0)
|
||||
close(tfd);
|
||||
|
||||
if (f->temp_path) {
|
||||
if (--f->temp->users == 0) {
|
||||
if (S_ISDIR(f->mode))
|
||||
unlinkat(ns_fd, f->mode, AT_REMOVEDIR);
|
||||
else
|
||||
unlinkat(ns_fd, f->temp->path);
|
||||
}
|
||||
}
|
||||
|
||||
close(ns_fd);
|
||||
if (f->temp) {
|
||||
if (--f->temp->users == 0) {
|
||||
if (S_ISDIR(f->mode))
|
||||
unlinkat(ns_fd, rel_path, AT_REMOVEDIR);
|
||||
else
|
||||
unlinkat(ns_fd, f->temp->path, 0);
|
||||
}
|
||||
}
|
||||
|
||||
close(ns_fd);
|
||||
```
|
||||
|
||||
Let's not dive into the details of how the `open_ns_root` looks like.
|
||||
Just know, that it opens a file descriptor, that refers to the root
|
||||
of the mount namespace that contains a mount point with the id `mnt_id`
|
||||
(they cannot be shared, and that's great).
|
||||
|
||||
Pretty complex already, isn't it? Just a couple of final touches left.
|
||||
First, opened files typically have a position. Flags we get need to be
|
||||
sanitated not to container those that only make sense during open,
|
||||
like `O_TRUNC` or `O_CREAT`. And file may have a thing called `fown` managed
|
||||
by the `F_SETSIG` and `F_SETOWN` fcntls. All this results in
|
||||
|
||||
```C
|
||||
|
||||
struct file {
|
||||
char *path;
|
||||
unsigned mode;
|
||||
struct temp_file *temp;
|
||||
unsigned mnt_id;
|
||||
unsigned long pos;
|
||||
struct fown fown;
|
||||
} *f;
|
||||
Finally, open files have attributes like current position and ownership (`fown`) that must be restored. Flags like `O_TRUNC` and `O_CREAT` must also be sanitized.
|
||||
|
||||
```c
|
||||
struct file {
|
||||
char *path;
|
||||
unsigned mode;
|
||||
struct temp_file *temp;
|
||||
unsigned mnt_id;
|
||||
unsigned long pos;
|
||||
struct fown fown;
|
||||
} *f;
|
||||
```
|
||||
|
||||
and
|
||||
```c
|
||||
int fd, tfd = -1, ns_fd, open_flags;
|
||||
char *rel_path = f->path + 1;
|
||||
|
||||
```C
|
||||
ns_fd = open_ns_root(f->mnt_id);
|
||||
|
||||
int fd, tfd = -1, ns_fd, open_flags;
|
||||
char *rel_path = f->path + 1;
|
||||
if (f->temp) {
|
||||
if (S_ISDIR(f->mode))
|
||||
mkdirat(ns_fd, rel_path);
|
||||
else
|
||||
linkat(ns_fd, f->temp->path, ns_fd, rel_path);
|
||||
}
|
||||
|
||||
ns_fd = open_ns_root(f->mnt_id);
|
||||
if (S_ISFIFO(f->mode))
|
||||
tfd = openat(ns_fd, rel_path, O_RDWR);
|
||||
|
||||
if (f->temp) {
|
||||
if (S_ISDIR(f->mode))
|
||||
mkdirat(ns_fd, rel_path);
|
||||
else
|
||||
linkat(ns_fd, f->temp->path, ns_fd, rel_path);
|
||||
}
|
||||
open_flags = sanitize_open_mode(f->mode);
|
||||
fd = openat(ns_fd, rel_path, open_flags);
|
||||
|
||||
if (S_ISFIFO(f->mode))
|
||||
tfd = openat(ns_fd, rel_path, O_RDWR);
|
||||
if (tfd >= 0)
|
||||
close(tfd);
|
||||
|
||||
open_flags = sanitize_open_mode(f->mode);
|
||||
fd = openat(ns_fd, rel_path, open_flags);
|
||||
if (f->temp) {
|
||||
if (--f->temp->users == 0) {
|
||||
if (S_ISDIR(f->mode))
|
||||
unlinkat(ns_fd, rel_path, AT_REMOVEDIR);
|
||||
else
|
||||
unlinkat(ns_fd, f->temp->path, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (tfd >= 0)
|
||||
close(tfd);
|
||||
|
||||
if (f->temp_path) {
|
||||
if (--f->temp->users == 0) {
|
||||
if (S_ISDIR(f->mode))
|
||||
unlinkat(ns_fd, f->mode, AT_REMOVEDIR);
|
||||
else
|
||||
unlinkat(ns_fd, f->temp->path);
|
||||
}
|
||||
}
|
||||
|
||||
close(ns_fd);
|
||||
|
||||
fcntl(fd, F_SETSIG, f->fown->sig);
|
||||
fcntl(fd, F_SETOWN, &f->fown->owner);
|
||||
lseek(fd, SEEK_SET, f->pos);
|
||||
close(ns_fd);
|
||||
|
||||
fcntl(fd, F_SETSIG, f->fown.sig);
|
||||
fcntl(fd, F_SETOWN, &f->fown.owner);
|
||||
lseek(fd, f->pos, SEEK_SET);
|
||||
```
|
||||
|
||||
And don't ask for details of the `f->fown` thing. It's tricky, but just
|
||||
follows the ABI and therefore boring.
|
||||
|
||||
OK, we've finished with the top of the iceberg — opening a file. Why
|
||||
top? Becase when opened file should be planted into a process' file
|
||||
descriptors table under desired number. You might think that it should be
|
||||
as simple as:
|
||||
```C
|
||||
|
||||
dup2(fd, desired_fd);
|
||||
|
||||
```
|
||||
|
||||
but it's not. Here's [how to assign needed file descriptor to a file](how-to-assign-needed-file-descriptor-to-a-file.md).
|
||||
|
||||
|
||||
This covers the basics of opening the file. Once opened, it must be assigned to the correct file descriptor number in the process's FDT. While `dup2(fd, desired_fd)` seems simple, the reality is more involved, as explained in [How to assign a needed file descriptor to a file](how-to-assign-needed-file-descriptor-to-a-file.md).
|
||||
|
|
|
|||
|
|
@ -1,172 +1,146 @@
|
|||
# How to assign needed file descriptor to a file
|
||||
# How to Assign a Needed File Descriptor to a File
|
||||
|
||||
Let's imagine we have [opened a file](how-hard-is-it-to-open-a-file.md) and want it to have some exact descriptor number, not the one kernel gave to us.
|
||||
Suppose we have [opened a file](how-hard-is-it-to-open-a-file.md) and want it to have a specific descriptor number rather than the one assigned by the kernel.
|
||||
|
||||
The information we have is
|
||||
Given:
|
||||
```c
|
||||
struct fd {
|
||||
struct file *file;
|
||||
int tgt_fd;
|
||||
} *fd;
|
||||
```
|
||||
|
||||
struct fd {
|
||||
struct file *file;
|
||||
int tgt_fd;
|
||||
} *fd;
|
||||
|
||||
And after calling:
|
||||
```c
|
||||
int fd_opened;
|
||||
fd_opened = open_a_file(fd->file);
|
||||
```
|
||||
|
||||
and we've just done the
|
||||
We can use the `dup2()` system call to assign the opened file to the target descriptor number:
|
||||
|
||||
```c
|
||||
int fd_opened;
|
||||
fd_opened = open_a_file(fd->file);
|
||||
dup2(fd_opened, fd->tgt_fd);
|
||||
close(fd_opened);
|
||||
```
|
||||
|
||||
int fd;
|
||||
|
||||
fd = open_a_file(fd->file);
|
||||
In some cases, a single file might be opened multiple times within a task (e.g., a shell where `/dev/tty` might be mapped to descriptors 0, 1, and 2). We can handle this by extending the `struct fd`:
|
||||
|
||||
```c
|
||||
struct fd {
|
||||
struct file *file;
|
||||
int n_fds;
|
||||
int *tgt_fds;
|
||||
} *fd;
|
||||
```
|
||||
|
||||
what's next? In Linux there's a cool system call `dup2()` which assigns to a file, referenced by one file descriptor, some other one, given by the caller.
|
||||
So the code would look like this:
|
||||
The updated restoration logic:
|
||||
|
||||
```c
|
||||
int fd_opened, i;
|
||||
fd_opened = open_a_file(fd->file);
|
||||
for (i = 0; i < fd->n_fds; i++)
|
||||
dup2(fd_opened, fd->tgt_fds[i]);
|
||||
close(fd_opened);
|
||||
```
|
||||
|
||||
int fd;
|
||||
Files shared between different tasks are also common (e.g., after a `fork()`). If a file is shared between two processes where neither is an ancestor of the other, CRIU handles this by [sending file descriptors](http://linux.die.net/man/3/cmsg) between processes.
|
||||
|
||||
fd = open_a_file(fd->file);
|
||||
dup2(fd, fd->tgt_fd);
|
||||
close(fd);
|
||||
This adds complexity to our structures:
|
||||
|
||||
```c
|
||||
struct pid_fd {
|
||||
int pid;
|
||||
int fd;
|
||||
};
|
||||
|
||||
struct fd {
|
||||
struct file *file;
|
||||
int n_fds;
|
||||
struct pid_fd *tgt_fds;
|
||||
} *fd;
|
||||
```
|
||||
|
||||
Now let's remember, that a file can be opened multiple times in one task, this happens when you e.g. start a shell. One of the `/dev/tty` or alike files will sit under 0, 1 and 2 descriptors. Not a big deal, we just expand the `struct fd`
|
||||
The logic now involves two parts: one process that opens the file and sends it to others, and other processes that receive it.
|
||||
|
||||
```c
|
||||
int fd_opened, i, pid = getpid(), sk;
|
||||
sk = create_socket();
|
||||
|
||||
if (pid == file_opener(fd)) {
|
||||
fd_opened = open_a_file(fd->file);
|
||||
|
||||
for (i = 0; i < fd->n_fds; i++) {
|
||||
if (fd->tgt_fds[i].pid == pid)
|
||||
dup2(fd_opened, fd->tgt_fds[i].fd);
|
||||
else
|
||||
send_fd(fd_opened, fd->tgt_fds[i], sk);
|
||||
}
|
||||
close(fd_opened);
|
||||
} else {
|
||||
for (i = 0; i < fd->n_fds; i++) {
|
||||
if (fd->tgt_fds[i].pid != pid)
|
||||
continue;
|
||||
|
||||
fd_opened = recv_fd(sk);
|
||||
dup2(fd_opened, fd->tgt_fds[i].fd);
|
||||
close(fd_opened);
|
||||
}
|
||||
}
|
||||
close(sk);
|
||||
```
|
||||
|
||||
struct fd {
|
||||
struct file *file;
|
||||
int n_fds;
|
||||
int *tgt_fds;
|
||||
} *fd;
|
||||
All `tgt_fds` belonging to a task are opened by another task and sent to the owner in the order they appear in the array. The receiver processes them in the same order, ensuring files are placed into the correct descriptors.
|
||||
|
||||
However, the logic above has some flaws:
|
||||
|
||||
1. **Transport Coordination**: `send_fd` and `recv_fd` cannot reliably use a single shared socket for all tasks. A descriptor intended for a specific PID must reach that task. CRIU creates dedicated sockets for receiving descriptors, often named uniquely like `criu-fd-transport-%pid-%fd`.
|
||||
2. **Descriptor Overwriting**: When the opener calls `dup2()`, it might overwrite the transport socket descriptor (`sk`). To avoid this, `sk` can be moved to a free descriptor using `dup()`.
|
||||
|
||||
Revised logic:
|
||||
|
||||
```c
|
||||
int fd_opened, i, pid = getpid(), sk;
|
||||
|
||||
if (pid == file_opener(fd)) {
|
||||
sk = create_socket();
|
||||
fd_opened = open_a_file(fd->file);
|
||||
|
||||
for (i = 0; i < fd->n_fds; i++) {
|
||||
if (fd->tgt_fds[i].pid == pid) {
|
||||
if (sk == fd->tgt_fds[i].fd)
|
||||
sk = dup(sk);
|
||||
dup2(fd_opened, fd->tgt_fds[i].fd);
|
||||
} else {
|
||||
send_fd(fd_opened, fd->tgt_fds[i], sk);
|
||||
}
|
||||
}
|
||||
close(fd_opened);
|
||||
close(sk);
|
||||
} else {
|
||||
for (i = 0; i < fd->n_fds; i++) {
|
||||
if (fd->tgt_fds[i].pid != pid)
|
||||
continue;
|
||||
|
||||
sk = create_socket();
|
||||
dup2(sk, fd->tgt_fds[i].fd);
|
||||
}
|
||||
|
||||
for (i = 0; i < fd->n_fds; i++) {
|
||||
if (fd->tgt_fds[i].pid != pid)
|
||||
continue;
|
||||
|
||||
fd_opened = recv_fd(fd->tgt_fds[i].fd);
|
||||
dup2(fd_opened, fd->tgt_fds[i].fd);
|
||||
close(fd_opened);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
and the code itself:
|
||||
|
||||
```
|
||||
|
||||
int fd, i;
|
||||
|
||||
fd = open_a_file(fd->file);
|
||||
for (i = 0; i < fd->n_fds; i++)
|
||||
dup2(fd, fd->tgt_fds[i]);
|
||||
close(fd);
|
||||
|
||||
```
|
||||
|
||||
Next thing to handle -- file shared between tasks. This is also very typical, once you called `open()` and then `fork()` the file becomes such. But what if a file is shared between two processes, none of which is the ancestor of another? There are two ways of doing this, CRIU uses the most straightforward one -- it [sends file descriptors](http://linux.die.net/man/3/cmsg) between processes.
|
||||
|
||||
This requires some complication in the structures we use
|
||||
|
||||
```
|
||||
|
||||
struct pid_fd {
|
||||
int pid;
|
||||
int fd;
|
||||
};
|
||||
|
||||
struct fd {
|
||||
struct file *file;
|
||||
int n_fds;
|
||||
struct pid_fd *tgt_fds;
|
||||
} *fd;
|
||||
|
||||
```
|
||||
|
||||
and in the code which now consists of two parts -- one that opens file and sends it to others, and the other one that just receives them. We will come back to this again below, let's enjoy the code we have at the moment:
|
||||
|
||||
```
|
||||
|
||||
int fd, i, pid = getpid(), sk;
|
||||
|
||||
sk = create_socket();
|
||||
|
||||
if (pid == file_opener(fd)) {
|
||||
fd = open_a_file(fd->file);
|
||||
|
||||
for (i = 0; i < fd->n_fds; i++) {
|
||||
if (fd->tgt_fds[i].pid == pid)
|
||||
dup2(fd, fd->tgt_fds[i].fd);
|
||||
else
|
||||
send_fd(fd, fd->tgt_fds[i], sk);
|
||||
}
|
||||
|
||||
close(fd);
|
||||
} else {
|
||||
for (i = 0; i < fd->n_fds; i++) {
|
||||
if (fd->tgt_fds[i].pid != pid)
|
||||
continue;
|
||||
|
||||
fd = recv_fd(sk);
|
||||
dup2(fd, fd->tgt_fds[i].fd);
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
close(sk);
|
||||
|
||||
```
|
||||
|
||||
Please, note, that all `tgt_fds` belonging to some task are opened by different one and then are sent to the real owner in the order they are met in the array. So does the receiver -- it receives the fds in the same order, so this algorithm puts files into proper descriptors.
|
||||
|
||||
There are several bugs in the above code snippet however :(
|
||||
|
||||
First, the `send_fd` and `recv_fd` routines cannot works using one socket for all tasks -- a descriptor sent to task `pid` should reach *this* task, not some arbitrary one that kernel woke up earlier on data arrival. That said, we have to create one socket per at least task to receive descriptors. But files can be shared in a tricky manner, so that task A may have one file shared with task B and some other file shared with task C. If the "who opens a file" voting selects B and C for respective files, they will have to send descriptors to A with proper coordination with each other. This coordination can be simplified if we create sockets not just per-pid, but per-(pid, fd). And where to keep all this bunch of sockets? The easiest answer -- in the places where the files they will receive should sit :) And we then use the `sendto()` syscall to send the descriptor via unconnected socket by address. These transport sockets may have some unique name like `"criu-fd-transport-%pid-%fd"`.
|
||||
|
||||
Second, when the file opener calls `dup2()` it may overwrite the `sk` descriptor. This is sad, but OK, since we can move the sk into any free place using plain `dup()` system call.
|
||||
|
||||
```
|
||||
|
||||
int fd, i, pid = getpid(), sk;
|
||||
|
||||
if (pid == file_opener(fd)) {
|
||||
sk = create_socket();
|
||||
fd = open_a_file(fd->file);
|
||||
|
||||
for (i = 0; i < fd->n_fds; i++) {
|
||||
if (fd->tgt_fds[i].pid == pid) {
|
||||
if (sk == fd->tgt_fds[i].fd)
|
||||
sk = dup(sk);
|
||||
dup2(fd, fd->tgt_fds[i].fd);
|
||||
} else
|
||||
send_fd(fd, fd->tgt_fds[i], sk);
|
||||
}
|
||||
|
||||
close(fd);
|
||||
close(sk);
|
||||
} else {
|
||||
for (i = 0; i < fd->n_fds; i++) {
|
||||
if (fd->tgt_fds[i].pid != pid)
|
||||
continue;
|
||||
|
||||
sk = create_socket();
|
||||
dup2(sk, fd->tgt_fds[i].fd);
|
||||
}
|
||||
|
||||
for (i = 0; i < fd->n_fds; i++) {
|
||||
if (fd->tgt_fds[i].pid != pid)
|
||||
continue;
|
||||
|
||||
fd = recv_fd(fd->tgt_fds[i].fd);
|
||||
dup2(fd, fd->tgt_fds[i].fd);
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
This is almost all. Only a few notes left.
|
||||
|
||||
First, the above code is still buggy -- the file opener should make sure that everybody has their transport sockets ready. This requires some synchronization around `create_socket()` and `send_fd`.
|
||||
|
||||
Second, the "who will open a file" voting. We should make sure, that the synchronization mentioned above doesn't AB-BA deadlock, so when deciding which task to open a file we always chose the one with the smallest pid. And the file sending wave goes upwards the process tree :)
|
||||
|
||||
Third, the `open_a_file()` is not just [this](how-hard-is-it-to-open-a-file.md). Opened can be pipe, socket, signalfd, inotify and many other fancy stuff none of which uses the open-by-path engine.
|
||||
|
||||
And the last, but not least, files can depend on each other. E.g. an eventpoll file may have some other file descriptor monitored, and if we call the `open_a_file()` on eventpoll fd before we open the fd being monitored, we fail. This also affects the code that forms an array of `struct fd`-s
|
||||
|
||||
Additional Considerations:
|
||||
|
||||
- **Synchronization**: The opener must ensure that all transport sockets are ready before sending descriptors.
|
||||
- **Deadlock Prevention**: To avoid deadlocks during synchronization, the task with the smallest PID is chosen to open a shared file, and descriptors are sent "upwards" through the process tree.
|
||||
- **Complexity of `open_a_file()`**: Opening a file is not always about a path; it could involve pipes, sockets, or other specialized objects.
|
||||
- **Dependencies**: Files can depend on one another. For example, an `eventpoll` descriptor might monitor other descriptors. If the `eventpoll` is opened before its monitored descriptors, it will fail.
|
||||
|
|
|
|||
|
|
@ -1,49 +1,43 @@
|
|||
# How to open a file without open system call
|
||||
# How to Open a File Without an `open` System Call
|
||||
|
||||
Sometimes CRIU meets an inode object (check [this](dumping-files.md) article for details of what inode is) without a name. This article describes when this happens and what CRIU does in this case.
|
||||
CRIU sometimes encounters an inode object without a corresponding name (refer to [this article](dumping-files.md) for details on inodes). This article explains when this occurs and how CRIU handles it.
|
||||
|
||||
## When this happens
|
||||
## When This Occurs
|
||||
|
||||
There are two nasty API calls in the Linux kernel -- the `inotify_init` and the `fanotify_init`. Both take a file path as an argument and screw one up. What they do is find an inode object using this path, then put an events generator on it and then forget the path completely. The result of both calls is a file-descriptor pointing to the created event generator object.
|
||||
Two Linux kernel APIs, `inotify_init` and `fanotify_init`, can cause this. Both take a file path as an argument but then internalize it. They identify the inode object associated with the path, set up an event generator on it, and then discard the path. The resulting file descriptor points to the event generator, not the original path.
|
||||
|
||||
When CRIU meets the inotify/fanotify (called fsnotify later for convenience) FD it has to find out the file on which the generator sits. But, since the inode's path is lost, this cannot be done in general case.
|
||||
When CRIU encounters an `inotify` or `fanotify` (collectively referred to as `fsnotify`) file descriptor, it must identify the file the generator is monitoring. Since the original path is lost, this is generally difficult.
|
||||
|
||||
## Chances to get the name back
|
||||
## Retrieving the Path
|
||||
|
||||
Chances to get the name back exist. To understand when let's dive a little bit more in how Linux manages dentries and inodes.
|
||||
In some cases, the path can be recovered by examining how Linux manages dentries and inodes.
|
||||
|
||||
### Inodes and dentries
|
||||
### Inodes and Dentries
|
||||
|
||||
So, every file on disk is represented by an inode object. Inode has an ID (inode number), access rights, owner, link count and some more data. Names are only stored in special files called *directories* -- in directories there's a set of name-to-inode mappings. When accessing a file by its name Linux kernel sequentially reads from disk these mapping tables and for every name found in it creates a dentry object in memory. It's important to know, that dentry is created not only for existing files, but also for non-existing to speed up the ENOENT report for second file lookup. IOW dentries form a cache, which contains records for both present and absent objects on disk.
|
||||
Every file on disk is represented by an inode object, which contains an ID (inode number), access rights, owner, link count, and other metadata. Names are stored only in directories as name-to-inode mappings. When a file is accessed by name, the kernel reads these mappings and creates a "dentry" object in memory. Dentries act as a cache for both existing and non-existing files to speed up lookups.
|
||||
|
||||
Since the tree of dentries can gorw infinitely Linux sometimes shrinks one, by freeing the unused dentries. The dentry is unused if no other object references one, and a dentry can be referenced by child dentries and by files (as described in [another](dumping-files.md) article).
|
||||
To manage memory, the kernel may shrink the dentry cache by freeing unused dentries. A dentry is considered unused if it is not referenced by child dentries or open files.
|
||||
|
||||
Having said that, at the time the fsnotify creating happens we have a full dentry chain and the inode sitting in memory. Then the events generator is put on the inode and that's it. Neither inode nor fsnotify object references the dentry, so eventually the whole dentry chain can be shrunk from memory.
|
||||
When an `fsnotify` object is created, the kernel has the full dentry chain and the inode in memory. However, since neither the inode nor the `fsnotify` object typically maintains a reference to the dentry, the dentry chain can eventually be freed.
|
||||
|
||||
|
||||
So, returning to the "can I get the name back" question. The answer is -- if the dentry cache is still alive -- yes, you can. But CRIU cannot rely on this, since it should also support situations when the dentry cache is not there.
|
||||
If the dentry cache is still alive, the path can be retrieved. However, CRIU cannot rely on this, as it must handle situations where the dentry cache has been cleared.
|
||||
|
||||
### Tmpfs
|
||||
|
||||
One filesystem, however, behaves friendly to this problem. The tempfs one pins the dentries in memory, since it has no other media on which to store the information about files on it. So for tmpfs the name is always at hands.
|
||||
The `tmpfs` filesystem is an exception. Since it exists only in memory and has no other storage medium, it "pins" dentries in memory. For `tmpfs`, the filename is always available.
|
||||
|
||||
## Opening a File via `open_by_handle_at`
|
||||
|
||||
Linux provides the `open_by_handle_at` system call, originally introduced for userspace NFS servers. This call allows opening an inode using an opaque "handle." This handle is a sequence of bytes that the filesystem uses to locate and open an inode. The kernel can generate a handle for an existing inode object. Since an `fsnotify` object references an inode, CRIU can request its handle. CRIU has even patched the kernel to expose this handle in `/proc/$pid/fdinfo/$fd` for `fsnotify` descriptors.
|
||||
|
||||
## Opening a file without open()
|
||||
|
||||
Linux provides a way to do this. The way is called `open_by_handle_at` system call. Introduced to make the user-space NFS server work, this call allows to open an inode using a blob called *handle*. The handle is (almost) meaning-less sequence of bytes by which filesystem promises to find the inode and open one. And the handle itself can be generated by the kernel using the inode object. Since fsnotify object references inode we can try to ask the kernel to generate the respective inode's handle. And we did that and patched the kernel to show this handle in the /proc/$pid/fdindo/$fd file for the fsnotify.
|
||||
|
||||
So when dumping the fsnotify we read the handle out of proc and save one in the images, and on restore time we call the `open_by_handle_at` with the handle value and get the inode back. Then we need to ask the kernel to put the fsnotify on this inode. To do this CRIU calls fsnotify init call on the /proc/self/fd/$fd path. While resolving the path kernel finds the inode opened previously and restores the handle in the proper place. Thus we fool the kernel and put fsnotify on an inode without even knowing its path.
|
||||
During a dump, CRIU reads the handle from `proc` and saves it. During restoration, it calls `open_by_handle_at` to retrieve the inode. CRIU then recreates the `fsnotify` object on this inode by calling the initialization function on the `/proc/self/fd/$fd` path. The kernel resolves this path to the previously opened inode, effectively restoring the `fsnotify` state without needing the original path.
|
||||
|
||||
## [Irmap](irmap.md)
|
||||
|
||||
But the problems are still not over. Not all filesystems provide handles. Hopefully yet, but still -- not always we can get a handle out of an inode and an inode out of a handle.
|
||||
This is very nasty situation, since Linux kernel provides no other APIs for getting the inode, only open by path and open by handle. With both ways closed we have to make a detour.
|
||||
Not all filesystems support file handles. If handles are unavailable, CRIU must use a different approach.
|
||||
|
||||
CRIU uses the empiric knowledge where fsnotify-s are typically put by programs (config files and alike) and does filesystem tree scan to find out the name by the inode number. The engine is caller *irmap* which stands for Inode Reverse MAP. The irmap cache recursively scans the tree starting from "known" locations and remembers all the name-inode pairs it meets. If we later try to irmap some inode which was met during the first scan, no additional FS access would occur, irmap would just report the name back.
|
||||
|
||||
### Caching the irmap cache
|
||||
|
||||
Since this FS scan can be quite long, this is recommended to be done while tasks are not frozen. So the irmap cache fill is also started on the pre-dump operation, when tasks are not frozen. After the scan the cache is stored in the working dir under the irmap-cache.img name. When CRIU's next pre-dump or final dump is performed, the irmap cache is read back and when required the cached entries are re-validated individually, w/o the full FS re-scan.
|
||||
CRIU leverages empirical knowledge about where programs typically place `fsnotify` watches (e.g., configuration files) and performs a filesystem tree scan to match inode numbers to paths. This engine is called **irmap** (Inode Reverse MAP). It recursively scans "known" locations and caches name-inode pairs. If a required inode was encountered during the scan, `irmap` reports the name immediately.
|
||||
|
||||
### Caching the Irmap
|
||||
|
||||
Because filesystem scans can be time-consuming, CRIU starts filling the `irmap` cache during the pre-dump operation while tasks are still running. The cache is saved as `irmap-cache.img`. During subsequent pre-dumps or the final dump, CRIU reads this cache and re-validates individual entries as needed, avoiding a full rescane.
|
||||
|
|
|
|||
|
|
@ -1,61 +1,52 @@
|
|||
# Invisible files
|
||||
# Invisible Files
|
||||
|
||||
In Linux files may be inaccessible for open() but still be present in the system. This can happen in several ways and this page is about how this can happen and what CRIU does about it.
|
||||
In Linux, files may be inaccessible via `open()` yet still exist within the system. This page explains how this occurs and how CRIU handles these situations.
|
||||
|
||||
## How a file can lose its path
|
||||
## How a File Can Lose Its Path
|
||||
|
||||
This is pretty simple. A process may do this series of operations
|
||||
```
|
||||
A common scenario involves a process performing the following:
|
||||
|
||||
int fd = open("/foo/bar");
|
||||
```c
|
||||
int fd = open("/foo/bar", O_RDONLY);
|
||||
unlink("/foo/bar");
|
||||
|
||||
```
|
||||
After it the /foo/bar file will have its name removed from the filesystem tree (and from the on-disk data too), but since the file is still held by the process (this structure is explained in the article about [dumping files](dumping-files.md)), the blob with data itself is still there.
|
||||
|
||||
There are two different sub-cases in this scenario. First, when the number of hard links associated with the file is zero, i.e. the /foo/bar name was the last one removed or the only it ever had. Another situation is when the link count is not zero. This means that some other name (hard link) for this file exists. In the latter case it is important to note that the Linux VFS layer typically does *not* allow to directly find out what the other name is.
|
||||
After the `unlink()`, the name `/foo/bar` is removed from the filesystem, but because the process still holds an open file descriptor, the file's data remains on disk.
|
||||
|
||||
### Virtual filesystems
|
||||
There are two primary sub-cases. First, if the file has no other hard links, the data is truly "orphaned." Second, if other hard links exist, the file still has a name elsewhere. However, the Linux VFS layer generally does not provide a direct way to find those other names.
|
||||
|
||||
For virtual filesystems like proc or sysfs there is another possibility when such invisible files may appear. It's the removal of the object represented of a file on this FS. In particular, if we open some file in /proc/$pid and the respective task dies the path of the opened file would get removed, while the file itself would be still alive (though reporting ENOENT error on any attempts to read from one).
|
||||
### Virtual Filesystems
|
||||
|
||||
### Name-less files
|
||||
On virtual filesystems like `proc` or `sysfs`, invisible files can appear if the underlying object is removed. For example, if a process opens a file in `/proc/$pid` and that task subsequently dies, the path is removed, but the file descriptor remains open (though subsequent reads will return `ENOENT`).
|
||||
|
||||
There is a third possibility for a file not to have a visible name but it has nothing to do with dumping opened files. This is described in [another article](how-to-open-a-file-without-open-system-call.md).
|
||||
### Name-less Files
|
||||
|
||||
### Overmounted files
|
||||
Some files are created without ever having a name. This is discussed in [another article](how-to-open-a-file-without-open-system-call.md).
|
||||
|
||||
If a task opens a file and then a new mountpoint appears on any part of its path, the original file's path may become non existing or point to different file.
|
||||
### Overmounted Files
|
||||
|
||||
CRIU doesn't work with such files yet, aborting the dump. However, there's a way to fix this.
|
||||
If a task opens a file and a new mount point is later mounted over any part of its path, the original path may become inaccessible or point to a different file. CRIU currently does not support checkpointing such files and will abort the dump.
|
||||
|
||||
Although in Linux there's no way to open a file by a name looking *under* certain mount points, there's the openat() call which may look up a file starting from arbitrary point which, in turn, can be over-mounted too.
|
||||
## How CRIU Handles Invisible Files
|
||||
|
||||
## What CRIU does about it
|
||||
### Detection and Dumping
|
||||
|
||||
### Detection and dumping
|
||||
Subject to certain [filesystem peculiarities](filesystems-pecularities.md), CRIU detects invisible files as follows:
|
||||
|
||||
First of all, CRIU should detect this situation to take place. Modulo some [filesystems pecularities](filesystems-pecularities.md), this is done like this.
|
||||
1. CRIU [retrieves the file descriptors](dumping-files.md) from the target process.
|
||||
2. For each FD, CRIU identifies the file's name by calling `readlink` on `/proc/self/fd/$fd`.
|
||||
3. CRIU then calls `fstat()` on the file descriptor.
|
||||
|
||||
First, we [get the files](dumping-files.md) from the target process via unix socket. Then for each of them get the file's name via /proc by calling `readlink` on the /proc/self/fd/$fd path. It's important to note, that we readlink *self* FD to get the file's name we can work with. Next we `fstat()` the respective self file descriptor.
|
||||
If `st_nlink` is zero, the file has been fully deleted. Since it cannot be reopened by path, CRIU must save the file's contents directly into the image as a **ghost file**. By default, CRIU limits the size of ghost files it can checkpoint to 1 MB, though this can be adjusted with the `--ghost-limit` option.
|
||||
|
||||
If the `st_nlink` field is zero, then the file is fully deleted from the system. Since no filesystems allow to recreate the name of such files, we have no other choice than to store the file itself into images. So we generate a so called *ghost file* in the image directory and copy the file contents into it. Since the content of the file is saved into images, CRIU has a limit for a maximum file size it can checkpoint. By default, this limit is set to 1Mb but it can be changed with the `--ghost-limit` option.
|
||||
If `st_nlink` is non-zero, CRIU verifies if the path retrieved from `proc` still refers to the same file. It calls `stat()` on that path and compares the `st_dev` and `st_inode` fields with those from the `fstat()` call. If they match, the file is still accessible by that name. If they do not match, the name now refers to a different file, and the dump fails (a rare situation that CRIU may support in the future).
|
||||
|
||||
But what happens if the link count is not zero. Then we should check than the name we got from proc is the one with which we can see this file. So we call `stat()` on this name and compare `st_dev` and `st_inode` fields of it with those obtained from the fstat() call earlier. If they match the file is alive and we can just dump its name. If they don't the name we got references some other file and we fail the dump. This can be handled, but this situation is quite rare so we decided to implement support for it later.
|
||||
A third possibility is that `stat()` fails with `ENOENT`, meaning the file has other names, but the one used to open it has been removed. In this case, CRIU uses the `linkat` system call to create a temporary name for the file on disk and records this name in the image. This is known as **link-remap**. Because this modifies the filesystem, the `--link-remap` option must be provided. These temporary names are removed after the restoration is complete.
|
||||
|
||||
But there's also a 3rd possibility -- the `stat()` could fail with ENOENT error, which means that the file has names, but the one we have it opened by is removed. In *this* situation we cannot just save the file name in the images, since this name is not longer alive. Neither we can dump the file as ghost, as the same file can be accessed by some other name. And, as was said, there's no way to find this other name. Fortunately, in this case filesystems allow to create a new name for a file, so CRIU calls `linkat` system call creating a temporary name for this file on the disk and saves this name in the image. This is called *link-remap*. Since this manoeuvre modifies the filesystem, CRIU requires the special option *--link-remap* to be passed to it allowing this behaviour. On restore the link-remap names are removed after files restore.
|
||||
|
||||
Please note that a file may have been opened by many removed names, and for each a link-remap name should point to the same file, so while dumping and restoring CRIU keeps track of those names to inode mappings.
|
||||
|
||||
### Chunked ghost files
|
||||
|
||||
When CRIU checkpoints an invisible (ghost) file with size larger than 12MB, it would try to reduce the size of the corresponding image by seeking for holes (e.g., in sparse files). This approach allows CRIU to save the content of the file into a set of chunks and skip over the "empty" space in a file by keeping track of the offsets.
|
||||
|
||||
However, determining the offsets in highly sparse file might encounter a significant amount of expensive system calls. In order to reduce the overhead of dumping such ghost files, CRIU supports the `--ghost-fiemap` option that uses an optimized algorithm based on the fiemap ioctl.
|
||||
|
||||
### Virtual filesystems
|
||||
|
||||
For proc CRIU uses a slightly different trick. When we see dead name in /proc we cannot link() a new name or create a ghost file. Instead, we remember the PID of the process that died, and on restore create a temporary task with the desired pid, which gets killed right after all its open()-ers are restored.
|
||||
### Chunked Ghost Files
|
||||
|
||||
When CRIU checkpoints a ghost file larger than 12 MB, it attempts to minimize the image size by identifying holes (sparse areas). This allows CRIU to save only the actual data chunks and record the offsets, skipping empty space. To optimize this process on highly sparse files, CRIU supports the `--ghost-fiemap` option, which uses the `fiemap` ioctl for better performance.
|
||||
|
||||
### Virtual Filesystems
|
||||
|
||||
For the `proc` filesystem, CRIU uses a different technique. If a name in `/proc` is dead, it cannot be linked or saved as a ghost file. Instead, CRIU records the PID of the deceased process. During restoration, CRIU creates a temporary task with that PID, which remains alive until all its openers have been restored.
|
||||
|
|
|
|||
|
|
@ -1,22 +1,19 @@
|
|||
# Irmap
|
||||
|
||||
## Problem
|
||||
## The Problem
|
||||
|
||||
How having an inode number and device of a file get its path back? This problem appears when dumping various notifies objects like inotify or fanotify. Those are put on an inode (that is looked up by a path) and after it the path string is forgotten.
|
||||
How can we retrieve a file's path given its inode number and device? This challenge arises when dumping notification objects like `inotify` or `fanotify`. These are established on an inode (initially identified by a path), after which the kernel "forgets" the original path string.
|
||||
|
||||
CRIU uses the empiric knowledge where *notify-s are typically put by programs (config files and alike) and does filesystem tree scan to find out the name by the inode number. The engine is caller *irmap* which stands for **Inode Reverse MAP**. The irmap cache recursively scans the tree starting from "known" locations and remembers all the name-inode pairs it meets. If we later try to irmap some inode which was met during the first scan, no additional FS access would occur, irmap would just report the name back.
|
||||
CRIU leverages empirical knowledge of where these notifications are typically placed (such as configuration files) and performs a filesystem tree scan to find the path associated with a specific inode number. This engine is called **irmap** (Inode Reverse MAP). The `irmap` cache recursively scans the filesystem starting from "known" locations and records name-inode pairs. If a required inode was encountered during the scan, `irmap` retrieves the path immediately without further filesystem access.
|
||||
|
||||
## Caching the irmap cache
|
||||
## Caching the Irmap
|
||||
|
||||
Since this FS scan can be quite long, this is recommended to be done while tasks are not frozen. So the irmap cache fill is also started on the pre-dump operation, when tasks are not frozen. After the scan the cache is stored in the working dir under the irmap-cache.img name. When CRIU's next pre-dump or final dump is performed, the irmap cache is read back and when required the cached entries are re-validated individually, w/o the full FS re-scan.
|
||||
Because filesystem scans can be time-consuming, CRIU performs this process while tasks are still running. The `irmap` cache filling begins during the pre-dump operation. The resulting cache is stored in the working directory as `irmap-cache.img`. During subsequent pre-dumps or the final dump, CRIU reads this cache and re-validates individual entries as needed, avoiding a full rescane.
|
||||
|
||||
## Other solutions?
|
||||
## Other Solutions?
|
||||
|
||||
We're currently unaware of any :(
|
||||
Currently, no other reliable APIs exist for this purpose.
|
||||
|
||||
## OverlayFS
|
||||
|
||||
[Irmap doesn't work on this FS](https://github.com/checkpoint-restore/criu/issues/136)
|
||||
|
||||
|
||||
|
||||
[Irmap does not currently work on OverlayFS](https://github.com/checkpoint-restore/criu/issues/136).
|
||||
|
|
|
|||
|
|
@ -1,40 +1,29 @@
|
|||
# Kcmp trees
|
||||
# Kcmp Trees
|
||||
|
||||
## Overview
|
||||
|
||||
Usually we dump not just a single process but a set of them, where every process may be sharing some resources with other processes.
|
||||
Thus we need somehow to distinguish which resources are shared and which are not.
|
||||
When checkpointing a group of processes, many of them may share resources. CRIU must distinguish between resources that are shared and those that are unique to a process.
|
||||
|
||||
For this sake [kcmp](http://man7.org/linux/man-pages/man2/kcmp.2.html) system call has been introduced to Linux kernel.
|
||||
It takes two processes and compare a resource asked, returning result similar to well known
|
||||
[strcmp](http://man7.org/linux/man-pages/man3/strcmp.3.html) call. This allows CRIU to track resources with a sorting algorithm.
|
||||
To achieve this, the [`kcmp`](http://man7.org/linux/man-pages/man2/kcmp.2.html) system call was introduced to the Linux kernel. It compares a specific resource between two processes and returns a result similar to [`strcmp`](http://man7.org/linux/man-pages/man3/strcmp.3.html). This allows CRIU to efficiently track shared resources using a sorting algorithm.
|
||||
|
||||
### API
|
||||
|
||||
CRIU gather files, filesystems, vitrual memory descriptors, signal handlers and file descriptors associated with a process
|
||||
each into Kcmp-tree. Thus at moment we are carrying five Kcmp-trees. Each declared with `DECLARE_KCMP_TREE` helper.
|
||||
For example
|
||||
CRIU organizes files, filesystems, virtual memory descriptors, signal handlers, and file descriptors into separate "Kcmp trees." Currently, CRIU maintains five such trees, each declared using the `DECLARE_KCMP_TREE` helper. For example:
|
||||
|
||||
```
|
||||
```c
|
||||
DECLARE_KCMP_TREE(vm_tree, KCMP_VM);
|
||||
```
|
||||
|
||||
Each tree internally represented as [red-black](http://en.wikipedia.org/wiki/Red%E2%80%93black_tree) tree.
|
||||
Internally, each tree is implemented as a [red-black tree](http://en.wikipedia.org/wiki/Red%E2%80%93black_tree).
|
||||
|
||||
When CRIU gathers process resources it check if a resource is already sitting inside of a tree calling
|
||||
`kid_generate_gen()` helper. If a resource is not in a tree - it pushed into a tree
|
||||
and a caller obtains new abstract ID which may be used inside CRIU images, otherwise the helper
|
||||
returns zero notifying that this kind of resource already known to CRIU and has been handled earlier.
|
||||
As CRIU gathers process resources, it uses the `kid_generate_gen()` helper to check if a resource already exists in the tree. If the resource is new, it is added to the tree, and the caller receives a new abstract ID for use in CRIU images. If the resource is already known, the helper returns zero, indicating it has already been handled.
|
||||
|
||||
This feature is quite important to eliminate duplication of entries inside CRIU dump images, because
|
||||
two processes might share a lot of resources and dumping them multiple times would cause very serious
|
||||
performance issue.
|
||||
This mechanism is critical for preventing duplicate entries in dump images, which would otherwise lead to significant performance issues.
|
||||
|
||||
### Two trees
|
||||
### Two-Tree Strategy
|
||||
|
||||
In order to minimize the number of `kcmp` calls we use two IDs for an object -- so called *gen_id* and the *ID* itself.
|
||||
To minimize the number of expensive `kcmp` calls, CRIU uses two identifiers for each object: a **gen_id** and the **ID** itself.
|
||||
|
||||
The gen_id is and ID that is created based on some visible attributes of an object. E.g. for a file it's generated out of the inode number, device and position. Having two gen_id-s different we can say that the objects differ to. E.g. file with different inodes are different. But two equal gen_id-s may refer to different files too. So to check *this* we call `kcmp`.
|
||||
|
||||
For faster lookup we store objects in two trees. First RB-tree is the sorted by gen_id-s tree. When we fail to find an element in a tree we assume that the object we check is the new one. When we *find* an element in the tree we need to go on and call `kcmp`. But since one gen_id leaf may refer to several elements *and* kernel reports equals/greater/less from `kcmp` we create the 2nd tree under the gen_id leaf -- the sorted by ID tree where the comparison function is the `kcmp`.
|
||||
The **gen_id** is generated from visible attributes of an object. For a file, it might be derived from the inode number, device, and position. If two objects have different `gen_id`s, they are guaranteed to be different. However, two identical `gen_id`s do not guarantee that the objects are the same.
|
||||
|
||||
To handle this efficiently, objects are stored in two layers of trees. The first is a red-black tree sorted by `gen_id`. If an object is not found here, it is considered new. If a match is found, CRIU must then call `kcmp` to confirm equality. Because one `gen_id` might correspond to multiple distinct objects, a second tree is maintained under each `gen_id` leaf, sorted by the results of the `kcmp` calls.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
# Kerndat
|
||||
|
||||
When doing its work CRIU needs to know some information about the kernel it runs on and the tool set that's available on the host.
|
||||
To function correctly, CRIU needs information about the kernel it is running on and the tools available on the host.
|
||||
|
||||
E.g. when performing [memory dumping and restoring](memory-dumping-and-restoring.md) CRIU needs to distinguish anonymous [shared memory](shared-memory.md) from tmpfs file mappings. This can be done by checking the mapped file device number to be the anon one. This device number is generated by the kernel on boot and is persistent while the node is up and running.
|
||||
For example, when [dumping and restoring memory](memory-dumping-and-restoring.md), CRIU must distinguish between anonymous [shared memory](shared-memory.md) and `tmpfs` file mappings. This is done by checking the mapped file's device number. This number is generated by the kernel at boot and remains persistent as long as the system is running.
|
||||
|
||||
Thus on start CRIU collects all the needed information, but the amount of data to find out and time it takes is quite big, especially if compared to the time it takes CRIU to perform requested action (dump or restore). To speed things up CRIU caches the collected information in `/run/criu.kdat` file. The directory is typically a tmpfs mount point that gets flushed when node reboots, so the cache is always clean after the kernel change.
|
||||
|
||||
The `/run` directory is not cut in stone, it can be changed compile-time by overriding the `RUNDIR` variable.
|
||||
Upon startup, CRIU collects this and other necessary information. Because the amount of data to collect and the time required can be significant—especially compared to the speed of a typical dump or restore—CRIU caches this information in the `/run/criu.kdat` file. This directory is typically a `tmpfs` mount that is cleared upon reboot, ensuring the cache is always fresh for the current kernel.
|
||||
|
||||
The `/run` directory is the default location, but it can be changed at compile-time by overriding the `RUNDIR` variable.
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
# Mac-Vlan
|
||||
|
||||
CRIU supports checkpointing and restoring network namespaces with macvlan devices.
|
||||
CRIU supports checkpointing and restoring network namespaces that use `macvlan` devices.
|
||||
|
||||
## Dump
|
||||
|
||||
On dump, criu will automatically detect these devices and no extra arguments are needed. The name of macvlan device inside the checkpointed namespace is saved to [images](images.md).
|
||||
During a dump, CRIU automatically detects these devices; no additional arguments are required. The name of the `macvlan` device within the checkpointed namespace is saved in the [images](images.md).
|
||||
|
||||
## Restore
|
||||
|
||||
On restore, users *must* specify the master device in the host network namespace via `--external macvlan[*inner_dev*]:*outer_dev*`, where `*inner_dev*` is the device name in restored namespace, and `*outer_dev*` is a network device existing in the same namespace as CRIU.
|
||||
During restoration, users *must* specify the master device in the host network namespace using the following syntax:
|
||||
|
||||
## Implementation details
|
||||
|
||||
The restore process for macvlan interfaces is somewhat convoluted, since the actual macvlan interface lives inside the network namespace, but the master device lives outside. CRIU uses `IFLA_NET_NS_ID` to specify the network namespace that the master link lives in, and uses `IFLA_NET_NS_FD` to specify the network namespace the slave link should be created in. In the user namespace case, the netlink call is made from usernsd, since the caller needs to have CAP_NET_ADMIN in both network namespaces. In the non-userns case, we setns around to create a netlink socket in CRIU's netns, and then use that socket to actually create the macvlan link.
|
||||
`--external macvlan[*inner_dev*]:*outer_dev*`
|
||||
|
||||
Where `*inner_dev*` is the device name within the restored namespace, and `*outer_dev*` is the corresponding network device in CRIU's namespace.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
Restoring `macvlan` interfaces is complex because the interface itself resides within the target network namespace, while its master device resides outside. CRIU uses `IFLA_NET_NS_ID` to specify the master link's namespace and `IFLA_NET_NS_FD` to specify the namespace where the slave link should be created.
|
||||
|
||||
In cases involving user namespaces, the `netlink` call is made from `usernsd`, as the caller must have `CAP_NET_ADMIN` in both network namespaces. In non-usernamespace cases, CRIU uses `setns` to create a `netlink` socket within the target namespace and then uses that socket to create the `macvlan` link.
|
||||
|
|
|
|||
|
|
@ -1,52 +1,51 @@
|
|||
# Memory changes tracking
|
||||
# Memory Changes Tracking
|
||||
|
||||
CRIU can detect what memory pages a task (or tasks) has changed since some moment of time. This page describes why this is required, how it works and how to use it.
|
||||
CRIU can detect which memory pages a task (or tasks) has changed since a specific point in time. This page describes why this is required, how it works, and how to use it.
|
||||
|
||||
## Why do we need to track memory changed
|
||||
## Why Track Memory Changes?
|
||||
|
||||
There are several scenarios where detecting what parts of memory has changed is required:
|
||||
Several scenarios require detecting which parts of memory have changed:
|
||||
|
||||
**[Incremental dumps](incremental-dumps.md)**
|
||||
When you take a series of dumps from a process tree, it is a very good optimization not to dump *all* the memory every time, but get only those memory pages that has changed since previous dump
|
||||
**[Incremental Dumps](incremental-dumps.md)**
|
||||
When taking a series of dumps from a process tree, it is a significant optimization to avoid dumping all memory every time. Instead, only pages that have changed since the previous dump are captured.
|
||||
|
||||
**Smaller freeze time for big applications**
|
||||
When a task uses a LOT of memory, dumping it may take time and during all this time this task should be frozen. To reduce the freeze time we can
|
||||
* get memory from task and start writing it in images
|
||||
* freeze task and get only changed memory from it
|
||||
**Reduced Freeze Time for Large Applications**
|
||||
When a task uses a vast amount of memory, dumping it can be time-consuming, and the task must remain frozen during the process. To reduce freeze time, CRIU can:
|
||||
1. Capture memory from the task and begin writing it to images while the task is still running.
|
||||
1. Briefly freeze the task to capture only the pages that changed during the initial write.
|
||||
|
||||
**[Live migration](live-migration.md)**
|
||||
When doing live migration, a lot of time is used by the procedure of copying tasks' memory to the destination host. Note that the processes are frozen during that time. Acting like in the previous example also reduces the freeze time, i.e. the live migration becomes more live.
|
||||
**[Live Migration](live-migration.md)**
|
||||
During live migration, much of the time is spent copying task memory to the destination host. Processes are typically frozen during this period. Using memory tracking reduces this "downtime," making the migration more seamless.
|
||||
|
||||
## How we track memory changes
|
||||
## How Memory Changes Are Tracked
|
||||
|
||||
In order to find out which memory pages have changed, we [patched](http://lwn.net/Articles/546966/) the kernel. Tracking the memory changes consists of two steps:
|
||||
To identify changed memory pages, CRIU uses a kernel feature (originally [patched](http://lwn.net/Articles/546966/) into the kernel). Tracking consists of two steps:
|
||||
|
||||
- ask the kernel to keep track of memory changes (by writing 4 into `/proc/$pid/clear_refs` file for each $pid we are interested in).
|
||||
1. Request that the kernel track memory changes by writing `4` into the `/proc/$pid/clear_refs` file for each process of interest.
|
||||
1. After a period of time, retrieve the list of modified pages by reading `/proc/$pid/pagemap` and checking the "soft-dirty" bit in the entries.
|
||||
|
||||
and, after a while,
|
||||
During the first step, the kernel remaps the task's mappings as read-only. If the task subsequently attempts to write to a page, a page fault occurs, and the kernel records the modification. Reading the `pagemap` file reveals these recorded changes.
|
||||
|
||||
- get the list of modified pages of a process by reading its `/proc/$pid/pagemap` file and looking at so called *soft-dirty* bit in the pagemap entries.
|
||||
## Using Memory Tracking with CRIU
|
||||
|
||||
During the first step, kernel will re-map all the tasks' mapping in read-only manner. If a task then tries to write into any of its pages, a page fault will occur, and the kernel will note which page is being written to. Reading the `pagemap` file reveals this information.
|
||||
First, verify that the feature is supported by running:
|
||||
|
||||
## How to use this with CRIU
|
||||
```bash
|
||||
criu check --feature mem_dirty_track
|
||||
```
|
||||
|
||||
First of all, the
|
||||
Memory change tracking was initially merged into Linux kernel v3.11 and refined until v3.18 (see [Upstream kernel commits](upstream-kernel-commits.md) for details).
|
||||
|
||||
# criu check --feature mem_dirty_track
|
||||
Command-line options for this functionality include:
|
||||
|
||||
command should say the feature is supported. The memory changes tracking was initially merged into Linux kernel v3.11, and was further polished until v3.18 (see [Upstream kernel commits](upstream-kernel-commits.md) for details).
|
||||
**`--prev-images-dir`**
|
||||
Provides the path to images from a previous `dump` or `pre-dump`. CRIU will then attempt to dump only the pages modified since that previous action.
|
||||
|
||||
There are several command line options to use the functionality:
|
||||
|
||||
**`--prev-images-dir` option**
|
||||
This option is used to provide the path where images from a previous `dump` or `pre-dump` (see below) action reside. If possible, CRIU will dump only the memory pages that have been modified since that time.
|
||||
|
||||
**`--track-mem` option**
|
||||
This option makes CRIU to reset memory changes tracker. If done, the next dump `--prev-images-dir` will have chances to successfully find not changed pages.
|
||||
**`--track-mem`**
|
||||
Causes CRIU to reset the memory change tracker. This ensures that the next dump using `--prev-images-dir` can successfully identify unchanged pages.
|
||||
|
||||
**`pre-dump` action**
|
||||
This action dumps only part of the information about processes and does that by keeping tasks frozen for the shortest possible time. The images generated by pre-dump cannot and should not be used for restore. After this action the proper `dump` should be performed with properly configured `--prev-images-dir` path.
|
||||
This action dumps only a portion of the process information, keeping tasks frozen for the shortest possible time. Images generated by a `pre-dump` cannot be used for restoration; a subsequent `dump` must be performed using the `--prev-images-dir` option.
|
||||
|
||||
## See also
|
||||
- [Live migration](live-migration.md)
|
||||
|
|
@ -57,6 +56,3 @@ There are several command line options to use the functionality:
|
|||
|
||||
## External links
|
||||
- http://lwn.net/Articles/546966/
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,99 +1,69 @@
|
|||
# Memory dumping and restoring
|
||||
# Memory Dumping and Restoring
|
||||
|
||||
This article describes how CRIU dumps and restores processes' memory. For memory image file formats, see [memory dumps](memory-dumps.md).
|
||||
This article describes how CRIU dumps and restores process memory. For memory image file formats, see [Memory dumps](memory-dumps.md).
|
||||
|
||||
## Basic C/R
|
||||
## Basic Checkpoint/Restore
|
||||
|
||||
### Dumping
|
||||
|
||||
Currently memory dumping depends on 3 big technologies:
|
||||
Memory dumping currently relies on three key technologies:
|
||||
|
||||
- /proc/pid/smaps file and /proc/pid/map_files/ directory with links are used to determine
|
||||
-* memory areas in use by task
|
||||
-* mapped files (if any)
|
||||
-* shared memory "identifier" to resolve the MAP_SHARED areas
|
||||
- /proc/pid/pagemap file that reveals important flags
|
||||
-* *present* indicates that the physical page is there. Non-present pages are not dumped.
|
||||
-* *anonymoys* for the MAP_FILE | MAP_PRIVATE mapping indicate that the page in question is already COW-ed from the file's. Not-anonymous pages are not dumped as they are still in sync with the file
|
||||
-* *soft-dirty* bit is used by [memory changes tracking](memory-changes-tracking.md)
|
||||
- Ptrace SEIZE, used to grab pages from task's VM into a pipe (with vmsplice)
|
||||
- The `/proc/$pid/smaps` file and `/proc/$pid/map_files/` directory are used to determine:
|
||||
- Memory areas currently in use by a task.
|
||||
- Mapped files (if any).
|
||||
- Shared memory identifiers used to resolve `MAP_SHARED` areas.
|
||||
- The `/proc/$pid/pagemap` file provides critical flags:
|
||||
- **Present**: Indicates the physical page is in memory. Only present pages are dumped.
|
||||
- **Anonymous**: For `MAP_FILE | MAP_PRIVATE` mappings, this indicates the page has been modified (COW) from the original file. Unmodified pages are not dumped as they can be recovered from the file.
|
||||
- **Soft-dirty**: Used for [memory changes tracking](memory-changes-tracking.md).
|
||||
- `ptrace SEIZE` is used to extract pages from a task's virtual memory into a pipe using `vmsplice`.
|
||||
|
||||
The last step deserves a more detailed explanation. In order to drain memory from a task, we first generate the bitmap of pages needed to be dumped (using the smaps, map_files and [pagemap cache](pagemap-cache.md) filled from proc). Next, we create a set of pipes to put pages into. Then we infect the process with [parasite code](parasite-code.md), which, in turn, gets the pipes and `vmsplice`s the required pages into it. Finally, we `splice` the pages from pipes into [image files](memory-dumps.md).
|
||||
The final step warrants a more detailed explanation. To drain memory from a task, CRIU first generates a bitmap of pages to be dumped (using `smaps`, `map_files`, and the [pagemap cache](pagemap-cache.md)). Next, a set of pipes is created. CRIU then injects [parasite code](parasite-code.md) into the process, which uses `vmsplice` to move the required pages into the pipes. Finally, CRIU `splice`s the pages from the pipes into [image files](memory-dumps.md).
|
||||
|
||||
### Restoring
|
||||
|
||||
Restoring is pretty straightforward. During restore, CRIU morphs itself into a target task. Two things worth mentioning before diving into explanation of steps.
|
||||
During restoration, CRIU morphs itself into the target task. Two points are worth noting:
|
||||
|
||||
**[COW](cow.md)**
|
||||
Anonymous private mappings might have pages shared between tasks till they get COW-ed. To restore this CRIU pre-restores those pages before forking the child processes and `mremap`-s them in the [final stage](restorer-context.md).
|
||||
**[Copy-on-Write (COW)](cow.md)**
|
||||
Anonymous private mappings may have pages shared between tasks until they are modified. To restore this, CRIU pre-restores these pages before forking child processes and uses `mremap` in the [final stage](restorer-context.md).
|
||||
|
||||
**[Shared memory](shared-memory.md)**
|
||||
Those areas are implemented in the kernel by supporting a pseudo file on a hidden tmpfs mount. So on restore we just determine who will create the shared are and who will attach to it (see the [postulates](postulates.md)). Then the creator `mmap`-s the region and the others open the /proc/pid/map_files/ link. However, on the recent kernels, we use the new `memfd` system call that does similar thing but works for user namespaces. Briefly -- creator creates the memfd, all the others get one via /proc/pid/fd link which is not that strict as compared to the map_files.
|
||||
**[Shared Memory](shared-memory.md)**
|
||||
Shared regions are implemented in the kernel via a pseudo-file on a hidden `tmpfs` mount. During restoration, CRIU determines which process will create the shared area and which will attach to it (see [Postulates](postulates.md)). The creator `mmap`s the region, and others open it via the `/proc/$pid/map_files/` link. On modern kernels, the `memfd` system call is used for similar functionality within user namespaces.
|
||||
|
||||
Having said that, the restore of memory is done in the following steps:
|
||||
The memory restoration process follows these steps:
|
||||
|
||||
**Open images and read in VMAs**
|
||||
Open all the mm.img, read mappings in, resolve shared memory segments and check whether we need to special-care mapped files.
|
||||
1. **Opening Images and Reading VMAs**: Open `mm.img`, read mappings, resolve shared memory segments, and identify mapped files requiring special handling.
|
||||
1. **Forking and Pre-mmapping**: Each task pre-maps private anonymous areas and populates them with pages from the images. The task then forks a child, which performs the same operations. This ensures that COW areas correctly share pages, as `fork()` is the standard mechanism for the Linux kernel to establish this sharing.
|
||||
1. **Opening File Mappings**: After forking, CRIU identifies `MAP_FILE` VMAs and uses the [files](files.md) engine to open them.
|
||||
1. **Opening Shared Mappings**: CRIU creates file descriptors for shared anonymous VMAs.
|
||||
1. **Entering the [Restorer Context](restorer-context.md)**: CRIU strips away its own original mappings, preparing the virtual memory for the restored mappings.
|
||||
1. **Restoring Mappings**: Anonymous private mappings are `mremap`ed from pre-mapped areas, while file mappings and anonymous shared mappings are created via `mmap`.
|
||||
|
||||
**Fork and pre-mmap**
|
||||
Each task pre-mmaps private anonymous areas and populates them with pages (from pagemap/pages images). Then task forks the child which does the same. It is done in such way in order to make COWed areas actually share the pages they should. On fork() the shared pages become actually shared, as currently this is the only way to make Linux kernel do this.
|
||||
### Non-linear Mappings
|
||||
|
||||
**Open file mappings**
|
||||
Soon after fork we check which VMA-s are MAP_FILE ones and request the [files](files.md) engine to open them.
|
||||
CRIU does not currently support non-linear mappings; the dump will fail if they are encountered.
|
||||
|
||||
**Open shared mappings**
|
||||
At almost the same place we create an FD for shared anonymous VMA-s.
|
||||
## Advanced Checkpoint/Restore
|
||||
|
||||
**Dive into [restorer context](restorer-context.md)**
|
||||
At this stage we strip off all the old CRIU mappings thus making the VM be ready for restored mappings.
|
||||
For scenarios like remote dumping, stackable images, and incremental dumps, CRIU supports sophisticated memory policies beyond a simple "dump all/restore all." Several command-line options are available:
|
||||
|
||||
**Restore mappings in their places**
|
||||
Anonymous private mappings are `mremap`-ed from the pre-mapped areas one-by-one, file mappings are created with `mmap` system call. Anonymous shared mappings are also just mmaped.
|
||||
- `dump` action
|
||||
- `pre-dump` action
|
||||
- `--track-mem`
|
||||
- `--prev-images-dir`
|
||||
- `--leave-running`
|
||||
- `--page-server`
|
||||
|
||||
### Non linear mappings
|
||||
The `pre-dump` action automatically enables `--track-mem` and `--leave-running`. While a `pre-dump` captures only memory, a full `dump` captures the entire state, including files and sockets. Common combinations include:
|
||||
|
||||
Currently we don't support non-linear mappings (so dump fails if such mappings are found).
|
||||
- **`dump`**: Dumps everything and terminates the tasks.
|
||||
- **`dump --leave-running`**: Dumps everything and allows tasks to continue execution.
|
||||
- **`dump --track-mem --leave-running --prev-images-dir <path>`**: Dumps only pages modified since the previous dump in `<path>`, while leaving tasks running.
|
||||
- **`pre-dump`**: Dumps memory only, enables tracking, and leaves tasks running.
|
||||
- **`pre-dump --prev-images-dir <path>`**: Performs an incremental memory dump.
|
||||
- **`dump --page-server`**: Sends pages directly to a [page server](page-server.md) (e.g., for [disk-less migration](disk-less-migration.md)).
|
||||
|
||||
## Advanced C/R
|
||||
|
||||
For things as remote dump, stackable images, and incremental dumps, CRIU supports a more sophisticated memory C/R policies rather than "dump all -- restore all" one. There are several CLI knobs that can be used.
|
||||
|
||||
- dump action
|
||||
- pre-dump action
|
||||
- --track-mem option
|
||||
-* --prev-images-dir option
|
||||
- --leave-running option
|
||||
- --page-server option
|
||||
|
||||
Let's see what all of this means.
|
||||
|
||||
First of all, the pre-dump action always turns on the `--track-mem` and the `--leave-running` options even if they are not specified in the command line. Next, the pre-dump action dumps *only* the memory, while the dump one dumps all the state including open files, sockets and other stuff. Having said that, let's see all the possible combinations and what they result in.
|
||||
|
||||
**dump**
|
||||
Without any options, dump everything and kill the dumped tasks.
|
||||
|
||||
**dump --track-mem**
|
||||
Dump everything, turn on memory changes tracking, and kill tasks after this. As you might have noticed, this is pretty useless combination of options!
|
||||
|
||||
**dump --leave-running**
|
||||
Dump everything, and leave the tasks running after dump.
|
||||
|
||||
**dump --track-mem --leave-running**
|
||||
Same as above, but turn on memory changes tracking.
|
||||
|
||||
**dump --track-mem --leave-running --prev-images-dir <path>**
|
||||
Same as above, but during dump also check whether the page in question is present in parent, and skip dumping it this time.
|
||||
|
||||
**pre-dump**
|
||||
Only dump memory, turn on memory changes tracking and leave the tasks running.
|
||||
|
||||
**pre-dump --prev-images-dir <path>**
|
||||
Same as above, but check for pages present in parent and skip them.
|
||||
|
||||
**<pre->dump <options> --page-server**
|
||||
Send the pages to the page server (e.g. for [disk-less migration](disk-less-migration.md)). See [page server](page-server.md) for more details.
|
||||
|
||||
## Messing with image files
|
||||
## Image File Workflow
|
||||
|
||||

|
||||
|
||||
|
|
@ -107,7 +77,3 @@ First of all, the pre-dump action always turns on the `--track-mem` and the `--l
|
|||
- [Postulates](postulates.md)
|
||||
- [Disk-less migration](disk-less-migration.md)
|
||||
- [Page server](page-server.md)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,31 +1,32 @@
|
|||
# Memory images deduplication
|
||||
# Memory Images Deduplication
|
||||
|
||||
When performing [incremental dumps](incremental-dumps.md) or [iterative migration](iterative-migration.md), a layered stack of memory images is created. In that stack, some data is duplicated (i.e. same memory page is present in multiple images). This article describes ways to deduplicate such data by punching holes in image files (using `fallocate()` syscall with `FALLOC_FL_PUNCH_HOLE` flag), effectively freeing used disk space.
|
||||
When performing [incremental dumps](incremental-dumps.md) or [iterative migration](iterative-migration.md), a layered stack of memory images is created. In this stack, some data is duplicated (i.e., the same memory page is present in multiple images). This article describes how to deduplicate this data by "punching holes" in image files using the `fallocate()` system call with the `FALLOC_FL_PUNCH_HOLE` flag, which effectively frees disk space.
|
||||
|
||||
## Deduplication mode
|
||||
## Deduplication Modes
|
||||
|
||||
Two ways to deduplicate memory images are available.
|
||||
Two methods for deduplicating memory images are available.
|
||||
|
||||
### Offline
|
||||
### Offline Deduplication
|
||||
|
||||
The `criu dedup` command opens the image directory and punches holes in the *parent* images where *child* images would replace them.
|
||||
The `criu dedup` command processes an image directory and punches holes in **parent** images where **child** images would otherwise replace them.
|
||||
|
||||
### On the fly
|
||||
### On-the-Fly Deduplication
|
||||
|
||||
The `--auto-dedup` option can be used for `criu dump`, `criu pre-dump` and `criu page-server`. It causes every write to images with process' pages to punch holes in the respective parent images, which is extremely useful in [disk-less migration](disk-less-migration.md) scenario.
|
||||
The `--auto-dedup` option can be used with `criu dump`, `criu pre-dump`, and `criu page-server`. This causes every write to a process's memory images to simultaneously punch holes in the respective parent images. This is particularly useful in [disk-less migration](disk-less-migration.md) scenarios.
|
||||
|
||||
The `--auto-dedup` option can also be used for `criu restore`. This makes CRIU to punch holes in images as memory is being restored. This should be used if images are stored on tmpfs (i.e. in RAM, see [disk-less migration](disk-less-migration.md)), as this way RAM usage is not growing.
|
||||
The `--auto-dedup` option can also be used with `criu restore`. This causes CRIU to punch holes in the images as the memory is being restored. This is recommended if images are stored on `tmpfs` (i.e., in RAM), as it prevents RAM usage from growing unnecessarily during restoration.
|
||||
|
||||
## Shared memory deduplication
|
||||
-Main article: [Shared memory](shared-memory.md)*
|
||||
## Shared Memory Deduplication
|
||||
|
||||
Deduplication only makes sense for incremental memory dumps. For now CRIU can only track changes, create incremental checkpoints and do dedup for anonymous memory. Changes tracking, increments and deduplication for shared memory is currently (August 2016) available in CRIU development branch.
|
||||
*Main article: [Shared memory](shared-memory.md)*
|
||||
|
||||
## Implementation notes
|
||||
Deduplication is primarily relevant for incremental memory dumps. Currently, CRIU can track changes, create incremental checkpoints, and perform deduplication for anonymous memory. Support for change tracking and deduplication for shared memory is also available.
|
||||
|
||||
Memory images are stored into two files: *pagemap* and *pages* (see [memory dumps](memory-dumps.md) for details). Note that the deduplication process does not change *pagemap* in any way, it only punches holes in *pages* image files.
|
||||
## Implementation Notes
|
||||
|
||||
Note that having a hole in an image file have totally different meaning that is in no way similar to the one of **in_parent** flag in *pagemap* entry (described in [memory dumps](memory-dumps.md)).
|
||||
Memory images are stored in two files: `pagemap` and `pages` (see [Memory dumps](memory-dumps.md) for details). Note that the deduplication process does not modify the `pagemap`; it only punches holes in the `pages` image files.
|
||||
|
||||
A hole in an image file has a different meaning than the `in_parent` flag in a `pagemap` entry (as described in [Memory dumps](memory-dumps.md)).
|
||||
|
||||
## See also
|
||||
|
||||
|
|
@ -38,7 +39,3 @@ Note that having a hole in an image file have totally different meaning that is
|
|||
## External links
|
||||
|
||||
- http://man7.org/linux/man-pages/man2/fallocate.2.html
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,35 +1,34 @@
|
|||
# Mount points
|
||||
# Mount Points
|
||||
|
||||
This page describes how CRIU handles mount point trees.
|
||||
|
||||
This page describes what we do with mount points trees.
|
||||
## Introduction
|
||||
When we are thinking about restoring a mount tree, we need to remember a few things:
|
||||
- shared and slave groups
|
||||
- how mounts are propagated inside one group
|
||||
- bind mounts (rw, ro)
|
||||
When restoring a mount tree, several factors must be considered:
|
||||
- Shared and slave groups.
|
||||
- How mounts propagate within a group.
|
||||
- Bind mounts (both read-write and read-only).
|
||||
|
||||
The algorithm described here is not able to cover all the cases, so this solution is a temporary one.
|
||||
The algorithm described here is a temporary solution and may not cover all complex edge cases.
|
||||
|
||||
## Dump
|
||||
|
||||
There is nothing interesting here. We just dump information about mounts and validate them to be sure that we are able to restore them.
|
||||
Dumping is straightforward. CRIU captures information about the mounts and validates them to ensure they can be successfully restored.
|
||||
|
||||
## Restore
|
||||
|
||||
Mounts are restored for a few iterations. On each iteration we enumerate all mounts and mount everything we can. On the next iteration we mount a bit more and continue to do so step by step. The idea is that we will be able to mount something new on each iteration. If we can't mount anything, we stop and report an error telling that we can't restore this configuration.
|
||||
For example, a mount can't be mounted if its parent isn't mounted yet. Or a more interesting example, a mount can't be mounted if not all the mounts from its parent shared group are mounted.
|
||||
Mounts are restored over several iterations. In each iteration, CRIU enumerates all mounts and restores those that are currently possible. This continues step-by-step until the tree is complete. The core idea is that each successful mount may enable others in the next iteration. If an iteration completes without any new mounts being added, CRIU stops and reports an error.
|
||||
|
||||
## Known issues
|
||||
CRIU doesn't support configurations where two mounts of one shared group have different set of mounts. This is not a feature, this is a bug and you are welcome to fix it.
|
||||
For example, a mount cannot be restored if its parent has not yet been mounted, or if certain dependencies within its parent's shared group are missing.
|
||||
|
||||
(done and checked by non_uniform_share_propagation in zdtm)
|
||||
## Known Issues
|
||||
CRIU does not currently support configurations where two mounts within the same shared group have different sets of sub-mounts. This is a known bug rather than a feature.
|
||||
|
||||
## TODO
|
||||
- Read-only bind mounts
|
||||
(not sure was meant here but e.g. ghost files on readonly mounts handled and checked by ghost_on_rofs zdtm test)
|
||||
- Skipping mountpoints
|
||||
- Enabling FS runtime
|
||||
(Note: This has been addressed and is verified by the `non_uniform_share_propagation` test in ZDTM.)
|
||||
|
||||
## To-Do
|
||||
- **Read-only bind mounts**: While certain cases (like ghost files on read-only mounts) are handled and verified by the `ghost_on_rofs` ZDTM test, full support may require further refinement.
|
||||
- **Skipping mount points**.
|
||||
- **Enabling filesystem runtime**.
|
||||
|
||||
## See also
|
||||
[External bind mounts](external-bind-mounts.md)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,109 +1,66 @@
|
|||
# Mount points/2.0
|
||||
# Mount Points 2.0
|
||||
|
||||
= Problem =
|
||||
A mount namespace is a tree of mount points. In addition, mounts have another type of dependencies which is called groups. Each mount can be a member of two groups, it can be a slave in one group and a member of another group. Currently groups can’t be set, it can be only inherited from a source mount. It is always a problem when more than one type of properties have to restored for one call. This means that we have to find a sequence of steps to get a required state.
|
||||
In case of mount namespaces, one more problem is over-mounts. A few mounts may be over-mounted or processes can have file descriptors which are linked with over-mounted files.
|
||||
Another difficulty is that we are not able to create bind-mounts between namespaces, but each file system have to be mounted from a specified user namespace.
|
||||
## The Problem
|
||||
A mount namespace is a tree of mount points. In addition, mounts have group-based dependencies: a mount can be a slave in one group and a member of another. Currently, these groups cannot be set directly; they must be inherited from a source mount. Recreating these properties alongside other mount attributes requires a specific sequence of operations.
|
||||
|
||||
= Solution =
|
||||
When we see all variation of commands to build a mount tree, we can understand that the final picture may be very complicated to be repeated, so we suggest to add [a new flag](https://patchwork.kernel.org/patch/9703885/) to the mount() syscall, which allows us to add a mount into an existing group.
|
||||
Additional challenges include:
|
||||
- **Over-mounts**: Multiple mounts may occupy the same location, or processes may hold file descriptors to over-mounted files.
|
||||
- **User Namespaces**: Bind mounts cannot be created between different namespaces directly; each filesystem must be mounted from its respective user namespace.
|
||||
|
||||
In this case the restore algorithm will be very simple.
|
||||
- Create a temporary mount which is called “root yard”
|
||||
- Create all namespaces (in specified user namespaces)
|
||||
- Add root yards from all namespaces into one shared group, so a mount is created in one mntns, will be propagated into others.
|
||||
- Create all mounts in separate directories in the root yards.
|
||||
- Restore opened files (nothing is over-mounted at this point)
|
||||
- Build mount trees in namespaces by moving mounts to right places
|
||||
- Do pivot_root() in all namespaces
|
||||
## The Solution
|
||||
Given the complexity of recreating a mount tree using standard commands, we proposed [a new flag](https://patchwork.kernel.org/patch/9703885/) for the `mount()` system call that allows adding a mount to an existing group.
|
||||
|
||||
Let’s look at the next example:
|
||||
With this capability, the restoration algorithm is simplified:
|
||||
1. Create a temporary "root yard" mount.
|
||||
1. Create all necessary namespaces (within their respective user namespaces).
|
||||
1. Add the root yards from all namespaces into a single shared group so that a mount created in one namespace propagates to others.
|
||||
1. Create all mounts in separate directories within the root yards.
|
||||
1. Restore open files (at this stage, no mounts are over-mounted).
|
||||
1. Assemble the final mount trees by moving mounts to their correct locations.
|
||||
1. Perform `pivot_root()` in all namespaces.
|
||||
|
||||
{| class="wikitable"
|
||||
!mnt_id
|
||||
!parent
|
||||
!shared
|
||||
!master
|
||||
|-
|
||||
|1
|
||||
|0
|
||||
|
|
||||
|
|
||||
|-
|
||||
|2
|
||||
|1
|
||||
|1
|
||||
|
|
||||
|-
|
||||
|3
|
||||
|2
|
||||
|2
|
||||
|
|
||||
|-
|
||||
|4
|
||||
|2
|
||||
|3
|
||||
|
|
||||
|-
|
||||
| 5
|
||||
| 1
|
||||
|
|
||||
|
|
||||
|-
|
||||
| 6
|
||||
| 0
|
||||
|
|
||||
|
|
||||
|-
|
||||
|7
|
||||
| 6
|
||||
| 1
|
||||
|
|
||||
|-
|
||||
| 8
|
||||
| 7
|
||||
| 2
|
||||
|
|
||||
|-
|
||||
| 9
|
||||
| 7
|
||||
| 4
|
||||
| 3
|
||||
|-
|
||||
| 10
|
||||
| 6
|
||||
|
|
||||
|
|
||||
|}
|
||||
### Example Configuration
|
||||
|
||||
The origin tree looks like this:
|
||||
| mnt_id | parent | shared | master |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 1 | 0 | | |
|
||||
| 2 | 1 | 1 | |
|
||||
| 3 | 2 | 2 | |
|
||||
| 4 | 2 | 3 | |
|
||||
| 5 | 1 | | |
|
||||
| 6 | 0 | | |
|
||||
| 7 | 6 | 1 | |
|
||||
| 8 | 7 | 2 | |
|
||||
| 9 | 7 | 4 | 3 |
|
||||
| 10 | 6 | | |
|
||||
|
||||

|
||||
The original tree:
|
||||
|
||||
The first stage is to restore all mounts in all namespace separately. In addition, we need to create all shared groups.
|
||||

|
||||
|
||||

|
||||

|
||||
First, each mount and shared group is restored separately within its respective namespace:
|
||||
|
||||
The next step is to move mounts to proper places to restore a tree and then we restore groups for each mount.
|
||||

|
||||
|
||||

|
||||
Finally, the mounts are moved into their proper positions and their group relationships are established:
|
||||
|
||||
= Restore of unix sockets =
|
||||

|
||||
|
||||
Unix sockets can be bound to a file. The problem is that an address and a file are not connected between each other in term of unix sockets. For example, if you move a socket file, ss shows the origin address and you can’t find a file where the socket is bound. Another example is that an address may contain a relative path (../socket_name).
|
||||
## Restoring UNIX Sockets
|
||||
|
||||
Currently socket_diag shows a device and an inode number for a socket file, but it says nothing about a path to this file and about its mount point. We introduced the SIOCUNIXFILE ioctl, which returns a file descriptor to a socket file.
|
||||
In this case to restore a unix socket we have to:
|
||||
- create a temporary directory and mount tmpfs into it before restoring sockets
|
||||
- Restore sockets
|
||||
- create a socket address directory where is the last part is a symlink to a proper directory on a required mount point
|
||||
- call chroot() to the temporary directory
|
||||
- bind the socket to a specified address
|
||||
if we restored a server socket, we can get a file descriptor for its file and use it to restore client sockets by calling connect() for /proc/self/fd/[SK_FILE_FD]
|
||||
umount tmpfs from the temporary directory and remove the directory after restoring all sockets
|
||||
UNIX sockets can be bound to files. However, a socket's address and its underlying file are not intrinsically linked within the kernel. For example, if a socket file is moved, tools like `ss` still show the original address. Addresses may also use relative paths (e.g., `../socket_name`).
|
||||
|
||||
= Source code =
|
||||
While `socket_diag` provides the device and inode of a socket file, it does not provide the path or mount point. To address this, we introduced the `SIOCUNIXFILE` ioctl, which returns a file descriptor for the socket file.
|
||||
|
||||
To restore a UNIX socket:
|
||||
1. Create a temporary directory and mount `tmpfs` onto it.
|
||||
1. Restore the sockets.
|
||||
1. Create a socket address directory where the final component is a symlink to the correct directory on the required mount point.
|
||||
1. `chroot()` into the temporary directory.
|
||||
1. Bind the socket to the specified address.
|
||||
1. If restoring a server socket, retrieve the file descriptor for its file and use it to restore client sockets by calling `connect()` on `/proc/self/fd/[SK_FILE_FD]`.
|
||||
1. Unmount the temporary `tmpfs` and remove the directory.
|
||||
|
||||
## Source Code
|
||||
- [github.com/avagin/criu/tree/mntns-2.0](https://github.com/avagin/criu/tree/mntns-2.0)
|
||||
- [[PATCH](https://lkml.org/lkml/2017/5/9/634) fs: add an ioctl to get an owning userns for a superblock]
|
||||
|
||||
- [[PATCH] fs: add an ioctl to get an owning userns for a superblock](https://lkml.org/lkml/2017/5/9/634)
|
||||
|
|
|
|||
|
|
@ -1,82 +1,63 @@
|
|||
# Mount-v2
|
||||
|
||||
Mount-v2 CRIU algorithm
|
||||
CRIU Mount-v2 Algorithm
|
||||
|
||||
## Introduction
|
||||
|
||||
After we've merged MOVE_MOUNT_SET_GROUP feature to mainstream linux v5.15 [torvalds/linux@9ffb14e](https://github.com/torvalds/linux/commit/9ffb14ef61bab83fa818736bf3e7e6b6e182e8e2) now we can use it to restore sharing groups of mounts without the need to care about inheriting those groups when create mounts, we can just set sharing groups at later stage and before that construct mount trees with private mounts.
|
||||
With the introduction of the `MOVE_MOUNT_SET_GROUP` feature in Linux v5.15 ([commit 9ffb14e](https://github.com/torvalds/linux/commit/9ffb14ef61bab83fa818736bf3e7e6b6e182e8e2)), CRIU can now restore mount sharing groups independently. We can construct mount trees using private mounts and then apply sharing groups at a later stage, rather than relying on complex inheritance during mount creation.
|
||||
|
||||
Restoring propagation right with conservative approach of both creating mounts and inheriting propagation groups looks like mission impossible task for us due to many problems:
|
||||
Restoring mount propagation using the traditional approach of inheriting groups is nearly impossible due to several factors:
|
||||
- CRIU lacks information about the original order or history of mount tree creation.
|
||||
- Propagation can trigger the creation of numerous unintended mounts.
|
||||
- Propagation can unexpectedly change the parent of an existing mount.
|
||||
- "Mount traps" can occur where propagation covers an initial mount.
|
||||
- "Non-uniform" propagation requires specific mount orders and temporary "lock" mounts to recreate.
|
||||
- Cross-namespace sharing requires strict ordering relative to namespace creation.
|
||||
|
||||
- Criu knows nothing about the initial history or order of mount tree creation;
|
||||
- Propagation can create tons of mounts;
|
||||
- Propagation may change parent mounts for existing mount tree;
|
||||
- "Mount trap" - propagation may cover initial mount;
|
||||
- "Non-uniform" propagation - there are different tricks with mount order and temporary children-"lock" mounts, which create mount trees which can't be restored without those tricks;
|
||||
- "Cross-namespace" sharing groups creation need to be ordered with mount namespace creation right;
|
||||
- Sharing groups vs mount tree order inversion can be very complex to restore and require multiple auxiliary. (see example below)
|
||||
|
||||
See my talks about it on Linux Plumbers Conference:
|
||||
For more details, see the following presentations from the Linux Plumbers Conference:
|
||||
- [CRIU mounts migration: problems and solutions](https://www.linuxplumbersconf.org/event/7/contributions/640/)
|
||||
- [Mount-v2 CRIU migration engine: status update](https://linuxplumbersconf.org/event/11/contributions/923/)
|
||||
|
||||
And here is the example of order inversion where multiple temporary mounts needed to achieve the result:
|
||||

|
||||
Below is an example of order inversion where multiple temporary mounts are required:
|
||||

|
||||
|
||||
## Mount-v2 description
|
||||
## Mount-v2 Description
|
||||
|
||||
New mount-v2 algorithm is integrated deeply in the original one, so that dumping of mounts is done exactly the same for original mount engine and new one. So mount-v2 series has preparatory steps related to bindmount detection, external mounts detection and helper mounts handling to make the original mount code more robust, to make it easier to reuse it in mount-v2.
|
||||
The Mount-v2 algorithm is integrated into the original engine; dumping remains unchanged. Preparatory steps—such as detecting bind mounts, external mounts, and helper mounts—have been refined to make the code more robust and reusable for Mount-v2.
|
||||
|
||||
#### Plain mountpoints
|
||||
### Plain Mount Points
|
||||
|
||||
One of main differences of mount-v2 comparing to original is that mounts are initially created "plain", for instance if we had **MOUNT** with **mnt_id=1000** and **ns_mountpoint="/mount/point/path"**, original mount engine would originally mount this **MOUNT** in the mount tree to **<criu_root_yard>/<mntns>/mount/point/path** so that if this mount had **PARENT** mount with **mnt_id=999** and **ns_mountpoint="/mount/point"** corresponding mount for **PARENT** would be created in **<criu_root_yard>/<mntns>/mount/point** thus restoring parent-child relationship between them initially. For mount-v2 **MOUNT** would be first mounted to **<criu_root_yard>/mnt-1000** and **PARENT** would be mounted to **<criu_root_yard>/mnt-999** so that on the first stage we only create mounts and then on separate second stage handle the tree assembling separately. This way we can have useful heuristics like on the second stage we can create overmounts after mounts they overmount, and on the first stage we can create external mounts before their bindmounts and these two do not clinch with each other.
|
||||
A key difference in Mount-v2 is that mounts are initially created as "plain" mounts. In the original engine, a mount with `mnt_id=1000` at `/mount/point/path` would be mounted directly into the target tree (e.g., `<root_yard>/<mntns>/mount/point/path`). This required the parent mount to exist beforehand.
|
||||
|
||||
But it is not so simple actually because we do not want to rewrite all the code for instance for restoring mount content or restoring ghost and remap files, which used mountpoint paths in "tree" format. So in all places where it does not matter (where we do not access <criu_root_yard>/<mntns>/... paths) we switched from using mount_info->mountpoint to mount_info->ns_mountpoint and in all places where we actually needed "tree" format paths we replace them with service_mountpoint() helper which would return "tree" paths for original mount engine and "plain" paths for mount-v2. This way we can safely switch from one to another.
|
||||
In Mount-v2, this mount is first created at a flat location like `<root_yard>/mnt-1000`. This allows the tree assembly to be handled as a separate second stage. This separation enables useful heuristics, such as creating over-mounts after the mounts they cover, or creating external mounts before their corresponding bind mounts, without causing conflicts.
|
||||
|
||||
#### Resolving sharing groups
|
||||
To maintain compatibility with existing code that expects a tree-like structure (e.g., for restoring file content or ghost files), CRIU uses a `service_mountpoint()` helper. This helper returns traditional "tree" paths for the original engine and "plain" paths for Mount-v2.
|
||||
|
||||
Just after reading mounts from images in read_mnt_ns_img() when mount-v2 is enabled we have an additional step to collect sharing group information from mounts and turn it to sharing groups forest graph (resolve_shared_mounts_v2). First, we just walk over all mounts and create sharing group for each mount with unique shared_id + master_id pair, also we sew all mounts to corresponding sharing group with same id pair. Second, we walk over all sharing groups which has non-zero master_id and lookup the corresponding parent sharing groups and connect them with a tree.
|
||||
### Resolving Sharing Groups
|
||||
|
||||
There is also a case when master_id is non-zero but there is no corresponding parent sharing group, this means that outside of dumped container there is mount with matching shared_id - external slavery detected. For this case we just collect sibling sharing groups in list with empty parent link. Also we detect source path from which the master_id would be inherited either from some mountpoint-external mount or from root container mount.
|
||||
When Mount-v2 is enabled, CRIU takes an additional step after reading mount images to resolve sharing group information (`resolve_shared_mounts_v2`).
|
||||
1. CRIU iterates through all mounts and creates a sharing group for each unique `shared_id` + `master_id` pair.
|
||||
1. Groups with a non-zero `master_id` are linked to their respective parent sharing groups to form a tree.
|
||||
1. If a `master_id` has no corresponding parent group within the container, CRIU detects this as external slavery and identifies the source path (either an external mount or the root container mount).
|
||||
|
||||
#### Actual restore of mounts
|
||||
### Actual Restore Process
|
||||
|
||||
Actual restore of mounts in original mount engine starts with prepare_mnt_ns() function, when mount-v2 is enabled we pass controll from it to prepare_mnt_ns_v2() instead. It consists of several stages:
|
||||
When Mount-v2 is enabled, `prepare_mnt_ns()` delegates to `prepare_mnt_ns_v2()`, which follows these stages:
|
||||
|
||||
1) We pre-create mount namespaces for each restored mount namespace in pre_create_mount_namespaces(). These namespaces appear almost empty: they contain tmpfs as their root, they have root yard path created in it with another tmpfs mounted in it, and"namespace" path for assembling tree of mounts in it created in corresponding subdirectory of root yard mount. Surely we also save nsfs fds to each mount namespace to be able to reenter them later.
|
||||
1. **Pre-create Namespaces**: Namespaces are initially created almost empty, containing only `tmpfs` at the root and a "root yard" for assembling the mount tree. CRIU preserves `nsfs` file descriptors to re-enter these namespaces.
|
||||
1. **Populate Namespaces**: CRIU iterates through the mount tree. Using `can_mount_now_v2()`, it skips mounts that depend on others (like bind mounts whose source is not yet ready) and restarts the walk as needed.
|
||||
1. **Detecting Directories**: For each new mount, CRIU determines if it is a directory or a file by performing a `stat` on its parent "plain" mount point.
|
||||
1. **Create Plain Mount Points**: CRIU creates an empty file or directory to serve as the "plain" mount point.
|
||||
1. **Create New Mounts**: CRIU creates the actual mount. This could be a new device, a bind of the container root, or an external bind mount. This process is simpler than the original engine because sharing group inheritance is not a concern.
|
||||
1. **Advanced Bind Mounts**: `do_bind_mount_v2()` uses `open_tree()` and `move_mount()` to perform bind mounts without traversing symlinks or `autofs` points.
|
||||
1. **Cross-namespace Binding**: The new mount is bind-mounted into the target namespace at the same "plain" location, ensuring it is visible and accessible for subsequent steps (like restoring UNIX sockets).
|
||||
1. **Set Unbindable**: Once bind mounts are complete, mounts are marked as unbindable.
|
||||
1. **Assemble Trees**: CRIU moves mounts from their "plain" locations into their final positions within the mount tree. It opens file descriptors for each mount point to allow file access later.
|
||||
1. **Restore Sharing Groups**: Finally, CRIU restores sharing groups across the assembled forest using `restore_mount_sharing_options()`. It traverses the sharing group trees and applies the correct sharing settings (e.g., making a mount a slave or shared).
|
||||
1. **Cleanup**: CRIU removes the temporary "service" mount points for deleted mounts.
|
||||
|
||||
2) In populate_mnt_ns_v2() we reuse mnt_tree_for_each() walk over mount tree from original mount engine and so we walk mounts in tree order with addition of temporary skipping mounts and their descendants with can_mount_now_v2() in case they depend from other mounts, restarting the walk for them later. The can_mount_now_v2() is basically skipping mounts which should be restored as bindmounts but their source is not ready yet, this is true for bindmounts of root, external or plugin mounts or non-fsroot mounts.
|
||||
|
||||
3) In the mentioned walk over mounts forest in do_mount_one_v2() we determine if the newly created mount is directory one or a file one in detect_is_dir(), we just open its mountpoint path relative to parent "plain" mountpoint and do stat. That's why it is important to use mnt_tree_for_each() as it insures that parent is already "plain" mounted.
|
||||
|
||||
4) In the mentioned walk over mounts forest in do_mount_one_v2() we create "plain" mountpoint for a new mount, empty file or directory based on the previous step.
|
||||
|
||||
5) In the mentioned walk over mounts forest in do_mount_one_v2() we actually create new mount, either we create completely new mount or device-external in do_new_mount_v2() if it's supported, or bind container root mount in do_mount_root_v2() from the still visible host mount tree, or bind mountpoint-external mount in do_bind_mount_v2() and similarly bind any mount for which superblock is already created by other mount beforehand and we can just bind it in do_bind_mount_v2(). These functions act similar to ones in original mount engine but simplified as they don't need to care about inheriting sharing groups.
|
||||
|
||||
6) The do_bind_mount_v2() is improved to do bindmount via open_tree() + move_mount() with flags allowing not to traverse symlinks or autofs mounts.
|
||||
|
||||
7) Also we cross-namespace bindmount the newly created mount to restored mount namespace to the same "plain" mountpoint in do_mount_in_right_mntns(). So that we initially have a mount which would be visible after restore, this would be required in future to be able to restore bindmounted unix sockets on the right mount.
|
||||
|
||||
8) Now after the walk we don't plan to do bindmounts anymore so we set unbindable flags on mounts.
|
||||
|
||||
9) Next we assemble mount trees in each restored mount namespace in assemble_mount_namespaces() by again reusing move_mount_to_tree() to have tree order of moving mounts into proper places in mount tree. Also we open fds on the mountpoint: one mp_fd_id before moving and another mnt_fd_id after, so that we can access files on each mount later from final mntns via those fds.
|
||||
|
||||
10) Finally we do restore sharing groups on the assembled mount forest in restore_mount_sharing_options(). It walks each root sharing group and their descendants with dfs tree walk. It creates sharing for the first mount in the sharing group and then sets the same sharing on all other mounts in this group.
|
||||
|
||||
Sharing creation for first mount is two step:
|
||||
|
||||
a) If mount has master_id we either copy shared_id from parent sharing group or from external source and then make mount slave thus converting it to right master_id.
|
||||
b) Next if mount has shared_id we just make us shared, creating right shared_id.
|
||||
|
||||
We need to use userns_call() for MOVE_MOUNT_SET_GROUP to have all right permissions for copying sharing (move_mount_set_group()). Also we need to resolve external paths given by user to their actual mountpoint, we do so with openat2(RESOLVE_NO_XDEV) in resolve_mountpoint, this also only works from userns_call().
|
||||
|
||||
11) We remove sources of deleted mounts making them actually deleted (from "service" mount namespace), as moving deleted mounts is not allowed and just to simplify things we do it at the last step.
|
||||
|
||||
#### Links
|
||||
|
||||
- "Virtuozzo" (original) version (using non-mainstream kernel interface): [Mounts-v2-Virtuozzo](mounts-v2-virtuozzo.md) It actually has cool features we don't have in mainstream yet, for instance - nested pidns proc handling, this feature requires nested pidns support beforehand.
|
||||
|
||||
- MOVE_MOUNT_SET_GROUP kernel feature: [torvalds/linux@9ffb14e](https://github.com/torvalds/linux/commit/9ffb14ef61bab83fa818736bf3e7e6b6e182e8e2)
|
||||
|
||||
- Mount-v2 PR to criu: [#1721](https://github.com/checkpoint-restore/criu/pull/1721)
|
||||
## Links
|
||||
|
||||
- **Virtuozzo (Original) Version**: [Mounts-v2-Virtuozzo](mounts-v2-virtuozzo.md). (Requires specific kernel support).
|
||||
- **Kernel Feature**: [`MOVE_MOUNT_SET_GROUP` commit](https://github.com/torvalds/linux/commit/9ffb14ef61bab83fa818736bf3e7e6b6e182e8e2).
|
||||
- **CRIU Pull Request**: [PR #1721](https://github.com/checkpoint-restore/criu/pull/1721).
|
||||
|
|
|
|||
|
|
@ -1,105 +1,76 @@
|
|||
# Mounts-v2-Virtuozzo
|
||||
|
||||
Mounts v2 CRIU algorithm
|
||||
CRIU Mounts-v2 Algorithm (Virtuozzo version)
|
||||
|
||||
This algorithm is designed to overcome problems with sharing group restore, overmounted files, mounts with namespace tags and some more smaller problems.
|
||||
This algorithm is designed to resolve issues with restoring sharing groups, over-mounted files, mounts with namespace tags, and other minor issues.
|
||||
|
||||
(assume single userns for now)
|
||||
### 1. Mount Image Read Stage (`read_mnt_ns_img_v2`)
|
||||
- Read `mount_infos` from images for each mount namespace into lists.
|
||||
- Build mount trees for each namespace.
|
||||
- Group mounts by superblock equality into "bind" lists.
|
||||
- **Prepare Sharing Groups**:
|
||||
- Group mounts into shared groups based on `master_id` and `shared_id` equality.
|
||||
- Organize shared groups into a tree where `parent->shared_id == child->master_id`.
|
||||
- If two groups have the same `master_id`, make them siblings.
|
||||
- **Set Up "Internal Yards"**:
|
||||
- Use a writable namespace root to create `/internal-yard-XXXXXX`.
|
||||
- This is used for the mounting stage after forking tasks.
|
||||
- **Prepare Nested PID Namespace procfses**:
|
||||
- Copy namespace tags across bind lists.
|
||||
- Create helpers for descendants of nested PID namespace `procfses` in the internal yard.
|
||||
- **Prepare "Root Yard"**:
|
||||
- Create a helper mount at `/tmp/.criu.mntns.XXXXXX/`.
|
||||
- Merge mount trees from all namespaces as subdirectories of the root yard.
|
||||
|
||||
- Mounts image read stage (read_mnt_ns_img + read_mnt_ns_img_v2)
|
||||
-* Read mount_infos from images for each mount namespace to lists (collect_mnt_from_image)
|
||||
-* Put mounts to trees for each mount namespace (mnt_build_tree)
|
||||
-* Group mounts by superblock equality into "bind" lists (search_bindmounts)
|
||||
-* Prepare sharing groups
|
||||
-** Group mounts into shared group by equality of (master_id + shared_id)
|
||||
-** Put shared groups in tree where parent->shared_id == child->master_id
|
||||
-** If two groups has same master_id, make them siblings (even if no parent)
|
||||
-* Prepare "internal yard" mount_info aside (setup_internal_yards)
|
||||
-** ns mountpoint "/internal-yard-XXXXXX"
|
||||
-** will require writable namespace root mount
|
||||
-** needed for mount stage after forking tasks
|
||||
-* Prepare nested pidns procfses
|
||||
-** Copy namespace tag across "bind" list (search_nested_pidns_proc)
|
||||
-** Create helpers for descendants of nested pidns procfses in "internal yard" (handle_nested_pidns_proc)
|
||||
-*** These helpers get root "/" for simplicity (deleted, file/dir)
|
||||
-**** no nsfs bind support
|
||||
-*** ns mountpoint "/internal-yard-XXXXXX/hlp-[mnt_id]
|
||||
-* Prepare "root yard"
|
||||
-** helper mount with mountpoint "/tmp/.criu.mntns.XXXXXX/"
|
||||
-** Merge mount trees of all mount namespaces as subdirectories of "root yard" (merge_mount_trees)
|
||||
-*** mountpoint "/tmp/.criu.mntns.XXXXXX/[nns_id]"
|
||||
### 2. First Mounting Stage (Before forking processes)
|
||||
Executed from the init task in the "service" mount namespace (`prepare_mnt_ns_v2`):
|
||||
- Create and mount the "root yard".
|
||||
- Replace mounts for the post-fork stage by inserting internal yards and removing nested PID namespace `procfses`.
|
||||
- **Walk the Merged Mount Tree**:
|
||||
- Mount all mounts "plain" and "private".
|
||||
- Check if a mount can be created (e.g., overlay, root, external, bind).
|
||||
- Create plain mount points (detecting file vs. directory via `stat`).
|
||||
- Bind mounts to the already mounted superblock or external sources.
|
||||
- Handle internal yards by mounting `tmpfs` and creating child mount points.
|
||||
- This stage allows for "cross-namespace" bind mounts by maintaining all mounts within a single service namespace.
|
||||
|
||||
- Mounting, first stage (before forking processes) from init task in "service" mntns (prepare_mnt_ns_v2)
|
||||
-* Actually create and mount "root yard" (populate_mnt_ns_v2 -> populate_roots_yard_v2)
|
||||
-* Replace mounts for after forking tasks stage (insert_internal_yards)
|
||||
-** Delete nested pidns procfses from tree
|
||||
-** Insert internal yards with helpers to tree
|
||||
-* Walk the merged mount tree (mnt_tree_for_each) parents before children
|
||||
-** Mount all mounts "plain" (do_mount_one_v2)
|
||||
-*** check mount can be mounted (can_mount_now_v2) e.g. for overlay, root, external, bind or nsfs
|
||||
-*** create mountpoint "/tmp/.criu.mntns.XXXXXX/mnt-[mnt_id]" (create_plain_mountpoint)
|
||||
-**** dir/file detected by stat on mountpoint
|
||||
-*** Mount all mounts private and "plain"
|
||||
-**** just mount a new mount (do_new_mount_v2)
|
||||
-***** setup as bind source for other mounts of this super block (propagate_mount_v2)
|
||||
-**** bind if superblock is already mounted or external or root (do_bind_mount_v2, do_mount_root_v2)
|
||||
-***** create sources for "deleted" bind mounts and leave it for now
|
||||
-**** Handle internal yard (do_internal_yard_mount_v2)
|
||||
-***** mount tmpfs
|
||||
-***** create mountpoints for children
|
||||
-***** mount host's proc helper mount inside
|
||||
-*** Exept for "plain", "private" and helpers from "internal yard" we restore each mount as it should be in the final mountns (all flags and options applied)
|
||||
-** This mounting all the mounts from all final mount namespaces in a single service mount namespace allows us to do "cross-namespace" bindmounts
|
||||
### 3. Second Mounting Stage ("Plain" to "Tree" transition)
|
||||
- For each restored mount namespace:
|
||||
- Perform `unshare(CLONE_NEWNS)`.
|
||||
- Move mounts from "plain" locations into their final tree positions.
|
||||
- Open and save file descriptors for the mount points (`mp_fd`) and the mounts themselves (`mnt_fd`).
|
||||
- Perform `pivot_root()` to the namespace's root, leaving only the intended mounts.
|
||||
- Extract internal yards and restore `procfses`.
|
||||
- Remove temporary sources of deleted mounts.
|
||||
|
||||
- Mounting, second stage ("plain" to "tree" mount) (prepare_mnt_ns_v2)
|
||||
-* Walk across all mount namespaces
|
||||
-** unshare(CLONE_NEWNS)
|
||||
-** Walk all mounts belonging to this mntns (tree order) (assemble_tree_from_plain_mounts)
|
||||
-*** mountpoint "/tmp/.criu.mntns.XXXXXX/[nns_id]/[ns_mountpoint]"
|
||||
-*** Open mountpoint fd before moving mount to it and save (mp_fd)
|
||||
-*** Move (MS_MOVE) mount to the tree
|
||||
-*** Open mount fd (root dentry on a mount) (mnt_fd)
|
||||
-** Pivot root to ""/tmp/.criu.mntns.XXXXXX/[nns_id]"
|
||||
-*** leaving only mounts which should be in this mntns
|
||||
-* Extract "internal yard"s from the tree and put back procfses and their ancestors (extract_internal_yards)
|
||||
-* Remove sources of deleted mounts making them really "deleted" from "service" mntns (remove_sources_of_deleted_mounts)
|
||||
### 4. Forking Stage
|
||||
- Fork all processes in tree order.
|
||||
- Recreate PID namespaces.
|
||||
- Enter the correct mount namespace.
|
||||
- Map files from the mounted filesystem to restore COW mappings.
|
||||
- Fork children.
|
||||
|
||||
- Forking stage: fork all processes (tree order)
|
||||
-* Inits also creat pid namespaces
|
||||
-* Enter mount namespace
|
||||
-* Mmap files from mounted filesystem to restore COW mappings
|
||||
-** We assume here that we don't have file mappings on delayed mounts else we can't handle it
|
||||
-** Ghost/Link remaps may be created here
|
||||
-* Fork children
|
||||
### 5. Third Mounting Stage (After forking processes)
|
||||
Executed from the main CRIU task (`fini_restore_mntns_v2`):
|
||||
- Enter the container's user namespace.
|
||||
- For each mount namespace:
|
||||
- Fix up nested PID namespace `procfses` by entering the tagged PID namespace and mounting `procfs`.
|
||||
- Walk the mount tree and bind any remaining mounts from internal yard helpers.
|
||||
- Open final `mnt_fd` and `mp_fd` descriptors.
|
||||
- Unmount and remove internal yards.
|
||||
|
||||
- Mounting, third stage (after forking processes) (from main criu task) (__fini_restore_mntns_v2)
|
||||
-* Enter CT userns (fini_restore_mntns_v2)
|
||||
-* For each mount namespace
|
||||
-** For each procfs of this mntns (fixup_nested_pidns_proc)
|
||||
-*** Enter tagged pidns
|
||||
-*** Mount procfs from it in "internal yard"
|
||||
-** Walk the mount tree of each mntns and mount all yet not mounted mounts to the tree
|
||||
-*** Find the mountpoint for the mount via mnt_fd of parent and mp_fds of sibling overmounts
|
||||
-*** Bind the mount to it from the internal yard helper or procfs helper
|
||||
-**** via /proc/self/fd/<id> on hosts proc in "internal yard"
|
||||
-*** Also open mnt_fd and mp_fd for a new mount (before and after bind)
|
||||
-** Umount and rmdir "internal yard"
|
||||
### 6. Final Stage
|
||||
- **Restore Sharing Groups**:
|
||||
- Use `mnt_fd` to access mounts.
|
||||
- Walk sharing group trees (parents before their children).
|
||||
- For the first mount in a group:
|
||||
- If it is a slave, find its parent group or external source and copy sharing via `MS_SET_GROUP`.
|
||||
- If it is shared, establish the shared group.
|
||||
- For other mounts in the group, copy the sharing settings from the first mount.
|
||||
|
||||
- And finally
|
||||
-* Restore sharing groups for each mount (use mnt_fd to access mounts) (restore_mount_sharing_options)
|
||||
-** Walk sharing group trees (parents before children)
|
||||
-*** Setup first (any) mount in a group
|
||||
-**** Is slave
|
||||
-***** Find any mount from parent sg or find external mount source
|
||||
-***** Copy sharing from it with MS_SET_GROUP
|
||||
-***** Make slave
|
||||
-**** Is shared - make it also shared
|
||||
-*** Setup other mounts - copy sharing from the first one
|
||||
|
||||
- Done
|
||||
|
||||
Here are links to mounts-v2 implementation in Virtuozzo criu:
|
||||
- Main part: https://src.openvz.org/projects/OVZ/repos/criu/commits?until=v3.12.3.12
|
||||
- Delayed proc part: https://src.openvz.org/projects/OVZ/repos/criu/commits?until=v3.12.5.13
|
||||
- Kernel patch for MS_SET_GROUP: https://lore.kernel.org/lkml/1485214628-23812-1-git-send-email-avagin@openvz.org/
|
||||
---
|
||||
|
||||
### Links
|
||||
- **Main Implementation**: [Virtuozzo CRIU commits](https://src.openvz.org/projects/OVZ/repos/criu/commits?until=v3.12.3.12)
|
||||
- **Delayed Proc Support**: [Virtuozzo CRIU commits](https://src.openvz.org/projects/OVZ/repos/criu/commits?until=v3.12.5.13)
|
||||
- **Kernel Patch for `MS_SET_GROUP`**: [Linux Kernel Mailing List](https://lore.kernel.org/lkml/1485214628-23812-1-git-send-email-avagin@openvz.org/)
|
||||
|
|
|
|||
|
|
@ -1,85 +1,68 @@
|
|||
# Optimizing pre dump algorithm
|
||||
# Optimizing the Pre-dump Algorithm
|
||||
|
||||
This article describes the implementation of optimized pre-dumping algorithm in CRIU.
|
||||
This project is completed under the [GSoC 2019 program](https://summerofcode.withgoogle.com/projects/#6174473131130880).
|
||||
This article describes the implementation of an optimized pre-dumping algorithm in CRIU. This project was completed as part of the [GSoC 2019 program](https://summerofcode.withgoogle.com/projects/#6174473131130880).
|
||||
|
||||
## Problems in existing Pre-dump
|
||||
## Problems with the Existing Pre-dump
|
||||
|
||||
Previously during pre-dump, target process needs to be frozen till all the memory pages are drained into pipes. Then the target process gets unfrozen and pages collected into pipes are written into image files at the end of pre-dump. This approach has two problems. First, target process remains frozen for longer duration. Second, pipes induce memory pressure in the system. If memory utilization during pre-dump is nearly equal to system's memory, then this risks running into out-of-memory failures as the pipe pages are not reclaimable.
|
||||
Previously, during a pre-dump, the target process had to remain frozen until all memory pages were drained into pipes. These pages were only written to image files at the end of the pre-dump. This approach had two major drawbacks:
|
||||
1. The target process remained frozen for a significant duration.
|
||||
2. The pipes created significant memory pressure. If memory usage during the pre-dump approached the system's total capacity, it risked triggering out-of-memory (OOM) failures, as pipe pages are non-reclaimable.
|
||||
|
||||
## Solution
|
||||
The optimized implementation solves above mentioned two issues of pre-dumping. Here, the target process needs to be frozen only till memory mappings are collected. Then the process will unfreeze and continue. Draining of pages from process happens while the process is running. We use [process_vm_readv](http://man7.org/linux/man-pages/man2/process_vm_readv.2.html) syscall to drain pages from process to user-space buffer by using memory mappings collected earlier. Since draining of pages and process execution happen simultaneously, there is a possibility that the process might modify memory mappings after they have been collected, in which case process_vm_readv will encounter the old mapping. This race needs to be handled on the fly for process_vm_readv to successfully drain complete iovec.
|
||||
## The Solution
|
||||
|
||||
## Design issues
|
||||
The optimized implementation addresses these two issues. Now, the target process is only frozen until its memory mappings are collected. Once collected, the process is unfrozen and resumes execution. The draining of pages from the process occurs while the process is running, using the [`process_vm_readv`](http://man7.org/linux/man-pages/man2/process_vm_readv.2.html) system call to copy data into a userspace buffer.
|
||||
|
||||
The following discussion covers the possibly faulty-iov locations in an iovec, which hinders process_vm_readv from dumping the entire iovec in a single invocation.
|
||||
Because page draining and process execution happen simultaneously, the process might modify its memory mappings (e.g., unmapping a region) after CRIU has collected them. This race condition must be handled on the fly to allow `process_vm_readv` to complete the transfer.
|
||||
|
||||
`**NOTE:**` *For easy representation and discussion purpose, we carry out further discussion at "page granularity". `length_in_bytes` will represent page count in iov instead of byte count. Same assumption applies for the syscall's return value. Instead of returning the number of bytes read, it returns a page count.*
|
||||
## Design Considerations
|
||||
|
||||
Consider memory layout of target process:
|
||||
The following sections discuss how to handle "faulty" locations within an `iovec` that prevent `process_vm_readv` from processing the entire vector in a single call.
|
||||
|
||||
**Note**: For simplicity, the following discussion uses "page granularity." `length_in_bytes` represents the page count, and the syscall's return value reflects the number of pages successfully read.
|
||||
|
||||
Consider the memory layout of a target process:
|
||||
|
||||

|
||||
|
||||
Single `iov` representation: `{starting_address, length_in_bytes}`. An iovec is array of iov-s.
|
||||
For above memory mapping, generated iovec: `{A,1}{B,1}{C,4}`
|
||||
A single `iov` is represented as `{starting_address, page_count}`. For the layout above, the `iovec` would be: `{A, 1}, {B, 1}, {C, 4}`.
|
||||
|
||||
This iovec remains unmodified once generated. At the same time some of the memory regions listed in iovec may get modified (unmap/change protection) by the target process while process_vm_readv is reading iovec regions.
|
||||
While this `iovec` is static once generated, the target process may unmap or change the protection of these regions while `process_vm_readv` is active.
|
||||
|
||||
- **Case 1:**
|
||||
|
||||
`A` is unmapped, `{A,1}` become faulty iov
|
||||
### Case 1: The first region is unmapped
|
||||
If region `A` is unmapped, `{A, 1}` becomes a faulty `iov`.
|
||||
|
||||

|
||||
|
||||
process_vm_readv will return -1. Increment start pointer(2), syscall will process `{B,1}{C,4}` in one go and copy 5 pages to userbuf from iov-B and iov-C.
|
||||
`process_vm_readv` will return `-1`. By incrementing the start pointer, the next call will process `{B, 1}, {C, 4}` and successfully copy 5 pages.
|
||||
|
||||
- **Case 2:**
|
||||
|
||||
`B` is unmapped, `{B,1}` become faulty iov
|
||||
### Case 2: A middle region is unmapped
|
||||
If region `B` is unmapped, `{B, 1}` becomes faulty.
|
||||
|
||||

|
||||
|
||||
process_vm_readv will return 1, i.e. page A copied to userbuf successfully and syscall stopped, since B got unmapped. Increment the start pointer to C(2) and invoke syscall. Userbuf contains 5 pages overall from iov-A and iov-C.
|
||||
`process_vm_readv` will return `1`, indicating page `A` was successfully copied before the syscall encountered the unmapped region `B`. CRIU then increments the pointer past `B` and resumes with region `C`.
|
||||
|
||||
- **Case 3:**
|
||||
|
||||
This case deals with partial unmapping of iov representing more than one pagesize region. The syscall can't process such faulty iov as whole. So we process such regions part-by-part and form new sub-iovs in aux_iov from successfully processed pages.
|
||||
|
||||
- **Part 3.1:**
|
||||
|
||||
First page of `C` is unmapped
|
||||
### Case 3: Partial unmapping of a large region
|
||||
If a large region (e.g., `C`) is partially unmapped, `process_vm_readv` cannot process the faulty `iov` as a whole. CRIU must process these regions part-by-part.
|
||||
|
||||
**Part 3.1: The first page of a region is unmapped**
|
||||

|
||||
`process_vm_readv` returns `2` (pages `A` and `B` copied). CRIU identifies that `iov-C` is larger than one page and introduces a "dummy-iov" `{C+1, 3}` to attempt to copy the remaining pages of the region.
|
||||
|
||||
process_vm_readv will return 2, i.e. pages A and B copied. We identify length of iov-C is more than 1 page, that is where this case differs from Case 2.
|
||||
|
||||
dummy-iov is introduced(2) as: `{C+1,3}`. dummy-iov can be directly placed at next page to failing page. This will copy
|
||||
remaining 3 pages from iov-C to userbuf. Finally create modified iov entry in aux_iov. Complete aux_iov look like:
|
||||
|
||||
`aux_iov: {A,1}{B,1}{C+1,3}*`
|
||||
|
||||
- **Part 3.2:**
|
||||
|
||||
In between page of `C` is unmapped, let's say third page
|
||||
|
||||
**Part 3.2: A page in the middle of a region is unmapped**
|
||||

|
||||
`process_vm_readv` returns `4` (A, B, and the first two pages of C copied). CRIU calculates the `partial_read_byte` count and creates a dummy-iov `{C+3, 1}` to skip the faulty page and copy the remainder of region `C`.
|
||||
|
||||
process_vm_readv will return 4, i.e. pages A and B copied completely and first two pages of C are also copied.
|
||||
## Limitations
|
||||
|
||||
Since, iov-C is not processed completely, we need to find `partial_read_byte` count to place out dummy-iov for remainig processing of iov-C. This function is performed by analyze_iov function.
|
||||
Only memory regions with `PROT_READ` protection can be pre-dumped. `process_vm_readv` cannot access regions lacking this flag. Non-readable regions are deferred to the final dump stage. If a process has a large number of such pages, the benefits of this optimized pre-dump are reduced.
|
||||
|
||||
dummy-iov will be(2): `{C+3,1}`. dummy-iov will be placed next to first failing address to process remaining iov-C.
|
||||
New entries in aux_iov will look like:
|
||||
## Invocation
|
||||
|
||||
`aux_iov: {A,1}{B,1}{C,2}*{C+3,1}*`
|
||||
The `--pre-dump-mode` option allows users to select the algorithm.
|
||||
- `splice`: Traditional parasite-based pre-dumping (default).
|
||||
- `read`: Optimized pre-dumping using `process_vm_readv`.
|
||||
|
||||
## What can't be pre-dumped
|
||||
The memory regions of the target process that have `PROT_READ` protection can only be pre-dumped. The syscall process_vm_readv can't process a memory region which lacks `PROT_READ` flag.
|
||||
All non-`PROT_READ` memory regions are delegated to dump stage. If some process has large number of non-`PROT_READ` pages, then this pre-dump method is not suitable as it increases load on the dump stage.
|
||||
|
||||
## How to invoke optimized pre-dump
|
||||
`--pre-dump-mode` option is added to specify the desired algorithm to be used for pre-dump. "splice" mode executes traditional parasite based pre-dumping. The "read" mode is optimized one and uses process_vm_readv for pre-dumping. "splice" is set as the default.
|
||||
|
||||
## Scope for more optimization
|
||||
- Processing a partially read iov could be costly, when size of partially read iov is huge and processing is done page-by-page until next mapped region is encountered.
|
||||
## Future Optimization
|
||||
|
||||
Processing partially read `iov`s can become expensive if the region is very large and CRIU must iterate page-by-page to find the next valid mapping.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
# Pagemap cache
|
||||
|
||||
Checkpointing processes with large number of small virtual memory areas (VMAs) can lead to significant performance overhead. This overhead is primarily due to the frequent reading of VMA information from `/proc/[pid]/pagemap`, as described in [Memory dumps](memory-dumps.md). To mitigate this problem, CRIU uses a pagemap cache (pmc) to reduce the frequency of `pread` calls by caching information about VMAs.
|
||||
|
||||
# Pagemap Cache
|
||||
|
||||
Checkpointing processes with a large number of small virtual memory areas (VMAs) can lead to significant performance overhead. This is primarily due to the frequent reading of VMA information from `/proc/$pid/pagemap`, as described in [Memory dumps](memory-dumps.md). To mitigate this, CRIU utilizes a pagemap cache (PMC) that caches VMA information, thereby reducing the number of required `pread` calls.
|
||||
|
|
|
|||
|
|
@ -1,60 +1,54 @@
|
|||
# Parasite code
|
||||
# Parasite Code
|
||||
|
||||
## Overview
|
||||
Parasite code is a binary blob of code built in [PIE](http://en.wikipedia.org/wiki/Position-independent_code) format for execution inside another process address space. The main and only purpose of the parasite code is to execute CRIU service routines inside dumpee tasks address space.
|
||||
Parasite code is a binary blob compiled in Position-Independent Executable ([PIE](http://en.wikipedia.org/wiki/Position-independent_code)) format for execution within the address space of another process. Its primary purpose is to execute CRIU service routines within the context of the dumpee tasks.
|
||||
|
||||
## Using the parasite
|
||||
## Using the Parasite
|
||||
|
||||
All architecture independent code calling for parasite service routines is sitting in `parasite-syscall.c` file. When we need to run parasite code inside some dumpee task we:
|
||||
The architecture-independent logic for calling parasite service routines is located in `parasite-syscall.c`. To run parasite code within a dumpee task:
|
||||
|
||||
1. Move task into that named seized state with `ptrace(PTRACE_SEIZE, …)` helper (thus task get stopped but does not notice that someone outside is trying to manipulate it).
|
||||
1. Inject and execute mmap syscall inside dumpee address space with help of `ptrace` system call, because we need to allocate a shared memory area which will be used for parasite stack and parameters exchange between CRIU and dumpee.
|
||||
1. Open local copy of shared memory space from */proc/$PID/map_files/*, where **$PID** is process identificator of a dumpee.
|
||||
1. The task is moved into a "seized" state using `ptrace(PTRACE_SEIZE, ...)`. This stops the task without it perceiving external manipulation.
|
||||
1. An `mmap` syscall is injected and executed within the dumpee's address space via `ptrace`. This allocates a shared memory area for the parasite's stack and for parameter exchange between CRIU and the dumpee.
|
||||
1. CRIU opens its own local copy of this shared memory via `/proc/$PID/map_files/`.
|
||||
|
||||
All these actions are gathered in `parasite_infect_seized()` helper. Once parasite is prepared and placed into dumpee address space, CRIU can call for parasite service routines.
|
||||
These actions are coordinated by the `parasite_infect_seized()` helper. Once the parasite is positioned, CRIU can invoke its service routines.
|
||||
|
||||
There are two modes the parasite can operate in:
|
||||
The parasite operates in two modes:
|
||||
|
||||
1. Trap mode
|
||||
1. Daemon mode
|
||||
1. **Trap Mode**: The parasite executes a single command and then yields via a CPU trap instruction, which CRIU intercepts. This is a one-command-at-a-time execution mode.
|
||||
1. **Daemon Mode**: The parasite acts like a UNIX daemon. It opens a UNIX socket and listens for commands. Upon receiving a command, it processes it and returns the result via a socket packet. The daemon then resumes listening for subsequent commands. Supported commands are defined in the `PARASITE_CMD_...` enum in `parasite.h`.
|
||||
|
||||
In trap mode parasite simply executes one command and yields cpu trap instruction which CRIU intercepts. This is like one command at a time mode.
|
||||
## Internal Structure
|
||||
|
||||
In daemon mode (as name implies) parasite behaves like a unix daemon - it opens a unix socket and start listening for commands on it. Once a command is received, it is handled and the daemon returns the result back via a socket packet. The daemon continues listening for the subsequent commands to execute. All currently known commands are assembled as `PARASITE_CMD_…` enum in `parasite.h` header.
|
||||
The parasite consists of the following functional blocks:
|
||||
|
||||
## Parasite internal structure
|
||||

|
||||
|
||||
Internally parasite might be represented as following blocks
|
||||
The bootstrap code is written in architecture-specific assembly (x86, ARM, ARM64), while the parasite daemon is common across architectures and written in C.
|
||||
|
||||

|
||||
The **sigframe** (signal frame) block deserves special mention. Its purpose is to handle the `rt_sigreturn()` system call, which is used to restore the victim's original execution context (registers, etc.) after the parasite's work is complete. It is prepared by the caller using the register values the victim had at the moment of injection.
|
||||
|
||||
Parasite bootstrap code written in assembly language, which is specific for every architecture (x86, arm, arm64), in turn parasite daemon is common for all architectures and written in C language. We consider only x86 architecture here but other architectures have the similar approach.
|
||||
### Parasite Bootstrap
|
||||
|
||||
While all blocks on the image above are self descriptive the sigframe (signal frame) block requires some comments. Its main purpose is to handle `rt_sigreturn()` system call which we use to restore victim’s execution context (registers and etc). It should be prepared by caller setting up original registers values and etc the victim has at the moment of parasite injection.
|
||||
The bootstrap code resides in `parasite-head.S`. It adjusts its own stack and calls the daemon's entry point. Immediately following the call is a trapping instruction that notifies the caller when the parasite has finished its work (when in trap mode).
|
||||
|
||||
### Parasite bootstrap
|
||||
### Parasite Daemon
|
||||
|
||||
Parasite bootstrap lives in `parasite-head.S` file and simply adjusts own stack and literally call the daemon entry point. Right after the call there is trapping instruction placed which triggers the notification to a caller that parasite has finished its work if been running in trap mode. When parasite is running in daemon mode the notifications are a bit more complex and will be considered later.
|
||||
The daemon code is located in `pie/parasite.c`, with `parasite_daemon()` as its entry point. Upon starting, it opens a command socket to communicate with CRIU. The daemon then waits for commands.
|
||||
|
||||
### Parasite daemon
|
||||

|
||||
|
||||
Parasite daemon code lives in `pie/parasite.c` file. Its entry point is `parasite_daemon()`. Upon enter it opens command socket which is used to communicate with the caller. Once socket is opened the daemon comes to sleep waiting for command to appear.
|
||||
Since the parasite memory block is a shared memory slab, data exchange between CRIU and the dumpee is performed via standard read/write operations in the arguments area, while commands are transmitted as network packets.
|
||||
|
||||

|
||||
## Removing Parasite Code from the Dumpee
|
||||
|
||||
Because the whole parasite memory block is a shared memory slab data exchange between CRIU and dumpee is regular read/write operations into arguments area while commands are sent as network packets.
|
||||
Once the parasite is no longer needed, it is removed using these steps:
|
||||
|
||||
## Curing dumpee from parasite code
|
||||
|
||||
Once everything is done and we no longer need parasite it is removed from the dumpee's address space by performing the following steps:
|
||||
|
||||
1. CRIU starts tracing the syscalls parasite is executing with help of `ptrace`
|
||||
1. CRIU sends `PARASITE_CMD_FINI` to the parasite via the control socket
|
||||
1. Parasite receives the command, then closes control socket and executes `rt_sigreturn()` system call
|
||||
1. CRIU intercepts exit from this syscall and unmaps parasite memory area, thus reverting the dumpee back to the state it was in before parasite injection
|
||||
1. CRIU begins tracing the syscalls executed by the parasite using `ptrace`.
|
||||
1. CRIU sends the `PARASITE_CMD_FINI` command via the control socket.
|
||||
1. The parasite closes the socket and executes an `rt_sigreturn()` system call.
|
||||
1. CRIU intercepts the completion of this syscall and unmaps the parasite's memory area, returning the dumpee to its original state.
|
||||
|
||||
## See also
|
||||
- [Code blobs](code-blobs.md)
|
||||
- [Compel](compel.md)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
# Pending signals
|
||||
# Pending Signals
|
||||
|
||||
### Introduction
|
||||
A process can block a set of signals and all signals will wait in two kernel queues. One queue is shared between threads and the other is private for a thread. Signals in a queue are called pending signals. Each signal has a siginfo, it’s a small message. Several types of siginfo-s exist.
|
||||
A process can block a set of signals, causing them to wait in two kernel queues: one shared between threads and another private to each thread. Signals in these queues are referred to as "pending signals." Each signal includes a `siginfo` message, and several different types of `siginfo` exist.
|
||||
|
||||
### What do we need to dump pending signals?
|
||||
### Dumping Pending Signals
|
||||
|
||||
We need to get the siginfo for each signal on dump, and then return it back on restore. It looks simple, doesn’t it? I thought so, before I started. The first problem is that the kernel doesn’t report complete siginfo-s in user-space. In a signal handler the kernel strips SI_CODE from siginfo. When a siginfo is received from signalfd, it has a different format with fixed sizes of fields. The interface of signalfd was extended. If a signalfd is created with the flag SFD_RAW, it returns siginfo in a raw format. We need to choose a queue, so two more flags SFD_GROUP and SFD_PRIVATE were added.
|
||||
Ok, signals can be dumped. Now we can think how to restore them. rt_sigqueueinfo looks suitable for that, but it can’t send siginfo with a positive si_code, because these codes are reserved for the kernel. In the real world each person has right to do anything with himself, so I think a process should able to send any siginfo to itself.
|
||||
Dumping pending signals requires capturing the `siginfo` for each signal during a dump and then restoring it later. While this sounds simple, several challenges exist. First, the kernel traditionally does not report complete `siginfo` structures to userspace; for example, it often strips the `SI_CODE` field in signal handlers. Additionally, `siginfo` received via a `signalfd` uses a different format with fixed-size fields.
|
||||
|
||||
To address this, the `signalfd` interface was extended. When created with the `SFD_RAW` flag, `signalfd` returns `siginfo` in its raw format. Furthermore, the `SFD_GROUP` and `SFD_PRIVATE` flags were added to allow selecting the specific queue to dump.
|
||||
|
||||
Once signals are captured, they must be restored. While `rt_sigqueueinfo` is suitable for this, it traditionally cannot send `siginfo` with a positive `si_code`, as those values are reserved for the kernel. However, since a process should have the right to manage its own state, it should be able to send any `siginfo` to itself during restoration.
|
||||
|
|
|
|||
|
|
@ -1,31 +1,34 @@
|
|||
# Pid restore
|
||||
# PID Restoration
|
||||
|
||||
## ns_last_pid
|
||||
In order to restore PID, CRIU uses /proc/sys/kernel/ns_last_pid, which is available in kernel since v3.3 (according to [Upstream_kernel_commits](upstream_kernel_commits.md)). It requires CONFIG_CHECKPOINT_RESTORE to be set and it is enabled in the vast majority of distros. ns_last_pid contains the last pid that was assigned by the kernel. So, when kernel needs to assign a new one, it looks into ns_last_pid, gets last_pid and assigns last_pid+1. To restore PID, criu locks ns_last_pid, writes PID-1 and calls clone().
|
||||
To restore PIDs, CRIU uses `/proc/sys/kernel/ns_last_pid`, which has been available in the kernel since version 3.3. This feature requires `CONFIG_CHECKPOINT_RESTORE` to be enabled, which is the case for the vast majority of Linux distributions. The `ns_last_pid` file contains the last PID assigned by the kernel. When the kernel needs to assign a new PID, it retrieves the value from `ns_last_pid` and assigns `ns_last_pid + 1`. To restore a specific PID, CRIU locks `ns_last_pid`, writes `PID - 1` to it, and then calls `clone()`.
|
||||
|
||||
## Example
|
||||
Here is a simple program that shows how to set PID for a forked child.
|
||||
The following C program demonstrates how to set a specific PID for a forked child process.
|
||||
|
||||
-*BEWARE! This program requires root. I don't take any responsibility for what this code might do to your system.**( tested though =) )
|
||||
**BEWARE**: This program requires root privileges. The authors take no responsibility for the impact of this code on your system (though it has been tested).
|
||||
|
||||
```c
|
||||
|
||||
1.include <sys/stat.h>
|
||||
1.include <fcntl.h>
|
||||
1.include <stdio.h>
|
||||
1.include <string.h>
|
||||
1.include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/file.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
int fd, pid;
|
||||
char buf[32];
|
||||
|
||||
if (argc != 2)
|
||||
return 1;
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "Usage: %s <desired_pid>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Opening ns_last_pid...\n");
|
||||
fd = open("/proc/sys/kernel/ns_last_pid", O_RDWR | O_CREAT, 0644);
|
||||
fd = open("/proc/sys/kernel/ns_last_pid", O_RDWR);
|
||||
if (fd < 0) {
|
||||
perror("Can't open ns_last_pid");
|
||||
return 1;
|
||||
|
|
@ -35,7 +38,7 @@ int main(int argc, char *argv[])
|
|||
printf("Locking ns_last_pid...\n");
|
||||
if (flock(fd, LOCK_EX)) {
|
||||
close(fd);
|
||||
printf("Can't lock ns_last_pid\n");
|
||||
perror("Can't lock ns_last_pid");
|
||||
return 1;
|
||||
}
|
||||
printf("Done\n");
|
||||
|
|
@ -43,37 +46,35 @@ int main(int argc, char *argv[])
|
|||
pid = atoi(argv[1]);
|
||||
snprintf(buf, sizeof(buf), "%d", pid - 1);
|
||||
|
||||
printf("Writing pid-1 to ns_last_pid...\n");
|
||||
if (write(fd, buf, strlen(buf)) != strlen(buf)) {
|
||||
printf("Can't write to buf\n");
|
||||
printf("Writing pid-1 (%d) to ns_last_pid...\n", pid - 1);
|
||||
if (write(fd, buf, strlen(buf)) != (ssize_t)strlen(buf)) {
|
||||
perror("Can't write to ns_last_pid");
|
||||
flock(fd, LOCK_UN);
|
||||
close(fd);
|
||||
return 1;
|
||||
}
|
||||
printf("Done\n");
|
||||
|
||||
printf("Forking...\n");
|
||||
int new_pid;
|
||||
new_pid = fork();
|
||||
int new_pid = fork();
|
||||
if (new_pid == 0) {
|
||||
printf("I'm child!\n");
|
||||
printf("I'm the child! My PID is %d\n", getpid());
|
||||
exit(0);
|
||||
} else if (new_pid == pid) {
|
||||
printf("I'm parent. My child got right pid!\n");
|
||||
printf("I'm the parent. My child received the correct PID (%d)!\n", new_pid);
|
||||
} else {
|
||||
printf("pid does not match expected one\n");
|
||||
printf("PID %d does not match expected PID %d\n", new_pid, pid);
|
||||
}
|
||||
printf("Done\n");
|
||||
|
||||
printf("Cleaning up...");
|
||||
printf("Cleaning up...\n");
|
||||
if (flock(fd, LOCK_UN)) {
|
||||
printf("Can't unlock");
|
||||
perror("Can't unlock");
|
||||
}
|
||||
|
||||
close(fd);
|
||||
|
||||
printf("Done\n");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,22 @@
|
|||
# Pidfd store
|
||||
# Pidfd Store
|
||||
|
||||
Pidfd store increases the reliability of pid reuse detection during pre-dumps/dumps using task pidfd instead of task creation time.
|
||||
The `pidfd` store increases the reliability of PID reuse detection during pre-dumps and dumps by using task `pidfds` instead of task creation times.
|
||||
|
||||
It is only supported for RPC and the C library.
|
||||
This feature is supported only via RPC and the C library.
|
||||
|
||||
## Usage
|
||||
|
||||
A connectionless unix socket is passed to CRIU during each pre-dump/dump through
|
||||
the RPC option `pidfd_store_sk` or `criu_set_pidfd_store_sk` routine in the the library.
|
||||
A connectionless UNIX socket is passed to CRIU during each pre-dump or dump operation using the `pidfd_store_sk` RPC option or the `criu_set_pidfd_store_sk` library routine.
|
||||
|
||||
<b>NOTE</b>: This is targeted at migration tools like P.Haul, because the passed socket must be kept alive throughout all pre-dump/dump iterations.
|
||||
**NOTE**: This feature is intended for migration tools like P.Haul, as the provided socket must remain active throughout all pre-dump and dump iterations.
|
||||
|
||||
## Feature check
|
||||
## Feature Check
|
||||
|
||||
This feature requires `pidfd_open` and `pidfd_getfd` syscalls.
|
||||
Support could be checked with:
|
||||
CLI: `criu check --feature pidfd_store`.
|
||||
RPC: `CRIU_REQ_TYPE__FEATURE_CHECK` and set `pidfd_store` to true in the "features" field of the request
|
||||
|
||||
## How it works
|
||||
|
||||
The `pidfd_store_sk` is used as a queue for task pidfds. CRIU sends tasks pidfds to this socket and receives them in the next pre-dump/dump iteration. Those pidfds could then be used to check whether the task is still alive, otherwise it is a case of pid reuse and CRIU should make a full page dump.
|
||||
This feature requires the `pidfd_open` and `pidfd_getfd` system calls. Support can be verified via:
|
||||
|
||||
- **CLI**: `criu check --feature pidfd_store`
|
||||
- **RPC**: Use `CRIU_REQ_TYPE__FEATURE_CHECK` and set `pidfd_store` to `true` in the `features` field of the request.
|
||||
|
||||
## How It Works
|
||||
|
||||
The `pidfd_store_sk` serves as a queue for task `pidfds`. CRIU sends task `pidfds` to this socket and retrieves them during the subsequent pre-dump or dump iteration. These `pidfds` are then used to verify if the task is still active. If the task is no longer alive, CRIU detects this as a PID reuse scenario and performs a full page dump.
|
||||
|
|
|
|||
|
|
@ -1,39 +1,31 @@
|
|||
# Pidfd
|
||||
|
||||
This article describes how we checkpoint/restore pidfds (process file descriptors).
|
||||
This article describes how CRIU checkpoints and restores `pidfds` (process file descriptors).
|
||||
|
||||
## Checkpointing
|
||||
All information that we require for restore of pidfds is available in this `/proc` entry: `/proc/$pid/fdinfo/$pidfd`
|
||||
All information required to restore a `pidfd` is available in the `/proc/$pid/fdinfo/$pidfd` entry.
|
||||
|
||||
Since CRIU does not support nested pid namespaces, the correct `pid` to use during restore is the last `pid` (i.e., `pid` in the most deeply nested pid namespace) in the `/proc/$pid/fdinfo/$pidfd/NSpid` field.
|
||||
So, we only dump that `pid`.
|
||||
Since CRIU does not currently support nested PID namespaces, the correct PID to use during restoration is the final entry in the `NSpid` field (representing the PID in the most deeply nested namespace). CRIU captures only this PID.
|
||||
|
||||
## Restoring `pidfd(s)` Pointing to an Alive Process
|
||||
CRIU, while restoring, first creates all processes in the process tree and then starts opening file descriptors in each process.
|
||||
## Restoring Pidfds for Active Processes
|
||||
During restoration, CRIU first recreates the entire process tree and then begins opening file descriptors for each process. If a `pidfd` points to a process that is already alive, CRIU simply uses the `pidfd_open()` system call to recreate it.
|
||||
|
||||
So, If the pidfd points to an alive process, we can simply use `pidfd_open()` to create the pidfd.
|
||||
## Restoring Pidfds for Terminated Processes
|
||||
If the process originally referenced by the `pidfd` has terminated, the PID information in `proc` is lost (the `NSpid` field becomes -1), and `pidfd_open()` cannot be used.
|
||||
|
||||
## Restoring `pidfd(s)` Pointing to a Dead Process
|
||||
To handle this, CRIU uses the following mechanism:
|
||||
|
||||
If the process (to which the pidfd was pointing) is dead, however, we lose access to its pid (`/proc/$pid/fdinfo/$pidfd/NSpid` becomes -1).
|
||||
We also cannot open a pidfd pointing to a dead process.
|
||||
1. A hash table is created using the `pidfd`'s inode number as the key. It stores:
|
||||
- A list of all processes that held a `pidfd` with that inode number.
|
||||
- A designated `creator_id` (the highest ID in the list).
|
||||
|
||||
To overcome these problems, we do the following things:
|
||||
1. For each unique inode number, the process identified as the `creator` creates a temporary process (let's call it `x`).
|
||||
|
||||
We create a `hashtable` with the `inode number` of the pidfd acting as the key that stores:
|
||||
- List of all processes that had a pidfd open with this inode number.
|
||||
- The highest id in the list of processes (`creator_id`)
|
||||
1. The `creator` process then opens a `pidfd` pointing to `x` and transmits it to all other processes that originally held a `pidfd` with the same inode number. This is done using CRIU's `send_desc_to_peer` and `recv_desc_from_peer` functions.
|
||||
|
||||
For each unique `inode number`, the process with `creator_id` (let's call it `creator`) creates a temporary process (let's name this process `x`).
|
||||
|
||||
The creator process will then open `pidfd(s)` pointing to `x` and will send them to all other processes (that had a pidfd with this inode number) using CRIU's `send_desc_to_peer` and `recv_desc_from_peer` functions, which allow processes to send and receive file descriptors.
|
||||
|
||||
This way, all processes with pidfds that point to the same dead process remain in the correct state (i.e., have pidfds with the same inode number) even after restoration.
|
||||
|
||||
After the creator process finishes opening and sending all pidfds, it kills the temporary process `x`.
|
||||
This ensures that all processes with `pidfds` pointing to the same terminated process are restored to a consistent state, sharing the same inode number. Once the `pidfds` have been distributed, the `creator` terminates the temporary process `x`.
|
||||
|
||||
## Limitations
|
||||
|
||||
- We currently do not support pidfd's opened with the `PIDFD_THREAD` flag.
|
||||
- We also cannot C/R pidfd's that point to process(es) outside the current process tree.
|
||||
|
||||
- CRIU does not currently support `pidfds` opened with the `PIDFD_THREAD` flag.
|
||||
- CRIU cannot checkpoint or restore `pidfds` that point to processes outside the captured process tree.
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
# Restartable Sequences
|
||||
|
||||
Restartable sequences (aka RSEQ) are short, carefully defined sections of user-space code that enable efficient access to per-CPU data structures without relying on heavyweight synchronization primitives such as mutexes or atomic operations.
|
||||
Restartable sequences (RSEQ) are short, carefully defined sections of userspace code that enable efficient access to per-CPU data structures without the overhead of heavyweight synchronization primitives like mutexes or atomic operations.
|
||||
|
||||
Support for RSEQ was introduced in the Linux kernel in version 4.18, allowing user-space programs to register critical code paths that the kernel can safely restart when a CPU migration or preemption occurs. This mechanism enables high-performance, scalable data access patterns while preserving correctness. [The 5-year journey to bring restartable sequences to Linux](https://www.efficios.com/blog/2019/02/08/linux-restartable-sequences/) article provides more information about how restartable sequences work, their design, use cases, and kernel integration.
|
||||
Introduced in Linux kernel version 4.18, RSEQ allows userspace programs to register critical code paths that the kernel can safely restart if a CPU migration or preemption occurs. This mechanism enables high-performance, scalable data access while ensuring correctness. For more background, see [The 5-year journey to bring restartable sequences to Linux](https://www.efficios.com/blog/2019/02/08/linux-restartable-sequences/).
|
||||
|
||||
## Linux Kernel Interface for Restartable Sequences
|
||||
## Linux Kernel Interface
|
||||
|
||||
The Linux kernel interface for RSEQ is intentionally minimal. It consists of a single system call:
|
||||
The kernel interface for RSEQ is minimal, consisting of a single system call:
|
||||
`sys_rseq(struct rseq *rseq, uint32_t rseq_len, int flags, uint32_t sig)`
|
||||
|
||||
The full definition of the RSEQ data structures and related flags is provided in [include/uapi/linux/rseq.h](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/rseq.h):
|
||||
|
||||
```
|
||||
The data structures and flags are defined in [`include/uapi/linux/rseq.h`](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/rseq.h):
|
||||
|
||||
```c
|
||||
enum rseq_cs_flags {
|
||||
RSEQ_CS_FLAG_NO_RESTART_ON_PREEMPT = (1U << RSEQ_CS_FLAG_NO_RESTART_ON_PREEMPT_BIT),
|
||||
RSEQ_CS_FLAG_NO_RESTART_ON_SIGNAL = (1U << RSEQ_CS_FLAG_NO_RESTART_ON_SIGNAL_BIT),
|
||||
|
|
@ -20,176 +19,60 @@ enum rseq_cs_flags {
|
|||
};
|
||||
|
||||
struct rseq_cs {
|
||||
__u32 version; /* always 0 at this moment */
|
||||
__u32 version; /* always 0 currently */
|
||||
enum rseq_cs_flags flags;
|
||||
void *start_ip;
|
||||
/* Offset from start_ip. */
|
||||
intptr_t post_commit_offset;
|
||||
void *abort_ip;
|
||||
}
|
||||
};
|
||||
|
||||
struct rseq {
|
||||
__u32 cpu_id_start;
|
||||
__u32 cpu_id;
|
||||
struct rseq_cs *rseq_cs;
|
||||
enum rseq_cs_flags flags;
|
||||
}
|
||||
|
||||
};
|
||||
```
|
||||
|
||||
From the userspace side, we need to keep `struct rseq` somewhere and register it on the kernel side using the `rseq` syscall.
|
||||
Then, once we want to execute some code as a rseq critical section (`rseq cs` or just CS) we need to allocate and fill with the data
|
||||
`struct rseq_cs`. We have to specify the start address of our CS, and the address of the abort handler (called when CS was interrupted by a preemption, migration
|
||||
or signal). Then we need to put an pointer to `struct rseq_cs` to the `(struct rseq).rseq_cs` field.
|
||||
To use RSEQ, an application must maintain a `struct rseq` and register it with the kernel. Before entering an RSEQ critical section, the application populates a `struct rseq_cs` with the start address and an abort handler (to be called if the section is interrupted) and updates the `rseq_cs` pointer in the registered `struct rseq`.
|
||||
|
||||
## Handling of RSEQ Flags
|
||||
## Handling RSEQ Flags
|
||||
|
||||
Flags can be specified at either `struct rseq` and `struct rseq_cs` using values from `enum rseq_cs_flags`. Regardless of where they are set, the kernel combines them when determining restart behavior:
|
||||
Flags can be specified in both `struct rseq` and `struct rseq_cs`. The kernel combines these flags to determine restart behavior. Typically, `flags` is zero, meaning the critical section is interrupted and the instruction pointer (IP) is redirected to the abort handler if preemption, migration, or a signal occurs. Applications can use specific flags to allow completion even during certain events.
|
||||
|
||||
```
|
||||
|
||||
static int rseq_need_restart(struct task_struct *t, u32 cs_flags)
|
||||
{
|
||||
u32 flags, event_mask;
|
||||
int ret;
|
||||
|
||||
/* Get thread flags. */
|
||||
ret = get_user(flags, &t->rseq->flags);
|
||||
if (ret)
|
||||
return ret;
|
||||
|
||||
/* Take critical section flags into account. */
|
||||
flags |= cs_flags; // <<<<<<<< here we have flags combined from struct rseq + struct rseq_cs
|
||||
|
||||
```
|
||||
|
||||
The most common `flags` value is zero. In this case, the restartable sequence critical section is interrupted whenever a preemption, CPU migration, or signal occurs, and the instruction pointer (IP) will be redirected to the abort handler. In some scenarios, however, applications may prefer to allow a critical section to complete even when certain events occur, and can do so by explicitly setting the appropriate flags.
|
||||
|
||||
Note that `RSEQ_CS_FLAG_NO_RESTART_ON_SIGNAL` must be used in conjunction with both `RSEQ_CS_FLAG_NO_RESTART_ON_PREEMPT` and `RSEQ_CS_FLAG_NO_RESTART_ON_MIGRATE`. The kernel enforces this constraint to prevent inconsistent restart semantics:
|
||||
```
|
||||
|
||||
/*
|
||||
* Restart on signal can only be inhibited when restart on
|
||||
* preempt and restart on migrate are inhibited too. Otherwise,
|
||||
* a preempted signal handler could fail to restart the prior
|
||||
* execution context on sigreturn.
|
||||
*/
|
||||
if (unlikely((flags & RSEQ_CS_FLAG_NO_RESTART_ON_SIGNAL) &&
|
||||
(flags & RSEQ_CS_PREEMPT_MIGRATE_FLAGS) !=
|
||||
RSEQ_CS_PREEMPT_MIGRATE_FLAGS))
|
||||
return -EINVAL;
|
||||
|
||||
```
|
||||
Note that `RSEQ_CS_FLAG_NO_RESTART_ON_SIGNAL` must be used alongside both `RSEQ_CS_FLAG_NO_RESTART_ON_PREEMPT` and `RSEQ_CS_FLAG_NO_RESTART_ON_MIGRATE` to ensure consistent restart semantics.
|
||||
|
||||
## Checkpoint/Restore of RSEQ
|
||||
|
||||
CRIU handles restartable sequences differently depending on the execution state of the process at checkpoint time. This can be categorized into the following cases:
|
||||
CRIU handles RSEQ based on the process's execution state at checkpoint time:
|
||||
|
||||
1. Process is not executing inside an rseq critical section
|
||||
1. Process is executing inside an rseq critical section
|
||||
## `flags` is `0`, `RSEQ_CS_FLAG_NO_RESTART_ON_PREEMPT`, or `RSEQ_CS_FLAG_NO_RESTART_ON_MIGRATE`
|
||||
## `flags` includes `RSEQ_CS_FLAG_NO_RESTART_ON_SIGNAL`
|
||||
### Case 1: Outside a Critical Section
|
||||
This is the simplest scenario. The process has a registered `struct rseq`, but the IP is not within a critical section.
|
||||
|
||||
### Executing outside critical section
|
||||
- **Checkpoint**: CRIU uses `PTRACE_GET_RSEQ_CONFIGURATION` to locate the `struct rseq` and record its address, length, and signature.
|
||||
- **Restore**: CRIU retrieves the `struct rseq` information from the image and re-registers it from the restorer context.
|
||||
|
||||
This is the simplest case. The process has a `struct rseq` registered with the kernel, but its instruction pointer (IP) is not currently executing within an RSEQ critical section.
|
||||
### Case 2: Inside an Abortable Critical Section
|
||||
If the IP is within an RSEQ critical section with standard flags (e.g., `0`), RSEQ semantics require that an interruption redirects the IP to the abort handler. Simply restoring the process with its original IP would violate these semantics.
|
||||
|
||||
#### Checkpoint
|
||||
CRIU only needs to locate the `struct rseq` instance and record its address, length, and signature. This information is obtained using the ptrace request `PTRACE_GET_RSEQ_CONFIGURATION` (see the `dump_thread_rseq` function).
|
||||
- **Checkpoint**: CRIU records the `struct rseq` configuration and explicitly adjusts the saved IP to point to the RSEQ abort handler.
|
||||
- **Restore**: The process resumes execution at the abort handler, ensuring a semantically correct state.
|
||||
|
||||
#### Restore
|
||||
During restore, CRIU retrieves the `struct rseq` information from the checkpoint image (see images/rseq.proto) and re-register it from the parasite context using the `rseq` syscall (see `restore_rseq` in `criu/pie/restorer.c`).
|
||||
The `fixup_thread_rseq` function implements this logic by detecting if the IP falls within the registered `rseq_cs` range and rewriting it as needed.
|
||||
|
||||
### Executing inside critical section
|
||||
### Case 3: Inside a Non-Abortable Critical Section
|
||||
When `RSEQ_CS_FLAG_NO_RESTART_ON_SIGNAL` is set, the section is non-abortable. However, special handling is still required because the kernel clears the `rseq_cs` pointer when CRIU transfers execution to the parasite code.
|
||||
|
||||
When a process is being checkpointed while its instruction pointer is inside an RSEQ critical section, CRIU preserves the instruction pointer exactly as it was at checkpoint time.
|
||||
However, RSEQ semantics require that if execution of a critical section is interrupted, the kernel redirects execution to the associated abort handler. In particular, when the `flags` value is `0`, `RSEQ_CS_FLAG_NO_RESTART_ON_PREEMPT` or `RSEQ_CS_FLAG_NO_RESTART_ON_MIGRATE` the kernel automatically redirects the instruction pointer to the abort handler associated with the RSEQ critical section. As a result, restoring the process with its instruction pointer unchanged violates the RSEQ semantics, potentially leading to incorrect behavior or application crashes. To address this issue, CRIU explicitly adjusts the instruction pointer to match kernel behavior.
|
||||
|
||||
The logic responsible for this is implemented in the `fixup_thread_rseq` function:
|
||||
```
|
||||
|
||||
if (task_in_rseq(rseq_cs, TI_IP(core))) {
|
||||
struct pid *tid = &item->threads[i];
|
||||
|
||||
...
|
||||
|
||||
pr_warn("The %d task is in rseq critical section. IP will be set to rseq abort handler addr\n",
|
||||
tid->real);
|
||||
|
||||
...
|
||||
|
||||
if (!(rseq_cs->flags & RSEQ_CS_FLAG_NO_RESTART_ON_SIGNAL)) {
|
||||
pr_warn("The %d task is in rseq critical section. IP will be set to rseq abort handler addr\n",
|
||||
tid->real);
|
||||
|
||||
TI_IP(core) = rseq_cs->abort_ip;
|
||||
|
||||
if (item->pid->real == tid->real) {
|
||||
compel_set_leader_ip(dmpi(item)->parasite_ctl, rseq_cs->abort_ip);
|
||||
} else {
|
||||
compel_set_thread_ip(dmpi(item)->thread_ctls[i], rseq_cs->abort_ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
This code detects when a thread's instruction pointer lies within an RSEQ critical section and, unless `RSEQ_CS_FLAG_NO_RESTART_ON_SIGNAL` is set, rewrites the instruction pointer to the abort handler address. By doing so, CRIU mirrors the kernel's rseq fixup behavior and ensures that the restored process resumes execution in a semantically correct state.
|
||||
|
||||
#### Checkpoint
|
||||
CRIU locates the `struct rseq` instance and records its address, length, and signature using the `PTRACE_GET_RSEQ_CONFIGURATION` ptrace request (see `dump_thread_rseq`).
|
||||
In addition, the instruction pointer is explicitly adjusted to point to the RSEQ abort handler.
|
||||
|
||||
#### Restore
|
||||
During restore, CRIU reads data about the `struct rseq` state from the checkpoint image (`images/rseq.proto`) and re-register it from the restorer context using the `rseq` system call (see `restore_rseq` in `criu/pie/restorer.c`). No further action is required: the process resumes execution at the abort handler, outside of the RSEQ critical section.
|
||||
|
||||
### Executing inside non-abortable critical section
|
||||
|
||||
This is a relatively rare case, but it is fully supported by CRIU. When an RSEQ critical section is marked with the `RSEQ_CS_FLAG_NO_RESTART_ON_SIGNAL` flag, it is effectively non-abortable.
|
||||
At first glance, this might suggest that no special handling is required as the RSEQ structure could simply be saved, and the instruction pointer left unchanged. However, this assumption is incorrect.
|
||||
|
||||
During checkpoint, when CRIU transfers execution to the parasite, the kernel clears the `(struct rseq).rseq_cs` pointer if it determines that execution is no longer within an rseq critical section:
|
||||
```
|
||||
|
||||
static int rseq_ip_fixup(struct pt_regs *regs)
|
||||
{
|
||||
...
|
||||
|
||||
/*
|
||||
* Handle potentially not being within a critical section.
|
||||
* If not nested over a rseq critical section, restart is useless.
|
||||
* Clear the rseq_cs pointer and return.
|
||||
*/
|
||||
if (!in_rseq_cs(ip, &rseq_cs))
|
||||
return clear_rseq_cs(t);
|
||||
|
||||
```
|
||||
|
||||
As a result, after restore, the process resumes execution at the correct instruction pointer within the critical section, but from the kernel's perspective it is no longer executing inside an RSEQ critical section. This discrepancy is problematic, because the kernel relies on the `(struct rseq).rseq_cs` field to determine rseq execution context.
|
||||
|
||||
#### Checkpoint
|
||||
|
||||
CRIU locates the `struct rseq` instance and records its address, length, and signature using the `PTRACE_GET_RSEQ_CONFIGURATION` ptrace request.
|
||||
The instruction pointer is saved without modification, but the `(struct rseq).rseq_cs` field is also recorded in the CRIU image.
|
||||
|
||||
#### Restore
|
||||
|
||||
During restore, CRIU re-registers the `struct rseq` from the checkpoint image (`images/rseq.proto`) using the `rseq` system call from the restorer context (see `restore_rseq` in `criu/pie/restorer.c`). In addition, CRIU explicitly restores the `(struct rseq).rseq_cs` field using `PTRACE_POKEAREA` (see `restore_rseq_cs`) to reestablish the correct `rseq` execution context in the kernel.
|
||||
- **Checkpoint**: CRIU saves the `struct rseq` configuration and the IP without modification, but also explicitly records the `rseq_cs` field.
|
||||
- **Restore**: CRIU re-registers the `struct rseq` and then uses `PTRACE_POKEAREA` to restore the `rseq_cs` pointer, re-establishing the kernel's RSEQ execution context.
|
||||
|
||||
## TODO
|
||||
|
||||
- tests for all architectures (right now we have ZDTM tests only for x86_64)
|
||||
- improvement support of built-in rseq for non-Glibc libraries
|
||||
- pre-dump tests (?)
|
||||
- leave-running tests (?)
|
||||
- crfail test
|
||||
- threaded test
|
||||
- Test support for all architectures (currently x86_64 only).
|
||||
- Improve support for built-in RSEQ in non-Glibc libraries.
|
||||
- Add comprehensive tests for pre-dump, leave-running, and multi-threaded scenarios.
|
||||
|
||||
## Useful links
|
||||
|
||||
- [1] https://github.com/torvalds/linux/blob/b2d229d4ddb17db541098b83524d901257e93845/kernel/rseq.c#L1
|
||||
- [2] https://www.efficios.com/blog/2019/02/08/linux-restartable-sequences/
|
||||
- [3] https://lwn.net/Articles/883104/
|
||||
- [4] https://patchwork.sourceware.org/project/glibc/list/?series=5530&state=*
|
||||
|
||||
|
||||
- https://github.com/torvalds/linux/blob/master/kernel/rseq.c
|
||||
- https://www.efficios.com/blog/2019/02/08/linux-restartable-sequences/
|
||||
- https://lwn.net/Articles/883104/
|
||||
|
|
|
|||
|
|
@ -1,43 +1,44 @@
|
|||
# Restorer context
|
||||
# Restorer Context
|
||||
|
||||
This page describes what this context is and why we need one.
|
||||
This page explains the restorer context and its role in the restoration process.
|
||||
|
||||
## What is it?
|
||||
## What is the Restorer Context?
|
||||
|
||||
The restorer context is the last stage of the [restore](checkpointrestore.md) process. It differs from the regular CRIU's (process) context like the [parasite code](parasite-code.md) does -- it doesn't have any libraries, it is PIE-compiled and can only work on fixed amount of memory. In this context CRIU restores
|
||||
The restorer context is the final stage of the [restoration](checkpointrestore.md) process. It is a specialized environment, similar to the [parasite code](parasite-code.md), that is compiled as a Position-Independent Executable (PIE) and operates without standard libraries. In this context, CRIU finalizes the restoration of:
|
||||
|
||||
1. memory
|
||||
1. timers
|
||||
1. credentials
|
||||
1. threads
|
||||
1. Memory mappings
|
||||
1. Timers
|
||||
1. Credentials
|
||||
1. Threads
|
||||
|
||||
## Why separate context?
|
||||
## Why is a Separate Context Necessary?
|
||||
|
||||
The reasoning for this is simple -- when CRIU comes to the state when it needs to restore process' memory, it should unmap all the old mappings and map the new ones. But since CRIU process would do this operation on itself, once the old code is unmapped CRIU will seg-fault right on the exit from the `munmap()` system call. Also this code should be get over-mmaped by the mapping is restores. So we need some code that would do this. And this other code should "sit" in two address spaces simultaneously -- in the CRIU's one and in the target one.
|
||||
A separate context is required because CRIU must eventually unmap its own memory mappings to make room for the target process's memory. Since CRIU is performing these operations on itself, it would segfault immediately upon exiting a `munmap()` call that removes its own code. To avoid this, a small "restorer blob" is used. This code is positioned in a memory "hole" that does not overlap with either CRIU's current mappings or the target process's intended mappings, allowing it to exist in both address spaces simultaneously.
|
||||
|
||||
The switch to this context is done in several steps.
|
||||
The transition to this context involves several steps:
|
||||
|
||||
First, we collect the data needed by the restorer code and puts all it into one sequential memory area. Then, knowing the data size and the restorer code size, we find the appropriate hole in the intersection of CRIU's and target mappings. Then we mmap() this region, mremap() the data into it, put the restorer blob nearby, fix the pointers (see below) and call assember's "jump" instruction to get there.
|
||||
1. CRIU collects all data needed by the restorer and places it into a single sequential memory area.
|
||||
1. CRIU identifies a suitable memory hole for the restorer code and data.
|
||||
1. CRIU maps this region, moves the data into it, and places the restorer blob nearby.
|
||||
1. Pointers within the restorer data are adjusted to be valid within the restorer's context.
|
||||
1. CRIU executes an assembly "jump" instruction to transfer control to the restorer blob.
|
||||
|
||||
Now what "fix the pointers" mean. When we collected data for CRIU we addressed the objects in this are using pointers valid in CRIU address space. When we will jump into restorer code pointers in there should "know" where the respective objects are. So knowing where from the restorer counts pointers and the structure of the restorer data, we alter them respectively.
|
||||
## What is Restored in This Context?
|
||||
|
||||
## What is restored there and why
|
||||
### Memory
|
||||
Memory is restored here to avoid the self-unmapping issue mentioned above. At this stage, CRIU:
|
||||
- Moves anonymous VMAs from their temporary locations to their final addresses using `mremap()`.
|
||||
- Maps file-backed VMAs using `mmap()`.
|
||||
|
||||
So, memory is restored here for the reasons described above. Note, that here CRIU does two things only
|
||||
### Timers
|
||||
Timers are armed at the very last moment to ensure they do not fire prematurely while processes are still being synchronized during restoration.
|
||||
|
||||
1. Move anonymous VMAs to proper places
|
||||
1. Map new file VMAs
|
||||
### Credentials
|
||||
Credentials are restored here to allow CRIU to perform its final privileged operations, such as `chdir()` or `chroot()`, just before the process resumes.
|
||||
|
||||
The anonymous memory is mmaped and filled with data earlier, in restorer it's only mremap()-ed into proper addressed. The files mapping are just mmap()-ed, as data in them sits in files :)
|
||||
|
||||
Timers are restore here, since CRIU processes can wait for each other for some time while restoring and not to lose timer ticks there, we delay timers arming this the last moment.
|
||||
|
||||
Credentials are restored here to allow CRIU perform privileged operations such as fork-with-pid or chroot().
|
||||
|
||||
Threads are restored here for simplicity. If we restored them before, we'd have to "park" them while we change the memory layout. Instead of doing this, we first toss the memory around, then create threads.
|
||||
### Threads
|
||||
Threads are restored in this final stage for simplicity. Restoring them earlier would require "parking" them during complex memory layout changes. Instead, CRIU completes the memory transition first and then recreates the threads.
|
||||
|
||||
## See also
|
||||
- [Code blobs](code-blobs.md)
|
||||
- [Compel](compel.md)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
# Service descriptors
|
||||
# Service Descriptors
|
||||
|
||||
These are the fds that help us to do checkpoint and restore. There must be only files, which are used by criu frequently, and it's difficult to obtain them by another way. The number of service fds on restore is significant, as they make file table grow, so service fds may increase memory usage after restore in comparison to dump. Other common used on restore files should be placed in fdstore instead. Note, that not all members of enum sfd_type from criu/include/servicefd.h have to be there. Some of them must go to fdstore sometimes.
|
||||
Service descriptors are file descriptors (FDs) used by CRIU to facilitate the checkpoint and restore processes.
|
||||
|
||||
## Restore
|
||||
-We try to place service fds as low as possible to minimize memory overhead. Per-fdtable variable service_fd_base points to the biggest service fd number. It's being chosen in choose_service_fd_base() in dependence of max file descriptor used by task.
|
||||
Only files that are used frequently by CRIU and are difficult to obtain by other means should be maintained as service FDs. Because these FDs cause the file table to grow, they can result in higher memory usage after restoration compared to the initial dump. Other files used during restoration should generally be placed in the `fdstore` instead. Note that not all members of the `sfd_type` enum in `criu/include/servicefd.h` are required to be present at all times; some may be moved to the `fdstore` when appropriate.
|
||||
|
||||
-Since ordinary files, which are open on restore, may be bigger than service_fd_base, there is code parts where service fds mustn't be modified at all. We mask such areas via sfds_protected variable, and they aborts restore if modifications of sfds are found in such code parts.
|
||||
## Restore Process
|
||||
|
||||
CRIU attempts to assign service FDs to the lowest possible numbers to minimize memory overhead. The `service_fd_base` variable tracks the highest service FD number and is determined by `choose_service_fd_base()` based on the maximum file descriptor used by the task.
|
||||
|
||||
Because standard files opened during restoration may have descriptors higher than `service_fd_base`, certain sections of code are marked where service FDs must not be modified. These areas are protected via the `sfds_protected` variable; if an unauthorized modification to a service FD is detected in these sections, the restoration process is aborted.
|
||||
|
|
|
|||
|
|
@ -1,93 +1,41 @@
|
|||
# Shared memory
|
||||
# Shared Memory
|
||||
|
||||
Every process has one or more memory mappings, i.e. regions of virtual memory it allows to use.
|
||||
Some such mappings can be shared between a few processes, and they are called shared mappings.
|
||||
In other words, these are shared **anonymous (not file-based) memory mappings**.
|
||||
The article describes some intricacies of handling such mappings.
|
||||
Every process has one or more memory mappings representing regions of virtual memory. Some mappings are shared among multiple processes; these are referred to as shared anonymous (non-file-based) memory mappings. This article describes the intricacies of handling these mappings in CRIU.
|
||||
|
||||
## Checkpoint
|
||||
|
||||
During the checkpointing, CRIU needs to figure out all the shared mappings in order to dump them as such.
|
||||
During a checkpoint, CRIU identifies all shared mappings to ensure they are captured correctly.
|
||||
|
||||
It does so by calling `fstatat()` on each entry found in the `/proc/$PID/map_files/`,
|
||||
noting the *device:inode* pair of the structure returned by `fstatat()`. Now, if some processes
|
||||
have a mapping with the same *device:inode* pair, this mapping is marked as shared between these processes
|
||||
and dumped as such.
|
||||
CRIU calls `fstatat()` on each entry found in `/proc/$PID/map_files/` and records the *device:inode* pair returned. If multiple processes share the same *device:inode* pair, the mapping is marked as shared. This works because the kernel creates a hidden `tmpfs` file for shared anonymous mappings that is accessible via the `map_files` entry.
|
||||
|
||||
Note that `fstatat()` works because the kernel actually creates a hidden
|
||||
tmpfs file, not visible from any tmpfs mounts, but accessible via its
|
||||
`/proc/$PID/map_files/` entry.
|
||||
Dumping a shared mapping involves two steps:
|
||||
1. Writing an entry into the process's `mm.img` file.
|
||||
1. Storing the actual memory contents in `pagemap-shmem.img` and `pages.img`. (See [Memory dumps](memory-dumps.md) for details).
|
||||
|
||||
Dumping a mapping means two things:
|
||||
- writing an entry into process' mm.img file;
|
||||
- storing the actual mapping data (contents).
|
||||
For shared mappings, the contents is stored into a pair of image files: pagemap-shmem.img and pages.img.
|
||||
For details, see [Memory dumps](memory-dumps.md).
|
||||
|
||||
Note that different processes can map different parts of a shared memory segment.
|
||||
In this case, CRIU first collects mapping offsets and lengths from all the processes
|
||||
to determine the total segment size, then reads all the parts contents
|
||||
from the respective processes.
|
||||
If different processes map different portions of a shared memory segment, CRIU collects the offsets and lengths from all involved processes to determine the total segment size and then aggregates the content.
|
||||
|
||||
## Restore
|
||||
|
||||
During the restore, CRIU already knows which mappings are shared, so they need to be
|
||||
restored as such. Here is how it is done.
|
||||
During restoration, CRIU uses the information gathered during the checkpoint to recreate the shared mappings.
|
||||
|
||||
Among all the processes sharing a mapping, the one with the lowest PID among the group
|
||||
(see [postulates](postulates.md)) is assigned to be a mapping creator. The creator task is to obtain a mapping
|
||||
file descriptor, restore the mapping data, and signal all the other process that it's ready.
|
||||
During this process, all the other processes are waiting.
|
||||
Among the processes sharing a mapping, the one with the lowest PID (see [Postulates](postulates.md)) is designated the **creator**. The creator's responsibility is to obtain a file descriptor for the mapping, restore the data, and signal the other processes when it is ready. Other processes wait for this signal.
|
||||
|
||||
First, the creator need to obtain a file descriptor for the mapping. To achieve it, two different
|
||||
approaches are used, depending on the availability.
|
||||
To obtain a file descriptor:
|
||||
- If `memfd_create()` is available (Linux kernel v3.17+), it is used to create the mapping, followed by `ftruncate()` to set the correct size.
|
||||
- If `memfd_create()` is unavailable, the creator calls `mmap()` to create the mapping and then opens the corresponding file in `/proc/self/map_files/` to obtain a descriptor. Note that `map_files` is unavailable for processes in user namespaces due to security restrictions.
|
||||
|
||||
In case [memfd_create()](http://man7.org/linux/man-pages/man2/memfd_create.2.html)
|
||||
syscall is available (Linux kernel v3.17+), it is used to obtain a file descriptor.
|
||||
Next, `ftruncate()` is called to set the proper size of mapping.
|
||||
Once the creator has the descriptor, it maps the region, copies the data from the dump using `memcpy()`, and then unmaps the region while keeping the descriptor open. It then uses `futex(FUTEX_WAKE)` to notify the waiting processes.
|
||||
|
||||
If `memfd_create()` is not available, the alternative approach is used.
|
||||
First, mmap() is called to create a mapping. Next, a file in `/proc/self/map_files/`
|
||||
is opened to get a file descriptor for the mapping. The limitation of this method is,
|
||||
due to security concerns, /proc/$PID/map_files/ is not available for processes that
|
||||
live inside a user namespace, so it is impossible to use it if there
|
||||
are any user namespaces in the dump.
|
||||
Other processes wait via `futex(FUTEX_WAIT)`, then open the creator's `/proc/$CREATOR_PID/fd/$FD` file to retrieve the shared descriptor. Finally, all processes (including the creator) call `mmap()` to establish their specific mapping of the segment and then close the shared descriptor.
|
||||
|
||||
Once the creator have the file descriptor, it mmap()s it and restores its content from
|
||||
the dump (using memcpy()). The creator then unmaps the the mapping (note the file
|
||||
descriptor is still available). Next, it calls futex(FUTEX_WAKE) to signal all the
|
||||
waiting processes that the mapping file descriptor is ready.
|
||||
## Change Tracking
|
||||
|
||||
All the other processes that need this mapping wait on futex(FUTEX_WAIT). Once the
|
||||
wait is over, they open the creator's /proc/$CREATOR_PID/fd/$FD file to get the
|
||||
mapping file descriptor.
|
||||
For [iterative migration](iterative-migration.md), it is useful to track memory changes. Since CRIU v2.5, change tracking is supported for shared memory as well as anonymous memory. CRIU achieves this by scanning the pagemaps of all shared memory segment owners and performing a logical AND on the collected soft-dirty bits. This tracking also enables [memory images deduplication](memory-images-deduplication.md) for shared segments.
|
||||
|
||||
Finally, all the processes (including the creator itself) call mmap() to create a
|
||||
needed mapping (note that mmap() arguments such as length, offset and flags may
|
||||
differ for different processes), and close() the mapping file descriptor as it is
|
||||
no longer needed.
|
||||
## Dumping Present Pages
|
||||
|
||||
## Changes tracking
|
||||
|
||||
For [iterative migration](iterative-migration.md) it's very useful to track changes in memory. Until CRIU v2.5, changes were tracked for anonymous memory only, but now it is also shared memory can be tracked as well. To achieve it, CRIU scans all the shmem segment owners' pagemap (as it does for anonymous memory) and then ANDs the collected soft-dirty bits.
|
||||
|
||||
The changes tracking caused developers to implement [memory images deduplication](memory-images-deduplication.md) for shmem segments as well.
|
||||
|
||||
## Dumping present pages
|
||||
|
||||
When dumping the contents of shared memory, CRIU does not dump all of the data. Instead, it determines which pages contain
|
||||
it, and only dumps those pages. This is done similarly to how regular [memory dumping and restoring](memory-dumping-and-restoring.md) works, i.e. by looking
|
||||
for PRESENT or SWAPPED bits in owners' pagemap entries.
|
||||
|
||||
There is one particular feature of shared memory dumps worth mentioning. Sometimes, a shared memory page
|
||||
can exist in the kernel, but it is not mapped to any process. CRIU detects such pages by calling mincore()
|
||||
on the shmem segment, which reports back the page in-memory status. The mincore bitmap is when ANDed with
|
||||
the per-process ones.
|
||||
When capturing shared memory, CRIU only dumps pages that are actually present or swapped, rather than the entire segment. This is done by checking the corresponding bits in the owners' pagemap entries. Additionally, a shared memory page may exist in the kernel but not be mapped to any specific process. CRIU identifies these pages using `mincore()` on the shared memory segment and ANDs this with the per-process bitmaps.
|
||||
|
||||
## See also
|
||||
|
||||
- [Memory dumping and restoring](memory-dumping-and-restoring.md)
|
||||
- [Memory images deduplication](memory-images-deduplication.md)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,16 @@
|
|||
# Sockets
|
||||
|
||||
## Unix sockets initial support
|
||||
## UNIX Sockets Initial Support
|
||||
|
||||
Currently we support Unix socket of all kinds, UDP both IPv4 and IPv6, TCP in Listen and (!) [Established states](tcp-connection.md) and Netlink ones.
|
||||
CRIU currently supports all types of UNIX sockets, both IPv4 and IPv6 UDP sockets, Netlink sockets, and TCP sockets in both Listen and [Established](tcp-connection.md) states.
|
||||
|
||||
The cpt part uses the sock_diag engine to collect extended information about socket, then CRIU uses the files dumping engine to get access to sockets state.
|
||||
During a checkpoint, CRIU uses the `sock_diag` engine to collect extended socket information, which the file dumping engine then uses to capture the socket state.
|
||||
|
||||
The restore part of Unix sockets is the most tricky part. Listen sockets are just restored, this is simple.
|
||||
Connected sockets are restored like this:
|
||||
|
||||
1. One end establishes a listening anon socket at the desired descriptor;
|
||||
1. The other end just creates a socket at the desired descriptor;
|
||||
1. All sockets, that are to be connect()-ed call connect. Unix sockets do not block connect() till the accept() time and thus we continue with...
|
||||
1. ... all listening sockets call accept() and ... dup2 the new fd into the accepting end.
|
||||
|
||||
There's a problem with this approach -- socket names are not preserved, but looking into our OpenVZ implementation I think this is OK for existing apps.
|
||||
Restoring UNIX sockets is particularly complex. While restoring listening sockets is straightforward, connected sockets are restored using the following procedure:
|
||||
|
||||
1. One endpoint establishes a listening anonymous socket at the target descriptor.
|
||||
1. The other endpoint creates a socket at its target descriptor.
|
||||
1. The endpoint to be connected calls `connect()`. Since UNIX sockets do not block `connect()` until `accept()` is called, the process continues.
|
||||
1. All listening sockets then call `accept()`, and the resulting file descriptor is `dup2`'d into the accepting end.
|
||||
|
||||
One limitation of this approach is that socket names may not be preserved. However, based on experience with the OpenVZ implementation, this is generally acceptable for most applications.
|
||||
|
|
|
|||
|
|
@ -1,16 +1,11 @@
|
|||
# Stages of restoring
|
||||
|
||||
A process of restoring has a few global synchronization points:
|
||||
{| class="wikitable"
|
||||
| `CR_STATE_FORKING` || Create tasks and restore group leaders
|
||||
|-
|
||||
| `CR_STATE_RESTORE_PGID` || Restore group IDs. Group leaders should exist in this moment and all helpers should be alive
|
||||
|-
|
||||
| `CR_STATE_RESTORE` || Wait helpers and restore a most part of resources. If anyone segfauls, its parent gets `SIGCHLD` and notify criu about that
|
||||
|-
|
||||
| `CR_STATE_RESTORE_SIGCHLD` || Restore `SIGCHLD` handlers
|
||||
|-
|
||||
| `CR_STATE_COMPLETE` || All handlers were restored, origin processes can be resumed.
|
||||
|}
|
||||
# Stages of Restoration
|
||||
|
||||
The restoration process involves several global synchronization points:
|
||||
|
||||
| State | Description |
|
||||
| :--- | :--- |
|
||||
| `CR_STATE_FORKING` | Create tasks and restore process group leaders. |
|
||||
| `CR_STATE_RESTORE_PGID` | Restore process group IDs. Group leaders and all helpers must exist at this point. |
|
||||
| `CR_STATE_RESTORE` | Wait for helpers and restore the majority of resources. If a process segfaults, its parent receives `SIGCHLD` and notifies CRIU. |
|
||||
| `CR_STATE_RESTORE_SIGCHLD` | Restore `SIGCHLD` handlers. |
|
||||
| `CR_STATE_COMPLETE` | All handlers are restored, and the original processes can be resumed. |
|
||||
|
|
|
|||
|
|
@ -1,81 +1,53 @@
|
|||
# TCP connection
|
||||
# TCP Connection
|
||||
|
||||
This page describes how we handle established TCP connections.
|
||||
This page describes how CRIU handles established TCP connections.
|
||||
|
||||
## TCP repair mode in kernel
|
||||
## TCP Repair Mode in the Kernel
|
||||
|
||||
The `TCP_REPAIR` socket option was added to the kernel 3.5 to help with C/R for TCP sockets.
|
||||
The `TCP_REPAIR` socket option was added to the Linux kernel in version 3.5 to facilitate checkpoint/restore for TCP sockets.
|
||||
|
||||
When this option is used, the socket is switched into a special mode, in which any action performed on it
|
||||
does not result in anything defined by an appropriate protocol actions, but rather directly puts the socket
|
||||
into the state that the socket is expected to be in at the end of a successfully finished operation.
|
||||
When this option is enabled, the socket enters a special mode where actions performed on it do not trigger standard protocol behaviors. Instead, they directly transition the socket into the expected state as if the operation had successfully completed.
|
||||
|
||||
For example, calling `connect()` on a repaired socket just changes its state to `ESTABLISHED`,
|
||||
with the peer address set as requested.
|
||||
The `bind()` call forcibly binds the socket to a given address (ignoring any potential conflicts).
|
||||
The `close()` call closes the socket without any transient `FIN_WAIT`/`TIME_WAIT`/etc states,
|
||||
socket is silently killed.
|
||||
For example:
|
||||
- Calling `connect()` on a socket in repair mode simply changes its state to `ESTABLISHED` with the specified peer address.
|
||||
- Calling `bind()` forcibly binds the socket to the given address, ignoring potential conflicts.
|
||||
- Calling `close()` silently terminates the socket without undergoing `FIN_WAIT`, `TIME_WAIT`, or other transient states.
|
||||
|
||||
### Sequences
|
||||
To correctly restore a connection, CRIU must also restore the TCP sequence numbers. This is achieved using the `TCP_REPAIR_QUEUE` and `TCP_QUEUE_SEQ` options. `TCP_REPAIR_QUEUE` selects either the input or output queue for repair, and `TCP_QUEUE_SEQ` gets or sets the sequence number. Note that sequence numbers can only be set on a closed socket.
|
||||
|
||||
To restore the connection properly, bind() and connect() is not enough. One also needs to restore the
|
||||
TCP sequence numbers. To do so, the `TCP_REPAIR_QUEUE` and `TCP_QUEUE_SEQ` options were introduced.
|
||||
|
||||
The former one selects which queue (input or output) will be repaired and the latter gets/sets the sequence. Note
|
||||
setting the sequence is only possible on CLOSE-d socket.
|
||||
|
||||
### Packets in queue
|
||||
|
||||
When set the queue to repair as described above, one can call recv or send syscalls on a repaired socket. Both calls
|
||||
result on peeking or poking data from/to the respective queue. This sounds funny, but yes, for repaired socket one
|
||||
can receve the outgoing and send the incoming queues. Using the `MSG_PEEK` flag for `recv()` is required.
|
||||
### Packets in Queue
|
||||
While in repair mode, standard `recv` and `send` system calls can be used to peek or poke data directly from/to the selected queue. This allows CRIU to capture the state of outgoing and incoming packet queues. The `MSG_PEEK` flag is required for `recv()` calls.
|
||||
|
||||
### Options
|
||||
Four primary options are negotiated during the TCP connection stage:
|
||||
- `mss_clamp`: The maximum segment size the peer can accept.
|
||||
- `snd_scale`: The window scaling factor.
|
||||
- `sack`: Whether selective acknowledgments are permitted.
|
||||
- `tstamp`: Whether timestamps are supported.
|
||||
|
||||
There are 4 options that are negotiated by the socket at the connecting stage. These are
|
||||
These can be retrieved via `getsockopt()` and restored using the `TCP_REPAIR_OPTIONS` socket option.
|
||||
|
||||
- mss_clamp -- the maximum size of the segment peer is ready to accept
|
||||
- snd _scale -- the scale factor for a window
|
||||
- sack -- whether selective acks are permitted or not
|
||||
- tstamp -- whether timestamps on packets are supported
|
||||
## Timestamps
|
||||
As per RFC 7323, the sender's timestamp clock provides monotonic, non-decreasing values for segments. The Linux kernel uses the `jiffies` counter as the TCP timestamp. CRIU uses the `TCP_TIMESTAMP` option to compensate for differences in `jiffies` counters when a connection is migrated to a different host. During a dump, CRIU records the current timestamp and, during restoration, sets it as the new starting point.
|
||||
|
||||
All four can be read with `getsockopt()` calls to a socket and in order to restore them the `TCP_REPAIR_OPTIONS` sockoption is introduced.
|
||||
## Checkpoint and Restore of TCP Connections
|
||||
|
||||
## Timestamp
|
||||
"The sender's timestamp clock is used as a source of monotonic non-decreasing values to stamp the segments"(rfc7323). The Linux kernel uses the jiffies counter as the tcp timestamp.
|
||||
Using these socket options, CRIU can read the socket state and restore it, allowing the protocol to resume the data sequence seamlessly.
|
||||
|
||||
`#define tcp_time_stamp ((__u32)(jiffies))`
|
||||
Crucially, while the socket is closed between the dump and restoration, the connection must be "locked." This prevents packets from the peer from reaching the networking stack and causing the kernel to send a reset (RST). This is typically achieved using a netfilter rule to drop incoming packets from the peer. This rule must remain in place from the end of the dump until the restoration is complete. The locking method can be configured using the `--network-lock` option.
|
||||
|
||||
We add the `TCP_TIMESTAMP` options to be able to compensate a difference between jiffies counters, when a connection is migrated on another host. When a connection is dumped, criu calls `getsockopt(TCP_TIMESTAMP)` to get a current timestamp, then on restore it calls `setsockopt(TCP_TIMESTAMP)` to set this timestamp as a starting point.
|
||||
During restoration, the IP address used by the original connection must be available. While this is automatic if restoring on the same host, it must be manually handled for live migrations. Consequently, the `--tcp-established` option must be explicitly used to indicate the caller is aware of the transitional netfilter state.
|
||||
|
||||
## Checkpoint and restore TCP connection
|
||||
|
||||
With the above sockoptions dumping and restoring TCP connection becomes possible. The criu just reads the socket
|
||||
state and restores it back letting the protocol resurrect the data sequence.
|
||||
|
||||
One thing to note here — while the socket is closed between dump and restore the connection should be "locked", i.e.
|
||||
no packets from peer should enter the stack, otherwise the RST will be sent by a kernel. In order to do so a simple
|
||||
netfilter rule is configured that drops all the packets from peer to a socket we're dealing with. This rule sits
|
||||
in the host netfilter tables after the criu dump command finishes and it should be there when you issue the
|
||||
criu restore one. The locking method can be specified using the {{opt|--network-lock}} option.
|
||||
|
||||
Another thing to note is -- on restore there should be available the IP address, that was used by the connection.
|
||||
This is automatically so if restore happens on the same box as dump. In case of hand-made live migration the
|
||||
IP address should be copied too.
|
||||
|
||||
That said, the command line option {{opt|--tcp-established}} should be used when calling criu to explicitly state, that the
|
||||
caller is aware of this "transitional" state of the netfilter.
|
||||
|
||||
In case the target process lives in NET namespace the connection locking happens the other way. Instead of
|
||||
per-connection iptables rules the "network-lock"/"network-unlock" [action scripts](action-scripts.md) are called so that the user
|
||||
could isolate the whole netns from network. Typically this is done by downing the respective veth pair end.
|
||||
If the target process resides in a network namespace, connection locking is handled via `network-lock` and `network-unlock` [action scripts](action-scripts.md), typically by bringing down the respective `veth` pair.
|
||||
|
||||
## States
|
||||
### TCP_SYN_SENT
|
||||
There is only one difference with TCP_ESTABLISHED, we have to restore a socket and disable the repair mode before calling `connect()`. The kernel will send a one syn-sent packet with the same initial sequence number and sets the TCP_SYN_SENT state for the socket.
|
||||
|
||||
### Half-closed sockets
|
||||
A socket is half-closed when it sent or received a fin packet. These sockets are in one for these states: TCP_FIN_WAIT1, TCP_FIN_WAIT2, TCP_CLOSING, TCP_LAST_ACL, TCP_CLOSE_WAIT. To restore these states, we restore a socket into the TCP_ESTABLISHED state and then we call shutfown(SHUT_WR), if a socket has sent a fin packet and we send a fake fin packet, if a socket has received it before. For example, if we want to restore the TCP_FIN_WAIT1 state, we have to call shutfown(SHUT_WR) and we can send a fake ack to the fin packet to restore the TCP_FIN_WAIT2 state.
|
||||
### TCP_SYN_SENT
|
||||
To restore this state, CRIU restores the socket and disables repair mode before calling `connect()`. The kernel then sends a single `SYN` packet with the original sequence number and transitions the socket to the `TCP_SYN_SENT` state.
|
||||
|
||||
### Half-Closed Sockets
|
||||
A socket is considered half-closed if it has sent or received a `FIN` packet (states include `TCP_FIN_WAIT1`, `TCP_FIN_WAIT2`, `TCP_CLOSING`, `TCP_LAST_ACK`, and `TCP_CLOSE_WAIT`). To restore these, CRIU first restores the socket to the `TCP_ESTABLISHED` state. If the socket had sent a `FIN`, CRIU calls `shutdown(SHUT_WR)`. If it had received a `FIN`, CRIU sends a "fake" `FIN` packet. For example, to restore `TCP_FIN_WAIT2`, CRIU calls `shutdown(SHUT_WR)` and then processes a fake acknowledgment for the `FIN`.
|
||||
|
||||
## See also
|
||||
- [Simple TCP pair](simple-tcp-pair.md)
|
||||
|
|
@ -84,6 +56,3 @@ A socket is half-closed when it sent or received a fin packet. These sockets are
|
|||
|
||||
## External links
|
||||
- http://lwn.net/Articles/495304/
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,35 +1,26 @@
|
|||
# Technologies
|
||||
|
||||
This page contains a list of technologies and tools have been developed while we were implementing CRIU, which might be useful on their own. These tools are still maintained within the CRIU itself, but eventually they can be split out into a standalone project and CRIU will be fixed to use those as libraries/sub-tools.
|
||||
This page lists technologies and tools that were developed during the implementation of CRIU and may be useful on their own. While these tools are currently maintained within the CRIU repository, they may eventually be split into standalone projects that CRIU uses as libraries or sub-tools.
|
||||
|
||||
## Parasite code injection
|
||||
This thing was split into a sub-project called [compel](compel.md).
|
||||
Compel is an utility to execute code in foreign process address space. It is based on the same technology we use in CRIU to obtain some process-private data on dump.
|
||||
We don't currently maintain compel and it's quite out-dated, but still functional.
|
||||
## Parasite Code Injection
|
||||
This functionality was split into a sub-project called [Compel](compel.md). Compel is a utility for executing code within the address space of a foreign process. It is based on the same technology CRIU uses to retrieve process-private data during a dump. While Compel is not actively maintained and may be outdated, it remains functional.
|
||||
|
||||
-See also: [Code blobs](code-blobs.md)*
|
||||
*See also: [Code blobs](code-blobs.md)*
|
||||
|
||||
## Managing protocol buffers objects
|
||||
We have [a Python module](https://github.com/xemul/criu/blob/master/pycriu/images/pb2dict.py) to convert google protocol buffers to python objects and back.
|
||||
## Managing Protocol Buffer Objects
|
||||
CRIU includes [a Python module](https://github.com/xemul/criu/blob/master/pycriu/images/pb2dict.py) for converting Google Protocol Buffers (protobuf) to Python objects and vice-versa.
|
||||
|
||||
Unlike other projects to convert pb to python dict or json, our pb2dict does treat optional fields with empty repeated inside properly. It includes special support for custom field options to mark those needed to be treated in a special way.
|
||||
For example, you could include our [opts.proto](https://github.com/xemul/criu/blob/master/protobuf/opts.proto) in your proto-file and use criu.* options
|
||||
to mark your fields, i.e.:
|
||||
Unlike other conversion projects, our `pb2dict` correctly handles optional fields that contain empty repeated fields. it also supports custom field options to mark fields that require special handling. For example, you can include [opts.proto](https://github.com/xemul/criu/blob/master/protobuf/opts.proto) in your proto-file and use `criu.*` options:
|
||||
|
||||
`required uint64 blk_sigset = 5[(criu).hex = true];`
|
||||
`required uint64 blk_sigset = 5 [(criu).hex = true];`
|
||||
|
||||
See more examples in our protobuf/*.proto files.
|
||||
Refer to the `.proto` files in the `protobuf/` directory for more examples. We also use a unique number for all custom protobuf options to avoid collisions with other projects.
|
||||
|
||||
It is also worth noting, that we have a unique number for all kinds of custom protobuf options, so you don't have to worry that it might collide with others.
|
||||
*See also: [Images](images.md)*
|
||||
|
||||
-See also: [Images](images.md)*
|
||||
|
||||
## Sharing of kernel objects
|
||||
|
||||
It's well known that some kernel objects can be shared between tasks. For example, if a task calls `open()` to obtain a file descriptor and then `fork()`-s the file object referenced by the file descriptor becomes shared between the parent and its child. The similar thing happens when a task `dup()`-s a file -- he gets two file descriptors that reference the same file. Unlike this two subsequent `open()`-s result in two different file objects.
|
||||
|
||||
In kernel there's no API that can just reveal the tasks-to-objects map. Instead, there's an API called `kcmp()` that compares two e.g. file descriptors and reports back whether the file referenced by them is the same or not. In CRIU we have a [KCMPIDS](https://github.com/xemul/criu/blob/master/kcmp-ids.c) engine that uses this API, build [kcmp trees](kcmp-trees.md) and turns the equals-notequals answers into a list of IDs of objects.
|
||||
|
||||
-See also: [Kcmp trees](kcmp-trees.md)*
|
||||
## Sharing of Kernel Objects
|
||||
Many kernel objects can be shared between tasks. For example, if a task calls `open()` and then `fork()`, the resulting file object is shared between the parent and child. Similarly, `dup()` creates two file descriptors referencing the same shared file object. In contrast, two separate `open()` calls for the same path result in two distinct file objects.
|
||||
|
||||
Since the kernel lacks an API to directly reveal the mapping of tasks to shared objects, CRIU uses the `kcmp()` system call. This API compares two resources (e.g., file descriptors) and reports whether they refer to the same kernel object. CRIU's [KCMPIDS](https://github.com/xemul/criu/blob/master/kcmp-ids.c) engine leverages this API to build [kcmp trees](kcmp-trees.md) and generate unique IDs for shared objects.
|
||||
|
||||
*See also: [Kcmp trees](kcmp-trees.md)*
|
||||
|
|
|
|||
|
|
@ -1,48 +1,38 @@
|
|||
# TTYs
|
||||
|
||||
## Overview
|
||||
Terminals (TTYs) are a critical component of how programs interact with users, providing the primary interface for output and user input. For example, when a program is executed from a shell, the shell provides a terminal peer; the program's output is displayed in the shell, where the user can perform further processing (e.g., piping to `grep`).
|
||||
|
||||
## Supported Terminal Types
|
||||
Linux supports a wide range of terminals, including *Unix98* pseudoterminals (**pty**), *BSD* terminals, and *virtual* terminals (**vt**). CRIU provides comprehensive support for **pty** and sufficient support for **vt**.
|
||||
|
||||
"Full support" for **pty** includes saving the complete internal state, including queued data. For **vt**, CRIU provides plain restoration; while this is sufficient for standard terminal operations after restoration, any data queued but not yet delivered will be lost. This is not considered an error, as the terminal transport layer does not guarantee data delivery.
|
||||
|
||||
= Overview =
|
||||
CRIU supports the following terminal types:
|
||||
- Console
|
||||
- Current
|
||||
- Virtual
|
||||
- External
|
||||
- Serial
|
||||
- Unix98
|
||||
|
||||
Terminals are one of the most important parts of how programs interact with users. Usually programs print output into them and read user input from. For example when one executes any program from a shell, the shell provides terminal peer into it, so the program output will be seen in a calling shell and a user can do additional processing (say grepping and etc).
|
||||
### Console Terminal
|
||||
The **console** terminal is the simplest type. Restoration is performed via a simple `open("/dev/console")`.
|
||||
|
||||
## Supported terminal types
|
||||
### Current Terminal
|
||||
The **current** terminal (`/dev/tty`) is an abstraction representing the terminal currently in use by an application. When opened, the kernel provides a reference to the actual terminal device. It is restored via `open("/dev/tty")` but must be restored last, after all other terminals.
|
||||
|
||||
There are wide range of terminals exist in linux world: *unix98* psesudoterminals (**pty**), *bsd* terminals, *virtual* terminals (**vt**) and etc. In CRIU we support the **pty** completely and **vt** in a way full enough for work. By full support we mean complete save of **pty** internal state including queued data. In turn for **vt** only plain restoration supported which means it is complete enough to do a traditional operations with terminals after restore but if there was some data queued and not yet delivered to a terminal user it will be lost. It is worth to mention that such situation should not be treated as any kind of error - the terminal transport layer never ever be one with guaranteed data delivery.
|
||||
### Virtual Terminal (vt)
|
||||
Virtual terminals correspond to `/dev/ttyN` devices. These are restored using `open("/dev/ttyN")`, where `N` is the terminal number.
|
||||
|
||||
Overall CRIU supports the following terminals:
|
||||
### External Terminal
|
||||
**External** terminals are used when file descriptors are expected to change between checkpoint and restoration and are passed via command-line options. For more details, see [Inheriting FDs on restore](inheriting-fds-on-restore.md). CRIU relies on these descriptors being already open and simply reuses them.
|
||||
|
||||
- console
|
||||
- current
|
||||
- virtual
|
||||
- external
|
||||
- serial
|
||||
- unix98
|
||||
### Serial Terminal
|
||||
**Serial** terminals are primarily supported for debugging purposes, such as when developers use them to access virtual machines. They are restored using a standard `open()` call.
|
||||
|
||||
### Console terminal
|
||||
|
||||
-*console** terminal is the most simple one. Its restore is just literally `open(/dev/console)`.
|
||||
|
||||
### Current terminal
|
||||
|
||||
-*current** terminal is rather an abstraction over real terminal an application uses. Upon its opening the kernel simply provides back the reference to the real one thus the same way as for **console** its restore is just `open(/dev/tty)` with one exception - it must be restored last, ie after all other terminals are restored.
|
||||
|
||||
### Virtual terminal
|
||||
|
||||
-*vt** stands for *ttyN* devices for which restore we simply do `open(/dev/ttyN)`, where N is a number.
|
||||
|
||||
### External terminal
|
||||
|
||||
-*external** terminals stands for cases when file descriptors known to be changing between checkpoint/restore cycles and passed from command line options, see [Inheriting FDs on restore](inheriting-fds-on-restore.md) for details. For this kind of terminals we are relying on the file descriptors to be already opened and passed, so on restore we simply reuse them.
|
||||
|
||||
### Serial terminal
|
||||
|
||||
-*serial** terminals are supported for debug purpose mostly, in particular some developers pass through into virtual machine with this terminals. Its restore as simple as plain `open()` call.
|
||||
|
||||
### Unix98 terminal
|
||||
|
||||
-*pty** terminals are most commonly used over all other kinds. The **pty** represent a pair of peers: upon `open(/dev/ptmx)` the kernel automatically create `/dev/pts/N` slave peer, where N is a numeric index of the pair opened. Thus on restore we simply need *ptmx* device and give process master and slave descriptors.
|
||||
### Unix98 Terminal (pty)
|
||||
**pty** terminals are the most common type. They consist of a pair of peers: opening `/dev/ptmx` causes the kernel to automatically create a corresponding `/dev/pts/N` slave peer. During restoration, CRIU opens the `ptmx` device and provides the resulting master and slave descriptors to the process.
|
||||
|
||||
## See also
|
||||
- [External files](external-files.md)
|
||||
- [External files](external-files.md)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,9 @@
|
|||
# Tun-Tap
|
||||
|
||||
Tun-Tap devices are used by e.g. OpenVPN software. CRIU has support for them.
|
||||
Tun-Tap devices, often used by software like OpenVPN, are supported by CRIU.
|
||||
|
||||
## Devices
|
||||
|
||||
The netdevice entry sits in `netdev-ID.img` image file and has optional `tun` field.
|
||||
The network device entry is stored in the `netdev-ID.img` image file and includes an optional `tun` field.
|
||||
|
||||
## Files
|
||||
|
||||
Other than this there's always a reg-file entry in the [images](images.md) for tun deivce. In addition to this there's an entry in the `tunfile` image. The device-to-file mapping is performed by device name.
|
||||
|
||||
|
||||
For every TUN device, there is a corresponding `reg-file` entry in the [images](images.md). Additionally, an entry is created in the `tunfile` image. CRIU performs device-to-file mapping based on the device name.
|
||||
|
|
|
|||
|
|
@ -1,48 +1,33 @@
|
|||
# Userfaultfd
|
||||
|
||||
This article describes usage of userfaultfd for lazy restore and lazy migration in CRIU.
|
||||
This article describes the use of `userfaultfd` for lazy restoration and lazy migration in CRIU.
|
||||
|
||||
## Background
|
||||
The [userfaultfd](http://man7.org/linux/man-pages/man2/userfaultfd.2.html) mechanism is designed to allow user-space paging. Its initial implementation merged in Linux 4.3 was designed for KVM/QEMU use-case and lacked some functionality necessary for CRIU. In Linux 4.11 the userfaultfd was extended with so-called "non-cooperative" mode, that allows, at least in theory, lazy (or post-copy) restore in CRIU.
|
||||
The [`userfaultfd`](http://man7.org/linux/man-pages/man2/userfaultfd.2.html) mechanism allows for userspace-driven paging. While its initial implementation in Linux 4.3 was tailored for KVM/QEMU, Linux 4.11 introduced a "non-cooperative" mode that enables lazy (or post-copy) restoration in CRIU.
|
||||
|
||||
## Concepts
|
||||
|
||||
- The `restore` action accepts yet another API switch: option `--lazy-pages`. In this mode, `restore` skips injection of lazy pages into the processes address space, but rather registers lazy memory areas with userfaultfd.
|
||||
- The lazy pages are completely handled by dedicated `lazy-pages` daemon. The daemon receives userfault file descriptors from `restore` via UNIX socket. The userfault file descriptors allow reception of page-fault and other events and resolution of these events by the daemon.
|
||||
- For the migration case, the `dump` action also accepts API switch: option `--lazy-pages`. When this option is used, the `dump` keeps the memory pages and allows the `lazy-pages` daemon to request these pages via TCP connection.
|
||||
- **Lazy Restoration**: The `restore` command includes a `--lazy-pages` option. In this mode, CRIU skips injecting memory pages into the process's address space and instead registers those areas with `userfaultfd`.
|
||||
- **Lazy-Pages Daemon**: A dedicated daemon manages these lazy memory areas. It receives `userfault` file descriptors from CRIU via a UNIX socket, allowing it to intercept and resolve page faults and other memory events.
|
||||
- **Lazy Migration**: The `dump` command also supports the `--lazy-pages` option. When enabled, the dumper retains the memory pages and allows the `lazy-pages` daemon to request them via a TCP connection as needed.
|
||||
|
||||

|
||||
|
||||
### Daemon
|
||||
### The Daemon
|
||||
After restoration, processes have lazy VMAs registered with `userfaultfd`. The `userfaultfd` descriptor is sent to the `lazy-pages` daemon before the processes resume. The daemon then monitors for UFFD events and populates the address spaces. It can retrieve pages from local images, remote images, or directly from a remote dumper.
|
||||
|
||||
Tasks after restore have lazy VMAs registered with userfaultfd, the fd itself is sent before resume to `lazy-pages` daemon and closed. The daemon monitors the UFFD events and repopulates the tasks address space. The `lazy-pages` daemon can get pages either from images (both local and remote) or directly from the remote side `dump`.
|
||||
When a restored task accesses a missing page, a page fault occurs. The `lazy-pages` daemon receives this notification and populates the required memory. To optimize the process, the daemon also proactively copies remaining memory pages in the background when no faults are pending.
|
||||
|
||||
When the restored task accesses a missing memory page, it causes a page fault. The `lazy-pages` daemon receives the page fault notification and resolves it by populating the faulting task memory. If there were no page faults for some time, the daemon copies the task's remaining memory pages in the background.
|
||||
|
||||
#### Local images
|
||||
|
||||
The daemon uses local page-read engine to read pages from images.
|
||||
|
||||
#### Remote images
|
||||
|
||||
- The [page server](page-server.md) is run on the remote side with `--lazy-pages` option.
|
||||
- The lazy-pages daemon connects to the remote [page server](page-server.md) with `--page-server` option. The `--address` and `--port` options allow setting of IP address and port of the listening [page server](page-server.md).
|
||||
- Current protocol allows the lazy-pages daemon to request several continuous pages.
|
||||
|
||||
#### Migration
|
||||
- The `dump` collects the pages into pipes and starts the [page server](page-server.md) in a mode that allows `lazy-pages` daemon to connect to it and request the memory pages
|
||||
- When the restored task accesses a missing memory page, the `lazy-pages` daemon request the page from the [page server](page-server.md) running on the dump side
|
||||
- After the page is received, the `lazy-pages` daemon injects it into the task's address space using userfautlfd
|
||||
#### Local and Remote Sources
|
||||
- **Local Images**: The daemon uses a local engine to read pages from image files.
|
||||
- **Remote Images**: The [page server](page-server.md) is run on the remote host with the `--lazy-pages` option. The daemon connects to it using the `--page-server`, `--address`, and `--port` options.
|
||||
- **Migration**: During migration, the `dump` process collects pages into pipes and starts a page server. The daemon requests missing pages from this server and injects them into the task's address space using `userfaultfd`.
|
||||
|
||||
## Limitations
|
||||
|
||||
- Currently only MAP_PRIVATE | MAP_ANONYMOUS is supported. Newer kernels (4.11+) allow userfaultfd for hugetlbfs and shared memory, yet to be implemented in CRIU.
|
||||
- Userfault is known not to map one page into two places. Thus -- COW-ed pages will get COW-ed.
|
||||
- Currently, only `MAP_PRIVATE | MAP_ANONYMOUS` mappings are supported. While newer kernels (4.11+) support `userfaultfd` for `hugetlbfs` and shared memory, these features are not yet implemented in CRIU.
|
||||
- `userfaultfd` does not support mapping a single page into multiple locations simultaneously. Consequently, COW-ed pages remain COW-ed.
|
||||
|
||||
## See also
|
||||
|
||||
- [Disk-less migration](disk-less-migration.md)
|
||||
- [Lazy migration](lazy-migration.md)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,96 +1,64 @@
|
|||
# Validate files on restore
|
||||
# Validating Files on Restore
|
||||
|
||||
This article describes what CRIU does to make sure it restores the correct set of files and how this file validation is implemented in CRIU. This project was completed under the [GSoC 2020 program](https://summerofcode.withgoogle.com/projects/#5773537320632320).
|
||||
This article describes how CRIU ensures it restores the correct set of files and how this validation is implemented. This project was completed as part of the [GSoC 2020 program](https://summerofcode.withgoogle.com/projects/#5773537320632320).
|
||||
|
||||
Note: This is NOT merged to CRIU, see https://github.com/checkpoint-restore/criu/pull/1148
|
||||
**Note**: This feature is currently maintained in a [pull request](https://github.com/checkpoint-restore/criu/pull/1148) and has not yet been merged into the main CRIU repository.
|
||||
|
||||
## The previous implementation
|
||||
Since CRIU doesn’t carry the contents of files into images while dumping (Except for ghost files), files that are being restored must be validated to make sure they are the “same” as they were during the dumping process (Especially true for ELF files since there is a risk of restoring executables or libraries of a different version). This was being done by only storing and comparing the size of the file. By itself, this isn’t a very strong check.
|
||||
## Previous Implementation
|
||||
Because CRIU does not include the full contents of files (except for ghost files) in its dump images, it must validate that the files present during restoration are identical to those captured during the dump. This is particularly important for ELF files to prevent restoring incompatible versions of executables or libraries. Previously, CRIU only stored and compared the file size, which is a relatively weak check.
|
||||
|
||||
## The current implementation
|
||||
The file size method is used as a preliminary method, if it fails there’s no need to do any of the more intensive checks, instead it will immediately give out an error and stop restoring. Stronger checks are used only if it passes.
|
||||
## Current Implementation
|
||||
The current implementation uses the file size check as an initial filter. If the size does not match, restoration is aborted immediately. If the size matches, CRIU proceeds with more rigorous validation methods.
|
||||
|
||||
The simplest and strongest check is to calculate the checksum for the entire file but this would be very intensive for large files and therefore not always feasible. A reasonable compromise would be to calculate the checksum only for certain parts of the file. This is the checksum method and it is one of the two methods that have been implemented in CRIU.
|
||||
### Checksum Method
|
||||
The strongest check is calculating a checksum for the entire file. However, this can be performance-intensive for large files. As a compromise, CRIU can be configured to calculate checksums for specific portions of a file.
|
||||
|
||||
The other method is the build-ID method. The build-ID is a "strongly unique embedded identifier" that (If present) is stored in a particular note section of ELF files.
|
||||
### Build-ID Method
|
||||
The second method uses the Build-ID, a "strongly unique embedded identifier" found in many ELF files.
|
||||
|
||||
## Build-ID
|
||||
The build-ID (If present) is stored in a note of type `NT_GNU_BUILD_ID` in the ELF file. All notes are in the note section which is a program header of type `PT_NOTE` in the ELF file. After the file has been mmap-ed, the first thing that needs to be done is to check whether the file is an ELF file or not. This is done by checking for the magic number. The next thing to do is to identify whether the file is a 32-bit ELF file or a 64-bit ELF file since the data types of the variables used to parse the ELF file will change depending on the bitness of the file (There are specific 32-bit and 64-bit variants of the data structures in `elf.h`) but the procedure will remain the same.
|
||||
## Build-ID Details
|
||||
If present, the Build-ID is stored in an `NT_GNU_BUILD_ID` note within a `PT_NOTE` program header. After `mmap`-ing the file, CRIU verifies the ELF magic number and determines if it is a 32-bit or 64-bit file to use the appropriate data structures from `elf.h`.
|
||||
|
||||
The position of the program headers is stored as an offset in `phoff`. Since all the program headers are stored in an arbitrary order, each program header needs to be checked. If a program header of type `PT_NOTE` is found, the position of this note section is stored as an offset in `p_offset`. The notes are stored in an arbitrary order as well, so each note needs to be checked. If a note of type `NT_GNU_BUILD_ID` is found, the build-ID is present in its description.
|
||||
CRIU then iterates through the program headers (located at the `phoff` offset) to find the `PT_NOTE` section. Within that section (at `p_offset`), it searches for the Build-ID note.
|
||||
|
||||
## Checksum
|
||||
CRC32C is used to calculate the checksum. The only difference between CRC32C and CRC32 is the polynomial being used. CRC32C uses the Castagnoli polynomial (0x82F63B78 in little-endian notation) and CRC32 uses 0xEDB88320 (In little-endian notation).
|
||||
## Checksum Details
|
||||
CRIU uses CRC32C (utilizing the Castagnoli polynomial `0x82F63B78`) for checksum calculation. The file is mapped in 10 MB increments, and the checksum is calculated based on the configuration:
|
||||
- Entire file.
|
||||
- First `N` bytes.
|
||||
- Every `N`-th byte.
|
||||
|
||||
The file is mapped 10 MB at a time and the checksum is calculated on the required bytes (Depending on the configuration set - Entire file, First N bytes of the file, or Every Nth byte of the file). N is the checksum parameter.
|
||||
The `N` parameter defaults to 1024. The iterator logic in `criu/files-reg.c` (`checksum_iterator_init`, `next`, and `stop`) manages which bytes are processed. If an iterator moves outside the currently mapped region, CRIU maps the next required segment.
|
||||
|
||||
Adding a new configuration is quite simple and only requires the iterator to be moved to the necessary bytes (0 refers to the first byte of the file and 1 refers to the second byte and so on):
|
||||
1. Input handling for the new configuration
|
||||
1. The `checksum_iterator_init` function in *"criu/files-reg.c”* sets the initial iterator position (The first byte to calculate the checksum)
|
||||
1. The `checksum_iterator_next` function in *“criu/files-reg.c”* moves the iterator to the next position (The next byte to calculate the checksum)
|
||||
1. The `checksum_iterator_stop` function in *“criu/files-reg.c”* returns true when the iterator has reached its final position
|
||||
There is a separate check in the `calculate_checksum` function to make sure the iterator refers to a valid byte (Not negative and smaller than the total number of bytes in the file). If the iterator is outside the mapped region but still valid, the required region of the file will be mapped.
|
||||
## Configuration and Fallbacks
|
||||
The Build-ID method is the default because it is highly reliable and less resource-intensive than a full checksum. The `--file-validation` option allows users to customize this behavior:
|
||||
- `--file-validation buildid`: Uses Build-ID (default).
|
||||
- `--file-validation checksum-full`: Checksums the entire file.
|
||||
- `--file-validation checksum`: Checksums the first `N` bytes (use `--checksum-parameter` to set `N`).
|
||||
- `--file-validation checksum-period`: Checksums every `N`-th byte.
|
||||
- `--file-validation filesize`: Uses only the file size check (fastest).
|
||||
|
||||
## Using different validation methods and parameters
|
||||
The build-ID method is much less intensive compared to the checksum method while still being a much stronger check than simply comparing the file size and is therefore the default. In other words, `--file-validation buildid` will not make a difference as this is the default method.
|
||||
If the Build-ID method is selected but the file lacks a Build-ID, CRIU falls back to checksumming the first 1024 bytes. Conversely, if the checksum method is inconclusive, it falls back to Build-ID. If both fail, CRIU relies on the file size and issues a warning.
|
||||
|
||||
CRIU can also be configured to use the checksum method by default by using the `--file-validation option`:
|
||||
- `--file-validation checksum-full` to calculate and use the checksum on the entire file.
|
||||
- `--file-validation checksum` to calculate and use the checksum on the first N bytes of the file. The parameter N is set by using the `--checksum-parameter` option.
|
||||
- `--file-validation checksum-period` to calculate and use the checksum on every Nth byte of the file (Including the first byte of the file). The parameter N is set by using the `--checksum-parameter option`.
|
||||
By default, the checksum parameter N is set to 1024. If a method that doesn’t require the checksum parameter is being used, then the checksum parameter is simply ignored.
|
||||
For example, to use the checksum method on the first 2048 bytes of the file: `--file-validation checksum --checksum-parameter 2048`
|
||||
## Performance Impact
|
||||
The following values represent the average time to complete ZDTM tests across multiple runs, indicating the general overhead of each method. Tests were performed on an undervolted i5 4800H using `tmpfs` to eliminate disk latency.
|
||||
|
||||
If the build-ID method is being used and is inconclusive (Maybe because the ELF file doesn’t contain a build-ID), then the checksum method on the first 1024 bytes of the file is used as a fallback. If the checksum method is being used and is inconclusive, then the build-ID method is used as a fallback. If both are inconclusive, only the file size check is used (And a warning is put out to inform the user that only a weak check has been used for that particular file).
|
||||
|
||||
To explicitly use only the file size check all the time, the following command-line option can be used: `--file-validation filesize` (This is the fastest and least intensive check).
|
||||
|
||||
## Performance impact
|
||||
The values shown below are the average times it took to finish the ZDTM tests over multiple runs, and are only to indicate the impact each method has in general.
|
||||
|
||||
Each test has 3 files (Of sizes: 0.09 MB, 2 MB and 0.17 MB approximately) and each test is run 3 times (In Host, Namespace and User Namespace). For each file the checksum/build-ID is obtained twice (During dump and restore) therefore the function to find checksum/build-ID is called 18 times overall per test.
|
||||
|
||||
For reference, these tests were run on tmpfs (To remove any disk latency) and on an undervolted i5 4800H.
|
||||
|
||||
{| class="wikitable" style="text-align: center;
|
||||
|+zdtm/transition/shmem:
|
||||
|-
|
||||
|File Size
|
||||
|3.782s
|
||||
|-
|
||||
|Build-ID
|
||||
|4.153s (~9% increase)
|
||||
|-
|
||||
|Checksum (First 1024 bytes)
|
||||
|4.465s (~18% increase)
|
||||
|-
|
||||
|Checksum (Entire File)
|
||||
|4.722s (~24% increase)
|
||||
|-
|
||||
|Checksum (Every 1024th byte)
|
||||
|4.498s (~19% increase)
|
||||
|}
|
||||
|
||||
{| class="wikitable" style="text-align: center;
|
||||
|+zdtm/static/maps04:
|
||||
|-
|
||||
|File Size
|
||||
|35.317s
|
||||
|-
|
||||
|Build-ID
|
||||
|35.720s (~1% increase)
|
||||
|-
|
||||
|Checksum (First 1024 bytes)
|
||||
|35.919s (~2% increase)
|
||||
|-
|
||||
|Checksum (Entire File)
|
||||
|36.679s (~4% increase)
|
||||
|-
|
||||
|Checksum (Every 1024th byte)
|
||||
|36.476s (~3% increase)
|
||||
|}
|
||||
|
||||
## Scope for improvement and future work
|
||||
- Calculating the checksum can be made faster by using a lookup table.
|
||||
**Test: `zdtm/transition/shmem`**
|
||||
| Method | Time | Increase |
|
||||
| :--- | :--- | :--- |
|
||||
| File Size | 3.782s | - |
|
||||
| Build-ID | 4.153s | ~9% |
|
||||
| Checksum (First 1024) | 4.465s | ~18% |
|
||||
| Checksum (Entire File) | 4.722s | ~24% |
|
||||
| Checksum (Every 1024th) | 4.498s | ~19% |
|
||||
|
||||
**Test: `zdtm/static/maps04`**
|
||||
| Method | Time | Increase |
|
||||
| :--- | :--- | :--- |
|
||||
| File Size | 35.317s | - |
|
||||
| Build-ID | 35.720s | ~1% |
|
||||
| Checksum (First 1024) | 35.919s | ~2% |
|
||||
| Checksum (Entire File) | 36.679s | ~4% |
|
||||
| Checksum (Every 1024th) | 36.476s | ~3% |
|
||||
|
||||
## Future Work
|
||||
- Implementing a lookup table to further accelerate CRC32C calculations.
|
||||
|
|
|
|||
|
|
@ -1,25 +1,19 @@
|
|||
# Vdso
|
||||
# vDSO
|
||||
|
||||
### Overview
|
||||
## Overview
|
||||
**vDSO** (virtual Dynamic Shared Object) is a small shared library that the kernel automatically maps into the address space of every userspace program. It provides highly optimized versions of frequently used system calls—such as `gettimeofday` and `getcpu`—that can be executed without the overhead of a full context switch. Most applications access these via **libc** rather than calling them directly.
|
||||
|
||||
-*vDSO** stands for [*virtual dynamic shared object*](http://man7.org/linux/man-pages/man7/vdso.7.html) and basically is a shared library exported by kernel into every userspace program. It usually exports a few frequently used functions (such as gettimeofday, getcpu and etc) and userspace applications don't call them directly but make use of **libc** instead.
|
||||
## The Challenge
|
||||
While the kernel maintains backward compatibility for vDSO functions, the internal structure and memory layout of the vDSO can change between kernel versions. This poses a significant problem for CRIU: if an application is migrated to a host running a different kernel, the vDSO image captured during the dump may be incompatible with the new kernel's internals.
|
||||
|
||||
### A problem
|
||||
## Call Proxification
|
||||
To address this, CRIU uses a technique called **call proxification**. During restoration, CRIU redirects calls from the original (dumped) vDSO to the version provided by the current kernel. This process involves several steps:
|
||||
|
||||
While the kernel guarantees backwards compatibility (i.e. helper functions won't suddenly disappear), the internal structure of the **vDSO** itself may vary. Userspace programs won't notice such changes but it brings a huge problem to CRIU. The **vDSO** structure is highly coupled with the kernel internals. If an application is being migrated to a new kernel release (with a brand new **vDSO**) the old **vDSO** (which is stored in the application's memory) is no longer usable.
|
||||
1. CRIU analyzes the vDSO provided by the restoration host's kernel, parsing its symbols, sections, and entry points.
|
||||
2. During the restoration of the dumped vDSO memory area, CRIU patches the function entry points with redirection instructions (e.g., a `jmp` instruction on x86) that point to the corresponding functions in the new vDSO.
|
||||
|
||||
### Calls proxification
|
||||
|
||||
To solve this problem CRIU does that named call proxification, where all functions from the original **vDSO** code are redirected to a new one provided by the kernel where application is restored. This done in a several steps on restore:
|
||||
|
||||
1. Scan runtime environment for current **vDSO** and parse its structure (size, sections, symbols, etc)
|
||||
1. On restore of dumped **vDSO** memory area we patch function entry points so that they are redirected to current **vDSO** (for x86 architecture is simply a `jmp` instruction)
|
||||
|
||||
After that an restored application can continue using original **vDSO** helpers without problems.
|
||||
|
||||
In case if an application is dumped and restored on same kernel CRIU detects that **vDSO** structure has not be changed and doesn't use calls proxification.
|
||||
|
||||
### Unsolved proxification problems
|
||||
|
||||
1. If an application in very unlikely case is Checkpointed over first bytes of vdso entry *and* vdso is different on the destination migration node, those might be the bytes that are being patched during proxification. If it's on vdso just after those entry-bytes, the stale data from vvar is taken (might be just fine afterall).
|
||||
This allows the application to continue using its existing vDSO entry points while actually executing the code compatible with the current kernel. If CRIU detects that the dump and restoration kernels are identical, proxification is skipped.
|
||||
|
||||
## Proxification Challenges
|
||||
- In very rare cases, a process might be checkpointed exactly while executing the first few bytes of a vDSO function. If these bytes are the ones being patched for proxification, it could lead to inconsistent state.
|
||||
- If the instruction pointer is immediately after these patched entry bytes, the function may attempt to use stale data from the `vvar` page, although this is typically handled by the kernel.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue