Powered By Blogger

Tuesday, March 18, 2008

Linux - syscalls. Системные вызовы в Linux.

О многом - молвил Морж,- пришла пора поговорить.
Л. Кэролл (Цитата по книге Б. Страустрапа)


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

Вместо введения.

По теме внутреннего устройства ядра Linux в общем, его различных подсистемах и о системных вызовах в частности, было писано и переписано уже порядком. Наверно, каждый уважающий себя автор должен хоть раз об этом написать, подобно тому, как каждый уважающий себя программист обязательно должен написать свой собственный файл-менеджер :) Хотя я и не являюсь профессиональным IT-райтером, да и вообще, делаю свои пометки исключительно для своей пользы в первую очередь, чтобы не забыть изученное слишком быстро. Но, если уж кому-то пригодятся мои путевые заметки, конечно, буду только рад. Ну и вообще, кашу маслом не испортишь, поэтому, может даже мне и удастся написать или описать что-то такое, о чём никто не удосужился упомянуть.

Теория. Что такое системные вызовы?

Когда непосвящённым объясняют, что такое ПО (или ОС), то говорят обычно следующее: компьютер сам по себе - это железяка, а вот софт - это то, благодаря чему от этой железяки можно получить какую-то пользу. Грубовато, конечно, но в целом, в чём-то верно. Так же я сказал бы наверно об ОС и системных вызовах. На самом деле, в разных ОС системные вызовы могут быть реализованы по-разному, может розниться число этих самых вызовов, но так или иначе, в том или ином виде механизм системных вызовов есть в любой ОС. Каждый день пользователь явно или не явно работает с файлами. Он конечно может явно открыть файл для редактирования в своём любимом MS Word'е или Notepad'е, а может и просто запустить игрушку, исполняемый образ которой, к слову, тоже хранится в файле, который, в свою очередь, должен открыть и прочитать загрузчик исполняемых файлов. В свою очередь игрушка также может открывать и читать десятки файлов в процессе своей работы. Естественно, файлы можно не только читать, но и писать (не всегда, правда, но здесь речь не о разделении прав и дискретном доступе :) ). Всем этим заведует ядро (в микроядерных ОС ситуация может отличаться, но мы сейчас будем ненавязчиво клониться к объекту нашего обсуждения - Linux, поэтому проигнорируем этот момент). Само по себе порождение нового процесса - это также услуга, предоставляемая ядром ОС. Всё это замечательно, как и то, что современные процессоры работают на частотах гигагерцевых диапазонов и состоят из многих миллионов транзисторов, но что дальше? Да то, что если бы небыло некого механизма, с помощью которого пользовательские приложения могли выполнять некоторые достаточно обыденные и, в то же время, нужные вещи (на самом деле, эти тривиальные действия при любом раскладе выполняются не пользовательским приложением, а ядром ОС - авт.), то ОС была просто вещью в себе - абсолютно бесполезной или же напротив, каждое пользовательское приложение само по себе должно было бы стать операционной системой, чтобы самостоятельно обслуживать все свои нужды. Мило, не правда ли?

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

Как выглядит системный вызов и что из себя представляет? Ну, из того, что было сказано выше, становится ясно, что системный вызов - это подпрограмма ядра, имеющая соответственный вид. Те, кто имел опыт программирования под Win9х/DOS, наверняка помнят прерывание int 0x21 со всеми (или хотя бы некоторыми) его многочисленными функциями. Однако, есть одна небольшая особенность, касающаяся всех системных вызовов Unix. По соглашению функция, реализующая системный вызов, может принимать N аргументов или не принимать их вовсе, но так или иначе, функция должна возвращать значение типа int. Любое неотрицательное значение трактуется, как успешное выполнение функции системного вызова, а стало быть и самого системного вызова. Значение меньше нуля является признаком ошибки и одновременно содержит код ошибки (коды ошибок определяются в заголовках include/asm-generic/errno-base.h и include/asm-generic/errno.h). В Linux шлюзом для системных вызовов до недавнего времени было прерывание int 0x80, в то время, как в Windows (вплоть до версии XP Service Pack 2, если не ошибаюсь) таким шлюзом является прерывание 0x2e. Опять же, в ядре Linux, до недавнего времени все системные вызовы обрабатывались функцией system_call(). Однако, как выяснилось позднее, классический механизм обрабатки системных вызовов через шлюз 0x80 приводит к существенному падению производительности на процессорах Intel Pentium 4. Поэтому на смену классическому механизму пришёл метод виртуальных динамических разделяемых объектов (DSO - динамический разделяемый объектный файл. Не ручаюсь за правильный перевод, но DSO, это то, что пользователям Windows известно под названием DLL - динамически загружаемая и компонуемая библиотека) - VDSO. В чём отличие нового метода от классического? Для начала разберёмся с классическим методом, работающим через гейт 0x80.

Классический механизм обслуживания системных вызовов в Linux.

Прерывания в архитектуре х86.

Как было сказано выше, ранее для обслуживания запросов пользовательских приложений использовался шлюз 0x80 (int 0x80). Работа системы на базе архитектуры IA-32 управляется прерываниями (строго говоря, это касается вообще всех систем на базе x86). Когда происходит некое событие (новый тик таймера, какая-либо активность на некотором устройстве, ошибки - деление на ноль и пр.), генерируется прерывание. Прерывание (interrupt) названо так, потому что оно, как правило, прерывает нормальный ход выполнения кода. Прерывания принято подразделять на аппаратные и программные (hardware and software interrupts). Аппаратные прерывания - это прерывания, которые генерируются системными и периферическими устройствами. При возникновении необходимости какого-то устройства привлечь к себе внимание ядра ОС оно (устройство) генерирует сигнал на своей линии запроса прерывания (IRQ - Interrupt ReQuest line). Это приводит к тому, что на определённых входах процессора формируется соответствующий сигнал, на основе которого процессор и принимает решение прервать выполнение потока инструкций и передать управление на обработчик прерывания, который уже выясняет, что произошло и что необходимо сделать. Аппаратные прерывания асинхронны по своей природе. Это значит, что прерывание может возникнуть в любое время. Кроме периферийных устройств, сам процессор может вырабатывать прерывания (или, точнее, аппаратные исключения - Hardware Exceptions - например, уже упомянутое деление на ноль). Делается это с тем, чтобы уведомить ОС о возникновении нештатной ситуации, чтобы ОС могла предпринять некое действие в ответ на возникновение такой ситуации. После обработки прерывания процессор возвращается к выполнению прерванной программы. Прерывание может быть инициировано пользовательским приложением. Такое прерывание называют программным. Программные прерывания, в отличие от аппаратных, синхронны. Т.е., при вызове прерывания, вызвавший его код приостанавливается, пока прерывание не будет обслужено. При выходе из обработчика прерывания происходит возврат по дальнему адресу, сохранённому ранее (при вызове прерывания) в стеке, на следующую инструкцию после инструкции вызова прерывания (int). Обработчик прерывания - это резидентный (постоянно находящийся в памяти) участок кода. Как правило, это небольшая программа. Хотя, если мы будем говорить о ядре Linux, то там обработчик прерывания не всегда такой уж маленький. Обработчик прерывания определяется вектором. Вектор - это ни что иное, как адрес (сегмент и смещение) начала кода, который должен обрабатывать прерывания с данным индексом. Работа с прерываниями существенно отличается в реальном (Real Mode) и защищённом (Protected Mode) режиме работы процессора (напомню, что здесь и далее мы подразумеваем процессоры Intel и совместимые с ними). В реальном (незащищённом) режиме работы процессора обработчики прерываний определяются их векторами, которые хранятся всегда в начале памяти выборка нужного адреса из таблицы векторов происходит по индексу, который также является номером прерывания. Перезаписав вектор с определённым индексом можно назначить прерыванию свой собственный обработчик.

В защищённом режиме обработчики прерываний (шлюзы, гейты или вентили) больше не определяются с помощью таблицы векторов. Вместо этой таблицы используется таблица вентилей или, правильнее, таблица прерываний - IDT (Interrupt Descriptors Table). Эта таблица формируется ядром, а её адрес хранится в регистре процессора idtr. Данный регистр недоступен напрямую. Работа с ним возможна только при помощи инструкций lidt/sidt. Первая из них (lidt) загружает в регистр idtr значение, указанное в операнде и являющееся базовым адресом таблицы дексрипторов прерываний, вторая (sidt) - сохраняет адрес таблицы, находящийся в idtr, в указанный операнд. Так же, как происходит выборка информации о сегменте из таблицы дескрипторов по селектору, происходит и выборка дескриптора сегмента, обслуживающего прерывание в защищённом режиме. Защита памяти поддерживается процессорами Intel начиная с CPU i80286 (не совсем в том виде, в каком она представлена сейчас, хотя бы потому, что 286 был 16-разрядным процессором - поэтому Linux не может работать на этих процессорах) и i80386, а посему процессор самостоятельно производит все необходимые выборки и, стало быть, сильно углубляться во все тонкости защищённого режима (а именно в защищённом режиме работает Linux) мы не будем. К сожалению, ни время, ни возможности не позволяют нам остановиться надолго на механизме обработки прерываний в защищённом режиме. Да это и не было целью при написании данной статьи. Все сведения, приводимые здесь касательно работы процессоров семейства х86 довольно поверхностны и приводятся лишь для того, чтобы помочь немного лучше понять механизм работы системных вызовов ядра. Кое-что можно узнать непосредственно из кода ядра, хотя, для полного понимания происходящего, желательно всё же ознакомиться с принципами работы защищённого режима. Участок кода, который заполняет начальными значениями (но не устанавливает!) IDT, находится в arch/i386/kernel/head.S:

/*
 *  setup_idt
 *
 *  sets up a idt with 256 entries pointing to
 *  ignore_int, interrupt gates. It doesn't actually load
 *  idt - that can be done only after paging has been enabled
 *  and the kernel moved to PAGE_OFFSET. Interrupts
 *  are enabled elsewhere, when we can be relatively
 *  sure everything is ok.
 *
 *  Warning: %esi is live across this function.
 */
1.setup_idt:
2. lea ignore_int,%edx
3. movl $(__KERNEL_CS << 16),%eax
4. movw %dx,%ax  /* selector = 0x0010 = cs */
5. movw $0x8E00,%dx /* interrupt gate - dpl=0, present */

6. lea idt_table,%edi
7. mov $256,%ecx
8.rp_sidt:
9. movl %eax,(%edi)
10. movl %edx,4(%edi)
11. addl $8,%edi
12. dec %ecx
13. jne rp_sidt

14..macro set_early_handler handler,trapno
15. lea \handler,%edx
16. movl $(__KERNEL_CS << 16),%eax
17. movw %dx,%ax
18. movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
19. lea idt_table,%edi
20. movl %eax,8*\trapno(%edi)
21. movl %edx,8*\trapno+4(%edi)
22..endm

23. set_early_handler handler=early_divide_err,trapno=0
24. set_early_handler handler=early_illegal_opcode,trapno=6
25. set_early_handler handler=early_protection_fault,trapno=13
26. set_early_handler handler=early_page_fault,trapno=14

28. ret
Несколько замечаний по коду: приведённый код написан на разновидности ассемблера AT&T, поэтому Ваше знание ассемблера в его привычной интеловской нотации может только сбить с толку. Самое основное отличие в порядке операндов. Если для интеловской нотации определён порядок - "аккумулятор" < "источник", то для ассемблера AT&T порядок прямой. Регистры процессора, как правило, должны иметь префикс "%", непосредственные значения (константы) префиксируются символом доллара "$". Синтаксис AT&T традиционно используется в Un*x-системах.

