⌨ Labor omnia vincit ☮

How the Linux kernel works

Posted in Linux Kernel by anaumov on 09.05.2010

Мой оксфордский словарь определяет kernel как “мягкая, как правило, съедобная часть ореха”, но есть так же и второй смысл: “Центральная и наиболее важная частью чего-то.” Если вы туманно представляете себе, что в действительности предствляет из себя ядро, я расскажу поподробней.

Ядро представляет из себя часть (слой) программного обеспечения, которое обеспечивает связь между железом и прикладными программами, запущенными на компьютере. Вообще, строго говоря, термин Linux относится только к ядру, т.е. той части, которую написал Линус Торвальдс в начале 90-х.

Все остальные части, которые мы можем найти в дистрибутиве GNU/Linux – оболочке Bash, графическом окружении KDE, web-браузеры, X-сервер и все остальное… является просто отдельными приложениями, запущенными поверх ядра.Чтобы вы могли себе это представить, я приведу такой пример – SLE11SP1 занимает приблизительно 2.5GB места на жестком диске (в зависимости конечно от того, что именно вы решили установить). Из этого всего само ядро, включая все его модули, занимает 47 МБ. Это приблизительно 2 %.

System call interface()

Ядро обеспечивает связь с железом… круто. Дык что же именно оно делает? Ядро предоставляет прикладным программам досуп к оборудованию через большую коллекцию точек входа, так называемых системных вызовов (system call).

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

Одной из таких абстракций, предоставляемых ядром, является файловая система. К примеру, в этой короткой программе (написанной на С) мы открываем файл, и копируем его содержимое в стандартный вывод:

#include <fcntl.h>
int main()
{
    int fd, count; char buf[1000];
    fd=open("mydata", O_RDONLY);
    count = read(fd, buf, 1000);
    write(1, buf, count);
    close(fd);
}

Здесь вы видите примеры 4 системных вызовов – open(), read(), write(), и close(). Не беспокойтесь о деталях синтаксиса; это не важно сейчас. Дело заключается в следующем: с помощью этих системных вызовов (и некоторых других) ядро Linux предоствляет нам некую иллюзию ‘файла’ – последовательность данных (в байтах), имя – и защищает нас от проблем, связынных с записью данных на диск. Каких проблем? Дело в том, что диск представляет из себя сложную систему из цилиндров, дорожек и секторов. Когда мы записываем данные на диск, мы используем эту систему, но ядро позволяет нам не задумываться о том, куда именно записывать данные, т.е. какие сектора, на каких дорожках чистые, т.е. пригодные для записи. Мы записываем какой-нить файл размером, скажем, в пару гигабайт на диск, и в нашем представлении это просто кусок информации. Этот файл может быть разбит на части и записан по кусочкам в совсем разные части жеского диска (да в принципе, именно так всегда и происходит), но нам нет до этого дела, и эти детали скрыты от нас. Для тех, кому это не нравится, может использовать Assembler и поговорить с аппаратурой напрямую😉

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

Как вы видите на картинке выше, ядро должно проделать непростую работу, чтобы поддержать эту абстракцию. Особенно в том случае, если надо поддерживать одну абстракцию для различных файловых систем, а файловые системы, в свою очередь, должны работать на различном оборудовании – жеские hdd диски, usb флеш карты, cd/dvd диски, SCSI, а так же удаленные файловые системы, доступ к которым обеспечивается через сетевые протоколы, такие как NFS или CIFS.

Так же, не будем забывать, про поддержку логических томов или RAID. Виртуальный слой файловой системы в ядре позволяет представить эти системы хранения данных в виде набора файлов в пределах одной файловой системы, несмотря не то, что это ОДНА файловая система физически находится на разных устройствах.

“Ложки не существует” (с) The Matrix

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

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

Вот еще маленькая программка C:

#include <stdlib.h>
main()
{
  if (fork()) {
    write(1, "Parent\n", 7);
    wait(0);
    exit(0);
  }
  else {
    write(1, "Child\n", 6);
    exit(0);
  }
}

Эта программа создает новый процесс – оригинальный процесс (родитель) и новый процесс (ребенок) каждый пишет сообщение на стандартный вывод, затем заканчивается. Не ломайте голову по поводу синтаксиа, это не важно сейчас. Только обратите внимание, что системные вызовы fork(), exit() и wait() выполяют процесс создания, прекращения и синхронизации. 3 системных вызова… изящных и простых… и нам не надо ломать голову над выделением процессорного времени для 2х разных программ (у процессов разное адресное пространство).

