GrabDuck

Linux Kernel Internals: Booting

:

Вперед Оглавление Назад


Этот раздел объясняет шаги, принимаемые в течение трансляции Linux ядра. Процесс построения зависит от архитектуры, так что я хотел бы подчеркнуть, что мы рассматриваем только формирование ядра Linux/x86.

Когда пользователь вводит 'make zImage' или 'make bzImage' создаваемый в результате самозагружающийся ядерный образ будет сохранен как arch/i386/boot/zImage или arch/i386/boot/bzImage соответственно. Как создается образ:

  1. Исходные файлы на C и ассемблере компилируются в объектный формат ELF (.o) и некоторые из них будут сгруппированы логически в архивы (.a), используя утилиту архивации ar(1).
  2. Используя ld(1), вышеупомянутые .o и .a будут связаны в vmlinux, который является статически связанным, non-stripped ELF 32-bit LSB 80386 исполняемым файлом.
  3. System.map создается с помощью команды nm vmlinux, несоответствующие или неинтересные символы вырезаются.
  4. Выполняется переход в каталог arch/i386/boot.
  5. Ассемблерный код загрузочного сектора bootsect.S обрабатывается с опредением -D__BIG_KERNEL__ или без него, в зависимости от того, является ли результат bzImage или zImage. После этого получается файл bbootsect.s или bootsect.s.
  6. bbootsect.s будет собран и затем преобразован в формат 'raw binary' под именем bbootsect (для bootsect.s все так же, только результат называется bootsect).
  7. Код настройки setup.S (setup.S включая video.S) преобразуется в bsetup.s для bzImage или в setup.s для zImage. Таким же образом, как и разница в коде загрузчика, здесь разница помечается опредением -D__BIG_KERNEL__ для bzImage. Результат затем будет преобразован в форму 'raw binary' под именем bsetup.
  8. Выполняется переход в каталог arch/i386/boot/compressed и /usr/src/linux/vmlinux конвертируется в $tmppiggy (имя временного файла) в формате raw binary, удалением секций .note и .comment из ELF.
  9. Затем результат сжимается: gzip -9 < $tmppiggy>$tmppiggy.gz
  10. Выполняется компоновка $tmppiggy.gz в ELF relocatable (ld -r) piggy.o.
  11. Компилируется подпрограмма сжатия из файлов head.S и misc.c (все еще в каталоге arch/i386/boot/compressed) в объекты ELF head.o и misc.o.
  12. Комопнуются в один файл head.o, misc.o и piggy.o в bvmlinux (или в vmlinux для zImage, не перепутайте это с /usr/src/linux/vmlinux!). Обратите внимание на различие между параметром -Ttext 0x1000, используемом для vmlinux и -Ttext 0x100000 для bvmlinux, то есть для образа bzImage загрузчик располагается выше.
  13. Преобразуется bvmlinux в 'raw binary' bvmlinux.out через удаление секций ELF .note и .comment.
  14. Выполняется возврат в каталог arch/i386/boot и, используя инструментальные средства tools/build, склеиваются вместе bbootsect, bsetup и compressed/bvmlinux.out в файл bzImage (для zImage все так же). Это пишет важные переменные, например, setup_sects и root_dev в конец загрузочного сектора.
Размер загрузочного сектора всегда равен 512 байт. Размер установки должен быть больше, чем 4 сектора, но ограничен примерно 12 КБ: 0x4000 байт>=512+ setup_sects*512+участок памяти для стека.

Мы увидим позже, где это ограничение происходит.

Верхнее ограничение размера bzImage около 2.5M для загрузки с LILO и 0xFFFF параграфов (0xFFFF0=1048560 байт) для загрузки raw image, например, с дискеты или CD-ROM.