В приведённом примере в строках 2-4 устанавливается адрес обработчика всех прерываний по умолчанию. Обработчиком по умолчанию является функция ignore_int, которая ничего не делает. Наличие такой заглушки необходимо для корректной обработки всех прерываний на данном этапе, так как других ещё просто нет (правда, ловушки (traps) устанавливаются немного ниже по коду - о ловушках см. Intel Architecture Manual Reference или что-то подобное, здесь мы не будем касаться ловушек). В строке 5 устанавливается тип вентиля. В строке 6 мы загружаем в индексный регистр адрес нашей таблицы IDT. Таблица должна содержать 255 записей, по 8 байт каждая. В строках 8-13 мы заполняем всю таблицу одними и теми же значениями, установленными ранее в регистрах eax и edx - т.е., это вентиль прерывания, ссылающийся на обработчик ignore_int. Чуть ниже мы определяем макрос для установки ловушек (traps) - строки 14-22. В строках 23-26 используя вышеопределённый макрос мы устанавливаем ловушки для следующих исключений: early_divide_err - деление на ноль (0), early_illegal_opcode - неизвестная инструкция процессора (6), early_protection_fault - сбой защиты памяти (13), early_page_fault - отказ страничной трансляции (14). В скобках приведены номера "прерываний", генерируемые при возникновении соответствующей нештатной ситуации. Перед проверкой типа процессора в arch/i386/kernel/head.S таблица IDT устанавливается вызовом setup_idt:

/*
 * start system 32-bit setup. We need to re-do some of the things done
 * in 16-bit mode for the "real" operations.
 */
1. call setup_idt
...
2. call check_x87
3. lgdt early_gdt_descr
4. lidt idt_descr
После выяснения типа (со)процессора и проведения всех подготовительных действий в строках 3 и 4 мы загружаем таблицы GDT и IDT, которые будут использоваться на самых первых порах работы ядра.

Системные вызовы и int 0x80.

От прерываний вернёмся обратно к системным вызовам. Итак, что необходимо, чтобы обслужить процесс, который запрашивает какую-то услугу? Для начала, необходимо перейти из кольца 3 (уровень привелегий CPL=3) на наиболее привелигированный уровень 0 (Ring 0, CPL=0), т.к. код ядра расположен в сегменте с наивысшими привелегиями. Кроме того, необходим код обработчика, который обслужит процесс. Именно для этого и используется шлюз 0x80. Хотя системных вызовов довольно много, для всех них используется единая точка входа - int 0x80. Сам обработчик устанавливается при вызове функции arch/i386/kernel/traps.c::trap_init():

void __init trap_init(void)
{
        ...
        set_system_gate(SYSCALL_VECTOR,&system_call);
        ...
}
Нас в trap_init() больше всего интересует эта строка. В этом же файле выше можно посмотреть на код функции set_system_gate():
static void __init set_system_gate(unsigned int n, void *addr)
{
        _set_gate(n, DESCTYPE_TRAP | DESCTYPE_DPL3, addr, __KERNEL_CS);
}
Здесь видно, что вентиль для прерывания 0х80 (а именно это значение определено макросом SYSCALL_VECTOR - можете поверить наслово :) ) устанавливается как ловушка (trap) с уровнем привелегий DPL=3 (Ring 3), т.е. это прерывание будет отловлено при вызове из пространства пользователя. Проблема с переходом из Ring 3 в Ring 0 т.о. решена. Функция _set_gate() определена в заголовочном файле include/asm-i386/desc.h . Для особо любопытных ниже приведён код, без пространных объяснений, впрочем:
static inline void _set_gate(int gate, unsigned int type, void *addr, unsigned short seg)
{
        __u32 a, b;
        pack_gate(&a, &b, (unsigned long)addr, seg, type, 0);
        write_idt_entry(idt_table, gate, a, b);
}
Вернёмся к функции trap_init(). Она вызывается из функции start_kernel() в init/main.c . Если посмотреть на код trap_init(), то видно, что эта функция переписывает некоторые значения таблицы IDT заново - обработчики, которые использовались на ранних стадиях инициализации ядра (early_page_fault, early_divide_err, early_illegal_opcode, early_protection_fault), заменяются на те, что будут использоваться уже в процессе работы ядра. Итак, мы практически добрались до сути и уже знаем, что все системные вызовы обрабатываются единообразно - через шлюз int 0x80. В качестве обработчика для int 0x80, как опять же видно из приведённого выше куска кода arch/i386/kernel/traps.c::trap_init(), устанавливается функция system_call().

system_call().

Код функции system_call() находится в файле arch/i386/kernel/entry.S и выглядит следующим образом:

        # system call handler stub
ENTRY(system_call)
        RING0_INT_FRAME   # can't unwind into user space anyway
        pushl %eax   # save orig_eax
        CFI_ADJUST_CFA_OFFSET 4
        SAVE_ALL
        GET_THREAD_INFO(%ebp)
        # system call tracing in operation / emulation
        /* Note, _TIF_SECCOMP is bit number 8, and so it needs testw and not testb */
        testw $(_TIF_SYSCALL_EMU|_TIF_SYSCALL_TRACE|_TIF_SECCOMP|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
        jnz syscall_trace_entry
        cmpl $(nr_syscalls), %eax
        jae syscall_badsys
syscall_call:
        call *sys_call_table(,%eax,4)
        movl %eax,PT_EAX(%esp)  # store the return value
        ...
Код приведён не полностью. Как видно, сперва system_call() настраивает стек для работы в Ring 0, сохраняет значение, переданное ей через eax в стек, сохраняет все регистры также в стек, получает данные о вызывающем потоке и проверяет, не выходит ли переданное значение-номер системного вызова за пределы таблицы системных вызовов и затем, наконец, пользуясь значением, переданным в eax в качестве аргумента, system_call() осуществляет переход на настоящий обработчик системного вывода, исходя из того, на какой элемент таблицы ссылается индекс в eax. Теперь вспомните старую добрую таблицу векторов прерываний из реального режима. Ничего не напоминает? В реальности, конечно, всё несколько сложнее. В частности, системный вызов должен скопировать результаты из стека ядра в стек пользователя, передать код возврата и ещё некоторые вещи. В том случае, когда аргумент, указанный в eax не ссылается на существующий системный вызов (значение выходит за диапазон), происходит переход на метку syscall_badsys. Здесь в стек по смещению, по которому должно находиться значение eax, заносится значение -ENOSYS - системный вызов не реализован. На этом выполнение system_call() завершается.

Таблица системных вызовов находится в файле arch/i386/kernel/syscall_table.S и имеет достаточно простой вид:

ENTRY(sys_call_table)
        .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
        .long sys_exit
        .long sys_fork
        .long sys_read
        .long sys_write
        .long sys_open  /* 5 */
        .long sys_close
        .long sys_waitpid
        .long sys_creat
        ...
Иными словами, вся таблица являет собой ничто иное, как массив адресов функций, расположенных в порядке следования номеров системных вызовов, которые эти функции обслуживают. Таблица - обычный массив двойных машинных слов (или 32-разрядных слов - кому как больше нравится). Код части функций, обслуживающих системные вызовы, находится в платформно-зависимой части - arch/i386/kernel/sys_i386.c, а часть, не зависящая от платформы - в kernel/sys.c .

Вот так обстоит дело с системными вызовами и вентилем 0x80.

Новый механизм обработки системных вызовов в Linux. sysenter/sysexit.

Как упоминалось, достаточно быстро выяснилось, что использование традиционного способа обработки системных вызовов на основе гейта 0х80 пиводит к потере производительности на процессорах Intel Pentium 4. Поэтому Линус Торвальдс реализовал в ядре новый механизм, основанный на инструкциях sysenter/sysexit и призванный повысить производительность ядра на машинах, оснащённых процессором Pentium II и выше (именно с Pentium II+ процессоры Intel поддерживают упомянутые инструкции sysenter/sysexit). В чём суть нового механизма? Как ни странно, но суть осталась та же. Изменилось исполнение. Согласно документации Intel инструкция sysenter является частью механизма "быстрых системных вызовов". В частности, эта инструкция оптимизирована для быстрого перехода с одного уровня привелегий на другой. Если точнее, то она ускоряет переход в кольцо 0 (Ring 0, CPL=0). При этом, операционная система должна подготовить процессор к использовании инструкции sysenter. Такая настройка осуществляется единожды при загрузке и инициализации ядра ОС. При вызове sysenter устанавливает регистры процессора согласно машинно-зависимых регистров, ранее установленных ОС. В частности, устанавливаются сегментный регистр и регистр указателя инструкций - cs:eip, а также сегмент стека и указатель вершины стека - ss, esp. Переход на новый сегмент кода и смещение осуществляется из кольца 3 в 0.

Инструкция sysexit выполняют обратные действия. Она производит быстрый переход с уровня привелегий 0 на 3-й (CPL=3). При этом регистр сегмента кода устанавливается в 16 + значение сегмента cs, сохранённое в машинно-зависимом регистре процессора. В регистр eip заносится содержимое регистра edx. В ss заносится сумма 24 и значения cs, занесённое ОС ранее в машинно-зависимый регистр процессора при подготовке контекста для работы инструкции sysenter. В esp заносится содержимое регистра ecx. Значения, необходимые для работы инструкций sysenter/sysexit хранятся по следующим адресам:

  1. SYSENTER_CS_MSR    0х174 - сегмент кода, куда заносится значение сегмента, в котором находится код обработчика системного вызова.
  2. SYSENTER_ESP_MSR   0х175 - указатель на вершину стека для обработчика системного вызова.
  3. SYSENTER_EIP_MSR    0х176 - указатель на смещение внутри сегмента кода. Указывает на начало кода обработчика системных вызовов.
Данные адреса ссылаются на модельно-зависимые регистры, которые не имеют имён. Значения записываются в модельно зависимые регистры с помощью инструкции wrmsr, при этом edx:eax должны содержать страшую и младшую части 64-битного машинного слова соответственно, а в ecx должен быть занесён адрес ригистра, в который будет произведена запись. В Linux адреса модельно-зависимых регистров определяются в заголовочном файле include/asm-i368/msr-index.h следующим образом (до версии 2.6.22 как минимум они определялись в заголовочном файле include/asm-i386/msr.h, напомню, что мы рассматриваем механизм системных вызовов на примере ядра Linux 2.6.22):
#define MSR_IA32_SYSENTER_CS  0x00000174
#define MSR_IA32_SYSENTER_ESP  0x00000175
#define MSR_IA32_SYSENTER_EIP  0x00000176
Код ядра, ответственный за установку модельно-зависимых регистров, находится в файле arch/i386/sysenter.c и выглядит следующим образом:
1.  void enable_sep_cpu(void)
{
2.        int cpu = get_cpu();
3.        struct tss_struct *tss = &per_cpu(init_tss, cpu);

4.        if (!boot_cpu_has(X86_FEATURE_SEP)) {
5.                put_cpu();
6.                return;
        }

7.        tss->x86_tss.ss1 = __KERNEL_CS;
8.        tss->x86_tss.esp1 = sizeof(struct tss_struct) + (unsigned long) tss;
9.        wrmsr(MSR_IA32_SYSENTER_CS, __KERNEL_CS, 0);
10.        wrmsr(MSR_IA32_SYSENTER_ESP, tss->x86_tss.esp1, 0);
11.        wrmsr(MSR_IA32_SYSENTER_EIP, (unsigned long) sysenter_entry, 0);
12.        put_cpu(); 
}
Здесь в переменную tss мы получаем адрес структуры, описывающей сегмент состояния задачи. TSS (Task State Segment) используется для описания контекста задачи и является частью механизма аппаратной поддержки многозадачности для архитектуры x86. Однако, Linux практически не использует аппаратное переключение контекста задач. Согласно документации Intel переключение на другую задачу производится либо путём выполнения инструкции межсегментного перехода (jmp или call), ссылающейся на сегмент TSS, либо на дескриптор вентиля задачи в GDT (LDT). Специальный регистр процессора, невидимый для программиста - TR (Task Register - регистр задачи) содержит селектор дескриптора задачи. При загрузке этого регистра также загружаются программно-невидимые регистры базы и лимита, связанные с TR.

Несмотря на то, что Linux не использует аппаратное переключение контекстов задач, ядро вынуждено отводить запись TSS для каждого процессора, установленного в системе. Это связано с тем, что когда процессор переключается из пользовательского режима в режим ядра, он извлекает из TSS адрес стека ядра. Кроме того, TSS необходим для управления доступом к портам ввода/вывода. TSS содержит карту прав доступа к портам. На основе этой карты становится возможным осуществлять контроль доступа к портам для каждого процесса, использующего инструкции in/out. Здесь tss->x86_tss.esp1 указывает на стек ядра. __KERNEL_CS естественно указывает на сегмент кода ядра. В качестве смещения-eip указывается адрес функции sysenter_entry().

Функция sysenter_entry() определена в файле arch/i386/kernel/entry.S и имеет такой вид:

/* SYSENTER_RETURN points to after the "sysenter" instruction in
   the vsyscall page.  See vsyscall-sysentry.S, which defines the symbol.  */

 # sysenter call handler stub
ENTRY(sysenter_entry)
        CFI_STARTPROC simple
        CFI_SIGNAL_FRAME
        CFI_DEF_CFA esp, 0
        CFI_REGISTER esp, ebp
        movl TSS_sysenter_esp0(%esp),%esp
sysenter_past_esp:
        /*
         * No need to follow this irqs on/off section: the syscall
         * disabled irqs and here we enable it straight after entry:
         */
        ENABLE_INTERRUPTS(CLBR_NONE)
        pushl $(__USER_DS)
        CFI_ADJUST_CFA_OFFSET 4
        /*CFI_REL_OFFSET ss, 0*/
        pushl %ebp
        CFI_ADJUST_CFA_OFFSET 4
        CFI_REL_OFFSET esp, 0
        pushfl
        CFI_ADJUST_CFA_OFFSET 4
        pushl $(__USER_CS)
        CFI_ADJUST_CFA_OFFSET 4
        /*CFI_REL_OFFSET cs, 0*/
        /*
         * Push current_thread_info()->sysenter_return to the stack.
         * A tiny bit of offset fixup is necessary - 4*4 means the 4 words
         * pushed above; +8 corresponds to copy_thread's esp0 setting.
         */
        pushl (TI_sysenter_return-THREAD_SIZE+8+4*4)(%esp)
        CFI_ADJUST_CFA_OFFSET 4
        CFI_REL_OFFSET eip, 0

        /*
         * Load the potential sixth argument from user stack.
         * Careful about security.
         */
         cmpl $__PAGE_OFFSET-3,%ebp
         jae syscall_fault
1:        movl (%ebp),%ebp
        .section __ex_table,"a"
        .align 4
        .long 1b,syscall_fault
        .previous

        pushl %eax
        CFI_ADJUST_CFA_OFFSET 4
        SAVE_ALL
        GET_THREAD_INFO(%ebp)

        /* Note, _TIF_SECCOMP is bit number 8, and so it needs testw and not testb */
        testw $(_TIF_SYSCALL_EMU|_TIF_SYSCALL_TRACE|_TIF_SECCOMP|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
        jnz syscall_trace_entry
        cmpl $(nr_syscalls), %eax
        jae syscall_badsys
        call *sys_call_table(,%eax,4)
        movl %eax,PT_EAX(%esp)
        DISABLE_INTERRUPTS(CLBR_ANY)
        TRACE_IRQS_OFF
        movl TI_flags(%ebp), %ecx
        testw $_TIF_ALLWORK_MASK, %cx
        jne syscall_exit_work
        /* if something modifies registers it must also disable sysexit */
        movl PT_EIP(%esp), %edx
        movl PT_OLDESP(%esp), %ecx
        xorl %ebp,%ebp
        TRACE_IRQS_ON
1:      mov  PT_FS(%esp), %fs
        ENABLE_INTERRUPTS_SYSEXIT
        CFI_ENDPROC
        .pushsection .fixup,"ax"
2:        movl $0,PT_FS(%esp)
        jmp 1b
        .section __ex_table,"a"
        .align 4
        .long 1b,2b
        .popsection
ENDPROC(sysenter_entry)
Как и в случае с system_call() основная работа выполняется в строке call *sys_call_table(,%eax,4). Здесь вызывается конкретный обработчик системного вызова. Итак, видно, что принципиально изменилось мало. То обстоятельство, что вектор прерывания теперь забит в железо и процессор помогает нам быстрее перейти с одного уровня привелегий на другой меняет лишь некоторые детали исполнения при прежнем содержании. Правда, на этом изменения не заканчиваются. Вспомните, с чего начиналось повествование. В самом начале я упоминал уже о виртуальных разделяемых объектах. Так вот, если раньше реализация системного вызова, скажем, из системной библиотеки libc выглядела, как вызов прерывания (при том, что библиотека брала некоторые функции на себя, чтобы сократить число переключений контекстов), то теперь благодаря VDSO системный вызов может быть сделан практически напрямую, без участия libc. Он и ранее мог быть осуществлён напрямую, опять же, как прерывание. Но теперь вызов можно затребовать, как обычную функцию, экспортируемую из динамически компонуемой библиотеки (DSO). При загрузке ядро определяет, какой механизм должен и может быть использован для данной платформы. В зависимости от обстоятельств ядро устанавливает точку входа в функцию, выполняющую системный вызов. Далее, функция экспортируется в пользовательское пространство ввиде библиотеки linux-gate.so.1 . Библиотека linux-gate.so.1 физически не существует на диске. Она, если можно так выразиться, эмулируется ядром и существует ровно столько, сколько работает система. Если выполнить останов системы, подмонтировать корневую ФС из другой системы, то Вы не найдёте на корневой ФС остановленной системы этот файл. Собственно, Вы не сможете его найти даже на работающей системе. Физически его просто нет. Именно поэтому linux-gate.so.1 - это нечто иное, как VDSO - т.е. Virtual Dynamically Shared Object. Ядро отображает эмулируемую таким образом динамическую библиотеку в адресное пространство каждого процесса. Убедиться в этом несложно, если выполнить следующую команду:
f0x@devel0:~$ cat /proc/self/maps
08048000-0804c000 r-xp 00000000 08:01 46         /bin/cat
0804c000-0804d000 rw-p 00003000 08:01 46         /bin/cat
0804d000-0806e000 rw-p 0804d000 00:00 0          [heap]
...
b7fdf000-b7fe1000 rw-p 00019000 08:01 2066       /lib/ld-2.5.so
bffd2000-bffe8000 rw-p bffd2000 00:00 0          [stack]
ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]
Здесь самая последняя строка и есть интересующий нас объект:
ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]
Из приведённого примера видно, что объект занимает в памяти ровно одну страницу - 4096 байт, практически на задворках адресного пространства. Проведём ещё один эксперимент:
f0x@devel0:~$ ldd `which cat`
        linux-gate.so.1 =>  (0xffffe000)
        libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e87000)
        /lib/ld-linux.so.2 (0xb7fdf000)
