Архитектура UNIX
Основные принципы архитектуры
UNIX строится на нескольких фундаментальных концепциях:
- "Все есть файл" - устройства, процессы, сокеты представлены как файлы
- Модульность - множество небольших специализированных утилит
- Конвейеры - программы взаимодействуют через стандартные потоки ввода/вывода
- Иерархическая файловая система - единое дерево каталогов
Слоистая архитектура
┌─────────────────────────────────────┐
│ Пользовательские │
│ приложения │
├─────────────────────────────────────┤
│ Shell и утилиты │
│ (bash, ls, grep, gcc...) │
├─────────────────────────────────────┤
│ Системная библиотека │
│ (libc) │
├─────────────────────────────────────┤
│ Системные вызовы │
│ (интерфейс к ядру) │
├─────────────────────────────────────┤
│ ЯДРО │
│ (kernel space) │
└─────────────────────────────────────┘
Ядро (Kernel) - сердце системы
Основные подсистемы ядра:
1. Управление процессами
- Планировщик - распределяет процессорное время
- Управление памятью - виртуальная память, подкачка
- Межпроцессное взаимодействие (IPC) - сигналы, каналы, семафоры
2. Файловая подсистема
- VFS (Virtual File System) - абстрактный слой над разными ФС
- Буферный кэш - кэширование блоков диска в памяти
- Драйверы файловых систем (ext4, XFS, etc.)
3. Подсистема ввода-вывода
- Драйверы устройств - взаимодействие с железом
- Блочные устройства (диски) и символьные (терминалы)
- Сетевой стек - TCP/IP, сокеты
Пространства выполнения
Система разделена на два пространства:
Kernel Space (пространство ядра)
- Полный доступ к железу
- Выполнение системных вызовов
- Обработка прерываний
- Управление ресурсами
User Space (пользовательское пространство)
- Ограниченные привилегии
- Доступ к системе только через системные вызовы
- Изоляция процессов друг от друга
Системные вызовы - интерфейс к ядру
Основные группы системных вызовов:
// Управление процессами
fork() // создание процесса
exec() // запуск программы
wait() // ожидание завершения процесса
// Файловые операции
open() // открытие файла
read() // чтение
write() // запись
close() // закрытие
// Управление памятью
mmap() // отображение файла в память
brk() // изменение размера кучи
Файловая система как основа
Единая иерархия:
- Корневой каталог
/- начало всего - Все устройства в
/dev/ - Системная информация в
/proc/и/sys/ - Временные файлы в
/tmp/
Каждый файл характеризуется:
- inode - метаинформация (права, размер, время)
- Путь - местоположение в иерархии
- Тип - обычный файл, каталог, устройство, ссылка
Управление процессами
Каждый процесс имеет:
- PID (Process ID) - уникальный идентификатор
- Родительский процесс - все процессы образуют дерево
- Виртуальное адресное пространство - изолированная память
- Дескрипторы файлов - открытые файлы и устройства
Состояния процесса:
- Running (выполняется)
- Ready (готов к выполнению)
- Blocked (ждет ресурс)
- Zombie (завершился, но не убран родителем)
Ключевые особенности архитектуры
Многозадачность
- Вытесняющая многозадачность - ядро принудительно переключает процессы
- Планировщик распределяет время между процессами справедливо
Многопользовательность
- Пользователи и группы - система прав доступа
- root - суперпользователь с полными правами
- Изоляция данных и процессов разных пользователей
Переносимость
- POSIX-совместимость - стандартизированный интерфейс
- Четкое разделение аппаратно-зависимого и независимого кода
- Поддержка различных архитектур процессоров
1. Что происходит при выполнении системного вызова
Системный вызов - это контролируемый переход из пользовательского режима в режим ядра.
Пошаговый процесс
Шаг 1: Инициация вызова
// Пользовательская программа вызывает:
int fd = open("/tmp/file.txt", O_RDONLY);
Шаг 2: Переход в библиотеку
- Библиотека libc содержит wrapper-функции
open()в libc подготавливает параметры и вызывает соответствующий системный вызов
Шаг 3: Программное прерывание
# На x86-64 используется инструкция syscall
mov $2, %rax # номер системного вызова open
mov $filename, %rdi # первый аргумент
mov $flags, %rsi # второй аргумент
syscall # переход в kernel mode
Шаг 4: Переключение контекста
- Процессор переключается в привилегированный режим (ring 0)
- Сохраняется состояние пользовательского процесса
- Переключается на стек ядра
- Включается обработчик системных вызовов в ядре
Шаг 5: Выполнение в ядре
// В ядре выполняется примерно такой код:
asmlinkage long sys_open(const char __user *filename, int flags, umode_t mode) {
// Проверка прав доступа
// Поиск файла в файловой системе
// Создание файлового дескриптора
// Обновление таблицы открытых файлов процесса
return fd; // возвращаем дескриптор файла
}
Шаг 6: Возврат в пользовательский режим
- Результат помещается в регистр
%rax - Восстанавливается пользовательский контекст
- Переключение обратно в непривилегированный режим (ring 3)
Важные моменты
- Изоляция: пользовательский код не может напрямую обращаться к ядру
- Проверка прав: ядро проверяет, имеет ли процесс право на операцию
- Атомарность: системный вызов выполняется целиком или не выполняется вообще
2. Как обеспечивается безопасность между процессами
UNIX использует многоуровневую систему изоляции и контроля доступа.
Изоляция памяти
Виртуальная память:
Процесс A Процесс B
┌─────────────┐ ┌─────────────┐
│ 0x00001000 │ │ 0x00001000 │ <- Одинаковые адреса!
│ 0x00002000 │ │ 0x00002000 │
│ 0x00003000 │ │ 0x00003000 │
└─────────────┘ └─────────────┘
│ │
▼ ▼
┌─────────────────────────────────┐
│ Физическая память │
│ 0x10001000 │ 0x20001000 │ <- Разные физические адреса
└─────────────────────────────────┘
- Каждый процесс видит свое виртуальное адресное пространство
- MMU (Memory Management Unit) транслирует виртуальные адреса в физические
- Процесс не может обратиться к памяти другого процесса
Привилегии пользователей
Модель UID/GID:
# Каждый процесс выполняется от имени пользователя
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
john 1234 0.1 0.8 12345 6789 pts/0 S 14:30 0:00 bash
mary 5678 0.2 1.2 23456 9876 pts/1 S 14:35 0:01 python
Проверка доступа к файлам:
// При каждом обращении к файлу ядро проверяет:
if (current_user_id == file_owner_id) {
// Проверяем права владельца (rwx)
} else if (current_group_id == file_group_id) {
// Проверяем права группы (rwx)
} else {
// Проверяем права остальных (rwx)
}
Изоляция файловых дескрипторов
Каждый процесс имеет свою таблицу дескрипторов:
// Процесс A
fd[0] -> stdin
fd[1] -> stdout
fd[2] -> stderr
fd[3] -> /home/user/file1.txt
// Процесс B
fd[0] -> stdin
fd[1] -> stdout
fd[2] -> stderr
fd[3] -> /tmp/another_file.txt // Совершенно другой файл!
Системы контроля доступа
Традиционные UNIX-права:
$ ls -l /etc/passwd
-rw-r--r-- 1 root root 2847 Jan 15 10:30 /etc/passwd
# rwx для owner, r-x для group, r-- для others
Современные расширения:
- SELinux: принудительный контроль доступа (MAC)
- AppArmor: профили безопасности для приложений
- Capabilities: детальные привилегии вместо root/user
Изоляция процессов
Пространства имен (namespaces):
# PID namespace - изоляция идентификаторов процессов
# Network namespace - изоляция сетевых интерфейсов
# Mount namespace - изоляция файловых систем
# User namespace - изоляция пользователей и групп
3. Почему "все есть файл" - важный принцип
Этот принцип создает единообразный интерфейс для работы с различными ресурсами системы.
Унификация интерфейса
Одни и те же операции для разных объектов:
// Работа с обычным файлом
int fd = open("/home/user/data.txt", O_RDONLY);
read(fd, buffer, size);
close(fd);
// Работа с устройством (например, последовательный порт)
int fd = open("/dev/ttyS0", O_RDWR);
read(fd, buffer, size); // читаем с порта
close(fd);
// Работа с сетевым соединением
int fd = socket(AF_INET, SOCK_STREAM, 0);
read(fd, buffer, size); // читаем из сокета
close(fd);
Типы "файлов" в UNIX
Обычные файлы:
$ ls -l /home/user/
-rw-r--r-- 1 user user 1024 Jan 15 10:00 document.txt
Каталоги:
drwxr-xr-x 2 user user 4096 Jan 15 10:00 Documents/
Символьные устройства (посимвольный ввод-вывод):
$ ls -l /dev/tty*
crw-rw-rw- 1 root tty 5, 0 Jan 15 10:00 /dev/tty
# c = character device
Блочные устройства (блочный ввод-вывод):
$ ls -l /dev/sd*
brw-rw---- 1 root disk 8, 0 Jan 15 10:00 /dev/sda
# b = block device
Именованные каналы (FIFO):
$ mkfifo /tmp/mypipe
$ ls -l /tmp/mypipe
prw-r--r-- 1 user user 0 Jan 15 10:00 /tmp/mypipe
# p = pipe
Символические ссылки:
lrwxrwxrwx 1 root root 7 Jan 15 10:00 /bin/sh -> /bin/bash
# l = link
Сокеты:
srwxr-xr-x 1 user user 0 Jan 15 10:00 /tmp/socket
# s = socket
Виртуальные файловые системы
Системная информация через /proc:
$ cat /proc/cpuinfo # информация о процессоре
$ cat /proc/meminfo # информация о памяти
$ cat /proc/1234/status # информация о процессе 1234
Конфигурация ядра через /sys:
$ cat /sys/class/net/eth0/address # MAC-адрес сетевого интерфейса
$ echo 1 > /sys/class/leds/led0/brightness # включить светодиод
Преимущества принципа
1. Простота программирования
// Одна функция для чтения из любого источника
ssize_t universal_read(int fd, void *buf, size_t count) {
return read(fd, buf, count); // работает для всех типов "файлов"
}
2. Композиция и конвейеры
# Можно соединять любые источники и приемники данных
cat /proc/cpuinfo | grep processor | wc -l
# файл -> утилита -> утилита -> утилита
3. Прозрачность и отладка
# Можно легко исследовать состояние системы
$ ls /proc/*/fd/ # посмотреть открытые файлы всех процессов
$ cat /sys/kernel/debug/tracing/trace # трассировка ядра
4. Гибкость
- Новые типы ресурсов легко интегрируются в существующую модель
- Утилиты работают с новыми типами "файлов" без изменений
- Система администрирования становится более единообразной
Ограничения принципа
Не все операции подходят:
- Некоторые устройства требуют специальных ioctl() вызовов
- Сетевые операции (bind, listen, accept) не вписываются в read/write модель
- Управление процессами требует отдельных системных вызовов
Тем не менее, принцип "все есть файл" остается одной из самых элегантных и мощных идей UNIX, обеспечивающей простоту, единообразие и гибкость системы.