Мегаобучалка Главная | О нас | Обратная связь


Многопоточные и многоядерные микропроцессоры



2016-09-17 2232 Обсуждений (0)
Многопоточные и многоядерные микропроцессоры 4.67 из 5.00 6 оценок




Процессоры

Многопоточные и многоядерные микропроцессоры

Память

Диски

Устройства ввода-вывода

Шины

Загрузка компьютера

 

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

Концептуально простой персональный компьютер можно представить в виде модели, аналогичной изображенной на рис. 1.6. Центральный процессор, память и устройства ввода-вывода соединены системной шиной, по которой они обмениваются информа­цией друг с другом. Современные персональные компьютеры имеют более сложную структуру и используют несколько шин, которые мы рассмотрим чуть позже. Для

начала нас вполне устроит и эта модель. В следующих разделах будет дан краткий обзор отдельных компонентов и рассмотрены некоторые аспекты аппаратного обеспечения, представляющие интерес для разработчиков операционных систем. Наверное, излишне упоминать о том, что это будет очень краткое изложение. Компьютерному оборудо­ванию и его организации посвящено множество книг. Можно порекомендовать две довольно известные книги (Tanenbaum, 2012; Patterson and Hennessy, 2013).

 

Процессоры

Центральный процессор — это «мозг» компьютера. Он выбирает команды из памяти и выполняет их. Обычный цикл работы центрального процессора выглядит так: выбор­ка из памяти первой команды, ее декодирование для определения ее типа и операндов, выполнение этой команды, а затем выборка, декодирование и выполнение последую­щих команд. Этот цикл повторяется до тех пор, пока не закончится программа. Таким образом программы выполняются.

Для каждого типа центрального процессора существует определенный набор команд, которые он может выполнять. Поэтому x86 не может выполнять программы, написан­ные для ARM-процессоров, а те, в свою очередь, не в состоянии выполнять программы, написанные для x86. Поскольку доступ к памяти для получения команды или данных занимает намного больше времени, чем выполнение команды, у всех центральных про­цессоров есть несколько собственных регистров для хранения основных переменных и промежуточных результатов. Соответственно набор команд содержит, как правило, команды на загрузку слова из памяти в регистр и на запоминание слова из регистра в память. Другие команды объединяют два операнда из регистров, памяти или обоих этих мест для получения результата — например, складывают два слова и сохраняют результат в регистре или в памяти.

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

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

Еще один регистр содержит слово состояния программы — PSW (Program Status Word). В этом регистре содержатся биты кода условия, устанавливаемые инструкциями сравнения, а также биты управления приоритетом центрального процессора, режимом (пользовательским или ядра) и другие служебные биты. Обычно пользовательские программы могут считывать весь регистр PSW целиком, но записывать — только в некоторые из его полей. Регистр PSW играет важную роль в системных вызовах и операциях ввода-вывода.

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

Для повышения производительности процессоров их разработчики давно отказались от простой модели извлечения, декодирования и выполнения одной команды за один цикл. Многие современные процессоры способны одновременно выполнять более одной команды. Например, у процессора могут быть отдельные блоки для выборки, декодирования и выполнения команд, тогда во время выполнения команды п он смо­жет декодировать команду п + 1 и осуществлять выборку команды п + 2. Подобная организация работы называется конвейером. На рис. 1.7, а показан конвейер с тремя стадиями обработки. Обычно используются более длинные конвейеры. В большинстве конструкций конвейеров, как только команда выбрана и помещена в конвейер, она должна быть выполнена, даже если предыдущая выбранная команда была условным ветвлением. Для разработчиков компиляторов и операционных систем конвейеры — это сплошная головная боль, обнажающая перед ними все сложности исходной маши­ны и заставляющая справляться с возникающими проблемами.

Более совершенной конструкцией по сравнению с конвейерной обладает суперскаляр- ный процессор, показанный на рис. 1.7, б. Он имеет несколько исполнительных блоков, например: один — для целочисленной арифметики, другой — для арифметики чисел с плавающей точкой, третий — для логических операций. Одновременно выбираются две и более команды, которые декодируются и помещаются в буфер хранения, в ко­тором ожидают возможности своего выполнения. Как только исполнительный блок становится доступен, он обращается к буферу хранения за командой, которую может выполнить, и если такая команда имеется, извлекает ее из буфера, а затем выполняет. В результате команды программы часто выполняются не в порядке их следования. При этом обеспечение совпадения конечного результата с тем, который получился бы при последовательном выполнении команд, возлагается в основном на аппаратуру. Однако, как мы увидим в дальнейшем, при этом подходе неприятные усложнения коснулись и операционной системы.

Как уже упоминалось, большинство центральных процессоров, за исключением самых простых, используемых во встраиваемых системах, имеют два режима работы: режим ядра и пользовательский режим (режим пользователя). Обычно режимом управляет специальный бит в слове состояния программы — PSW. При работе в режиме ядра процессор может выполнять любые команды из своего набора и использовать лю­бые возможности аппаратуры. На настольных и серверных машинах операционная система обычно работает в режиме ядра, что дает ей доступ ко всему оборудованию. На большинстве встроенных систем в режиме ядра работает только небольшая часть операционной системы, а вся остальная ее часть — в режиме пользователя.

Пользовательские программы всегда работают в режиме пользователя, который допу­скает выполнение только подмножества команд и дает доступ к определенному подмно­жеству возможностей аппаратуры. Как правило, в пользовательском режиме запрещены все команды, касающиеся операций ввода-вывода и защиты памяти. Также, разумеется, запрещена установка режима ядра за счет изменения значения бита режима PSW.

Для получения услуг от операционной системы пользовательская программа должна осуществить системный вызов, который перехватывается внутри ядра и вызывает операционную систему. Инструкция перехвата TRAP осуществляет переключение из пользовательского режима в режим ядра и запускает операционную систему. Когда обработка вызова будет завершена, управление возвращается пользовательской про­грамме и выполняется команда, которая следует за системным вызовом. Подробности механизма системного вызова будут рассмотрены в этой главе чуть позже, а сейчас его следует считать специальной разновидностью инструкции вызова процедуры, у которой есть дополнительное свойство переключения из пользовательского режима в режим ядра. В дальнейшем для выделения в тексте системных вызовов будет ис­пользоваться такой же шрифт, как в этом слове: read.

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

Многопоточные и многоядерные микропроцессоры

Закон Мура гласит, что количество транзисторов на одном кристалле удваивается каждые 18 месяцев. Этот «закон», в отличие от закона сохранения импульса, не имеет никакого отношения к физике, он появился в результате наблюдений одного из со­учредителей корпорации Intel Гордона Мура (Gordon Moore) за темпами, с которыми шло уменьшение размеров транзисторов. Закон Мура соблюдался в течение трех де­сятилетий и, как ожидается, будет соблюдаться как минимум еще одно десятилетие. После этого число атомов в транзисторе станет настолько мало, что дальнейшему уменьшению размеров транзистора воспрепятствует усиливающаяся роль законов квантовой механики.

Высокая плотность размещения транзисторов ведет к проблеме: как распорядиться их возросшим количеством? Ранее мы уже ознакомились с одним из подходов к ее решению — использованием суперскалярной архитектуры, имеющей множество функциональных блоков. Но с ростом числа транзисторов открываются более широкие возможности. Одно из очевидных решений — размещение на кристалле центрального процессора более объемной кэш-памяти — уже воплощено в жизнь. Однако уже достиг­нут порог, за которым дальнейшее увеличение объема кэш-памяти только уменьшает отдачу от этого решения.

Следующим очевидным шагом является дублирование не только функциональных блоков, но и части управляющей логики. Это свойство, впервые использованное в Pentium 4 и названное многопоточностью, или гиперпоточностью (hyperthreading по версии Intel), стало неотъемлемой принадлежностью процессора x86 и ряда других процессоров, включая SPARC, Power5, Intel Xeon, а также процессоры семейства Intel Core. В первом приближении эта технология позволяет процессору сохранять состо­яние двух различных потоков и переключаться между ними за наносекунды. (Поток является разновидностью легковесного процесса, который, в свою очередь, является выполняющейся программой; подробности мы рассмотрим в главе 2.) Например, если одному из процессов нужно прочитать слово из памяти (что занимает несколько тактов), многопоточный процессор может переключиться на другой поток. Многопо­точность не предлагает настоящей параллельной обработки данных. Одновременно работает только один процесс, но время переключения между процессами сведено до наносекунд.

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

Кроме процессоров с многопоточностью в настоящее время применяются процессоры, имеющие на одном кристалле четыре, восемь и более полноценных процессоров, или ядер. Например, четырехъядерные процессоры (рис. 1.8) фактически имеют в своем составе четыре мини-чипа, каждый из которых представляет собой независимый про­цессор. (Кэши мы рассмотрим чуть позже.) Некоторые процессоры, например Intel Xeon Phi и Tilera TilePro, могут похвастаться более чем 60 ядрами на одном кристалле. Несомненно, для использования такого многоядерного процессора потребуется много­процессорная операционная система.

Кстати, с точки зрения абсолютных чисел нет ничего лучше, чем современные графи­ческие процессоры (Graphics Processing Unit, GPU). На их кристаллах содержатся тысячи крохотных ядер. Они очень хорошо подходят для множества небольших про­изводимых параллельно вычислений, таких как визуализация многоугольников в гра­фических приложениях. Но для выполнения последовательных задач они не годятся. К тому же их трудно программировать. Хотя графические процессоры могут найти применение и для операционных систем (например, при кодировании или обработке сетевого трафика), не похоже, что на них могла бы запускаться основная часть самой операционной системы.

Память

Второй основной составляющей любого компьютера является память. В идеале память должна быть максимально быстрой (работать быстрее, чем производится выполнение одной инструкции, чтобы работа центрального процессора не замедлялась обраще­ниями к памяти), довольно большой и чрезвычайно дешевой. Никакая современная технология не в состоянии удовлетворить все эти требования, поэтому используется другой подход. Система памяти создается в виде иерархии уровней (рис. 1.9). Верхние уровни обладают более высоким быстродействием, меньшим объемом и более высокой удельной стоимостью хранения одного бита информации, чем нижние уровни, иногда в миллиарды и более раз.

Верхний уровень состоит из внутренних регистров процессора. Они выполнены по той же технологии, что и сам процессор, и поэтому не уступают ему в быстродействии. Следовательно, к ним нет и задержек доступа. Внутренние регистры обычно предо­ставляют возможность для хранения 32 х 32 бит для 32-разрядного процессора или 64 х 64 бит — для 64-разрядного. В обоих случаях этот объем не превышает 1 Кбайт. Программы могут сами управлять регистрами (то есть решать, что в них хранить), без вмешательства аппаратуры.

Затем следует кэш-память, которая управляется главным образом аппаратурой. Опе­ративная память разделяется на кэш-строки, обычно по 64 байт, с адресами от 0 до 63 в кэш-строке 0, адресами от 64 до 127 в кэш-строке 1 и т. д. Наиболее интенсивно используемые кэш-строки оперативной памяти сохраняются в высокоскоростной кэш­памяти, находящейся внутри процессора или очень близко к нему. Когда программе нужно считать слово из памяти, аппаратура кэша проверяет, нет ли нужной строки в кэш-памяти. Если строка в ней имеется, то происходит результативное обращение к кэш-памяти (cache hit — кэш-попадание), запрос удовлетворяется за счет кэш-памяти без отправки запроса по шине к оперативной памяти. Обычно результативное обраще­ние к кэшу занимает по времени два такта. Отсутствие слова в кэш-памяти вынуждает обращаться к оперативной памяти, что приводит к существенной потере времени. Кэш-память из-за своей высокой стоимости ограничена в объеме. Некоторые маши­ны имеют два или даже три уровня кэша, причем каждый из последующих медленнее и объемнее предыдущего.

Кэширование играет существенную роль во многих областях информатики, это от­носится не только к кэшированию строк оперативной памяти. Довольно часто для повышения производительности к кэшированию прибегают везде, где есть какой-либо объемный ресурс, который можно поделить на фрагменты, часть из которых использу­ется намного интенсивнее всех остальных. Операционные системы используют кэши­рование повсеместно. Например, большинство операционных систем держат интенсив­но используемые файлы (или фрагменты файлов) в оперативной памяти, избегая их многократного считывания с диска. Точно так же результаты преобразования длинных имен файлов вроде /home/ast/projects/minix3/src/kernel/clock.c в дисковый адрес, по которому расположен файл, могут кэшироваться, чтобы исключить необходимость в повторных поисках. И наконец, может кэшироваться для дальнейшего использования результат преобразования адресов веб-страниц (URL) в сетевые адреса (IP-адреса). Можно привести массу других примеров использования технологии кэширования.

В любой системе кэширования довольно скоро возникает ряд вопросов.

1. Когда помещать в кэш новый элемент?

2. В какую область кэша помещать новый элемент?

3. Какую запись удалять из кэша, когда необходимо получить в нем свободное про­странство?

4. Куда именно в памяти большей емкости помещать только что «выселенный» элемент?

Не каждый из этих вопросов имеет отношение к кэшированию. Например, в процессе кэширования строк оперативной памяти в кэше центрального процессора при каждом неудачном обращении к кэш-памяти в нее, как правило, будет вводиться новый эле­мент. При вычислении нужной кэш-строки для размещения нового элемента обычно используются некоторые старшие биты того адреса памяти, на который осуществляется ссылка. Например, при наличии 4096 кэш-строк по 64 байта и 32-разрядных адресов биты с 6-го по 17-й могли бы использоваться для определения кэш-строки, а биты с 0-го по 5-й — для определения байта в кэш-строке. В этом случае элемент, подлежащий удалению, совпадает с тем элементом, в который попадают новые данные, но в других системах такой порядок может и не соблюдаться. Наконец, когда кэш-строка перепи­сывается в оперативную память (если она была изменена в процессе кэширования), место в памяти, в которое ее нужно переписать, однозначно определяется адресом, фигурирующим в запросе.

Применение кэширования оказалось настолько удачным решением, что многие со­временные процессоры имеют сразу два уровня кэш-памяти. Первый уровень, или кэш L1, всегда является частью самого процессора и обычно подает декодированные команды в процессорный механизм исполнения команд. У многих процессоров есть и второй кэш L1 для тех слов данных, которые используются особенно интенсивно. Обычно каждый из кэшей L1 имеет объем 16 Кбайт. Вдобавок к этому кэшу процессо­ры часто оснащаются вторым уровнем кэш-памяти, который называется кэш L2 и со­держит несколько мегабайт недавно использованных слов памяти. Различия между кэш-памятью L1 и L2 заключаются во временной диаграмме. Доступ к кэшу первого уровня осуществляется без задержек, а доступ к кэшу второго уровня требует задержки в один или два такта.

При разработке многоядерных процессоров конструкторам приходится решать, куда поместить кэш-память. На рис. 1.8, а показан один кэш L2, совместно использующийся всеми ядрами. Такой подход применяется в многоядерных процессорах корпорации Intel. Для сравнения на рис. 1.8, б каждое ядро имеет собственную кэш-память L2. Именно такой подход применяет компания AMD. Каждый из подходов имеет свои аргументы «за» и «против». Например, общая кэш-память L2 корпорации Intel требует использования более сложного кэш-контроллера, а избранный AMD путь усложняет поддержание согласованного состояния кэш-памяти L2 разных ядер.

Следующей в иерархии, изображенной на рис. 1.9, идет оперативная память. Это глав­ная рабочая область системы памяти машины. Оперативную память часто называют оперативным запоминающим устройством (ОЗУ), или памятью с произвольным до­ступом (Random Access Memory (RAM)). Ветераны порой называют ее core memory — памятью на магнитных сердечниках, поскольку в 1950-1960-е годы в оперативной па­мяти использовались крошечные намагничиваемые ферритовые сердечники. Прошли десятилетия, но название сохраняется. В настоящее время блоки памяти имеют объем от сотен мегабайт до нескольких гигабайт, и этот объем стремительно растет. Все за­просы процессора, которые не могут быть удовлетворены кэш-памятью, направляются к оперативной памяти.

Дополнительно к оперативной памяти многие компьютеры оснащены небольшой по объему неизменяемой памятью с произвольным доступом — постоянным запоминаю­щим устройством (ПЗУ), оно же память, предназначенная только для чтения (Read Only Memory (ROM)). В отличие от ОЗУ она не утрачивает своего содержимого при отключении питания, то есть является энергонезависимой. ПЗУ программируется на предприятии-изготовителе и впоследствии не подлежит изменению. Эта разновид­ность памяти характеризуется высоким быстродействием и дешевизной. На некото­рых компьютерах в ПЗУ размещается начальный загрузчик, используемый для их запуска. Такой же памятью, предназначенной для осуществления низкоуровневого управления устройством, оснащаются некоторые контроллеры ввода-вывода.

Существуют также другие разновидности энергонезависимой памяти, которые в от­личие от ПЗУ могут стираться и перезаписываться, — электрически стираемые про­граммируемые постоянные запоминающие устройства (ЭСППЗУ) , они же EEPROM (Electrically Erasable PROM), и флеш-память. Однако запись в них занимает на не­сколько порядков больше времени, чем запись в ОЗУ, поэтому они используются для тех же целей, что и ПЗУ. При этом они обладают еще одним дополнительным свой­ством — возможностью исправлять ошибки в содержащихся в них программах путем перезаписи занимаемых ими участков памяти.

Флеш-память также обычно используется как носитель информации в портативных электронных устройствах. Если упомянуть лишь два ее применения, то она служит «пленкой» в цифровых фотоаппаратах и «диском» в переносных музыкальных пле­ерах. По быстродействию флеш-память занимает промежуточное положение между ОЗУ и диском. Также, в отличие от дисковой памяти, если флеш-память стирается или перезаписывается слишком часто, она приходит в негодность.

