10-bo‘lim

Virtual xotira va sahifalash

Virtual va fizik manzil, sahifa jadvali, MMU va TLB, sahifa xatosi, dangasa ajratish va nusxalash-yozishda.

🕑 13 daqiqa o‘qish 📄 901 so‘z 👁 6 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Virtual manzildan fizikka
  2. Sahifa hajmi
  3. Ko'p darajali sahifa jadvali
  4. TLB - tarjima keshi
  5. Dangasa ajratish (lazy allocation)
  6. Sahifa xatolarini sanash
  7. Nusxalash-yozishda (copy-on-write)
  8. Bo'lishilgan xotira (shared mapping)
  9. Sahifa himoyasi
  10. Xulosa

Oldingi bo'limda ko'rgan uch muammoni bitta g'oya hal qiladi: har bir jarayonga o'zining manzil fazosini berish.

Virtual manzildan fizikka #

Manzil tarjimasi A jarayoni virtual fazo 0x0000 sahifa 0 0x1000 sahifa 1 0x2000 sahifa 2 bog'lanmagan MMU sahifa jadvali bo'yicha tarjima qiladi Fizik RAM ramkalar (frame) ramka 12 (B niki) ramka 7 (A: sah. 1) ramka 3 (A: sah. 0) ramka 9 (A: sah. 2) Ketma-ket virtual sahifalar - tarqoq fizik ramkalar Shuning uchun tashqi fragmentatsiya yo'qoladi
Jarayon virtual manzil ko'radi, RAM fizik ramkalarni biladi

Sahifa hajmi #

C
#include <stdio.h>
#include <unistd.h>

int main(void) {
    long sahifa = sysconf(_SC_PAGESIZE);

    printf("Sahifa hajmi        : %ld bayt\n", sahifa);
    printf("Kilobaytda          : %ld KB\n", sahifa / 1024);
    printf("4096 baytmi?        : %s\n", sahifa == 4096 ? "ha" : "yo'q");

    /* Sahifa hajmi doim ikkining darajasi */
    printf("Ikkining darajasimi?: %s\n",
           (sahifa & (sahifa - 1)) == 0 ? "ha" : "yo'q");

    /* Manzilning ichki siljishini hisoblash */
    long manzil = 0x12345;
    printf("\n0x12345 manzili uchun:\n");
    printf("  Sahifa raqami : %ld\n", manzil / sahifa);
    printf("  Ichki siljish : %ld\n", manzil % sahifa);

    return 0;
}
Natija
Sahifa hajmi        : 4096 bayt
Kilobaytda          : 4 KB
4096 baytmi?        : ha
Ikkining darajasimi?: ha

0x12345 manzili uchun:
  Sahifa raqami : 18
  Ichki siljish : 837
Nima uchun sahifa hajmi ikkining darajasi?

Chunki u holda bo'lish va qoldiq bitli amallarga aylanadi:

C
sahifa_raqami = manzil >> 12;        /* 4096 = 2^12 ga bo'lish */
siljish       = manzil & 0xFFF;      /* 4096 ga qoldiq */

Bu apparatda bir taktda bajariladi. Agar sahifa 5000 bayt bo'lsa, har bir xotira murojaatida haqiqiy bo'lish kerak bo'lardi - bu tizimni o'nlab marta sekinlashtirardi.

64 bitli manzilda 4 KB sahifa uchun:

  • Quyi 12 bit - sahifa ichidagi siljish;
  • Qolgan bitlar - sahifa raqami.

Ko'p darajali sahifa jadvali #

x86-64: to'rt darajali sahifa jadvali ishlatilmaydi PML4 (9 bit) PDPT (9 bit) PD (9 bit) PT (9 bit) siljish (12 bit) 1-daraja 2-daraja 3-daraja 4-daraja ramka Bitta tarjima = 4 ta qo'shimcha xotira murojaati TLB keshi bo'lmasa, dastur 5 barobar sekin ishlardi
Ko'p darajali jadval bo'sh sohalar uchun joy sarflamaydi
Nima uchun bitta katta jadval emas?

