00 userfaultfd neden var — kullanım senaryoları
userfaultfd (UFFD), kullanıcı alanının belirli bellek bölgeleri için sayfa hatalarını yakalamasına ve yanıt vermesine olanak tanır. Kernel sayfa hatası işleyicisi devreye girmeden önce kullanıcı alanı çağrılır; bu mekanizma güçlü bellek sanallaştırma senaryolarını mümkün kılar.
Geleneksel sayfa hata mekanizması
Normal Linux bellek modelinde bir işlem mmap'lenmiş ancak henüz fiziksel sayfası olmayan bir adrese eriştiğinde donanım (MMU) bir page fault exception üretir. Kernel bu hatayı yakalar, uygun fiziksel sayfayı ayırır veya dosyadan yükler ve sayfa tablosunu günceller. Tüm bu işlem uygulama kodunun bilgisi dışında gerçekleşir. userfaultfd ise bu akışa kullanıcı alanını dahil eder.
Geleneksel akış:
Uygulama → belleğe erişir → MMU page fault → kernel handler → sayfa ayır → devam
userfaultfd akışı:
Uygulama → belleğe erişir → MMU page fault → kernel handler
→ "Bu adres UFFD bölgesinde!" → uffd fd'ye mesaj yaz
→ Uygulama uffd thread'i mesajı okur
→ Hangi sayfayı koyacağına karar verir (ağdan getir, decompress et, sıfırla...)
→ UFFDIO_COPY ile sayfayı yerleştir
→ Kernel: fault eden thread devam eder
Neden ihtiyaç var — kullanım senaryoları
Kernel sürümü ve özellik matrisi
| Özellik | Minimum Kernel | Not |
|---|---|---|
| Temel UFFD (MISSING fault) | 4.3 | Anonim bellek |
| File-backed UFFD | 4.11 | tmpfs, shmem |
| Write-protect modu | 5.7 | UFFDIO_WRITEPROTECT |
| Minor fault modu | 5.13 | hugetlb; 5.14 shmem |
| UFFD_FEATURE_THREAD_ID | 5.14 | Hangi thread fault ettiği |
| UFFD_FEATURE_WP_HUGETLBFS | 5.19 | Hugetlbfs write-protect |
| UFFD_FEATURE_WP_UNPOPULATED | 6.4 | Boş sayfalar da WP olabilir |
01 userfaultfd API — userfaultfd(), UFFDIO_API, UFFDIO_REGISTER
userfaultfd API üç temel adımdan oluşur: fd oluşturma, API anlaşması ve izlenecek bellek bölgelerini kaydetme. Kayıt sonrasında o bölgeye yapılan hatalar fd üzerinden kullanıcı alanına iletilir.
Kurulum: userfaultfd() sistem çağrısı
#include <linux/userfaultfd.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
/* userfaultfd() wrapper (bazı libc'lerde eksik) */
static int userfaultfd_open(int flags)
{
/* Kernel 5.11+: /dev/userfaultfd veya UFFD_USER_MODE_ONLY flag */
int fd = (int)syscall(SYS_userfaultfd, flags);
if (fd < 0 && errno == ENOSYS) {
/* /dev/userfaultfd ile dene (kernel 5.11+) */
fd = open("/dev/userfaultfd", O_RDWR | O_CLOEXEC);
}
return fd;
}
int setup_uffd(void)
{
int uffd;
/* O_NONBLOCK: read() bloklamaz — poll/epoll ile kullanılabilir */
/* O_CLOEXEC: fork sonrası child'da fd kapanır */
uffd = userfaultfd_open(O_CLOEXEC | O_NONBLOCK);
if (uffd < 0) {
perror("userfaultfd");
return -1;
}
return uffd;
}
UFFDIO_API: API anlaşması ve özellik müzakeresi
int negotiate_uffd_api(int uffd, uint64_t *features_out)
{
struct uffdio_api uffdio_api = {
.api = UFFD_API, /* sabit: 0xAA */
/* İstenen özellikler: */
.features = UFFD_FEATURE_PAGEFAULT_FLAG_WP /* write-protect desteği */
| UFFD_FEATURE_MISSING_SHMEM /* shmem missing fault */
| UFFD_FEATURE_MISSING_HUGETLBFS /* hugetlb missing fault */
| UFFD_FEATURE_THREAD_ID, /* fault eden thread TID */
};
if (ioctl(uffd, UFFDIO_API, &uffdio_api) < 0) {
perror("UFFDIO_API");
return -1;
}
/* Kernel desteklediği özellikleri geri yazar */
printf("UFFD API versiyonu: 0x%llx\n", uffdio_api.api);
printf("Desteklenen özellikler: 0x%llx\n", uffdio_api.features);
if (features_out)
*features_out = uffdio_api.features;
return 0;
}
UFFDIO_REGISTER: bellek bölgesi kaydetme
int register_memory_region(int uffd, void *addr, size_t len,
bool write_protect, bool minor_mode)
{
struct uffdio_register reg = {
.range = {
.start = (uint64_t)addr,
.len = len,
},
/* Hangi hata türleri yakalanacak? */
.mode = UFFDIO_REGISTER_MODE_MISSING, /* sayfa yok hatası */
};
if (write_protect)
reg.mode |= UFFDIO_REGISTER_MODE_WP; /* yazma koruması ihlali */
if (minor_mode)
reg.mode |= UFFDIO_REGISTER_MODE_MINOR; /* minor fault (5.13+) */
if (ioctl(uffd, UFFDIO_REGISTER, ®) < 0) {
perror("UFFDIO_REGISTER");
return -1;
}
/* reg.ioctls: hangi ioctl komutlarının desteklendiği */
printf("Desteklenen ioctls: 0x%llx\n", reg.ioctls);
return 0;
}
/* Kaydı iptal et */
int unregister_region(int uffd, void *addr, size_t len)
{
struct uffdio_range range = {
.start = (uint64_t)addr,
.len = len,
};
return ioctl(uffd, UFFDIO_UNREGISTER, &range);
}
İzleme modu özeti
| Mod | Flag | Tetikleyici | Yanıt ioctl |
|---|---|---|---|
| MISSING | UFFDIO_REGISTER_MODE_MISSING | Fiziksel sayfa olmayan adrese erişim | UFFDIO_COPY, UFFDIO_ZEROPAGE |
| WP (write-protect) | UFFDIO_REGISTER_MODE_WP | Write-protected sayfaya yazma | UFFDIO_WRITEPROTECT (kaldır) |
| MINOR | UFFDIO_REGISTER_MODE_MINOR | Sayfa var ama tlb map yok (shared mem) | UFFDIO_CONTINUE |
02 Sayfa hatası yakalama — uffd_msg okuma ve fault address
Kayıtlı bölgede sayfa hatası oluştuğunda kernel, fault eden thread'i dondurur ve userfaultfd fd'ye bir mesaj yazar. Handler thread bu mesajı okuyarak hangi adresin ne tür bir hata ürettiğini öğrenir.
uffd_msg yapısı
/* linux/userfaultfd.h'dan */
struct uffd_msg {
__u8 event; /* UFFD_EVENT_PAGEFAULT, UFFD_EVENT_FORK, vb. */
__u8 reserved1;
__u16 reserved2;
__u32 reserved3;
union {
struct {
__u64 flags; /* UFFD_PAGEFAULT_FLAG_WRITE, UFFD_PAGEFAULT_FLAG_WP */
__u64 address; /* fault eden sanal adres (sayfa hizalı) */
union {
__u32 ptid; /* fault eden thread TID (THREAD_ID özelliği varsa) */
} feat;
} pagefault;
struct {
__u32 ufd; /* child'ın uffd fd'si (fork event'te) */
} fork;
struct {
__u64 from;
__u64 to;
__u64 len;
} remap;
/* ... */
} arg;
};
Handler thread — temel yapı
struct uffd_handler_ctx {
int uffd;
void *base_addr; /* izlenen bölgenin başı */
size_t region_len;
/* ... uygulama bağlamı */
};
static void *uffd_handler_thread(void *arg)
{
struct uffd_handler_ctx *ctx = arg;
int uffd = ctx->uffd;
struct pollfd pfd = { .fd = uffd, .events = POLLIN };
while (1) {
/* Sayfa hatası gelene kadar bekle */
int ret = poll(&pfd, 1, -1 /* sonsuz */);
if (ret < 0) {
if (errno == EINTR) continue;
perror("poll");
break;
}
/* Mesajı oku */
struct uffd_msg msg;
ssize_t nread = read(uffd, &msg, sizeof(msg));
if (nread != sizeof(msg)) {
if (nread < 0 && errno == EAGAIN) continue;
perror("read uffd_msg");
break;
}
/* Event türüne göre işle */
switch (msg.event) {
case UFFD_EVENT_PAGEFAULT:
handle_pagefault(ctx, &msg);
break;
case UFFD_EVENT_FORK:
/* fork() sonrası child için yeni uffd */
handle_fork_event(ctx, msg.arg.fork.ufd);
break;
case UFFD_EVENT_REMAP:
/* mremap sonrası adres değişimi */
handle_remap_event(ctx, &msg.arg.remap);
break;
case UFFD_EVENT_REMOVE:
/* MADV_DONTNEED veya MADV_FREE */
break;
case UFFD_EVENT_UNMAP:
/* munmap */
break;
}
}
return NULL;
}
Pagefault mesajını ayrıştırma
static void handle_pagefault(struct uffd_handler_ctx *ctx,
const struct uffd_msg *msg)
{
uint64_t fault_addr = msg->arg.pagefault.address;
uint64_t fault_flags = msg->arg.pagefault.flags;
uint32_t fault_tid = msg->arg.pagefault.feat.ptid;
/* Sayfa hizalı adres */
uint64_t page_addr = fault_addr & ~(PAGE_SIZE - 1);
/* Hata türünü belirle */
bool is_write = !!(fault_flags & UFFD_PAGEFAULT_FLAG_WRITE);
bool is_wp_fault = !!(fault_flags & UFFD_PAGEFAULT_FLAG_WP);
bool is_minor = !!(fault_flags & UFFD_PAGEFAULT_FLAG_MINOR);
printf("Sayfa hatası: addr=0x%lx write=%d wp=%d minor=%d tid=%u\n",
fault_addr, is_write, is_wp_fault, is_minor, fault_tid);
/* Hangi sayfanın gerektiğini hesapla */
size_t page_offset = page_addr - (uint64_t)ctx->base_addr;
size_t page_index = page_offset / PAGE_SIZE;
if (is_wp_fault) {
/* Yazma koruması: sayfayı yazılabilir yap */
handle_wp_fault(ctx, page_addr);
} else if (is_minor) {
/* Minor fault: sayfa var, harita yok */
handle_minor_fault(ctx, page_addr);
} else {
/* Missing fault: sayfayı sağla */
provide_page(ctx, page_addr, page_index, is_write);
}
}
03 Hata yanıtları — UFFDIO_COPY, UFFDIO_ZEROPAGE, UFFDIO_CONTINUE
Handler, sayfa hatasını aldıktan sonra üç yöntemden biriyle yanıt vermelidir: başka bir bellekten kopyalama, sıfır sayfa sağlama veya mevcut sayfanın haritasını oluşturma. Yanıt gelene kadar fault eden thread bloklanmış kalır.
UFFDIO_COPY — bellekten sayfa kopyalama
static int provide_page_copy(int uffd, void *dst_addr,
const void *src_page, int write_protect)
{
struct uffdio_copy copy = {
.dst = (uint64_t)dst_addr, /* hata eden sayfa adresi (hizalı) */
.src = (uint64_t)src_page, /* kopyalanacak kaynak sayfa */
.len = PAGE_SIZE,
.mode = write_protect ? UFFDIO_COPY_MODE_WP : 0,
/* UFFDIO_COPY_MODE_WP: kopyalandıktan sonra sayfa write-protected olur */
/* UFFDIO_COPY_MODE_DONTWAKE: fault eden thread'i uyandırma (batch için) */
};
if (ioctl(uffd, UFFDIO_COPY, ©) < 0) {
if (errno == EEXIST) {
/* Başka bir thread zaten bu sayfayı sağladı — yarış durumu */
return 0;
}
perror("UFFDIO_COPY");
return -1;
}
/* copy.copy: gerçekte kopyalanan byte sayısı */
return 0;
}
UFFDIO_ZEROPAGE — sıfır sayfa sağlama
static int provide_zero_page(int uffd, void *addr)
{
struct uffdio_zeropage zp = {
.range = {
.start = (uint64_t)addr,
.len = PAGE_SIZE,
},
.mode = 0,
/* UFFDIO_ZEROPAGE_MODE_DONTWAKE: toplu işlem için */
};
if (ioctl(uffd, UFFDIO_ZEROPAGE, &zp) < 0) {
if (errno == EEXIST) return 0; /* zaten sağlanmış */
perror("UFFDIO_ZEROPAGE");
return -1;
}
return 0;
}
UFFDIO_CONTINUE — minor fault yanıtı
/* Minor fault: sayfa fiziksel bellekte var (paylaşımlı bellek gibi)
ama bu süreç için sayfa tablosuna henüz eklenmemiş.
UFFDIO_CONTINUE mevcut sayfayı sayfa tablosuna ekler. */
static int continue_page_mapping(int uffd, void *addr)
{
struct uffdio_continue cont = {
.range = {
.start = (uint64_t)addr,
.len = PAGE_SIZE,
},
.mode = 0,
/* UFFDIO_CONTINUE_MODE_WP: ekledikten sonra write-protected yap */
/* UFFDIO_CONTINUE_MODE_DONTWAKE: toplu işlem için */
};
if (ioctl(uffd, UFFDIO_CONTINUE, &cont) < 0) {
if (errno == EEXIST) return 0;
perror("UFFDIO_CONTINUE");
return -1;
}
return 0;
}
DONTWAKE ve toplu uyandırma
/* Birden fazla sayfa sağla — sonuncusunda uyandır */
void provide_range_batched(int uffd, void *base, size_t npages,
void *src_pages)
{
for (size_t i = 0; i < npages; i++) {
void *dst = (char *)base + i * PAGE_SIZE;
void *src = (char *)src_pages + i * PAGE_SIZE;
bool last = (i == npages - 1);
struct uffdio_copy copy = {
.dst = (uint64_t)dst,
.src = (uint64_t)src,
.len = PAGE_SIZE,
/* Son sayfa değilse uyandırma */
.mode = last ? 0 : UFFDIO_COPY_MODE_DONTWAKE,
};
ioctl(uffd, UFFDIO_COPY, ©);
}
/* DONTWAKE kullandıysak explicit wake gerekir */
/* (son COPY zaten wake etti; aşağıdaki sadece örnek) */
}
/* Manuel uyandırma */
static int wake_thread(int uffd, void *addr, size_t len)
{
struct uffdio_range range = {
.start = (uint64_t)addr,
.len = len,
};
return ioctl(uffd, UFFDIO_WAKE, &range);
}
Yanıt yanıt senaryoları özeti
| Senaryo | Kullanılan ioctl | Kaynak |
|---|---|---|
| İlk erişim — sıfır bellek isteniyor | UFFDIO_ZEROPAGE | Kernel sıfır sayfa sağlar |
| Live migration — uzaktan sayfa al | UFFDIO_COPY | Ağdan alınan veri |
| Checkpoint restore — diskten oku | UFFDIO_COPY | Dosyadan okunan sayfa |
| Shared memory minor fault | UFFDIO_CONTINUE | Mevcut fiziksel sayfa |
| Write-protect ihlali sonrası | UFFDIO_WRITEPROTECT (kaldır) | Korumayı kaldır |
04 Write-protect modu — CoW izleme
Write-protect modu, fiziksel sayfa varken üzerine yazılmasını engeller ve yazma girişimini UFFD'e bildirir. Bu mekanizma, bir bellek bölgesinin hangi sayfalarının değiştirildiğini izlemek için ideal bir copy-on-write sağlayıcısı oluşturur.
Write-protect kaydı
/* Bellek bölgesini write-protect modunda kaydet */
int register_wp_region(int uffd, void *addr, size_t len)
{
struct uffdio_register reg = {
.range = { .start = (uint64_t)addr, .len = len },
.mode = UFFDIO_REGISTER_MODE_MISSING
| UFFDIO_REGISTER_MODE_WP,
};
return ioctl(uffd, UFFDIO_REGISTER, ®);
}
/* Mevcut (dolu) sayfaları write-protect et */
int writeprotect_range(int uffd, void *addr, size_t len)
{
struct uffdio_writeprotect wp = {
.range = { .start = (uint64_t)addr, .len = len },
.mode = UFFDIO_WRITEPROTECT_MODE_WP, /* korumayı etkinleştir */
};
return ioctl(uffd, UFFDIO_WRITEPROTECT, &wp);
}
/* Write-protect'i kaldır (yazma hatasına yanıt olarak) */
int remove_writeprotect(int uffd, void *addr, size_t len)
{
struct uffdio_writeprotect wp = {
.range = { .start = (uint64_t)addr, .len = len },
.mode = 0, /* 0 = koru kaldır; DONTWAKE olmadan thread uyandırılır */
};
return ioctl(uffd, UFFDIO_WRITEPROTECT, &wp);
}
Dirty page izleme — GC örüntüsü
/* Tüm belleği WP ile işaretle → yalnızca değişenleri bul */
#define HEAP_SIZE (64 * 1024 * 1024) /* 64 MB yığın */
struct heap_tracker {
int uffd;
void *heap;
uint8_t *dirty_bitmap; /* her bit bir sayfa */
size_t npages;
pthread_mutex_t lock;
};
void *heap_tracker_create(size_t size)
{
struct heap_tracker *ht = calloc(1, sizeof(*ht));
ht->npages = size / PAGE_SIZE;
ht->dirty_bitmap = calloc(1, (ht->npages + 7) / 8);
ht->heap = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
ht->uffd = userfaultfd_open(O_CLOEXEC | O_NONBLOCK);
struct uffdio_api api = { .api = UFFD_API,
.features = UFFD_FEATURE_PAGEFAULT_FLAG_WP };
ioctl(ht->uffd, UFFDIO_API, &api);
register_wp_region(ht->uffd, ht->heap, size);
/* Başlangıçta tüm sayfalar WP değil — ilk erişimde missing fault gelir */
/* UFFDIO_COPY ile sağla + WP flag = sağlandıktan hemen write-protected */
return ht;
}
/* Handler'da WP fault geldiğinde: */
static void handle_wp_fault_gc(struct heap_tracker *ht,
uint64_t fault_addr)
{
uint64_t page_addr = fault_addr & ~(PAGE_SIZE - 1);
size_t idx = (page_addr - (uint64_t)ht->heap) / PAGE_SIZE;
pthread_mutex_lock(&ht->lock);
/* Dirty bitmap'de işaretle */
ht->dirty_bitmap[idx / 8] |= (1 << (idx % 8));
pthread_mutex_unlock(&ht->lock);
/* Korumayı kaldır — yazma işlemi devam edebilir */
remove_writeprotect(ht->uffd, (void *)page_addr, PAGE_SIZE);
}
/* GC döngüsünde: dirty sayfaları tara, temizle, WP'yi yeniden etkinleştir */
void gc_scan_and_reset(struct heap_tracker *ht)
{
for (size_t i = 0; i < ht->npages; i++) {
if (ht->dirty_bitmap[i / 8] & (1 << (i % 8))) {
void *page = (char *)ht->heap + i * PAGE_SIZE;
scan_page_for_gc(page);
ht->dirty_bitmap[i / 8] &= ~(1 << (i % 8));
writeprotect_range(ht->uffd, page, PAGE_SIZE);
}
}
}
05 Minor fault modu — paylaşımlı bellek ve hugetlb
Minor fault modu, fiziksel sayfanın bellekte var olduğu ancak bu sayfanın o sürecin sayfa tablosuna henüz eklenmediği durumları yakalar. Paylaşımlı bellek (shmem, hugetlbfs) üzerine kurulan live migration ve CRIU senaryolarında kritik öneme sahiptir.
Minor fault senaryosu
Post-copy live migration örneği:
1. Kaynak VM çalışıyor; bellek uzaktaki makineye kopyalanıyor
2. Hedef VM başlatıldı; tüm sayfalar shared memory'de "var" (sıfır veya eski değer)
3. Hedef VM sayfaya erişiyor → sayfa fiziksel bellekte var ama sayfa tablosuna eklenmemiş
4. MINOR fault → UFFD handler çağrılır
5. Handler: bu sayfa güncel mi? Hayır → uzak makineden al, COPY ile güncelle
Evet → UFFDIO_CONTINUE ile mevcut sayfayı eşle
Minor fault kaydı ve yanıtı
/* tmpfs / shmem üzerinde minor fault */
int fd = memfd_create("shared_heap", MFD_CLOEXEC);
ftruncate(fd, REGION_SIZE);
void *addr = mmap(NULL, REGION_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
/* Minor mode kayıt (kernel 5.14+ shmem için) */
struct uffdio_register reg = {
.range = { .start = (uint64_t)addr, .len = REGION_SIZE },
.mode = UFFDIO_REGISTER_MODE_MINOR,
};
ioctl(uffd, UFFDIO_REGISTER, ®);
/* Sayfayı önceden fiziksel belleğe yükle (punch-in) */
void *fill_buf = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, page_offset);
memcpy(fill_buf, source_data, PAGE_SIZE);
munmap(fill_buf, PAGE_SIZE);
/* Handler'da minor fault geldiğinde:
- Sayfa güncellenmiş mi kontrol et
- Güncel değilse: UFFDIO_COPY ile yeni içerik yaz
- Güncel ise: UFFDIO_CONTINUE ile mevcut sayfayı eşle */
static void handle_minor(int uffd, uint64_t fault_addr,
bool page_is_current)
{
uint64_t page_addr = fault_addr & ~(PAGE_SIZE - 1);
if (!page_is_current) {
/* Güncel sayfayı sağla */
void *new_page = fetch_page_from_source(fault_addr);
struct uffdio_copy copy = {
.dst = page_addr,
.src = (uint64_t)new_page,
.len = PAGE_SIZE,
};
ioctl(uffd, UFFDIO_COPY, ©);
free(new_page);
} else {
/* Mevcut sayfa yeterli — sadece eşle */
struct uffdio_continue cont = {
.range = { .start = page_addr, .len = PAGE_SIZE },
};
ioctl(uffd, UFFDIO_CONTINUE, &cont);
}
}
Hugetlbfs minor fault
Kernel 5.13'te hugetlbfs için minor fault desteği eklendi; tmpfs/shmem desteği 5.14'te geldi. Büyük sayfa (2 MB veya 1 GB) kullanan VM workload'larında page fault frekansı dramatik düşer; her page fault çözümü de daha büyük granülaritede gerçekleşir, bu da hem latency hem de throughput açısından avantaj sağlar.
/* 2 MB huge page ile minor fault */
void *huge_addr = mmap(NULL, HUGE_SIZE, PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_HUGETLB | MAP_PRIVATE,
-1, 0);
struct uffdio_register reg = {
.range = { .start = (uint64_t)huge_addr, .len = HUGE_SIZE },
.mode = UFFDIO_REGISTER_MODE_MISSING
| UFFDIO_REGISTER_MODE_MINOR, /* kernel 5.13+ */
};
ioctl(uffd, UFFDIO_REGISTER, ®);
/* UFFDIO_COPY ile huge page sağlama: len = 2 MB */
struct uffdio_copy copy = {
.dst = page_addr,
.src = (uint64_t)src_huge_page,
.len = HPAGE_PMD_SIZE, /* 2 * 1024 * 1024 */
};
ioctl(uffd, UFFDIO_COPY, ©);
06 Bellek şişirmesi (balloon) — MADV_DONTNEED kombinasyonu
Bellek balonu (balloon driver), sanallaştırma ortamlarında hypervisor'ın VM'den fiziksel bellek geri almasını sağlar. userfaultfd bu mekanizmanın kullanıcı alanı uygulamasında temel rol oynar.
Klasik balloon driver vs userfaultfd balloon
| Özellik | Kernel balloon driver | userfaultfd balloon |
|---|---|---|
| Kernel modülü gereksinimi | Evet (virtio-balloon vb.) | Hayır |
| Granülarite | Sayfa (4 KB) | Sayfa (4 KB) veya hugetlb |
| Uygulama farkındalığı | Hayır — uygulama bilmez | Evet — UFFD ile erişim anında tespit |
| Geri alma hızı | Hızlı (kernel doğrudan) | Biraz daha yavaş (user round-trip) |
| Esneklik | Düşük | Yüksek — politika user-space'de |
userfaultfd balloon uygulaması
struct balloon_ctx {
int uffd;
void *balloon_region; /* "şişirilen" bellek bölgesi */
size_t balloon_size;
bool inflated; /* şu an şişirilmiş mi? */
};
/* Bölgeyi balloon moduna al */
int balloon_inflate(struct balloon_ctx *ctx, size_t size)
{
ctx->balloon_region = mmap(NULL, size,
PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
ctx->balloon_size = size;
/* UFFD kaydet */
register_memory_region(ctx->uffd, ctx->balloon_region,
size, false, false);
/* Fiziksel sayfaları serbest bırak — hypervisor'a iade */
madvise(ctx->balloon_region, size, MADV_DONTNEED);
/* Artık sayfalar erişilirse MISSING fault üretir */
ctx->inflated = true;
return 0;
}
/* Balloon şişirilmiş bölgeye erişim geldiğinde — handler */
static void handle_balloon_fault(struct balloon_ctx *ctx,
uint64_t fault_addr)
{
uint64_t page_addr = fault_addr & ~(PAGE_SIZE - 1);
if (ctx->inflated) {
/* VM belleğe ihtiyaç duydu — balloon'u söndür */
balloon_deflate_page(ctx, page_addr);
}
/* Sıfır sayfa sağla */
provide_zero_page(ctx->uffd, (void *)page_addr);
}
/* Balloon söndürme: bellek geri alınıyor */
int balloon_deflate(struct balloon_ctx *ctx)
{
ctx->inflated = false;
/* Bölgeyi UFFD'den çıkar */
unregister_region(ctx->uffd, ctx->balloon_region,
ctx->balloon_size);
/* Belleği tekrar kullanılabilir kıl */
/* (Sayfalar zaten DONTNEED ile geri alındı;
yeni erişimlerde kernel sıfır sayfa ayırır) */
munmap(ctx->balloon_region, ctx->balloon_size);
ctx->balloon_region = NULL;
return 0;
}
MADV_DONTNEED ile sayfa geri alma akışı
1. mmap() ile büyük bölge ayır (anonim, özel) 2. Kullanıcı uygulama sayfaları kullanmaya başlar → fiziksel sayfa atanır 3. balloon_inflate() çağrılır → MADV_DONTNEED: fiziksel sayfalar kernel'a iade edilir → VM bellek footprint küçülür → hypervisor fiziksel RAM'i geri alır 4. Uygulama balloon bölgesine erişirse → MISSING fault → UFFD handler 5. Handler: sıfır sayfa sağlar + gerekirse notify hypervisor 6. balloon_deflate(): balloon kaldırılır → bölge normal bellek olarak kullanılır
07 CRIU ve userfaultfd — checkpoint/restore entegrasyonu
CRIU (Checkpoint/Restore In Userspace), çalışan süreçleri dondurarak tüm durumlarını diske yazar ve sonra geri yükler. userfaultfd ile lazy restore: restore edilen süreç hemen çalışmaya başlar; bellek sayfaları sadece erişildikçe yüklenir.
CRIU lazy restore mimarisi
Checkpoint: 1. CRIU süreci dondurur (SIGSTOP veya ptrace) 2. /proc/PID/maps → bellek haritası 3. Her bellek bölgesi için sayfaları oku ve dosyaya yaz 4. FD'ler, register'lar, soketler, timer'lar... hepsi diske Lazy Restore: 1. CRIU yeni süreç oluşturur (exec + personality) 2. Bellek bölgelerini mmap ile oluşturur (içerik yok) 3. userfaultfd ile tüm bölgeleri kaydet 4. LAZY daemon (lzd): uffd handler thread + CRIU server iletişimi 5. Süreç çalışmaya başlar 6. Sayfaya erişildiğinde: MISSING fault → lzd → sayfayı diskten oku → UFFDIO_COPY 7. Süreç devam eder (sadece gerçekten erişilen sayfalar yüklendi)
Lazy restore handler implementasyonu
/* CRIU lazy daemon: sayfa sunucusuyla iletişim */
struct lazy_pages_ctx {
int uffd;
int page_server_fd; /* CRIU page server bağlantısı */
void *restore_base;
size_t restore_size;
};
static void *lazy_handler_thread(void *arg)
{
struct lazy_pages_ctx *ctx = arg;
struct pollfd pfds[2] = {
{ .fd = ctx->uffd, .events = POLLIN },
{ .fd = ctx->page_server_fd, .events = POLLIN },
};
while (1) {
poll(pfds, 2, -1);
if (pfds[0].revents & POLLIN) {
/* Sayfa hatası */
struct uffd_msg msg;
read(ctx->uffd, &msg, sizeof(msg));
if (msg.event == UFFD_EVENT_PAGEFAULT) {
uint64_t fault_addr = msg.arg.pagefault.address;
uint64_t page_addr = fault_addr & ~(PAGE_SIZE - 1);
/* Page server'dan sayfayı iste */
request_page(ctx->page_server_fd, page_addr);
/* Sayfayı al */
char page_buf[PAGE_SIZE];
recv_page(ctx->page_server_fd, page_buf);
/* Sürece sağla */
struct uffdio_copy copy = {
.dst = page_addr,
.src = (uint64_t)page_buf,
.len = PAGE_SIZE,
};
ioctl(ctx->uffd, UFFDIO_COPY, ©);
}
}
if (pfds[1].revents & POLLIN) {
/* Page server'dan proaktif push: ön yükleme */
prefetch_page(ctx);
}
}
return NULL;
}
Post-copy live migration ile UFFD
/* Post-copy: VM hedefte başladı, bellek hâlâ kaynakta */
struct postcopy_ctx {
int uffd;
int remote_fd; /* kaynak makinaya bağlantı */
void *vm_memory;
size_t vm_mem_size;
/* Hangi sayfaların transferi tamamlandı */
uint8_t *page_bitmap;
};
static void handle_postcopy_fault(struct postcopy_ctx *ctx,
uint64_t fault_addr)
{
uint64_t page_addr = fault_addr & ~(PAGE_SIZE - 1);
size_t page_index = (page_addr - (uint64_t)ctx->vm_memory)
/ PAGE_SIZE;
if (page_bitmap_test(ctx->page_bitmap, page_index)) {
/* Sayfa zaten transfer edildi ama UFFD hâlâ aktif
(arka planda transfer sürerken ikinci erişim) */
struct uffdio_continue cont = {
.range = { .start = page_addr, .len = PAGE_SIZE },
};
ioctl(ctx->uffd, UFFDIO_CONTINUE, &cont);
return;
}
/* Sayfayı uzak makineden iste — yüksek öncelikli */
char page_buf[PAGE_SIZE];
request_urgent_page(ctx->remote_fd, page_addr, page_buf);
struct uffdio_copy copy = {
.dst = page_addr,
.src = (uint64_t)page_buf,
.len = PAGE_SIZE,
};
ioctl(ctx->uffd, UFFDIO_COPY, ©);
page_bitmap_set(ctx->page_bitmap, page_index);
}
08 Hata ayıklama ve güvenlik kısıtları
userfaultfd, güçlü bir mekanizma olmakla birlikte güvenlik açısından dikkatli kullanılmasını gerektirir. Kernel, kullanıcı programların kötüye kullanımını önlemek için çeşitli kısıtlamalar uygular; hata ayıklama için /proc, strace ve yardımcı araçlar kullanılır.
Güvenlik kısıtları
Güvenli userfaultfd açma
/* Güvenli açma: kernel-mode fault'ları reddet */
int safe_userfaultfd_open(void)
{
int fd;
/* Kernel 5.11+: UFFD_USER_MODE_ONLY ile sadece user-space hataları */
fd = (int)syscall(SYS_userfaultfd,
O_CLOEXEC | O_NONBLOCK | UFFD_USER_MODE_ONLY);
if (fd >= 0) return fd;
/* Eski kernel: UFFD_USER_MODE_ONLY yok — normal aç */
if (errno == EINVAL)
fd = (int)syscall(SYS_userfaultfd, O_CLOEXEC | O_NONBLOCK);
/* /dev/userfaultfd (kernel 5.11+ alternatif) */
if (fd < 0)
fd = open("/dev/userfaultfd", O_RDWR | O_CLOEXEC | O_NONBLOCK);
return fd;
}
/* unprivileged_userfaultfd değerini kontrol et */
static int check_uffd_privilege(void)
{
FILE *f = fopen("/proc/sys/vm/unprivileged_userfaultfd", "r");
if (!f) return -1;
int val;
fscanf(f, "%d", &val);
fclose(f);
if (val == 0 && geteuid() != 0) {
fprintf(stderr, "userfaultfd: root veya CAP_SYS_PTRACE gerekli\n");
fprintf(stderr, "echo 1 | sudo tee /proc/sys/vm/unprivileged_userfaultfd\n");
return -1;
}
return 0;
}
Hata ayıklama araçları ve teknikleri
/* /proc/PID/smaps ile UFFD bölgelerini görüntüle */
/* sudo cat /proc/PID/smaps | grep -A 20 "VmFlags.*uf" */
/* strace ile UFFD sistem çağrılarını izle */
/* strace -e userfaultfd,ioctl,read,poll ./myprogram */
/* perf ile sayfa hata istatistikleri */
/* perf stat -e page-faults,minor-faults,major-faults ./myprogram */
/* uffd bölgelerini listele */
void dump_uffd_regions(void *addr, size_t size)
{
FILE *maps = fopen("/proc/self/smaps", "r");
char line[256];
uintptr_t start, end;
char perms[8], dev[8];
uint64_t offset, inode;
char name[128] = "";
while (fgets(line, sizeof(line), maps)) {
if (sscanf(line, "%lx-%lx %7s %lx %7s %lu %127s",
&start, &end, perms, &offset,
dev, &inode, name) >= 6) {
if (start == (uintptr_t)addr) {
printf("Bölge: %lx-%lx %s\n", start, end, perms);
}
}
/* VmFlags satırında "uf" = userfaultfd kayıtlı */
if (strstr(line, "VmFlags") && strstr(line, "uf"))
printf(" UFFD kayıtlı: %s", line);
}
fclose(maps);
}
Yaygın hatalar ve çözümleri
| Hata | Neden | Çözüm |
|---|---|---|
| EPERM (userfaultfd) | unprivileged_userfaultfd=0 ve root değil | sysctl ayarla veya root çalıştır |
| EINVAL (UFFDIO_API) | Yanlış api versiyonu veya desteklenmeyen feature | features maskesini features & desteklenen ile maskele |
| EEXIST (UFFDIO_COPY) | Başka thread aynı sayfayı sağladı | Yarış koşulu — hata değil, yok say |
| ENOENT (UFFDIO_COPY) | Adres UFFD bölgesinde kayıtlı değil | UFFDIO_REGISTER kontrolü |
| EINVAL (UFFDIO_REGISTER) | Adres sayfa hizalı değil veya len=0 | Adres ve len'i PAGE_SIZE'a hizala |
| Thread sonsuza dek bloklanıyor | Handler çöktü veya yanıt vermedi | Handler'da watchdog thread; DONTWAKE + WAKE ile manuel kontrol |
| EFAULT (syscall) | Kayıtlı UFFD bölgesine kernel erişimi | UFFD_USER_MODE_ONLY kullan; kernel erişimli bölgeleri kaydetme |
Tam kurulum özeti
/* Tam userfaultfd kurulum şablonu */
int uffd_full_setup(void **region_out, size_t size)
{
/* 1. Yetki kontrolü */
if (check_uffd_privilege() < 0) return -1;
/* 2. userfaultfd aç */
int uffd = safe_userfaultfd_open();
if (uffd < 0) return -1;
/* 3. API anlaşması */
uint64_t features;
if (negotiate_uffd_api(uffd, &features) < 0) goto err;
/* 4. Bellek bölgesi oluştur */
void *region = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (region == MAP_FAILED) goto err;
/* 5. Bölgeyi kaydet */
if (register_memory_region(uffd, region, size, false, false) < 0)
goto err_mmap;
/* 6. Handler thread başlat */
struct uffd_handler_ctx *ctx = calloc(1, sizeof(*ctx));
ctx->uffd = uffd;
ctx->base_addr = region;
ctx->region_len = size;
pthread_t handler_tid;
pthread_create(&handler_tid, NULL, uffd_handler_thread, ctx);
pthread_detach(handler_tid);
*region_out = region;
return uffd;
err_mmap:
munmap(region, size);
err:
close(uffd);
return -1;
}