O Torvalds Linus

Сайт о *nix системах и всем что с ними связано

Макрос switch__to

Mar-17-2012 By root

Второй шаг переключения процессов выполняется макросом switch to. Это одна из наиболее аппаратно – зависимых процедур ядра, и для понимания ее действий требуются некоторые усилия.

Во-первых, макрос принимает три параметра, prev, next и last. Нетрудно догадаться о назначении параметров prev и next: они играют ту же роль, что и локальные переменные prev и next, т. е. являются входными параметрами, задающими ячейки памяти, хранящие адреса дескрипторов замещаемого процесса и замещающего процесса соответственно.

А как насчет третьего параметра, last? Дело в том, что в каждое переключение процессов вовлечены не два, а три процесса. Предположим, ядро приняло решение переключиться с процесса А на процесс В. В функции schedule о переменная prev указывает на дескриптор процесса A, a next —на дескриптор процесса В. Как только макрос switch to деактивирует процесс А, его выполнение прекращается.

Впоследствии, когда ядро решит возобновить выполнение процесса А, оно должно будет приостановить некоторый процесс С (вообще говоря, отличный от В), для чего вызовет макрос switch to с параметром prev, указывающим на С, и параметром next, указывающим на А. Когда процесс А продолжит работу, он найдет свой старый стек режима ядра, где локальная переменная prev указывает на дескриптор процесса А, а переменная next указывает на дескриптор процесса В. Планировщик, который теперь выполняется от имени процесса А, не имеет ссылки на процесс С. Однако оказывается, что эта ссылка нужна для завершения процедуры переключения процессов.

Последний параметр макроса switch to является выходным и задает ячейку памяти, в которую макрос запишет адрес дескриптора процесса С (естественно, это делается после того, как процесс А возобновит выполнение). Перед переключением процессов макрос сохраняет в процессорном регистре еах содержимое переменной, идентифицируемой первым входным параметром prev, т. е. содержимое локальной переменной prev, хранящейся в стеке режима ядра, принадлежащем процессу А. У процессов, в эту ячейку записывается адрес дескриптора процесса С. В текущей реализации функции schedule о последний параметр идентифицирует локальную переменную prev процесса А, так что prev переписывается адресом процесса С.

Содержимое стеков режима ядра процессов А, В и С, а также значения регистрах. Необходимо учитывать, что на рисунке показано значение локальной переменной prev до того, как оно будет переписано содержимым регистра еах.

Макрос switch to написан на расширенном встроенном ассемблере, который довольно трудно читать. В частности, ссылки на регистры делаются с помощью специальной позиционной нотации, которая позволяет компилятору самостоятельно решать, какими регистрами общего назначения он будет пользоваться. Вместо того чтобы приводить здесь громоздкий код на расширенном встроенном ассемблере, мы опишем работу макроса switch to в архитектуре 80×86, пользуясь стандартным ассемблером. Итак, макрос switch to выполняет следующие действия:

1. Сохраняет значения параметров prev и next в регистрах еах и edx соответственно:

movl prev, %еах movl next, %edx

2. Сохраняет содержимое регистров efiags и еЬр в стеке режима ядра процесса prev. Эти регистры нужно сохранить, потому что компилятор предполагает их неизменность вплоть до окончания макроса switchto:

pushfl pushl %ebp

3. Сохраняет содержимое регистра esp В поле prev ->thread.esp, чтобы это поле указывало на верхушку стека режима ядра процесса prev:

movl %esp,484(%еах)

Операнд 484(%еах) идентифицирует ячейку памяти, адрес которой равен содержимому регистра еах, увеличенному на 484.

4. Загружает значение next ->thread.esp в регистр esp. С этого момента ядро работает со стеком режима ядра процесса next, т. е. эта инструкция фактически выполняет переключение от процесса prev к процессу next. Поскольку адрес дескриптора процесса тесно связан с адресом стека режима ядра, смена стека ядра равносильна смене текущего процесса.

movl 484(%edx), %esp

5. Сохраняет адрес с меткой 1 (о нем далее в этом разделе) в поле prev ->thread. eip. Когда замещаемый процесс возобновит свое выполнение, он выполнит инструкцию с меткой 1:

movl $lf, 480(%еах)

6. Заносит значение поля next ->thread. eip в стек режима ядра процесса next. В большинстве случаев это адрес с меткой 1:

pushl 480(%edx)

7. Переходит на выполнение функции switch to (), написанной на языке С

(см. следующий раздел):

jmp switch_to

8. Здесь процесс А, который был замещен процессом В, снова получает в свое распоряжение процессор. Выполняется ряд инструкций, восстанавливающих содержимое регистров efiags. Первая инструкция имеет метку 1:

1:

popl %ebp popf 1

Обратите внимание, что эти инструкции pop работают со стеком ядра, принадлежащим процессу prev. Они будут выполнены, когда планировщик выберет prev в качестве нового процесса, подлежащего выполнению, и вызовет макрос switch to, передав ему prev в качестве второго параметра. Таким образом, регистр esp указывает на стек режима ядра процесса

prev.

9. Копирует содержимое регистра еах (загруженное на шаге 1) в ячейку памяти, идентифицируемую параметром last макроса switch to:

movl %еах, last

Как было сказано ранее, регистр еах указывает на дескриптор только что замещенного процесса6.

Функция switch_to()

Функция switch to о выполняет основную часть работы по переключению процессов, начатой макросом switch to (). Она принимает параметры prev p и next p, обозначающие старый и новый процессы соответственно. Вызов этой функции отличается от вызова среднестатистической”функции, поскольку функция switch to о берет значения параметров prev p и next p из регистров еах и edx (в которых, как мы видели, эти значения сохраняются), а не из стека, как большинство других функций. Чтобы заставить функцию брать параметры из регистров, ядро применяет ключевые слова attribute и regparm, являющиеся нестандартными расширениями языка С, реализованными в компиляторе gcc. Функция switch toO объявлена в заголовочном