64 bitli tizimda 4 KB sahifalar bilan bitta tekis jadval kerak bo'lardi:

Ko'rsatkichQiymat
Sahifalar soni2⁵² ta
Har bir yozuv8 bayt
Jami hajm32 petabayt

Bu bitta jarayon uchun! Ko'p darajali jadvalda esa jarayon faqat haqiqatan ishlatgan sohalar uchun jadval sahifalarini oladi - odatiy dastur uchun bu bir necha o'nlab kilobayt.

Bu ma'lumotlar tuzilmalaridagi klassik siyrak (sparse) massiv g'oyasi: bo'sh sohalarda yuqori darajadagi yozuv shunchaki NULL bo'ladi.

TLB - tarjima keshi #

C
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define SAHIFA   4096
#define SAHIFALAR 4096          /* 16 MB */
#define TAKROR   200

static double olchash(char *xotira, long qadam) {
    struct timespec b, o;
    volatile long yigindi = 0;

    clock_gettime(CLOCK_MONOTONIC, &b);

    for (int t = 0; t < TAKROR; t++) {
        for (long i = 0; i < (long) SAHIFALAR * SAHIFA; i += qadam) {
            yigindi += xotira[i];
        }
    }

    clock_gettime(CLOCK_MONOTONIC, &o);

    return (o.tv_sec - b.tv_sec) + (o.tv_nsec - b.tv_nsec) / 1e9;
}

int main(void) {
    size_t hajm = (size_t) SAHIFALAR * SAHIFA;
    char *xotira = malloc(hajm);

    if (xotira == NULL) {
        return 1;
    }

    for (size_t i = 0; i < hajm; i++) {
        xotira[i] = 1;
    }

    /* Ketma-ket: bitta sahifadan 64 marta o'qiladi */
    double ketma_ket = olchash(xotira, 64);

    /* Sakrab: har safar yangi sahifa - TLB doim yangilanadi */
    double sakrab = olchash(xotira, SAHIFA);

    /* Sakrab variantda 64 barobar kam o'qish bor,
       shunga qaramay u kutilganidek arzon emas */
    double nisbat = ketma_ket / sakrab;

    printf("Ketma-ket o'qish 64 barobar ko'p bayt o'qidi\n");
    printf("Lekin vaqti 64 barobar ko'p emas: %s\n",
           nisbat < 40.0 ? "ha" : "yo'q");
    printf("Sabab - sahifa bo'ylab sakrash TLB ni buzadi\n");

    free(xotira);

    return 0;
}
Natija
Ketma-ket o'qish 64 barobar ko'p bayt o'qidi
Lekin vaqti 64 barobar ko'p emas: ha
Sabab - sahifa bo'ylab sakrash TLB ni buzadi
TLB - eng muhim kesh

TLB (Translation Lookaside Buffer) - MMU ichidagi kichik kesh. Unda oxirgi ishlatilgan virtual → fizik tarjimalar saqlanadi.

HolatNarxi
TLB hit~1 takt
TLB miss (jadval keshda)~10-30 takt
TLB miss + sahifa xatosi~100 000+ takt

Zamonaviy protsessorlarda TLB 1500-3000 ta yozuvni saqlaydi. 4 KB sahifalar bilan bu ~10 MB ni qamraydi.

Katta ma'lumotlar bilan ishlaydigan dasturlar uchun katta sahifalar (huge pages, 2 MB) yoqiladi - o'shanda bir xil TLB ~5 GB ni qamraydi.

Dangasa ajratish (lazy allocation) #

C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static long rss_kb(void) {
    FILE *f = fopen("/proc/self/status", "r");
    char qator[256];
    long qiymat = -1;

    if (f == NULL) {
        return -1;
    }

    while (fgets(qator, sizeof qator, f) != NULL) {
        if (strncmp(qator, "VmRSS:", 6) == 0) {
            sscanf(qator + 6, "%ld", &qiymat);
            break;
        }
    }

    fclose(f);

    return qiymat;
}

