Разработка модулей ядра ос linux Kernel newbie's manual


Глава 2. Разработка ядра Linux



Pdf просмотр
страница2/3
Дата15.11.2016
Размер0.53 Mb.
Просмотров736
Скачиваний0
1   2   3
Глава 2. Разработка ядра Linux
2.1. Системные вызовы
2.1.1. Системные вызовы в Linux
Ядро предоставляет набор интерфейсов, именуемых системными вызовами, которые обеспечивают взаимодействие прикладных программ, работающих в пространстве пользователя, и аппаратной части компьютера. Например, при работе с файлами программы могут не заботиться о типе жесткого диска и файловой системе на нем.
Системные вызовы гарантируют безопасность и стабильность системы. Так как ядро работает посредником между ресурсами системы и программами, оно может принимать решения о предоставлении доступа в соответствии с правами пользователя и другими критериями.
Прикладные программы разрабатываются с применением программных интерфейсов приложений (Application Programming Interface, API). В этом случае нет необходимости между в корреляции между интерфейсами, которые используют приложения и интерфейсами, которые предоставляет ядро. POSIX,
WinAPI – примеры таких API. Может существовать один и тот же API для различных операционных систем, а реализация его может отличаться.
Например, операционные системы Linux и FreeBSD соответствуют стандарту
POSIX (Linux на 100% соответствует стандарту POSIX 1003.1 [9]), и многие приложения, написанные для FreeBSD, могут сравнительно легко быть перенесены в Linux и наоборот. Этим объясняется схожесть наборов приложений для этих операционных систем.
Частично интерфейс к системным функциям обеспечивает библиотека C.
Например, функция printf() формирует строку в соответствии с заданным
24
форматом и передает ее системному вызову write(), который отправляет ее на стандартное устройство вывода (чаще всего терминал).
Рис. 2.1. Взаимодействие между приложением, библиотекой C и ядром на примере функции printf()
Дополнительно библиотека функций языка C предоставляет большую часть
API-стандарта POSIX. С помощью команды strace можно отслеживать обращения программы к ядру, производимые с помощью системных вызовов
(поэтому ее можно использовать для отладки и поиска причины ошибки). [10]
Реализация системных вызовов
Системные вызовы (в ОС Linux их часто именуют syscall) обычно реализуются в виде функции с возвращаемым значением типа long. Для них могут быть определены один или боле аргументов. В случае ошибки системные вызовы записывают специальный код ошибки в глобальную переменную errno.
Значение errno может быть переведено с помощью библиотечной функции perror(), которая выводит в стандартный поток сообщения, описывая ошибку, произошедшую при последнем системном вызове или вызове библиотечной функции.
Рассмотрим системный вызов getuid() (реализован в kernel/timer.c), который возвращает uid текущего процесса:
asmlinkage long sys_getuid(void)
{
return current->uid;
}
25
Функция printf()
printf()
в библиотеке C
write() в билиотеке C
Системный вызов write()
Приложение
Библиотека языка C
Ядро

Модификатор asmlinkage говорит компилятору, что обращение к функции должно производиться только через стек. Все системные вызовы возвращают значение типа long. Системный вызов getuid() объявлен как sys_getuid(). Это соглашение о присваивании имен, принятое в системе Linux.
Каждому системному вызову в Linux присвоен уникальный номер
системного вызова (syscall number). Процессы не обращаются к системным вызовам по имени, вместо этого они используют его номер. Однажды назначенный номер не должен меняться никогда для совместимости с прикладными программами. Если системный вызов удаляется, соответствующий номер не должен использоваться повторно. В ядре Linux версии 2.6.18 реализовано 318 системных вызовов для платформы i386 (это значение хранится в постоянной NR_syscalls). Объявление вызовов находится в файле include/asm/unistd.h. Этому файлу в коде ядра соответствует файл include/asm-i386/unistd.h в дереве исходных кодов ядра. Для лучшей переносимости в коде ядра вместо asm-<тип архитектуры> используется asm
(на этапе сборки создается символическая ссылка include/asm на каталог include/asm-i386/). Поэтому в дальнейшем под каталогом asm/ будет подразумеваться каталог asm-i386/.
В Linux предусмотрен "не реализованный" ("not implemented") системный вызов – функция sys_ni_syscall(), которая не делает ничего, кроме того, что возвращает значение, равное -ENOSYS – код ошибки, соответствующий неправильному системному вызову. Эта функция используется вместо удаленных системных вызовов.
Ядро хранит список зарегистрированных системных вызовов в таблице системных вызовов (syscall table). Таблица хранится в памяти, на которую указывает переменная sys_call_table. Данная таблица для платформы i386
(каждая платформа позволяет определять свои уникальные системные вызовы) хранится в файле arch/i386/kernel/syscall_table.S. [10]
26

Обработка системных вызовов
Пользовательские программы, если хотят обратиться к ядру через системный вызов, должны использовать определенный механизм. Таким механизмом является программное прерывание: создается исключительная ситуация (exception), и система переключается в режим ядра для выполнения обработчика этой ситуации. В данном случае это обработчик системного вызова (system call heandler) – зависимая от аппаратной платформы функция system_call() (функция на языке ассемблера, реализована в arch/i386/kernel/entry.S).
Многие системные вызовы осуществляют переход в режим ядра одинаковым образом, поэтому ядру должен также передаваться номер системного вызова. Для аппаратной платформы i386 этот номер сохраняется в регистре eax. Обработчик системных вызовов считывает из регистра eax это значение. Функция system_call() проверяет правильность системного вызова сравнением с постоянной NR_syscalls. если значение больше или равно
NR_syscalls, возвращается значение -ENOSYS, в противном случае вызывается соответствующий системный вызов:
call *sys_call_table(,%eax,4)
Большинство системных вызовов требуют также передачи одного или нескольких параметров. Самый простой способ передачи параметров – это сохранить их в регистрах процессора. Для платформы i386 регистры ebx, ecx, edx, esi, edi хранят соответственно первые пять аргументов. В случае шести или более аргументов используется регистр, который содержит указатель на память пространства пользователя, где хранятся все параметры.
Возвращаемое значение также передается через регистр. Для платформы i386 оно сохраняется в регистре eax. [10]
27

2.1.2. Создание нового системного вызова
Задание 1
Добавить новый системный вызов в ядро. Системный вызов должен возвращать размер стека ядра.
Ход работы
Значение размера стека ядра в постоянной THREAD_SIZE, определенной в файле include/asm/thread_info.h. Системный вызов возвращает это значение.
#include
asmlinkage long sys_getstsize(void)
{
return THREAD_SIZE;
}
Регистрация нового системного вызова проходит в несколько этапов.
Код системного вызова размещается в дереве исходных кодов ядра.
Разместим код системного вызова в файле arch/i386/kernel/new_calls.c. Вообще, лучше размещать код в наиболее подходящем файле. Для того, чтобы файл new_calls.c был скомпилирован с ядром, необходимо вписать в файл arch/i386/kernel/Makefile строку obj-y += new_calls.o
Добавляется новая запись в конец таблицы системных вызовов в файле arch/i386/kernel/syscall_table.S. Это необходимо сделать для всех платформ, которые поддерживают данный системный вызов (как в нашем случае, но мы ограничимся только платформой i386). Отсчет в таблице начинается с нуля.
.long sys_tee
/* 315 */
28

