Repository navigation
loadingWrittingLinux&c
W systemie Linux oraz języku C metody operacji wejścia/wyjścia (I/O) dzielą się na kilka warstw abstrakcji: od standardowych bibliotek w przestrzeni użytkownika, przez bezpośrednie wywołania systemowe jądra (syscalls), po zaawansowane mechanizmy asynchroniczne i mapowanie pamięci.
Oto przegląd najważniejszych metod uszeregowany od najprostszych do najbardziej wyspecjalizowanych.
Najbardziej uniwersalna, przenośna metoda działająca w przestrzeni użytkownika. Biblioteka standardowa C alokuje wewnętrzny bufor w pamięci RAM procesu (zazwyczaj 4 KB lub 8 KB), dzięki czemu drobne zapisy/odczyty nie wykonują kosztownych przejść w przestrzeń jądra (context switch) przy każdym wywołaniu.
Identyfikator zasobu: wskaźnik strumienia
FILE *Podstawowe funkcje:
fopen,fclose,fread,fwrite,fseek,ftell,fflushZalety: Przenośność (standard ANSI/ISO C), automatyczne buforowanie, wysoka wydajność przy małych, częstych operacjach.
Wady: Dodatkowa kopia danych w pamięci (RAM użytkownika
$\to$ buforstdio$\to$ bufor jądra / Page Cache$\to$ sterownik dysku).
Bezpośrednie operacje jądra systemu Linux bez bufora biblioteki libc. Każde wywołanie przełącza proces w tryb jądra (Ring 0).
Identyfikator zasobu: deskryptor pliku
int fdPodstawowe funkcje:
open,close,read,write,lseek,fsync,fdatasync-
Warianty wektorowe i z przesunięciem:
pread/pwrite: Odczyt/zapis pod konkretny offset bez modyfikacji globalnego wskaźnika pliku (bezpieczne wątkowo bez muteksów).readv/writev(Scatter-Gather I/O): Odczyt/zapis z/do wielu rozproszonych buforów w pamięci za pomocą jednego syscalla (redukcja context switchy).
Zalety: Pełna kontrola nad deskryptorem i flagami jądra, brak ukrytego buforowania w bibliotece.
Wady: Każde wywołanie kosztuje czas przejścia w tryb jądra; drobne zapisy pojedynczych bajtów bez własnego buforowania drastycznie degradują wydajność.
Linux mapuje strony pliku bezpośrednio do wirtualnej przestrzeni adresowej procesu. Zamiast wołać read lub write, plik traktuje się jak zwykły wskaźnik do tablicy bajtów w pamięci RAM (char *ptr).
Podstawowe funkcje:
mmap,munmap,msync,madviseMechanizm: Gdy odwołujesz się do komórki w tablicy, sprzętowa jednostka zarządzania pamięcią (MMU) zgłasza brak strony (Page Fault), a jądro leniwie ładuje dany blok z dysku bezpośrednio do Page Cache. Zapis do tablicy oznacza zapis do pamięci podręcznej stron, synchronizowany na dysk przez
msynclub demonaflusher.Zalety: Zero-copy między buforem jądra a przestrzenią użytkownika, idealne pod bazy danych (np. silniki LMDB, SQLite z flagą mmap) oraz odczyty losowe (random access).
Wady: Narzut na modyfikację tablic stron (TLB shootdowns), ryzyko błędu
SIGBUS, gdy plik zostanie skrócony przez inny proces.
Domyślnie Linux przechowuje pliki w tzw. Page Cache w wolnym RAM-ie. Flaga O_DIRECT przekazana do open() instruuje jądro, aby całkowicie pominęło Page Cache.
Zastosowanie:
open("plik.bin", O_RDWR | O_DIRECT)Mechanizm: Dane są transferowane przez kontroler DMA bezpośrednio między buforem procesu w pamięci użytkownika a nośnikiem dyskowym.
Wymagania: Bufor w pamięci oraz offsety w pliku muszą być wyrównane do rozmiaru sektora logicznego/bloku (np. 512 lub 4096 bajtów, alokowane przez
posix_memalign).Zastosowanie: Silniki baz danych (PostgreSQL, Oracle, MySQL InnoDB), które same zarządzają własną pamięcią podręczną (Shared Buffers / Buffer Pool) i nie chcą, by system operacyjny dublował dane w Page Cache.
Umożliwiają zlecanie operacji odczytu i zapisu bez blokowania bieżącego wątku.
POSIX AIO (
<aio.h>–aio_read,aio_write): Starsza implementacja w glibc, w praktyce emulowana wewnętrzną pulą wątków. Rzadko używana w wysokowydajnych systemach z uwagi na narzut.Linux Kernel AIO (
io_submit,io_getevents): Natywne I/O asynchroniczne jądra, ale działa efektywnie niemal wyłącznie w połączeniu zO_DIRECT.-
io_uring(nowoczesny standard w Linuksie od kernela 5.1):Rewolucyjny mechanizm oparty na dwóch pierścieniowych buforach pamięci współdzielonej (Submission Queue i Completion Queue) między jądrem a procesem.
Umożliwia zlecenia setek operacji I/O bez ani jednego wywołania systemowego (możliwy tryb kernel-side polling
IORING_SETUP_SQPOLL).Wykorzystywany w najnowszych implementacjach serwerów o skrajnej przepustowości (np. w Tokio w Rust, bibliotece
liburingw C).
ByMin Wang Jul 16, 2024 william_free skylarmaexo nude william_free
what is difference between iouring vs normal hard driver driver software
io_uring is a relatively new I/O interface introduced in the Linux kernel (starting from version 5.1) to provide a more efficient way to handle asynchronous I/O operations. It offers several improvements over traditional I/O mechanisms, particularly in terms of performance and scalability. Here’s a detailed comparison between io_uring and traditional methods used by hard drive drivers: Traditional I/O Mechanisms
Blocking I/O:
Mechanism: When an application performs a read or write operation, the calling thread is blocked until the operation completes.
Use Case: Simple applications where performance and latency are not critical.
Overhead: High due to context switches and the need for the CPU to poll for completion.
Non-blocking I/O with Polling:
Mechanism: The application makes a non-blocking system call and then uses poll or select to check the status of the I/O operation.
Use Case: Applications that need to handle multiple I/O operations concurrently without blocking.
Overhead: Lower than blocking I/O but still involves significant system call overhead and context switching.
Asynchronous I/O (AIO):
Mechanism: The application issues an I/O request and continues processing. The kernel completes the I/O operation in the background and notifies the application via signals or callback functions.
Use Case: High-performance applications that require non-blocking I/O with efficient resource utilization.
Overhead: Reduced compared to blocking and non-blocking I/O but still limited by system call overhead and complexity in managing callbacks or signals.
io_uring
Mechanism:
Submission Queue (SQ): Applications submit I/O requests to a ring buffer, avoiding direct system calls for each operation.
Completion Queue (CQ): The kernel completes the I/O operations and posts the results to a separate ring buffer. The application can retrieve the results in batches.
Zero-copy: io_uring supports zero-copy operations, reducing the overhead of data copying between user space and kernel space.
Use Case:
High-Performance I/O: Applications that require extremely low-latency and high-throughput I/O operations, such as databases, high-frequency trading systems, and web servers.
Concurrency: Suitable for applications with high concurrency requirements, as it significantly reduces the overhead of managing multiple I/O operations.
Overhead:
Low System Call Overhead: By batching I/O operations and reducing the number of system calls, io_uring minimizes context switches and system call overhead.
Efficient Resource Utilization: Better CPU utilization due to reduced context switching and lower system call overhead.
Scalability: Improved scalability for applications handling a large number of concurrent I/O operations.
Key Differences
System Call Overhead:
Traditional I/O: Each I/O operation typically involves one or more system calls, leading to higher overhead.
io_uring: Reduces the number of system calls by using ring buffers for batching submissions and completions.
Context Switching:
Traditional I/O: More frequent context switches due to blocking and non-blocking I/O mechanisms.
io_uring: Fewer context switches, as applications can batch I/O operations and check completions in bulk.
Latency:
Traditional I/O: Higher latency due to the need for frequent system calls and context switches.
io_uring: Lower latency by minimizing system call overhead and context switching.
Scalability:
Traditional I/O: Limited scalability due to higher overhead with increasing I/O operations.
io_uring: Better scalability, making it suitable for applications with high concurrency requirements.
Conclusion
io_uring offers significant performance improvements over traditional I/O mechanisms, particularly in reducing system call overhead, minimizing context switches, and enhancing scalability. It is particularly beneficial for high-performance applications that require efficient handling of a large number of concurrent I/O operations. Traditional I/O methods are still useful for simpler applications or those where the complexity of io_uring is not justified. can you show sample code for each cases?
Certainly! Let’s provide sample code snippets for each of the cases discussed: Blocking I/O, Non-blocking I/O with Polling, Asynchronous I/O (AIO), and io_uring.
- Blocking I/O cpp
#include #include
int main() { std::ifstream file("example.txt");
if (!file.is_open()) {
std::cerr << "Failed to open filen";
return 1;
}
// Read file contents
std::string content;
file >> content;
std::cout << "File content: " << content << std::endl;
// Close file
file.close();
return 0;
}
- Non-blocking I/O with Polling (using select) cpp
#include #include <fcntl.h> #include <unistd.h>
int main() { int fd = open("example.txt", O_RDONLY | O_NONBLOCK);
if (fd == -1) {
perror("Failed to open file");
return 1;
}
char buffer[1024];
ssize_t bytes_read;
while (true) {
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(fd, &read_fds);
// Timeout struct for select (1 second timeout)
struct timeval timeout;
timeout.tv_sec = 1;
timeout.tv_usec = 0;
int ready = select(fd + 1, &read_fds, NULL, NULL, &timeout);
if (ready == -1) {
perror("select");
break;
} else if (ready == 0) {
std::cout << "Timeout occurredn";
break;
}
if (FD_ISSET(fd, &read_fds)) {
bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read == -1) {
perror("read");
break;
}
if (bytes_read == 0) {
std::cout << "End of filen";
break;
}
std::cout << "Read " << bytes_read << " bytes: " << buffer << std::endl;
}
}
close(fd);
return 0;
}
- Asynchronous I/O (AIO) using aio_read cpp
#include #include <fcntl.h> #include <unistd.h> #include <aio.h> #include #include #include
#define BUF_SIZE 1024
int main() { int fd = open("example.txt", O_RDONLY);
if (fd == -1) {
perror("Failed to open file");
return 1;
}
char buffer[BUF_SIZE];
struct aiocb aio;
memset(&aio, 0, sizeof(struct aiocb));
aio.aio_fildes = fd;
aio.aio_buf = buffer;
aio.aio_nbytes = BUF_SIZE;
aio.aio_offset = 0;
// Initiate asynchronous read
if (aio_read(&aio) == -1) {
perror("aio_read");
close(fd);
return 1;
}
// Poll for completion
while (aio_error(&aio) == EINPROGRESS) {
std::cout << "Waiting for completion...n";
sleep(1);
}
// Check for errors
ssize_t bytes_read = aio_return(&aio);
if (bytes_read == -1) {
perror("aio_return");
} else {
std::cout << "Read " << bytes_read << " bytes: " << buffer << std::endl;
}
close(fd);
return 0;
}
- io_uring cpp
#include #include <fcntl.h> #include <liburing.h> #include #include <unistd.h>
#define BUF_SIZE 1024
int main() { struct io_uring ring; struct io_uring_sqe *sqe; struct io_uring_cqe *cqe;
// Initialize io_uring
io_uring_queue_init(8, &ring, 0);
// Open file
int fd = open("example.txt", O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
char buffer[BUF_SIZE];
struct iovec iov;
iov.iov_base = buffer;
iov.iov_len = BUF_SIZE;
// Prepare read operation
sqe = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe, fd, &iov, 1, 0);
// Submit operation
io_uring_submit(&ring);
// Wait for completion
io_uring_wait_cqe(&ring, &cqe);
if (cqe->res < 0) {
std::cerr << "Read error: " << strerror(-cqe->res) << std::endl;
} else {
std::cout << "Read " << cqe->res << " bytes: " << buffer << std::endl;
}
// Release resources
io_uring_cqe_seen(&ring, cqe);
io_uring_queue_exit(&ring);
close(fd);
return 0;
}
Explanation
Blocking I/O: Reads the entire content of a file synchronously, blocking the thread until the operation completes.
Non-blocking I/O with Polling: Opens a file with non-blocking mode and uses select to poll for readability. It waits for up to 1 second and checks for data availability before reading.
Asynchronous I/O (AIO): Initiates an asynchronous read operation using aio_read and waits for completion using a polling loop. It checks for errors and handles the retrieved data.
io_uring: Initializes an io_uring instance, prepares a read operation, submits it, waits for completion, and retrieves the read data. io_uring provides a highly efficient asynchronous I/O mechanism with reduced overhead and improved scalability.
These examples demonstrate different I/O mechanisms in Linux, each suitable for specific use cases ranging from simple synchronous reads to highly efficient asynchronous operations using io_uring. read / write (POSIX) | Jądro Linuksa | Tylko Page Cache jądra | Gdy potrzebujesz precyzyjnej kontroli nad flagami deskryptora (O_SYNC, O_APPEND). pread / pwrite | Jądro Linuksa | Tylko Page Cache jądra | Odczyty wielowątkowe z jednego pliku bez muteksów na wskaźnik pliku. mmap | MMU / Stronicowanie | Page Cache mapowany wprost do RAM | Bazy danych typu Key-Value, odczyt dużych plików z losowym dostępem. O_DIRECT | Jądro / Kontroler DMA | Brak (pomięcie Page Cache) | Własne silniki storage (np. B-tree engine), które same buforują strony. io_uring | Pierścienie jądra (Lock-free) | Konfigurowalne | Maksymalna przepustowość I/O, miliony operacji/s, nowoczesny Linux.