Внутреннее устройство ядра Linux 2.4

         

Sem_revalidate()


Функция sem_revalidate() вызывается, когда глобальная блокировка семафора временно была снята и необходимо снова ее получить. Вызывается из и . Производит проверку ID семафора и права доступа, в случае успеха выполняет глобальную блокировку семафора.



SEM_STAT


Операция SEM_STAT инициализирует временный буфер . На время копирования значений sem_otime, sem_ctime, и sem_nsems в буфер выполняется глобальная блокировка семафора. И затем данные копируются в пространство пользователя.



Семафоры


Иногда возникает необходимость в запрещении доступа к разделяемым данным, например, при копировании данных в пользовательское пространство. Для этих целей Linux предоставляет стандартные средства, называемые семафорами. Семафоры бывают двух типов: базовые и read-write семафоры. В зависимости от начального значения семафоры могут обеспечить либо взаимоисключающий (начальное значение 1), либо более сложный тип доступа.

Read-write семафоры отличаются от базовых тем же самым, чем read-write блокировки отличаются от базовых блокировок: они разрешают множественный доступ "на чтение" одновременно нескольким процессам, но доступ "на запись" может получить только один процесс.

Кроме того, семафоры могут быть прерываемыми - при использовании down/up_interruptible() вместо простых down()/up(). Если возвращаемое из down_interruptible() значение не ноль - то операция была прервана

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

Простой пример использования семафоров можно найти в реализации системных вызовов gethostname(2)/sethostname(2)

(kernel/sys.c).

asmlinkage long sys_sethostname(char *name, int len) { int errno;

if (!capable(CAP_SYS_ADMIN)) return -EPERM; if (len < 0 len > __NEW_UTS_LEN) return -EINVAL; down_write(&uts_sem); errno = -EFAULT; if (!copy_from_user(system_utsname.nodename, name, len)) { system_utsname.nodename[len] = 0; errno = 0; } up_write(&uts_sem); return errno; }

asmlinkage long sys_gethostname(char *name, int len) { int i, errno;

if (len < 0) return -EINVAL; down_read(&uts_sem); i = 1 + strlen(system_utsname.nodename); if (i > len) i = len; errno = 0; if (copy_to_user(name, system_utsname.nodename, i)) errno = -EFAULT; up_read(&uts_sem); return errno; }

Комментарии к примеру:

Блокировка может выполняться на время копирования в/из пространство пользователя в copy_from_user()/copy_to_user(). Поэтому здесь не используются какого либо рода блокировки.



В системе возможно параллельное исполнение нескольких gethostname(2), для которых не требуется исключительного доступа, поэтому используется read-write семафор, а не базовый.

Хотя реализация семафоров в Linux очень сложна, тем не менее возможны сценарии, которые еще не реализованы, например: нет концепции прерываемых read-write семафоров. Очевидно потому, что не встречалась реальная ситуация, которая требовала бы наличия таких экзотических свойств от семафоров.



Semctl_down()


Функция semctl_down() предназначена для выполнения операций и системного вызова semctl(). Перед выполнением этих операций проверяется ID набора семафоров и права доступа. Обе эти операции выполняются под глобальной блокировкой семафоров.



Semctl_main()


Функция semctl_main() вызывается из для выполнения ряда операций, которые описаны ниже. Перед выполнением операций, semctl_main() блокирует семафор и проверяет ID набора семафоров и права доступа. Перед возвратом блокировка снимается.



Semctl_nolock()


Функция semctl_nolock() вызывается из для выполнения операций IPC_INFO, SEM_INFO и SEM_STAT.



SETALL


Операция SETALL копирует значения семафора из пользовательского пространства во временный буфер и затем в набор семафоров. На время копирования из пользовательского пространства во временный буфер и на время проверки значений блокировка сбрасывается. При небольшом объеме данных, временный буфер размещается на стеке, иначе в памяти размещается буфер большего размера. На время выполнения следующих действий блокировка восстанавливается:

Информация копируется в набор семафоров.

Очищается очередь откатов для заданного набора семафоров.

Устанавливается значение sem_ctime для набора семафоров.

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



SETVAL


Проверяет новое значение семафора и выполняет следующие действия:

В очереди отката отыскиваются любые корректировки данного семафора и эти корректировки сбрасываются в ноль.

Значение семафора устанавливается в заданное.

Корректируется значение sem_ctime .

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



Shm_close()


Функция shm_close() обновляет содержимое полей shm_lprid и shm_dtim и уменьшает счетчик подключений. Если счетчик обнулился, то вызывается для освобождения ресурсов, занятых сегментом разделяемой памяти. Эти действия выполняются после выполнения глобальной блокировки и получения глобального семафора разделяемой памяти.



Shm_destroy()


В результате исполнения shm_destroy() общее количество страниц, занятых разделяемой памятью, уменьшается на количество страниц, занятых удаляемым сегментом. Затем вызывается (через shm_rmid()) для удаления ID сегмента Страницы памяти в сегменте разблокируются функцией . Счетчик ссылок каждой страницы устанавливается в 0. Вызывается fput(), чтобы уменьшить счетчик f_count соответствующего файла. И в заключение вызывается kfree() для освобождения памяти под дескриптором сегмента.



Shm_get_stat()


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



Shm_inc()


shm_inc() устанавливает PID, текущее время и увеличивает счетчик подключений для заданного сегмента разделяемой памяти. Эти действия выполняются после выполнения глобальной блокировки разделяемой памяти.



SHM_INFO


На время сбора статистической информации по разделяемой памяти, производится захват глобального семафора и глобальной блокировки разделяемой памяти. Для подсчета количества страниц, резмещенных резидентно в памяти и количества страниц на устройстве свопинга, вызывается . Дополнительно подсчитываются общее количество страниц разделяемой памяти и количество используемых сегментов. Количество swap_attempts и swap_successes жестко зашито в 0. Эти статистики заносятся во временный буфер и затем копируются в пользовательское пространство вызывающего приложения.



SHM_LOCK, SHM_UNLOCK


После проверки прав доступа выполняется глобальная блокировка разделяемой памяти и проверяется ID сегмента разделяемой памяти. Для выполнения обеих операций вызывается . Параметры, передаваемые функции однозначно определяют выполняемую операцию.



SHM_STAT, IPC_STAT


Для выполнения SHM_STAT и IPC_STAT инициализируется временный буфер типа и выполняется глобальная блокировка разделяемой памяти.

Для случая SHM_STAT, параметр ID сегмента разделяемой памяти трактуется как простой индекс (т.е. как число в диапазоне от 0 до N, где N - количество зарегистрированных ID в системе). После проверки индекса вызывается (через shm_buildid()) для преобразования индекса в ID разделяемой памяти, который в данном случае и будет возвращаемым значением. Примечательно, что это обстоятельство не документировано, но используется для поддержки программы ipcs(8).

Для случая IPC_STAT, параметр ID сегмента разделяемой памяти трактуется как нормальный ID, сгенерированный вызовом . Перед продолжением работы ID проверяется на корректность. В данном случае в качестве результата будет возвращен 0.

Для обоих случаев SHM_STAT и IPC_STAT, проверяются права доступа вызывающей программы. Требуемые статистики загружаются во временный буфер и затем передаются в вызывающую программу.



Shmem_file_setup()


shmem_file_setup() создает файл в файловой системе tmpfs с требуемым именем и размером. Если в системе достаточно ресурсов для размещения файла в памяти, то создается новый dentry в корне tmpfs и размещается новый файловый дескриптор и новый inode типа tmpfs. Затем связывает dentry и inode вызовом d_instantiate() и сохраняет адрес dentry в файловом дескрипторе. Поле i_size inode устанавливается равным размеру файла, а в поле i_nlink

заносится 0. Также shmem_file_setup() записывает адрес таблицы файловых операций shmem_file_operations в поле f_op и инициализирует поля f_mode и f_vfsmnt файлового дескриптора. Для завершения инициализации inode вызывается shmem_truncate(). И в случае успешного выполнения всех операций возвращает новый файловый дескриптор.



Shmem_lock()


shmem_lock() принимает в качестве параметров указатель на дескриптор сегмента разделяемой памяти и флаг требуемой операции - блокирование или разблокирование. Состояние блокировки запоминается в соответствующем inode. Если предыдущее состояние блокировки совпадает с требуемым, то shmem_lock() просто возвращает управление не производя дополнительных действий.

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

Функция find_lock_page() блокирует страницу (устанавливает бит PG_locked) и увеличивает счетчик ссылок на страницу. Увеличение счетчика ссылок служит гарантией того, что страница останется в памяти на все время выполнения операции.

Если страницу требуется заблокировать, то бит PG_locked сбрасывается, но счетчик ччылок не уменьшается.

Если страницу требуется разблокировать, то счетчик ссылок уменьшается дважды, первый раз аннулируется увеличение счетчика, выполненное функцией find_lock_page() и второй раз для ссылки, которая заблокировала страницу в памяти. После этого бит PG_locked сбрасывается.



первичный процессор проходит обычную последовательность


В случае SMP (многопроцессорной системы), первичный процессор проходит обычную последовательность - bootsector, setup и т.д., пока не встретится вызов функции start_kernel(), в которой стоит вызов функции smp_init(), откуда вызывается arch/i386/kernel/smpboot.c:smp_boot_cpus(). Функция smp_boot_cpus() в цикле (от 0 до NR_CPUS) вызывает do_boot_cpu() для каждого apicid. Функция do_boot_cpu() создает (т.е. fork_by_hand) фоновую задачу для указанного CPU и записывает, согласно спецификации Intel MP (в 0x467/0x469) трамплин-код, содержащийся в trampoline.S. Затем генерирует STARTUP IPI, заставляя вторичный процессор выполнить код из trampoline.S.

Ведущий процессор создает трамплин-код для каждого процессора в нижней памяти. Ведомый процессор, при исполнении "трамплина", записывает "магическое число", чтобы известить ведущий процессор, что код исполнен. Требование, по размещению трамплин-кода в нижней памяти, обусловлено спецификацией Intel MP.

Трамплин-код просто записывает 1 в %bx, переводит процессор в защищенный режим и передает управление на метку startup_32, которая является точкой входа в arch/i386/kernel/head.S.

При исполнении кода head.S, ведомый CPU обнаруживает, что он не является ведущим, перепрыгивает через очистку BSS и входит в initialize_secondary() которая переходит в фоновую задачу для данного CPU - минуя вызов init_tasks[cpu], поскольку она уже была проинициирована ведущим процессором при исполнении do_boot_cpu(cpu).

Характерно, что код init_task может использоваться совместно, но каждая фоновая задача должна иметь свой собственный TSS. Именно поэтому init_tss[NR_CPUS] является массивом.


Создание и завершение задач и потоков ядра.


В литературе можно встретить самые разные определения термина "процесс", начиная от "экземпляр исполняемой программы" и заканчивая "то, что является результатом работы системного вызова clone(2) или fork(2)". В Linux, существует три типа процессов:

фоновая задача(и),

потоки ядра,

пользовательские задачи.