int main(void) {
    long oldin = rss_kb();

    size_t hajm = 512L * 1024 * 1024;      /* 512 MB */
    char *katta = malloc(hajm);

    if (katta == NULL) {
        return 1;
    }

    long sorovdan_keyin = rss_kb();

    /* Endi haqiqatan tegib chiqamiz */
    for (size_t i = 0; i < hajm; i += 4096) {
        katta[i] = 1;
    }

    long tegishdan_keyin = rss_kb();

    printf("512 MB so'raldi, lekin hali tegilmadi:\n");
    printf("  RSS o'sishi 10 MB dan kammi? %s\n",
           sorovdan_keyin - oldin < 10 * 1024 ? "ha" : "yo'q");

    printf("Har bir sahifaga tegildi:\n");
    printf("  RSS o'sishi 400 MB dan ko'pmi? %s\n",
           tegishdan_keyin - sorovdan_keyin > 400 * 1024 ? "ha" : "yo'q");

    free(katta);

    return 0;
}
Natija
512 MB so'raldi, lekin hali tegilmadi:
  RSS o'sishi 10 MB dan kammi? ha
Har bir sahifaga tegildi:
  RSS o'sishi 400 MB dan ko'pmi? ha
Dangasa ajratish nima uchun ishlaydi

malloc(512 MB) bajarilganda yadro faqat manzil fazosida joy belgilaydi - RAM ajratmaydi.

Fizik ramka faqat siz o'sha sahifaga birinchi marta tekkaningizda ajratiladi. Bu jarayon:

  1. Siz bog'lanmagan sahifaga murojaat qilasiz;
  2. MMU sahifa xatosini (page fault) qo'zg'atadi;
  3. Yadro bo'sh ramka topib, sahifa jadvaliga yozadi;
  4. Buyruq qaytadan bajariladi - endi u ishlaydi.

Bu "yumshoq" sahifa xatosi (minor fault) - disk ishtirok etmaydi.

Sahifa xatolarini sanash #

C
#include <stdio.h>
#include <stdlib.h>
#include <sys/resource.h>

int main(void) {
    struct rusage r;

    getrusage(RUSAGE_SELF, &r);
    long yumshoq_oldin = r.ru_minflt;
    long qattiq_oldin  = r.ru_majflt;

    size_t hajm = 64L * 1024 * 1024;       /* 64 MB */
    char *xotira = malloc(hajm);

    if (xotira == NULL) {
        return 1;
    }

    for (size_t i = 0; i < hajm; i += 4096) {
        xotira[i] = 1;
    }

    getrusage(RUSAGE_SELF, &r);
    long yumshoq_keyin = r.ru_minflt;
    long qattiq_keyin  = r.ru_majflt;

    long farq = yumshoq_keyin - yumshoq_oldin;
    long kutilgan = (long) (hajm / 4096);

    printf("64 MB ga tegildi = %ld ta sahifa\n", kutilgan);
    printf("Yumshoq sahifa xatolari kamida shuncha bo'ldimi? %s\n",
           farq >= kutilgan / 2 ? "ha" : "yo'q");
    printf("Bu yerda qattiq (diskli) xato bo'ldimi? %s\n",
           qattiq_keyin - qattiq_oldin > 0 ? "ha" : "yo'q");

    free(xotira);

    return 0;
}
Natija
64 MB ga tegildi = 16384 ta sahifa
Yumshoq sahifa xatolari kamida shuncha bo'ldimi? ha
Bu yerda qattiq (diskli) xato bo'ldimi? yo'q
Xato turiNima bo'ladiNarxi
Yumshoq (minor)RAM dagi ramka bog'lanadi~1 mikrosekund
Qattiq (major)Diskdan o'qish kerak~100 mikrosekund - 10 ms
Xato (invalid)SIGSEGV yuboriladiDastur qulaydi
Terminal
/usr/bin/time -v ./dastur 2>&1 | grep -i faults
Natija
    Major (requiring I/O) page faults: 0
    Minor (reclaiming a frame) page faults: 16891

Nusxalash-yozishda (copy-on-write) #