.long sys_vmsplice
.long sys_move_pages
.long sys_ getstsize
Для всех поддерживаемых платформ номер системного вызова должен быть определен в файле unistd.h (мы ограничимся только файлом include/asm/unistd.h).
#define __NR_tee
315
#define __NR_vmsplice
316
#define __NR_move_pages
317
#define __NR_ getstsize
318
Значение постоянной NR_syscalls тоже должно быть изменено (увеличено на количество добавляемых вызовов – в нашем случае 1).
#define NR_syscalls 319
После этого ядро собирается с новым системным вызовом.
Непрямой доступ к вызовам
Новый системный вызов не поддерживается библиотекой языка C. Но в ОС
Linux существует функция syscall(), предоставляющая непрямой доступ к системным вызовам. В качестве параметра в функцию syscall() передаются номер системного вызова и далее его параметры в том же порядке, в котором они определены в коде ядра. Функция syscall() впервые появилась в ОС 4.0BSD
(информация взята из справки man 2 syscall). Для работы с функцией необходимо подключить заголовочный файл sys/syscall.h.
#include
Например, рассмотрим системный вызов open(), который определен следующим образом.
29
long open(const char *filename, int flags, int mode)
Вызов этой функции с помощью syscall() будет выглядеть так.
#define __NR_open 5
syscall(__NR_open, filename, flags, mode)
Постоянная __NR_open – это номер системного вызова, определенный в файле include/asm/unistd.h.
Задание 2
Реализовать доступ к новому вызову. Для этого написать программу, эксплуатирующую новый системный вызов.
Ход работы
Исходный код программы на языке C:
#include
#include
#define __NR_getstsize 318
int main()
{
long stsize;
stsize = syscall(__NR_getstsize);
printf("Kernel stack size = %ld bytes\n", stsize);
return 0;
}
В переменной stsize типа long сохраним результат системного вызова getstsize() и затем выведем сообщение с размером стека. После компиляции и
30
запуска программы, получаем сообщение, которое подтверждает, что новый системный вызов работает.
# gcc getstsize.c -o getstsize; ./getstsize
Kernel stack size = 8192 bytes
Код программы находится в файле getstsize.c приложений. Код системного вызова находится в файле new_calls.c приложений.
Указания
Для выполнения заданий необходимо знание языка С и основ программирования в Linux (точнее, знание компилятора gcc). Хорошими пособиями для начинающих являются [6, 13]. В качестве редактора программного кода рекомендуется использовать текстовый редактор с подсветкой синтаксиса, например mcedit (поставляется вместе с программой mc).
2.2. Управление памятью в Linux
2.2.1. Страницы памяти
Ядро рассматривает страницы физической памяти как основные единицы управления памятью. Наименьшая единица памяти, которую может адресовать процессор – это машинное слово, однако модуль управления памятью (MMU) обычно работает со страницами памяти. Модуль MMU управляет таблицами страниц на уровне страничной детализации. Для каждой аппаратной платформы существует свой объем страниц памяти. Большинство 32-разрядных платформ имеют размер 4 Кбайт, а большинство 64-разрядных – 8 Кбайт.
Таким образом, на 32-разрядной платформе объем памяти в 1 Гбайт разбивается на 262.144 страницы.
31

Структура page
Страница представлена в ядре в виде структуры page, описанной в файле include/linux/mm.h. Важно понимать, что структура page описывает страницы физической, а не виртуальной памяти. Ядро использует эту структуру, чтобы описывать область физической памяти, а не данных, которые в ней содержаться. Размер структуры равен 40 байт. В этом случае для описания всех страниц памяти объемом 1 Гбайт используется 262.144 * 40 = 10.485.760 байт, то есть 10 Мбайт. [10]
Зоны памяти
Из-за ограничений аппаратного обеспечения ядро не может рассматривать все страницы физической памяти как идентичные. Некоторые страницы не могут использоваться для некоторых типов задач. Поэтому ядро делит всю доступную физическую память на зоны. В зонах представлены страницы с аналогичными свойствами. Ядро должно учитывать ограничения аппаратного обеспечения, связанные с адресацией памяти:

Некоторые устройства могут выполнять прямой доступ к памяти (DMA,
Direct Memory Access) только в определенную область.

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

ZONE_DMA. Страницы, совместимые с режимом DMA.

ZONE_NORMAL. Страницы, которые отображаются в адресные пространства пользователя обычным способом.

