Linux Kernel 2.4 Internals
Tigran Aivazian tigran@veritas.com
Когда мы набираем команду 'make zImage' или 'make bzImage' , результат должен записаться в каталоге
arch/i386/boot/zImage или arch/i386/boot/bzImage .
Порядок компиляции следующий :
1. Файлы с расширением .c и .s нужно откомпилировать
в файлы с расширением .o и сгруппировать
в архивы - файлы с расширением .a , используя ar.
2. С помощью ld слинковать полученные обьектные файлы в файл vmlinux
3. С помощью команды nm vmlinux получаем System.map
4. Переходим в каталог arch/i386/boot
5. bootsect.S и setup.S конвертируется в формат 'raw binary'
6. Переходим в каталог arch/i386/boot/compressed и конвертируем
/usr/src/linux/vmlinux в $tmppiggy
7. gzip -9 < $tmppiggy > $tmppiggy.gz
8. Линкуем $tmppiggy.gz в piggy.o с помощью (ld -r).
9. Компилируем head.S и misc.c
10. Линкуем head.o, misc.o и piggy.o в bvmlinux
11. Конвертируем bvmlinux в 'raw binary' bvmlinux.out путем удаления comments .
12. С помощью tools/build из файлов bbootsect, bsetup и compressed/bvmlinux.out
получаем zImage .
Размер бутсектора=512 байт . Размер setup должен быть больше 4-х секторов , но не более 12 КБ :
0x4000 bytes >= 512 + setup_sects * 512 + stack
Верхняя граница для zImage - 2.5M .
Процесс загрузки БИОС-а можно разбить на следующие шаги :
1. При включении питания тактовый генератор отсылает на шину
сигнал #POWERGOOD.
2. Включается CPU
3. Обнуляются основные регистры :
%ds=%es=%fs=%gs=%ss=0, %cs=0xFFFF0000,%eip = 0x0000FFF0 (ROM BIOS POST code).
4. Блокировка прерываний
5. IVT (Interrupt Vector Table) инициализируется в адрес 0.
6. Вызывается функция биоса BIOS Bootstrap Loader , которая загружает
физический адрес 0x7C00.
После чего начинается загрузка бут-сектора .
Бут-сектор может быть : нативным линуксовым , лило-шным ,
или же загрузчик вообще может быть loadlin.
Рассмотрим код в bootsect.s :
SETUPSECS = 4 /* default nr of setup-sectors */
BOOTSEG = 0x07C0 /* original address of boot-sector */
INITSEG = DEF_INITSEG /* we move boot here - out of the way */
SETUPSEG = DEF_SETUPSEG /* setup starts here */
SYSSEG = DEF_SYSSEG /* system loaded at 0x10000 (65536) */
SYSSIZE = DEF_SYSSIZE
Значения констант берутся из хидера boot.h :
#define DEF_INITSEG 0x9000
#define DEF_SYSSEG 0x1000
#define DEF_SETUPSEG 0x9020
#define DEF_SYSSIZE 0x7F00
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 - changed 0xff00 to 0x4000 to use debugger at 0x6400 up (bde). We
66 # wouldn't have to worry about this if we checked the top of memory. Also
67 # my BIOS can be configured to put the wini drive tables in high memory
68 # instead of in the vector table. The old stack might have clobbered the
69 # drive table.
70 go: movw $0x4000-12, %di # 0x4000 is an arbitrary value >=
71 # length of bootsect + length of
72 # setup + room for stack;
73 # 12 is disk parm size.
74 movw %ax, %ds # ax and es already contain INITSEG
75 movw %ax, %ss
76 movw %di, %sp # put stack at INITSEG:0x4000-12.
В строчках с номера 54-63 код бутсектора копируется с 0x7C00 в 0x90000 .
Достигается это следующим образом : в регистр si пишем адрес-источник 0x7C0:0 ,
в регистр di пишем адрес-назначение 0x90000 ,
пишем в cx=256 - или 512 байт - размер сектора , очищаем флаг DF и копируем .
Дальше в строках 70-76 идет настройка стека .
Затем нужно настроить таблицу секторов , которая позволит работать сразу с 36 секторами.
89 # Segments are as follows: ds = es = ss = cs - INITSEG, fs = 0,
90 # and gs is unused.
91 movw %cx, %fs # set fs to 0
92 movw $0x78, %bx # fs:bx is parameter table address
93 pushw %ds
94 ldsw %fs:(%bx), %si # ds:si is source
95 movb $6, %cl # copy 12 bytes
96 pushw %di # di = 0x4000-12.
97 rep # don't need cld -> done on line 66
98 movsw
99 popw %di
100 popw %ds
101 movb $36, 0x4(%di) # patch sector count
102 movw %di, %fs:(%bx)
103 movw %es, %fs:2(%bx)
Дальше грузимся с дискеты по адресу 0x90200 ($INITSEG:0x200),
что делается с помощью прерывания 0x13 .
107 load_setup:
108 xorb %ah, %ah # reset FDC
109 xorb %dl, %dl
110 int $0x13
111 xorw %dx, %dx # drive 0, head 0
112 movb $0x02, %cl # sector 2, track 0
113 movw $0x0200, %bx # address = 512, in INITSEG
114 movb $0x02, %ah # service 2, "read sector(s)"
115 movb setup_sects, %al # (assume all on head 0, track 0)
116 int $0x13 # read it
117 jnc ok_load_setup # ok - continue
118 pushw %ax # dump error code
119 call print_nl
120 movw %sp, %bp
121 call print_hex
122 popw %ax
123 jmp load_setup
124 ok_load_setup:
После чего попадаем на строку 124 .
Затем грузим kernel image в физический 0x10000 . Это в нижней памяти .
После загрузки , переходим в $SETUPSEG:0 (arch/i386/boot/setup.S).
Загруженный kernel перемещается с 0x10000 в 0x1000 .
Далее вызывается функция decompress_kernel() .
Kernel разворачивается по адресу 0x100000 и запускается .
Для этого используется прерывание 0x15.
Для загрузки бут-сектора можно использовать другой путь - ЛИЛО .
Этот загрузчик позволяет выбирать между несколькими версиями линукса , при этом можно передавать
внешние командные параметры .
Далее происходит т.н. высокоуровневая инициализация :
устанавливаются сегментные регистры
%ds = %es = %fs = %gs = __KERNEL_DS = 0x18 ,
инициализируются page tables ,
разрешается paging путем установки бита PG в %cr0 ,
очищается BSS ,
копируются 2kб bootup-параметров ,
проверяется тип цпу ,
вызывается start_kernel(), которая написана уже на си .
Она блокирует ядро , выполняет первичный setup ,
на экран выводится версия линукс ,
инициализируются таблицы traps , irqs , шедулятор ,
инициализируется консоль ,
если поддерживаются модули , инициализируется их динамическая поддержка ,
инициализируется профайлер ,
разрешаются прерывания ,
вычисляется BogoMips,
вычисляется mem_init(),
инициализация базовых структур procfs,
fork_init(),
инициализация VFS, VM, buffer cache,
создается kernel thread init() и запускаетс с pid=0.
Этот трэд вызывает do_basic_setup(),который вызывает do_initcalls()
с вызовом базовых независимых функций.
Порядок вызова этих функций зависит от того , в каком порядке
они записаны в makefile .
Если 2 функции зависят друг от друга и при этом статически
вкомпилены в ядро ,
становится важным порядок их компиляции .
При инициализаци операционной системы , большинство кода и данных впоследствии становится ненужным .
Ядро компилируется как ELF binary.
В ядре есть 2 макроса для инициализации ядра , которые определены в include/linux/init.h :
#define __init __attribute__ ((__section__ (".text.init")))
#define __initdata __attribute__ ((__section__ (".data.init")))
Это означает , что код будет размещен в специальную ELF-секцию памяти .text.init .
При загрузке будет вызвана функция free_initmem(), которая освободит память между __init_begin и __init_end.
Это около 260 КБ памяти .
Процессы .
Каждый процесс создает структуру task_struct .
Максимальное количество процессов ограничено только размером физической памяти :
max_threads = mempages / (THREAD_SIZE/PAGE_SIZE) / 2
или что то же самое - число физических страниц , разделенное на 4 .
Можно набрать команду :
cat /proc/sys/kernel/threads-max
Яковлев С: При обьеме памяти в 400 метров на моем компьютере эта команда выдала число 6143
Процессы обьединены в хэш-таблицу по id-шникам , а также в двойной список - linked list ,
состоящий из указателей p->next_task И p->prev_task .
Хэш-таблица называется pidhash[] и прописана в include/linux/sched.h :
#define PIDHASH_SZ (4096 >> 2)
extern struct task_struct *pidhash[PIDHASH_SZ];
#define pid_hashfn(x) ((((x) >> 8) ^ (x)) & (PIDHASH_SZ - 1))
По этой таблице легко найти процесс по его id-шнику с помощью find_task_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;
}
Вставка и удаление процесса выполняется с помощью hash_pid() и unhash_pid()
Двойной список реализован для прохода по всем процессам . Для этого существует
макрос for_each_task() , прописанный в include/linux/sched.h :
#define for_each_task(p) \
for (p = &init_task ; (p = p->next_task) != &init_task ; )
Для модификации нужно изменить параметр tasklist_lock , кокотрый по умолчанию установлен на чтение .
На время модификации все прерывания должны быть запрещены , что очень важно .
Сама структура task_struct является комбинацией 2-х структур - 'struct proc' и 'struct user' .
Обычно первая половинка постоянно висит в памяти , а вторая подгружается при работе , хотя it depends .
Структура task_struct продекларирована в include/linux/sched.h , размер 1680 bytes
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
volatile говорит о том , что изменение состояния процесса может быть асинхронным , например по прерыванию .
TASK_RUNNING - процесс может быть поставлен в очередь ,
по умолчанию эта опция выключена
TASK_INTERRUPTIBLE - процесс в спячке и может быть активирован
TASK_UNINTERRUPTIBLE - процес в спячке , причем навсегда
TASK_ZOMBIE - процесс завершился , но родитель это не отловил
TASK_STOPPED - процесс остановлен
TASK_EXCLUSIVE - при пробуждении из спячки этот процесс из общей кучи
будет выдернут вне очереди .
Процесс имеет флаги :
unsigned long flags; /* per process flags, defined below */
/*
* Per process flags
*/
#define PF_ALIGNWARN 0x00000001 /* Print alignment warning msgs */
/* Not implemented yet, only for 486*/
#define PF_STARTING 0x00000002 /* being created */
#define PF_EXITING 0x00000004 /* getting shut down */
#define PF_FORKNOEXEC 0x00000040 /* forked but didn't exec */
#define PF_SUPERPRIV 0x00000100 /* used super-user privileges */
#define PF_DUMPCORE 0x00000200 /* dumped core */
#define PF_SIGNALED 0x00000400 /* killed by a signal */
#define PF_MEMALLOC 0x00000800 /* Allocating memory */
#define PF_VFORK 0x00001000 /* Wake up parent in mm_release */
#define PF_USEDFPU 0x00100000 /* task used FPU this quantum (SMP) */
Поле p->mm есть адрес структуры процесса , p->active_mm - то же самое для процесса ядра .
Эти поля позволяют минимизировать переключения между тасками .
Поле p->fs структуры включает информацию о рутовой директории и текущей директории .
Структура также имеет reference count в случае , если процесс клонируется с помощью
системного вызова clone(2).
Создание процессов и kernel threads
В линуксе есть 3 типа процессов :
фоновые задачи - idle thread(s),
kernel threads,
user tasks.
idle thread создаются при загрузке и имеют pid=0 и затем "вручную" размножается для каждого процессора
вызовом fork_by_hand() из arch/i386/kernel/smpboot.c.
kernel threads создается с помощью clone(2) , не имеют адреса p->mm = NULL и имеют напрямую доступ
к kernel address space .
user tasks создаются как clone(2) , так и fork(2) .
Функции системных вызовов под линукс имеют префикс sys_ и обладают вложенностью ,
например sys_exit() вызывает в свою очередь do_exit() , которую можно найти в kernel/exit.c.
do_exit() блокирует ядро , вызывает шедулер , устанавливает TASK_ZOMBIE ,
уведомляет об этом все порожденные процессы , закрывает все файлы .
Роль шедулера - распределять ресурсы процессора между процессами . Его реализация лежит
в kernel/sched.c . В структуре процесса есть поля , с которыми работает шедулер :
p->need_resched - подготовка процесса к вызову
p->counter - счетчик , при установке которого в 0
включается p->need_resched
p->priority - приоритет процесса , который может быть изменен
через системный вызов nice(2)
p->rt_priority - real-time приоритет
p->policy - тип процесса : может быть SCHED_OTHER , SCHED_FIFO ,
SCHED_RR , SCHED_YIELD .
Несмотря на кажущуюся простоту , шедулер имеет много функций . В каждой версии он переписывается
практически с нуля .
Особенности шедулера :
текущий процесс должен иметь конкретный p->active_mm , иначе что-то не то
переменные prev и this_cpu - это текущий процесс и текущий процессор
функция schedule() не может быть вызвана из прерывания
Реализация linked list
Очередь процессов построена с помощью doubly-linked list , которые определены в include/linux/list.h.
Базовая структура - list_head :
struct list_head {
struct list_head *next, *prev;
};
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
#define INIT_LIST_HEAD(ptr) do { \
(ptr)->next = (ptr); (ptr)->prev = (ptr); \
} while (0)
#define list_entry(ptr, type, member) \
((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
#define list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); pos = pos->next)
Первые 3 макроса служат для инициализации пустого списка,при этом оба указателя указывают на себя .
Они создают 3 экземпляра , каждый из которых служит для различных целей.
4-й макрос list_entry() дает доступ к индивидуальным элементам списка , например :
struct super_block {
...
struct list_head s_files;
...
} *sb = &some_super_block;
struct file {
...
struct list_head f_list;
...
} *file;
struct list_head *p;
for (p = sb->s_files.next; p != &sb->s_files; p = p->next) {
struct file *file = list_entry(p, struct file, f_list);
do something to 'file'
}
С помощью list_for_each() шедулер просматривает очередь процессов :
static LIST_HEAD(runqueue_head);
struct list_head *tmp;
struct task_struct *p;
list_for_each(tmp, &runqueue_head) {
p = list_entry(tmp, struct task_struct, run_list);
if (can_schedule(p)) {
int weight = goodness(p, this_cpu, prev->active_mm);
if (weight > c)
c = weight, next = p;
}
}
Модернизация task list выполняется с помощью list_del()/list_add()/list_add_tail() .
Следующий пример показывает реализацию механизма этого процесса :
static inline void del_from_runqueue(struct task_struct * p)
{
nr_running--;
list_del(&p->run_list);
p->run_list.next = NULL;
}
static inline void add_to_runqueue(struct task_struct * p)
{
list_add(&p->run_list, &runqueue_head);
nr_running++;
}
static inline void move_last_runqueue(struct task_struct * p)
{
list_del(&p->run_list);
list_add_tail(&p->run_list, &runqueue_head);
}
static inline void move_first_runqueue(struct task_struct * p)
{
list_del(&p->run_list);
list_add(&p->run_list, &runqueue_head);
}
Очередь
Процесс посылает запрос ядру на собственную активацию . Этот запрос может быть либо принят ,
либо отклонен , во втором случае он становится "спящим" и ждет лучших времен .
Механизм ожидания называется 'wait queue'.
Имеется специальный флаг TASK_EXCLUSIVE .
Для работы с очередью можно использовать
sleep_on/sleep_on_timeout/interruptible_sleep_on/interruptible_sleep_on_timeout,
add/remove_wait_queue , wake_up/wake_up_interruptible .
Например , когда page allocator нужно освободить память , вызывается из спячки демон kswapd .
В качестве примера автономной очереди можно рассмотреть взаимодействие процессов пользователя ,
работающих с некими ресурсами , с ядром с использованием механизма прерываний :
static DECLARE_WAIT_QUEUE_HEAD(rtc_wait);
void rtc_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
spin_lock(&rtc_lock);
rtc_irq_data = CMOS_READ(RTC_INTR_FLAGS);
spin_unlock(&rtc_lock);
wake_up_interruptible(&rtc_wait);
}
В функции данные получают за счет прерывания с какого-то специфического порта ,
после чего проверяют очередь rtc_wait
Далее идет реализация системного вызова read(2):
ssize_t rtc_read(struct file file, char *buf, size_t count, loff_t *ppos)
{
DECLARE_WAITQUEUE(wait, current);
unsigned long data;
ssize_t retval;
add_wait_queue(&rtc_wait, &wait);
current->state = TASK_INTERRUPTIBLE;
do {
spin_lock_irq(&rtc_lock);
data = rtc_irq_data;
rtc_irq_data = 0;
spin_unlock_irq(&rtc_lock);
if (data != 0)
break;
if (file->f_flags & O_NONBLOCK) {
retval = -EAGAIN;
goto out;
}
if (signal_pending(current)) {
retval = -ERESTARTSYS;
goto out;
}
schedule();
} while(1);
retval = put_user(data, (unsigned long *)buf);
if (!retval)
retval = sizeof(unsigned long);
out:
current->state = TASK_RUNNING;
remove_wait_queue(&rtc_wait, &wait);
return retval;
}
Здесь мы сначала декларируем указатель на текущий пользовательский процесс ,
который добавляем в очередь rtc_wait . Данный процесс помечается как TASK_INTERRUPTIBLE.
Если есть данные , они копируются в буффер , маркируем TASK_RUNNING , удаляем себя из очереди .
Также проверяются системные вызовы sigaction(2).
Далее , процесс засыпает до следующего прерывания .
Механизм system calls реализован двояко :
вызов lcall7/lcall27
прерывание int 0x80
Первый вариант используется в солярисе . Второй вариант - основной .
При загрузке системы функция arch/i386/kernel/traps.c:trap_init() инициализирует таблицу IDT ,
при этом 0x80 настраивается на system_call в arch/i386/kernel/entry.S .
При вызове системной функции приложение выполняет команду 'int 0x80' .
При этом :
читаются регистры
регистры %ds и %es устанавливаются в KERNEL_DS
читаются аргументы из стека
Для системных вызовов может быть до 6 аргументов .
Они передаются через регистры %ebx, %ecx, %edx, %esi, %edi , %ebp .
Spinlocks
На начальном этапе развития линукса , актуальной была проблема расшаренных ресурсов .
Поддержка SMP была добавлена в версии 1.3.42 в 1995 году .
При возникновении коллизии между процессом и прерыванием , применяется обычный механизм сохранения :
unsigned long flags;
save_flags(flags);
cli();
/* critical code */
restore_flags(flags);
Это работает для одного ЦПУ , но не работает для нескольких. Для этого существуют spinlocks.
Существуют 3 типа spinlocks
vanilla (basic)
read-write
big-reader spinlocks
Read-write spinlocks используется тогда , когда к малому числу ресурсов обращается большое число процессов,
Если происходит запись , то писать может только один процесс , все остальные читающие при этом
полностью блокируются .
Spinlocks бывают 3 типов :
1. spin_lock()/spin_unlock() - используются тогда , когда нет прерываний
2. spin_lock_irq()/spin_unlock_irq() - используются с прерываниями
3. spin_lock_irqsave()/spin_unlock_irqrestore() - комбинированный вариант
Пример :
spinlock_t my_lock = SPIN_LOCK_UNLOCKED;
my_ioctl()
{
spin_lock_irq(&my_lock);
/* critical section */
spin_unlock_irq(&my_lock);
}
my_irq_handler()
{
spin_lock(&lock);
/* critical section */
spin_unlock(&lock);
}
Semaphores
Семафоры служат для блокировки пользовательских процессов .
Существуют 2 типа семафоров :
basic
read-write
Пример исользования семафора в 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;
}
Loading Modules
Linux есть и всегда будет монолитным, это означает, что все подсистемы работают в привелигированном режиме и
используют общее адресное пространство; связь между ними выполняется через обычные C-функции.
Однако, не смотря на то, что выделение функциональности ядра в отдельные "процессы" (как это делается в ОС
на микро-ядре) - определенно не лучшее решение, тем не менее, в некоторых случаях, желательно наличие
поддержки динамически загружаемых модулей (например: на машинах с небольшим объемом памяти или для ядер,
которые автоматически подбирают (auto-probing) взаимоисключающие драйверы для ISA устройств). Поддержка
загружаемых модулей устанавливается опцией CONFIG_MODULES во время сборки ядра.
Поддержка автозагружаемых модулей через механизм request_module() определяется отдельной опцией (CONFIG_KMOD).
Ниже приведены функциональные возможности, которые могут быть реализованы как загружаемые модули:
1. Драйверы символьных и блочных устройств.
2. Terminal line disciplines.
3. Виртуальные (обычные) файлы в /proc и
в devfs (например /dev/cpu/microcode и /dev/misc/microcode).
4. Обработка двоичных форматов файлов (например ELF, a.out, и пр.).
5. Обработка доменов исполнения (например Linux, UnixWare7, Solaris, и пр.).
6. Файловые системы.
7. System V IPC.
А здесь то, что нельзя вынести в модули (вероятно потому, что это не имеет смысла):
1. Алгоритмы планирования.
2. Политики VM (VM policies).
3. Кэш буфера, кэш страниц и другие кзши.
Решение о поддержки модулей принимается на этапе компиляции и определяется опцией CONFIG_MODULES .
В линуксе есть несколько системных вызовов для работы с модулями :
1 caddr_t create_module
2 long init_module
3 long delete_module
4 long query_module
Из командной строки возможны команды :
insmod - загрузка 1 модуля
modprobe - загрузка дерева модулей
rmmod - выгрузка модуля
modinfo: - получение информации о модуле
kernel-функция request_module(name) создает при этом трэд
Пример загрузки модуля в вызове mount(2) system call :
static struct file_system_type *get_fs_type(const char *name)
{
struct file_system_type *fs;
read_lock(&file_systems_lock);
fs = *(find_filesystem(name));
if (fs && !try_inc_mod_count(fs->owner))
fs = NULL;
read_unlock(&file_systems_lock);
if (!fs && (request_module(name) == 0)) {
read_lock(&file_systems_lock);
fs = *(find_filesystem(name));
if (fs && !try_inc_mod_count(fs->owner))
fs = NULL;
read_unlock(&file_systems_lock);
}
return fs;
}
|
Sezon | Нормалек !!! 2007-06-20 22:35:25 | |
|