Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология
Образование Политология Производство Психология Стандартизация Технологии


Текущая задача и структура задачи



Как мы видели в ядре, в том случае, когда нам нужно получить или сохранить информацию в задачу (процесс), которая является текущей для данного процессора, мы используем гло­бальную переменную current для получения соответствующей структуры задачи. Напри­мер, current-> pid хранит ID процесса. Linux предоставляет быстрый (и оригинальный) ме­тод получения структуры текущей задачи.

Каждому процессу назначается 8 Кб последовательной памяти при создании. (В версии 2.6 при компиляции имеется возможность изменить это значение на 4 Кб вместо 8 Кб.) Этот блок в 8 Кб занят структурой задачи и стеком данного процесса. Во время создания процесса Linux помещает структуру задачи в нижний конец 8 Кб памяти, а указатель на стек устанавли­вается в верхний конец этого блока. Указатель на стек ядра (особенно для х86 и РРС rl) уве­личивается по мере накопления данных в стеке. Так как эта область 8 Кб выровнена по страни­цам, ее начальный адрес (в шестнадцатеричной нотации) всегда кончается на 0x000 (множи­тель 4 Кб).

Как вы можете догадаться, оригинальным методом, с помощью которого Linux определя­ет структуру текущей задачи, является AND содержимого указателя на стек и 0xffff_f000. Последние версии Linux на РРС пошли немного далее и используют для хранения указателя на текущий процесс регистр общего назначения 2.



Глава 7 • Планировщик и синхронизация ядра


Строки 17и 30

Конструкция asm volatileO1 включает блок встроенного ассемблерного кода, а ключевое слово volatile подразумевает, что компилятор не будет изменять (опти­мизировать) данную функцию.

Строки 17-18

Помещает регистры flags и ebp в стек. (Обратите внимание, что мы используем стек, связанный с задачей prev.)

Строка 19

Эта строка сохраняет указатель текущей задачи esp в структуру задачи prev.

Строка 20

Перемещает указатель стека со следующей структуры задачи в esp текущего процессора.

ОБРАТИТЕ ВНИМАНИЕ: по определению мы просто желаем переключения контекста.

Теперь у нас есть новый стек ядра, и поэтому все ссылки к текущему процессу будут производиться к структуре задачи next.

Строка 21

Сохранение адреса возврата в prev в структуру задачи. Отсюда задача prev продолжает свое выполнение после повторного запуска.

Строка 22

Помещение адреса возврата [откуда мы возвращаемся из_ switch_to () ] в стек.

Это eis из задачи next.

Строка 23

Переход в С-функцию__ switch_to () для обновления следующей информации:

• структуры следующего потока указателем на стек ядра;

• локального описателя хранилища потока для данного процессора;

• при необходимости f s и gs для prev и next;

• при необходимости регистров отладки;

• при необходимости битовых карт ввода-вывода.

Далее__ switch_to () возвращается и обновляет структуру задачи prev.

*• Более подробную информацию о volatile см. в гл. 2


7.1 Планировщик Linux



Строки 24-25

Извлечение базового указателя и регистра flags из стека ядра новой (следующей за­дачи).

Строки 26-29

Это параметры ввода-вывода для функции ассемблерной вставки. (См. разд. 2.4 для получения более подробной информации о принудительной передаче этих параметров.)

Строка 29

С помощью магии ассемблера prev возвращается в еах, являющийся третьим по счету параметром. Другими словами, входной параметр prev передается из макроса switch_to () как последний выходной параметр.

Так как switch_to() является макросом, он выполняется внутри кода, ко­торый вызывает context_switch (). Обычно он не возвращает функций.

Для простоты запомните, что switch_to () передает prev обратно в регистр еах, а выполнение продолжается в context_switch (), где следующей инструкци­ей является return prev (строка 1074 в kernel/sched.с). Это позволяет context_swi ten () передавать указатель обратно в последнюю запущенную задачу.

7.1.2.2 Отслеживание context_switch() на РРС

Код context_switch () для РРС делает немного больше работы для получения того же результата. В отличие от регистра сгЗ на архитектуре х86, РРС использует функцию хеширования для указания на окружение контекста. Следующий код для switch_mm() касается этой функции, но в гл. 4, «Управление памятью», находится его более подроб­ное описание.