fork() dan keyin: nusxalash-yozishda Darhol fork() dan keyin Ota Bola bitta ramka faqat o'qish Bola yozgandan keyin Ota Bola asl ramka yangi nusxa yozish mumkin Nusxa faqat o'zgartirilgan sahifa uchun olinadi Shuning uchun fork() 2 GB jarayonda ham tez ishlaydi
fork ning tezligi copy-on-write hisobiga
C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
    size_t hajm = 64L * 1024 * 1024;       /* 64 MB */
    char *xotira = malloc(hajm);

    if (xotira == NULL) {
        return 1;
    }

    memset(xotira, 'A', hajm);             /* haqiqatan RAM da */

    fflush(stdout);                        /* fork dan oldin shart! */

    pid_t bola = fork();

    if (bola < 0) {
        return 1;
    }

    if (bola == 0) {
        /* Bola faqat o'qiydi - nusxa olinmaydi */
        printf("Bola: birinchi belgi = %c\n", xotira[0]);

        /* Endi yozamiz - shu sahifaning nusxasi olinadi */
        xotira[0] = 'B';
        printf("Bola: o'zgartirdi -> %c\n", xotira[0]);

        fflush(stdout);                    /* _exit bufer tozalamaydi */
        _exit(0);
    }

    wait(NULL);

    printf("Ota: qiymat o'zgardimi? %s\n", xotira[0] == 'A' ? "yo'q" : "ha");
    printf("Ota: birinchi belgi = %c\n", xotira[0]);

    free(xotira);

    return 0;
}
Natija
Bola: birinchi belgi = A
Bola: o'zgartirdi -> B
Ota: qiymat o'zgardimi? yo'q
Ota: birinchi belgi = A
fork dan oldin fflush(stdout)

Bu misolda fflush(stdout) bo'lmasa, fork dan oldingi chiqish buferi bolaga ham nusxalanadi va ikki marta chop etiladi.

Chiqish terminalga ketganda bufer qatorlab tozalanadi va muammo ko'rinmaydi. Faylga yoki quvurga yozganda esa u to'liq bufer bo'ladi va xato yuzaga chiqadi:

Terminal
./dastur > natija.txt

Shuning uchun har doim fork dan oldin fflush(stdout) yozing.

Bo'lishilgan xotira (shared mapping) #

C
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>