Фоновая задача создается во время компиляции (at compile time) для первого CPU; и затем "вручную" размножается для каждого процессора вызовом fork_by_hand() из arch/i386/kernel/smpboot.c. Фоновая задача имеет общую структуру init_task, но для каждого процессора создается свой собственный TSS, в массиве init_tss. Все фоновые задачи имеют pid = 0 и никакой другой тип задач больше не может разделять pid, т.е. не могут клонироваться с флагом CLONE_PID через clone(2).

Потоки ядра порождаются с помощью функции kernel_thread(), которая делает системный вызов clone(2) в режиме ядра. Потоки ядра обычно не имеют пользовательского адресного пространства, т.е. p->mm = NULL, поэтому они явно вызывают exit_mm(), например через функцию daemonize(). Потоки ядра всегда имеют прямой доступ к адресному пространству ядра. Получают pid из нижнего диапазона. Работают в нулевом кольце защиты и, следовательно, имеют высший приоритет во всех операциях ввода/вывода и имеют преимущество перед планировщиком задач.

Пользовательские задачи создаются через системные вызовы clone(2) или fork(2). И тот и другой обращаются к kernel/fork.c:do_fork().

Давайте рассмотрим что же происходит, когда пользовательский процесс делает системный вызов fork(2). Хотя fork(2) и является аппаратно-зависимым из-за различий в организации стека и регистров, тем не менее основную часть действий выполняет функция do_fork(), которая является переносимой и размещена в kernel/fork.c.

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

Локальной переменной retval присваивается значение -ENOMEM, которое возвращается в случае невозможности распределить память под новую структуру задачи


Если установлен флаг CLONE_PID в параметре clone_flags, тогда возвращается код ошибки (-EPERM). Наличие этого флага допускается только если do_fork() была вызвана из фонового потока (idle thread), т.е. из задачи с pid == 0 (только в процессе загрузки). Таким образом, пользовательские потоки не должны передавать флаг CLONE_PID в clone(2), ибо этот номер все равно не "проскочит".

Инициализируется current->vfork_sem (позднее будет очищен потомком). Он используется функцией sys_vfork() (системный вызов vfork(2), передает clone_flags = CLONE_VFORK|CLONE_VM|SIGCHLD) для того, чтобы "усыпить" родителя пока потомок не выполнит mm_release(), например , в результате исполнения exec() или exit(2).

В памяти размещается новая структура с помощью макроса alloc_task_struct(). На x86 это производится с приоритетом GFP_KERNEL. Это главная причина, по которой системный вызов fork(2) может "заснуть". Если разместить структуру не удалось, то возвращается код ошибки -ENOMEM.

Все поля структуры текущего процесса копируются во вновь созданную структуру посредством присваивания *p = *current. Может быть следует заменить на memset? Позднее, в поля, которые не наследуются потомком, будут записаны корректные значения.

Для сохранения реентерабельности кода, выполняется big kernel lock.

Если "родитель" является пользовательским ресурсом, то проверяется - не превышен ли предел RLIMIT_NPROC, если превышен - тогда возвращается код ошибки -EAGAIN, если нет - увеличивается счетчик процессов для заданного uid p->user->count.

Если превышено системное ограничение на общее число задач - max_threads, возвращается код ошибки -EAGAIN.

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

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

Потомок помечается как 'has not execed' (p->did_exec = 0)



Потомок помечается как 'not-swappable' (p->swappable = 0)

Потомок переводится в состояние TASK_UNINTERRUPTIBLE, т.е. p->state = TASK_UNINTERRUPTIBLE (TODO: зачем это делается? Я думаю, что в этом нет необходимости - следует избавиться от этого, Linus подтвердил мое мнение)

Устанавливаются флаги потомка p->flags в соответствии с clone_flags; в случае простого fork(2), это будет p->flags = PF_FORKNOEXEC.

Вызовом функции, kernel/fork.c:get_pid(), реализующей быстрый алгоритм поиска, находится pid потомка (p->pid) (TODO: блокировка (spinlock) lastpid_lock может быть опущена, так как get_pid() всегда выполняется под блокировкой ядра (big kernel lock) из do_fork(), так же можно удалить входной параметр flags для get_pid(), патч (patch) отправлен Алану (Alan) 20/06/2000).

Далее инициализируется остальная часть структуры task_struct потомка. В самом конце структура хешируется в таблицу pidhash и потомок активируется (TODO: вызов wake_up_process(p) устанавливает p->state = TASK_RUNNING и добавляет процесс в очередь runqueue, поэтому, вероятно, нет нужды устанавливать p->state в состояние TASK_RUNNING

ранее в do_fork()). Обратите внимание на установку p->exit_signal в значение clone_flags & CSIGNAL, которое для fork(2) может быть только SIGCHLD, и на установку p->pdeath_signal в 0. Сигнал pdeath_signal используется когда процесс лишается "родителя" (в случае его "смерти") и может быть получен/установлен посредством команд PR_GET/SET_PDEATHSIG системного вызова prctl(2)

Задача создана. Для завершения задачи имеется несколько способов.

выполнить системный вызов exit(2);

передать сигнал, приказывающий "умереть";

вынужденная "смерть" в результате возникновения некоторых исключений;

вызвать bdflush(2) с func == 1

(эта особенность Linux оставлена для сохранения совместимости со старыми дистрибутивами, которые имели строку 'update' в /etc/inittab - на сегодняшний день эта работа выполняется процессом ядра kupdate).



Имена функций, реализующих системные вызовы, в Linux начинаются с префикса sys_, но они, как правило, ограничиваются только проверкой аргументов или платформо-зависимой передачей информации, а фактически всю работу выполняют функции do_. Это касается и sys_exit(), которая вызываетdo_exit() для выполнения необходимых действий. Хотя, в других частях ядра иногда встречается вызов sys_exit (), на самом деле вызывается do_exit ().

Функция do_exit() размещена в kernel/exit.c. Некоторые примечания по поводу функции do_exit():

Устанавливает глобальную блокировку ядра (устанавливает, но не снимает).

Вызывает schedule(), которая уже не возвращает управление.

Переводит задачу в состояние TASK_ZOMBIE.

Передает всем потомкам current->pdeath_signal, если он не ноль.

Передает "родителю" current->exit_signal, который обычно равен SIGCHLD.

Освобождает ресурсы, захваченные при ветвлении, закрывает открытые файлы и т.д.




Ss_add()


Функция ss_add() принимает в качестве входных параметров указатель на дескриптор очереди сообщений и указатель на структуру msg_sender. Она заносит в поле tsk указатель на текущий процесс, изменяет статус процесса на TASK_INTERRUPTIBLE после чего вставляет структуру msg_sender в начало очереди ожидания процессов-отправителей заданной очереди сообщений.



Ss_wakeup()


Функция ss_wakeup() активизирует все процессы-отправители, стоящие в заданной очереди ожидания. Если функция вызывается из , то процессы исключаются из очереди ожидания.



Store_msg()


Функция store_msg() вызывается при передаче сообщения в пользовательское пространство. Данные, описываемые структурами и последовательно копируются в пользовательский буфер.



Struct ipc_id


Массив структур ipc_id имеется в каждом экземпляре . Массив размещается динамически и размер его может быть изменен функцией . Иногда этот массив упоминается как массив дескрипторов, так как тип данных используется универсальным функциями IPC для доступа к дескрипторам.

struct ipc_id { struct kern_ipc_perm* p; };

Вперед



Struct ipc_ids


Структура ipc_ids описывает данные, одинаковые для семафоров, очередей сообщений и разделяемой памяти. Существует три глобальных экземпляра этого типа -- semid_ds, msgid_ds и shmid_ds -- для семафоров, очередей сообщений и разделяемой памяти соответственно. Каждый экземпляр содержит семафор sem, предназначенный для разграничения доступа к нему. Поле entries указывает на массив дескрипторов и поле ary - блокировку доступа к этому массиву. Поле seq хранит порядковый номер, который увеличивается всякий раз при создании нового ресурса IPC.

struct ipc_ids { int size; int in_use; int max_id; unsigned short seq; unsigned short seq_max; struct semaphore sem; spinlock_t ary; struct ipc_id* entries; };



Struct kern_ipc_perm


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

/* используется в структурах данных ядра */ struct kern_ipc_perm { key_t key; uid_t uid; gid_t gid; uid_t cuid; gid_t cgid; mode_t mode; unsigned long seq; };



Struct msg_msg


/* по одной структуре на каждое сообщение */ struct msg_msg { struct list_head m_list; long m_type; int m_ts; /* размер сообщения */ struct msg_msgseg* next; /* Далее следует само сообщение */ };



Struct msg_msgseg


/* сегмент сообщения на каждое сообщение */ struct msg_msgseg { struct msg_msgseg* next; /* Далее следует остальная часть сообщения */ };



Struct msg_queue


/* по одной структуре msq_queue на каждую очередь сообщений в системе */ struct msg_queue { struct kern_ipc_perm q_perm; time_t q_stime; /* время последнего вызова msgsnd */ time_t q_rtime; /* время последнего вызова msgrcv */ time_t q_ctime; /* время последнего изменения */ unsigned long q_cbytes; /* текущий размер очереди в байтах */ unsigned long q_qnum; /* количество сообщений в очереди */ unsigned long q_qbytes; /* максимальный размер очереди в байтах */ pid_t q_lspid; /* pid последнего процесса вызвавшего msgsnd */ pid_t q_lrpid; /* pid последнего процесса-получателя */

struct list_head q_messages; struct list_head q_receivers; struct list_head q_senders; };



Struct msg_receiver


/* по одной структуре msg_receiver на каждый ожидающий процесс-получатель */ struct msg_receiver { struct list_head r_list; struct task_struct* r_tsk;

int r_mode; long r_msgtype; long r_maxsize;

struct msg_msg* volatile r_msg; };



Struct msg_sender


/* по одной структуре msg_sender на каждый ожидающий процесс-отправитель */ struct msg_sender { struct list_head list; struct task_struct* tsk; };



Struct msqid_ds


struct msqid_ds { struct ipc_perm msg_perm; struct msg *msg_first; /* первое сообщение в очереди, не используется */ struct msg *msg_last; /* последнее сообщение в очереди, не используется*/ __kernel_time_t msg_stime; /* время последнего вызова msgsnd */ __kernel_time_t msg_rtime; /* время последнего вызова msgrcv */ __kernel_time_t msg_ctime; /* время последнего изменения */ unsigned long msg_lcbytes; /* Используется для временного хранения 32 бит */ unsigned long msg_lqbytes; /* то же */ unsigned short msg_cbytes; /* текущий размер очереди в байтах */ unsigned short msg_qnum; /* количество сообщений в очереди */ unsigned short msg_qbytes; /* максимальный размер очереди в байтах */ __kernel_ipc_pid_t msg_lspid; /* pid процесса последним вызвавшего msgsnd */ __kernel_ipc_pid_t msg_lrpid; /* pid последнего процесса-получателя */ };



Struct msqid64_ds