файле include/asm – i386/system.h следующим образом:

switch_to(struct task_struct prev_p,

struct task_struct next_p)

attribute (гедрагш(З) ) ;

Она выполняет следующие действия:

1. Выполняет код, возвращенный макросом uniazy_fpu() (см. разд. “Сохранение и загрузка регистров FPU, ММХ и ХММ”далее в этой главе), чтобы сохранить содержимое регистров FPU, ММХ и ХММ процесса prev p, если необходимо.

unlazy_fpu (prev_p) ;

2. Выполняет макрос smp_processor_id(), чтобы получить индекс локального процессора, т. е. процессора, выполняющего код. Макрос извлекает искомый индекс из поля сри структуры thread info текущего процесса и сохраняет его в локальной переменной.

3. Загружает значение поля next_p ->thread.espO в поле espO сегмента состояния задачи, ассоциированного с локальным процессором. Как мы узнаем из разд. “Выдача системного вызова с помощью инструкции sysenter”, любое последующее изменение уровня привилегий от режима пользователя до режима ядра, вызванное ассемблерной инструкцией sysenter, приведет к копированию этого адреса в регистр esp:

init_tss[cpu].espO = next_p ->thread.espO;

4. Загружает в глобальную таблицу дескрипторов локального процесса сегменты TLS (Thread – Local Storage), используемые процессом next p. Три селектора сегментов хранятся в массиве tis array в дескрипторе процесса (см. главу 2).

cpu_gdt_table[сри][б] = next_p ->thread.tls_array[0]; cpu_gdt_table[сри][7] = next_p ->thread.tls_array[1]; cpu_gdt_table[cpu][8] = next_p ->thread.tls_array[2];

5. Сохраняет содержимое сегментных регистров fs и gs в полях prev_p -> thread.fs и prev_p ->thread.gs, соответственно, выполняя следующие ассемблерные инструкции:

movl %fs, 40 (%esi) movl %gs, 44(%esi)

Регистр esi указывает на структуру prev_p ->thread.

6. Если сегментный регистр fs или gs был уже использован процессом prev p или next p (то есть если они содержат ненулевое значение), функция загружает в эти регистры значения, хранящиеся в дескрипторе thread struct процесса next p. Этот шаг логически дополняет действия, выполненные в предыдущем. Главными ассемблерными инструкциями при этом являются:

movl 40(%ebx),%fs movl 44(%ebx),%gs

Регистр ebx указывает на структуру next_p ->thread. На самом деле, код намного сложнее, потому что процессор может возбудить исключение, если он обнаружит недопустимое значение в сегментном регистре. Код учитывает эту возможность, прибегая к приему “обработка исключения”. Поскольку процессы редко модифицируют битовую карту разрешений ввода/вывода, она обрабатывается в ленивом”режиме. Реальная битовая карта копируется в сегмент состояния задачи локального процессора, только если процесс действительно обращается к порту ввода/вывода в текущем интервале времени. Битовая карта разрешений ввода/вывода, принадлежащая какому-то процессу, хранится в буфере, на который указывает поле io bitmap ptr структуры thread info этого процесса. Функция handie io bitmap () устанавливает значение в поле io bitmap сегмента состояния задачи локального процессора для процесса next p следующим образом:

•если процесс next p не имеет собственной битовой карты разрешений ввода/вывода, в поле io bitmap сегмента состояния задачи записывается значение 0×8000;

•если у процесса next p есть собственная битовая карта разрешений ввода/вывода, в поле io bitmap сегмента состояния задачи записывается значение 0×9000.

Поле io bitmap сегмента состояния задачи должно содержать смещение внутри этого сегмента, определяющее место хранения реальной битовой карты. Значения 0×8000 и 0×9000 указывают за пределы сегмента TSS, что послужит причиной возбуждения исключения Общий сбой защиты”, как только процесс режима пользователя попытается обратиться к порту ввода/вывода (см. разд. “Исключения” главы 4). Обработчик исключения do_general_protection() проверит Значение В ПОЛе io bitmap. ЕСЛИ ОНО равно 0×8000, функция отправит сигнал sigsegv процессу в пользовательском режиме. Если же оно равно 0×9000, функция скопирует битовую карту процесса (на которую указывает поле io bitmap ptr структуры thread info) в сегмент состояния задачи локального процессора, запишет в поле io bitmap фактическое смещение битовой карты и форсирует новое выполнение ассемблерной инструкции, приведшей к возбуждению исключения.

9. Завершает работу. Функция switch to о (написанная на языке С) заканчивается оператором:

return prev_p;

Соответствующие ассемблерные инструкции, сгенерированные компилятором, выглядят так:

movl %edi,%eax ret

Параметр prev р (сейчас хранящийся в регистре edi) копируется в регистр еах, потому что значение, возвращаемое любой функцией на языке С, передается через регистр еах. Обратите внимание, что таким образом значение регистра еах оказывается одинаковым до и после вызова функции

switch to (). Это очень важно, потому что при вызове макроса switch to

подразумевается, что регистр еах всегда хранит адрес дескриптора замещаемого процесса.

Ассемблерная инструкция ret загружает в счетчик команд eip адрес возврата, хранящийся на верхушке стека. Однако функция switch to () была вызвана простой инструкцией перехода. Поэтому инструкция ret находит на верхушке стека адрес инструкции с меткой 1, который был положен в стек макросом switch to. Если процесс next p до этого не приостанавливался, потому что выполняется впервые, функция находит начальный адрес функции ret f rom fork ().

Add A Comment