ZONE_HIGHMEM. "Верхняя память", содержащая страницы, которые не могут постоянно отображаться в адресное пространство ядра.
Размер зон сильно зависит от типа процессора. Для платформы i386 размер
ZONE_DMA равен 16 Мбайт, ZONE_HIGHMEM – это все, что лежит выше
32
отметки 896 Мбайт, ZONE_NORMAL – вся остальная память (16-896 Мбайт).
Память в зоне ZONE_HIGHMEM называется "верхней памятью" (high memory), вся остальная память называется "нижней памятью"(low memory). Зона памяти
– это логическое группирование, и оно никак не связано с аппаратным обеспечением. Каждая зона представлена в include/linux/mmzone.h структурой zone. В структуре представлены количество свободных страниц в зоне (unsigned long free_pages), имя зоны (char *name, возможные значения: "DMA", "Normal",
"HighMem") и другие значения. [10]
2.2.2. Интерфейсы для работы с памятью
Функции kmalloc() и kfree()
Функция kmalloc() (объявлена в include/linux/slab.h) аналогична malloc() пространства пользователя за исключением добавленного параметра flags. kmalloc() выделяет участок памяти с заданным размером байт. Выделенные страницы памяти являются виртуально смежными.
void *kmalloc(size_t size, gfp_t flags)
Функция возвращает указатель на область памяти размером не меньше size байт. В случае ошибки возвращается значение NULL.
Функция kfree() (объявлена в include/linux/slab.h) позволяет освободить память, ранее выделенную kmalloc() (вызывать kfree() надо для участков, которые предварительно были выделены kmalloc()).
void kfree(const void *);
Вызов kfree(NULL) специально проверяется и поэтому является безопасным. Пример использования функций:
struct some * p;
p = kmalloc(sizeof(some), GFP_KERNEL);
if (!p)
/*обработчик ошибки*/
33
else {
/*что-то делаем*/
kfree(p);
}
Функции vmalloc() и vfree()
Функция vmalloc() объявлена в include/linux/vmalloc.h. Функция возвращает указатель на виртуально непрерывную область размером не менее size байт. В противном случае функция возвращает NULL.
void *vmalloc(unsigned long size)
Функция работает аналогично kmalloc(), но выделяет страницы, которые виртуально смежные, но необязательно смежные физически. Однако функция vmalloc() менее производительна по сравнению с kmalloc(), поскольку страницы, выделенные vmalloc(), должны отображаться посредством таблиц страниц. Это приводит к менее эффективному использованию буфера TLB (см. раздел "адресное пространство процесса"). Функция vmalloc() используется для выделения очень больших участков памяти. Например, при динамической загрузке модулей ядра память для модуля выделяется с помощью vmalloc().
Для освобождения памяти, выделенной ранее функцией vmalloc(), используется функция vfree() (объявлена в include/linux/vmalloc.h):
void vfree(void *addr)
Функция освобождает участок памяти, начинающийся с адреса addr, выделенный ранее функцией vmalloc(). Ничего не возвращает.
Флаги gfp_mask
Флаги модифицируют работу подсистемы памяти в зависимости от ситуации и разбиты на три категории:

модификаторы операций. Указывают, каким образом ядро должно использовать память.
34


модификаторы зон. Указывают, откуда ядро должно выделять память.

флаги типов. Различные комбинации первых двух категорий.
Все флаги определены в файле include/linux/gfp.h.
Таблица 2.1 – Модификаторы операций выделения памяти
Флаг
Описание
__GFP_WAIT
Операция не может переводить текущий процесс в состояние ожидания
__GFP_HIGH
Операция может обращаться к аварийным запасам
__GFP_IO
Операция может использовать дисковые операции ввода/вывода
__GFP_FS
Операция может использовать операции ввода/вывода файловой системы
__GFP_COLD
Операция должна использовать страницы, содержимое которых не находится в кэше процессора (cache cold)
__GFP_NOWARN
Операция не будет печатать сообщение об ошибках
__GFP_REPEAT
Операция повторит попытку в случае ошибки
__GFP_NOFAIL
Операция будет повторять попытки выделения неограниченное число раз
__GFP_NORETRY
Операция никогда не будет повторять попытку
__GFP_COMP
Добавить метаданные составной (compound) страницы памяти. Используется для поддержки больших страниц памяти
__GFP_ZERO
В случае успеха, вернуть страницу, заполненную нулями
Таблица 2.2 – Модификаторы зоны
Флаг
Описание
__GFP_DMA
Выделять память только из зоны ZONE_DMA
__GFP_HIGHMEM
Выделять память только из зон ZONE_HIGHMEM и
ZONE_NORMAL
Таблица 2.3 – Флаги типов
35

Флаг
Описание
GFP_ATOMIC
Запрос высокоприоритетный и в состояние ожидания переходить нельзя
GFP_NOIO
Запрос может блокироваться, но при его выполнении нельзя выполнять операции дискового ввода/вывода
GFP_NOFS
Запрос на выделение памяти может блокироваться и выполнять дисковые операции ввода/вывода, но запрещено выполнять операции, связанные с файловой системой
GFP_KERNEL
Обычный запрос на выделение, который может блокироваться. Флаг предназначен для использования в коде, который выполняется в контексте процесса
GFP_USER
Обычный запрос на выделение, который может блокироваться. Флаг используется для выделения памяти процессам пространства пользователя
GFP_HIGHUSER Запрос на выделение памяти из зоны ZONE_HIGHMEM, который может блокироваться
GFP_DMA
Запрос на выделение памяти из зоны ZONE_DMA
Таблица 2.4 – Соответствие флагов типов и модификаторов
Флаг
Модификаторы
GFP_ATOMIC
__GFP_HIGH
GFP_NOIO
__GFP_WAIT
GFP_NOFS
__GFP_WAIT | __GFP_IO
GFP_KERNEL
__GFP_WAIT | __GFP_IO | __GFP_FS
GFP_USER
__GFP_WAIT | __GFP_IO | __GFP_FS |
__GFP_HARDWALL
GFP_HIGHUSER __GFP_WAIT | __GFP_IO | __GFP_FS |
__GFP_HARDWALL | __GFP_HIGHMEM
GFP_DMA
__GFP_DMA
36

Большинство операций выделения памяти в ядре используют флаг
GFP_KERNEL. Операции имеют обычный приоритет. При использовании флага GFP_KERNEL ядро попытается вытеснить страницы в swap.
Выделение памяти с флагом GFP_NOIO не будет запускать операций ввода/вывода. С флагом GFP_NOFS могут запускаться операции ввода/вывода, но не могут запускаться операции файловых систем. Эти флаги используются в основном в коде файловых систем или в коде низкоуровневого ввода/вывода.
Флаг GFP_DMA указывает, что память обязательно должна быть выделена из зоны ZONE_DMA. Используется в основном драйверами устройств. [10]
2.2.3. Работа с памятью в ядре
Задание 1
Создать системный вызов upper(), который будет преобразовывать символы строки, хранящейся в адресном пространстве пользователя, к верхнему регистру.
Ход работы
Системный вызов будет принимать три параметра: указатель на строку, которую необходимо преобразовать, указатель на область памяти, куда необходимо поместить результат, и длину этой строки.
asmlinkage long sys_upper(char *src, char *dst, int len)
{
int i;
char *buff;
buff = (char *)kmalloc(len, GFP_KERNEL);
memset(buff, 0, len);
copy_from_user(buff, src, len);
for (i = 0; i < len; i++)
37
if((buff[i] >= 0x61) && (buff[i] <= 0x7A)) buff[i] -= 0x20;
copy_to_user(dst, buff, len);
kfree(buff);
return len;
}
Для новой строки необходимо выделить область памяти в адресном пространстве ядра с помощью функции kmalloc(). В функцию передается флаг
GFP_KERNEL. Функция memset() заполняет область памяти размером len байт значением 0, начиная с позиции, на которую указывает указатель buff.
Функция copy_from_user() копирует исходную строку из адресного пространства пользователя в адресное пространство ядра. Поскольку строка находится в пользовательском адресном пространстве, здесь нельзя использовать простое присвоение (иначе это создаст угрозу безопасности).
Затем производится смена регистра символов, находящихся в диапазоне 97
– 122 (0x61 – 0x7A в шестнадцатеричной системе счисления): английские символы нижнего регистра.
Результат (преобразованная строка) копируется из адресного пространства ядра в пространство пользователя при помощи функции copy_to_user().
Функция принимает в качестве параметров указатель выходную строку (в пространстве ядра), указатель на входную строку (в пространстве пользователя) и длину входной строки.
Объявление функций copy_from_user и copy_to_user() находится в файле include/asm-i386/uacces.h. Однако при подключении заголовочного файла в таком случае тип архитектуры не пишется (пишется просто "asm"). Объяснение этому было дано выше.
#include
После преобразований область памяти необходимо освободить с помощью функции kfree().
38