f0x@devel0:~$ ldd `which gcc`
        linux-gate.so.1 =>  (0xffffe000)
        libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e3c000)
        /lib/ld-linux.so.2 (0xb7f94000)
f0x@devel0:~$
Здесь мы просто навскидку взяли два приложения. Видно, что библиотека отображается в адресное пространство процесса по одному и тому же постоянному адресу - 0xffffe000. Теперь попробуем посмотреть, что же такое хранится на этой странице памяти на самом деле...

Сделать дамп страницы памяти, где хранится разделяемый код VDSO, можно с помощью следующей программы:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main () {
        char* vdso = 0xffffe000;
        char* buffer;
        FILE* f;

        buffer = malloc (4096);
        if (!buffer)
                exit (1);
        memcpy (buffer, vdso, 4096);

        if (!(f = fopen ("test.dump", "w+b"))) {
                free (buffer);
                exit (1);
        }

        fwrite (buffer, 4096, 1, f);
        fclose (f);
        free (buffer);

        return 0;
}
Строго говоря, раньше это можно было сделать проще, с помощью команды dd if=/proc/self/mem of=test.dump bs=4096 skip=1048574 count=1, но ядра начиная с версии 2.6.22 или, быть может, даже более ранней, больше не отображают память процесса в файл /proc/`pid`/mem. Этот файл, сохранён, очевидно, для совместимости, но не содержит более информации.

Скомпилируем и прогоним приведённую программу. Попробуем дизассемблировать полученный код:

f0x@devel0:~/tmp$ objdump --disassemble ./test.dump

./test.dump:     file format elf32-i386

Disassembly of section .text:

ffffe400 <__kernel_vsyscall>:
ffffe400:       51                      push   %ecx
ffffe401:       52                      push   %edx
ffffe402:       55                      push   %ebp
ffffe403:       89 e5                   mov    %esp,%ebp
ffffe405:       0f 34                   sysenter
...
ffffe40e:       eb f3                   jmp    ffffe403 <__kernel_vsyscall+0x3>
ffffe410:       5d                      pop    %ebp
ffffe411:       5a                      pop    %edx
ffffe412:       59                      pop    %ecx
ffffe413:       c3                      ret
...

f0x@devel0:~/tmp$
Вот он наш шлюз для системных вызовов, весь, как на ладони. Процесс (либо, системная библиотека libc), вызывая функцию __kernel_vsyscall попадает на адрес 0хffffe400 (в нашем случае). Далее, __kernel_vsyscall сохраняет в стеке пользовательского процесса содержимое регистров ecx, edx, ebp, О назначении регистров ecx и edx мы уже говорили ранее, в ebp используется позже для восстановления стека пользователя. Выполняется инструкция sysenter, "перехват прерывания" и, как следствие, очередной переход на sysenter_entry (см. выше). Инструкция jmp по адресу 0xffffe40e вставлена для перезапуска системного вызова с числом 6 аргументами (см. http://lkml.org/lkml/2002/12/18/). Код, размещаемый на странице, находится в файле arch/i386/kernel/vsyscall-enter.S (или arch/i386/kernel/vsyscall-int80.S для ловушки 0x80). Хотя я и нашёл, что адрес функции __kernel_vsyscall постоянный, но есть мнение, что это не так. Обычно, положение точки входа в __kernel_vsyscall() можно найти по вектору ELF-auxv используя параметр AT_SYSINFO. Вектор ELF-auxv содержит информацию, передаваемую процессу через стек при запуске и содержит различную информацию, нужную в процессе работы программы. Этот вектор в частности содержит переменные окружения процесса, аргументы, и проч..

Вот небольшой пример на С, как можно обратиться к функции __kernel_vsyscall напрямую:

#include <stdio.h>

int pid;

int main ()
{
        __asm (
                "movl $20, %eax \n"
                "call *%gs:0x10 \n"
                "movl %eax, pid \n"
        );

        printf ("pid: %d\n", pid);
        return 0;
}
Данный пример взят со страницы Manu Garg, http://www.manugarg.com. Итак, в приведённом примере мы делаем системный вызов getpid() (номер 20 или иначе __NR_getpid). Чтобы не лазить по стеку процесса в поисках переменной AT_SYSINFO воспользуемся тем обстоятельством, что системная библиотека libc.so при загрузке копирует значение переменной AT_SYSINFO в блок управления потоком (TCB - Thread Control Block). На этот блок информации, как правило, ссылается селектор в gs. Предполагаем, что по смещению 0х10 находится искомый параметр и делаем вызов по адресу, хранящемуся в %gs:$0x10.

Итоги.

На самом деле, практически, особого прироста производительности даже при поддержке на данной платформе FSCF (Fast System Call Facility) добиться не всегда возможно. Проблема в том, что так или иначе, процесс редко обращается напрямую к ядру. И для этого есть свои веские причины. Использование библиотеки libc позволяет гарантировать переносимость программы вне зависимости от версии ядра. И именно через стандартную системную библиотеку идёт большинство системных вызовов. Если даже Вы соберёте и установите самое последнее ядро, собранное для платформы, поддерживающей FSCF, это ещё не гарантия прироста производительности. Дело в том, что Ваша системная библиотека libc.so будет попрежнему использовать int 0x80 и справиться с этим можно лишь пересобрав glibc. Поддерживается ли в glibc вообще интерфейс VDSO и __kernel_vsyscall, я, честно признаться, на данный момент ответить затрудняюсь.

Ссылки.

[1] Manu Garg's page, http://www.manugarg.com
[2] Scatter/Gather thoughts by Johan Petersson, http://www.trilithium.com/johan/2005/08/linux-gate/
[3] Старый добрый Understanding the Linux kernel Куда же без него :)
[4] Ну и конечно же, исходные коды Linux (2.6.22)

2008-03-18. Further revisions are possible...

Thursday, November 29, 2007

Toshiba Satellite A100-811: ещё немного о Bluetooth. Удалённый доступ.

