Перейти к содержанию

Блочные и символьные устройства

Отличие блочных и символьных устройств

Устройства в Linux представлены специальными файлами в /dev. Первый символ в выводе ls -l указывает на тип:

ls -l /dev/sda /dev/tty
# brw-rw---- ... /dev/sda    (b — блочное)
# crw-rw-rw- ... /dev/tty    (c — символьное)

Символьное устройство (character device, c):

  • работает как последовательный поток байт;
  • обычно не поддерживает произвольное позиционирование (lseek);
  • примеры: /dev/tty, /dev/null, /dev/random, /dev/urandom, /dev/input/mouse0.

Блочное устройство (block device, b):

  • предоставляет доступ к данным фиксированными блоками (обычно 512 байт или 4 КБ);
  • поддерживает случайный доступ по смещению (lseek);
  • используется файловыми системами для хранения данных;
  • примеры: /dev/sda, /dev/sda1, /dev/nvme0n1.

Иерархия ввода-вывода: от userspace до железа

  Userspace
  ┌────────────────────────────────────────────────────────────┐
  │  open("/dev/sda", ...)   open("/dev/tty", ...)             │
  │  read(fd, buf, n)        write(fd, buf, n)                 │
  └──────────────────┬─────────────────────┬───────────────────┘
                     │  syscall            │  syscall
                     ▼                     ▼
  ┌────────────────────────────────────────────────────────────┐
  │                  VFS (Virtual File System)                 │
  │   единый интерфейс: struct file_operations                 │
  │   (.read, .write, .ioctl, .mmap, ...)                      │
  └──────────────────┬─────────────────────┬───────────────────┘
                     │                     │
         блочное устр.             символьное устр.
                     │                     │
                     ▼                     ▼
  ┌──────────────────────────┐  ┌──────────────────────────┐
  │     Block I/O layer      │  │   Char device driver     │
  │                          │  │                          │
  │  ┌────────────────────┐  │  │  поток байт без буфера   │
  │  │  Page cache        │  │  │  нет произвольного       │
  │  │  (буферизация:     │  │  │  позиционирования        │
  │  │  блоки кешируются  │  │  │                          │
  │  │  в RAM)            │  │  │  /dev/tty  — терминал    │
  │  └────────┬───────────┘  │  │  /dev/null — пустота     │
  │           │              │  │  /dev/random — энтропия  │
  │  ┌────────▼───────────┐  │  └──────────────┬───────────┘
  │  │  I/O scheduler     │  │                 │
  │  │  (CFQ, deadline,   │  │                 │
  │  │   noop, ...)       │  │                 │
  │  └────────┬───────────┘  │                 │
  └───────────┼──────────────┘                 │
              │                                │
              ▼                                ▼
  ┌──────────────────────────────────────────────────────────┐
  │                    Device driver                         │
  │         (модуль ядра, регистрирует major/minor)          │
  └──────────────────────────────┬───────────────────────────┘
  ┌──────────────────────────────────────────────────────────┐
  │                    Hardware                              │
  │   SSD/HDD (SCSI/NVMe)        UART / USB / GPU / ...      │
  └──────────────────────────────────────────────────────────┘

Ключевое различие:

  • Блочное устройство: запросы накапливаются в очереди, page cache позволяет переиспользовать данные без повторного чтения с диска, поддерживается lseek.
  • Символьное устройство: каждый read/write уходит напрямую в драйвер, кеш не задействован, последовательный поток без позиционирования.

Чтение и запись с устройств

С точки зрения программы устройства — это обычные файлы, доступные через open, read, write:

#include <fcntl.h>
#include <unistd.h>

// Чтение случайных байт
int fd = open("/dev/random", O_RDONLY);
unsigned char buf[32];
ssize_t n = read(fd, buf, sizeof(buf));
close(fd);

// Запись в терминал
int tty = open("/dev/tty", O_WRONLY);
write(tty, "Hello\n", 6);
close(tty);

Виртуальные устройства: /dev/null, /dev/zero, /dev/random

Виртуальные устройства не связаны с реальным физическим оборудованием — они реализованы прямо в ядре:

Устройство Чтение Запись
/dev/null Всегда возвращает EOF (0 байт) Молча поглощает всё
/dev/zero Бесконечный поток нулевых байт Молча поглощает всё
/dev/random Случайные байты (с Linux 5.6 блокирует только до инициализации CRNG, далее как /dev/urandom) Пополняет пул энтропии
/dev/urandom Случайные байты (не блокирует) Пополняет пул энтропии
/dev/full Бесконечный поток нулей Всегда возвращает ENOSPC

Пример: создать файл из 10 МБ нулей:

dd if=/dev/zero of=zeros.bin bs=1M count=10

Перенаправление stdout/stderr в терминал

Каждый активный терминал (псевдотерминал) виден как /dev/pts/N. Перенаправить вывод программы в конкретный терминал можно через dup2:

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

int main(void) {
    int fd = open("/dev/pts/3", O_WRONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    dup2(fd, 1);      // перенаправить stdout
    dup2(fd, 2);      // перенаправить stderr
    close(fd);

    printf("Hello from process!\n");
    return 0;
}

То же самое из оболочки:

./prog > /dev/pts/3 2>&1

Узнать номер своего терминала: команда tty выводит путь к устройству (/dev/pts/1 и т.п.).

Информация об оборудовании в файловой системе

Linux экспортирует информацию о железе через виртуальные файловые системы — данные генерируются ядром динамически и не хранятся на диске:

Путь Содержимое
/proc/cpuinfo Модель, тактовая частота и флаги каждого процессора
/proc/meminfo Объём и состояние оперативной памяти
/proc/<pid>/maps Карта виртуальной памяти конкретного процесса
/sys/block/* Информация о блочных устройствах (дисках, SSD)
/sys/class/net/* Сетевые интерфейсы
/sys/class/hwmon/* Датчики температуры, напряжения, скорости вентилятора

Старший и младший номера устройства (major/minor)

Каждый файл устройства идентифицируется парой чисел:

  • major — номер драйвера (ядро использует его для поиска нужного драйвера);
  • minor — номер конкретного экземпляра устройства внутри драйвера.
ls -l /dev/sda /dev/sda1 /dev/null
# brw-rw---- 1 root disk 8, 0  /dev/sda     (major=8, minor=0)
# brw-rw---- 1 root disk 8, 1  /dev/sda1    (major=8, minor=1)
# crw-rw-rw- 1 root root 1, 3  /dev/null    (major=1, minor=3)

Программно получить major/minor из struct stat:

#include <sys/stat.h>
#include <sys/sysmacros.h>

struct stat st;
stat("/dev/sda", &st);
unsigned int maj = major(st.st_rdev);
unsigned int min = minor(st.st_rdev);

Создать файл устройства вручную (требует root):

#include <sys/stat.h>
#include <sys/sysmacros.h>

dev_t dev = makedev(8, 0);
mknod("/dev/mysda", S_IFBLK | 0660, dev);

Или через команду:

mknod /dev/mysda b 8 0   # блочное устройство, major=8, minor=0

Связанные темы

Источники

  • man 4 null — устройства /dev/null и /dev/zero
  • man 4 random — генераторы случайных чисел
  • man 4 tty — терминальные устройства
  • man 2 mknod — создание файла устройства
  • man 3 major — макросы major/minor/makedev
  • man 5 proc — виртуальная файловая система /proc
  • man 5 sysfs — виртуальная файловая система /sys