WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     | 1 |   ...   | 6 | 7 || 9 |

«; ...»

-- [ Страница 8 ] --

depends on DUMB_DRIVERS && !NO_FISHING_ALLOWED 350 Глава После директив t r i s t a t e и b o o l можно указать директиву if, что позволяет сделать соответствующий параметр зависимым от другого конфигурационного па раметра. Если условие не выполняется, то конфигурационный параметр не только запрещается, но и не будет отображаться утилитами конфигурации. Например, сле дующая строка указывает, что функция "Deep Sea Mode" будет доступна, только если разрешен конфигурационный параметр CONFIG_OKEAN.

bool TDeep Sea ModeY if OCEAN Директива if также может быть указана после директивы d e f a u l t, что означает, что значение по умолчанию будет установлено, только если выполняется условие, указанное в директиве if.

Система конфигурации экспортирует несколько метапараметров, чтобы упро стить процесс конфигурации. Параметр CONFIG_EMBEDDED устанавливается только тогда, когда пользователь указывает, что он хочет видеть вес параметры, отвечаю щие за запрещение некоторых ключевых позможностей ядра (обычно с целью со хранения памяти на встраиваемых системах). Параметр CONFIG_BROKEN_ON_SMP используется, чтобы указать, что драйвер не рассчитан на системы с симметричной многопроцессорностью. Обычно этот параметр не устанавливается, при этом от пользователя требуется, чтобы он сам убедился в возможности компиляции драйве ра для SMP. Новые драйверы этот флаг использовать не должны.

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

Параметры модулей Ядро Linux предоставляет возможность драйверам определять параметры, кото рые пользователь будет указывать при загрузке ядра или модуля. Эти параметры бу дут доступны коду модуля в качестве глобальных переменных. Указанные параметры модулей также будут отображаться в файловой системе sysfs (см. главу 17, "Объекты kobject и файловая система sysf "). Определять параметры модуля и управлять ими просто.

Параметр модуля определяется с помощью макроса module_param () следующим образом.

module_param(name, type, perm);

где аргумент name — это имя неременной, которая появляется в модуле, и имя пара метра, который может указать пользователь. Аргумент t y p e — это тип данных па раметра. Значения типа могут быть следующими: b y t e, s h o r t, u s h o r t, i n t. u i n t, l o n g, u l o n g, c h a r p, b o o l или i n v b o o l. Эти значения соответствуют следующим типам данных: байт;

короткое целое число;

короткое целое число без знака;

целое число;

целое число без знака;

длинное целое;

длинное целое число без знака;

указа тель на строку символов;

булев тип;

булев тип, значение которого инвертируется по сравнению с тем, которое указывает пользователь. Данные типа b y t e хранятся в пе ременной типа char, а данные булевых типов — в переменных типа i n t. Остальные типы соответствуют аналогичным типам языка С. Наконец, аргумент perm указыва М одул и ет права доступа к соответствующему файлу в файловой системе sysfs. Права досту па можно указать как в обычном восьмеричном формате, например 0644 (владелец имеет права на чтение и запись, группа имеет права на чтение и запись, остальные пользователи имеют право только на чтение), так и в виде определений препроцес сора, объединенных с помощью оператора " | ", например S_IRUGO | S_IWUSR (все могут считывать данные, а владелец также и записывать). Нулевое значение этого параметра приводит к тому, что соответствующий файл в файловой системе sysfs не появляется.

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

/* параметр модуля, который управляет переменной bait */ static int allow live bait = 1;

/* по умолчанию включено */ module_param(allow_live_bait, bool, 0644);

/* булев тип */ Это определение должно быть в глобальной области видимости, т.е. неременная a l l o w _ l i v e _ b a i t должна быть глобальной.

Существует возможность дать внешнему параметру модуля имя, отличное от име ни переменной. Это можно сделать с помощью макроса module_param_named ().

module_param_named(name, variable, type, perm);

где name — это имя внешнего параметра модуля, a v a r i a b l e — имя внутренней гло бальной переменной модуля, как показано ниже.

static unsigned int max_test = DEFAULT_МАХ_LINE_TEST;

module_param_named (maximum_line_test, max_test, int, 0 ) ;

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

static char *name;

module_param(name, charp, 0 ) ;

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

module_param_string(name, string, len, perm);

где name — это имя внешнего параметра, s t r i n g — имя внутренней переменной, которая содержит указатель на область памяти массива, l e n — размер буфера string (или некоторое меньшее число, чем размер буфера, что, однако, обычно не имеет смысла), perm — права доступа к файлу на файловой системе sysfs (нулевое значение запрещает доступ к параметру через sysfs). Пример показан ниже.

static char species[BUF_LEN];

module_param_string (specif ies, species, BUF_LEN, 0 ) ;

В качестве параметров модуля также можно передавать список значений, кото рые разделены запятой и в коде модуля будут записаны в массив данных. Эти пара 352 Глава метры модуля можно обработать с помощью макроса module_param_array() следу ющим образом.

module_param array(name, type, nump, perm);

В данном случае аргумент name — это имя внешнего параметра и внутренней пере менной, type — это тип данных одного значения, a perm — это права доступа к файлу на файловой системе sysfs. Новый аргумент nump — это указатель на целочисленное значение, где ядро сохраняет количество элементов, записанных в массив. Обратите внимание, что массив, который передается в качестве параметра name, должен быть выделен статически. Ядро определяет размер массива на этапе компиляции и гаран тирует, что он не будет переполнен. Как использовать данный макрос, показано в следующем примере.

static int fish[MAX_FISH];

static int nr_fish;

module_param_array(fish, int, &nr_fish, 0444);

Внутренний массив может иметь имя, отличное от имени внешнего параметра, в этом случае следует использовать макрос module_param_array_named ().

module_param_array_named(name, array, type, nump, perm);

Параметры идентичны аналогичным параметрам других макросов.

Наконец, параметры модуля можно документировать, используя макрос MODULE_ PARM_DESC().

static unsigned short size = 1;

module_param(size, ushort, 0644);

MODULE_PARMDESC(size, "The size in inches of the fishing pole " \ "connected to this computer.");

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

Экспортируемые символы При загрузке модули динамически компонуются с ядром. Так же как и в случае динамически загружаемых бинарных файлов пространства пользователя, в коде мо дулей могут вызываться только те функции ядра (основного образа или других мо дулей), которые явно экспортируются для использования. В ядре экспортирование осуществляется с помощью специальных директив EXPORT_SYMBOL () и EXPORT_ SYMBOL_GPL().

Функции, которые экспортируются, доступны для использования модулями.

Функции, которые не экспортируются, не могут быть вызваны из модулей. Правила компоновки и вызова функций для модулей значительно более строгие, чем для основного образа ядра. Код ядра может использовать любые интерфейсы ядра (кро ме тех, которые определены с ключевым словом s t a t i c ), потому что код ядра ком понуется в один выполняемый образ. Экспортируемые символы, конечно, тоже не должны определяться как s t a t i c.

Набор символов ядра, которые экспортируются, называется экспортируемым ин терфейсом ядра или даже (здесь не нужно удивляться) API ядра.

Модули Экспортировать символы просто. После того как функция определена, необходи мо вызвать директиву EXPORT_SYMBOL ().

/* * get_pirate_beard_color — возвратить значение цвета бороды текущего * пирата pirate — это глобальная переменная, доступная из данной * функции цвета определены в файле */ int get_pirate_beard_color(void) { return pirate->beard->color;

} EXPORT_SYMBOL(get_pirate_beard_color);

Допустим, что функция g e t _ p i r a t e _ b e a r d _ c o l o r ( ) объявлена в заголовочном файле и ее может использовать любой модуль.

Некоторые разработчики хотят, чтобы их интерфейсы были доступны только для модулей с лицензией GPL. Такая возможность обеспечивается компоновщиком ядра с помощью макроса MODULE_LICENSE ( ). Если есть желание, чтобы рассматриваемая функция была доступна только для модулей, которые помеченные как соответствую щие лицензии GPL, то экспортировать функцию можно следующим образом.

EXPORT_SYMBOL_GPL(get_pirate_beard_color);

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

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

В следующей главе будут рассмотрены объекты k o b j e c t и файловая система sysf's, которые являются основным интерфейсом к драйверам устройств и, следовательно, к модулям ядра.

354 Глава Объекты kobject и файловая система sysfs У нифицированная модель представления устройств — это существенно новая осо бенность, которая появилась в ядрах серии 2.6. Модель устройств— это еди ный механизм для представления устройств и описания их топологии в системе.

Использование единого представления устройств позволяет получить следующие преимущества.

• Уменьшается дублирование кода.

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

• Появляется возможность систематизации всех устройств в системе, возмож ность просмотра состояний устройств и определения, к какой шине то или другое устройство подключено.

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

• Обеспечивается возможность связывания устройств с их драйверами и наоборот.

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

• Обеспечивается возможность просмотра иерархии устройств от листьев к кор ню и выключения питания устройств в правильном порядке.

Последний пункт был самой первой мотивацией необходимости создания общей модели представления устройств. Для того чтобы реализовать интеллектуальное управление электропитанием в ядре, необходимо построить дерево, которое пред ставляет топологию устройств в системе. Для выключения питания устройств, ко торые организованы в виде древовидной топологии, ориентированной сверху вниз, ядро должно выключить питание нижних узлов (листьев) перед выключением пита ния верхних узлов. Например, ядро должно выключить питание USB-мыши перед тем, как выключать питание контроллера шины USB, а питание контроллера шины USB должно быть выключено перед выключением питания шины PCI. Чтобы делать это эффективно и правильно для всей системы, ядру необходимо отслеживать топо логию дерева всех устройств в системе.

Объекты k o b j e c t Сердцем модели представления устройств являются объекты kobject, которые представляются с помощью структуры s t r u c t k o b j e c t, определенной в файле < l i n u x / k o b j e c t. h >. Тип kobject аналогичен классу Object таких объектно-ори ентированных языков программирования, как С# и Java. Этот тип определяет общую функциональность, такую как счетчик ссылок, имя, указатель на родительский объ ект, что позволяет создавать объектную иерархию.

Структура, с помощью которой реализованы объекты kobject, имеет следующий вид.

struct kobject { char *k_name;

char name[KOBJ_NAME_LEN];

struct kref kref;

struct list_head entry;

struct kobject *parent;

struct kset *kset;

struct kobj_type *ktype;

struct dentry *dentry;

};

Поле k_name содержит указатель на имя объекта. Если длина имени меньше KOBJ_NAME_LEN, что сейчас составляет 20 байт, то имя хранится в массиве name, a ноле kname указывает на первый элемент этого массива. Если длина имени больше KOBJ_NAME_LEN байт, то динамически выделяется буфер, размер которого достато чен для хранения строки символов имени, имя записывается в этот буфер, а поле k_name указывает на него.

Указатель p a r e n t указывает на родительский объект данного объекта kobject.

Таким образом, с помощью структур k o b j e c t может быть создана иерархия объ ектов в ядре, которая позволяет устанавливать соотношения родства между раз личными объектами. Как будет видно дальше, с помощью файловой системы sysfs осуществляется представление в пространстве пользователя той иерархии объектов kobject, которая существует в ядре.

Указатель d e n t r y содержит адрес структуры s t r u c t dentry, которая представ ляет этот объект в файловой системе sysfs.

Поля kref, ktype и k s e t указывают на экземпляры структур, которые использу ются для поддержки объектов kobject. Поле e n t r y используется совместно с полем kset. Сами эти структуры и их использование будут обсуждаться ниже.

Обычно структуры kobject встраиваются в другие структуры данных и сами по себе не используются. Например, такая важная структура, как s t r u c t cdev, имеет поле kobj.

356 Глава /* структура cdev - объект для представления символьных устройств */ struct cdev { struct kobject kobj;

struct module *owner;

struct file_operations *ops;

struct list_head list;

dev_t dev;

unsigned int count;

};

Когда структуры kobject встраиваются в другие структуры данных, то последние получают те стандартизированные возможности, которые обеспечиваются структу рами kobject. Еще более важно, что структуры, которые содержат в себе объекты kobject, становятся частью объектной иерархии. Например, структура cdev пред ставляется в объектной иерархии с помощью указателя на родительский объект cdev->kobj->parent и списка cdev->kobj->entry.

Типы k t y p e Объекты kobject могут быть связаны с определенным типом, который называ ется ktype. Типы ktype представляются с помощью структуры s t r u c t kobj_type, определенной в файле следующим образом.

struct kobj_type { void (*release)(struct kobject * ) ;

struct sysfs_ops *sysfs_ops;

struct attribute **default_attrs;

};

Тип ktype имеет простое назначение— представлять общее поведение для не которого семейства объектов kobject. Вместо того чтобы для каждого отдельного объекта задавать особенности поведения, эти особенности связываются с их полем ktype, и объекты одного "типа" характеризуются одинаковым поведением.

Поле r e l e a s e содержит указатель на деструктор, который вызывается, когда ко личество ссылок на объект становится равным нулю. Эта функция отвечает за осво бождение памяти, связанной с объектом, и за другие операции очистки.

Поле sysfs_ops указывает на структуру sysfs_ops. Эта структура определяет поведение файлов на файловой системе sysfs при выполнении операций записи и чтения. Более детально она рассматривается в разделе "Добавлепие файлов на фай ловой системе sysfs".

Наконец, поле d e f a u l t _ a t t r s указывает на массив структур a t t r i b u t e. Эти структуры определяют атрибуты, которые связаны с объектом kobject и использу ются но умолчанию. Атрибуты соответствуют свойствам данного объекта. Если не который объект kobject экспортируется через файловую систему sysfs, то атрибуты экспортируются как отдельные файлы. Последний элемент этого массива должен со держать значению NULL.

Объекты kobject и файловая система sysfs Множества объектов k s e t Множества k s e t представляют собой коллекции объектов kobject. Множество kset работает как базовый контейнерный класс для объектов, например, "все блоч ные устройства". Множества kset очень похожи на типы ktype, и возникает вопрос:

"Для чего нужны два разных обобщения?" Множество k s e t объединяет несколько объектов kobject, а типы ktype определяют общие свойства, которые связаны с объектами k o b j e c t одного типа. Существует возможность объединить объекты одного типа ktype в различные множества kset.

Поле kset объекта kobject указывает на связанное с данным объектом множе ство k s e t. Множество объектов k s e t представляется с помощью структуры kset, которая определена в файле < l i n u x / k o j e c t. h > следующим образом.

struct kset { struct subsystem *subsys;

struct kobj_type *ktype;

struct list_head list;

struct kobject kobj;

struct kset_hotplug_ops *hotplug_ops;

};

Указатель ktype указывает на структуру ktype, которая определяет тип всех объ ектов данного множества, поле l i s t - список всех объектов kobject данного мно жества, поле kobj — объект kobject, который представляет базовый класс для всех объектов данного множества, а поле hotplug_ops указывает на структуру, которая определяет поведение объектов kobject при горячем подключении устройств, свя занных с данным множеством.

Наконец, поле sybsys указывает на структуру s t r u c t subsystem, которая связа на с данным множеством kset.

Подсистемы Подсистемы используются для представления высокоуровневых концепций ядра и являются коллекцией одного или нескольких множеств kset. Множества k s e t со держат объекты kobject, подсистемы — множества kset, но связь между множества ми в подсистеме значительно более слабая, чем связь между объектами k o b j e c t в множестве. Множества kset одной подсистемы могут иметь только наиболее общие объединяющие факторы.

Несмотря на их важную роль, подсистемы представляются с помощью очень про стой структуры данных — s t r u c t subsystem.

struct subsystem { struct kset ksot;

struct rw_semaphore rwsem;

};

Структура subsystem содержит только одно множество kset, тем не менее не сколько множеств kset могут указывать на общую структуру subsystem с помощью 358 Глава поля subsys. Такие однонаправленные взаимоотношения означают, что нет возмож ности определить все множестпа подсистемы, только имея ее структуру subsystem.

Поле k s e t, которое содержится в структуре subsystem, — это множество k s e t подсистемы, которое используется по умолчанию, чтобы зафиксировать положение этой подсистемы в иерархии объектов.

Поле rwsem структуры s u b s y s t e m — это семафор чтения-записи (см. главу 9, "Средства синхронизации в ядре"), который используется для защиты подсистемы и ее множеств k s e t от конкурентного доступа. Все множества k s e t должны принадле жать какой-нибудь подсистеме, поскольку они используют семафор подсистемы для защиты своих данных от конкурентного доступа.

Путаница со структурами Те несколько структур, которые только что были описаны, приводят к путани це не потому, что их много (только четыре) или они сложные (все они достаточно просты), а потому что они сильно друг с другом переплетаются. При использовании объектов k o b j e c t достаточно сложно рассказать об одной структуре, не упоминая другие. Тем не менее, на основании рассмотренных особенностей этих структур можно построить прочное понимание их взаимоотношений.

Самым важным является объект k o b j e c t, который представляется с помощью структуры s t r u c t k o b j e c t. Структура k o b j e c t используется для представления наиболее общих объектных свойств структур данных ядра, таких как счетчик ссы лок, взаимоотношения родитель-порожденный и имя объекта. С помощью структуры k o b j e c t эти свойства можно обеспечить одинаковым для всех стандартным спосо бом. Сами по себе структуры k o b j e c t не очень полезны, они обычно встраиваются в другие структуры данных.

С каждым объектом k o b j e c t связан один определенный тип данных— k t y p e, который представляется с помощью структуры s t r u c t k o b j _ t y p e. На экземпляр такой структуры указывает поле ktype каждого объекта k o b j e c t. С помощью типов ktype определяются некоторые общие свойства объектов: поведение при удалении объекта, поведение, связанное с файловой системой sysfs, а также атрибуты объекта.

Объекты k o b j e c t группируются в множества, которые называются k s e t.

Множества k s e t представляются с помощью структур данных s t r u c t k s e t. Эти множества предназначены для двух целей. Во-первых, они позволяют использовать встроенный в них объект k o b j e c t в качестве базового класса для группы других объектов k o b j e c t. Во-вторых, они позволяют объединять вместе несколько связан ных между собой объектов k o b j e c t. На файловой системе sysfs объекты k o b j e c t представляются отдельными каталогами файловой системы. Связанные между собой каталоги, например все подкаталоги одного каталога, могут быть включены в одно множество k s e t.

Подсистемы соответствуют большим участкам ядра и являются набором мно жеств k s e t. Подсистемы представляются с помощью структур s t r u c t subsystem.

Все каталоги, которые находятся в корне файловой системы sysfs, соответствуют подсистемам ядра.

На рис. 17.1 показаны взаимоотношения между этими структурами данных.

Объекты kobject и файловая система sysfs Подсистема Подсистема kobj kset kobj kset kobj kobj kobj kobj kobj kobj Рис. 17.1. Взаимоотношения между объектами kobject, множествами kset и подсистемами Управление и манипуляции с объектами k o b j e c t Теперь, когда у нас уже есть представление о внутреннем устройстве объектов k o b j e c t и связанных с ними структурах данных, самое время рассмотреть экспор тируемые интерфейсы, которые дают возможность управлять объектами kobject и выполнять с ними другие манипуляции. В основном, разработчикам драйверов не посредственно не приходится иметь дело с объектами kobject. Структуры kobject встраиваются в некоторые специальные структуры данных (как это было в приме ре структуры устройства посимвольного ввода-вывода) и управляются "за кадром" с помощью соответствующей подсистемы драйверов. Тем не менее, объекты kobject не всегда могут оставаться невидимыми, иногда с ними приходится иметь дело, как при разработке кода драйверов, так и при разработке кода управления подсистема ми ядра.

Первый шаг при работе с объектами k o b j e c t - это их декларация и инициали зация. Инициализируются объекты kobject с помощью функции k o b j e c t _ i n i t (), которая определена в файле < l i n u x / k o b j e c t. h > следующим образом.

void kobject_init(struct kobject *kobj);

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

memset(kobj, 0, sizeof (*kobj));

После заполнения нулями безопасным будет инициализация полей p a r e n t и kset, как показано в следующем примере.

360 Глава kobj = kmalloc(sizeof (*kobj), GFP_KERNEL);

if (!kobj) return -ENOMEM;

memset(kobj, 0, sizeof (*kobj));

kobj->ksct - kset;

kobj->parent = parent_kobj;

kobject_init (kobj);

После инициализации необходимо установить имя объекта с помощью функции k o b j e c t _ s e t _ n a m e ( ), которая имеет следующий прототип.

int kobject_set_name(struct kobject * kobj, const char * fmt,,... ) ;

Эта функция принимает переменное количество параметров, по аналогии с функ циями p r i n t f () и p r i n t k (). Как уже было сказано, на имя объекта указывает поле k_name структуры k o b j e c t. Если это имя достаточно короткое, то оно хранится в статически выделенном массиве name, поэтому есть смысл без необходимости не указывать длинные имена.

После того как для объекта выделена память и объекту присвоено имя, нужно установить значение его поля k s e t, а также опционально поле k t y p e. Последнее необходимо делать только в том случае, если множество k s e t не предоставляет типа ktype для данного объекта, в противном случае значение поля ktype, которое указано в структуре k s e t, имеет преимущество. Если интересно, почему объекты k o b j e c t имеют свое поле ktype, то добро пожаловать в клуб!

Счетчики ссылок Одно из главных свойств, которое реализуется с помощью объектов k o b j e c t, — это унифицированная система поддержки счетчиков ссылок. После инициализации количество ссылок на объект устанавливается равным единице. Пока значение счет чика ссылок на объект не равно нулю, объект существует в памяти, и говорят, что он захвачен (pinned, буквально, пришпилен). Любой код, который работает с объектом, вначале должен увеличить значение счетчика ссылок. После того как код закончил работу с объектом, он должен уменьшить значение счетчика ссылок. Увеличение значения счетчика называют захватом (getting), уменьшение — освобождением (putting) ссылки на объект. Когда значение счетчика становится равным нулю, объект может быть уничтожен, а занимаемая им память освобождена.

Увеличение значения счетчика ссылок выполняется с помощью функции kobject_get().

struct kobject * kobject_get(struct kobject *kobj);

Эта функция возвращает указатель на объект kobject в случае успеха и значение N L в случае ошибки.

UL Уменьшение значения счетчика ссылок выполняется с помощью функции kobject_put().

void kobject put(struct kobject *kobj);

Объекты kobject и файловая система sysfs Если значение счетчика ссылок объекта, который передается в качестве параме тра, становится равным нулю, то вызывается функция, на которую указывает указа тель r e l e a s e поля ktype этого объекта.

Структуры k r e f Внутреннее представление счетчика ссылок выполнено с помощью структуры kref, которая определена в файле следующим образом.

struct kref{ atomic_t refcount;

};

Единственное поле этой структуры — атомарная переменная, в которой хранится значение счетчика ссылок. Структура используется просто для того, чтобы выпол нять проверку типов. Чтобы воспользоваться структурой kref, необходимо ее ини циализировать с помощью функции k r e f _ i n i t ().

void kref_init(struct kref *krcf) { atomic_set(&kref->refcount, 1 ) ;

} Как видно из определения, эта функция просто инициализирует атомарную пере менную тина a t o m i c _ t в значение, равное единице.

Следовательно, структура k r e f является захваченной сразу же после инициализа ции, так же ведут себя и объекты kobject.

Для того чтобы захватить ссылку на структуру k r e f, необходимо использовать функцию k r e f _ g e t ( ).