Итак, мы настроили устройство Bluetooth-адаптера. Что дальше? Ну, для начала, если Ваш телефон умеет GPRS и у Вас есть подписка, можно настроить удалённый доступ через GPRS-модем, в роли которого будет выступать Ваш телефон, оснащённый Bluetooth. В принципе, всё, о чём пойдёт речь ниже - вполне применимо и к настройке доступа с помощью GPRS + IrDA. Разве что во втором случае в качестве модема будет использоваться устройство ircomm, а не знакомый нам уже rfcomm.

Самый простой способ ИМХО, это настроить соединение в какой-нибудь звонилке, а ля kppp. Хотя, можно сделать это и с помощью скриптов ppp. Описание второго способа попадалось мне довольно часто, поэтому я думаю, у желающих и без меня будет масса возможностей настроить доступ с помощью скриптов. Я приведу пример, но подробно останавливаться на деталях не буду. С kppp и того проще. Моя цель лишь указать на некоторые вещи, которые мешают установить работоспособное соединение и которые не всегда очевидны для начинающих пользователей.

Будем исходить их того, что Bluetooth у Вас уже настроен, как рассказывалось ранее и есть конфиг для устройства, эмулирующего последовательную линию - rfcomm (для IrDA это будет устройство ircomm). На настройках телефона останавливаться не будем. Они в любом случае не универсальны и будут зависеть от Вашего оператора, которого и надо будет терроризировать на предмет информации по этому поводу :)

Первое, что мы сделаем, это убедимся, что телефон предоставляет услугу удалённого доступа (для Bluetooth это профиль DUN, для IrDA всё немного проще, ибо у IrDA устройств нет профилей, как это имеет место с Bluetooth). При включенном Bluetooth-адаптере запускаем:

root@devel0:/home/f0x# hcitool scan
Scanning ...
        00:0E:07:DC:F8:3E       Red Quick Fox
root@devel0:/home/f0x# hcitool inq
Inquiring ...
        00:0E:07:DC:F8:3E       clock offset: 0x71ce    class: 0x520204
root@devel0:/home/f0x# sdptool browse
Inquiring ...
Browsing 00:0E:07:DC:F8:3E ...
Service Name: Dial-up Networking
Service RecHandle: 0x10000
Service Class ID List:
  "Dialup Networking" (0x1103)
  "Generic Networking" (0x1201)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 1
Profile Descriptor List:
  "Dialup Networking" (0x1103)
    Version: 0x0100

Service Name: Voice gateway
Service RecHandle: 0x10002
Service Class ID List:
  "Headset Audio Gateway" (0x1112)
  "Generic Audio" (0x1203)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 3
Profile Descriptor List:
  "Headset" (0x1108)
    Version: 0x0100

Service Name: Serial Port 1
Service RecHandle: 0x10003
Service Class ID List:
  "Serial Port" (0x1101)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 4

Service Name: Serial Port 2
Service RecHandle: 0x10004
Service Class ID List:
  "Serial Port" (0x1101)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 5

Service Name: OBEX Object Push
Service RecHandle: 0x10005
Service Class ID List:
  "OBEX Object Push" (0x1105)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 10
  "OBEX" (0x0008)
Profile Descriptor List:
  "OBEX Object Push" (0x1105)
    Version: 0x0100

Service Name: IrMC Synchronization
Service RecHandle: 0x10006
Service Class ID List:
  "IrMC Sync" (0x1104)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 11
  "OBEX" (0x0008)
Profile Descriptor List:
  "IrMC Sync" (0x1104)
    Version: 0x0100

Service Name: HF Voice gateway
Service RecHandle: 0x10007
Service Class ID List:
  "Handfree Audio Gateway" (0x111f)
  "Generic Audio" (0x1203)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 6
Profile Descriptor List:
  "Handsfree" (0x111e)
    Version: 0x0100

Service Name: OBEX Basic Imaging
Service RecHandle: 0x1000b
Service Class ID List:
  "Imaging Responder" (0x111b)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 15
  "OBEX" (0x0008)
Profile Descriptor List:
  "Imaging" (0x111a)
    Version: 0x0100

Service Name: OBEX File Transfer
Service RecHandle: 0x1000f
Service Class ID List:
  "OBEX File Transfer" (0x1106)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 7
  "OBEX" (0x0008)
Profile Descriptor List:
  "OBEX File Transfer" (0x1106)
    Version: 0x0100

Первой командой мы просто сканируем эфир в поисках другого Bluetooth-устройства и находим его. Второй командной смотрим, что это за устройство - класс. Хотя второе - это скорее чисто эстетический манёвр для маньяков :) И наконец с помощью третьей команды мы узнаём, какие сервисы нам предоставляет удалённое устройство. Первый блок информации и есть то, что нам нужно:

Service Name: Dial-up Networking
Service RecHandle: 0x10000
Service Class ID List:
  "Dialup Networking" (0x1103)
  "Generic Networking" (0x1201)
Protocol Descriptor List:
  "L2CAP" (0x0100)
  "RFCOMM" (0x0003)
    Channel: 1
Profile Descriptor List:
  "Dialup Networking" (0x1103)
    Version: 0x0100
Это наш профиль DUN (Dial-Up Networking).

Чобы создать соединение РРР, в /etc/ppp/peers создайте файл с именем новой точки. В этот файл мы свалим параметры для нового соединения. Скорее всего (во всяком случае, так обстоит дело с моим оператором), нам придётся отключить всякое сжатие. Должно получиться что-то вроде этого:

/dev/rfcomm0 # [ устройство, эмулирующее последовательною линию RS-232 по Bluetooth ]
 115200
 connect "/usr/sbin/chat /etc/chatscripts/gprs"

 noauth 
 defaultroute 
 lock 
 debug   # [ вставляем на всякий случай, пока всё не будет обкатано ]
 novjccomp
 nopcomp
 noaccomp
 nodeflate
 novj
 noccp
 nobsdcomp
 default-asyncmap
 ipcp-accept-local
 ipcp-accept-remote
 lcp-echo-failure 100
 lcp-echo-interval 30
 usepeerdns
 user <user_name>
Здесь user_name - имя пользователя, под которым Вы будете стучаться на РРР-сервер Вашего оператора. Как показывает практика, у некототорых операторов есть определённые ограничения на работу с PPP при доступе по GPRS. Это касается прежде всего поддержки сжатия (специально наматывают траф? :( ). Отключаем, всё что можно:
  1. novjccomp, novj - отключить сжатие TCP/IP по Ван Якобсону.
  2. nobsdcomp - отключить сжатие пакетов по схеме BSD.
  3. nodeflate - отключить сжатие Deflation.
  4. noaccomp - отключить сжатие адресов/управляющих пакетов.
  5. nopcomp - отключить согласование сжатия между удалённой точкой и нашей машиной.
  6. noccp - отключить протокол управления сжатием.
Далее, часто встречается ситуация, когда удалённая точка по каким-либо причинам не в состоянии корректно обеспечить управление линией (на уровне LCP - Link Control Protocol - протокол управления линией). Как правило, это выражается в том, что клиентская машина посылает LCP-запросы, чтобы выяснить состояние линии, но не получает отклика от сервера. В подобном случае содинение разрывается по инициативе клиента, т.к. pppd исходит из того, что линия неработоспособна. Насколько это хорошо, сказать сложно. Но изменить поведение демона, отключив ожидание откликов не представляется возможным. Я более склонен возложить вину на оператора, нежели на демон pppd, ибо ИМХО это вполне в силах оператора обеспечить корректную работу ppp-сервера.

Работа LCP в частности управляется параметрами lcp-echo-failure и lcp-echo-interval (на самом деле, параметров LCP, конечно, немного больше). Первый (lcp-echo-failure) задаёт максимальное количество попыток, по истечении которого считается, что произошёл сбой линии. Второй параметр (lcp-echo-interval) определяет временной интервал между посылками LCP-запросов, на которые удалённый сервер должен прислать своё эхо. Время устанавливается в секундах. В нашем случае параметры имеют значения 100 и 30 соответственно, т.е. если ни на один из посланных 100 запросов не был получен отклик, при том, что запросы отправляются с периодичностью в 30 с, то линия считается мёртвой и демон разрывает соединение. При этом в лог сыплется что-то вроде этого (при включении debug):

Nov  3 23:55:57 devel0 pppd[19993]: pppd 2.4.4 started by f0x, uid 1000
Nov  3 23:55:57 devel0 pppd[19993]: Using interface ppp0
Nov  3 23:55:57 devel0 pppd[19993]: Connect: ppp0 <--> /dev/rfcomm0
Nov  3 23:55:57 devel0 pppd[19993]: LCP: Rcvd Code-Reject for code 9, id 0
Nov  3 23:55:57 devel0 pppd[19993]: PAP authentication succeeded
Nov  3 23:55:58 devel0 pppd[19993]: local  IP address 10.20.85.219
Nov  3 23:55:58 devel0 pppd[19993]: remote IP address 212.98.170.50
Nov  3 23:55:58 devel0 pppd[19993]: primary   DNS address 77.74.32.66
Nov  3 23:55:58 devel0 pppd[19993]: secondary DNS address 77.74.32.11
Nov  3 23:56:27 devel0 pppd[19993]: LCP: Rcvd Code-Reject for code 9, id 1
Nov  3 23:56:57 devel0 pppd[19993]: LCP: Rcvd Code-Reject for code 9, id 2
Nov  3 23:57:27 devel0 pppd[19993]: LCP: Rcvd Code-Reject for code 9, id 3
...
Nov  4 00:43:29 devel0 pppd[19993]: LCP: Rcvd Code-Reject for code 9, id 95
Nov  4 00:43:59 devel0 pppd[19993]: LCP: Rcvd Code-Reject for code 9, id 96
Nov  4 00:44:29 devel0 pppd[19993]: LCP: Rcvd Code-Reject for code 9, id 97
Nov  4 00:44:59 devel0 pppd[19993]: LCP: Rcvd Code-Reject for code 9, id 98
Nov  4 00:45:59 devel0 pppd[19993]: No response to 100 echo-requests

lock - традиционный параметр, предписывающий pppd создавать файл блокировки и т.о. обеспечивает исключительный доступ к соединению. defaultroute - установить новый маршрут к PPP-шлюзу, полученный при установке соединения, маршрутом по умолчанию. usepeerdns - использовать DNS удалённой точки, позволяет не заботиться о поиске собственного DNS :) noauth - не требовать от удалённой точки авторизации. ipcp-accept-local и ipcp-accept-remote - параметры, определяющие, будут ли приняты адреса удалённой точки и нашей машины по инициативе удалённого сервера, даже если локально адреса уже были заданы. Так в общих чертах обстоит дело с конфигурированием соединения с удалённой точкой - РРР-сервером.

Далее, в случае с настройкой соединения с помощью скриптов идёт скрипт чата, определяющий реакцию на состояния линии ("занято", "звонок", "удалённый модем положил трубку" и т.п.). Здесь же прописывается номер, который набирает модем. В общих чертах скрипт чата выглядит так:

ECHO ON 
 ABORT '\nBUSY\r' 
 ABORT '\nERROR\r' 
 ABORT '\nNO ANSWER\r' 
 ABORT '\nNO CARRIER\r' 
 ABORT '\nNO DIALTONE\r' 
 ABORT '\nRINGING\r\n\r\nRINGING\r' 
 '' \rAT 
 TIMEOUT 12 
 OK ATH 
 OK ATE1 
 OK AT+CGDCONT=1,"IP","internet-address" 
 OK ATD*99*1# 
 TIMEOUT 22 
 CONNECT
Кладём этот файл в /etc/chatscripts/ под именем, ну, хотя бы gprs (за более подробными сведениями обращайтесь к man-страницам). Далее, в файле /etc/ppp/pap-secrets прописываем имя пользователя и пароль:
user_name * password
где user_name и password - соответственно имя пользователя и пароль, под которыми Вы ходите на сервер оператора. В общих чертах, всё. Теперь можно устанавливать соединение командой pppd call peer-name, где peer-name - имя точки доступа в /etc/ppp/peers.

Настройка доступа с помощью kppp отличается лишь тем, что параметры конфигурации для pppd и прочее нужно прописывать не в файлах ppp, а в диалогах kppp. В качестве устройства модема при конфигурировании устройства в kppp указывайте /dev/ircomm0 (или другое число вместо нуля в зависимости от того, как у вас настроена подсистема ircomm).

Sunday, November 11, 2007

