Практическая работа по теме: "Процессы и нити ядра Linux"
Цель работы:
Освоить практические навыки работы с процессами, нитями, механизмами синхронизации, межпроцессным взаимодействием и планированием в Linux на основе пройденной теории.
Подготовка
- Установите ПО:
- Компилятор
gcc
, библиотекаpthread
(sudo apt install build-essential
). - Утилиты:
htop
,strace
,lsof
,netcat
(sudo apt install htop strace lsof netcat
).
- Компилятор
- Все команды и код выполняются в терминале Linux (рекомендуется использовать Ubuntu/WSL).
Задание 1. Исследование процессов и нитей
Цель: Сравнить процессы и нити, изучить их создание и управление.
Шаги:
Создание процесса через
fork()
c// process.c #include <unistd.h> #include <stdio.h> int main() { pid_t pid = fork(); if (pid == 0) { printf("[Child] PID: %d, PPID: %d\n", getpid(), getppid()); } else { printf("[Parent] PID: %d, Child PID: %d\n", getpid(), pid); } return 0; }
- Скомпилируйте:
gcc process.c -o process
. - Запустите:
./process
. - Наблюдайте за процессами:bash
ps aux | grep process # Найдите PID процесса и его потомка top -p $(pgrep -d ',' -f process) # Отслеживайте использование CPU
- Скомпилируйте:
Создание нитей через
pthread
c// thread.c #include <pthread.h> #include <stdio.h> void* thread_func(void* arg) { printf("[Thread] TID: %lu\n", (unsigned long)pthread_self()); return NULL; } int main() { pthread_t tid; pthread_create(&tid, NULL, thread_func, NULL); pthread_join(tid, NULL); return 0; }
- Скомпилируйте:
gcc thread.c -o thread -lpthread
. - Запустите:
./thread
. - Сравните использование ресурсов:bash
htop # В режиме просмотра нитей (F2 → Other Options → Set Left/Right Columns → добавьте NLWP)
- Скомпилируйте:
Вопросы для самоконтроля:
- Чем отличается
PID
процесса отTID
нити? - Почему создание нити эффективнее создания процесса?
Задание 2. Race Condition и взаимоисключение
Цель: Продемонстрировать race condition и решить её с помощью мьютекса и семафора.
Шаги:
Race Condition без синхронизации
c// race.c #include <pthread.h> #include <stdio.h> int counter = 0; void* increment(void* arg) { for (int i = 0; i < 100000; i++) counter++; return NULL; } int main() { pthread_t t1, t2; pthread_create(&t1, NULL, increment, NULL); pthread_create(&t2, NULL, increment, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf("Counter: %d (ожидалось 200000)\n", counter); return 0; }
- Скомпилируйте и запустите:
gcc race.c -o race -lpthread && ./race
. - Наблюдение: Значение
counter
будет меньше 200000 из-за race condition.
- Скомпилируйте и запустите:
Решение через мьютекс
Замените телоincrement
на:cpthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; void* increment(void* arg) { for (int i = 0; i < 100000; i++) { pthread_mutex_lock(&lock); counter++; pthread_mutex_unlock(&lock); } return NULL; }
- Перекомпилируйте и запустите. Убедитесь, что
counter = 200000
.
- Перекомпилируйте и запустите. Убедитесь, что
Решение через семафор
c#include <semaphore.h> sem_t sem; void* increment(void* arg) { for (int i = 0; i < 100000; i++) { sem_wait(&sem); counter++; sem_post(&sem); } return NULL; } int main() { sem_init(&sem, 0, 1); // Инициализация семафора // ... создание потоков ... sem_destroy(&sem); }
- Скомпилируйте с флагом
-lrt
:gcc -o sem race.c -lpthread -lrt
.
- Скомпилируйте с флагом
Вопросы для самоконтроля:
- Почему мьютекс подходит для критической секции, а семафор — для управления доступом к ресурсам?
- Как аппаратная поддержка (например, инструкция
xchg
) помогает в реализации мьютекса?
Задание 3. Межпроцессное взаимодействие (IPC)
Цель: Организовать обмен данными через каналы и сокеты.
Шаги:
Неименованный канал (pipe)
bashls -l | grep ".c" # Данные из ls передаются в grep через pipe
Именованный канал (FIFO)
bashmkfifo my_pipe # Создание FIFO # В терминале 1: cat > my_pipe # В терминале 2: cat my_pipe # Введите текст в терминале 1 — он появится в терминале 2
UNIX-сокет (локальный)
bash# Сервер: nc -U -l socket.sock # Клиент: nc -U socket.sock
- Введите текст в клиенте — он отобразится на сервере.
Вопросы для самоконтроля:
- В чём разница между неименованным и именованным каналом?
- Как сообщения в IPC связаны с понятием «монитора»?
Задание 4. Тупики (Deadlock)
Цель: Создать и обнаружить тупик, предложить решение.
Шаги:
Создание тупика
c// deadlock.c #include <pthread.h> pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER; void* thread1(void* arg) { pthread_mutex_lock(&lock1); sleep(1); pthread_mutex_lock(&lock2); pthread_mutex_unlock(&lock2); pthread_mutex_unlock(&lock1); return NULL; } void* thread2(void* arg) { pthread_mutex_lock(&lock2); sleep(1); pthread_mutex_lock(&lock1); pthread_mutex_unlock(&lock1); pthread_mutex_unlock(&lock2); return NULL; } int main() { pthread_t t1, t2; pthread_create(&t1, NULL, thread1, NULL); pthread_create(&t2, NULL, thread2, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); return 0; }
- Запустите программу: она зависнет из-за deadlock.
- Найдите PID:
ps aux | grep deadlock
. - Проанализируйте блокировку:bash
strace -p <PID> # Ищите вызовы futex (системный вызов для мьютексов)
Решение тупика
Измените порядок захвата мьютексов вthread2
:cvoid* thread2(void* arg) { pthread_mutex_lock(&lock1); // Сначала lock1, как в thread1 sleep(1); pthread_mutex_lock(&lock2); // ... }
- Перекомпилируйте и убедитесь, что программа завершается.
Вопросы для самоконтроля:
- Какие 4 условия возникновения тупика продемонстрированы в примере?
- Как метод «иерархии ресурсов» предотвращает тупики?
Задание 5. Планирование процессов
Цель: Изменить приоритеты и политики планирования.
Шаги:
Изменение приоритета через
nice
bashnice -n 10 ./process # Запуск с приоритетом 10 renice -n 5 -p <PID> # Изменение приоритета запущенного процесса top # Столбец NI показывает nice-уровень
Real-time планирование через
chrt
bashchrt -r 50 ./process # Запуск с политикой SCHED_FIFO и приоритетом 50 chrt -m # Показать диапазон приоритетов для real-time
Анализ политики планирования
bashcat /proc/<PID>/sched # Политика и приоритет процесса
Вопросы для самоконтроля:
- В чём разница между вытесняющим и невытесняющим планированием?
- Почему для real-time процессов используется приоритет выше, чем для обычных?
Отчёт по работе
- Для каждого задания предоставьте:
- Скриншоты терминала с результатами выполнения.
- Ответы на вопросы для самоконтроля.
- Выводы:
- Как аппаратные механизмы (например, атомарные операции) поддерживают взаимоисключение?
- Почему игнорирование проблемы тупиков недопустимо в критических системах?
Дополнительные материалы
- Мануалы:
man 2 fork
,man 7 pthreads
,man 2 sem_overview
. - Для углубления: Изучите исходный код планировщика CFS в ядре Linux (файл
kernel/sched/fair.c
).
Преподаватель проверяет:
- Корректность выполнения всех заданий.
- Понимание связи между теорией и практикой (через ответы на вопросы).
Успешной работы! 😊