14-bo‘lim

Kiritish-chiqarish va qurilmalar

Qurilma fayllari, drayverlar, uzilishlar va DMA, buferlash, bloklaydigan va bloklamaydigan rejim, select va epoll.

🕑 20 daqiqa o‘qish 📄 948 so‘z 👁 1 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Tezliklar masshtabi
  2. Qurilma fayllari
  3. Uzilish va DMA
  4. Buferlash: read va fread
  5. Bloklaydigan va bloklamaydigan rejim
  6. Ko'p deskriptorni kuzatish: select
  7. select, poll va epoll
  8. Disk rejalashtiruvchisi
  9. To'g'ridan-to'g'ri I/O
  10. ioctl - qurilmaga maxsus buyruq
  11. Kiritish-chiqarish statistikasi
  12. Xulosa

Protsessor nanosekundlarda ishlaydi, disk millisekundlarda. Bu farqni boshqarish - OT ning eng qiyin vazifalaridan biri.

Tezliklar masshtabi #

Kechikishlar (inson miqyosiga o'tkazilgan) L1 kesh (1 ns) 1 soniya RAM (100 ns) 2 daqiqa SSD (100 us) 1.5 kun HDD (10 ms) 4 oy Tarmoq (100 ms) 3 yil Disk kutish - protsessor uchun abadiyat Shuning uchun OT kutish paytida boshqa jarayonni ishga soladi
Bu tafovut butun kiritish-chiqarish arxitekturasini belgilaydi

Qurilma fayllari #

Terminal
ls -l /dev/null /dev/zero /dev/random /dev/sda
Natija
crw-rw-rw- 1 root root   1, 3 sen  9 08:14 /dev/null
crw-rw-rw- 1 root root   1, 5 sen  9 08:14 /dev/zero
crw-rw-rw- 1 root root   1, 8 sen  9 08:14 /dev/random
brw-rw---- 1 root disk   8, 0 sen  9 08:14 /dev/sda

Birinchi belgi turini bildiradi:

BelgiTuriXususiyati
cBelgili (character)Bayt oqimi, buferlanmaydi
bBlokli (block)Blok-blok, keshlanadi
sSoketTarmoq ulanishi
pNomlangan quvurFIFO
lRamziy havolaBoshqa yo'lga ishora

1, 3 - major va minor raqamlar. Major drayverni, minor o'sha drayver boshqaradigan aniq qurilmani bildiradi.

C
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int main(void) {
    struct stat h;

    stat("/dev/null", &h);
    printf("/dev/null belgili qurilmami? %s\n",
           S_ISCHR(h.st_mode) ? "ha" : "yo'q");

    /* /dev/zero - cheksiz nollar manbai */
    int fd = open("/dev/zero", O_RDONLY);

    if (fd < 0) {
        return 1;
    }

    char bufer[8];
    memset(bufer, 'X', sizeof bufer);

    ssize_t oqildi = read(fd, bufer, sizeof bufer);
    close(fd);

    int hammasi_nol = 1;

    for (size_t i = 0; i < sizeof bufer; i++) {
        if (bufer[i] != 0) {
            hammasi_nol = 0;
        }
    }

    printf("/dev/zero dan o'qildi : %zd bayt\n", oqildi);
    printf("Hammasi nolmi?        : %s\n", hammasi_nol ? "ha" : "yo'q");

    /* /dev/null - hamma narsani yutadi */
    fd = open("/dev/null", O_WRONLY);
    ssize_t yozildi = write(fd, "juda uzun matn", 14);
    close(fd);

    printf("/dev/null ga yozildi  : %zd bayt (lekin hech qayerga)\n", yozildi);

    return 0;
}
Natija
/dev/null belgili qurilmami? ha
/dev/zero dan o'qildi : 8 bayt
Hammasi nolmi?        : ha
/dev/null ga yozildi  : 14 bayt (lekin hech qayerga)

Uzilish va DMA #

Uch usul: so'rov, uzilish, DMA 1. So'rov (polling) while (qurilma band) { /* hech nima */ } CPU 100% behuda Faqat juda tez qurilmada 2. Uzilish 1. Buyruq berildi 2. Jarayon uxlaydi 3. Qurilma uzilish beradi 4. Jarayon uyg'onadi CPU boshqa ish qiladi 3. DMA Qurilma xotiraga to'g'ridan-to'g'ri yozadi - CPU aralashmaydi Bitta uzilish - butun blok Zamonaviy standart DMA siz 4 KB o'qish CPU 4096 marta bayt ko'chiradi DMA bilan: 1 ta buyruq + 1 ta uzilish
DMA kontrolleri protsessorni nusxalash ishidan ozod qiladi
Terminal
cat /proc/interrupts | head -8
Natija
           CPU0       CPU1       CPU2       CPU3
  0:         12          0          0          0   IO-APIC   2-edge      timer
  1:       2841       1024        512        256   IO-APIC   1-edge      i8042
  8:          1          0          0          0   IO-APIC   8-edge      rtc0
  9:          0          0          0          0   IO-APIC   9-fasteoi   acpi
 16:     184215     102384      88213      76112   IO-APIC  16-fasteoi   nvme0q0
 24:      45821      38214      29103      27441   PCI-MSI 327680-edge   iwlwifi

Buferlash: read va fread #

C
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/resource.h>

#define BAYTLAR  200000

int main(void) {
    const char *nom = "bufer_sinov.dat";

    /* Sinov faylini tayyorlaymiz */
    int fd = open(nom, O_WRONLY | O_CREAT | O_TRUNC, 0644);

    if (fd < 0) {
        return 1;
    }

    char blok[4096];
    memset(blok, 'A', sizeof blok);

    for (int i = 0; i < BAYTLAR / 4096 + 1; i++) {
        write(fd, blok, sizeof blok);
    }

    close(fd);

    /* 1-usul: har bayt uchun alohida tizim chaqiruvi */
    struct rusage r;
    getrusage(RUSAGE_SELF, &r);

    fd = open(nom, O_RDONLY);
    char bir;
    long tizim_chaqiruvi = 0;

    while (read(fd, &bir, 1) == 1) {
        tizim_chaqiruvi++;

        if (tizim_chaqiruvi >= BAYTLAR) {
            break;
        }
    }

    close(fd);

    /* 2-usul: stdio buferi orqali */
    FILE *f = fopen(nom, "rb");
    long belgilar = 0;

    while (fgetc(f) != EOF) {
        belgilar++;

        if (belgilar >= BAYTLAR) {
            break;
        }
    }

    fclose(f);

    printf("read() bilan tizim chaqiruvlari : %ld ta\n", tizim_chaqiruvi);
    printf("fgetc() bilan o'qilgan belgilar : %ld ta\n", belgilar);
    printf("fgetc ichida tizim chaqiruvi    : ~%ld ta (4 KB lik bufer)\n",
           belgilar / 4096);
    printf("Necha barobar kam?              : ~4096\n");

    unlink(nom);

    return 0;
}
Natija
read() bilan tizim chaqiruvlari : 200000 ta
fgetc() bilan o'qilgan belgilar : 200000 ta
fgetc ichida tizim chaqiruvi    : ~48 ta (4 KB lik bufer)
Necha barobar kam?              : ~4096
Uch daraja buferlash
Natija
Sizning dasturingiz
      |
      v
[1] stdio buferi (libc, foydalanuvchi fazosida)  <- fread/fwrite
      |  write() tizim chaqiruvi
      v
[2] Sahifa keshi (yadro, RAM da)                 <- read/write
      |  qurilma navbati
      v
[3] Disk keshi (qurilmaning o'zida)
      |
      v
   Fizik disk

Har bir daraja o'z buferiga ega. Shuning uchun:

  • fflush(f) - 1-darajadan 2-darajaga o'tkazadi;
  • fsync(fd) - 2-darajadan diskka tushiradi;
  • Ba'zi disklar fsync dan keyin ham o'z keshida ushlab turadi (shuning uchun serverlarda quvvat himoyalangan RAID kontrollerlari ishlatiladi).

fflush hech qachon fsync o'rnini bosmaydi.

Bloklaydigan va bloklamaydigan rejim #

C
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

int main(void) {
    int quvur[2];

    if (pipe(quvur) < 0) {
        return 1;
    }

    /* Bloklamaydigan rejimga o'tkazamiz */
    int bayroq = fcntl(quvur[0], F_GETFL);
    fcntl(quvur[0], F_SETFL, bayroq | O_NONBLOCK);

    char bufer[16];

    /* Quvur bo'sh - bloklamaydigan o'qish darhol qaytadi */
    ssize_t natija = read(quvur[0], bufer, sizeof bufer);

    printf("Bo'sh quvurdan o'qish: %zd\n", natija);
    printf("errno EAGAIN mi?     : %s\n", errno == EAGAIN ? "ha" : "yo'q");
    printf("Ma'nosi: hozir ma'lumot yo'q, keyinroq urinib ko'ring\n");

    /* Endi yozamiz */
    write(quvur[1], "salom", 5);

    natija = read(quvur[0], bufer, sizeof bufer);
    bufer[natija > 0 ? natija : 0] = '\0';

    printf("\nYozgandan keyin o'qish: %zd bayt\n", natija);
    printf("Mazmuni               : %s\n", bufer);

    close(quvur[0]);
    close(quvur[1]);

    return 0;
}
Natija
Bo'sh quvurdan o'qish: -1
errno EAGAIN mi?     : ha
Ma'nosi: hozir ma'lumot yo'q, keyinroq urinib ko'ring

Yozgandan keyin o'qish: 5 bayt
Mazmuni               : salom
EAGAIN xato emas

EAGAIN (ba'zi tizimlarda EWOULDBLOCK) - "hozir bo'lmaydi" degani, "ishlamadi" emas.

Bloklamaydigan kod har doim shu holatni ajratishi kerak:

C
ssize_t n = read(fd, bufer, hajm);

if (n > 0) {
    /* ma'lumot keldi */
} else if (n == 0) {
    /* fayl oxiri yoki ulanish yopildi */
} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
    /* hozir yo'q - keyin qaytamiz */
} else if (errno == EINTR) {
    /* signal uzdi - qayta urinamiz */
} else {
    /* haqiqiy xato */
}

Bu to'rt holatni chalkashtirish - tarmoq dasturlaridagi eng ko'p uchraydigan xato.

Ko'p deskriptorni kuzatish: select #

C
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>

int main(void) {
    int birinchi[2];
    int ikkinchi[2];

    if (pipe(birinchi) < 0 || pipe(ikkinchi) < 0) {
        return 1;
    }

    /* Faqat ikkinchi quvurga yozamiz */
    write(ikkinchi[1], "ma'lumot", 8);

    fd_set toplam;
    FD_ZERO(&toplam);
    FD_SET(birinchi[0], &toplam);
    FD_SET(ikkinchi[0], &toplam);

    int eng_katta = birinchi[0] > ikkinchi[0] ? birinchi[0] : ikkinchi[0];

    struct timeval kutish = { .tv_sec = 1, .tv_usec = 0 };

    int tayyor = select(eng_katta + 1, &toplam, NULL, NULL, &kutish);

    printf("Tayyor deskriptorlar soni: %d\n", tayyor);
    printf("Birinchi quvur tayyormi? : %s\n",
           FD_ISSET(birinchi[0], &toplam) ? "ha" : "yo'q");
    printf("Ikkinchi quvur tayyormi? : %s\n",
           FD_ISSET(ikkinchi[0], &toplam) ? "ha" : "yo'q");

    if (FD_ISSET(ikkinchi[0], &toplam)) {
        char bufer[16];
        ssize_t n = read(ikkinchi[0], bufer, sizeof bufer - 1);
        bufer[n] = '\0';
        printf("O'qilgan mazmun          : %s\n", bufer);
    }

    close(birinchi[0]);
    close(birinchi[1]);
    close(ikkinchi[0]);
    close(ikkinchi[1]);

    return 0;
}
Natija
Tayyor deskriptorlar soni: 1
Birinchi quvur tayyormi? : yo'q
Ikkinchi quvur tayyormi? : ha
O'qilgan mazmun          : ma'lumot

select, poll va epoll #

Xususiyatselectpollepoll
Maksimal fd1024 (FD_SETSIZE)CheklanmaganCheklanmagan
MurakkabligiO(n)O(n)O(1)
Ro'yxatni qayta uzatishHar chaqiruvdaHar chaqiruvdaBir marta
PortativlikHamma joydaPOSIXFaqat Linux
10 000 ulanishdaYaramaydiSekinTez
C10K muammosi va epoll

1999-yilda "bitta server 10 000 ulanishni ko'tara oladimi?" degan savol qo'yilgan edi. select bilan javob - yo'q.

Sabab: select har chaqiruvda butun ro'yxatni yadroga uzatadi va yadro hammasini tekshirib chiqadi. 10 000 ulanishda bu har bir hodisa uchun 10 000 tekshiruv.

epoll boshqacha ishlaydi:

C
int ep = epoll_create1(0);

struct epoll_event hodisa = { .events = EPOLLIN, .data.fd = soket };
epoll_ctl(ep, EPOLL_CTL_ADD, soket, &hodisa);   /* bir marta */

struct epoll_event tayyorlar[64];
int n = epoll_wait(ep, tayyorlar, 64, -1);      /* faqat tayyorlar */

Ro'yxat yadroda saqlanadi, epoll_wait esa faqat tayyor bo'lganlarini qaytaradi.

Nginx, Node.js, Redis - hammasi epoll ga tayanadi. BSD da kqueue, Windows da IOCP shu vazifani bajaradi.

Disk rejalashtiruvchisi #

Terminal
cat /sys/block/sda/queue/scheduler
Natija
[none] mq-deadline kyber bfq
RejalashtiruvchiKimga mos
noneNVMe SSD - navbat kerak emas
mq-deadlineServer, ma'lumotlar bazasi
bfqIsh stoli - interaktivlik muhim
kyberTez SSD, ko'p navbatli
Terminal
echo mq-deadline | sudo tee /sys/block/sda/queue/scheduler
Aylanuvchi diskda tartib muhim edi

HDD da o'qish boshini siljitish (seek) eng qimmat amal. Shuning uchun rejalashtiruvchilar so'rovlarni blok raqami bo'yicha saralab, boshni bir yo'nalishda harakatlantirardi (lift algoritmi).

SSD da mexanik harakat yo'q - istalgan blokka kirish bir xil tez. Shuning uchun NVMe disklarda none tanlanadi: saralash faqat vaqt yo'qotadi.

Buni tekshirish oson:

Terminal
lsblk -d -o NAME,ROTA
Natija
NAME ROTA
sda     0      <- SSD
sdb     1      <- HDD

ROTA=1 - aylanuvchi disk.

To'g'ridan-to'g'ri I/O #

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

int main(void) {
    const char *nom = "sync_sinov.txt";

    /* O_SYNC - har write diskka kafolatli tushadi */
    int fd = open(nom, O_WRONLY | O_CREAT | O_TRUNC | O_SYNC, 0644);

    if (fd < 0) {
        return 1;
    }

    ssize_t n = write(fd, "muhim ma'lumot", 14);
    close(fd);

    printf("O_SYNC bilan yozildi: %zd bayt\n", n);
    printf("Diskda kafolatlanganmi? ha\n");

    /* Oddiy yozish + fsync */
    fd = open(nom, O_WRONLY | O_TRUNC, 0644);
    write(fd, "boshqa ma'lumot", 15);

    int natija = fsync(fd);
    close(fd);

    printf("\nfsync natijasi      : %d (0 = muvaffaqiyat)\n", natija);
    printf("Bu ham kafolat beradi\n");

    unlink(nom);

    return 0;
}
Natija
O_SYNC bilan yozildi: 14 bayt
Diskda kafolatlanganmi? ha

fsync natijasi      : 0 (0 = muvaffaqiyat)
Bu ham kafolat beradi
O_DIRECT - qat'iy talablar bilan

O_DIRECT yadro sahifa keshini butunlay chetlab o'tadi:

C
fd = open(nom, O_WRONLY | O_DIRECT);

Lekin u qattiq shartlar qo'yadi:

TalabOdatiy qiymat
Bufer manzili tekislangan512 yoki 4096 baytga
Yozish hajmi karraliBlok hajmiga
Fayldagi siljish karraliBlok hajmiga

Buferni to'g'ri olish:

C
void *bufer;
posix_memalign(&bufer, 4096, 4096);

Shartlar buzilsa EINVAL olasiz.

O_DIRECT faqat o'z keshini o'zi boshqaradigan dasturlar (PostgreSQL, Oracle) uchun foydali. Oddiy dasturda u tezlikni pasaytiradi, chunki yadro keshining foydasidan mahrum bo'lasiz.

ioctl - qurilmaga maxsus buyruq #

C
#include <stdio.h>
#include <unistd.h>
#include <sys/ioctl.h>

int main(void) {
    struct winsize oyna;

    /* Terminal o'lchamini so'raymiz */
    int natija = ioctl(STDOUT_FILENO, TIOCGWINSZ, &oyna);

    if (natija == 0) {
        printf("Terminal o'lchami: %d x %d\n", oyna.ws_col, oyna.ws_row);
    } else {
        printf("Chiqish terminal emas (quvur yoki fayl)\n");
        printf("ioctl xato qaytardi - bu kutilgan holat\n");
    }

    printf("stdout terminalmi? %s\n", isatty(STDOUT_FILENO) ? "ha" : "yo'q");

    return 0;
}
Natija
Chiqish terminal emas (quvur yoki fayl)
ioctl xato qaytardi - bu kutilgan holat
stdout terminalmi? yo'q
Bu misolning natijasi qayerda ishlatilishiga bog'liq

Terminalda ishga tushirsangiz:

Natija
Terminal o'lchami: 120 x 30
stdout terminalmi? ha

Quvurga yo'naltirsangiz:

Terminal
./dastur | cat
Natija
Chiqish terminal emas (quvur yoki fayl)
stdout terminalmi? yo'q

Aynan shu tekshiruv tufayli ls terminalda rangli, quvurda esa rangsiz chiqadi. git log ham terminalda less ishlatadi, quvurda esa to'g'ridan-to'g'ri yozadi.

Bu dasturingizni "aqlli" qiladigan oddiy usul:

C
if (isatty(STDOUT_FILENO)) {
    ranglarni_yoqish();
}

Kiritish-chiqarish statistikasi #

Terminal
iostat -x 1 2
Natija
Device   r/s    w/s   rkB/s   wkB/s  await  %util
nvme0n1  12.4   84.2   198.4  1842.6   0.42   8.21
sda       0.8    2.1    12.8    84.0  14.82  42.10
UstunMa'nosi
r/s, w/sSekundiga o'qish/yozish soni
awaitO'rtacha kutish vaqti (ms)
%utilQurilma band bo'lgan vaqt ulushi

%util 100% ga yaqin va await yuqori bo'lsa - disk to'siq bo'lgan.

Terminal
sudo iotop -o
Natija
  TID  PRIO  USER   DISK READ  DISK WRITE  COMMAND
 4521  be/4  mysql   0.00 B/s   12.42 M/s  mysqld
 8213  be/4  root    2.14 M/s    0.00 B/s  rsync
Amaliy topshiriq
  1. /dev dagi belgili va blokli qurilmalarni ajrating.
  2. /dev/zero dan o'qib, nollar kelishini tasdiqlang.
  3. /proc/interrupts da eng ko'p uzilish beruvchi qurilmani toping.
  4. Baytlab va buferlab o'qish tizim chaqiruvlarini solishtiring.
  5. Quvurni O_NONBLOCK ga o'tkazib, EAGAIN ni oling.
  6. select bilan ikki quvurni kuzating.
  7. epoll ning select dan afzalligini tushuntiring.
  8. Diskingiz rejalashtiruvchisini va ROTA qiymatini ko'ring.
  9. O_SYNC va fsync farqini ayting.
  10. isatty bilan chiqish terminal ekanini tekshirib, rang qo'shing.

Xulosa #

  • Disk RAM dan 100 000 barobar sekin - bu hamma narsani belgilaydi.
  • Qurilmalar belgili (c) va blokli (b) bo'ladi.
  • Major raqam drayverni, minor aniq qurilmani bildiradi.
  • So'rov CPU ni behuda band qiladi; uzilish uni ozod qiladi.
  • DMA bilan qurilma xotiraga to'g'ridan-to'g'ri yozadi.
  • Buferlash uch darajada: stdio, sahifa keshi, disk keshi.
  • fflush libc buferini, fsync yadro buferini bo'shatadi.
  • EAGAIN xato emas - "hozir emas" degani.
  • select O(n), epoll O(1) - C10K muammosining yechimi.
  • SSD da rejalashtiruvchi kerak emas (none).
  • O_DIRECT faqat o'z keshi bor dasturlar uchun.
  • isatty bilan terminal va quvurni ajrating.

Keyingi bo'limda jarayonlararo aloqani 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.