Toshiba Satellite A100-811: Core 2 Duo и частота ядер после возврата из suspend-to-RAM (solved).

Довольно долгое время у меня на ноуте под управлением Ubuntu 7.04 наблюдался такой странный глюк. Во время работы от аккумулятора при засыпании ноута в память (suspend-to-RAM) и по возвращении из сна одно ядро процессора работало на пониженной частоте, как и положено в режиме экономии, а другое - почему-то начинало работать на полных оборотах. На просторах интернета я нашёл следующее решение: накатать скрипт следующего содержания:

#!/bin/sh

SYS_DIR=/sys/devices/system/cpu
POLICY=powersave

# ondemand

for CPU in `ls $SYS_DIR`
do
    echo -n "$POLICY" > $SYS_DIR/$CPU/cpufreq/scaling_governor
done
и положить его под именем <priority>-<name> в папку /etc/acpi/resume.d. Здесь priority - это число, определяющее порядок выполнения скрипта. Как и в директориях rc?.d файлы обрабатываются демоном acpid в алфавитном порядке и чтобы задать приоритет, необходимо приписать соответствующий префикс. Пусть в нашем случае это будет 10-core2duo-freq-fix.sh

Не понадобилось много времени, чтобы заметить, что данный способ не работает. Долго и упорно я пытался найти решение, копаясь в спецификации ACPI, в частности, производя поиски на предмет генерируемых событий. Много чего нового узнал, но того, что надо, так и не нашёл. Я не мог понять, почему в моей системе не генерируется событие по выходу из сна - resume... И не мог понять, отдаётся ли такое событие в принципе и предусмотрено ли оно самим ACPI. И хотя ответ на этот вопрос сразу найден не был, решение задачи оказалось гораздо проще и лежало оно в немного другой области. Собственно, решение мне подсказали на форуме и было даже немного обидно, до того всё оказалось очевидным :)
Чтобы понять все "что, как и почему" и вникнуть в суть, давайте немного пройдёмся по матчасти.

ACPI
Во времена оные всё было иначе - солнышко светило ярче, трава была зеленее и забористее, деревья большие, а для управления питанием использовался APM - интерфейс Advanced Power Management. APM - не что иное, как "расширение" BIOS'а по управлению питанием. Или говоря иначе, в системах с APM было устройство /dev/apm, на котором можно было сделать ioctl(), а остальное уже выполнялось железом. APM работал не идеально, но работал. Кто-то пришёл к выводу, что это не есть хорошо и так появился ACPI - Advanced Configuration and Power Interface. В отличие от APM, ACPI возлагает часть работы на операционную систему. Поэтому, строго говоря, если с APM всё было достаточно просто, т.к. со стороны ОС требовалась лишь поддержка интерфейса APM и вся работа выполнялась уже вне ядра ОС аппаратным обеспечением, то для нормальной работы энергосбережения через ACPI необходима корректная реализация этого самого ACPI в ОС, что несколько усложняет вещи. Но с другой стороны у ACPI есть преимущество перед APM. Если APM - это функция железа, то ACPI - это функция во многом ОС. Что легче заменить в случае неправильной работы? Железо или ядро ОС? Не будем углубляться в тонкости работы обоих интерфейсов. Сосредоточимся на ACPI. Как было упомянуто, тут ядро выполняет гораздо больше работы, чем в случае с APM. В частности, это позволяет переопределить поведение системы в некоторых случаях, где это было сложно или невозможно с APM. Работа ACPI управляется во многом событиями. Допустим такой пример: пользователь нажимает кнопку "power", это генерирует прерывание, которое перехватывается встроенным контроллером. Реакцией на прерывание становится выставление флагов. Таким образом, ОС уведомляется о том, что что-то произошло. Далее, ОС смотрит, что должно произойти дальше согласно DSDT и передаёт сообщение пользовательскому процессу в виде события. В роли процесса обычно выступает демон acpid, который читает файл /proc/acpi/event. Далее, acpid выполняет какие-то предопределённые конфигурацией действия. В общих чертах схема такова.

Suspend, как он есть.
Спецификация ACPI определяет набор так называемых состояний (глобальных - Gx и сна - Sx). suspend-2-ram - одно из S-состояний системы. В случае с переходом в suspend дела могут обстоять несколько иначе, чем было описано выше. В частности, если пользователь нажимает какую-то кнопку в диалоге выхода из системы, то процесс идёт в обратном направлении. Сам по себе процесс засыпания на этот раз инициируется пользовательским процессом - всё тем же acpid, допустим. Он выполняет некие действия, предусмотренные конфигурацией. Пусть была нажата кнопка перехода в состояние suspend-2-ram, происходит приблизительно следующее:

  1. acpid поучает сообщение через unix-сокет. Как гласит файл /etc/acpi/events/sleepbtn, обработчиком события является скрипт /etc/acpi/sleep.sh:
    # /etc/acpi/events/sleepbtn
    # Called when the user presses the sleep button
    # [ Здесь-то мы и определяем обработчик события ]
    
    event=button[ /]sleep
    action=/etc/acpi/sleep.sh
  2. Далее управление передаётся скрипту /etc/acpi/sleep.sh. Мы не будем рассматривать его здесь очень детально, скажем лишь, что этот скрипт вполне может умыть руки и передать управление gnome-power-manager'у или klaptopdaemon'у например, если кто-то из них запущен и не определено форсирование выполнения обработчика ACPI - /etc/acpi/sleep.sh. В противном случае, если демоны Gnome или KDE не работают или необходимо форсировать выполнение /etc/acpi/sleep.sh, далее по очереди вызывается скрипт prepare.sh. Он подготавливает пользовательское окружение к переходу в S-состояние. Всё, что делает этот скрипт - выполняет скрипты, сложенные в директории /etc/acpi/suspend.d:
    #!/bin/bash
    
    for SCRIPT in /etc/acpi/suspend.d/*.sh; do
      . $SCRIPT
    done
  3. Далее идёт кое-какая рутина. И, наконец, самое интересное, в конце... То, чего я не заметил и то, что оказалось так очевидно. Вызывается скрипт /etc/acpi/resume.sh, который тянет в свою очередь всё, что лежит в директории /etc/acpi/resume.d. Знакомое имя? :)
  4. acpid просит ядро заснуть. И наконец, очередь доходит до ядра, которое выполняет всю чёрную работу - останавливает и замораживает процессы и переводит железо в S-состояние. Теперь мы спим.

Хотя, на самом деле, есть событие ACPI, которое генерируется при пробуждении (Wakeup) - WAK, но как мы увидели, обработчика такого события у acpid нет. Всё сделано гораздо проще. Выполнение скрипта приостанавливается как раз в той точке, где в специальный файл записывается значение, определяющее тип сна - echo -n $ACPI_SLEEP_MODE >/sys/power/state. При пробуждении нам не нужны никакие обработчики, потому что скрипт продолжает выполняться со следующей строки и вполне логично доходит до вызова скриптов, что находятся в директории /etc/acpi/resume.d. Довольно красиво и просто. Итак, мы ответили на два вопроса - "что?" и "как?". Остался ещё один - почему не работает наш скрипт, помещённый в /etc/acpi/resume.d? Нет, конечно же он работает, если запускать его вручную. Но это не наш метод. Ответ на самом деле уже был дан. Осталось придать ему более чёткую формулировку.

KDE, Gnome и HAL
HAL - Hardware Abstraction Layer - предоставляет ещё один метод абстракции от железа и управления питанием. В десктоп-менеджерах, Gnome и KDE часто работают свои менеджеры питания. У меня, в частности, работает утилитка из kde-guidance-powermanager. Она не пользуется услугами acpid, используя вместо него HAL. HAL использует и gnome-power-manager. Дальше всё просто. Можно считать, решение задачи найдено. Мы уже знаем, где находятся скрипты HAL: /usr/lib/hal/scripts/linux. Здесь нам нужен скрипт hal-system-power-suspend-linux. Выполняется он по такому же принципу, как и скрипт-обработчик ACPI - т.е., доходит до определённой точки, усыпляет систему и продолжает выполнение далее по пробуждении. Вот этим-то мы и воспользуемся, добавив в конец скрипта такие строки:

for x in /proc/acpi/ac_adapter/*; do
    grep -q off-line $x/state

    if [ $? = 0 ] && [ x$1 != xstop ]; then
        /etc/acpi/resume.d/10-dualcore-freq.sh
    fi
done
Здесь мы проверяем, работает ли ноутбук от аккумулятора. Если мы работаем от сети, то ничего делать не надо. Профиль производительности cpufreq как был performance, так и остаётся. В противном случае, если мы работаем на аккумуляторе, сбиваем частоту ядер записью профиля powersave в /sys/devices/system/cpu/*/cpufreq/scaling_governor. Выравниваем частоту обоих ядер, чтобы не ошибиться :), так как ничего страшного в этом нет и искать ядро-спринтер нет нужды. Теперь всё должно работать. Мысленно поздравьте себя ;)


При написании частично использовалась статья http://www.advogato.org/. Большое спасибо аффтару.

udev в GNU/Linux

Содержание

* 1 udev - реализация devfs на уровне пользователя
* 2 Вступление
* 3 /dev
* 4 Проблемы. Какому файлу в /dev соответствует какое устройство?
* 5 Недостаточно номеров
* 6 объём статического /dev слишком велик
* 7 devfs
* 8 Цели, стоящие перед udev
* 9 namedev
* 10 libsysfs
* 11 udev

udev - реализация devfs на уровне пользователя

Вступление

Начиная с ядра версии 2.5 все физические и виртуальные устройства в системе видны пользователю через файловую систему sysfs, которая представляет все устройства ввиде иерархической древовидной структуры. Когда в системе появляется новое устройство или удаляется старое, hotplug уведомляет об этом окружение пользователя. Используя два этих момента, можно создать пользовательскую реализацию динамической файловой системы в /dev, которая предоставляла бы более гибкие правила именования устройств, нежели это было в devfs. В этом документе обсуждается udev - программа, которая заменяет devfs без потерь для функциональности системы. Более того, udev создаёт в /dev файлы только для тех устройств, которые присутствуют на данный момент в системе. udev предоставляет и новые возможности, которых не было в devfs:

1. постоянно закреплённые за устройствами имена, которые не зависят от того, какое положение они занимают в дереве устройств.
2. уведомление внешних по отношению к ядру программ, если устройство было заменено.
3. гибкие правила именования устройств.
4. динамическое использование нижних и верхних номеров устройств.
5. переход правил именования устройств из ядра в пространство пользователя.

Этот документ объясняет, почему программа, выполняемая в режиме пользователя предпочтительнее чем devfs, которая работает в режиме ядра. Далее также будет объяснено, как работает udev, как создавать для неё расширения (разные схемы именования устройств).

/dev

На каждом Linux-box'е /dev - это директория, в которой находятся файлы устройств, известных системе. Файл устройства - это интерфейс между пользовательскими приложениями и аппаратурой, механизм, предоставляемый ядром для доступа к физическим устройствам системы. Например устройство /dev/hda, как правило, представляет первый (master) IDE-накопитель на первичном канале. Имени hda соответствуют два числа - верхний и нижний номер устройства. Эти номера используются ядром для того, чтобы узнать, к какому физическому устройству необходимо обратиться (да, имена устройств существуют лишь для отвода глаз, реально же используются номера). Обычно каждому типу устройств отводится свой диапазон номеров. На данный момент есть ряд устройств одного типа с разными именами, но одинаковыми номерами (правда, их не так уж и много). Присвоение имён и номеров разным устройствам было проведено LANANA - The Linux Assigned Names And Numbers Authority. Список всех устройств можно найти в http://www.lanana.org/docs/device-list/devices.txt или в директории с исходными кодами ядра в Documentation/devices.txt. По мере того как в ядре появляется поддержка новых устройств, им необходимо выделать новые диапазоны номеров, с тем чтобы к ним можно было получить доступ через /dev. Одна из возможностей преодолеть дефицит номеров в статической /dev - организовать её как файловую систему (devfs). В ядрах серии 2.4 и более ранних диапазоны доступных номеров были 1-255 для нижней и 1-255 для верхней части.

Проблемы. Какому файлу в /dev соответствует какое устройство?

Когда ядро обнаруживает устройство, оно присваивает ему нижний/верхний номер, соответствующий типу устройства. Допустим, при загрузке ядро обнаружило USB-принтер. Оно присвоило этому устройству номера major:180 minor:0, в /dev этому устройству будет соответствовать файл /dev/usb/lp0. Второй USB-принтер получает номера 180:1 и к нему можно обращаться как к /dev/usb/lp1. Но если пользователь изменит топологию USB-устройств, скажем добавив USB-хаб? Тогда при следующей загрузке порядок обнаружения ядром принтеров может измениться. Принтер, на который раньше ссылались как на /dev/usb/lp1 превратиться в /dev/usb/lp0 и наоборот. Это справедливо и для устройств, которые могут быть подключены или удалены во время работы компьютера. Сказанное касается шин PCI hotplug, IEEE 394, USB, CardBus. Появление sysfs в ядрах 2.5 позволяет узнать какое именно устройство данного класса "скрывается" за данным minor-номером. Для системы с двумя подключенными USB-принтерами sysfs sys/class/usb может выглядеть следующим образом

/sys/class/usb

|--  lp0
|      |--dev
|      |--device -> ../../../devices/pci0/00:09.0/usb1/1-1/1-1:0
|      +--driver -> ../../../bus/usb/drivers/usblp
+--  lp1
       |--dev
       |--device -> ../../../devices/pci0/00:0d.0/usb3/3-1/3-1:0
       +--driver -> ../../../bus/usb/drivers/usblp


$ cat /sys/class/usb/lp0/device/serial HXOLL0012202323480 $cat /sys/class/usb/lp1/device/serial W09090207101241330

Симлинки lp0/device и lp1/device ссылаются на директории, содержащие дополнительную информацию об устройствах. Таким образом можно выяснить производителя устройства и его (устройства) уникальный (надеемся) серийный номер. Как мы выяснили, принтеру lp0 соответствует S/N HXOLL0012202323480, а lp1 - W09090207101241330. Если поменять порядок подключения принтеров или кинуть их через хаб, поменяются и их имена (но не серийные номера, конечно!). Вот что у нас получится:

/sys/class/usb

|--  lp0
|      |--dev
|      |--device -> ../../../devices/pci0/00:09.0/usb1/1-1/1-1.1/1-1.1:0
|      +--driver -> ../../../bus/usb/drivers/usblp
+--  lp1
       |--dev
       |--device -> ../../../devices/pci0/00:09.0/usb1/1-1/1-1.4/1-1.4:0
       +--driver -> ../../../bus/usb/drivers/usblp


$ cat /sys/class/usb/lp0/device/serial W09090207101241330 $cat /sys/class/usb/lp1/device/serial HXOLL0012202323480

Теперь принтеру lp0 соответствует S/N W09090207101241330, а lp1 - HXOLL0012202323480. sysfs позволяет с лёгкостью узнать, какому именно устройству соответствует какой файл. Это достаточно мощный механизм сопоставления физическим устройствам файлов устройств в /dev. Однако пользователя не интересует то, что lp0 и lp1 поменялись местами и что не мешало бы внести изменения в некоторые конфигурационные файлы. Пользователь просто хочет печатать на том принтере, на котором ему нужно, не задумываясь, к какому порту подключено USB-устройство и не подключено ил оно вообще через хаб.

Недостаточно номеров

Major- и minor-части номера устройства занимают по 8 бит (т.е., как было уже упомянуто 1-255 для верхней и столько же для нижней части). На данный момент для символьных и блочных устройств осталось не так уж и много major-номеров. Символьные и блочные устройства могут использовать один и тот же major-номер, ядро относит их к разным типам, поэтому конфликтов не возникает. Казалось бы, ещё море свободных номеров. Но на некоторых системах может использоваться, скажем, 4000 дисков, а 8 бит нижней части номера для этого уже явно не достаточно. Для каждого диска ядро резервирует 16 младших номеров, т.к. на диске может быть до 16 разделов. 4000 дисков требуют 64000 (!) файлов-устройств (не хило, не так ли, господа?). Но для того чтобы получить к ним доступ, есть лищь 255 номеров. Конечно и 8 битовая схема сработала бы, если распределить все диски по всему диапазону от 1 до 65535, но ведь большинство старших номеров уже зарезервировано. Дисковая подсистема не может просто оттяпать кусок из диапазона старших номеров. Если же это произошло, необходимо уведомить окружение пользователя, что отныне часть старших номеров переназначена под использование подсистемой SCSI. Из-за этого ограничения многие настаивают на увеличении диапазонов старших и младших номеров. Не стоит забывать и о том, что 4000 дисков это ещё не предел, на некоторых системах есть необходимость подключить одновременно 10000 накопителей. Похоже, что увеличение разрядности номера устройства могло бы произойти в ядрах серии 2.6, но по прежнему проблемой остаётся уведомление пользовательского окружения, какие major- и minor-номера заняты, а какие нет. Даже если диапазон верхних половин номеров будет увеличен, необходимо определить, какому устройству соответствует какой участок этого диапазона. Это требует наличия некоего органа, который будет заниматься распределением блоков номеров и стандартизированием имён файлов-устройств. Кроме того, неисключено, что в будущем и этот ресурс будет исчерпан. Если бы ядро автоматически распределяло диапазоны старших и младших номеров между устройствами, которые представлены в данный момент в системе, необходимость в таком органе отпала бы и верхний предел номеров, вероятно, не будет превышен. Идея с динамическим распределением номеров наталкивается на одно "но": окружение пользователя по прежнему не имеет понятия, какое оборудование подключено к машине и какие номера соответствуют каким устройствам. Пока ещё лишь немногие подсистемы ядра умеют динамически распределять номера устройств. Так например с успехом поступает подсистема USB2Serial (ещё со времён ядер серии 2.2). И вновь старая проблема, неведение пользователя. Было бы бесчеловечно заставлять его просматривать логи ядра, чтобы выяснить, какому устройству соответствуют определённые номера... sysfs, появившаяся в ветке 2.5, позволяет легко выяснить то, что нам так нужно.

Объём статического /dev слишком велик

Далеко не все устройства, для которых есть файлы в /dev рально присутствуют в системе. Некоторые могут быть просто не подключены на данный момент, а некоторых в ней никогда и не было. /dev как правило генерируется при установке ОС на жёстский диск и содержит файлы для всех устройств, известных на данный момент. На машине с установленным Red Hat 9 около 18000 файлов устройств. Иногда это сбивает с толку пользователей, которые пытаются определить, какие устройства реально присутствуют в системе. Поэтому в некоторых Unix-like ОС и дистрибутивах Linux управление /dev отдаётся на откуп ядру, которое знает, какие устройства подключены и которое создаёт в памяти devfs-систему. Такая схема стала достаточно популярной.

devfs

devfs используется в некоторых Unix-like ОС. Её реализация есть и в Linux. Однако в Linux devfs по-прежнему не решает многих проблем. devfs содержит лишь те устройства, которые представлены в системе и таким образом решает проблему с диким объёмом /dev. Но имена файлов в devfs не соответствуют именам, утверждённым LANANA. По этой причине полный переход со статического /dev на динамическую devfs проблематичен. Необходимы кардинальные изменения в файлах конфигурации. Создатели devfs попытались решить эту проблему, введя дополнительный слой совместимости (devfsd-демон), чтобы эмулировать /dev. Однако при том, что совместимость достигнута, правила именования устройств находятся в ведении ядра. Первый IDE-накопитель на первичном канале будет называться /dev/hda или /dev/ide/hd/c0b0t0u0 (лично у меня - /dev/ide/host0/bus0/target0/lun0/disc) и тут ничего не сделаешь. Вообще разработчики ядра не любят выносить правила именования устройств за пределы ядра. Но правила присвоения имён должны быть убраны из ядра. Это позволит разработчикам драйверов не заострять внимание на схемах именования. Короче, ядро не должно волновать то, как пользователь называет устройство. В случае с devfs это невозможно. devfs не позволяет распределять номера устройств динамически, в ней используются номера определённые LANANA. Хотя devfs можно модифицировать, чтобы она позволяла использовать динамические номера, никто до сих пор этого не сделал. Все данные (база данных, имена) devfs хранит в памяти ядра, которая не выгружается на диск. При больших количествах подключенных устройств (те же 4000 дисков) затраты памяти становятся весьма существенными.

Цели, стоящие перед udev

В свете упомянутых проблем началась работа на проектом udev. Цели этого проекта следующие:

1 переход в пространство пользователя
2 создание динамического /dev
3 предоставление пользователю возможности, самому формировать и назначать имена файлов-устройств
4 создание пользовательского API для доступа к информации об устройствах, подключённых к системе

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

1 namedev - работа с именами устройств
2 libsysfs - библиотека доступа к информации в sysfs
3 udev - динамический аналог /dev

namedev

Ввиду необходимости разных схем именования, эта часть проекта была выделена в отдельную подсистему. Это было сделано чтобы отграничить, собственно udev от правил назначения имён и обеспечить поддержку подключаемых схем другими группами разработчиков. Подсистема именования, namedev, - это стандартный интерфейс, который udev использует, чтобы присвоить имя данному устройству. В первых релизах проекта namedev пока компонуется в один исполняемый файл с udev. Вместе с этими релизами идёт и простая схема именования, основанная на спецификации LANANA. Она скорее всего подойдёт большинству пользователей Linux. namedev позволяет присвоить устройству имя исходя из пяти признаков:

1 метка или серийный номер
2 номер шины
3 номер устройства на шине
4 другое (подстановочное) имя
5 имя устройства в ядре

Первый признак - метка или серийный номер, проверяется при подключении устройства. Этот признак интерпретируется в зависимости от типа устройства. В случае SCSI - это UUID, для USB - серийный номер, для прочих блочных устройств - метка файловой системы. Второй признак - номер шины. Номер шины на большинстве систем меняется редко. Это может произойти при апгрейде BIOS или при подключении hotplug-контроллера. В кажды момент времени номер шины постоянен и уникален. Трерий признак - топология шины. Это может оказаться полезным в случае с USB-шиной. Подстановочное имя используется вместо имени устройства в ядре. И, наконец, если ни один из 4 предыдущих признаков не прошёл, то используется имя устройства в ядре. Это имя совпадает с именем, которое использовалось бы на системе без devfs или udev со статическим /dev. Вот пример конфига, который позволяет назначить устройствам имена, отличные от тех, которые использует ядро:

   1. USB Epson printer to be called lp_epson

LABEL, BUS="usb", serial="HXOLL0012202323480", NAME="lp_epson"

   1. USB HP printer to be called lp_hp

LABEL, BUS="usb", serial=@W09090207101241330", NAME="lp_hp"

   1. sound card with PCI bus id 00:0b.0 to be the first

NUMBER, BUS="pci", id="00:0b.0", NAME="dsp"

   1. sound card with PCI bus id 00:07.1 to be the second

NUMBER, BUS="pci", id="00:0b.0", NAME="dsp1"
libsysfs

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

udev

Чтобы приводить в действие правила именования устройств udev пользутся услугами namedev и libsysfs. udev вызывается каждый раз, когда ядро обращается к hotplug. Это происходит благодаря ссылке на udev в /etc/hotplug.d/default. Ядро сообщает hotplug массу интересной информации: что за устройство было добавлено (USB, PCI или ещё что-нибудь), а может оно было удалено, а не добавлено, где в sysfs находятся данные об устройстве, вызвавшем событие. Всё это передаётся в udev, который обращается в свою очередь к namedev, чтобы выяснить, какое имя присвоить устройству или просто удалить имя отключенного устройства (если action=remove). Т.о. содержимое /dev динамически изменяется на протяжении работы системы.

По материалам Linux Symposium Версия 2. Публикуется с небольшими изменениями.

Перевод (c) by Ваш покорный слуга, red f0x

Sunday, October 28, 2007

Toshiba Satellite A100-811: Bluetooth (solved)

Привет, мир! Я долго и упорно молчал, как румынский партизан и вот, наконец нашёл о чём написать :) Долго мне не удавалось заставить полноценно работать встроенный в ноутбук Bluetooth-адаптер. В конце-концов, задача была решена. Не скажу, что решение оказалось экстраординарным, но как это часто бывает, правильные мысли появляются пост-фактум.