void kref_get(struct kref *kref) { WARN_ON(!atomic_read(&kref->refcount));

atomic_inc(&kref->refcount);

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

void kref_put(struct kref *kref, void (*release) (struct kref *kref)) { WARN_ON(release == NULL);

WARN_ON(release == (void (*)(struct kref *))kfree);

if (atomic dec_and_test (&kref->refcount)) release (kref);

} Эта функция уменьшает значение счетчика ссылок на единицу и вызывает функ цию r e l e a s e ( ), которая передастся ей в качестве параметра, когда значение счет чика ссылок становится равным нулю. Как видно из использованного выражения W R _ N ( ), функция r e l e a s e () не может просто совпадать с функцией k f r е е ( ), A MO 362 Глава а должна быть специальной функцией, которая принимает указатель на структуру s t r u c t kref в качестве своего единственного параметра и не возвращает никаких значений.

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

Все эти функции определены в файле l i b / k r e f. c и объявлены в файле .

Файловая система sysfs Файловая система sysfs — это виртуальная файловая система, которая существу ет только в оперативной памяти и позволяет просматривать иерархию объектов kobject. Она позволяет пользопателям просматривать топологию устройств опера ционной системы в виде простой файловой системы. Атрибуты объектов k o b j e c t могут экспортироваться в виде файлов, которые позволяют считывать значения пе ременных ядра, а также опционально записывать их.

Хотя изначально целью создания модели представления устройств было описа ние топологии устройств системы для управления электропитанием, файловая си стема sysfs стала удачным продолжением этой идеи. Для того чтобы упростить отлад ку, разработчик унифицированной модели устройств решил экспортировать дерево устройств в виде файловой системы. Такое решение показало свою полезность вна чале в качестве замены файлов, связанных с устройствами, которые раньше экспор тировались через файловую систему /ргос, а позже в качестве мощного инструмен та просмотра информации о системной иерархии объектов. Вначале, до появления объектов k o b j e c t, файловая система sysfs называлась driverfs. Позже стало ясно — новая объектная модель была бы очень кстати, и в результате этого появилась кон цепция объектов k o b j e c t. Сегодня каждая система, на которой работает ядро 2.6, имеет поддержку файловой системы sysfs, и практически во всех случаях эта файло вая система монтируется.

Основная идея работы файловой системы sysfs — это привязка объектов kobject к структуре каталогов с помощью поля d e n t r y, которое есть в структуре k o b j e c t.

Вспомните из материала главы 12, "Виртуальная файловая система", что структура d e n t r y используется для представления элементов каталогов. Связывание объектов с элементами каталогов проявляется в том, что каждый объект просто видится как каталог файловой системы. Экспортирование объектов k o b j e c t в виде файловой системы выполняется путем построения дерева элементов каталогов в оперативной памяти. Но обратите внимание, объекты kobject уже образуют древовидную струк туру — нашу модель устройств! Поэтому простое назначение каждому объекту иерар хии, которые уже образуют дерево в памяти, соответствующего элемента каталога позволяет легко построить файловую систему sysfs.

На рис. 17.2 показан частичный вид файловой системы sysfs, которая смонтиро вана на каталог /sys.

Корневой каталог файлопой системы sysfs содержит семь подкаталогов: block, bus, c l a s s, devices, firmware, module и power. В каталоге b l o c k содержатся каталоги для каждого зарегистрированного в системе устройства блочного ввода-вывода.

Объекты kobject и файловая система sysfs /sys block/ fd hda dev device->../devices/psi0000:00/0000:00:1f.1/ide/0. hda dev size start star hda hda hda hda hda queue lidc hdd loop loop Ioop Ioop Ioop Ioop Ioop Ioop md bus/ class/ devices/ firmware/ power Рис. 17.2. Содержимое части каталога /sys Каждый из каталогов в свою очередь содержит подкаталоги, соответствующие разделам блочного устройства. Каталог bus позволяет просматривать информа цию о системных шинах. В каталоге c l a s s представлена информация о системных устройствах, которая организована в соответствии с высокоуровневыми функциями этих устройств. Каталог devices содержит информацию о топологии устройств в системе. Она отображается непосредственно на иерархию структур устройств ядра.

Каталог firmware содержит специфичное для данной системы дерево низкоуровне вых подсистем, таких как ACPI, EDD, EFT и т.д. В каталоге power содержатся данные по управлению электропитанием всех устройств системы.

Наиболее важным является каталог d e v i c e s, который экспортирует модель устройств ядра во внешний мир. Структура каталога соответствует топологии устройств в системе. Большинство информации, которая содержится в других ка талогах, — это просто другое представление данных каталога devices. Например, в каталоге / s y s / c l a s s / n e t / информация представлена в соответствии с высоко уровневым представлением зарегистрированных сетевых устройств. В этом каталоге может содержаться подкаталог eth0, который содержит символьную ссылку device на соответствующее устройство каталога devices.

Посмотрите на содержимое каталога /sys той системы Linux, к которой вы имеете доступ. Такое представление системных устройств является очень четким и ясным. Оно показывает взаимосвязь между высокоуровневым представлением ин формации в каталоге c l a s s, низкоуровневым представлением в каталоге devices 364 Глава и драйверами устройств — в каталоге bus. Такое представление взаимосвязи между устройствами очень информативно. Оно становится еще более ценным, если осо знать, что все эти данные свободно доступны и описывают все то, что происходит внутри ядра1.

Добавление и удаление объектов на файловой системе sysfs Инициализированные объекты kobject автоматически не экспортируются через файловую систему sysfs. Для того чтобы сделать объект видимым через sysfs, необхо димо использовать функцию kobject_add().

int kobject_add(struct kobject *kobj);

Положение объекта на файловой системе sysfs зависит от его положения в объ ектной иерархии. Если установлен указатель p a r e n t объекта, то объект будет ото бражен внутри каталога, соответствующего объекту, на который указывает указатель p a r e n t. Если указатель p a r e n t не установлен, то объект будет отображен в каталоге, соответствующем значению переменной kset->kobj. Если для некоторого объекта не установлены ни значение поля p a r e n t, ни значение поля kset, то считается, что данный объект не имеет родительского и будет отображаться в корневом каталоге файловой системы sysfs. Такое поведение практически всегда соответствует тому, что нужно. Поэтому одно из полей p a r e n t или k s e t (или оба) должно быть установ лено правильным образом перед вызовом функции kobject_add (). Имя каталога, который представляет объект kobject в файловой системе sysfs, будет определяться значением поля kobj->name.

Вместо того чтобы последовательно вызывать функции k o b j e c t _ i n i t () и kobject_add(), можно вызвать функцию k o b j e c t _ r e g i s t e r ().

int kobject_register(struct kobject *kobj);

Удаление объекта из файловой системы sysfs выполняется с помощью функции kobject_del().

void kobject_del(struct kobject *kobj);

Функция kob j е c t u n r e g i s t er () сочетает в себе выполнение функций kobject_del() и kobject_put().

void kobject_unregister(struct kobject * kobj);

Все эти четыре функции определены в файле l i b / k o b j e c t. с и объявлены в файле .

Если вас заинтересовала информация о файловой системе sysfs, то, вероятно, вам будет интересно также ознакомиться с HAL, hardware abstraction layer (уровень абстракции аппаратного обеспече ния), информация о котором доступна по адресу h t t p : / / h a l. f r e e d e s k t o p. o r g /. Подсистема HAL позволяет создать в оперативной памяти базу данных на основании информации файловой системы sysfs, объединяя вместе понятия классов, устройств и драйверов. На основании этих дан ных уровень HAL предоставляет API, которое позволяет разрабатывать более интеллектуальные программы.

Объекты kobject и файловая система sysfs Добавление файлов на файловой системе sysfs Объекты kobject отображаются на каталоги, и такое отображение выполняется естественным образом. А как насчет создания файлов? Файловая система sysfs — это не что иное, как дерево каталогов без файлов.

Атрибуты, используемые по умолчанию Набор файлов, которые создаются в каталоге по умолчанию, определяется с по мощью поля ktype объектов kobject и множеств kset. Следовательно, все объекты kobject одного типа имеют один и тот же набор файлов в каталогах, которые этим объектам соответствуют. Структура kobject_type содержит поле d e f a u l t _ a t t r s, которое представляет собой массив структур a t t r i b u t e. Атрибуты отображают дан ные ядра на файлы в файловой системе sysfs.

Структура a t t r i b u t e s определена в файле .

/* структура a t t r i b u t e - атрибуты позволяют отобразить данные ядра на файлы файловой системы sysfs */ struct attribute { char *name;

/* имя атрибута */ struct module *owner;

/* модуль, если есть, которому принадлежат данные */ mode_t mode;

/* права доступа к файлу */ };

Поле name содержит имя атрибута. Такое же имя будет иметь и соответствующий файл на файловой системе sysfs. Поле owner — это указатель на структуру module, которая представляет загружаемый модуль, содержащий соответствующие данные.

Если такого модуля не существует, то значение поля равно NULL. Поле mode имеет тип mode_t и указывает права доступа к файлу на файловой системе sysfs. Если атри бут предназначен для чтения всеми, то флаг прав доступа доллсен быть установлен в значение S_IRUGO, если атрибут имеет право на чтение только для владельца, то права доступа устанавливаются в значение S_IRUSR. Атрибуты с правом на запись, скорее всего, будут иметь права доступа S_IRUGO | S_IWUSR. Все файлы и каталоги на файловой системе sysfs принадлежат пользователю с идентификаторами пользова теля и группы равными нулю.

Структура a t t r i b u t e используется для представления атрибутов, а структура sysfs_ops описывает, как эти атрибуты использовать. Поле sysfs_ops — это ука затель на одноименную структуру, которая определена в файле < l i n u x / s y s f s. h > следующим образом.

struct sysfs_ops { /* метод вызывается при чтении файла на файловой системе sysfs */ ssize_t (*show) (struct kobject *kobj, struct attribute *attr, char *buffer);

/* метод вызывается при записи файла на файловой системе sysfs */ ssize_t (*store) (struct kobject *kobj, struct attribute *attr, const char *buffer, size_t size);

};

366 Глава Метод show() вызывается при чтении файла. Он должен выполнить копирова ние значения атрибута, который передается в качестве параметра a t t r, в буфер, на который указывает параметр buffer. Размер буфера равен PAGE_SIZE байт. Для ап паратной платформы значение PAGE_SIZE равно 4096 байтов. Функция должна воз вратить количество байтов данных, которые записаны в буфер в случае успешного завершения, и отрицательный код ошибки, если такая ошибка возникает.

Метод s t o r e ( ) вызывается при записи. Он должен скопировать s i z e байт дан ных из буфера buffer в атрибут a t t r. Размер буфера всегда равен PAGE_SIZE или меньше. Функция должна возвратить количество байтов данных, которые прочита ны из буфера при успешном выполнении, и отрицательный код ошибки в случае не удачного завершения.

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

Создание нового атрибута Обычно атрибутов, которые используются по умолчанию и предоставляются ти пом ktype, связанным с объектом kobject, оказывается достаточно. Действительно, все объекты k o b j e c t одного типа должны быть чём-то похожи друг на друга или даже быть идентичными по своей природе. Например, для всех разделов жестких дисков один и тот же набор атрибутов должен подходить для всех объектов kobject.

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

Тем не менее иногда требуется, чтобы определенный экземпляр объекта kobject имел некоторые специфические свойства. Для таких объектоп может оказаться же лательным (или необходимым) создать атрибут, которого нет у общего типа данного объекта. Для такого случая ядро предоставляет функцию sysf s _ c r e a t e _ f i l e () для добавления атрибута к существующему объекту.

int sysfs_create_file(struct kobject *kobj, const struct attribute *attr);

Эта функция позволяет привязать структуру a t t r i b u t e, на которую указывает параметр a t t r, к объекту k o b j e c t, на который указывает параметр kobj. Перед тем как вызвать эту функцию, необходимо установить значение атрибута (заполнить поля структуры). Эта функция возвращает значение нуль в случае успеха и отрица тельное значение в случае ошибки.

Обратите внимание, что для обработки указанного атрибута используется струк тура sysfs_ops, соответствующая типу ktype объекта. Иными словами, существую щие функции show () и s t o r e {), которые используются для объекта по умолчанию, должны иметь возможность обработать вновь созданный атрибут.

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

int sysfs_create_link(struct kobject *kobj, struct kobject *target, char *name);

Объекты kobject и файловая система sysfs Эта функция создает символьную ссылку с именем name в каталоге объекта, соот ветствующего параметру kobj, на каталог, соответствующий параметру t a r g e t. Эта функция возвращает нулевое значение в случае успеха и отрицательный код ошибки в противном случае.

Удаление созданного атрибута Удаляется атрибут с помощью вызова функции sysfs_remove_f i l e ().

void sysfs_remove_file(struct kobject *kobj, const struct attribute *attr);

После возврата из этой функции указанный атрибут больше не отображается в каталоге объекта.

Символьная ссылка, созданная с помощью функции sysfs_create_link (), мо жет быть удалена с помощью функции sysfs_remove_link().

void sysfs_remove_link (struct kobject *kobj, char *name) ;

После возврата из функции символьная ссылка с именем name удаляется из ката лога, на который отображается объект kobj.

Все эти четыре функции объявлены в файле < l i n u x / k o b j e c t. h >. Функции s y s f s _ c r e a t e _ f i l e ( ) и sysfs_remove_file() определены в файле fs/sysfs/ f i l e. с, а функции s y s f s _ c r e a t e _ l i n k ( ) и sysfs_remove_link() — в файле fs/sysfs/symlink.c.

Соглашения по файловой системе sysfs Файловая система sysfs — это место, где должна реализовываться функциональ ность, для которой раньше использовался системный вызов i o c t l ( ) для специ альных файлов устройств, или файловая система procfs. Сегодня модно выполнять такие вещи через атрибуты файловой системы sysfs в соответствующем каталоге.

Например, вместо того чтобы реализовать новую директиву i o c t l ( ) для специаль ного файла устройства, лучше добавить соответствующий атрибут в каталоге файло вой системы sysfs, который относится к этому устройству. Такой подход позволяет избежать использования небезопасных, из-за отсутствия проверки типов аргументов, директив i o c t l (), а также файловой системы / р r о с с ее бессистемным расположе нием файлов и каталогов.

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

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

Использование одной переменной на файл позволяет легко считывать и записывать данные из командной строки, а также просто работать через файловую систему sysfs с данными ядра в программах, написанных на языке С. В случаях, когда одно значе ние на файл приводит к неэффективному представлению информации, допустимо использование файлов, в которых хранится несколько значений одного типа. Эти данные необходимо четко разделять. Наиболее предпочтительным разделителем яв ляется символ пробела. При разработке кода ядра необходимо всегда помнить, что 368 Глава файлы файловой системы sysfs являются представлениями переменных ядра, и ори ентироваться на доступ к ним из пространства пользователя, в частности из команд ной строки.

Во-вторых, данные файловой системы sysfs должны быть организованы в виде четкой иерархии. Для этого необходимо правильно разрабатывать связи "родитель потомок" объектов kobject. Связывать атрибуты с объектами kobject необходимо с учетом того, что эта иерархия объектов существует не только в ядре, но и экспор тируется в пространство пользователя. Структуру файловой системы sysfs необходи мо поддерживать в четком виде!

Наконец, необходимо помнить, что файловая система sysfs является службой ядра и в некотором роде интерфейсом ядра к прикладным программам (Application Binary Interface, ABT). Пользовательские программы должны разрабатываться в соот ветствии с наличием, положением, содержимым и поведением каталогов и файлов на файловой системе sysfs. Изменение положения существующих файлов крайне не рекомендуется, а изменение поведения атрибутов, без изменения их имени или по ложения, может привести к серьезным проблемам.

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

Уровень событий ядра Уровень событий ядра (kernel event layer) — это подсистема, которая позволяет передавать информацию о различных событиях из ядра в пространство пользовате ля и реализована, как вы уже, наверное, догадываетесь, на базе объектов kobject.

После выпуска ядра версии 2.6.0 стало ясно, что необходим механизм для отправле ния сообщений из ядра в пространство пользователя, в частности для настольных рабочих компьютеров, что позволит сделать такие системы более функциональны ми, а также лучше использовать асинхронную обработку. Идея состояла в том, что ядро будет помещать возникающие события в стек. Например, "Жесткий диск пере полнен!", "Процессор перегрелся!", "Раздел диска смонтирован!", "На горизонте по явился пиратский корабль!" (последнее, конечно, шутка).

Первые реализации подсистемы событий ядра появились незадолго до того, как эта подсистема стала тесно связанной с объектами kobject и файловой системой sy sfs. В результате такой связи реализация получилась достаточно красивой. В модели уровня событий ядра, события представляются в виде сигналов, которые посылают ся объектами, в частности объектами типа kobject. Так как объекты отображаются на элементы каталогов файловой системы sysfs, то источниками событий являются определенные элементы пути на файловой системе sysfs. Например, если поступив шее событие связано с первым жестким диском, то адресом источника события яв ляется каталог /sys/block/hda. Внутри же ядра источником события является со ответствующий объект kobject.

Каждому событию присваивается определенная строка символов, которая пред ставляет сигнал и называется командой (verb) или действием (action). Эта строка симво лов содержит в себе информацию о том, что именно произошло, например изменение (modified) или размонтирование (unmounted).

Объекты kobject и файловая система sysfs Каждое событие может нести в себе некоторую дополнительную информацию (дополнительную нагрузку, payload). Вместо того чтобы передавать в пространство пользователя строку, которая содержит эту полезную информацию, данная дополни тельная информация представляется с помощью атрибутов, отображаемых на фай ловой системе sysfs.

События ядра поступают из пространства ядра в пространство пользователя че рез интерфейс n e t l i n k. Интерфейс n e t l i n k - это специальный тип высокоско ростного сетевого сокета групповой передачи (multicast), который используется для передачи сообщений, связанных с сетевой подсистемой. Использование интерфейса n e t l i n k позволяет выполнить обработку событий ядра с помощью простых блоки рующих вызовов функций для чтения информации из сокетов. Задача пространства пользователя— реализовать системный процесс-демон, который выполняет прослу шивание сокета, считывает информацию о всех приходящих событиях, обрабатыва ет их и отправляет полученные сообщения в системный стек пространства пользо вателя. Одна из возможных реализаций такого демона, работающего в пространстве пользователя, — это D-BUS2. который также реализует и системную шину сообщений.

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

Для отправки события в пространство пользователя код ядра должен вызвать функцию k o b j e c t _ u e v e n t ( ).

int kobject_uevent(struct kobject *kobj, enurn kobject_action action, struct attribute *attr);

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

Второй параметр позволяет указать команду или событие, которое описывает сиг нал. Сгенерированное событие ядра будет содержать строку, которая соответству ет номеру, передаваемому в качестве значения параметра enurn k o b j e c t _ a c t i o n.

Вместо того чтобы непосредственно передать строку, здесь используется ее номер, который имеет тип перечисления (enum). Это дает возможность более строго выпол нить проверку типов, изменить соответствие между номером строки и самой строкой в будущем, а также уменьшить количество ошибок и опечаток. Перечисления опре делены в файле и имеют имена в формате KOBJ_foo.

На момент написания книги были определены следующие события: KOBJ_MOUNT, KOBJ_UNMOUNT, KOBJ_ADD, KOBJ_REMOVE и КОВJ_CHANGE. Эти значения отобража ются на строки "mount" (монтирование), "unmount" (размонтироваыие), "add" (до бавление), "remove" (удаление) и "change" (изменение) соответственно. Допускается добавление новых значений событий, если существующих значений недостаточно.

Последний параметр — опциональный указатель на структуру a t t r i b u t e. Этот параметр можно трактовать как дополнительную информацию (payload) о событии.

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

Более подробную информацию о демоне D-BUS можно найти на сайте http://dbus.freedesktop.org/.

370 Глава Рассмотренная функция использует динамическое выделение памяти и поэтому может переходить в состояние ожидания. Существует атомарная версия рассмотрен ной функции, которая идентична ей по всем, кроме того что при выделении исполь зует флаг GFP_ATOMIC.

int kobject_uevent_atomic (struct kobject *kobj, enum kobject_action action, struct attribute *attr);

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

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

Обе рассмотренные функции определены в файле l i b / k o b j e c t _ u e v e n t. с и объявлены в файле .

Кратко об объектах k o b j e c t и файловой системе sysfs В этой главе рассматривается модель представления устройств, файловая си стема sysfs, объекты k o b j e c t и уровень событий ядра. Описание материала главы было бы невозможно без рассмотрения родственных вещей: были также описаны множества kset, подсистемы, атрибуты, типы ktype и счетчики ссылок kref. Эти структуры предназначены для использования разными людьми в разных местах.

Разработчикам драйверов необходимо только ознакомление с внешними интерфей сами. Большинство подсистем драйверов эффективно скрывают внутренние механиз мы использования объектов kobject и других, близких к ним структур. Понимание основных принципов работы и знание основного назначения интерфейсов, таких как s y s f s _ c r e a t e _ f i l e ( ), является достаточным для разработчиков драйверов.

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

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

Объекты kobject и файловая система sysfs Отладка О дин из самых существенных факторов, который отличает разработку ядра от разработки пользовательских приложений, — это сложность отладки.

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

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

С чего необходимо начать Итак, готовы ли вы начать охоту за ошибками? Этот путь может оказаться длин ным и полным разочарований. Некоторые ошибки ставили в тупик все сообщество разработчиков ядра на несколько месяцев. К счастью, на каждую из таких злостных ошибок находятся простые, которые легко исправить. Если вам повезет, то все про блемы, с которыми вы столкнетесь, будут простыми и тривиальными. Однако чтобы это проверить, необходимо начать исследования. Для этого понадобится следую щее.

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

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

• Немного удачи, опыта и их комбинации.

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

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

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

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

от плохой производительности до неправильного функционирования и даже до потери данных.

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

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

Для этого также вероятно потребуется применение блокировок.

374 Глава Отладка ядра может показаться сложным делом, тем не менее, ядро не особо от личается от других больших программных проектов. У ядра есть свои уникальные особенности, такие как ограничения связанные со временем выполнения участков кода, возможности возникновения состояний конкуренции (race) — как результат параллельного выполнения множества потоков в ядре. Можно дать стопроцентную гарантию, что если приложить некоторые усилия и понимание, то проблемы ядра можно с успехом находить и решать (и даже, возможно, получать удовольствие от успешного преодоления трудностей).

Функция p r i n t k ( ) Функция форматированного вывода сообщений p r i n t k ( ) работает аналогично библиотечной функции p r i n t f ( ) языка С. Действительно в этой книге до этого мо мента мы не видели никаких существенных отличий в ее использовании. Для боль шинства задач это именно так: функция p r i n t k ( ) — это просто функция ядра, вы полняющая форматированный вывод сообщений. Однако, некоторые различия все же имеются.

Устойчивость функции p r i n t k ( ) Одно из проверенных и часто используемых свойств функции p r i n t k ( ) — это ее устойчивость. Функцию p r i n t k () можно вызывать практически в любое время и в любом месте ядра. Её можно вызывать из контекста прерывания и из контекста процесса. Её можно вызывать во время удержания блокировки. Её можно вызывать одновременно на нескольких процессорах и она не требует при этом удерживать ка кие-нибудь блокировки.

Эта функция очень устойчива, и это очень важно, потому что полезность функ ции p r i n t k () базируется на том факте, что она всегда доступна и всегда работает.

Неустойчивость функции p r i n t k ( ) Слабое место у функции p r i n t k ( ) в плане устойчивости все же существует. Её нельзя использовать до некоторого момента при загрузки ядра, пока консоль еще не инициализирована. Действительно, если нет консоли, то куда будут выводится со общения?

Обычно это не проблема, если не нужно выполнять отладку кода, который выполняется на очень ранних стадиях процесса загрузки (например, функции s e t u p _ a r c h ( ), которая выполняет инициализацию специфичную для аппаратной платформы). Отладка такого рода— настоящая задача: отсутствие каких-либо спосо бов вывода сообщений, а только проблема в полном составе.

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

Отладка Одно из решений проблемы — вариант функции p r i n t k ( ), который может выводить информацию па консоль на очень ранних стадиях процесса загрузки e a r l y _ p r i n t k ( ). Поведение этой функции аналогично функции p r i n t k ( ), за исключением имени и возможности работать на очень ранних стадиях загрузки.

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

Кроме ситуаций, когда необходимо выводить на консоль информацию на очень ранних стадиях загрузки системы, можно положиться на функцию p r i n t k ( ), кото рая работает практически всегда.

Уровни вывода сообщений ядра Главное отличие между функциями p r i n t k ( ) и p r i n t f ( ) — это возможность в первой указывать уровень вывода сообщений ядра (loglevel). Ядро использует уровень вы вода сообщений для принятия решения о том, выводить сообщение на консоль или нет. Ядро выводит на консоль все сообщение с уровнями меньшими, или равными, соответствующему значению для консоли (console logleyel). Уровень вывода сообще ний можно указывать следующим образом.

printk(KERN_WARNTNG "Это предупреждение!\n");

printk(KERN_DEBUG "Это отладочное сообщение!\n");

p r i n t k ( " М ы не указали значения l o q l e v e l ! \ n " );

Строки KERN_WARNING и KERN_DEBUG определены через препроцессор в заголо вочном файле < l i n u x / k e r n e l. h >. Эти макросы раскрываются в строки, соответ стпенно "<4>" и "<7>", которые объединяются со строкой формата в самом начале сообщения, выводимого функцией p r i n t k ( ). После этого на основании уровня выпо да сообщения и уровня вывода консоли (значение переменной c o n s o l e _ l o g l e v e l ) ядро принимает решение выводить информацию на консоль или нет. В табл. 18. приведен полный список возможных значений уровня вывода сообщений.

Таблица 18.1. Доступные значения уровня вывода сообщений ядра (loglevel) Значение loglevel Описание KERN_EMERG Аварийная ситуация KERN_ALERT Проблема, на которую требуется немедленно обратить внимание KERN_CRIT Критическая ситуация KERN_ERR Ошибка KERN_WARNING Предупреждение KERN_NOTICE Обычная ситуация, но на которую следует обратить внимание KERN_INFO Информационное сообщение KERN_DEBUG Отладочное сообщение — обычно избыточная информация Если уровень вывода сообщений ядра не указан, то его значение по умолча нию равно DEFAULT_MESSAGE_LOGLEVEL, который в данный момент равен KERN_ WARNING. Так как это значение может измениться, то для своих сообщений необхо димо всегда указывать уровень вывода.

376 Глава Наиболее важный уровень вывода— KERN_EMERG определен как "<0>", а наиме нее важный — KERN_DEBUG, как "<7>". Например, после обработки препроцессором кода из предыдущего примера получается следующее.

printk("<4>Этo предупреждение!\n");

printk("<7>Это отладочное сообщение!\n");

printk("<4>Мы не указали значения loglevel!\n");

Как вы будете использовать функцию p r i n t k ( ) зависит только от вас. Конечно, обычные сообщения, которые должны быть видимы, должны иметь соответству ющий уровень вывода. Отладочные сообщения, которые в большом количестве встраиваются в самые разные места кода с целью разобраться с проблемой — "до пустим ошибка здесь", "пробуем", "работает" - могут иметь любой уровень вывода.

Один вариант — оставить уровень при котором сообщения выводятся на консоль равным значению этого параметра по умолчанию, а уровень вывода ваших сообще ний установить в значение KERN_CRIT, или что-то около этого. Можно поступить и наоборот— для отладочных сообщений установить уровень KERN_DEBUG и поднять уровень при котором сообщения выводятся на консоль. Каждый из вариантов имеет свои положительные и отрицательные стороны — вам решать.

Уровни вывода сообщений определены в файле < l i n u x / k e r n e l. h >.

Буфер сообщений ядра Сообщения ядра хранятся в кольцевом буфере (log buffer) размером LOG_BUF_LEN.

Этот размер можно изменять во время компиляции с помощью параметра CONFIG_ LOG_BUF_SHIFT. Для однопроцессорной машины это значение по умолчанию равно 16 Кбайт. Другими словами в ядре может хранится до 16 Кбайт системных сообще ний. Если общий размер всех сообщений ядра достигает этого максимального зна чения и приходит новое сообщение, то оно переписывается поверх самого старого из хранящихся в буфере сообщений. Буфер сообщений ядра называется кольцевым, потому что запись и считывание сообщений выполняется по круговой схеме.

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

Так как одновременные операции чтения и записи в кольцевом буфере выполняют ся достаточно просто, то функцию p r i n t k ( ) можно использовать даже из контекста прерывания. Более того, это позволяет просто организовать управление системны ми сообщениями. Если сообщений оказывается очень много, то новые сообщения просто затирают старые. Если возникает проблема, которая проявляется в генера ции большого количества сообщений, то буфер сообщений просто начинает пере писывать себя вместо того, чтобы бесконтрольно занимать память. Единственный недостаток кольцевого буфера — возможность потерять сообщения, что не такая уж и большая плата за ту устойчивость, которую такое решение предоставляет.

Демоны syslogd и klogd В стандартной системе Linux для извлечения сообщений ядра из буфера исполь зуется специальный демон пространства пользователя klogd, который направляет эти сообщения в файл журнала системных сообщений. Для чтения системных со общений программа k l o g d может считывать данные из файла / p r o c / k m s g, или использовать системный вызов s y s l o g ( ). По умолчанию используется подход на Отладка основе файловой системы /рrоc. Если сообщений нет, то демон klogd блокируется на операции чтения, пока не поступит новое сообщение. Когда приходит новое со общение, демон возвращается к выполнению, считывает сообщения и обрабатывает их. По умолчанию сообщения отправляются демону syslogd.

Демон s y s l o g d добавляет полученные сообщения в конец файла журнала, по умолчанию — / v a r / l o g / m e s s a g e s. Имя соответствующего файла можно настроить в конфигурационном файле / e t c / s y s l o g. c o n f.

Изменить уровень вывода сообщений на консоль (console loglevel) можно при старте демона klogd с помощью флага -с.

Замечание относительно функции p r i n t k ( ) и разработки ядра Когда впервые начинают разрабатывать код ядра, то скорее всего очень часто приходится заменять функцию p r i n t f ( ) на функцию p r i n t k ( ). Это нормально, по тому что нельзя не принимать во внимание многолетний опыт по написанию поль зовательских программ и использовании функции p r i n t f ( ). Следует надеяться, что повторение таких ошибок не будет продолжаться долго, потому что повторяющиеся ошибки компоновщика начнут быстро надоедать.

Однажды вдруг окажется, что вы поймали себя на том, что начали использовать функцию p r i n t k ( ) вместо функции p r i n t f ( ) в пользовательских программах.

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

Сообщения Oops Сообщения oops — обычный для ядра способ сообщить пользователю, что произо шло что-то нехорошее. Так как ядро управляет всей системой, то оно не может само себя исправить, или завершить, как это возможно для программ пространства поль зователя, когда они делают что-то не так. Вместо этого, ядро выводит сообщение oops. Такое сообщение включает вывод информации об ошибке на консоль, вывод дампа содержимого всех регистров и вывод обратной трассировки вызовов функций (back trace). Сбои в работе ядра трудно обработать, поэтому ядро должно "пролезть'' через многие дыры, чтобы вывести сообщение oops и выполнить за собой все не обходимые действия по очистке. Часто после выдачи сообщения oops ядро нахо дится в несогласованном состоянии. Например, в момент возникновения ситуации, в которой выдается сообщение oops, ядро может находится в процессе обработки важных данных. В этот момент может удерживаться блокировка, или выполняться сеанс взаимодействия с оборудованием. Ядро должно аккуратно отойти от текуще го состояния и попытаться восстановить контроль над системой. Во многих случаях это невозможно. Если ситуация, в которой выдается сообщение oops, возникает в контексте прерывания, то ядро не может продолжать работу и переходит в состо яние паники. Состояние паники проявляется в полной остановке системы. Если oops возникает в холостой задаче (idle task, идентификатор p i d равен нулю), или при выполнении процесса i n i t (идентификатор p i d равен единице), то ядро также переходит в состояние паники, потому что ядро не может продолжать выполнение 378 Глава без этих важных процессов. Однако, если oops возникает при выполнении любого другого процесса, то ядро завершает этот процесс и продолжает работу.

Сообщение oops может выдаваться по многим причинам, включая недопустимый доступ к памяти (memory access violation) и выполнение недопустимой машинной команды. Как разработчику ядра, вам придется иметь дело с сообщениями oops и далее, несомненно, быть причиной их появления.

Ниже показано сообщение oops для машины аппаратной платформы РРС, кото рое возникло и обработчике таймера для сетевого интерфейсного адаптера tulip.

Oops: Exception in kernel mode, sig: Unable to handle kernel NULL pointer dereference at virtual address NIP: C013A7F0 LR: C013A7F0 SP: C0685E00 REGS: c0905dl0 TRAP: Not tainted MSR: 00089037 ЕЕ: 1 PR: 0 FP: 0 ME: 1 IR/DR: TASK = c0712530[0] swapper Last syscall: GPROO: C013A7C0 C0295E00 C0231530 0000002F 00000001 C0380CB8 C0291B80 C02D GPR08: 000012AO 00000000 00000000 C0292AA0 4020A088 00000000 00000000 GPR16: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 GPR24: 00000000 00000005 00000000 00001032 C3F7C000 00000032 FFFFFFFF C3F7C1C Call trace:

[c013ab30] tulip_timer+0xl28/0xlc [c0020744] run_timer_softirq+0xl0c/0xl [c001b864] do_softirq+0x88/0xl [c0007e80] timer_mterrupt+0x284/0x [c00033c4] ret_from_except+0x0/0x [c0007b84] default_idle+0x20/0x [c0007bf8] cpu_idle+0x34/0x [c0003ae8] rest_init+0x24/0x У пользователей ПК может вызвать удивление количество регистров процессора (32 - огромное число!). Сообщение oops для аппаратной платформы х86, которые возможно вам более знакомы, имеют несколько более простой вид. Тем не менее, важная информация идентична для всех аппаратных платформ: содержимое всех ре гистров и обратная трассировка.

Обратная трассировка показывает точную последовательность вызовов функ ций, которая привела к проблеме. В данном случае можно точно определить, что случилось: машина выполняла холостое задание - холостой цикл: вызов функ ции c p u _ i d l e ( ), из которой циклически вызывается функция d e f a u l t _ i d l e ( ).

Поступило прерывание от системного таймера, в котором вызываются обра ботчики таймеров ядра. Среди них вызывается обработчик таймера — функция t u l i p _ t i m e r ( ), в которой выполнено разыменование указателя со значением NULL.

Можно даже воспользоваться значением смещения (числа вроде 0х128/0х1с4, кото рые указаны справа от имени функции) для точного нахождения команды, в кото рой возникла ошибка.

Содержимое регистров точно также полезно, хотя и используется не так часто.

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

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

NIP: C013A7F0 LR: C013A7F0 SP: C0685E00 REGS: c0905dl0 TRAP: Not tainted MSR: 00089037 ЕЕ: 1 PR: 0 FP: 0 ME 1 IR/DR: TASK = c0712530[0] 'swapper' Last syscall: GPROO: C013A7CO C0295E00 C0231530 0000002F 00000001 C0380CB8 C0291B80 C02D GPR08: 000012AO 00000000 00000000 C0292AA0 4020A088 00000000 00000000 GPR16: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 GPR24: 00000000 00000005 00000000 00001032 C3F7C000 00000032 FFFFFFFF C3F7C1C Call trace: [c013ab30] [c0020744] [c001b864] [c0007e80] [c00061c4] [c0007b84] [c0007bf8] [c0003ae8] Адреса обратной трассировки должны быть переведены в символические име на функций. Это можно сделать с помощью команды ksymoops при наличии файла System.map, который сгенерирован во время компиляции данного ядра. Если ис пользуются загружаемые модули ядра, то необходима также информация о модулях.

Утилита ksymoops пытается самостоятельно определить всю необходимую информа цию, поэтому обычно ее можно просто вызывать следующим образом.

ksymoops saved_oops.txt Программа выводит декодированную информацию сообщения oops. Если инфор мация, которая используется по умолчанию, недоступна, или есть необходимость указать альтернативное положение соответствующих информационных файлов, то на такой случай программа принимает различные параметры. Страницы руковод ства по данной программе, которые необходимо прочитать перед использованием, содержат всю необходимую информацию.

Программа ksymoops включена в большинство поставок операционной системы Linux.

Функция k a l l s y m s К счастью, больше нет необходимости использовать программу ksymoops. Это очень полезно, потому что, хотя, у разработчиков обычно нет проблем с ее исполь зованием, пользователи часто указывают неправильный файл System.map, или не правильно декодируют сообщение oops.

В разрабатываемой серии ядра 2.5 была введено новая возможность k a l l s y m s, которая включается с помощью конфигурационного параметра CONFIG_KALLSYMS.

Эта функция включает в исполняемый образ ядра информацию для отображения адресов памяти в соответствующие имена функций ядра, что дает возможность ядру 380 Глава самостоятельно декодировать информацию обратной трассировки. Следовательно, декодирование сообщений oops больше не требует файла System.map, или утилиты kallsyms. Как недостаток такого подхода следует отметить некоторое увеличение объема памяти, используемой ядром, так как таблица перевода адресов памяти в имена функций загружается в постоянно отображаемую память ядра. На такое уве личение объемов используемой памяти стоит пойти, по крайней мере, на этапе раз работки ядра.

Конфигурационные параметры отладки ядра Существует несколько конфигурационных параметров, которые помогают в от ладке и тестировании кода ядра и которые включаются во премя компиляции. Эти параметры доступны в пункте Kernel hacking меню редактора конфигурации ядра.

Все эти параметры зависят от параметра CONFIG_DEBUG_KERNEL. Для разработки кода ядра следует включать только те параметры, которые необходимы.

Некоторые из этих параметров достаточно полезны, такие как отладка работы со слябовым распределителем памяти (slab layer debugging), отладка работы с верхней памятью (high memory debugging), отладка работы с отображаемым на память вво дом-выводом (I/O mapping debugging), отладка работы со спин-блокировками (spin lock debugging) и проверка переполнения стека (stack overflow checking). Однако, один из самых полезных параметров — это проверка перехода в состояние ожидания при захваченной спин-блокировке (sleep-inside-spinlock checking), которая на самом деле выполня ет значительно больше работы.

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

Код, который удерживает спин-блокировку, или выполняется при запрещенной пре емптивности ядра, является атомарным. Во время атомарного выполнения нельзя переходить в состояние ожидания. Ожидание при удерживаемой спин-блокировке — один из вариантов взаимоблокировки.

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

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

Отладка CONFIG_PREEMPT=y CONFIG_DEBUG_KERNEL=y CONFIG_KALLSYMS=y CONFIG_SPINLOCK_SLEEP=y Генерация ошибок и выдача информации Существует несколько подпрограмм ядра, которые позволяют легко сигнали зировать о наличии дефектов кода, обеспечивать объявления об ошибках и выво дить необходимую информацию. Две наиболее часто используемые — это BUG() и BUG_ON(). При вызове эти функции создают ситуацию oops, которая проявляется в выводе обратной трассировки стека ядра и сообщения об ошибке. Каким обра зом эти вызовы генерируют ситуацию oops зависит от аппаратной платформы. Для большинства аппаратных платформ вызовы BUG() и BUG_ON() определяются как некоторая недопустимая машинная команда, которая приводит к выводу желаемого сообщения oops.

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

if (bad_thing) BUG();

Или даже так.

BUG_ON(bad_thing);

О более критичной ошибке можно сигнализировать с помощью функции panic ().

Функция p a n i c () печатает сообщение об ошибке и останавливает ядро. Ясно, что эту функцию следует использовать только в самой плохой ситуации.

if (terrible_thing) panic("foo is %ld!\n", foo);

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

if (!debug_check) { printk(KERN_DEBUG "выдать некоторую информацию... \ n " ) ;

dump_stack();

} Магическая клавиша SysRq Использование магической клавиши SysRq, которую можно активизировать с по мощью конфигурационного параметра CONFIG_MAGIC_SYSRQ на этапе компиляции, часто позволяет значительно облегчить жизнь. Клавиша SysRq является стандарт ной на многих клавиатурах. Для аппаратных платформ i386 и РРС ей соответствует комбинация клавиш A L T - P r i n t S c r e e n. Если указанный конфигурационный пара метр активизирован, то специальные комбинации клавиш позволяют взаимодейство 382 Глава вать с ядром независимо от того, чем ядро в данный момент нанимается. Это в свою очередь позволяет выполнять некоторые полезные операции даже на неработоспо собной системе.

В дополнение к конфигурационному параметру существует вызов s y s c t l для включения и выключения этого свойства.

echo 1 > /proc/sys/kernel/sysrq Список возможных комбинаций клавиш можно получить с консоли путем на жатия комбинации клавиш SysRq-h. Комбинация клавиш SysRq-s выполняет синхронизацию не сохраненных буферов файловых систем на диск, комбинация SysRq-u размонтирует все файлопые системы, a SysRq-b — перегружает машину.

Последовательное использование этих комбинаций клавиш позволяет более безопас но перегрузить машину, которая зависла, чем простое нажатие кнопки r e s e t.

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

Таблица 18.2. Список поддерживаемых команд SysRq Команда Описание SysRq-b Перегрузить машину (reboot) SysRq-e Послать сигнал S I G T E R M всем процессам, кроме процесса i n i t SysRq-h Отобразить на консоли помощь по использованию комбинаций клавиш SysRq SysRq-i Послать сигнал SIGKILL всем процессам, кроме процесса i n i t SysRq-k Клавиша безопасного доступа: завершить все процессы, связанные с текущей консолью i SysRq-l Послать сигнал S I G K I L L всем процессам, включая процесс i n i t SysRq-m Отобразить на консоли дамп информации по использованию памяти SysRq-o Завершить работу машины (shutdown) SysRq-p Отобразить на консоли дамп регистров памяти SysRq-r Отключить прямой режим работы клавиатуры (raw mode) SysRq-s Синхронизировать данные смонтированных файловых систем с дисковыми устройствами SysRq-t Отобразить на консоли дамп информации о заданиях SysRq-u Размонтировать все смонтированные файловые системы В файле D o c u m e n t a t i o n / s y s r q. t x t, который находится в каталоге исходных кодов ядра, приводится более полное описание. Реализация поддержки магической комбинации клавиш находится в файле d r i v e r s / c h a r / s y s r q. с. Магические ком бинации клавиш SysRq — жизненно необходимый инструмент, который помогает в отладке и сохранении "гибнущей" системы, так как предоставляет большие возмож ности для любого пользователя при работе с консолью. Тем не менее необходимо соблюдать осторожность при его использовании на критичных машинах. Если же машина используется для разработок, то польза от этих команд огромная.

Отладка Сага об отладчике ядра Многие разработчики ядра давно высказываются о необходимости встроенного в ядро отладчика. К сожалению, Линус не желает видеть отладчик ядра в своем де реве исходного кода, Он уверен, что использование программ-отладчиков приводит к плохому исправлению ошибок неправильно информированными разработчиками.

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

Перед тем, как обращаться к этим решениям, посмотрим, на сколько нам может по мочь стандартный отладчик ОС Linux - gdb.

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

gdb vmlinux /proc/kcore Файл vmlinux — это декомпрессированный исполняемый образ ядра, который хранится в корне каталога исходных кодов, где выполнялась сборка выполняющего ся ядра. Сжатые файлы zlmage, или bzlmage использовать нельзя.

Опциональный параметр /proc/kcore исполняет роль файла core, чтобы позво лить отладчику читать из памяти выполняющегося ядра. Чтобы иметь возможность читать этот файл, необходимо иметь права пользователя root.

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

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

disassemble function Если ядро было скомпилировано с указанием флага -g (необходимо добавить -g к значению переменной CFLAGS в файле Makefile ядра), то отладчик gdb сможет выдавать больше информации. Например, можно выводить дампы структур данных и разыменовывать указатели. При этом также получается ядро значительно больше го размера, поэтому для обычной работы не следует компилировать ядро с отладоч ной информацией.

К сожалению, на этом заканчиваются возможности использования отладчика gdb.

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

Отладчик kgdb Отладчик kgdb —это заплата ядра, которая позволяет с помощью отладчика gdb отлаживать ядро по линии последовательной передачи. Для этого требуется два ком пьютера. На перпом выполняется ядро с заплатой kgdb. Второй компьютер использу ется для отладки ядра по линии последовательной передачи (нуль-модемный кабель, соединяющий две машины) с помощью gdb. Благодаря отладчику kgdb полностью доступен весь набор функций gdb: чтение и запись любых переменных, установка точек остановки, установка точек слежения (watch points), пошаговое исполнение и др.. Специальные версии kgdb даже позволяют вызывать функции.

Установка kgdb и линии последовательной передачи несколько сложная проце дура, но если ее выполнить, то отладка ядра значительно упрощается. Заплата ядра также устанавливает большое количество документации в каталог Documentation/, ее следует прочитать.

Несколько человек выполняют поддержку заплаты kgdb для различных аппарат ных платформ и версий ядра. Поиск в Интернет — наилучший способ найти необхо димую заплату для заданного ядра.

Отладчик kdb Альтернативой kgdb является отладчик kdb. В отличие от kgdb отладчик kdb — не удаленный отладчик. Отладчик kdb — это заплата, которая сильно модифициру ет ядро и позволяет выполнять отладку прямо на той же машине, где выполняется ядро. Кроме всего прочего поддерживается возможность изменения переменных, установки точек остановки и пошаговое выполнение. Выполнять отладку просто — необходимо нажать на консоли клавишу b r e a k. При выводе сообщения oops пере ход в отладчик выполняется автоматически. Более подробная документация доступ на в каталоге Documentation/kdb после применения заплаты. Заплата kdb доступна в Интернет по адресу h t t p : / / o s s. s g i. c o m /.

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

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

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

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

if (current->uid != 7777) { /* старый алгоритм.. */ } else { /* новый алгоритм.. */ } lice пользователи, кроме того, у которого идентификатор UID равен 7777 будут использовать старый алгоритм. Для тестирования нового алгоритма можно создать нового пользователя с идентификатором 7777. Это позволяет более просто оттести ровать критические участки кода, связанные с выполнением процессов.

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

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

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

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

unsigned long foo_stat = 0;

unsigned long bar_stat = 0;

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

Следует обратить внимание, что такой подход принципиально не безопасен на SMP машине. В идеале необходимо использовать атомарные переменные. Однако, 386 Глава для временной статистики, которая необходима только для отладки, никакой защи ты обычно не требуется.

Ограничение частоты следования событий при отладке Часто необходимо встроить в код отладочные проверки (с соответствующими функциями вывода информации), чтобы визуально производить мониторинг пробле мы. Однако, в ядре некоторые функции вызываются по много раз в секунду. Если в такую функцию будет встроен вызов функции p r i n t k (), то системная консоль будет перегружена выводом отладочных сообщений и ее будет невозможно использовать.

Для предотвращения такой проблемы существует два сравнительно простых при ема. Первый — ограничение частоты следования событий — очень полезен, когда не обходимо наблюдать, как развивается событие, но частота возникновения события очень большая. Чтобы ограничить поток отладочных сообщений, эти сообщения вы водятся только раз в несколько секунд, как это показано в следующем примере.

static unsigned long prev_jiffy = jiffies;

/* ограничение частоты */ if (time_after (jiffies, prev_jiffy + 2*HZ)) { prev_jiffy = jiffies;

printk (KERN_ERR "blah blah blah\n");

} В этом примере отладочные сообщения выводятся не чаще, чем один раз в две секунды. Это предотвращает перегрузку консоли сообщениями и системой можно нормально пользоваться. Частота вывода может быть большей, или меньшей, в за висимости от требопаний.

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

static unsigned long limit = 0;

if (limit < 5) { limit++;

printk(KERN_ERR "blah blah blah\n");

}., В этом примере количество отладочных сообщений ограничено числом пять.

После пяти сообщений условие всегда будет ложно.

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

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

Отладка Нахождение исполняемых образов с изменениями приводящими к ошибкам Обычно полезно знать, в какой версии исходных кодов ядра появился дефект.

Если известно, что дефект появился в версии 2.4.18, но его не было в версии 2.4.17, то сразу появляется ясная картина изменений, которые привели к появлению ошиб ки. Исправление ошибки сводится к обратным изменениям, или другим исправлени ям измененного кода.

Однако, чаще оказывается неизвестным в какой версии появился дефект.

Известно, что проблема проявляется в текущей версии ядра, и кажется, что она всег да была в текущей версии. Хотя это и требует некоторой исследовательской работы, но приложив небольшие усилия можно найти изменения, которые привели к ошиб кам. Если известны эти изменения, то до исправления ошибки уже недалеко.

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

Далее, необходимо ядро, в котором гарантированно есть дефект. Для облегче ния поиска следует воспользоваться наиболее ранней версией ядра, в которой есть дефект. После этого начинается поиск исполняемого образа, в котором появилась ошибка. Рассмотрим пример. Допустим, что последнее ядро, в котором не было ошибки— 2.4.11, а последнее с ошибкой— 2.4.20. Начинаем с версии ядра, которая находится посредине— 2.4.15. Проверяем версию 2.4.15 на наличие ошибки. Если версия 2.4.15 работает, значит ошибка появилась в более поздней версии. Следует попробовать версию между 2.4.15 и 2.4.20, скажем версию 2.4.17. С другой стороны, если версия 2.4.15 не работает, то тогда ясно, что ошибка появилась в более ранней версии, и следует попробовать версию, скажем 2-4.13. И так далее.

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

Такой подход избавляет от необходимости проверять ядра всех версий!

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

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

Глава 20, "Заплаты, разработка и сообщество" специально посвящена сообществу разработчиков ядра и его основному форуму — списку рассылки разработчиков ядра Linux (Linux Kernel Mail List, LKML).

388 Глава Переносимость L inux — переносимая операционная система, которая поддерживает большое ко личество различных компьютерных аппаратных платформ. Переносимость— это свойство, которое указывает, насколько легко можно перенести код, который вы полнялся на одной аппаратной платформе, для выполнения на другой аппаратной платформе (если вообще это возможно). Известно, что ОС Linux является переноси мой операционной системой, поскольку ее уже перенесли (портировали) па большое количество различных аппаратных платформ. Тем не менее переносимость не воз никает сама по себе, для выполнения она требует большого количества проектных решений. Сегодня процесс перенесения ОС Linux на другую аппаратную платформу достаточно прост (в относительном смысле, конечно). В этой главе рассказывается о том, как писать переносимый код, — вопрос, о котором всегда необходимо помнить при написании нового кода ядра или драйверов устройств.

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

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

С противоположной стороны можно поставить операционные системы, в кото рых оптимизация кода выполняется в ущерб переносимости. Код, по возможности, пишется на языке ассемблера или специфические свойства аппаратных платформ ис пользуются каким-либо другим образом. Свойства ядра разрабатываются на основа нии свойств аппаратной платформы. В этом случае перенос операционной системы на другую аппаратную платформу сводится к написанию ядра с нуля. Оптимальность выполняется в ущерб переносимости. Примером таких систем могут быть DOS и Windows 9x. Сегодня таким системам нет необходимости иметь более оптимальный код, чем переносимым операционным системам, однако они предоставляют возмож ность в максимальной степени использовать ручную оптимизацию кода.

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

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

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

Хороший пример — планртровщик. Большая часть планировщика-написана незави симым от аппаратной платформы образом на языке С. Реализация находится в фай ле k e r n e l / s c h e d. с. Некоторые из функций планировщика, такие как переключе ние состояния процессора или переключение адресного пространства, очень сильно зависят от аппаратной платформы. Следовательно, функция c o n t e x t _ s w i t c h (), ко торая переключает выполнение от одного процесса к другому и написана на языке С, вызывает функции s w i t c h _ t o () и Kwitch_ram () для переключения состояния процессора и переключения адресного пространства соответственно.

Код функций s w i t c h _ t o () и s w i t c h _ m m () выполнен отдельно для каждой ап паратной платформы, которую поддерживает операционная система Linux. Когда операционная система Linux портируется на новую аппаратную платформу, то для новой аппаратной платформы просто необходимо реализовать эти функции.

Файлы, которые относятся к определенной аппаратной платформе, находятся в каталоге arch/<аппаратная платформа>/ и include/asm-/, где <апппаратная платформа> — это короткое имя, которое представляет аппарат ную платформу, поддерживаемую ядром Linux. Например, аппаратной платформе Intel х8б присвоено имя i386. Для этого типа машин файлы находятся в каталогах a r c h / i 3 8 6 и i n c l u d e / a s m - i 3 8 6. Ядра серии 2.6 поддерживают следующие аппа ратные платформы: a l p h a, arm, c r i s, h8300, i38, i a 6 4, m68k, m68knommu, mips, mips64, p a r i s c, ppc, ppc64, s390, sh, s p a r e, s p a r c 6 4, um, v850 и х8б-б4. Более полное описание приведено в табл. 19.1.

История переносимости Linux Когда Линус Торвальдс впервые выпустил операционную систему Linux в ничего не подозревающий мир, эта ОС работала только на аппаратной платформе Intel i386.

Хотя данная операционная система и была достаточно хорошо обобщена и хорошо написана, переносимость для нее не была основным требованием. Однажды Линус даже говорил, что операционная система Linux не будет работать ни на какой ап паратной платформе, кроме i386! Тем не менее в 1993 году началась работа по пор 390 Глава тиропаиию ОС Linux на машины Digital Alpha. Аппаратная платформа Digital Alpha была повой высокопроизводительной RISC-платформой с поддержкой 64-разрядной адресации памяти. Она очень сильно отличалась от аппаратной платформы i386, о которой говорил Линус. Тем не менее, первоначальный перенос на аппаратную плат форму Alpha занял около года, и аппаратная платформа Alpha стала первой офици ально поддерживаемой аппаратной платформой после х8б. Это портировапие было, наверное, самым сложным, потому что — первым. Вместо простого переписывания ядра для поддержки новой аппаратной платформы, части ядра были переписаны с целью введения переносимости. Хотя это и привело к выполнению большого коли чества работы, в результате получился более ясный для понимания код, и в будущем перенос стало выполнять более просто.

Первые выпуски ОС Linux поддерживали только платформу i386, а серия ядер 1.2 уже поддерживала Digital Alpha, Intel x86, MIPS и SPARC, хотя такая поддержка была отчасти экспериментальной.

С выпуском ядра версии 2.0 была добавлена официальная поддержка платформ Motorola 68k и PowerPC. В дополнение к этому поддержка всех аппаратных плат форм, которые ранее поддерживались ядрами серии 1.2, стала официальной и ста бильной.

В серию ядер 2.2 была введена поддержка еще большего количества аппаратных платформ: добавлены ARM, IBM S/390 и UltraSPARC. Через несколько лет в серии ядер 2.4 количество поддерживаемых аппаратных платформ было почти удвоено, и их количество стало равным 15. Была добавлена поддержка платформ CRIS, IA-64, 64-разрядная MIPS, HP PA-RISC, 64-разрядная IBM S/390 и Hitachi SH.

В серии 2.6 количество поддерживаемых аппаратных платформ было доведено до 20 за счет добавления платформ Motorola 68k бел устройства MMU, Н8/300, IBM POWER, v850, x86-64 и версии ядра, которое работает на виртуальной машине под ОС Linux - Usermode Linux. Поддержка 64-разрядной s390 была объединена с 32 разрядной платформой s390, чтобы избежать дублирования.

Необходимо заметить, что каждая из этих аппаратных платформ поддерживает различные типы машин и микросхем. Некоторые из поддерживаемых аппаратных платформ, такие как ARM и PowerPC, поддерживают очень большое количество ти пов микросхем и машин. Поэтому, хотя ОС Linux и работает на 20 аппаратных плат формах, она работает на гораздо большем количестве типов компьютеров!

Размер машинного слова и типы данных Машинное слово (word) — это количество данных, которые процессор может обработать за одну операцию. Здесь можно применить аналогию документа, состо ящего из символов (character, 8 бит) и страниц (много слов). Слово— это некоторое количество битов, как правило 16, 32 или 64. Когда говорят о "n-битовой" машине, то чаще всего имеют в виду размер машинного слова. Например, когда говорят, что процессор Intel Pentium — это 32-разрядный процессор, то обычно имеют в виду раз мер машинного слова, равный 32 бит, или 4 байт.

Это нормальная ситуация при разработке ядра. Если что-либо должно быть сделано, то это долж но быть сделано хорошо! Разработчики ядра неохотно переписывают большие участки кода даже во имя совершенства.

Переносимость Размер процессорных регистров общего назначения равен размеру машинного слова этого процессора. Обычно разрядность остальных компонентов этой же аппа ратной платформы в точности равна размеру машинного слова. Кроме того, по край ней мере для аппаратных платформ, которые поддерживаются ОС Linux, размер адресного пространства соответствует размеру машинного слова2. Следовательно, размер указателя равен размеру машинного слова. В дополнение к этому, размер типа long языка С также равен размеру машинного слова. Например, для аппарат ной платформы Alpha размер машинного слова равен 64 бит. Следовательно, реги стры, указатели и тип long имеют размер 64 бит. Тип i n t для этой платформы име ет размер 32 бит. Машины платформы Alpha могут обработать 64 бит— одно слово с помощью одной операции.

Слова, двойные слова и путаница Для некоторых операционных систем и процессоров стандартную порцию данных не называют машинным словом. Вместо этого, словом называется некоторая фиксированная порция данных, название которой выбрано случайным образом или имеет исторические корни. Например, в не которых системах данные могут разбиваться на байты (byte — 8 бит), слова (word — 16 бит), двойные слова (double w o r d — 32 бит) и четверные слова (quad w o r d — 64 бит), несмотря на то что на самом деле система является 32-разрядной. В этой книге и вообще в контексте опе рационной системы Linux под машинным словом понимают стандартную порцию данных про цессора, как обсуждалось ранее.

Для каждой аппаратной платформы, поддерживаемой операционной системой Linux, в файле определяется константа BITTS_PER_LONG, которая равна размеру типа long языка С и совпадает с размером машинного слова системы.

Полный список всех поддерживаемых аппаратных платформ и их размеры машин ного слова приведены в табл. 19.1.

Стандарт языка С явно указывает, что размер памяти, которую занимают пере менные стандартных типов данных, зависит от аппаратной реализации3, при этом также определяется минимально возможный размер типа. Неопределенность разме ров стандартных типов языка С для различных аппаратных платформ имеет свои положительные и отрицательные стороны. К плюсам можно отнести то, что для стандартных типов языка С можно пользоваться преимуществами, связанными с раз мером машинного слова, а также отсутствие необходимости явного указания разме ра. Для ОС Linux размер типа long гарантированно равен размеру машинного слова.

Это не совсем соответствует стандарту ANSI С, однако является стандартной прак тикой в ОС Linux. Как недостаток можно отметить, что при разработке кода нельзя рассчитывать на то, что данные определенного типа занимают в памяти определен ный размер. Более того, нельзя гарантировать, что переменные типа int занимают столько же памяти, сколько и переменные типа long.

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

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

За исключением размера типа c h a r, который всегда равен 8 бит.

На самом деле, для 64-разрядных аппаратных платформ, которые поддерживаются ОС Linux, раз меры типов i n t и l o n g не совпадают. Размер типа int равен 32 бит, а размер типа l o n g — 64 бит.

Для хорошо знакомых 32-раphядных аппаратных платформ оба типа данных имеют размер 32 бит.

392 Глава Таблица 1 9. 1. Поддерживаемые аппаратные платформы Аппаратная платформа Описание Размер машинного слова alpha Digital Alpha 64 бит ARM и StrongARM arm 32 бит cris CRIS 32 бит h8300 32 бит H8/ I386 32 бит Intel x ia64 IA-64 64 бит m68k 32 бит Motorola 68k 32 бит m86knommu m68k без устройства MMU 32 бит mips MIPS 64-разрядная MIPS 64 бит mips HP PA-RISC 32 бит, или 64 бит parisc 32 бит ppc PowerPC 64 бит ppc64 POWER 32 бит, или 64 бит s390 IBM S/ 32 бит sh Hitachi SH 32 бит spare SPARC sparc64 UltraSPARC 64 бит um Usermode Linux 32 бит, или 64 бит 32 бит v850 v x8_ 64 X86-64 64 бит Ситуация еще более запутывается тем, что одни и те же типы данных в простран стве пользователя и в пространстве ядра не обязательно должны соответствовать друг другу. Аппаратная платформа sparc64 предоставляет 32-разрядное пространство пользователя, а поэтому указатели, типы i n t и long имеют размер 32 бит. Однако в пространстве ядра для аппаратной платформы размер типа i n t равен 32 бит, а раз мер указателей и типа long равен 64 бит. Тем не менее такая ситуация не является обычной.

Всегда необходимо помнить о следующем.

• Как и требует стандарт языка С, размер типа char всегда равен 8 бит (1 байт), • Нет никакой гарантии, что размер типа i n t для всех поддерживаемых аппа ратных платформ будет равен 32 бит, хотя сейчас для всех платформ он равен именно этому числу.

• То же касается и типа short, который для всех поддерживаемых аппаратных платформ сейчас равен 16 бит.

• Никогда нельзя надеяться, что тип long или указатель имеет некоторый задан ный размер. Этот размер для поддерживаемых аппаратных платформ может быть равен 32, или 64 бит.

Переносимость • Так как размер типа long разный для различных аппаратных платформ, никог да нельзя предполагать, что s i z e o f (int.) == s i z e o f ( l o n g ).

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

Скрытые типы данных Скрытые (opaque) типы данных — это те типы, для которых не раскрывается их внутренняя структура, или формат. Они похожи на черный ящик, насколько это можно реализовать в языке программирования С. В этом языке программирования нет какой-либо особенной поддержки для этих типов. Вместо этого, разработчики определяют новый тип данных через оператор t y p e d e f, называют его скрытым и надеются на то, что никто не будет преобразовывать этот тип в стандартный тип данных языка С. Любые использования данных этих типов возможны только через специальные интерфейсы, которые также создаются разработчиком. Примером мо жет быть тип данных p i d _ t, в котором хранится информация об идентификаторе процесса. Размер этого типа данных не раскрывается, хотя каждый может смошен ничать, использовать размер по максимуму и работать с этим типом, как с типом int. Если нигде явно не используется размер скрытого типа данных, то размер этого типа может быть изменен, и это не вызовет никаких проблем. На самом деле так уже однажды случилось: в старых Unix-подобных операционных системах тип p i d _ t был определен как a h o r t.

Еще один пример скрытого типа данных — это тип a t o m i c _ t. Как уже обсужда лось в главе 9, "Средства синхронизации в ядре", этот тип содержит данные цело численного типа, с которыми можно выполнять атомарные операции. Хотя этот тип и соответствует типу int, использование скрытого типа данных позволяет гарантиро вать, что данные этого типа будут использоваться только в специальных функциях, которые выполняют атомарные операции. Скрытые типы позволяют скрыть размер типа данных, который не всегда равен полным 32 разрядам, как в случае платформы SPARC.

Другие примеры скрытых типов данных в ядре — это dev_t, g i d _t и u i d _ t. При работе со скрытыми типами данных необходимо помнить о следующем.

• Нельзя предполагать, что данные скрытого типа имеют некоторый определен ный размер в памяти.

• Нельзя преобразовывать скрытый тип обратно в стандартный тип данных.

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

Специальные типы данных Некоторые данные в ядре, кроме того, что представляются с помощью скрытых типов, требуют еще и специальных типов данных. Два примера — счетчик импульсов системного таймера j i f f i e s и параметр f l a g s, используемый для обработки пре рываний. Для хранения этих данных всегда должен использоваться тип u n s i g n e d long.

394 Глава При хранении и использовании специфических данных всегда необходимо об ращать особенное внимание на тот тип данных, который представляет эти данные, и использовать именно его. Часто встречающейся ошибкой является использование другого типа, например типа unsigned i n t. Хотя для 32-разрядных аппаратных платформ это не приведет к проблемам, на 64-разрядных системах возникнут проб лемы.

Типы с явным указанием размера Часто при программировании необходимы типы данных заданного размера.

Обычно это необходимо для удовлетворения некоторых внешних требований, связанных с аппаратным обеспечением, сетью или бинарной совместимостью.

Например, звуковой адаптер может иметь 32-разрядный регистр, пакет сетевого протокола — 16-разрядное поле данных, а исполняемый файл - 8 битовый иденти фикатор cookie. В этих случаях тип, который представляет данные, должен иметь точно заданный размер.

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

Таблица 19.2. Типы данных явно заданного размера Тип Описание s8 байт со знаком u8 байт без знака s16 16-разрядное целое число со знаком ul6 16-разрядное целое число без знака s32 32-разрядное целое число со знаком u32 32-разрядное целое число без знака s64 64-разрядное целое число со знаком u64 64-разрядное целое число без знака Варианты сo знаком используются редко.

Эти типы данных, с явно заданным размером, просто определены с помощью оператора typedef через стандартные типы данных языка С. Для 64-разрядной ма шины они могут быть определены следующим образом.

typedef signed char s8;

typedef unsigned char u8;

typedef signed short s16;

typedef unsigned short ul6;

typedef signed int s32;

typedef unsigned int u32;

typedef signed long s64;

typedef unsigned long u64;

Переносимость Для 32-разрядной машины их можно определить, как показано ниже.

typedef signed char s8;

typedef unsigned char u8;

typedef signed short s16;

typedef unsigned short ul6;

typedef signed int s32;

typedef unsigned int u32;

typedef signed long long s64;

typedef unsigned long long u64;

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

Для большинства аппаратных платформ тип char является знаковым, а диапазон значений данных этого типа от -128 до 127. Для небольшого количества аппаратных платформ, таких как ARM, тип char по умолчанию без знака, а возможные значения данных этого типа лежат в диапазоне от 0 до 255.

Например, для систем, на которых тип char без знака, выполнение следующего кода приведет к записи в переменную i числа 255 вместо -1.

char i = -1;

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

signed char i = -1;

Если в вашем коде используется тип данных char, то следует помнить, что этот тип может на самом деле быть как signed char, так и unsigned char. Если необ ходим строго определенный вариант, то это нужно явно декларировать.

Выравнивание данных Выравнивание (alignment) соответствует размещению порции данных в памяти.

Говорят, что переменная имеет естественное выравнивание (naturally aligned), если она находится в памяти по адресу, значение которого кратно размеру этой переменной.

Например, переменная 32-разрядного типа данных имеет естественное выравнива ние, если она находится в памяти по адресу, кратному 4 байт (т.е. два младших бита адреса равны нулю). Таким образом, структура данные размером 2n байт должна хра ниться в памяти по адресу, младшие n битов которого равны нулю.

Для некоторых аппаратных платформ существуют строгие требования относи тельно выравнивания данных. На некоторых системах, обычно RISC, загрузка не правильно выровненных данных приводит к генерации системного прерывания (trap), ошибки, которую можно обработать. На других системах с неестественно выравниваемыми данными можно работать, но это приводит к уменьшению произ 396 Глава водительности. При написании переносимого кода необходимо предотвращать про блемы, связанные с выравниванием, а данные всех типов должны иметь естествен ное выравнивание.

Как избежать проблем с выравниванием Компилятор обычно предотвращает проблемы, связанные с выравниванием, пу тем естественного выравнивания всех типов данных. На самом деле, разработчики ядра обычно не должны заниматься проблемами, связанными с выравниванием, об этом должны заботиться разработчики компилятора gcc. Однако такие проблемы все же могут возникать, когда разработчику приходится выполнять операции с указате лями и осуществлять доступ к данным, не учитывая того, как компилятор выполняет операции доступа к данным.

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

char dog [10];

char *p = &dog[1];

unsigned long 1 = * (unsigned long *)p;

В этом примере указатель на данные типа unsigned char используется, как указа тель на тип u n s i g n e d long, что может привести к тому, что 32-разрядное значение типа u n s i g n e d long будет считываться из памяти по адресу, не кратному четырем.

Если вы думаете: "Зачем мне это может быть нужно?', то вы, скорее всего, правы.

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

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

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

• Выравнивание объединения (union) соответствует выравниванию самого боль шого, по размеру, типа данных из тех, которые включены в объединение.

• Выравнивание структуры соответствует выравниванию самого большого, по размеру, типа данных среди типов всех полей структуры.

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

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

struct animal_struct { char dog;

/* 1 байт */ unsigned long cat;

/* 4 байт */ unsigned short pig;

/* 2 байт */ char fox;

/* 1 байт */ };

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

struct animal_struct { char dog;

/* 1 байт */ u8 pad0[3];

/* 3 байт */ unsigned long cat;

/* 4 байт */ unsigned short pig;

/* 2 байт */ char fox;

/* 1 байт */ u8 padl;

/* 1 байт */ };

Переменные заполнения вводятся для того, чтобы обеспечить естественное вы равнивание всех элементов структуры. Первая переменная заполнения вводит до полнительные затраты памяти для того, чтобы разместить поле c a t на границе 4-байтового адреса. Вторая переменная используется для выравнивания размера самой структуры. Дополнительный байт гарантирует, что размер структуры будет кратен четырем байтам и что каждый элемент массива таких структур будет иметь естественное выравнивание.

Следует обратить внимание, что выражение s i z e o f (foo_struct) равно значе нию 12 для любого экземпляра этой структуры на большинстве 32-разрядных аппарат ных платформ. Компилятор языка С автоматически добавляет элементы заполнения, чтобы гарантировать необходимое выравнивание.

Часто имеется возможность переставить поля структуры так, чтобы избежать не обходимости заполнения. Это позволяет получить правильно выровненные данные без введения дополнительных элементов заполнения и, соответственно, структуру меньшего размера.

s t r u c t animal s t r u c t { unsigned long c a t ;

/* 4 байта */ unsigned short pig;

2 байта */ /* char dog;

/* 1 байт */ char fox;

/* 1 байт */ };

Эта структура данных имеет размер 8 байт. Однако не всегда существует возмож ность перестановки элементов структуры местами и изменения определения струк 398 Глава туры. Например, если структура поставляется как часть стандарта, или уже исполь зуется в существующем коде, то порядок следования полей менять нельзя. Иногда, по некоторым причинам, может потребоваться специальный порядок следования полей структуры, например специальное выравнивание переменных для оптимиза ции попадания в кэш. Заметим, что, согласно стандарту ANSI С, компилятор никогда не должен менять порядок следования полей в структурах5 данных — этим правом обладает только программист.

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

Порядок следования байтов Порядок следования байтов (byte ordering) — это порядок, согласно которому байты расположены в машинном слове. Для разных процессоров может использоваться один из двух типов нумерации байтов в машинном слове: наименее значимый (са мый младший) байт является либо самым первым (самым левым, left-most), либо самым последним (самым правым, right-most) в слове. Порядок байтов называется обратным (big-endian), если наиболее значимый (самый старший) байт хранится пер вым, а за ним идут байты в порядке убывания значимости. Порядок байтов называ ется прямым (little-endian), если наименее значимый (самый младший) байт хранится первым, а за ним следуют байты в порядке возрастания значимости.

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

На рис. 19.1 показан пример обратного порядка следования байтов, а на рис. 19.2 — прямого порядка следования байтов.

Аппаратная платформа i386 использует прямой (little-endian) порядок байтов.

Большинство других аппаратных платформ обычно использует обратный (big-endian) порядок.

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

Переносимость Номер байта 0 2 Старший Младший байт байт Рис. 19.1. Обратный (big-endian) порядок следования байтов Номер байта 2 1 О Старший Младший байт байт Рис. 19.2. Прямей (little-endian) порядок следования байтов Рассмотрим, что эти типы кодирования обозначают на практике и как выглядит двоичное представление числа 1027, которое хранится в виде четырехбайтового це лочисленного типа данных.

00000000 00000000 00000100 Внутренние представления этого числа в памяти при использовании прямого и обратного порядка байтов отличаются, как это показано в табл. 19.3.

Pages:     | 1 |   ...   | 6 | 7 || 9 |



© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.