Еще одной менее заметной функцией ядра, даже для программистов, является управление памятью. Каждый процесс “думает”, что у него есть собственное адресного пространство (допустимый диапазон адресов памяти), который может использовать только этот процесс. Однако на самом деле вся память принадлежит всем процессам, запущенным на машине, и если памяти не хватает, то ядро может выгрузить какую-то часть в swap, т.е. память окажется уже не в памяти, а на жеском диске… но, тсссс…. процессу мы об этом ничего говорить пока не будем, а когда ему будет переданно процессорное время, то и из swap мы перенесем все обратно🙂

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

GNU/Linux (как Unix-образная операционная система) имеет хорошую репутацию для обеспечения безопасности. Безопасность операционной системы и ее стабильность – это качество и логика ядра. Ядро отслеживает идентификатор пользователя и группы каждого процесса, и использует их для принятия решения о предоставлении доступа к ресурсу (например, открытие файла для записи), проверяя права доступа к файлу. Эта модель контроля доступа, в конечном счете несет ответственность за безопасность систем GNU/Linux в целом… и реализованна она в ядре.

Ну и наконец ядро поддерживает большой набор модулей, которые знают, как работать с аппаратными устройствами – как читать сектора с диска, как получить пакет от сетевой карты и так далее. Они иногда называются драйверами устройств.

Модульная структура ядра

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

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

Основное ядро загружается в память во время загрузки из файла в дириктории /boot, и называется как-то типа vmlinuz-KERNELVERSION, где KERNELVERSION – версия ядра (чтобы узнать версию ядра, выполните команду uname -г). Модули ядра находятся в каталоге /lib/modules/KERNELVERSION.

Управление модулями ядра

Вообще-то Linux сам прекрасно справляется с загрузкой и заменой необходимых модулей, но это не значит, что мы не можем подгрузить какой-то модуль сами, если в этом появится необходимость. Например, чтобы выяснить какие модули загружены в данный момент в ядро, используйте lsmod.

# lsmod
pcspkr              4224  0
hci_usb            18204  2
psmouse            38920  0
bluetooth          55908  7 rfcomm,l2cap,hci_usb
yenta_socket       27532  5
rsrc_nonstatic     14080  1 yenta_socket
isofs              36284  0

Вывод lsmod показывает нам имя модуля, его размер, количество раз его использования, а также список модулей, которые зависят от него. То количество раз, которое ядро использует этот модуль, очень важно. Не стоит выгружать модуль, если это количесво больше 0, т.е. ядро работает сейчас (в данный момент) с этим модулем. Хотя, в принципе, Linux и не даст выгрузить модуль, с куском кода, с которым он сейчас работает. Это возможно лишь для тех модулей, у которых это значение равно нулю.

Вы можете вручную загружать и выгружать модули, используя modprobe. (Есть два нижних уровня команд называется insmod и rmmod, которые делают работу, но modprobe проще в использовании, поскольку она автоматически разрешает зависимостей модулей.) Например, вывод lsmod (вывод в моем примере) показывает загруженный модуль isofs, который сейчас не используется, и у которого нет никаких зависимых модулей. Этот модуль можно выгрузить:

# modprobe -r isofs

После этого модуль isofs не появляется при выходе lsmod. Это значит, что ядро использует на 36284 байт меньше памяти🙂 Если вы вставите диск в DVD/CD-привод и подмонтируете его (или, если настроенно, то ОС сделает это автоматичнски), ядро так же автоматически перезагрузит isofs модуль и “количество раз его использования” вырастет до 1. Если вы попытаетесь удалить модуль сейчас, Linux не разрешит вам этого… и правильно сделает🙂

# modprobe -r isofs
FATAL: Module isofs is in use.

Если lsmod показывает нам список загруженных модулей, modprobe -l выведет список всех доступных модулей. Вывод будет содержать все модули из /lib/modules/KERNELVERSION. Большой список!

Если вы решили подгрузить модуль в ручную с помощью modprobe, то вы так же можете передать параметры этому модулю при загрузке:

# modprobe usbcore blinkenlights=1

Нет, я не выдумал, Blinkenlights – это реальный параметр usbcore модуля.

Как я узнал, что у этого модуля есть такой параметр? Очень просто. Наиболее эффективным способом получить информацию о модуле, это использование modinfo. Вот например информация о модуле snd-hda-intel:

# modinfo snd-hda-intel
filename:       /lib/modules/2.6.20-16-generic/kernel/sound/pci/hda/snd-hda-intel.ko
description:    Intel HDA driver
license:        GPL
srcversion:     A3552B2DF3A932D88FFC00C
alias:          pci:v000010DEd0000055Dsv*sd*bc*sc*i*
alias:          pci:v000010DEd0000055Csv*sd*bc*sc*i*
depends:        snd-pcm,snd-page-alloc,snd-hda-codec,snd
vermagic:       2.6.20-16-generic SMP mod_unload 586
parm:           index:Index value for Intel HD audio interface. (int)
parm:           id:ID string for Intel HD audio interface. (charp)
parm:           model:Use the given board model. (charp)
parm:           position_fix:Fix DMA pointer (0 = auto, 1 = none, 2 = POSBUF, 3 = FIFO size). (int)
parm:           probe_mask:Bitmask to probe codecs (default = -1). (int)
parm:           single_cmd:Use single command to communicate with codecs (for debugging only). (bool)
parm:           enable_msi:Enable Message Signaled Interrupt (MSI) (int)
parm:           enable:bool

Интересным для нас являются информация Parm – показывает параметры, которые можно передать модулю. Эти описания очень краткие😦
Однако у нас есть так же и исходники ядра, которые помогут нам разобраться. Так же есть много интересного в /usr/src/KERNELVERSION/Documentation.

Например, в файле /usr/src/KERNELVERSION/Documentation/sound/alsa/ALSA-Configuration.txt описаны параметры, которые можно передавать многим ALSA-модулям. Много интересного есть в файле /usr/src/KERNELVERSION/Documentation/kernel-parameters.txt

Так же стоит упомянуть о возможности автоматической загрузки модулей с определенными параметрами. Для этого добавте в /etc/modprobe.conf команду, где будет указано название модуля и параметр, с которым ядро должно его загрузить:

options snd-hda-intel probe_mask=1

Первоисточник

Ядро Linux также предоставляет большой объем информации через файловую систему /proc. Чтобы разобраться в /proc, мы должны расширить наши представления о том, что такое файл.

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

Файлы в /proc – это от и до воображения ядра, достаточно абстрактные и иллюзорные. Эта ФС показывает нам многие внутренние структуры данных ядра.

Многие механизмы оповещения о происходящем в ядре, работают как раз через эту ФС. К примеру, cat /proc/modules показывает нам список модулей, загруженных в настоящее время в ядро. Чем-то схоже с выводом команды lsmod.

Аналогично, содержание /proc/meminfo дает более подробную информацию о текущем состоянии виртуальной памяти, чем можно получить, к примеру, с помощью vmstat. Другой пример /proc/net/arp показывает текущее содержимое кэша ARP системы. Сравните ее с выводом arp -a.

Особенно интересными являются файлы в /proc/sys. Например, /proc/sys/net/ipv4/ip_forward расскажет нам о том, является ли наша машинка шлюзом. У меня нет:

# cat /proc/sys/net/ipv4/ip_forward
0

Нам никто не запрещает писать в эти файлы:

# echo 1 > /proc/sys/net/ipv4/ip_forward

…тем самым мы включили IP forwarding.

Кстати, работать можно не только с помощью cat и echo, чтобы посмотреть что-то или изменить какие-то параметры в /proc/sys, можно также использовать sysctl:

# sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0

Т.е. вместо двух предыдущих команд

# cat /proc/sys/net/ipv4/ip_forward
0
# echo 1 > /proc/sys/net/ipv4/ip_forward
# cat /proc/sys/net/ipv4/ip_forward
1

Можно просто сделать

# sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1

Обратите внимание, что во-первых, при написании пути, который мы используем при работе с sysctl, мы используем точку вместо слеша, и во-вторых путь записываем отностительно /proc/sys.

Не забывайте так же, что параметры настройки актуальны лишь для текущего ядра, т.е. которое сейчас загруженно. После перезагрузки все измененные настройки будут обратно изменены. Чтобы эти настройки сохранить, запишите их в файл /etc/sysctl.conf. Во время загрузки sysctl автоматически восстановит любые параметры и настройки, которые найдет в этом файле. Формат записи как и при запуске самой sysctl:

net.ipv4.ip_forward=1

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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: