14-bo‘lim
Kiritish-chiqarish va qurilmalar
Qurilma fayllari, drayverlar, uzilishlar va DMA, buferlash, bloklaydigan va bloklamaydigan rejim, select va epoll.
Ushbu bo‘lim mundarijasi
Protsessor nanosekundlarda ishlaydi, disk millisekundlarda. Bu farqni boshqarish - OT ning eng qiyin vazifalaridan biri.
Tezliklar masshtabi #
Qurilma fayllari #
ls -l /dev/null /dev/zero /dev/random /dev/sda
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:
| Belgi | Turi | Xususiyati |
|---|---|---|
c | Belgili (character) | Bayt oqimi, buferlanmaydi |
b | Blokli (block) | Blok-blok, keshlanadi |
s | Soket | Tarmoq ulanishi |
p | Nomlangan quvur | FIFO |
l | Ramziy havola | Boshqa yo'lga ishora |
1, 3 - major va minor raqamlar. Major drayverni, minor
o'sha drayver boshqaradigan aniq qurilmani bildiradi.
#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;
}
/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 #
cat /proc/interrupts | head -8
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 #
#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;
}
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
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
fsyncdan 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 #
#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;
}
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 emasEAGAIN (ba'zi tizimlarda EWOULDBLOCK) - "hozir bo'lmaydi"
degani, "ishlamadi" emas.
Bloklamaydigan kod har doim shu holatni ajratishi kerak:
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 #
#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;
}
Tayyor deskriptorlar soni: 1
Birinchi quvur tayyormi? : yo'q
Ikkinchi quvur tayyormi? : ha
O'qilgan mazmun : ma'lumot
select, poll va epoll #
| Xususiyat | select | poll | epoll |
|---|---|---|---|
| Maksimal fd | 1024 (FD_SETSIZE) | Cheklanmagan | Cheklanmagan |
| Murakkabligi | O(n) | O(n) | O(1) |
| Ro'yxatni qayta uzatish | Har chaqiruvda | Har chaqiruvda | Bir marta |
| Portativlik | Hamma joyda | POSIX | Faqat Linux |
| 10 000 ulanishda | Yaramaydi | Sekin | Tez |
epoll1999-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:
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 #
cat /sys/block/sda/queue/scheduler
[none] mq-deadline kyber bfq
| Rejalashtiruvchi | Kimga mos |
|---|---|
none | NVMe SSD - navbat kerak emas |
mq-deadline | Server, ma'lumotlar bazasi |
bfq | Ish stoli - interaktivlik muhim |
kyber | Tez SSD, ko'p navbatli |
echo mq-deadline | sudo tee /sys/block/sda/queue/scheduler
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:
lsblk -d -o NAME,ROTA
NAME ROTA
sda 0 <- SSD
sdb 1 <- HDD
ROTA=1 - aylanuvchi disk.
To'g'ridan-to'g'ri I/O #
#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;
}
O_SYNC bilan yozildi: 14 bayt
Diskda kafolatlanganmi? ha
fsync natijasi : 0 (0 = muvaffaqiyat)
Bu ham kafolat beradi
O_DIRECT - qat'iy talablar bilanO_DIRECT yadro sahifa keshini butunlay chetlab o'tadi:
fd = open(nom, O_WRONLY | O_DIRECT);
Lekin u qattiq shartlar qo'yadi:
| Talab | Odatiy qiymat |
|---|---|
| Bufer manzili tekislangan | 512 yoki 4096 baytga |
| Yozish hajmi karrali | Blok hajmiga |
| Fayldagi siljish karrali | Blok hajmiga |
Buferni to'g'ri olish:
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 #
#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;
}
Chiqish terminal emas (quvur yoki fayl)
ioctl xato qaytardi - bu kutilgan holat
stdout terminalmi? yo'q
Terminalda ishga tushirsangiz:
Terminal o'lchami: 120 x 30
stdout terminalmi? ha
Quvurga yo'naltirsangiz:
./dastur | cat
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:
if (isatty(STDOUT_FILENO)) {
ranglarni_yoqish();
}
Kiritish-chiqarish statistikasi #
iostat -x 1 2
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
| Ustun | Ma'nosi |
|---|---|
r/s, w/s | Sekundiga o'qish/yozish soni |
await | O'rtacha kutish vaqti (ms) |
%util | Qurilma band bo'lgan vaqt ulushi |
%util 100% ga yaqin va await yuqori bo'lsa - disk to'siq
bo'lgan.
sudo iotop -o
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
/devdagi belgili va blokli qurilmalarni ajrating./dev/zerodan o'qib, nollar kelishini tasdiqlang./proc/interruptsda eng ko'p uzilish beruvchi qurilmani toping.- Baytlab va buferlab o'qish tizim chaqiruvlarini solishtiring.
- Quvurni
O_NONBLOCKga o'tkazib,EAGAINni oling. selectbilan ikki quvurni kuzating.epollningselectdan afzalligini tushuntiring.- Diskingiz rejalashtiruvchisini va
ROTAqiymatini ko'ring. O_SYNCvafsyncfarqini ayting.isattybilan 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.
fflushlibc buferini,fsyncyadro buferini bo'shatadi.EAGAINxato emas - "hozir emas" degani.selectO(n),epollO(1) - C10K muammosining yechimi.- SSD da rejalashtiruvchi kerak emas (
none). O_DIRECTfaqat o'z keshi bor dasturlar uchun.isattybilan terminal va quvurni ajrating.
Keyingi bo'limda jarayonlararo aloqani ko'ramiz.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
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.