int main(void) {
    /* MAP_SHARED - fork dan keyin ham umumiy qoladi */
    int *umumiy = mmap(NULL, 4096,
                       PROT_READ | PROT_WRITE,
                       MAP_SHARED | MAP_ANONYMOUS, -1, 0);

    /* MAP_PRIVATE - copy-on-write bo'ladi */
    int *shaxsiy = mmap(NULL, 4096,
                        PROT_READ | PROT_WRITE,
                        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

    if (umumiy == MAP_FAILED || shaxsiy == MAP_FAILED) {
        return 1;
    }

    umumiy[0] = 100;
    shaxsiy[0] = 100;

    fflush(stdout);

    pid_t bola = fork();

    if (bola < 0) {
        return 1;
    }

    if (bola == 0) {
        umumiy[0] = 999;
        shaxsiy[0] = 999;
        _exit(0);
    }

    wait(NULL);

    printf("MAP_SHARED  qiymati : %d\n", umumiy[0]);
    printf("MAP_PRIVATE qiymati : %d\n", shaxsiy[0]);
    printf("Bola o'zgarishi ko'rindimi? shared=%s private=%s\n",
           umumiy[0] == 999 ? "ha" : "yo'q",
           shaxsiy[0] == 999 ? "ha" : "yo'q");

    munmap(umumiy, 4096);
    munmap(shaxsiy, 4096);

    return 0;
}
Natija
MAP_SHARED  qiymati : 999
MAP_PRIVATE qiymati : 100
Bola o'zgarishi ko'rindimi? shared=ha private=yo'q

Sahifa himoyasi #

C
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <setjmp.h>
#include <sys/mman.h>
#include <unistd.h>

static sigjmp_buf sakrash;

static void segv_ishlovchi(int signal) {
    (void) signal;
    siglongjmp(sakrash, 1);
}

int main(void) {
    long sahifa = sysconf(_SC_PAGESIZE);

    char *xotira = mmap(NULL, (size_t) sahifa,
                        PROT_READ | PROT_WRITE,
                        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

    if (xotira == MAP_FAILED) {
        return 1;
    }

    xotira[0] = 'X';
    printf("Yozish mumkin: %c\n", xotira[0]);

    /* Endi faqat o'qishga o'tkazamiz */
    mprotect(xotira, (size_t) sahifa, PROT_READ);

    printf("O'qish hali ham mumkin: %c\n", xotira[0]);

    signal(SIGSEGV, segv_ishlovchi);

    if (sigsetjmp(sakrash, 1) == 0) {
        xotira[0] = 'Y';                   /* SIGSEGV qo'zg'atadi */
        printf("Yozish o'tdi - kutilmagan\n");
    } else {
        printf("Yozishga urinish SIGSEGV berdi - himoya ishladi\n");
    }

    munmap(xotira, (size_t) sahifa);

    return 0;
}
Natija
Yozish mumkin: X
O'qish hali ham mumkin: X
Yozishga urinish SIGSEGV berdi - himoya ishladi
mprotect ning kuchi va xavfi

mprotect bilan sahifa huquqlarini ish vaqtida o'zgartirish mumkin. Bu quyidagi joylarda ishlatiladi:

Qo'llanishQanday
Guard pageStek oxiriga PROT_NONE sahifa qo'yiladi
JIT kompilyatorKod yozilgach PROT_EXEC ga o'tkaziladi
DebugBuzilayotgan xotirani PROT_READ qilib topish
Garbage collectorO'zgargan sahifalarni kuzatish

Xavfsizlik qoidasi: sahifa hech qachon bir vaqtda yoziladigan va bajariladigan (W^X buzilishi) bo'lmasligi kerak. Aks holda hujumchi kod kiritib, uni ishga tushirishi mumkin.

Amaliy topshiriq
  1. sysconf(_SC_PAGESIZE) bilan sahifa hajmini aniqlang.
  2. Bitli surish orqali manzildan sahifa raqamini chiqaring.
  3. Tekis sahifa jadvali 64 bitda qancha joy olishini hisoblang.
  4. Sahifa bo'ylab sakrab o'qish tezligini ketma-ket bilan solishtiring.
  5. 512 MB malloc qilib, RSS o'zgarmasligini kuzating.
  6. Har bir sahifaga tegib, RSS o'sishini o'lchang.
  7. getrusage bilan yumshoq sahifa xatolarini sanang.
  8. fork dan keyin copy-on-write ishlashini isbotlang.
  9. MAP_SHARED va MAP_PRIVATE farqini ko'rsating.
  10. mprotect bilan sahifani himoyalab, SIGSEGV ni ushlang.

Xulosa #

  • Har bir jarayon o'z virtual manzil fazosida yashaydi.
  • MMU virtual manzilni sahifa jadvali orqali fizikka tarjima qiladi.
  • Sahifa hajmi ikkining darajasi - tarjima bitli amallarga aylanadi.
  • x86-64 da to'rt darajali jadval; bo'sh sohalar joy olmaydi.
  • TLB tarjimalarni keshlaydi; usiz tizim bir necha barobar sekin.
  • Dangasa ajratish: malloc faqat manzil ajratadi, RAM emas.
  • Yumshoq sahifa xatosi arzon, qattiq xato disk kutadi.
  • Copy-on-write fork ni katta jarayonlarda ham tez qiladi.
  • MAP_SHARED bolaga ko'rinadi, MAP_PRIVATE yo'q.
  • mprotect sahifa huquqlarini ish vaqtida o'zgartiradi.
  • Sahifa bir vaqtda yoziladigan va bajariladigan bo'lmasin.

Keyingi bo'limda sahifa almashtirish algoritmlarini ko'ramiz.

Xatolik topdingizmi?

Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.