Здесь приведена функция switch_mm(), которая, в свою очередь, вызывает функ­цию set_context ().

/include/asm-ppc/mmu_context.h

155 static inline void switch_mm(struct mm_struct *prev,

struct mm_struct *next, struct task_struct *tsk)

156 {

157 tsk-> thread.pgdir = next-> pgd;

158 get_mmu_context(next);

159 set_context(next-> context/ next-> pgd);

160 }

Строка 157

Глобальная директория страницы (регистр сегмента) для нового потока устанавли­вается указывающей на указатель next-> pgd.



Глава 7 • Планировщик и синхронизация ядра


Строка 158

Поле context структуры mm_struct (next-> context) передается в switch__iran() и обновляется значением соответствующего контекста. Эта ин­формация получается из глобальной ссылки на переменную context_map[ ], которая содержит набор полей битовых карт.

Строка 159

Это вызов ассемблерной функции set_context. После самого кода приводится обсуждение этой функции. Во время выполнения инструкции Ыг на строке 1468 код возвращается в функцию switch_mm.


/arch/ppc/kernel/head.S

1437 _GLOBAL(set_context)

1438 mulli r3, r3, 897 /* умножение контекста на фактор ассимметрии */

1439 rlwinm r3, r3, 4, 8, 27 /* VSID = (context & Oxfffff) « 4 */

1440 addis r3, r3, 0x6000 /* установка битов Ks, Ku */

1441 li r0, NUM_USER_SEGMENTS

1442 mtctr rO

1457 3: isync

/* следующий VSID */ /* очистка переполнения поля VSID */ /* адрес следующего сегмента */

1461 mtsrin r3, r4

1462 addi r3, r3, 0x111

1463 rlwinm r3, r3, 0, 8, 3

1464 addis r4, r4, 0x1000

1465 bdnz 3b

1466 sync

1467 isync

1468 blr

Строки 1437-1440

Поле context структуры mm_struct (next-> context) передается в set_ context () через гЗ и устанавливает хеш-функцию для сегментации РРС.

Строки 1461-1465

Поле структуры mm_struct (next-> pgd) передается в set_context () через г4 и указывает на регистр сегмента.

Сегментация - это основа управления памятью на РРС (см. гл. 4). Во время возвраще­ния из set_context (), инициализируется mm__struct для соответствующей области памяти и выполняется возвращение в switch_mm ().


7.1 Планировщик Linux



7.1.2.3 Отслеживание прохождения switch_to() на РРС

Результат реализации switch_to () на РРС не обязательно идентичен вызову х86; он берет указатели на current и next задачи и возвращает указатель на предыдущую вы­полнявшуюся задачу:

include/asm-ppc/system.h

88 extern struct task_struct *_ switch_to(struct task_struct *,

89 struct task_struct *);

90 #define switch_to(prev, next, last)

((last) = __ switch_to((prev), (next)))

92 struct thread_struct;

93 extern struct task_struct *_switch(struct thread_struct *prev,

94 struct thread_struct *next);

В строке 88__ switch_to () получает параметр типа task_struct и на строке 93

получает в качестве параметра thread_struct. Это необходимо для того, чтобы вхож­
дение в task_struct содержало архитектурно-зависимую информацию о регистрах
процессора, необходимую данному процессу. Теперь давайте рассмотрим реализацию
__ switch_to ().

/arch/ppc/kernel/process.с

200 struct task_struct *_ switch_to(struct task__struct *prev,

struct task_struct *new)

201 {

202 struct thread__struct *new_thread, *old_thread;

203 unsigned long s;

204 struct task_struct *last;

205 local_irq__save(s);

 

247 new_thread = & new-> thread;

248 old_thread = & current-> thread;

249 last = „switchtold^hread, new__thread);

250 local_irq_restore(s);

251 return last;

252 }

Строка 205

Отключение прерываний перед переключением контекста.



Глава 7 • Планировщик и синхронизация ядра


Строки 247-248

Выполнение еще продолжается в контексте старого потока, а указатель на структуру потока передается в функцию _switch ().

Строка 249

Ассемблерная функция _switch() вызывается для выполнения работы по переключению двух структур потоков (см. следующий раздел).

Строка 250

Включение прерываний после переключения контекста.

Для лучшего понимания того, что нужно обменять в потоке РРС, нам нужно рас­смотреть структуру thread_struct, передаваемую в строке 249.

Вы можете вспомнить из описания переключения контекста х86, что переключение официально не происходит до тех пор, пока мы не указываем на новый стек ядра. Это про­исходит в _swi tch ().

Отслеживание кода РРС для switch 0

По соглашению параметры С-функции РРС (слева направо) хранятся в гЗ, г4, г5,..., г12. При вхождении в switch () гЗ указывает на thread_struct для текущей задачи, а г4 указывает на thread_struct для новой задачи:

/arch/ppc/kernel/entry.S

437 _GLOBAL(_switch)

438 stwu rl, -INT_FRAME_SIZE(rl)
43 9 mflr rO

440 stw rO, INT_FRAME_SIZE+4(rl)

441 /* r3-rl2 сохраняются вызывающим -- Cort */

442 SAVE_NVGPRS(r1)

443 stw rO, _NIP(rl) /* возвращение в вызов переключателя */

444 mfmsr rll

458 1: stw rll/_MSR(rl)

459 mfcr rlO

460 stw rlO, _CCR(rl)

461 stw rl, KSP(r3) /* Установка старого указателя на стек */ 462

 

463 tophys(r0, r4)

464 CLR_TOP32(rO)

465 mtspr SPRG3/rO /* обновление физического адреса текущего THREAD */

466 lwz rl, KSP(r4) /* Загрузка нового указателя на стек */

467 /* сохранение старого текущего " последнего"

для возвращаемого значения */

468 mr r3, r2


Планировщик Linux



469 addi r2, r4, -THREAD /* Обновление текущего */

478 lwz rO, _CCR(rl)

479 mtcrf OxFF, rO

480 REST_NVGPRS(rl) 481

482 lwz r4, _NIP(rl) /* Возвращение в вызывающий _switch код

в новой задаче */

483 mtlr r4

484 addi rl, rl, INT_FRAME_SIZE

485 blr

Побайтовый механизм замены предыдущей thread_struct новой мы оставляем на ваше самостоятельное изучение. Это не сложно, а основы использования rl, r2, гЗ, SPRG3 и г4 вы можете увидеть в _switch ().

Строки 438-460

Окружение сохраняется в текущий стек с сохранением указателя на текущий стек вг1.

Строка 461

Далее окружение полностью сохраняется в текущую thread_struct, передающуюся через указатель в гЗ.

Строки 463-465

SPRG3 обновляется для указания на структуру потока для новой задачи.

Строка 466

KSP - это отступ в структуре задачи (г4) в указателе на новый стек ядра задачи. Указатель на стек rl обновляется этим значением. (В этой точке выполняется переключение контекста РРС.)

Строка 468

Текущий указатель на предыдущую задачу возвращается из _switch() в гЗ. Он представляет последнюю задачу.

Строка 469

Текущий указатель (г2) обновляется указателем на новую структуру задачи (г4).

Строки 478-486

Восстановление оставшегося окружения из нового стека и возвращение в вызы­вающую функцию с предыдущей структурой задачи в гЗ.

На этом мы заканчиваем объяснение context_switch (). В этой точке процессор обменял два процесса: prev и next, вызванные context_switch в schedule ().



Глава 7 • Планировщик и синхронизация ядра


kernel/sched.с

1709 prev = context_switch(rq, prev, next);

Теперь prev указывает на процесс, который мы только что переключили, a next указывает на текущий процесс.

Теперь, когда мы обсудили планирование процессов в ядре Linux, мы можем разо­браться в том, как планируются задачи, а именно, что вызывает schedule () и как про­цесс передает процессор другому процессу.

Занятие процессора

Процессы могут занимать процессор простым вызовом функции schedule (). Она обычно используется в коде ядра и драйвером устройства, которое хочет заснуть или до­ждаться поступления сигнала1. Другие задачи тоже постоянно хотят использовать про­цессор, и системный таймер должен сообщать им, когда они смогут выполниться. Ядро Linux периодически захватывает процессор, при этом активные процессы останавли­ваются, и затем выполняет несколько зависящих от времени задач. Одна из этих задач, scheduler_tick (), позволяет ядру заставить процесс приостановиться. Если процесс выполняется слишком долго, ядро не возвращает этому процессу управление и вместо него выбирает другой процесс. Теперь мы изучим, как scheduler_tick () определяет текущий процесс, который должен занять процессор.