Обратите внимание, что, в то время как инструментальные средства tools/build утверждают размер загрузочного сектора, ядерного изображения и блока настройки, это не проверяет верхний предел кода настройки. Следовательно можно легко сформировать ошибочное ядро, добавив большой ".space" в конце setup.S.

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

  1. BIOS выбирает устройство начальной загрузки.
  2. BIOS читает загрузочный сектор из устройства начальной загрузки.
  3. Загрузочный сектор загружает установку, подпрограммы декомпрессии и сжатый образ ядра.
  4. Ядро распаковывается в защищенном режиме.
  5. Инициализация низкого уровня выполняется с помощью asm-кода.
  6. Происходит инициализация высокого уровня.
  1. Питание запускает генератор часов и утверждает сигнал #POWERGOOD на шине.
  2. Активизируется линия CPU #RESET (CPU сейчас в режиме real 8086).
  3. %ds=%es=%fs=%gs=%ss=0, %cs=0xFFFF0000, %eip=0x0000FFF0 (ROM BIOS POST).
  4. Все проверки POST выполняются с заблокированными прерываниями.
  5. IVT (Interrupt Vector Table, таблица векторов прерываний) инициализирована по адресу 0.
  6. Функция BIOS Bootstrap Loader (загрузчик системного загрузочного сектора) вызывается через int 0x19, где %dl хранит устройство начальной загрузки (номер диска). Это загружает дорожку 0, сектор 1 на физический адрес 0x7C00 (0x07C0:0000).

Загрузочный сектор (bootsector), используемый, чтобы загрузить ядро Linux, может быть таким:

  • Загрузочный сектор Linux (arch/i386/boot/bootsect.S).
  • LILO (или другой загрузчик).
  • Вообще отсутствует (используется программа, подобная loadlin).

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


29 SETUPSECS = 4                /* default nr of setup-sectors */
30 BOOTSEG   = 0x07C0           /* original address of boot-sector */
31 INITSEG   = DEF_INITSEG      /* we move boot here - out of the way */
32 SETUPSEG  = DEF_SETUPSEG     /* setup starts here */
33 SYSSEG    = DEF_SYSSEG       /* system loaded at 0x10000 (65536) */
34 SYSSIZE   = DEF_SYSSIZE      /* system size: # of 16-byte clicks */

Числа слева представляют собой номера строк из файла bootsect.S. Значения DEF_INITSEG, DEF_SETUPSEG, DEF_SYSSEG и DEF_SYSSIZE принимаются из include/asm/boot.h:


/* Don't touch these, unless you really know what you're doing. */
#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 - 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. Это достигается так:

  1. %ds:%si устанавливается в $BOOTSEG:0 (0x7C0:0=0x7C00).
  2. %es:%di устанавливается в $INITSEG:0 (0x9000:0=0x90000).
  3. Устанавливается в число 16-битных слов в %cx (256 слов=512 байт=1 сектор).
  4. Очищается флажок DF (direction) в EFLAGS для автоприращения адресов (cld).
  5. Копируется 512 байт (rep movsw).

Причина того, почему этот код не использует rep movsd сводится к определению .code16).

Строка 64 обходит метку go: в недавно сделанной копии загрузочного сектора, то есть в сегменте 0x9000. Эта и следующие три команды (строки 64-76) готовят стек в $INITSEG:0x4000-12, то есть %ss=$INITSEG (0x9000) и %sp=0x3FEE (0x4000-12). Это причина ограничения размера установки ядра, о котором я писал выше.

Строки 77-103 исправляют дисковую таблицу параметров для первого диска, чтобы использовать многосекторное чтение:


    77  # Many BIOS's default disk parameter tables will not recognise
    78  # multi-sector reads beyond the maximum sector number specified
    79  # in the default diskette parameter tables - this may mean 7
    80  # sectors in some cases.
    81  #
    82  # Since single sector reads are slow and out of the question,
    83  # we must take care of this by creating new parameter tables
    84  # (for the first disk) in RAM.  We will set the maximum sector
    85  # count to 36 - the most we will encounter on an ED 2.88.
    86  #
    87  # High doesn't hurt.  Low does.
    88  #
    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)