Как писалось ранее, в некоторых моделях Toshiba Bluetooth включается через интерфейс HCI, в некоторых - через ACPI. Но стандартные драйвера toshiba и toshiba_acpi, входящие в ядро, для меня так и не заработали. Таким образом, первая проблема, с которой я здесь столкнулся, оказалось отсутствие драйвера. После некоторых поисков я наткнулся на проект omnibook на sourceforge.net. Предполагается, что все необходимые дайверы были скомпилированы статически или в виде модулей. Конфигурация приблизительно такая:

Configuration for 2.6 kernels
  Networking --->

<*> Bluetooth subsystem support --->
--- Bluetooth subsystem support <M> L2CAP protocol support <M> SCO links support <M> RFCOMM protocol support [*] RFCOMM TTY support <M> BNEP protocol support [*] Multicast filter support [*] Protocol filter support <M> HIDP protocol support
Bluetooth device drivers ---> <M> HCI USB driver [*] SCO (voice) support <M> HCI UART driver [*] UART (H4) protocol support [*] BCSP protocol support [*] Transmit CRC with every BCSP packet <M> HCI BCM203x USB driver <M> HCI BPA10x USB driver <M> HCI BlueFRITZ! USB driver (The four drivers below are for PCMCIA Bluetooth devices and will only show up if you have also selected PCMCIA support in your kernel.) <M> HCI DTL1 (PC Card) driver <M> HCI BT3C (PC Card) driver <M> HCI BlueCard (PC Card) driver <M> HCI UART (PC Card) device driver (The driver below is intended for HCI Emulation software.) <M> HCI VHCI (Virtual HCI device) driver
(Move back three levels to Device Drives and then check if USB is enabled. This is required if you use a Bluetooth dongle, which are mostly USB based.) USB support --->
<*> Support for Host-side USB --- USB Host Controller Drivers <M> EHCI HCD (USB 2.0) support [ ] Full speed ISO transactions (EXPERIMENTAL) [ ] Root Hub Transaction Translators (EXPERIMENTAL) <*> OHCI HCD support <*> UHCI HCD (most Intel and VIA) support < > SL811HS HCD support
Правда, PCMCIA я у себя отключил.

Итак, качаем последнюю версию модуля с sourceforge.net.

$ svn co https://omnibook.svn.sourceforge.net/svnroot/omnibook/trunk
...
$ mv trunk omnibook

Собираем модуль:

# cd omnibook
# make -C /lib/modules/$(uname -r)/build modules SUBDIRS=$(pwd)
или просто
# make; make install
:-) Теперь загружаем модуль с помощью modprobe omnibook.

В моём случае, правда, модуль не загрузился, т.к. моя модель не определилась как поддерживаемая. Модель определяется по данным из пула DMI (Desktop Management Interface - часть SMBIOS, хранящая данные о конфигурации платформы). Список официально поддерживаемых моделей можно найти здесь. Но если даже Ваша модель не поддерживается официально, можно выяснить, какая из официально поддерживаемых моделей наиболее близка Вашей и загрузить модуль принудительно с параметром ectype=x (ectype - embedded controller type - тип встроенного контроллера). В моём случае ectype равен 12 (Toshiba Satellite M70). Если всё прошло удачно, в директории /proc появится поддиректория omnibook. Каждый файл в этой директории соответствует одной функции контроллера, кроме файлов version и dmi (версия драйвера и информация, полученная из пула DMI соответственно):

f0x@devel0:~$ ls -la /proc/omnibook/
total 0
dr-xr-xr-x   2 root root 0 2007-10-23 23:50 .
dr-xr-xr-x 161 root root 0 2007-09-02 11:41 ..
-rw-r--r--   1 root root 0 2007-10-23 23:50 blank
-rw-r--r--   1 root root 0 2007-10-23 23:50 bluetooth
-rw-r--r--   1 root root 0 2007-10-23 23:50 display
-r--r--r--   1 root root 0 2007-10-23 23:50 dmi
-rw-r--r--   1 root root 0 2007-10-23 23:50 lcd
-r--r--r--   1 root root 0 2007-10-23 23:50 temperature
-rw-r--r--   1 root root 0 2007-10-23 23:50 throttling
-r--r--r--   1 root root 0 2007-10-23 23:50 version
-r--r--r--   1 root root 0 2007-10-23 23:50 wifi
За описанием функций контроллера Вы можете обратиться на http://omnibook.sourceforge.net/doku.php. Нас же здесь больше всего интересует файл bluetooth. Файл содержит фразу "Bluetooth adapter is present and disabled." Т.е., по умолчанию bluetooth-адаптер отключен. Включить его можно простой записью единицы в файл:
echo 1 > /proc/omnibook/bluetooth
Для выключения, аналогично, записываем в файл "0".

Далее предполагается, что все необходимые пакеты для работы с bluetooth (bluez-* или метапакет bluetooth, к-й потянет за собой по зависимостям всё, что нужно и что ненужно тоже) установлены. Таким образом, следующий шаг - настройка всего, что нужно для работы с bluetooth. Прежде всего следует сконфигурировать hcid - демон, предоставляющий интерфейс к HCI - интерфейсу хост-контроллера. Ниже приводится моя конфигурация с комментариями:

# /etc/bluetooth/hcid.conf
#
# HCI daemon configuration file.
#

# HCId options
options {
 # Automatically initialize new devices
 autoinit yes;

 # Security Manager mode
 #   none - Security manager disabled
 #   auto - Use local PIN for incoming connections
 #   user - Always ask user for a PIN
 #
 security auto;

 # Pairing mode
 #   none  - Pairing disabled
 #   multi - Allow pairing with already paired devices
 #   once  - Pair once and deny successive attempts
 pairing multi;

 # Default PIN code for incoming connections
 passkey "1234";
}

# Default settings for HCI devices
device {
 # Local device name
 #   %d - device id
 #   %h - host name
 name "%h";

 # Local device class
 class 0x3e0100;

 # Default packet type
 pkt_type DM1,DM3,DM5,DH1,DH3,DH5,HV1,HV2,HV3;

 # Inquiry and Page scan
 iscan enable;
 pscan enable;
 discovto 0;

 # Default link mode
 #   none   - no specific policy 
 #   accept - always accept incoming connections
 #   master - become master on incoming connections,
 #            deny role switch on outgoing connections
 lm accept;

 # Default link policy
 #   none    - no specific policy
 #   rswitch - allow role switch
 #   hold    - allow hold mode
 #   sniff   - allow sniff mode
 #   park    - allow park mode
 lp rswitch,hold,sniff,park;
}
Немного по опциям. Секция options - настройки демона:
  1. autoinit - автоматически инициализировать подсоединённые устройства (bluetooth-адаптеры)
  2. security - безопасность: none - не использовать PIN-код;
    auto - использовать для доступа к устройству, подсоединяемому через bluetooth локальный PIN по умолчанию, указанный в опции passkey;
    user - запрашивать PIN-код для установления соединения у пользователя.
  3. pairing - опции соединения: multi - разрешать соединения с устройствами, которые уже соединены;
    none - запретить соединения;
    once - допускать только одно соединение с помощью данного адаптера и игнорировать все последующие попытки установить новое соединение.
  4. passkey - PIN bluetooth-хоста по умолчанию. Он будет использоваться при добавлении Вашего компьютера другими bluetooth-устройствами в их списки устройств. Используйте любое число, которое Вам нравится.
Секция device - свойства контроллера:
  1. name - имя Вашего bluetooth-устройства (хоста). В моём случае bluetooth-хосту присваивается то же имя, что и имя Unix-хоста.
  2. class - класс устройства. Шестнадцатеричное число, определяющее класс bluetooth-устройства (хоста). С помощью битовой маски устанавливается тип хоста. За описанием битов обращайтесь к man hcid.conf или спецификации bluetooth. В моём случае средний байт 01 в 0x3e0100 обозначает, что мой хост - это компьютер, а не телефон, принтер или HID-устройство :-)
  3. pkt_type - типы пакетов. За разъяснениями также советую обращаться к спецификации Bluetooth или просто оставьте, как есть.
  4. iscan - разрешить другим bluetooth-устройствам обнаруживать Ваш хост при сканировании.
  5. pscan - разрешить другим устройствам устанавливать соединения с Вашим хостом, используя канал "page channel".
  6. discovto - время, на протяжении которого Ваш хост будет позволять обнаружить себя другому устройству. Устанавливается в секундах. 0 - устройство может быть обнаружено в любое время.
  7. lm - режим установки соединения: none - не определено;
    accept - всегда принимать входящие запросы на соединение;
    master - принимать входящие соединения в роли ведущего и запретить изменение этой роли при исходящих соединениях.
  8. lp - политика при установке соединения (касается пикосетей): none - не определено;
    rswitch - разрешить изменять роль (ведущий-ведомый);
    hold - эта политика необходима для синхронного обмена данными (нужно для беспроводной гарнитуры);
    sniff - позволить Вашему хосту пребывать в пикосети в определённые временные слоты. Когда устройство "отсутствует", оно может делать что-то другое, например, сканировать эфир, искать другие устройства;
    park - разрешить парковку хоста, т.е. позволять хосту переходить в энергосберегающий режим (сон). Устройство, входящее в пикосеть может "простаивать".
В принципе, то что здесь написано - это мой перевод информации из man hcid.conf, здесь я не буду разъяснять все принципы работы bluetooth-устройств и сетей, в которые такие устройства могут объединяться. Всем любопытным советую обратиться к первоисточнику - спецификации по Bluetooth.

Далее, нам необходимо отредактировать файл настройки подсистемы bluetooth - rfcomm для нашего хост-контроллера. rfcomm - Radio Frequency Communication - радиочастотная связь, это подсистема, которая эмулирует последовательную линию RS232 поверх протокола L2CAP. В принципе, настройка подсистемы rfcomm опциональна. Но для устройства, которое Вы хотите использовать, как модем, для сотового телефона, настроить её будет желательно, чтобы не делать этого каждый раз вручную. Ниже приведён пример конфигурационного файла:

# /etc/bluetooth/rfcomm.conf
#
# RFCOMM configuration file.
#

rfcomm0 {
# # Automatically bind the device at startup
 bind yes;
#
# # Bluetooth address of the device
 device 00:0A:0B:0C:0D:0E;
#
# # RFCOMM channel for the connection
# channel 1;
#
# # Description of the connection
 comment "SonyEricsson T630";
}
Пояснения:
  1. rfcomm0 - первый последовательный интерфейс. Вообще-то, Вы можете сконфигурировать их столько, сколько захотите. Конфигурация каждого последовательного интерфейса начинается с его имени (rfcomm0, rfcomm1, rfcomm2, ...). Сами параметры интерфейса указываются в блоке между двумя фигурными скобками { и };
  2. bind - автоматически привязывать сокет для нашего устройства при его обнаружении (yes или no, в случае, если Вы не хотите, чтобы сокет связывался автоматически;
  3. device - MAC-адрес bluetooth-устройства (сотового телефона, например);
  4. channel - канал bluetooth, который будет использоваться для связи с устройством;
  5. comment - опциональный комментарий к интерфейсу.
При указании МАС-адреса Вашего устройства, его сначала надо узнать. Для этого поднимем устройство hci и просканируем эфир:
# hciconfig hci0 up
# hciconfig piscan
# hcitool scan
Scanning ...
        00:0E:0F:10:11:12       Red Quick Fox
Здесь мы просто запускаем устройство hci0 с помощью утилиты hciconfig и сканируем эфир с помощью hcitool с параметром scan. 00:0E:0F:10:11:12 - это и есть нужный нам MAC-адрес. Утилита hciconfig предназначена для конфигурирования хост-контроллера hci. Если контроллеров несколько, то они имеют имена hci0, hci1 и т.д.. hcitool позволяет совершать некие вменяемые действия с помощью нашего хост-контроллера. Иными словами, она позволяет им управлять. Для списка агрументов обеих утилит см. man hciconfig и man hcitool. Используя утилиту l2ping и МАС-адрес в качестве её аргумента, можно послать пинга телефону :-)

Следующая стадия конфигурирования bluetooth - редактирование ещё одного файла. В debian-based дистрибутивах linux это /etc/default/bluetooth. По существу, этот файл нужен только стартовому скрипту bluetooth из /etc/init.d. Сложного там ничего нет. Всё, что от Вас требуется, указать, должны ли сервисы bluetooth стартовать при загрузке системы и если да, то какие. Приведём пример файла /etc/default/bluetooth:

# Defaults for bluez-utils

# This file supersedes /etc/default/bluez-pan.  If
# that exists on your system, you should use this
# file instead and remove the old one.  Until you
# do so, the contents of this file will be ignored.

# start bluetooth on boot?
# compatibility note: If this variable is not found bluetooth will
# start
BLUETOOTH_ENABLED=1

############ DUND
#
# Run dund -- this allows ppp logins. 1 for enabled, 0 for disabled.
DUND_ENABLED=1

# Arguments to dund: defaults to acting as a server
DUND_OPTIONS="--listen --persist"

# Run dund --help to see the full array of options.
# Here are some examples:
#
# Connect to any nearby host offering access
# DUND_OPTIONS="--search"
#
# Connect to host 00:11:22:33:44:55
# DUND_OPTIONS="--connect 00:11:22:33:44:55"
#
# Listen on channel 3
# DUND_OPTIONS="--listen --channel 3"

# Special consideration is needed for certain devices. Microsoft
# users see the --msdun option.  Ericsson P800 users will need to
# listen on channel 3 and also run 'sdptool add --channel=3 SP'

############ SDPTOOL
# this variable controls the options passed to sdptool on boot, useful if you
# need to setup sdpd on boot.
# options are ;-separated, i.e. for every ; an sdptool instance will be
# launched
#
# examples:
# SDPTOOL_OPTIONS="add --channel=3 SP" # ericsson P800 serial profile
# SDPTOOL_OPTIONS="add --channel=8 OPUSH ; add --channel=9 FTRN" # motorola
#                                             # object push and file transfer
SDPTOOL_OPTIONS=""

Здесь переменная BLUETOOTH_ENABLED=1 определяет, что при загрузке системы должны стартовать и сервисы bluetooth. Если переменная установлена в 0, поддержка bluetooth не включается. Если переменная не найдена, то по умолчанию сервисы загружаются. DUND_ENABLED=1 определяет, что должен быть запущен демон Dial-up-доступа. Ну, вот практически и всё. В переменной SDPTOOL_OPTIONS определяется, какие сервисы должен предоставлять наш компьютер другим bluetooth-устройствам. Список сервисов можно пополнить и вне этого файла с помощью sdptool (см. man sdptool). В общем и в целом Unix-box теперь готов к работе с bluetooth-устройствами. Можете по bluetooth зайти на сотовый телефон, настроить пикосеть, работающую через BNEP или ещё что-нибудь. Про HID я не писал, ибо не имею таковых девайсов. Про настройку пикосети на bluetooth Вы сможете узнать сами.

Что касаемо телефонов, было замечено одно занятное обстоятельство: PIN, указанный в конфигурационном файле hcid.conf внаглую игнорируется с новыми телефонами (т.е., с теми, с которыми мы раньше не работали). В качестве противоядия можно использовать следующее: создать маленький скрипт, который просто выводит с помощью echo PIN-код - echo "PIN:XXXX", где ХХХХ - Ваш PIN. Допустим, этот скрипт будет называться helper. Далее, можно запустить passkey-agent --default ./helper. По идее, агент должен запускаться автоматически для каждой пользовательской сессии, но как показывает опыт, он этого почему-то не делает. Можно самостоятельно запихать passkey-agent в какой-нибудь скрипт. Ну, вот, собственно, и всё. Чтобы использовать сотовый телефон для Dial-up доступа в интернет в качестве GPRS-модема, укажите в качестве устройства модема /dev/rfcomm0 в конфиге Вашей звонилки, настройте доступ согласно информации Вашего оператора. А дальше - всё как всегда.

Tuesday, August 28, 2007

Селим OpenSolaris рядом с Linux: загрузка одним bootloader'ом или история одной глупости

Уже довольно давно, может с месяца с 4, у меня валялся набор из 2 дисков OpenSolaris 10 Starter Kit. Так как солярой я интересовался и раньше и эпизодически ею пользовался (Sun Solaris 9 x86), то решил наконец попробовать и этот продукт. На диске есть 3 дистрибутива в редакции community: Nexenta, Schilix и Belenix. Опробовав последовательно все 3 образа (они загружаются с DVD-диска как Live без необходимоси установки), я остановил свой выбор на последнем, как на наиболее соответствующем моим требованиям дистре (gcc в комплекте и всё необходимое для разработки - библиотеки, плюс, конечно, zfs, Solaris zones, DTrace и прочее).

Кратко о самом дистре Belenix: довольно симпатичный дистр, в качестве рабочей графической среды предоставляется на выбор KDE 3.5 или Xfce, коим я и пользуюсь. По умолчанию графика не запускается. В смысле, она запускается на полуавтомате при загрузке LiveCD, но не в уже установленном солярисе, где по умолчанию пускать Xfce нужно явно, руками (хотя, никто не мешает Вам настроить этот момент под Ваши нужды). Порадовало, что без проблем определяются интегрированное аудио и eth-адаптер на nForce430, с дисковым контроллером проблем также никаких (у меня 2 SATA-II винчестера). В отличие от Nexenta здесь ничего не валится в коры буквально на ровном месте. Установка проходит без проблем и в принципе, процедура довольна тривиальна, поэтому на ней я останавливаься не буду. Единственное, что хотелось бы отметить, что инсталлятор как был во всех версиях Solaris, что мне попадались - текстовый, таковым остался и здесь. Однако, я не считаю это недостатком. Но вместе с тем, опций здесь лишь самый минимум - думаю, можно было бы сделать и побольше, хотя бы на предмет того, какой софт нужно ставить, а какой нет.

Итак, я выкроил свободное место на втором винчестере и поставил OS туда. Непредвиденные трудности начались, когда я попытался при перезагрузке получить мой свежеустановленный Belenix. Суть проблемы в следующем: до этого у меня уже был установлен Debian GNU/Linux 'Etch' и всё, что обреталось у меня на компьютере, грузилось его grub'ом. Честно говоря, мне не было особого резона использовать в качестве общесистемного загрузчика Solaris'овский grub, потому как всё же большую часть времени я провожу в Linux и такая схема была бы не очень удобна. Но Linux'овый grub понятия не имеет о zfs или ufs2 (хотя, последнее по идее должен уметь). В конце концов я уже готов был пойти на крайность и сделать общесистемным загрузчиком Solaris'овский grub. Перезагрузившись с Belenix LiveCD, вызвал installgrub со следующими параметрами:

# grubinstall -m /mnt/solaris0/boot/grub/stage1 /mnt/solaris0/boot/grub/stage2 /dev/rdsk/c2d0s0
В общем, здесь всё довольно self-explanatory: указываем файл для первой стадии загрузки grub (сектор, внедряемый в MBR), образ для второй стадии и слайс, на котором стоит Solaris (честно говоря, я до сих пор путаюсь в схемах BSD и Solaris, но если я не ошибаюсь, то это должен бы быть лейбл, а не слайс, ведь слайс - это раздел в терминологии DOS, однако по имени видно, что у нас контроллер 2, первый диск на 2-м контроллере и 1-й слайс на этом диске - так или иначе, но эта схема совершенно отлична от той, что используется в Linux и не совсем похожа на Free- или NetBSD'шную. ИМХО в FreeBSD имена устройств выглядят логичнее и нагляднее, чем в Solaris). Ключ -m здесь просто добавляет интерактивности. Если Вы не накачались пивом и прекрасно понимаете, что Вы собираетесь сделать, то им можно и пренебречь :) /mnt/solaris0 - подмонтированная ФС с уже установленной на диск солярой - мы будем брать необходимые файлы с этой ФС. Так вот, каково было моё разочарование, когда я обнаружил, что первый загрузочный сектор оказался не в MBR первого винчестера, как я ожидал, а в mboot'e второго. Указать installgrub'у явным образом, куда необходимо воткнуть загрузочный сектор не представлялось возможным.

В общем, ситуация вышла достаточно комичной. Linux'овый grub в упор не воспринимал Solaris'овскую ufs2 и о том, чтобы он мог грузить соляровое ядро непосредственно не могло быть и речи, а с другой стороны соляровый загрузчки упорно становился в mboot второго диска и, следовательно, пролетал при загрузке.

Решение было ясно как день и простое, как карандаш. Честно говоря, если бы не лень, можно было бы не морочить себе голову. Дело в том, что я пытался редактировать меню загрузки на лету, добавляя
rootnoverify (hd1)
chainloader +1
Но по не совсем понятной мне причине Linux'овый grub ругался кодом ошибки 27 и как резанный кричал, что не знает такой команды. Редактирование menu.lst для Linux'ового grub'а расставило всё по своим местам (хотя и странно, почему такого результата не удавалось добиться редактированием параметров загрузки на лету - ограничение внедряемого кода grub-шелла для экономии, чтобы по возможности сократить число секторов, которые необходимо внедрить?). Теперь и солярис и линукс грузятся одним загрузчиком - Linux'овым, с той поправкой, что солярис грузится не непосредственно, а через встроенный в цепь собственный загрузчик, который живёт в MBR второго диска.

Результат: в MBR 1-го диска установлен Linux grub, а в файл menu.lst для загрузки зацеплённого солярового grub'a, что установлен нами ранее в MBR 2-го винчестера installgrub'ом, были добавлены строки:

title  Belenix 32-bit
rootnoverify (hd1)  # Используем в качестве корня MBR 2-го диска (hd1), и не пытаемся ничего монтировать - не получится, т.к. там просто нет ФС и нечего монтировать :)
chainloader +1  # Встраиваем в цепь вторичный бутлоадер, на к-й ссылается MBR 2-го диска
Есть ещё одно обстоятельство. Файл menu.lst для солярового grub'a также нуждается в небольшой правке, которую лучше сделать именно в файле, чем каждый раз корректировать на лету команды загрузки. А именно, по непонятному недоразумению, которое уже описывалось ранее, соляровый инсталлятор grub'a, а вместе с ними вдогонку и сам grub почему-то считают, что они установлены на 1-м диске, а не на 2-м, как у меня обстоит дело в реальности. В результате, в /boot/grub/menu.lst для соляриса имеем такие сроки:
title  Belenix (32Bit, ACPI)
  root (hd0,3,a) # Ошибка: Корневая ФС, на которой находятся файлы образа ядра и загрузочный архив находится на втором диске, а не на первом (первое число должно быть 1, а не 0)!
  # Далее - всё в полном порядке :)
  kernel /platform/i86pc/multiboot kernel/unix
  module /platform/i86pc/boot_archive
Строка root нуждается в следующей правке: 0 необходимо заменить на 1 для второго диска, а в остальном всё правильно в моём случае - слайс 3, лейбл - а. С учётом сказанного, она должна выглядеть так: root (hd1,3,a). Хотя, если у Вас OS установлен на 1-м винчестере, то эта правка Вам не нужна.

Вместо Afterword: как и в Linux'e в OpenSolaris есть определённые ограничения для корневой ФС или, по крайней мере, /boot. Так, в Linux нет возможности внедрить секторы со второй стадией загрузки grub в раздел, который отформатирован в xfs (просто в xfs исторически не предусмотрена такая возможность, т.к. эта ФС была разработана для SGI IRIX и изначально к Linux никакого отношения не имела. Итог состоит в том, что в голове xfs-раздела нет необходимых 64 Кб для внедрения grub-секторов. Поэтому или всю корневую ФС или, по крайней мере, /boot нужно держать на ФС, более традиционной для Linux - ReiserFS, Ext2/3 и т.п.) Аналогично, grub на самом деле не имеет понятия о пулах zfs (на самом деле, если не ошибаюсь), но тот, что имеется в OS прекрасно работает по крайней мере с ufs2. Поэтому, выход здесь, как и в случае с Linux - держать корневую ФС на ufs2, а уже остальное, типа /home - в пуле на zfs.

PS: небольшая попроавка к примеру с xfs и grub. Честно говоря, я не уверен, что информация актуальна, но насколько можно судить сейчас по некоторым данным grub прекрасно работает и с корневой ФС на xfs. Но так как я не уверен, то текст оставил без исправлений. Если я всё-таки ошибся, то извините за неточность - мы все учимся, и не редко на ошибках и с их же помощью :)

ПОСЕТИТЕЛИ

free counters