kernel/sched.с

1981 void scheduler_tick(int user_ticks/ int sys_ticks)

1982 {

 

1983 int cpu = smp_processor_id();

1984 struct cpu_usage_stat *cpustat = & kstat_this_cpu.cpustat;

1985 runqueue_t *rq = this_rq();

1986 task_t *p = current; 1987

1988 rq-> timestamp_last_tick = sched_clock(); 1989

1990 if (rcu_pending(cpu))

1991 rcu_check_callbacks(cpu, user_ticks);

Строки 1981-1986

Этот блок кода инициализирует структуры данных, необходимые функции scheduler_tick(); cpu, cpu_usage_stat и rq получают значения иденти-

1 Соглашение Linux утверждает, что вы никогда не должны вызывать schedule во время циклической блокировки, так как это может завести систему в тупик. Это действительно хороший совет!


Планировщик Linux



фикатора процессора, статус процессора и очередь выполнения для текущего процессора; р - это указатель на текущий выполняемый на ери процесс.

Строка 1988

Последний тик очереди выполнения устанавливается в текущее время в наносекундах.

Строки 1990-1991

На SMP-системах нам нужно проверить наличие требующих выполнения просроченных обновлений чтения-записи (RCU). Если это так, мы выполняем их с помощью rcu__check_callback ().

kernel/sched.с

1993 /* обратите внимание: контекст irq этого таймера также

должен учитываться циклом for */

1994 if (hardirq_count() - HARDIRQ_OFFSET) {

1995 cpustat-> irq += sys_ticks;

1996 sys_ticks = 0;

1997 } else if (softirq_count()) {

1998 cpustat-> softirq += sys_ticks;

1999 sys_ticks = 0;

2000 }
2001

2002 if (p == rq-> idle) {

2 003 if (atomic_read(& rq-> nr_iowait) > 0)

2 004 cpustat-> iowait += sys_ticks;

2005 else

2006 cpustat-> idle += sys_ticks;

2007 if (wake_priority_sleeper(rq))

2008 goto out;

2009 rebalance_tick(cpu, rq, IDLE);

2010 return;

 

2011 }

2012 if (TASK_NICE(p) > 0)

2013 cpustat-> nice += user_ticks;

2014 else

2 015 cpustat-> user += user_ticks;

2 016 cpustat-> system += sys_ticks;

Строки 1994-2000

cpustat следит за статистикой ядра, и мы обновляем статистику об аппаратных и программных прерываниях по количеству наступивших системных тиков.

Строки 2002-2011

Если текущего выполняемого процесса нет, мы автоматически проверяем наличие процессов, ожидающих ввода-вывода. Если это так, статистика ввода-вывода


374 Глава 7 • Планировщик и синхронизация ядра

процессора увеличивается; в противном случае увеличивается статистика ожи­дающего процессора. В однопроцессорных системах rebalance_tick () не дела­ет ничего, а на многопроцессорных системах rebalance_tick() старается сба­лансированно загрузить текущий процессор, так как он простаивает.

Строки 2012-2016

В этом блоке кода собирается дополнительная статистика о процессоре. Если теку­щий процесс был niced, мы увеличиваем счетчик nice для процессора; в против­ном случае увеличивается пользовательский счетчик тиков. И наконец, мы уве­личиваем системный счетчик тиков процессора.

kernel/sched.с

2019 if (p-> array! = rq-> active) {

2020 set_tsk_need_resched(p);

2021 goto out;

 

2022 }

2023 spin_lock(& rq-> lock);

Строка 2019-2022

Здесь мы видим, почему мы сохраняем указатель на массив приоритетов в task_struct процесса. Планировщик проверяет текущий процесс и смотрит, не является ли он больше активным. Если процесс завершился, планировщик устанав­ливает флаг перепланировки процесса и переходит в конец функции scheduler., tick(). В этой точке (строки 2092-2093) планировщик пытается сбалансированно загрузить процессор, так как активных задач нет. Этот случай наступает, когда пла­нировщик перехватывает управление процессором перед тем, как текущий процесс сможет себя перепланировать или очиститься после удачного выполнения.

Строка 2023

В этой точке мы знаем, что текущий процесс был запущен и, не завершился и суще­ствует. Теперь планировщик хочет передать управление процессором другому процессу; первое, что он должен сделать, - это заблокировать очередь выполнения.

kernel/sched.с

2024 /*

2 025 * Задача, выполняющаяся во время этого тика, обновляет счетчик

2026 * временного среза. Обратите внимание: мы не обновляем приоритета

2027 * потока до тех пор, пока он не засыпает или не расходует свой

2 028 * временной срез. Это позволяет интерактивным задачам использовать 2 029 * свои временные срезы на наивысшем уровне приоритета levels.

2030 */

2031 if (unlikely(rt_task(p))) {


Планировщик Linux



2032 /*

2 033 * RR-задаче требуется специальная форма управления временным
* срезом.

2034 * FIFO-задача не имеет временных срезов.

2035 */

2036 if ((p-> policy == SCHED_RR) & & ! --p-> time_slice) {

 

2037 p-> time_slice = task_timeslice(p);

2038 p-> first_time_slice = 0;

2039 set_tsk_need_resched(p); 2040

 

2041 /* помещение в конец очереди */

2042 dequeue_task(p, rq-> active);

2043 enqueue_task(p, rq-> active);

 

2044 }

2045 goto out_unlock;

2046 }

Строки 2031-2046

Простейший случай для планировщика наступает, когда текущий процесс является задачей реального времени. Задачи реального времени всегда имеют наивысший приоритет по сравнению с другими задачами. Если задача является FIFO и запуще­на, она продолжает свои операции, а мы переходим в конец функции и снимаем бло­кировку очереди выполнения. Если текущий процесс является циклической задачей реального времени, мы уменьшаем его временной срез. Если у задачи не осталось временного среза, наступает время перепланировать следующую циклическую за­дачу реального времени. Текущая задача получает новый временной срез, рассчиты­ваемый task_timeslice (). Далее задача сбрасывает свой первый временной срез. Затем задача помечается как требующая перепланировки и, наконец, помеща­ется в конец списка циклических задач реального времени удалением ее из активно­го массива очереди выполнения и помещением в его конец. После этого планиров­щик переходит в конец функции и снимает блокировку с очереди выполнения.

kernel/sched.с

2047 if (! —p-> time_slice) {

2048 dequeue_task(p/ rq-> active);

2049 set_tsk_need_resched(p);

2050 p-> prio = effective_prio(p);

2051 p-> time_slice = task_timeslice(p);

2052 p-> first_time_slice = 0; 2053

2054 if (! rq-> expired_timestamp)

2055 rq-> expired_timestamp = jiffies;



Глава 7 • Планировщик и синхронизация ядра


2056 if (! TASK_INTERACTIVE(p) || EXPIRED_STARVING(rq) ) {

2 057 enqueue_task(p, rq-> expired);

2 058 if (p-> static_prio < rq-> best_expired_prio)

2059 rq-> best_expired__prio = p-> static_prio;

2060 } else

2061 enqueue_task(p, rq-> active);

2062 } else {

Строки 2047-2061

В этой точке планировщик знает, что текущий процесс не является процессом реаль­ного времени. Он увеличивает временной срез процесса а также, в этом же блоке, временной срез, который истек и достиг 0. Планировщик удаляет задачу из активно­го массива и устанавливает флаг перепланировки процесса. Приоритет задачи пересчитывается, а временной срез сбрасывается. Обе эти операции производятся с учетом предыдущей активности процесса1. Если временная отметка истекшей очереди ожидания достигла 0, что обычно происходит, когда в массиве активной очереди выполнения не остается процессов, мы присваеваем ему значение текущего момента.

Моменты

Моменты (jiffies) - это 32-битовые переменные, отсчитывающие количество тиков с момента загрузки системы. На процессоре с частотой 100 Гц эти переменные переполнятся и будут обнулены примерно через 497 дней. Макрос в строке 20 представляет собой метод для доступа к этому значению в качестве и64. Кроме этого, в include/jiffies.h существует макрос для определения переполнения моментов.

include/linux/jiffies.h

017 extern unsigned long volatile jiffies;

020 u64 get_jiffies_64(void);

Обычно мы поощряем интерактивные задачи, заменяя их в активном массиве приоритетов очереди выполнения; случай else в строке 2060. Тем не менее мы не хотим тормозить истекшие задачи. Для определения того, не ожидает ли истекшая задача передачи ей процессора слишком долго, мы используем EXPIRED_STARVING () (см. EXPIRED_STARVING в строке 1968).

Функция возвращает true, если первая истекшая задача ожидает «неоправданно» дол­гое время или если массив истекших задач содержит задачу, имеющую более высокий приоритет, чем у текущего процесса. Неоправданность ожидания зависит от загрузки

к См. ef f ective__prio () и task_timeslice ().


Планировщик Linux



и количества обменов массивов активных и истекших задач и увеличивается с ростом ко­личества запущенных задач.

Если задача не является интерактивной или если истекшая задача тормозится, пла­нировщик берет текущий процесс и включает его в массив приоритетов истекшей очереди выполнения. Если статический приоритет текущего процесса больше, чем наибольший приоритет задачи из истекшей очереди выполнения, мы обновляем очередь выполнения для отражения факта, что приоритет истекшего массива увеличился. [Помните, что задачи с большим приоритетом в Linux имеют меньшие номера и поэтому в коде производится проверка (< ).]

kernel/sched.с

2062 } else {

2063 /*

2 064 * Предотвращение слишком долгих временных срезов, позволяющих

2065 * монополизировать процессор. Мы делаем это, разбивая временные

2066 * срезы на меньшие порции.

2 068 * ОБРАТИТЕ ВНИМАНИЕ: это не значит, что временные срезы задачи

2069 * истекли или были удалены другим путем, их обслуживание просто

2070 * приоритетно прерываются другой задачей с эквивалентным

2071 * приоритетом.(Задача с большим приоритетом может приоритетно

2072 * прервать выполнение данной задачи.) Мы переносим эту задачу в

2073 * конец списка уровней приоритетов, представляющий собой
2 074 * карусельную структуру задач с одинаковым приоритетом.

2075 *

2076 * Применяется только к интерактивным задачам

2 077 * с диапазоном не менее TIMESLICE_GRANULARITY.

2078 */

2079 if (TASK_INTERACTIVE(p) & & ! ((task_timeslice(p) -

2080 p-> time_slice) % TIMESLICE_GRANULARITY(p)) & &

2081 (p-> time_slice > = TIMESLICE_GRANULARITY(p) ) & &

2082 (p-> array == rq-> active)) { 2083

2 084 dequeue_task(p, rq-> active);

2085 set_tsk_need_resched(p);

2086 p-> prio = effective_prio(p); 2 087 enqueue_task(p, rq-> active);

2088 }

2089 }

2 090 out_unlock:

2 091 spin_unlock(& rq-> lock);

2092 out:

2093 rebalance_tick(cpu, rq, NOT_IDLE);

2094 }



Глава 7 • Планировщик и синхронизация ядра


Строки 2079-2089

Последний случай перед вызовом планировщика - это когда текущий процесс явля­ется запущенным и у него еще остался временной срез. Планировщику нужно удо­стовериться, что процесс с большим временным срезом не заблокирует процессор. Если задача интерактивна, имеет временной срез большей длины, чем TIMESLICE_GRANUARITY, и активна, планировщик убирает ее из активной очереди. После этого устанавливается флаг перепланировки задачи, пересчитывает-ся ее приоритет и она помещается обратно в активный массив очереди выполнения. Это позволяет быть уверенным, что процесс с определенным приоритетом и с боль­шим временным срезом не застопорит другой процесс с аналогичным приоритетом.

Строки 2090-2094

Планировщик завершает перераспределение очереди выполнения и разблокирует ее; если выполнение происходит на SMP-системе, производится попытка сбалан­сированной нагрузки.

Связь того, как процесс помечается для перепланировки с помощью scheduler_ tick () и как процесс планируется с помощью schedule (), иллюстрирует работу пла­нировщика в ядре Linux версии 2.6. Теперь мы углубимся в детали того, что называется в планировщике «приоритетом».


Поделиться:



Популярное:

Последнее изменение этой страницы: 2016-03-25; Просмотров: 783; Нарушение авторского права страницы


lektsia.com 2007 - 2024 год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! (0.134 с.)
Главная | Случайная страница | Обратная связь