struct msqid64_ds { struct ipc64_perm msg_perm; __kernel_time_t msg_stime; /* время последнего вызова msgsnd */ unsigned long __unused1; __kernel_time_t msg_rtime; /* время последнего вызова msgrcv */ unsigned long __unused2; __kernel_time_t msg_ctime; /* время последнего изменения */ unsigned long __unused3; unsigned long msg_cbytes; /* текущий размер очереди в байтах */ unsigned long msg_qnum; /* количество сообщений в очереди */ unsigned long msg_qbytes; /* максимальный размер очереди в байтах */ __kernel_pid_t msg_lspid; /* pid процесса последним вызвавшего msgsnd */ __kernel_pid_t msg_lrpid; /* pid последнего процесса-получателя */ unsigned long __unused4; unsigned long __unused5; };



Struct sem


/* По одной структуре для каждого семафора в системе. */ struct sem { int semval; /* текущее значение */ int sempid; /* pid последней операции */ };



Struct sem_array


/* По одной структуре данных для каждого набора семафоров в системе. */ struct sem_array { struct kern_ipc_perm sem_perm; /* права доступа .. см. ipc.h */ time_t sem_otime; /* время последнего обращения */ time_t sem_ctime; /* время последнего изменения */ struct sem *sem_base; /* указатель на первый семафор в массиве */ struct sem_queue *sem_pending; /* операции, ожидающие исполнения */ struct sem_queue **sem_pending_last; /* последняя ожидающая операция */ struct sem_undo *undo; /* список отката для данного массива * / unsigned long sem_nsems; /* кол-во семафоров в массиве */ };



Struct sem_queue


/* По одной очереди на каждый ожидающий процесс в системе. */ struct sem_queue { struct sem_queue * next; /* следующий элемент очереди */ struct sem_queue ** prev; /* предыдующий элемент очереди, *(q->prev) == q */ struct task_struct* sleeper; /* этот процесс */ struct sem_undo * undo; /* структура откатов */ int pid; /* pid процесса */ int status; /* результат выполнения операции */ struct sem_array * sma; /* массив семафоров для выполнения операций */ int id; /* внутренний sem id */ struct sembuf * sops; /* массив ожидающих операций */ int nsops; /* кол-во операций */ int alter; /* признак изменения семафора */ };



Struct sem_undo


/* Каждая задача имеет список откатов. Откаты выполняются автоматически * по завершении процесса. */ struct sem_undo { struct sem_undo * proc_next; /* следующий элемент списка для данного процесса */ struct sem_undo * id_next; /* следующий элемент в данном наборе семафоров */ int semid; /* ID набора семафоров */ short * semadj; /* массив изменений, по одному на семафор */ };



Struct sembuf


/* системный вызов semop берет массив отсюда. */ struct sembuf { unsigned short sem_num; /* индекс семафора в массиве */ short sem_op; /* операция */ short sem_flg; /* флаги */ };



Struct semid64_ds


struct semid64_ds { struct ipc64_perm sem_perm; /* права доступа .. см. ipc.h */ __kernel_time_t sem_otime; /* время последнего обращения */ unsigned long __unused1; __kernel_time_t sem_ctime; /* время последнего изменения */ unsigned long __unused2; unsigned long sem_nsems; /* кол-во семафоров в массиве */ unsigned long __unused3; unsigned long __unused4; };



Struct seminfo


struct seminfo { int semmap; int semmni; int semmns; int semmnu; int semmsl; int semopm; int semume; int semusz; int semvmx; int semaem; };



Struct shm_info


struct shm_info { int used_ids; unsigned long shm_tot; /* общее количество сегментов */ unsigned long shm_rss; /* общее количество резидентных сегментов */ unsigned long shm_swp; /* общее количество сегментов на свопинге */ unsigned long swap_attempts; unsigned long swap_successes; };



Struct shmem_inode_info


struct shmem_inode_info { spinlock_t lock; unsigned long max_index; swp_entry_t i_direct[SHMEM_NR_DIRECT]; /* для первых блоков */ swp_entry_t **i_indirect; /* doubly indirect blocks */ unsigned long swapped; int locked; /* into memory */ struct list_head list; };



Struct shmid_kernel


struct shmid_kernel /* private to the kernel */ { struct kern_ipc_perm shm_perm; struct file * shm_file; int id; unsigned long shm_nattch; unsigned long shm_segsz; time_t shm_atim; time_t shm_dtim; time_t shm_ctim; pid_t shm_cprid; pid_t shm_lprid; };



Struct shmid64_ds


struct shmid64_ds { struct ipc64_perm shm_perm; /* права доступа */ size_t shm_segsz; /* размер сегмента в байтах */ __kernel_time_t shm_atime; /* время последнего присоединения */ unsigned long __unused1; __kernel_time_t shm_dtime; /* время последнего отсоединения */ unsigned long __unused2; __kernel_time_t shm_ctime; /* время последнего изменения */ unsigned long __unused3; __kernel_pid_t shm_cpid; /* pid процесса-создателя */ __kernel_pid_t shm_lpid; /* pid последней операции */ unsigned long shm_nattch; /* количество присоединений */ unsigned long __unused4; unsigned long __unused5; };



unsigned long shmmax; unsigned long


struct shminfo64 { unsigned long shmmax; unsigned long shmmin; unsigned long shmmni; unsigned long shmseg; unsigned long shmall; unsigned long __unused1; unsigned long __unused2; unsigned long __unused3; unsigned long __unused4; };


Структура задачи и таблица процессов


Каждый процесс динамически размещает структуру struct task_struct. Максимальное количество процессов, которое может быть создано в Linux, ограничивается только объемом физической памяти и равно (см. kernel/fork.c:fork_init()):

/* * В качестве максимально возможного числа потоков принимается безопасное * значение: структуры потоков не могут занимать более половины * имеющихся страниц памяти. */ max_threads = mempages / (THREAD_SIZE/PAGE_SIZE) / 2;

что для архитектуры IA32 означает, как правило, num_physpages/4. Например, на машине с 512M памяти, возможно создать 32k потоков. Это значительное усовершенствование по сравнению с 4k-epsilon пределом для ядер 2.2 и более ранних версий. Кроме того, этот предел может быть изменен в процессе исполнения, передачей значения KERN_MAX_THREADS в вызове sysctl(2), или через интерфейс procfs:

# cat /proc/sys/kernel/threads-max 32764 # echo 100000 > /proc/sys/kernel/threads-max # cat /proc/sys/kernel/threads-max 100000 # gdb -q vmlinux /proc/kcore Core was generated by `BOOT_IMAGE=240ac18 ro root=306 video=matrox:vesa:0x118'. #0 0x0 in ?? () (gdb) p max_threads $1 = 100000

Множество процессов в Linux-системе представляет собой совокупность структур struct task_struct, которые взаимосвязаны двумя способами.

как хеш-массив, хешированный по pid, и

как кольцевой двусвязный список, в котором элементы ссылаются друг на друга посредством указателей p->next_task

и p->prev_task.

Хеш-массив определен в include/linux/sched.h как pidhash[]:

/* PID hashing. (shouldnt this be dynamic?) */ #define PIDHASH_SZ (4096 >> 2) extern struct task_struct *pidhash[PIDHASH_SZ];

#define pid_hashfn(x) ((((x) >> 8) ^ (x)) & (PIDHASH_SZ - 1))

Задачи хешируются по значению pid, вышеприведенной хеш-функцией, которая равномерно распределяет элементы по диапазону от 0 до PID_MAX-1. Хеш-массив используется для быстрого поиска задачи по заданному pid с помощью inline-функции find_task_by_pid(), определенной в include/linux/sched.h:


static inline struct task_struct *find_task_by_pid(int pid) { struct task_struct *p, **htable = &pidhash[pid_hashfn(pid)];

for(p = *htable; p && p->pid != pid; p = p->pidhash_next) ;

return p; }

Задачи в каждом хеш-списке (т.е. хешированные с тем же самым значением) связаны указателями p->pidhash_next/pidhash_pprev, которые используются функциями hash_pid() и unhash_pid() для добавления/удаления заданного процесса в/из хеш-массив. Делается это под блокировкой (spinlock) tasklist_lock, полученной на запись.

Двусвязный список задач организован таким образом, чтобы упростить навигацию по нему, используя указатели p->next_task/prev_task. Для прохождения всего списка задач, в системе предусмотрен макрос for_each_task() из include/linux/sched.h:

#define for_each_task(p) \ for (p = &init_task ; (p = p->next_task) != &init_task ; )

Перед использованием for_each_task() необходимо получить блокировку tasklist_lock на ЧТЕНИЕ. Примечательно, что for_each_task() использует init_task в качестве маркера начала (и конца) списка - благодаря тому, что задача с pid=0 всегда присутствует в системе.

Функции, изменяющие хеш-массив и/или таблицу связей процессов, особенно fork(), exit() и ptrace(), должны получить блокировку (spinlock) tasklist_lock на ЗАПИСЬ. Что особенно интересно - перед записью необходимо запрещать прерывания на локальном процессоре, по той причине, что функция send_sigio(), при прохождении по списку задач, захватывает tasklist_lock на ЧТЕНИЕ, и вызывается она из kill_fasync() в контексте прерывания. Однако, если требуется доступ ТОЛЬКО ДЛЯ ЧТЕНИЯ, запрещать прерывания нет необходимости.

Теперь, когда достаточно ясно представляется как связаны между собой структуры task_struct, можно перейти к рассмотрению полей task_struct.

В других версиях UNIX информация о состоянии задачи разделяется на две части, в одну часть выделяется информация о состоянии задачи (называется 'proc structure', которая включает в себя состояние процесса, информацию планировщика и пр.) и постоянно размещается в памяти, другая часть, необходима только во время работы процесса ('u area', которая включает в себя таблицу дескрипторов, дисковые квоты и пр.) Единственная причина такого подхода - дефицит памяти. Современные операционные системы (не только Linux, но и другие, современная FreeBSD например) не нуждаются в таком разделении и поэтому вся информация о состоянии процесса постоянно хранится в памяти.



Структура task_struct объявлена в include/linux/sched.h и на сегодняшний день занимает 1680 байт.

Поле state объявлено как:

volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */

#define TASK_RUNNING 0 #define TASK_INTERRUPTIBLE 1 #define TASK_UNINTERRUPTIBLE 2 #define TASK_ZOMBIE 4 #define TASK_STOPPED 8 #define TASK_EXCLUSIVE 32

Почему константа TASK_EXCLUSIVE имеет значение 32 а не 16? Потому что раньше значение 16 имела константа TASK_SWAPPING и я просто забыл сместить значение TASK_EXCLUSIVE, когда удалял все ссылки на TASK_SWAPPING (когда-то в ядре 2.3.x).

Спецификатор volatile в объявлении p->state означает, что это поле может изменяться асинхронно (в обработчиках прерываний):

TASK_RUNNING: указывает на то, что задача "вероятно" находится в очереди запущенных задач (runqueue). Причина, по которой задача может быть помечена как TASK_RUNNING, но не помещена в runqueue в том, что пометить задачу и вставить в очередь - не одно и то же. Если заполучить блокировку runqueue_lock на чтение-запись и просмотреть runqueue, то можно увидеть, что все задачи в очереди имеют состояние TASK_RUNNING. Таким образом, утверждение "Все задачи в runqueue имеют состояние TASK_RUNNING" не означает истинность обратного утверждения. Аналогично, драйверы могут отмечать себя (или контекст процесса, под которым они запущены) как TASK_INTERRUPTIBLE (или TASK_UNINTERRUPTIBLE) и затем производить вызов schedule(), который удалит их из runqueue (исключая случай ожидания сигнала, тогда процесс остается в runqueue).

TASK_INTERRUPTIBLE: задача в состоянии "сна", но может быть "разбужена" по сигналу или по истечении таймера.

TASK_UNINTERRUPTIBLE: подобно TASK_INTERRUPTIBLE, только задача не может быть "разбужена".

TASK_ZOMBIE: задача, завершившая работу, до того как родительский процесс ("естественный" или "приемный") произвел системный вызов wait(2).

TASK_STOPPED: задача остановлена, либо по управляющему сигналу, либо в результате вызова ptrace(2).



TASK_EXCLUSIVE: не имеет самостоятельного значения и используется только совместно с TASK_INTERRUPTIBLE или с TASK_UNINTERRUPTIBLE (по OR). При наличии этого флага, будет "разбужена" лишь эта задача, избегая тем самым порождения проблемы "гремящего стада" при "пробуждении" всех "спящих" задач.

Флаги задачи представляют не взаимоисключающую информацию о состоянии процесса:

unsigned long flags; /* флаги процесса, определены ниже */ /* * Флаги процесса */ #define PF_ALIGNWARN 0x00000001 /* Print alignment warning msgs */ /* Not implemented yet, only for 486*/ #define PF_STARTING 0x00000002 /* создание */ #define PF_EXITING 0x00000004 /* завершение */ #define PF_FORKNOEXEC 0x00000040 /* создан, но не запущен */ #define PF_SUPERPRIV 0x00000100 /* использует привилегии супер-пользователя */ #define PF_DUMPCORE 0x00000200 /* выполнен дамп памяти */ #define PF_SIGNALED 0x00000400 /* "убит" по сигналу */ #define PF_MEMALLOC 0x00000800 /* Распределение памяти */ #define PF_VFORK 0x00001000 /* "Разбудить" родителя в mm_release */ #define PF_USEDFPU 0x00100000 /* задача использует FPU this quantum (SMP) */

Поля p->has_cpu, p->processor, p->counter, p->priority, p->policy и p->rt_priority связаны с планировщиком и будут рассмотрены позднее.

Поля p->mm и p->active_mm

указывают, соответственно, на адресное пространство процесса, описываемое структурой mm_struct и активное адресное пространство, если процесс не имеет своего (например потоки ядра). Это позволяет минимизировать операции с TLB при переключении адресных пространств задач во время их планирования. Так, если запланирован поток ядра (для которого поле p->mm не установлено), то next->active_mm будет установлено в значение prev->active_mm предшествующей задачи, которое будет иметь то же значение, что и prev->mm

если prev->mm != NULL. Адресное пространство может разделяться потоками, если в системный вызов clone(2) был передан флаг CLONE_VM, либо был сделан системный вызов vfork(2).

Поле p->fs ссылается на информацию о файловой системе, которая в Linux делится на три части:

корень дерева каталогов и точка монтирования,

альтернативный корень дерева каталогов и точка монтирования,

текущий корень дерева каталогов и точка монтирования.

Эта структура включает в себя так же счетчик ссылок, поскольку возможно разделение файловой системы между клонами, при передаче флага CLONE_FS в вызов clone(2).

Поле p->files ссылается на таблицу файловых дескрипторов, которая так же может разделяться между задачами при передаче флага CLONE_FILES в вызов clone(2).

Поле p->sig содержит ссылку на обработчики сигналов и может разделяться между клонами, которые были созданы с флагом CLONE_SIGHAND.


Структуры даных поддержки механизма семафоров


Следующие структуры данных используются исключительно для поддержки семафоров:



Структуры очередей сообщений


Структуры данных механизма очередей сообщений определены в msg.c.



Sys_msgctl()


Функции sys_msgctl() передаются следующие параметры: ID очереди сообщений (msqid), код операции (cmd) и указатель на буфер в пользовательском пространстве типа (buf). Функция принимает шесть кодов операций: IPC_INFO, MSG_INFO,IPC_STAT, MSG_STAT, IPC_SET и IPC_RMID. После проверки ID очереди и кода операции выполняются следующие действия:



Sys_msgget()


На входе в sys_msgget() захватывается глобальный семафор очереди сообщений ( ).

Для создания новой очереди сообщений вызывается функция , которая создает и инициализирует новую очередь и возвращает ID новой очереди.

Если значение параметра key представляет существующую очередь, то вызывается для поиска соответствующего индекса в глобальном массиве дескрипторов очередей сообщений (msg_ids.entries). перед возвратом ID очереди производится проверка параметров и прав доступа. И поиск и проверки выполняются под блокировкой (msg_ids.ary).



Sys_msgrcv()


На вход функции sys_msgrcv() передаются ID очереди (msqid), указатель на буфер типа (msgp), предполагаемый размер сообщения (msgsz), тип сообщения (msgtyp) и флаги (msgflg). Функция, по очереди ожидающих сообщений, ищет сообщение с заданным типом и первое же найденное сообщение копирует в пользовательский буфер. Если сообщения с заданным типом не обнаружено, то процесс-получатель заносится в очередь ожидания для процессов-получателей и остается там до тех пор, пока не будет получено ожидаемое сообщение. Более подробное описание действий функции sys_msgrcv() приводится ниже:

В первую очередь вызывается функция , которая устанавливает режим поиска, исходя из значения msgtyp. Далее выполняется глобальная блокировка очереди сообщений и находится дескриптор очереди по заданному ID. Если искомая очередь сообщений не найдена, то возвращается код ошибки EINVAL.

Проверяются права доступа текущего процесса.

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

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

Если размер сообщения больше чем ожидаемый и установлен флаг MSG_NOERROR, то глобальная блокировка снимается и в вызывающий процесс передается код E2BIG.

Сообщение удаляется из очереди ожидающих сообщений и обновляются статистики очереди сообщений.

Активируются процессы, находящиеся в очереди ожидания для процессов-отправителей. Удаление сообщения из очереди на предыдущем шаге делает возможным продолжить работу одному из процессов-отправителей. Переход к выполнению операций (к п. 10)

Если сообщение не было найдено, то проверяется msgflg. Если установлен флаг IPC_NOWAIT, то глобальная блокировка снимается и вызывающему процессу возвращается код ENOMSG. В противном случае процесс помещается в очередь ожидания для процессов-получателей:




В памяти размещается новая структура msr и добавляется в начало очереди ожидания.

В поле r_tsk в msr заносится указатель на текущий процесс.

В поле r_msgtype и r_mode

заносятся ожидаемый тип сообщения и режим поиска соответственно.

Если установлен флаг MSG_NOERROR, то в поле r_maxsize заносится значение из msgsz, в противном случае - значение INT_MAX.

В поле r_msg заносится признак того, что сообщение не найдено.

После завершения инициализации, процесс приобретает статус TASK_INTERRUPTIBLE, глобальная блокировка очереди сообщений снимается и вызывается планировщик schedule().

После активизации ожидающего процесса сразу же проверяется поле r_msg. Это поле содержит либо сообщение переданное напрямую, либо код ошибки. Если поле содержит сообщение то далее переходим к операциям (к п. 10). В противном случае - опять выполняется глобальная блокировка.

После того как блокировка установлена, поле r_msg проверяется еще раз. Если в процессе установки блокировки было получено сообщение, то производится переход к операциям (к п. 10).

Если поле r_msg осталось без изменений, то, следовательно, процесс был активирован для выполнения повторной попытки получить сообщение. Проверяется наличие необработанных сигналов для данного процесса и если таковые имеются, то глобальная блокировка снимается и процессу возвращается код EINTR. Иначе - производится попытка получить сообщение.

Если поле r_msg содержит код ошибки, то снимается глобальная блокировка и процессу передается ошибка.

После проверки адреса пользовательского буфера msp, тип сообщения записывается в mtype и содержимое сообщения копируется в поле mtext функцией . И в заключение освобождается память вызовом функции .


Sys_msgsnd()


Функция sys_msgsnd() принимает через входные параметры ID очереди сообщений (msqid), указатель на буфер типа (msgp), размер передаваемого сообщения (msgsz) и флаг - признак разрешения перехода процесса в режим ожидания (msgflg). Каждая очередь сообщений имеет две очереди ожидания для процессов и одну очередь ожидающих сообщений. Если в очереди ожидания имеется процесс, ожидающий данное сообщение (msgp), то это сообщение передается непосредственно ожидающему процессу, после чего процесс-получатель "пробуждается". В противном случае производится проверка - достаточно ли места в очереди ожидающих сообщений и если достаточно, то сообщение сохраняется в этой очереди. Если же места недостаточно, то процесс-отправитель ставится в очередь ожидания. Более подробное освещение этих действий приводится ниже:

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

Выполняется глобальная блокировка очереди сообщений и по ID очереди отыскивается ее дескриптор. Если такой очереди не было найдено, то в вызывающую программу возвращается код ошибки EINVAL.

В функции (вызывается из msg_checkid()) проверяется ID очереди сообщений и права доступа процесса вызовом .

Производится проверка - достаточно ли места в очереди ожидающих сообщений для помещения туда данного сообщения. Если места недостаточно, то выполняются следующие действия:

Если установлен флаг IPC_NOWAIT (в msgflg) то глобальная блокировка очереди сообщений снимается, память, занимаемая сообщением, освобождается и возвращается код ошибки EAGAIN.

Вызовом текущий процесс помещается в очередь ожидания для процессов-отправителей. Так же снимается блокировкаи вызывается планировщик schedule() который переведет процесс в состояние "сна".

После "пробуждения" снова выполняется глобальная блокировка очереди сообщений и проверяется ID очереди сообщений. Если во время "сна" очередь была удалена, то процессу возвращается признак ERMID.


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

Для передачи сообщения напрямую процессу-получателю вызывается .

Если процессов-получателей, ожидающих данное сообщение, не было обнаружено, то сообщение msg помещается в очередь ожидающих сообщений (msq->q_messages). Обновляются поля q_cbytes и q_qnum в дескрипторе очереди сообщений, а так же глобальные переменные msg_bytes и msg_hdrs, содержащие в себе общий объем сообщений в байтах и общее количество сообщений.

Если сообщение было благополучно передано процессу-получателю либо поставлено в очередь, то обновляются поля q_lspid и q_stime в дескрипторе очереди и освобождается глобальная блокировка.


Sys_semctl()


Для выполнения команд , , и вызывает функцию .

Для выполнения команд , , , , , , , и вызывает функцию .

Для выполнения команд и вызывает функцию . При этом выполняется захват глобального семафора ядра .



Sys_semget()


Вызов sys_semget() защищен глобальным семафором ядра .

Для создания и инициализации нового набора семафоров вызывается функция . В вызывающую программу возвращается ID нового набора семафоров.

Если через аогумент key передается существующий набор семафоров, вызывается функция поиска заданного набора. Перед возвратом ID проверяются права доступа вызывающей программы.



Sys_semop()


После проверки входных параметров данные копируются из пространства пользователя во временный буфер. Если объем информации невелик, то буфер размещается на стеке, в противном случае в памяти размещается буфер большего размера. После копирования данных выполняется глобальная блокировка семафоров, проверяется ID, указанного пользователем набора семафоров и права доступа.

Производится разбор всех, указанных пользователем, операций. В ходе разбора обслуживается счетчик для всех операций, для которых установлен флаг SEM_UNDO. Устанавливается флаг decrease, если какая либо операция выполняет уменьшение значения семафора, и устанавливается флаг alter, если значение любого из семафоров изменяется (т.е. увеличивается или уменьшается). Так же проверяется и номер каждого модифицируемого семафора.

Если установлен флаг SEM_UNDO для какой либо из операций, то в списке отыскивается структура отката текущей задачи (процесса), ассоциированной с данным набором семафоров. Если в ходе поиска в списке обнаруживается структура отката для несуществующего набора семафоров (un->semid == -1), то занимаемая память освобождается и структура исключается из списка вызовом функции . Если структура для заданного набора семафоров не найдена, то вызовом создается и инициализируется новая.

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



Sys_shmat()


Функция sys_shmat() принимает в качестве параметров ID сегмента разделяемой памяти, адрес по которому должен быть присоединен сегмент (shmaddr) и флаги, котроые описаны ниже.

Если параметр shmaddr не нулевой и установлен флаг SHM_RND, то shmaddr округляется "вниз" до ближайшего кратного SHMLBA. Если shmaddr не кратен SHMLBA и флаг SHM_RND не установлен, то возвращается код ошибки EINVAL.

Производится проверка прав доступа вызывающего процесса, после чего поле shm_nattch сегмента разделяемой памяти увеличивается на 1. Увеличение этого поля гарантирует сегмент разделяемой памяти от ликвидации, пока он присоединен к сегменту памяти процесса. Эти операции выполняются после установки глобальной блокировки разделяемой памяти.

Вызывается функция do_mmap(), которая отображает страницы сегмента разделяемой памяти на виртуальное адресное пространство. Делается это под семафором mmap_sem текущего процесса. В функцию do_mmap() передается флаг MAP_SHARED, а если вызывающий процесс передал ненулевое значение shmaddr, то передается и флаг MAP_FIXED. В противном случае do_mmap() самостоятельно выберет виртуальный адрес для сегмента разделяемой памяти.

ВАЖНО Из do_mmap() будет вызвана функция через структуру shm_file_operations. Эта функция вызывается для установки PID, текущего времени и увеличения счетчика присоединений данного сегмента разделяемой памяти.

После вызова do_mmap() приобретается глобальный семафор и глобальная блокировка разделяемой памяти. Счетчик присоединений затем уменьшается на 1, уменьшение производится потому, что в вызове счетчик был увеличен на 1. Если после уменьшения счетчик стал равен нулю и если сегмент имеет метку SHM_DEST, то вызывается для ликвидации сегмента разделяемой памяти.

В заключение в вызывающую программу возвращается виртуальный адрес разделяемой памяти. Если функция do_mmap() вернула код ошибки, то этот код будет передан как возвращаемое значение системного вызова.



Sys_shmdt()


На время исполнения функции sys_shmdt() приобретается глобальный семафор разделяемой памяти. В структуре mm_struct

текущего процесса отыскивается vm_area_struct, ассоциированная с заданным адресом разделяемой памяти. Если таковая найдена, то вызывается do_munmap(), чтобы отменить отображение сегмента разделяемой памяти в виртуальные адреса.

Важно так же то, что do_munmap() вызывает , которая освобождает ресурсы, занятые сегментом разделяемой памяти, если не было выполнено других присоединений.

sys_shmdt() всегда возвращает 0.



Sys_shmget()


Вызов sys_shmget() регулируется глобальным семаформ разделяемой памяти.

В случае необходимости создания нового сегмента разделяемой памяти, вызывается функция , которая создает и инициализирует новый сегмент. ID нового сегмента передается в вызывающую программу.

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



Таймеры


Теперь обратим наше внимание на таймеры ядра. Таймеры используются для передачи управления различным функциям (называющимся 'timer handler') в назначенное время. Основная структура данных - это struct timer_list

объявленная в include/linux/timer.h:

struct timer_list { struct list_head list; unsigned long expires; unsigned long data; void (*function)(unsigned long); volatile int running; };

Поле list служит для связи с внутренним списком, защищенным блокировкой (spinlock) timerlist_lock. Поле expires содержит значение времени (jiffies), оставшееся до вызова указанной function с входным параметром data. Поле running используется на SMP-системах для предотвращения запуска одного и того же обработчика на нескольких процессорах.

Функции add_timer() и del_timer()

добавляют и удаляют таймер в/из списка. По достижении заданного времени, таймер удаляется автоматически. Перед использованием таймер ДОЛЖЕН быть инициализирован вызовом функции init_timer(). А перед тем как добавить таймер в список должны быть установлены поля function и expires.



Tasklets


Секция будет написана в одной из последующих версий документа.



Testmsg()


Функция testmsg() проверяет сообщение на соответсвтвие заданным критериям. Возвращает 1, если следующие условия соблюдены:

Режим поиска - SEARCH_ANY.

Режим поиска - SEARCH_LESSEQUAL и сообщение имеет тип меньший либо равный требуемому.

Ркжим поиска - SEARCH_EQUAL и тип сообщения равен требуемому.

Режим поиска - SEARCH_NOTEQUAL и тип сообщения не равен требуемому.



Try_atomic_semop()


Функция try_atomic_semop() вызывается из и и пытается выполнить каждую из операций в последовательности.

Если была встречена заблокированная операция, то процесс исполнения последовательности прерывается и все операции "откатываются". Если последовательность имела флаг IPC_NOWAIT, то возвращается код ошибки -EAGAIN. Иначе возвращается 1 для индикации того, что последовательность операций заблокирована.

Если значение семафора вышло за рамки системных ограничений, то выполняется "откат" всех операций и возвращается код ошибки -ERANGE.

Если последовательность операций была успешно выполнена и при этом аргумент do_undo не равен нулю, то выполняется "откат" всех операций и возвращается 0. Если аргумент do_undo равен нулю, то результат операций остается в силе и обновляется поле sem_otime.



Универсальные примитивы, используемые всеми тремя механизмами IPC


Механизмы семафоров, очередей сообщений и разделяемой памяти в Linux основаны на наборе общих примитивов. Этот раздел посвящен их описанию.



Update_queue()


update_queue() проходит по очереди ожидающих операций заданного набора семафоров и вызывает для каждой последовательности операций. Если статус элемента очереди показывает, что заблокированная задача уже была разбужена, то такой элемент пропускается. В качестве аргумента do_undo в функцию передается флаг q-alter, который указывает на то, что любые изменяющие операции необходимо "откатить" перед возвратом управления.

Если последовательность операций заблокирована, то update_queue() возвращает управление без внесения каких-либо изменений.

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

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

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



Управление файловой структурой


Структура file объявлена в include/linux/fs.h:

struct fown_struct { int pid; /* pid или -pgrp процесса, которому должен передаваться SIGIO */ uid_t uid, euid; /* uid/euid процесса-владельца */ int signum; /* posix.1b rt signal to be delivered on IO */ };

struct file { struct list_head f_list; struct dentry *f_dentry; struct vfsmount *f_vfsmnt; struct file_operations *f_op; atomic_t f_count; unsigned int f_flags; mode_t f_mode; loff_t f_pos; unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin; struct fown_struct f_owner; unsigned int f_uid, f_gid; int f_error;

unsigned long f_version;

/* требуется для драйвера tty, а возможно и для других */ void *private_data; };

Остановимся подробнее на полях struct file:

f_list: поле связи с одним (и только одним) из списков:

а) sb->s_files - список всех открытых файлов в данной файловой системе, если соответствующий inode не является анонимным, то dentry_open() (вызываемая из filp_open()) вставляет файл в этот список;

б) fs/file_table.c:free_list - список неиспользуемых структур;

в) fs/file_table.c:anon_list - в этот список включаются структуры, создаваемые в get_empty_filp().

Доступ к этим спискам производится под блокировкой files_lock.

f_dentry: dentry файла. Создается в процессе поиска nameidata в open_namei() (или точнее в path_walk()), но в действительности поле file->f_dentry заполняется в dentry_open().

f_vfsmnt: указатель на структуру vfsmount файловой системы, содержащей файл. Заполняется функцией dentry_open() и является частью nameidata, поиск которой производится в open_namei()

(или точнее в path_init()).

f_op: указатель на список file_operations, который содержит адреса методов для работы с файлом. Копируется из inode->i_fop

методом s_op->read_inode(), вызываемым в процессе поиска nameidata. Более подробно на списке file_operations мы остановимся ниже в этом разделе.

f_count: счетчик ссылок, изменяется в get_file/put_filp/fput.

f_flags: флаги O_XXX системного вызова open(2), копируются функцией dentry_open() (с небольшими изменениями в filp_open()), при чем флаги O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC


сбрасываются, поскольку они не могут модифицироваться по параметру F_SETFL (или F_GETFL) в системном вызове fcntl(2).

f_mode: комбинация флагов состояния, устанавливается в dentry_open(). Флаги режимов доступа для чтения и записи выведены в отдельные биты, чтобы облегчить контроль состояния: (f_mode & FMODE_WRITE) и (f_mode & FMODE_READ).

f_pos: текущая позиция чтения/записи в файле. Для архитектуры i386 имеет тип long long, т.е. 64 бита.

f_reada, f_ramax, f_raend, f_ralen, f_rawin: поддержка опережающего чтения (readahead) слишком сложна, чтобы обсуждаться простыми смертными ;)

f_owner: владелец файла, который будет получать I/O уведомления посредством механизма SIGIO

(см. fs/fcntl.c:kill_fasync()).

f_uid, f_gid - user id и group id процесса, открывшего файл, заполняются во время создания структуры в get_empty_filp(). Если файл является сокетом, то эти поля могут быть использованы в ipv4 netfilter.

f_error: используется клиентом NFS для возврата ошибки записи. Поле устанавливается в fs/nfs/file.c и проверяется в mm/filemap.c:generic_file_write().

f_version - механизм контроля версий, служит для синхронизации с кэшем. Увеличивается на единицу (используя глобальный event) всякий раз, когда изменяется f_pos.

private_data: скрытая информация о файле, может использоваться файловой системой (например coda хранит здесь удостоверения) или драйверами устройств. Драйверы устройств (при наличии devfs) могут использовать это поле для различения нескольких экземпляров вместо классического анализа младшего номера версии в file->f_dentry->d_inode->i_rdev.

Перейдем к рассмотрению списка методов управления файлом file_operations. Позволю себе напомнить, что он копируется из inode->i_fop методом s_op->read_inode(). Структура (список методов) объявлена в include/linux/fs.h:

struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char *, size_t, loff_t *); ssize_t (*write) (struct file *, const char *, size_t, loff_t *); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); };



owner: указатель на модуль рассматриваемой подсистемы. Это поле устанавливается только драйверами устройств, файловая система может игнорировать его, поскольку счетчик ссылок модуля файловой системы изменяется во время монтирования/демонтирования, в то время как для драйверов это должно делаться во время открытия/закрытия устройства.

llseek: реализация системного вызова lseek(2). Обычно опускается и используется fs/read_write.c:default_llseek(). (TODO: Принудительно устанавливать это поле в NULL, тем самым сэкономится лишний if() в llseek())

read: реализация системного вызова read(2). Файловые системы могут использовать mm/filemap.c:generic_file_read() для обычных файлов и fs/read_write.c:generic_read_dir() (которая просто возвращает -EISDIR) для каталогов.

write: реализация системного вызова write(2). Файловые системы могут использовать mm/filemap.c:generic_file_write() для обычных файлов и игнорировать его для каталогов.

readdir: используется файловой системой. Реализует системные вызовы readdir(2) и getdents(2) для каталогов и игнорируется для обычных файлов.

poll: реализация системных вызовов poll(2) и select(2)

ioctl: реализация специфичного для драйвера или для файловой системы метода ioctl ( управление вводом/выводом). Обратите внимание: общие методы ioctl типа FIBMAP, FIGETBSZ, FIONREAD

реализуются на более высоком уровне, поэтому они никогда не пользуются методом f_op->ioctl().

mmap: реализация системного вызова mmap(2). Файловая система может использовать generic_file_mmap для обычных файлов и игнорировать это поле для каталогов.

open: вызывается во время выполнения open(2) функцией dentry_open(). Редко используется файловыми системами, например coda пытается кэшировать файл во время открытия.

flush: вызывается при каждом вызове close(2) для заданного файла, не обязательно в последнем (см. метод release() ниже). Единственная файловая система, которая вызывает этот метод - это NFS клиент, которая "выталкивает" все измененные страницы. Примечательно, что этот метод может завершаться с кодом ошибки, который передается обратно в пространство пользователя, откуда делался системный вызов close(2).



release: метод вызывается в последнем вызове close(2) для заданного файла, т.е. когда file-> f_count станет равным нулю. Хотя и возвращает целое (int) значение, но VFS игнорирует его (см. code>fs/file_table.c:__fput()).

fsync: преобразуется в системные вызовы fsync(2)/fdatasync(2), причем последний аргумент определяет сам вызов - fsync или fdatasync. Не выполняет почти никаких действий за исключением преобразования файлового дескриптора в файловую структуру (file = fget(fd)) и сброса/установки семафора inode->i_sem. Файловая система Ext2, на сегодняшний день, игнорирует последний аргумент, передаваемый методу и выполняет одни и те же действия как для fsync(2) так и для fdatasync(2).

fasync: этот метод вызывается при изменении file->f_flags & FASYNC.

lock: специфичная для файловой системы часть механизма блокировки области файла POSIX fcntl(2). Единственная неувязка состоит в том, что этот метод вызывается перед независимой от типа файловой системы posix_lock_file(), если метод завершается успешно, а стандартный POSIX код блокировки терпит неудачу, то блокировка не будет снята на зависимом от типа файловой системы уровне..

readv: реализация системного вызова readv(2).

writev: реализация системного вызова writev(2).


Управление файловыми дескрипторами


В Linux между пользовательским файловым дескриптором и структурой inode в ядре, существует несколько уровней косвенных ссылок. Когда процесс открывает файл системным вызовом open(2), ядро возвращает положительное малое целое число, которое затем используется в операциях ввода/вывода над заданным файлом. Это целое число является индексом в массиве указателей на struct file. Каждая struct file содержит указатель на dentry file->f_dentry. Каждая dentry имеет указатель на inode dentry->d_inode.

Каждая задача содержит поле tsk->files которое указывает на struct files_struct, определенную в include/linux/sched.h:

/* * Структура таблицы открытых файлов */ struct files_struct { atomic_t count; rwlock_t file_lock; int max_fds; int max_fdset; int next_fd; struct file ** fd; /* массив дескрипторов */ fd_set *close_on_exec; fd_set *open_fds; fd_set close_on_exec_init; fd_set open_fds_init; struct file * fd_array[NR_OPEN_DEFAULT]; };

Поле file->count - это счетчик ссылок, увеличивается в get_file() (обычно вызывается из fget()) и уменьшается в fput() и в put_filp().Различие между fput() и put_filp() состоит в том, что fput()

выполняет больший объем работы, необходимый для регулярных файлов, т.е. освобождение блокировок, освобождение dentry и пр., в то время как put_filp() работает только с таблицей файловых структур, т.е. уменьшает счетчик, удаляет файл из anon_list и добавляет его в free_list, под блокировкой files_lock.

Таблица tsk->files может использоваться совместно родителем и потомком, если потомок был создан системным вызовом clone() с флагом CLONE_FILES. В качестве примера можно привести kernel/fork.c:copy_files() (вызывается из do_fork()), где только лишь увеличивается счетчик ссылок file->count. вместо обычного (для классического fork(2) в UNIX) копирования таблицы дескрипторов.

При открытии файла в памяти размещается новая файловая структура, которая устанавливается в слот current->files->fd[fd] и взводится бит fd в current->files->open_fds. Действия эти выполняются под защитой от записи read-write блокировкой current->files->file_lock. При закрытии дескриптора сбрасывается бит fd в current->files->open_fds, а поле current->files->next_fd устанавливается равным fd на случай поиска первого неиспользуемого дескриптора при следующем открытии файла.



Управление Суперблоком и точкой монтирования


В Linux, информация о смонтированных файловых системах хранится в двух различных структурах - super_block и vfsmount. Сделано это для того, чтобы имелась возможность смонтировать одну и ту же файловую систему к нескольким точкам монтирования одновременно, это означает, что одна и та же структура super_block может соответствовать нескольким структурам vfsmount.

В первую очередь рассмотрим структуру struct super_block, объявленную в include/linux/fs.h:

struct super_block { struct list_head s_list; /* Хранится первым */ kdev_t s_dev; unsigned long s_blocksize; unsigned char s_blocksize_bits; unsigned char s_lock; unsigned char s_dirt; struct file_system_type *s_type; struct super_operations *s_op; struct dquot_operations *dq_op; unsigned long s_flags; unsigned long s_magic; struct dentry *s_root; wait_queue_head_t s_wait;

struct list_head s_dirty; /* "грязные" inodes */ struct list_head s_files;

struct block_device *s_bdev; struct list_head s_mounts; /* vfsmount(s) of this one */ struct quota_mount_options s_dquot; /* параметры для Diskquota */

union { struct minix_sb_info minix_sb; struct ext2_sb_info ext2_sb; ..... Информация sb-private, необходимая для всех файловых систем ... void *generic_sbp; } u; /* * Следующее поле предназначено *только* для VFS. * Ни одна файловая система не должна изменять его, * даже если она обращается к этому полю. * Вас предупредили. */ struct semaphore s_vfs_rename_sem; /* Kludge */

/* Следующее поле используется демоном knfsd для преобразования(inode number based) * file handle в dentry. Поскольку путь в дереве dcache строится снизу вверх * то в течение некоторого времени путь является неполным, никак не связанным * с главным деревом. Этот семафор гарантирует существование единственного * такого свободного пути в файловой системе. * Заметьте, что такие "несвязанные" файлы допустимы * но не каталоги. */ struct semaphore s_nfsd_free_path_sem; };

Более подробно о полях структуры super_block:

s_list: двусвязный список всех активных суперблоков; Заметьте, что я не говорю "всех смонтированных файловых систем", потому что в Linux всем смонтированным экземплярам файловой системы соответствует единственный суперблок.


s_dev: предназначено для файловых систем, требующих наличие блочного устройства, т.е. для файловых систем, зарегистрированных с флагом FS_REQUIRES_DEV, это поле представляет собой копию i_dev блочного устройства. Для других файловых систем (называемых анонимными) представляет собой целое число MKDEV(UNNAMED_MAJOR, i), где i принадлежит диапазону от 0 до 255 включительно и является порядковым номером первого неустановленного бита в массиве unnamed_dev_in_use. Смотрите fs/super.c:get_unnamed_dev()/put_unnamed_dev(). Неоднократно предлагалось отказаться от использования поля s_dev анонимными файловыми системами.

s_blocksize, s_blocksize_bits: Размер блока и количество бит, необходимое для хранения размера блока (log2(blocksize)).

s_lock: индикатор блокировки суперблока функциями lock_super()/unlock_super().

s_dirt: устанавливается при внесении изменений в суперблок и сбрасывается при записи его обратно на диск.

s_type: указатель на структуру struct file_system_type, соответствующую файловой системе. Метод файловой системы read_super() не должен устанавливать это поле, так как это поле устанавливается VFS в функции fs/super.c:read_super(), в случае успешного вызова метода read_super() конкретной файловой, и сбрасывется в NULL в противном случае.

s_op: указатель на структуру (список) super_operations, которая содержит специфичные для заданной файловой системы методы, такие как чтение/запись inode и пр. Корректное заполнение этой структуры - задача метода файловой системы read_super().

dq_op: операции по дисковому квотированию.

s_flags: флаги суперблока.

s_magic: "магическое" число файловой системы. Используется файловой системой minix для различения разных вариантов ее.

s_root: dentry корня файловой системы. Метод read_super() считывает корневой inode с диска и передает его в d_alloc_root(), который выделяет память под dentry и заполняет ее. Некоторые файловые системы используют иное обозначение корня, нежели "/", поэтому используется более общая функция d_alloc() для образования полного имени, например pipefs использует "pipe:" для обозначения своего корня.



s_wait: очередь ожидания, в которой нахдятся процессы, ожидающие снятия блокировки с суперблока.

s_dirty: список всех "грязных" (измененных) inodes. Напомню, что если inode изменился (т.е. inode->i_state & I_DIRTY), то этот список связуется через inode->i_list.

s_files: список всех открытых файлов в данном суперблоке. Полезен при принятии решения о перемонтировании файловой системы в режиме "только для чтения", см. fs/file_table.c:fs_may_remount_ro(), которая просматривает список sb->s_files и отвергает возможность перемонтирования если имеется хотя бы один файл, открытый "на запись" (file->f_mode & FMODE_WRITE) или ожидающий удаления (inode->i_nlink == 0).

s_bdev: для случая FS_REQUIRES_DEV указывает на структуру block_device, описывающую блочное устройство, с которого смонтирована файловая система.

s_mounts: список всех структур vfsmount для каждого смонтированного экземпляра данного суперблока.

s_dquot: используется при квотировании диска.

Методы управления суперблоком перечисляются в структуре super_operations, объявленной в include/linux/fs.h:

struct super_operations { void (*read_inode) (struct inode *); void (*write_inode) (struct inode *, int); void (*put_inode) (struct inode *); void (*delete_inode) (struct inode *); void (*put_super) (struct super_block *); void (*write_super) (struct super_block *); int (*statfs) (struct super_block *, struct statfs *); int (*remount_fs) (struct super_block *, int *, char *); void (*clear_inode) (struct inode *); void (*umount_begin) (struct super_block *); };

read_inode: операция чтения inode из файловой системы. Вызывается только в fs/inode.c:get_new_inode() из iget4()

(и следовательно из iget()). Если файловая система предполагает вызов iget() то метод read_inode() должен быть реализован, в противном случае get_new_inode() будет приводить к "впадению в панику" (panic). Во время чтения inode заблокирован (inode->i_state = I_LOCK). Когда функция возвращает управление, все процессы из очереди inode->i_wait пробуждаются. В задачу метода read_inode() входит обнаружение дискового блока, который содержит заданный inode и с помощью функйии буферного кэша bread() прочитать его и инициализировать различные поля в структуре inode, например inode->i_op и inode->i_fop, чтобы уровень VFS "знал" какие операции над inode и соответствующим ему файлом, считаются допустимыми. Имеются файловые системы, в которых метод read_inode() не реализован - это ramfs и pipefs. Так ramfs имеет свою собственную функцию генерации inode (ramfs_get_inode()).



write_inode: операция записи inode на диск. так же как и read_inode() отыскивает нужный дисковый блок и вызывает функцию буферного кэша mark_buffer_dirty(bh). Этот метод вызывается для "грязных" inode (которые были помечены вызовом mark_inode_dirty()) при возникновении необходимости синхронизации как отдельно взятого inode, так и файловой системы в целом.

put_inode: вызывается всякий раз при уменьшении счетчика ссылок.

delete_inode: вызывается всякий раз, когда inode->i_count и inode->i_nlink

достигают нулевого значения. Файловая система удаляет дисковую копию inode и вызывает clear_inode() для VFS inode, чтобы "прекратить его существование окончательно".

put_super: вызывается на последней стадии работы системного вызова umount(2), чтобы уведомить файловую систему о том, что любая приватная информация, удерживаемая ею, должна быть освобождена. Обычно это brelse() блока, содержащего суперблок, и kfree() для освобождения всех ранее размещенных блоков, inodes и т.п.

write_super: вызывается в случае необходимости записать суперблок на диск. Должен отыскать блок, содержащий суперблок, (обычно хранится в области sb-private) и вызвать mark_buffer_dirty(bh). А так же должен сбросить флаг sb->s_dirt flag.

statfs: реализация системного вызова fstatfs(2)/statfs(2). Заметьте, что указатель на struct statfs, передаваемый в качестве аргумента, является указателем пространства ядра а не пользовательского пространства, поэтому не следует выполнять каких либо операций ввода-вывода в пользовательском пространстве. В случае отсутствия этого метода вызов statfs(2) будет возвращвть код ошибки ENOSYS.

remount_fs: вызывается всякий раз при перемонтировании файловой системы.

clear_inode: вызывается из функции clear_inode() уровня VFS. Файловая система должна освободить приватную информацию в структуре inode (присоединенную через поле generic_ip).

umount_begin: вызывается в случае принудительно размонтирования для уведомления файловой системы заранее, чтобы убедиться, что она не занята. В настоящее время используется только NFS. Этот метод не имеет никакого отношения к идее поддержки принудительного размонтирования на уровне VFS.



Теперь рассмотрим последовательность действий, выполняемых при монтировании дисковой (FS_REQUIRES_DEV) файловой системы. Реализация системного вызова mount(2)

находится в fs/super.c:sys_mount(), которая по сути является лишь оберткой, которая передает опции монтирования, тип файловой системы и название устройства в функцию do_mount().

В случае необходимости, загружается модуль драйвера файловой системы и увеличивается счетчик ссылок на этот модуль. Примечательно, что в процессе монтирования счетчик ссылок на модуль файловой системы увеличивается дважды - один раз в do_mount(), вызываемой из get_fs_type(), и один раз в get_sb_dev(), вызываемой из get_filesystem(), если read_super()

выполнилась успешно. Первое увеличение предотвращает выгрузку модуля пока выполняется метод read_super() и второе увеличение указывает на то, что модуль используется смонтированным экземпляром. Вполне понятно, что перед завершением do_mount() уменьшает счетчик ссылок на единицу, таким образом суммарное приращение счетчика составляет единицу после каждого монтирования.

Для нашего случая выражение fs_type->fs_flags & FS_REQUIRES_DEV истинно, поэтому далее инициализируется суперблок, вызовом get_sb_bdev(), который получает ссылку на блочное устройство и вызывом метода read_super() заполняет поля суперблока. Если все прошло гладко, то структура super_block считается инициализированной и мы получаем дополнительно ссылку на модуль файловой системы и ссылку на основное блочное устройство.

В памяти размещается новая структура vfsmount и "прицепляется" к списку sb->s_mounts и к глобальному списку vfsmntlist. С помощью поля mnt_instances структуры vfsmount можно найти все смонтированные экземпляры файловой системы для одного и того же суперблока. С помощью списка mnt_list можно отыскать все смонтированные экземпляры файловых систем для всех суперблоков в системе. Поле mnt_sb указывает на данный суперблок, а mnt_root получает новую ссылку на sb->s_root dentry.


Высокоуровневая инициализация


Под "высокоуровневой инициализацией" следует понимать действия, непосредственно не связанные с начальной загрузкой, даже не смотря на то, что часть кода, выполняющая ее, написана на ассемблере, а именно в файле arch/i386/kernel/head.S, который является началом декомпрессированного ядра. При инициализации выполняются следующие действия:

Устанавливаются сегментные регистры (%ds = %es = %fs = %gs = __KERNEL_DS = 0x18).

Инициализируются таблицы страниц.

Разрешается листание страниц, установкой бита PG в %cr0.

Обнуляется BSS (для SMP (мультипроцессорных систем (прим. перев.)), это действие выполняет только первый CPU).

Копируются первые 2k bootup параметров (kernel commandline).

Проверяется тип CPU, используя EFLAGS и, если возможно, cpuid, позволяющие обнаружить процессор 386 и выше.

Первый CPU вызывает start_kernel(), все остальные - arch/i386/kernel/smpboot.c:initialize_secondary(), если переменная ready=1, которая только переустанавливает esp/eip.

Функция init/main.c:start_kernel() написана на C и выполняет следующие действия:

Выполняется глобальная блокировка (необходимая для того, чтобы через процесс инициализации проходил только один CPU)

Выполняются платформо-зависимые настройки (анализируется раскладка памяти, копируется командная строка и пр.).

Вывод "баннера" ядра, который содержит версию, компилятор, использованные при сборке, и пр., в кольцевой буфер для сообщений. Текст "баннера" задается в переменной linux_banner, определенной в init/version.c. Текст этот можно вывести на экран командой cat /proc/version.

Инициализация ловушек.

Инициализация аппаратных прерываний (irqs).

Инициализация данных для планировщика.

Инициализация данных хранения времени.

Инициализация подсистемы программных прерываний (softirq).

Разбор параметров командной строки.

Инициализация консоли.

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

Инициализируются профилирующие буферы, если командная строка содержит указание "profile=".


kmem_cache_init(), начало инициализации менеджера памяти.

Разрешаются прерывания.

Подсчет значения BogoMips для данного CPU.

Вызывается mem_init() которая подсчитывает max_mapnr, totalram_pages и high_memory и выводит строку "Memory: ...".

kmem_cache_sizes_init(), завершение инициализации менеджера памяти.

Инициализация структур данных для procfs.

fork_init(), создает uid_cache, инициализируется max_threads исходя из объема доступной памяти и конфигурируется RLIMIT_NPROC для init_task как max_threads/2.

Создаются различные кэши для VFS, VM, кэш буфера и пр..

Инициализируется подсистема IPC, если имеется поддержка System V IPC. Обратите внимание, что для System V shm, это включает монтирование внутреннего (in-kernel) экземпляра файловой системы shmfs.

Создается и инициализируется специальный кэш, если поддержка квот (quota) включена.

Выполняется платформо-зависимая "проверка ошибок" ("check for bugs") и, если это возможно, активируется обработка ошибок процессора/шины/проч. Сравнение различных архитектур показывает, что "ia64 не имеет ошибок" а "ia32 имеет несколько дефектов", хороший пример - "дефект f00f" который проверен только для ядра, собранного под процессор ниже, чем 686.

Устанавливается флаг, указывающий на то, что планировщик должен быть вызван "при первой возможности" и создается поток ядра init(), который выполняет execute_command, если она имеется среди параметров командной строки в виде "init=", или пытается запустить /sbin/init, /etc/init, /bin/init, /bin/sh в указанном порядке; если не удается ни один из запусков то ядро "впадает в панику" с "предложением" задать параметр "init=".

Переход в фоновый поток с pid=0.

Здесь важно обратить внимание на то, что задача init() вызывает функцию do_basic_setup(), которая в свою очередь вызывает do_initcalls() для поочередного (в цикле) вызова функций, зарегистрированных макросом __initcall или module_init() Эти функции либо являются независимыми друг от друга, либо их взаимозависимость должна быть учтена при задании порядка связывания в Makefile - ах. Это означает, что порядок вызова функций инициализации зависит от положения каталогов в дереве и структуры Makefile - ов. Иногда порядок вызова функций инициализации очень важен. Представим себе две подсистемы: А и Б, причем Б существенным образом зависит от того как была проинициализирована подсистема А. Если А скомпилирована как статическая часть ядра, а Б как подгружаемый модуль, то вызов функции инициализации подсистемы Б будет гарантированно произведен после инициализации подсистемы А. Если А - модуль, то и Б так же должна быть модулем, тогда проблем не будет. Но что произойдет, если и А, и Б скомпилировать с ядром статически? Порядок, в котором они будут вызываться (иницализироваться) зависит от смещения относительно точки .initcall.init ELF секции в образе ядра (грубо говоря - от порядка вызова макроса __initcall или module_init() прим. перев.). Rogier Wolff предложил ввести понятие "приоритетной" инфраструктуры, посредством которой модули могли бы задавать компоновщику порядок связывания, но пока отсутствуют заплаты, которые реализовали бы это качество достаточно изящным способом, чтобы быть включенным в ядро. А посему необходимо следить за порядком компоновки. Если А и Б (см. пример выше) скомпилированы статически и работают корректно, то и при каждой последующей пересборке ядра они будут работать, если порядок следования их в Makefile не изменяется. Если же они не функционируют, то стоит изменить порядок следования объектных файлов.

Еще одна замечательная особенность Linux - это возможность запуска "альтернативной программы инициализации", если ядру передается командная строка "init=". Эта особенность может применяться для перекрытия /sbin/init или для отладки скриптов инициализации (rc) и /etc/inittab

вручную, запуская их по одному за раз


Заблокированные операции над семафорами


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

В поле semsleeping текущей задачи заносится указатель на очередь ожидания . Задача переводится в состояние TASK_INTERRUPTIBLE и поле sleeper структуры инициализируется указателем на текущую задачу. Далее снимается глобальная блокировка семафора и вызывается планировщик schedule(), чтобы перевести задачу в разряд "спящих".

После пробуждения задача повторно выполняет глобальную блокировку семафора, определяет причину пробуждения и реагирует на нее соответствующим образом:

Если набор семафоров был удален, то в вызывающую программу возвращается код ошибки EIDRM.