Контроллер гибкого диска сброшен, используя обслуживание BIOS int 0x13 (функция 0, reset FDC), и сектора установки загружены сразу после кода загрузочного сектора, то есть в физическом адресе 0x90200 ($INITSEG:0x200), снова используя сервис BIOS int 0x13, функция 2 (read sector(s)). Эти действия реализованы строками 107-124:


   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:

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

При загрузке setup_sects секторов кода настройки обходится метка ok_load_setup:.

Затем мы продолжаем чтение данных, чтобы загрузить сжатый образ ядра на адрес 0x10000. Это выполнено, чтобы сохранить микропрограммные области данных в нижней памяти (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 сектора кода настройки, а код из них должен сам подкачать все остальное, если нужно. Кроме того, этот код должен заботиться о различных комбинациях типа и версии загрузчика, правильной обработке 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 не исчерпает нижнюю память при считывании данных с диска.

Специализированные загрузчики (например, LILO) имеют ряд преимуществ перед чисто Linux-загрузчиком (bootsector):

  1. Возможность выбора загрузки одного из нескольких ядер Linux или одной из нескольких ОС.
  2. Возможность передачи параметров загрузки ядру (существует патч BCP, который добавляет такую же возможность и к чистому bootsector+setup).
  3. Возможность загружать большие ядра (bzImage) до 2,5M (против обычного 1M).

Старые версии LILO (v17 и более ранние) не в состоянии загрузить ядро bzImage. Более новые версии (не старше 2-3 лет) используют ту же методику, что и bootsect+setup, для перемещения данных из нижней в верхнюю память посредством функций BIOS. Отдельные разработчики (особенно Peter Anvin) выступают за отказ от поддержки ядер zImage. Тем не менее, поддержка zImage остается в основном из-за (согласно Alan Cox) существования некоторых BIOS, которые не могут загружать ядра bzImage, в то время как zImage грузятся ими без проблем.

В заключение, LILO передает управление setup.S, а далее загрузка продолжается как обычно.

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

  1. Устанавливаются сегментные регистры (%ds=%es=%fs=%gs=__KERNEL_DS=0x18).
  2. Инициализируются таблицы страниц.
  3. Разрешается листание страниц, установкой бита PG в %cr0.
  4. Обнуляется BSS (на SMP это действие выполняет только первый CPU).
  5. Копируются первые 2k загрузочных параметров (командная строка ядра).
  6. Проверяется тип CPU, используя EFLAGS и, если возможно, cpuid, позволяющие обнаружить процессор 386 и выше.
  7. Первый CPU вызывает start_kernel(), все остальные вызовут arch/i386/kernel/smpboot.c:initialize_secondary(), если переменная ready=1, это только переустанавливает esp/eip и ничего не вернет.

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

  1. Выполняется глобальная блокировка (необходимая для того, чтобы через процесс инициализации проходил всегда только один CPU).
  2. Выполняются платформно-зависимые настройки (анализируется раскладка памяти, копируется командная строка и прочее).
  3. Выводится баннер ядра, который содержит версию компилятора, использованную при сборке, версию ядра и прочие данные о нем в кольцевой буфер для сообщений. Текст баннера задается в переменной linux_banner, определенной в init/version.c. Текст этот также можно вывести на экран командой cat /proc/version.
  4. Инициализируются вспомогательные структуры.
  5. Инициализируются irq.
  6. Инициализируются данные, требуемые для планировщика.
  7. Инициализируются данные для хранения времени.
  8. Инициализируется подсистема программных прерываний.
  9. Обрабатываются опции загрузки.
  10. Инициализируется консоль.
  11. Если поддержка модулей компилировалась в ядро, инициализируется динамический загручик модулей.
  12. Если указан параметр "profile=", инициализируются буферы профилирований.
  13. kmem_cache_init() запускает распределитель памяти.
  14. Разрешаются любые прерывания.
  15. Вычисляется значение BogoMips для этого CPU.
  16. Вызов mem_init() вычисляет параметры max_mapnr, totalram_pages и high_memory на основании чего печатает строку "Memory: ... ".
  17. Вызов kmem_cache_sizes_init() завершает запуск распределителя памяти.
  18. Инициализируются структуры данных, используемые procfs.
  19. fork_init() созданный uid_cache, запускает max_threads, исходя из доступного объема памяти, и конфигурирует RLIMIT_NPROC для init_task, чтобы он был равен max_threads/2.
  20. Создаются различные кэши, необходимые для VFS, VM, буферов и прочего.
  21. Если поддержка System V IPC компилируется в ядро, инициализируется IPC. Обратите внимание, что для System V shm это включает установку внутреннего (in-kernel) образца файловой системы shmfs.
  22. Если включена поддержка квот в ядре, создается и инициализируется специальный кэш для них.
  23. Запускается проверка архитектурных ошибок, которая по мере сил своих старается их обойти. Сравнение разных архитектур показывает, что ia64 не имеет никаких ошибок, а ia32 имеет несколько, например, "f00f bug", которая проверена только, если ядро компилируется для процессора ниже, чем 686.
  24. Устанавливается флажок, чтобы указать, что планировщик должен вызваться при следующей возможности, и создается процесс init(), который выполняет execute_command, заданную параметром загрузки init=, или пробует выполнить процессы /sbin/init, /etc/init, /bin/init, /bin/sh (именно в этом порядке). Если ничего не получилось, выдается предложение применить параметр init=.
  25. Система входит в неактивный цикл, это неактивный процесс с pid=0.

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

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

На SMP BP проходит нормальную последовательность загрузки пока не достигает шага start_kernel(), а затем выполняет smp_init() и src/i386/kernel/smpboot.c:smp_boot_cpus(). Функция smp_boot_cpus() входит в цикл для каждого apicid (пока не достигнут NR_CPUS) и вызывает do_boot_cpu(). Здесь do_boot_cpu() создает (то есть, fork_by_hand) неактивную задачу для целевого процессора и пишет в известных расположениях, определенных спецификацией Intel MP (0x467/0x469), EIP кода трамплина, найденный в trampoline.S. Затем это генерирует STARTUP IPI на целевой процессор, который заставляет AP выполнить код в trampoline.S.

Загружаемый CPU создает копию кода трамплина для каждого CPU в нижней памяти. Код AP пишет волшебный номер в собственном коде, который проверен BP, чтобы удостовериться, что AP выполняет код трамплина. Требование, чтобы код трамплина был в нижней памяти, предписано спецификацией Intel MP.

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

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

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

Когда операционная система инициализирует себя, большинство кода и структур данных никогда не понадобятся ей снова. Большинство операционных систем (BSD, FreeBSD и т. д.) не могут распорядиться этой ненужной информацией, таким образом тратя впустую драгоценную физическую ядерную память. Оправдание, которое они используют (см., например, McKusick's 4.4BSD book) таково: релевантный код распространен вокруг различных подсистем так, что невозможно освободить его. Linux, конечно, не может использовать такие оправдания потому, что под Linux действует другое правило: если что-то возможно в принципе, значит это уже выполнено, или кто-то работает над этим.

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

  • __init для кода инициализации и
  • __initdata для данных.

Они оценивают спецификаторы атрибутов gcc (также известные как gcc magic) как определено в файле include/linux/init.h:

#ifndef MODULE
#define __init        __attribute__ ((__section__ (".text.init")))
#define __initdata    __attribute__ ((__section__ (".data.init")))
#else
#define __init
#define __initdata
#endif

Это означает, что, если код компилируется статически в ядро (то есть, MODULE не определен), то это будет помещено в специальный раздел ELF .text.init, который объявлен в карте компоновщика в файле arch/i386/vmlinux.lds. Иначе (то есть, если это модуль), макрокоманды ничего не дадут.

Что случается в течение начальной загрузки? Поток ядра init (функция init/main.c:init()) вызывает архитектурно-специфическую функцию free_initmem(), которая освобождает все страницы между адресами __init_begin и __init_end.

На типичной системе (мое автоматизированное рабочее место), это кончается освобождением приблизительно 260K памяти.

Функции, зарегистрированные через module_init(), помещены в .initcall.init, который также освобожден в статическом случае. Текущая тенденция в Linux: при проектировании подсистемы (не обязательно модулей), следует обеспечить точки входа и выхода на ранних стадиях проекта так, чтобы в будущем рассматриваемая подсистема могла быть переделана в модуль, если это понадобится. Пример этого: pipefs, см. файл fs/pipe.c. Даже если данная подсистема никогда не станет модулем, например, bdflush (см. fs/buffer.c), это все еще хорошо и опрятно, чтобы использовать макрокоманду module_init() вместо функции инициализации при вызове функции.

Имеются еще две макрокоманды, которые работают подобным способом, вызывая __exit и __exitdata, но они более непосредственно связаны с поддержкой модулей, а, следовательно, будут объясняться в более позднем разделе.

Рассмотрим то, что случается с командной строкой, переданной ядру в течение начальной загрузки:

  1. LILO (или BCP) принимает командную строку, используя сервис BIOS для клавиатуры и сохраняет ее в известном расположении в физической памяти, так же, как и специальную сигнатуру, говорящую, что там есть имеющая силу командная строка.
  2. arch/i386/kernel/head.S копирует первые 2k этого блока данных в zeropage. Обратите внимание, что текущая версия (21) LILO прерывает командную строку после 79 байт. Это нетривиальная ошибка в LILO (она происходит, когда допускается поддержка большого EBDA), и Werner обещал это когда-нибудь исправить. Если Вы действительно должны передать командные строки длиннее 79 байтов, то Вы можете использовать BCP или жестко вписать Вашу командную строку в функцию arch/i386/kernel/setup.c:parse_mem_cmdline().
  3. arch/i386/kernel/setup.c:parse_mem_cmdline() (вызывается setup_arch(), которая непосредственно вызвана из start_kernel()) копирует 256 байт из zeropage в saved_command_line, которая отображается в /proc/cmdline. Эта же самая подпрограмма обрабатывает опцию mem=, если она представлена, и делает соответствующие корректировки параметров VM.
  4. Мы возвращаемся к командной строке в parse_options() (вызывается из start_kernel()), которая обрабатывает некоторые внутриядерные параметры (в настоящее время это init= и параметры для init) и передает каждое слово checksetup().
  5. checksetup() проходит код в ELF-разделе .setup.init и вызывает каждую функцию, передавая это слово, если оно соответствует. Обратите внимание, что использование значения возврата 0 из функции, зарегистрированной через __setup(), то же самое, что передать пару "variable=value" больше, чем одной функции с value, недопустимым в одной, и имеющим силу в другой. Почему? Потому, что это специфический ld-порядок, то есть ядро, связанное в одном порядке, будет иметь вызов functionA до functionB.

Как писать код для обработки командной строки? Мы используем макрокоманду __setup(), определенную в файле include/linux/init.h:

/*
 * Used for kernel command line parameter setup
 */
struct kernel_param {
  const char *str;
  int (*setup_func)(char *);
};
extern struct kernel_param __setup_start, __setup_end;

#ifndef MODULE
#define __setup(str, fn) \
  static char __setup_str_##fn[] __initdata = str; \
  static struct kernel_param __setup_##fn __initsetup = \
  { __setup_str_##fn, fn }

#else
#define __setup(str,func) /* nothing */
endif

Вы обычно использовали бы это в Вашем коде примерно так (образец взят из кода реального драйвера, BusLogic HBA drivers/scsi/BusLogic.c):

static int __init
BusLogic_Setup(char *str)
{
  int ints[3];

  (void) get_options(str, ARRAY_SIZE(ints), ints);
  if (ints[0] != 0)
  {
     BusLogic_Error("BusLogic: Obsolete Command Line Entry "
                    "Format Ignored\n", NULL);
     return 0;
  }
  if (str == NULL || *str == '\0') return 0;
  return BusLogic_ParseDriverOptions(str);
}
__setup("BusLogic=", BusLogic_Setup);

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


Вперед Оглавление Назад