Поместим реализацию нового системного вызова в файл arch/i386/kernel/new_calls.c. Процесс регистрации нового системного вызова был описан в параграфе 2.1 "Системные вызовы".
Задание 2
Написать программу, которая будет принимать строку символов в качестве аргумента и возвращать преобразованную с помощью нового системного вызова upper() строку.
Ход работы
Исходный код программы на языке C:
#include
#include
#define __NR_upper 319
int main(int argc, char *argv[])
{
char *s1, *s2;
int len, ret;
if (argc > 1)
s1 = argv[1];
else s1 = "This Is (1234) Default TEST String";
len = strlen(s1);
s2 = (char *)malloc(len);
memset(s2, 0, len);
printf("Input string: %s\n", s1);
ret = syscall(__NR_upper, s1, s2, len);
printf("Output string: %s\n", s2);
return 0;
39

}
Программа получает в качестве параметра исходную строку s1. Если никакой строки в качестве параметра не передается (параметр argc равен 1), то строка s1 инициализируется строкой по умолчанию, содержащей символы верхнего, нижнего регистров, символы арабских цифр и символы скобок ("This
Is (1234) Default TEST String"). Программа на выходе печатает исходную строку s1 и конечную строку s2, полученную из исходной преобразованием с помощью системного вызова upper().
# gcc upper.c -o upper; ./upper
Input string: This Is (1234) Default TEST String
Output string: THIS IS (1234) DEFAULT TEST STRING
# ./upper HeLlo
Input string: HeLlo
Output string: HELLO
Код программы находится в файле upper.c приложений. Код системного вызова находится в файле new_calls.c приложений.
Указание
Обязательно знание главы 3 "Системные вызовы". Для успешной компиляции системного вызова upper() необходимо подключение нескольких заголовочных файлов. Все эти файлы указаны в теоретическом разделе.
2.3. Процессы
2.3.1. Процессы в Linux
В Linux используется уникальная реализация потоков: между процессами и потоками нет никакой разницы. Многопоточность организована в виде процессов с общими ресурсами. Иное название для процесса – задание или задача (task). Существуют также потоки в пространстве ядра (kernel thread) –
40
процессы, выполняемые строго в пространстве ядра. Тем не мене они планируются и выполняются как обычные процессы. По возможности, о программах, работающих в ядре говорят как о задачах, для работающих в режиме пользователя используют термин процесс.
В современных операционных системах процессы предусматривают наличие двух виртуальных ресурсов: виртуального процессора и виртуальной памяти. Виртуальный процессор создает иллюзию того, что процесс использует всю компьютерную систему, даже если физическим процессором пользуются другие процессы. Виртуальная память создает иллюзию того, что процесс использует всю доступную физическую память. Потоки в Linux совместно используют одну и ту же виртуальную память, хотя каждый поток получает свой виртуальный процессор.
Процесс начинает свое существование с момента создания. Создание процесса в операционной системе Linux (и во всех Unix – системах) выполняется с помощью системного вызова fork() (буквально – ветвление), который создает новый процесс путем копирования уже существующего.
Процесс, вызывающий fork(), называется порождающим (родительским, parent), новый процесс называют порожденным (дочерним, child). После вызова fork() родительский процесс продолжает выполнение, а порожденный процесс выполняется с места возврата из системного вызова. Часто после ветвления в одном из процессов необходимо выполнить какую-то программу.
Семейство вызовов exec*() позволяет создать новое адресное пространство и загрузить в него новую программу. Выход из программы осуществляется с помощью системного вызова exit(), который завершает процесс и освобождает все занятые им ресурсы. Завершенный процесс переходит в состояние зомби, которое используется для представления завершенного процесса до момента, пока порождающий процесс не удалит его.
Ядро хранит информацию о всех процессах в двухсвязном кольцевом списке, называемом списком задач (task list). Элементами списка являются
41
структуры task_struct, определенные в файле include/linux/sched.h, и называемые дескриптором процесса. Структура занимает около 1,7 Кбайт на
32-разрядных системах, однако она полностью описывает процесс.
Система идентифицирует процессы с помощью уникального значения pid, называемое идентификатором процесса. Для совместимости со старыми версиями Linux и Unix максимальное значение по умолчанию равно 32768 (это значение хранится в /proc/sys/kernel/pid_max).
В Linux существует четкая иерархия процессов. Все процессы являются потомками процесса init (pid = 1). Каждый процесс в системе имеет только одного родителя. Процессы, имеющие общего родителя, называются
родственными (sibling). Структура task_struct содержит указатель на дескриптор родительского процесса (поле struct task_struct *parent) и список порожденных процессов children (поле struct list_head children). Доступ к текущему процессу осуществляется с помощью макроса current.
Системный вызов getpid() возвращает pid текущего процесса:
asmlinkage long sys_getpid(void)
{
return current->tgid;
}
Почему возвращается значение tgid (идентификатор группы потоков)? Для обычных процессов значение tgid совпадает со значением pid. Но поскольку процессы в Linux не отличаются от потоков, то текущим процессом может оказаться на самом деле порожденный поток. При наличии нескольких потоков значение tgid одинаково для всех потоков одной группы. Такая реализация дает возможность получать одинаковое значение для процессов и порожденных ими потоков.
Исполняемый программный код процесса считывается из выполняемого файла (executable) и выполняется в адресном пространстве пользователя. Когда программа выполняет системный вызов, или возникает исключительная
42
ситуация, программа входит в пространство ядра. С этого момента говорят, что ядро выполняет программу от имени процесса и делает это в контексте
процесса. В контексте процесса макрос current является действительным.
Любые обращения к ядру из процесса возможны только через интерфейсы системных вызовов и обработчиков исключительных ситуаций.
Поле state структуры task_struct описывает состояние процесса. Каждый процесс в системе может находиться в одном из пяти состояний:

TASK_RUNNING – процесс готов к выполнению (runnable). Он либо выполняется в данный момент, либо находится в одной из очередей на выполнение.

TASK_INTERRUPTIBLE – процесс приостановлен (находится в состоянии ожидания, sleeping). Процесс в этом состоянии ожидает выполнения некоторого условия. Когда условие выполнится, ядро переводит процесс в состояние TASK_RUNNING. Процесс может возобновить выполнение также при получении им некоторого сигнала.

TASK_UNINTERRUPTIBLE – аналогично TASK_INTERRUPTIBLE, однако процесс не возобновляет выполнение при получении сигнала.
Используется в том случае, если процесс должен работать беспрерывно или когда некоторое событие может возникать достаточно часто.
Используется реже , чем TASK_INTERRUPTIBLE.

TASK_ZOMBIE – процесс завершен, однако дескриптор процесса должен оставаться доступным на тот случай, если родительскому процессу потребуется получить доступ к этому дескриптору. Дескриптор освобождается, когда родительский процесс вызывает wait4().

TASK_STOPPED – выполнение процесса приостановлено. Задача не выполняется, и не может выполняться. Такое может случиться, если процесс получает сигнал SIGSTOP, SIGTSTP, SIGTTOU, или если сигнал приходит в тот момент, когда процесс находится в состоянии отладки.
43

Ядро может менять состояние процесса. Предпочтительно использовать для этого функцию __set_task_state(). Определение функции для платформы i386 находится в include/linux/sched.h:
#define __set_task_state(tsk, state_value)
\
do { (tsk)->state = (state_value); } while (0)
/*задание tsk установить в состояние state_value*/
Создание процесса
В большинстве операционных систем для создания процесса используется метод порождения (spawn). Процесс создается в новом адресном пространстве, в который считывается исполняемый файл, и после этого происходит исполнение процесса. В Unix (и Linux) эти операции разбиты на две функции: fork() и exec().
Традиционно функция fork() делала дубликат всех всех родительских ресурсов и передавала их порожденному. Однако этот подход достаточно неэффективный. В Linux вызов fork() реализован с применением технологии копирования при записи (copy-on-write) страниц памяти. В этом случае родительский и порожденный процессы используют одну копию адресного пространства. Данные помечаются таким образом, что если один из процессов пытается изменить данные, то создается дубликат, и каждый процес получает свою копию. До этого они используются read-only.
Реализация системного вызова fork() для платформы i386 находится в файле arch/i386/kernel/process.c. В Linux он реализован через функцию do_fork()
(функция do_fork() реализована в файле kernel/fork.c).
asmlinkage int sys_fork(struct pt_regs regs)
{
return do_fork(SIGCHLD, regs.esp, ®s, 0, NULL, NULL);
}
44

Функция do_fork() – свидетельство происхождения Linux от Minix, где этот вызов также реализован через функцию do_fork(). Функция do_fork() вызывает функцию copy_process() (реализована в kernel/fork.c) и запускает новый процесс на выполнение. В функции copy_process() выполняются следующие действия:

создается стек для нового процесса с помощью dup_task_struct().
Дескрипторы родительского и порожденного процессов на этом этапе идентичны.

различные поля дескриптора порожденного процесса очищаются или устанавливаются в начальные значения. На этом этапе устанавливается значение поля tgid.

в зависимости от переданных флагов, решается какие ресурсы будут общими, а какие – уникальными для процессов.
После всех действий функция возвращает указатель на новый процесс.
Далее происходит возврат в do_fork(). Если возврат copy_process() происходит успешно, то новый порожденный процесс продолжает выполнение. В зависимости от флагов и их комбинаций, передаваемых в функцию do_fork, можно создавать различные типы процессов. Возможные флаги приведены в таблице 2.5.
Таблица 2.5 – Флаги функции do_fork()



Поделитесь с Вашими друзьями:
1   2   3


База данных защищена авторским правом ©nethash.ru 2019
обратиться к администрации

войти | регистрация
    Главная страница


загрузить материал