Если поле status в структуре установлено в 1, то задача была разбужена для повторной попытки выполнения операций над семафорами. В этом случае повторно вызывается для выполнения последовательности операций Если try_atomic_semop() вернула 1, то задача снова блокируется, как описано выше. Иначе возвращается 0 либо соответствующий код ошибки. Перед выходом из sys_semop() сбрасывается поле current->semsleeping и удаляется из очереди. Если какая либо из операций производила изменения (увеличение или уменьшение), то вызывается , которая проходит через очередь операций, ожидающих выполнения, для данного набора семафоров и активирует все ожидающие процессы, которые больше не нужно блокировать.

Если поле status в структуре НЕ

установлено в 1 и не была удалена из очереди, то это означает, что задача была разбужена по прерыванию. В этом случае в вызывающую программу возвращается код ошибки EINTR. Перед возвратом сбрасывается поле current->semsleeping и удаляется из очереди. А так же вызывается , если какая либо из операций производила изменения.

Если поле status в структуре НЕ

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



Загрузка: BIOS POST


При включении питания запускается тактовый генератор и схема контроля питания устанавливает на шине сигнал #POWERGOOD.

На вывод CPU #RESET подается сигнал (после чего CPU переходит в реальный режим 8086).

%ds=%es=%fs=%gs=%ss=0, %cs=0xFFFF0000,%eip = 0x0000FFF0 (запуск кода Power On Self Test в ROM BIOS).

На время выполнения проверок, прерывания запрещены.

По адресу 0 инициализируется таблица векторов прерываний (IVT, Interrupts Vector Table).

По прерыванию 0x19 вызывается начальный (bootstrap) загрузчик BIOS, регистр %dl содержит 'номер загрузочного устройства'. В результате по физическому адресу 0x7C00 (0x07C0:0000) загружается содержимое первого сектора нулевой дорожки.



Загрузка: bootsector и setup


Для загрузки ядра Linux можно воспользоваться следующими загрузочными секторами:

Загрузочным сектором Linux из (arch/i386/boot/bootsect.S),

Загрузочный сектор LILO (или другого менеджера загрузки), или

обойтись без загрузочного сектора (loadlin и т.п.)

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

29 SETUPSECS = 4 /* число секторов установщика по умолчанию */ 30 BOOTSEG = 0x07C0 /* первоначальный адрес загрузочного сектора */ 31 INITSEG = DEF_INITSEG /* сюда перемещается загрузчик - чтобы не мешал */ 32 SETUPSEG = DEF_SETUPSEG /* здесь начинается установщик */ 33 SYSSEG = DEF_SYSSEG /* система загружается по адресу 0x10000 (65536) */ 34 SYSSIZE = DEF_SYSSIZE /* размер системы: в 16-байтных блоках */

(числа в начале - это номера строк в файле bootsect.S file) Значения DEF_INITSEG, DEF_SETUPSEG, DEF_SYSSEG и DEF_SYSSIZE берутся из файла include/asm/boot.h:

/* Ничего не меняйте, если не уверены в том, что делаете. */ #define DEF_INITSEG 0x9000 #define DEF_SYSSEG 0x1000 #define DEF_SETUPSEG 0x9020 #define DEF_SYSSIZE 0x7F00

Рассмотрим поближе код bootsect.S:

54 movw $BOOTSEG, %ax 55 movw %ax, %ds 56 movw $INITSEG, %ax 57 movw %ax, %es 58 movw $256, %cx 59 subw %si, %si 60 subw %di, %di 61 cld 62 rep 63 movsw 64 ljmp $INITSEG, $go

65 # bde - 0xff00 изменено на 0x4000 для работы отладчика с 0x6400 и выше (bde). 66 # Если мы проверили верхние адреса, то об этом можно не беспокоиться. Кроме того, 67 # мой BIOS можно сконфигурировать на загрузку таблицы дисков wini в верхнюю память 68 # вместо таблицы векторов. Старый стек может "помесить" 69 # таблицу устройств [drive table].

70 go: movw $0x4000-12, %di # 0x4000 - произвольное значение >= 71 # длины bootsect + длины 72 # setup + место для стека; 73 # 12 - размер параметров диска. 74 movw %ax, %ds # INITSEG уже в ax и es 75 movw %ax, %ss 76 movw %di, %sp # разместим стек по INITSEG:0x4000-12.


Строки 54- 63 перемещают код начального загрузчика из адреса 0x7C00 в адрес 0x90000. Для этого:

в регистровую пару %ds:%si заносится значение $BOOTSEG:0 (0x7C0:0 = 0x7C00)

в регистровую пару %es:%di заносится значение $INITSEG:0 (0x9000:0 = 0x90000)

в регистр %cx записывается число 16-битовых слов (256 слов = 512 байт = 1 сектор)

В регистре флагов EFLAGS сбрасывается флаг направления DF (Direction Flag) (копирование с автоинкрементом адресных регистров) (cld)

копируется 512 байт (rep movsw)

Здесь умышленно не используется инструкция rep movsd (обратите внимание на директиву - .code16).

В строке 64 выполняется переход на метку go:, в только что созданную копию загрузчика, т.е. в сегмент 0x9000. Эта, и следующие три инструкции (строки 64-76) переустанавливают регистр сегмента стека и регистр указателя стека на $INITSEG:0x4000-0xC, т.е. %ss = $INITSEG (0x9000) и %sp = 0x3FF4 (0x4000-0xC). Это и есть то самое ограничение на размер setup, которое упоминалось ранее (см. Построение образа ядра Linux).

Для того, чтобы разрешить считывание сразу нескольких секторов (multi-sector reads), в строках 77-103 исправляются некоторые значения в таблице параметров для первого диска :

77 # Часто в BIOS по умолчанию в таблицы параметров диска не признают 78 # чтение по несколько секторов кроме максимального числа, указанного 79 # по умолчанию в таблице параметров дискеты - что может иногда равняться 80 # 7 секторам. 81 # 82 # Поскольку чтение по одному сектору отпадает (слишком медленно), 83 # необходимо позаботиться о создании в ОЗУ новой таблицы параметров 84 # (для первого диска). Мы установим максимальное число секторов 85 # равным 36 - максимум, с которым мы столкнемся на ED 2.88. 86 # 87 # Много - не мало. А мало - плохо. 88 # 89 # Сегменты устанавливаются так: ds = es = ss = cs - INITSEG, fs = 0, 90 # а gs не используется.

91 movw %cx, %fs # запись 0 в fs 92 movw $0x78, %bx # в fs:bx адрес таблицы 93 pushw %ds 94 ldsw %fs:(%bx), %si # из адреса ds:si 95 movb $6, %cl # копируется 12 байт 96 pushw %di # di = 0x4000-12. 97 rep # инструкция cld не нужна - выполнена в строке 66 98 movsw 99 popw %di 100 popw %ds 101 movb $36, 0x4(%di) # записывается число секторов 102 movw %di, %fs:(%bx) 103 movw %es, %fs:2(%bx)



Контроллер НГМД переводится в исходное состояние функцией 0 прерывания 0x13 в BIOS (reset FDC) и секторы установщика загружаются непосредственно после загрузчика, т.е. в физические адреса, начиная с 0x90200 ($INITSEG:0x200), с помощью функции 2 прерывания 0x13 BIOS (read sector(s)). Смотри строки 107-124:

107 load_setup: 108 xorb %ah, %ah # переинициализация FDC 109 xorb %dl, %dl 110 int $0x13 111 xorw %dx, %dx # диск 0, головка 0 112 movb $0x02, %cl # сектор 2, дорожка 0 113 movw $0x0200, %bx # адрес в INITSEG = 512 114 movb $0x02, %ah # функция 2, "read sector(s)" 115 movb setup_sects, %al # (все под головкой 0, на дорожке 0) 116 int $0x13 # читать 117 jnc ok_load_setup # получилось - продолжить

118 pushw %ax # запись кода ошибки 119 call print_nl 120 movw %sp, %bp 121 call print_hex 122 popw %ax 123 jmp load_setup

124 ok_load_setup:

Если загрузка по каким-либо причинам не прошла (плохая дискета или дискета была вынута в момент загрузки), то выдается сообщение об ошибке и производится переход на бесконечный цикл. Цикл будет повторяться до тех пор, пока не произойдет успешная загрузка, либо пока машина не будет перезагружена.

Если загрузка setup_sects секторов кода установщика прошла благополучно, то производится переход на метку ok_load_setup:.

Далее производится загрузка сжатого образа ядра в физические адреса начиная с 0x10000, чтобы не затереть firmware-данные в нижних адресах памяти (0-64K). После загрузки ядра управление передается в точку $SETUPSEG:0 (arch/i386/boot/setup.S). Поскольку обращений к BIOS больше не будет, данные в нижней памяти уже не нужны, поэтому образ ядра перемещается из 0x10000 в 0x1000 (физические адреса, конечно). И наконец, установщик setup.S завершает свою работу, переводя процессор в защищенный режим и передает управление по адресу 0x1000 где находится точка входа в сжатое ядро, т.е. arch/386/boot/compressed/{head.S,misc.c}. Здесь производится установка стека и вызывается decompress_kernel(), которая декомпрессирует ядро в адреса, начиная с 0x100000, после чего управление передается туда.



Следует отметить, что старые загрузчики (старые версии LILO) в состоянии загружать только первые 4 сектора установщика (setup), это объясняет присутствие кода, "догружающего" остальные сектора в случае необходимости. Кроме того, установщик содержит код, обрабатывающий различные комбинации типов/версий загрузчиков и zImage/bzImage.

Теперь рассмотрим хитрость, позволяющую загрузчику выполнить загрузку "больших" ядер, известных под именем "bzImage". Установщик загружается как обычно, в адреса с 0x90200, а ядро, с помощью специальной вспомогательной процедуры, вызывающей BIOS для перемещения данных из нижней памяти в верхнюю, загружается кусками по 64К. Эта процедура определена в setup.S как bootsect_helper, а вызывается она из bootsect.S как bootsect_kludge. Метка bootsect_kludge, определенная в setup.S, содержит значение сегмента установщика и смещение bootsect_helper в нем же, так что для передачи управления загрузчик должен использовать инструкцию lcall (межсегментный вызов). Почему эта процедура помещена в setup.S? Причина банальна - в bootsect.S просто больше нет места (строго говоря это не совсем так, поскольку в bootsect.S свободно примерно 4 байта и по меньшей мере еще 1 байт, но вполне очевидно, что этого недостаточно) Эта процедура использует функцию прерывания BIOS 0x15 (ax=0x8700) для перемещения в верхнюю память и переустанавливает %es так, что он всегда указывает на 0x10000. Это гарантирует, что bootsect.S не исчерпает нижнюю память при считывании данных с диска.


Загрузка: Обзор


Процесс загрузки во многом зависит от аппаратной платформы, поэтому основное внимание будет уделено платформе IBM PC/IA32. Для сохранения обратной совместимости, firmware-загрузчики загружают операционную систему устаревшим способом. Процесс этот можно разделить на несколько этапов:

BIOS выбирает загрузочное устройство.

BIOS загружает bootsector с загрузочного устройства.

Код bootsector-а загружает установщика, процедуры декомпрессии и сжатый образ ядра.

Ядро декомпрессируется в защищенном режиме (protected mode).

Выполняется низкоуровневый инициализирующий ассемблерный код.

Выполняется высокоуровневый инициализирующий C код.