Еще одна разновидность памяти — CMOS-память, которая является энергозависимой. Во многих компьютерах CMOS-память используется для хранения текущих даты и вре­мени. CMOS-память и схема электронных часов, отвечающая за отсчет времени, получа­ют питание от миниатюрной батарейки (или аккумулятора), поэтому значение текущего времени исправно обновляется, даже если компьютер отсоединен от внешнего источника питания. CMOS-память также может хранить параметры конфигурации, указывающие, например, с какого диска системе следует загружаться. Потребление энергии CMOS- памятью настолько низкое, что батарейки, установленной на заводе-изготовителе, часто хватает на несколько лет работы. Однако когда батарейка начинает выходить из строя, на компьютере могут проявиться признаки «болезни Альцгеймера» и он станет «забывать» то, что помнил годами, например с какого жесткого диска следует производить загрузку.

Диски

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

Жесткий диск состоит из одной или нескольких металлических пластин, вращающихся со скоростью 5400, 7200, 10 800 и более оборотов в минуту. Механический привод по­ворачивается на определенный угол над пластинами, подобно звукоснимателю старого проигрывателя виниловых пластинок на 33 оборота в минуту. Информация записыва­ется на диск в виде последовательности концентрических окружностей. В каждой за­данной позиции привода каждая из головок может считывать кольцеобразный участок, называемый дорожкой. Из совокупности всех дорожек в заданной позиции привода составляется цилиндр.

Каждая дорожка поделена на определенное количество секторов, обычно по 512 байт на сектор. На современных дисках внешние цилиндры содержат больше секторов, чем внутренние. Перемещение привода с одного цилиндра на другой занимает около 1 мс. Перемещение к произвольно выбранному цилиндру обычно занимает от 5 до 10 мс в зависимости от конкретного накопителя. Когда привод расположен над нужной

дорожкой, накопитель должен выждать, когда нужный сектор попадет под головку. Это приводит к возникновению еще одной задержки от 5 до 10 мс в зависимости от скорости вращения диска. После попадания требуемого сектора под головку произво­дится операция чтения или записи со скоростью от 50 Мбайт/с (для низкоскоростных дисков) до 160 Мбайт/с (для высокоскоростных).

Порой речь заходит о таких дисках, которые на самом деле дисками не являются, на­пример о твердотельных накопителях — SSD (Solid State Disks). У них нет движущихся частей, дисковых пластин, а данные хранятся во флеш-памяти. Они напоминают диски только тем, что содержат большой объем данных, который при отключении питания не теряется.

Многие компьютеры поддерживают схему, которая называется виртуальной памятью. Ее мы довольно основательно рассмотрим в главе 3. Она дает возможность запускать про­граммы, превышающие по объему физическую память компьютера, за счет помещения их на диск и использования оперативной памяти как некой разновидности кэша для наи­более интенсивно исполняемых частей. Эта схема требует прозрачного для программы преобразования адресов памяти, чтобы конвертировать адрес, сгенерированный програм­мой, в физический адрес, по которому слово размещено в ОЗУ. Такое отображение адре­сов осуществляется частью центрального процессора, называется блоком управления памятью (Memory Management Unit (MMU)), или диспетчером памяти (см. рис. 1.6).

Использование кэширования и MMU может оказать существенное влияние на про­изводительность. При работе в мультипрограммном режиме, когда осуществляется переключение с одной программы на другую, иногда называемое переключением контекста (context switch), может потребоваться сброс всех измененных блоков из кэш-памяти и изменение регистров отображения в MMU. Обе эти операции обходятся слишком дорого, и программисты всеми силами стараются их избежать. Некоторые последствия применяемых ими тактических приемов мы рассмотрим чуть позже.



2016-09-17 2232 Обсуждений (0)
Многопоточные и многоядерные микропроцессоры 4.67 из 5.00 6 оценок









Обсуждение в статье: Многопоточные и многоядерные микропроцессоры

Обсуждений еще не было, будьте первым... ↓↓↓

Отправить сообщение

Популярное:
Почему люди поддаются рекламе?: Только не надо искать ответы в качестве или количестве рекламы...
Как выбрать специалиста по управлению гостиницей: Понятно, что управление гостиницей невозможно без специальных знаний. Соответственно, важна квалификация...



©2015-2024 megaobuchalka.ru Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. (2232)

Почему 1285321 студент выбрали МегаОбучалку...

Система поиска информации

Мобильная версия сайта

Удобная навигация

Нет шокирующей рекламы



(0.017 сек.)