QNX RTP Logo QNX Realtime Platform: Русский Портал QNX
Tuesday, 18 Sep 2007 15:02
Меню

Проект OpenNET - все о Unix
Получение списка файловых дескрипторов процесса Print E-mail
Андрей Чиликин (atchilikine at hotmail.com)

Трудно найти приложение, которое работало бы само по себе и не нуждалось в обмене данными с другими процессами. QNX 6.x предоставляет мощный механизм обмена данными, основанный на передаче сообщений с помощью семейства функций MsgSend(). Сообщения передаются через так называемые каналы, создаваемые серверным приложением с помощью системного вызова ChannelCreate(). После того, как канал создан, к нему могут подключаться клиентские приложения с помощью вызова ConnectAttach(). ConnectAttach() возвращает клиенту идентификатор, который тот потом использует для отправки сообщений серверному приложению с помощью MsgSend(). Все это может происходить совершенно неявно для разработчика, скрываясь под такими библиотечными вызовами стандарта POSIX, как open(), read(), close()...

Условно говоря, идентификаторы каналов, полученные клиентом после выполнения вызова ConnectAttach(), можно разделить на три группы:

  • стандартные каналы для передачи данными, которые по сути своей являются обычными файловыми дескрипторами. Примеры: открыть файл на диске, создать сокет, открыть для чтения последовательный порт и т.д.;
  • дескрипторы каналов непосредственно подключенных к серверу. Применяются для прямой связи между программами, в поле index вызова ConnectAttach() добавляется _NTO_SIDE_CHANNEL. Очень распространенной ошибкой является объявление канала связи как обычного файлового дескриптора, без указания _NTO_SIDE_CHANNEL, при вызове ConnectAttach(). Такие приложения часто аварийно завершаются при параллельном запуске диагностических утилит.
  • дескриптор канала связи с ядром - дескриптор канала, получаемого каждым процессом при его создании;
  • Дескрипторы первой группы всегда меньше значения _NTO_SIDE_CHANNEL, второй - всегда больше, а в третью группу входит лишь один дескриптор - он равен _NTO_SIDE_CHANNEL. Непосредственно по нему выполняется взаимодействие процесса с ядром.

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

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

    struct _server_info info; int fd = 0; while(( fd = ConnectServerInfo(pid, fd, &info)) != -1) { // анализ структуры info fd++; }

    Нужно отметить такую особенность работы ConnectServerInfo() – если запрашиваемый канал не существует, то будет возвращен следующий по номеру. То есть, если мы запрашивали информацию о fd = 5, а такого дескриптора у процесса нет, но есть дескриптор 7, то вызов вернет именно 7. Если fd больше или равен _NTO_SIDE_CHANNEL, то такой канал интереса для нас не представляет, так как является каналом для передачи системных сообщений, и никакой полезной дополнительной информации мы получить по нему не можем.

    В случае успеха в структуре info будут заполнены многие поля, но нас интересуют три:

    info.nd – сетевой номер узла QNX, на котором запущен сервер,
    info.pid – идентификатор менеджера ресурсов, с которым установлен канал связи,
    info.chid – идентификатор канала связи с менеджером ресурсов.

    Здесь можно провести параллель с работой tcp – каждый канал связи определяется набором client pid + connection id / server pid + channel id. Сервер (менеджер ресурса) создает канал, на котором он принимает соединения от клиентов (то же самое делает и сервер в tcp – создает сокет, привязывает его к какому-то порту и слушает на этом порту соединение от клиентов). Клиент, который хочет получить доступ к ресурсам сервера должен установить с ним связь, подсоединившись к каналу с известным номером (то же самое делает и клиент в tcp – устанавливает связь с сервером по известному ему порту). То есть мы можем сказать что info.pid и info.chid – это tcp адрес и порт сервера, а pid и fd, которые мы передали в ConnectServerInfo() – это адрес и порт клиента.

    Таким образом мы теперь знаем, на какой сервер и channel id отображается файловый дескриптор процесса pid и можем сами подключиться к этому серверу используя ConnectAttach():

    coid = ConnectAttach( info.nd, info.pid, info.chid, 0, 0)) if ( coid != -1 ) { /* мы смогли подключится к тому же самому ресурсу, что процесс pid */ }

    Теперь нам осталось только попытаться продублировать полученный coid чтобы иметь возможность работать с ним как с простым файловым дескриптором. Для дублирования мы должны послать серверу, с которым мы установили связь, сообщение _IO_DUP:

    /* структура для передачи сообщения _IO_DUP, определена в sys/iomsg.h */ io_dup_t dup; /* сбрасываем все поля структуры в ноль */ memset(&dup, 0, sizeof(dup)); /* заполняем структуру info значениями, полученными ранее с помощью ConnectServerInfo */ dup.i.info = info; dup.i.type = _IO_DUP; dup.i.combine_len = sizeof(dup); dup.i.info.pid = pid; if ( MsgSend( coid, &dup.i, sizeof(dup.i), 0, 0 ) != -1 ) { /* coid является дубликатом файлового дескриптора fd, который мы использовали для вызова ConnectServerInfo() и мы можем общаться с ним как с обыкновенным файловым дескриптором в нашем процессе */ }

    Обратите внимание на строчку:

    dup.i.info.pid = pid;

    Без этой маленькой хитрости мы бы не смогли получить копию файл дескриптора. Подставляя в это поле id процесса, для которого мы вызывали ConnectServerInfo(), мы заставляем думать сервер, что именно этот процесс, а не мы, хочет получить копию fd. Если бы не сделали этого, то тогда связка client id + connection id / server id + channel id была бы нарушена и сервер не смог бы продублировать данный файловый дескриптор, так как просто не нашел бы дескриптор, копию которого мы хотели бы получить.

    Теперь, когда вызов MsgSend() завершился удачно, мы можем работать с coid как с простым файлом. Конечно этим не нужно злоупотреблять, чтобы не нарушить работу процесса pid. Но зато теперь мы можем попробовать получить полную информацию о файле, на которую способна система.

    Для этого воспользуемся вызовом iofdinfo():

    struct _fdinfo info; char path[PATH_MAX]; if ( iofdinfo( coid, 0, &info, path, PATH_MAX ) != -1 ) { printf("flags %d size %llu offset %llu path %s\n", info.flags, info.size, info.offset, path);

    В случае успеха info.flags будет содержать режим, в котором открыт файл – READ ONLY, READ/WRITE, WRITE ONLY, поле size – текущий размер файла, offset – текущую позицию файла. Path – полный путь к файлу, если система может его установить. А что если вызов iofdinfo() вернул ошибку? Вполне возможно, что обработка сообщения _IO_FDINFO не реализована для данного типа файлового дескриптора. Что же, можно предположить что это не файл, а tcp/ip сокет. Проверить это довольно легко, нужно просто попытаться получить тип сокета:

    int sock = coid; int type; socklen_t optlen = sizeof(opt); if (getsockopt(sock, SOL_SOCKET, SO_TYPE, &type, &optlen)!= -1){ ... }

    Если вызов getsockopt() завершился успешно, то мы можем выяснить и остальные свойства данного сокета. Сначала можно попытаться выяснить, к какому локальному адресу он привязан:

    struct sockaddr_in sa; int len = sizeof(sa); getsockname(sock, (struct sockaddr *)&sa, &len);

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

    if (getpeername(sock, (struct sockaddr *)&sa, &len == 0) { /* sa теперь содержит адрес и порт, к которому присоединен sock */ } else { /* скорее всего этот сокет еще не соединен с удаленным адресом, или этот порт в состоянии LISTEN */ }

    Теперь было бы полезно узнать текущее состояние данного TCP/IP соединения. Зачем, спросит читатель? Представьте, что вы отлаживаете сетевое приложение и ожидаете соединения к определенному порту. Но если соединение было установлено, и после этого разорвано, то, в зависимости от того, как оно было разорвано, соединение может перейти в различные состояния: CLOSE_WAIT, LAST_ACK или CLOSED. Таким образом состояние сокета может дать много полезной информации. К сожалению, авторам не известно как получить состояние сокета напрямую, зная лишь его дескриптор, поэтому используется чтение внутренних данных самого стека TCP/IP с последующим нахождением сокетов, относящихся к заданному процессу. Такой подход не дает стопроцентной гарантии получения полной информации так как за промежуток времени получения списка дескрипторов и получения списка сокетов эти списки в системе могут измениться, но для наших целей это вполне приемлемо.

    Поскольку стек TCP/IP в QNX является полным аналогом TCP/IP стека из NetBSD, вплоть до полного соответствия заголовочных файлов, эту задачу можно решить, изучив код утилиты netstat, входящей в стандартную поставку системы. Так как исходный код netstat для QNX не доступен, был проанализирован код системы NetBSD [8].

    Для получения массивов данных используется структура типа nlist_t, описанная в заголовочном файле . Массив структур nlist может состоять из нескольких элементов, каждый из которых определяется своим номером и содержит символьную строку - тип запрашиваемой информации. В нашем случае она определяется так:

    // socket.c в примере к статье static struct nlist nl[] = { #define N_TCBTABLE 0 {"_tcbtable"}, // элемент nl[N_TCBTABLE] - таблица TCP сокетов #define N_UDBTABLE 1 {"_udbtable"}, // элемент nl[N_UDBTABLE] - таблица UDP сокетов {""} // пустая строка завершает список };

    Чтобы заполнить эту структуру, надо отправить менеджеру сети сообщение _IO_SOCK_NLIST. Безусловно, мы не собираемся делать это руками - в библиотеке libsocket специально для этого есть вызов kvm_nlist(). Исходный код этой библиотеки, к сожалению, несколько устаревший, доступен в [7].

    Структуры данных внутри TCP/IP стека хранятся в виде двунаправленных списков, что означает, что нам необходим только один адрес - адрес головного элемента. Именно его мы и получаем в поле n_value структуры nlist: nl[N_TCBTABLE].n_value - указатель на таблицу TCP сокетов, и nl[N_UDBTABLE].n_value - соответственно на таблицу UDP сокетов. Таблица сокетов в памяти стэка описывается структурой inpcbtable, а ее элементы - структурой inpcb.

    Заполним структуру inpcbtable вызовом kvm_read() из библиотеки libsocket:

    struct inpcbtable table; kvm_read( nl[N_TCBTABLE].n_value, (char*)&table, sizeof(table) );

    И найдем первый элемент списка:

    struct inpcb *head, *next; head = (struct inpcb*)&((struct inpcbtable *)nl[N_TCBTABLE].n_value)->inpt_queue.cqh_first;

    Теперь мы можем перемещаться по списку сокетов и получать по ним детальную информацию:

    struct inpcb inpcb; struct tcpcb tcpcb;

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

    kvm_read((unsigned long)next, (char*)&inpcb, sizeof inpcb ); kvm_read((unsigned long)inpcb.inp_ppcb, (char*)&tcpcb, sizeof(tcpcb));

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

    Хотя все это рассматривалось исходя из предположения, что сокет работает по протоколу IPv4, все это может быть с легкостью переделано для работы с любым протоколом – IPv4 или IPv6. Как это можно сделать очень подробно рассматривает W. Richard Stevens в [2]. Однако для работы с IPv6 необходимо иметь в своем распоряжении QNX Momentics PE версии 6.2 и выше.

    Что же, после того, как мы смогли узнать столько много о файловых дескрипторах процесса мы попытались оформить это как утилиту, которая может помочь при отладке других программ. Полные исходные коды утилиты fdl (file descriptors list) можно скачать по адресу ftp://ftp.qnx.org.ru/pub/projects/fdl/fdl.tgz

    Запускается fdl просто – нужно указать в командной строке идентификатор или имя процесса, для которого вы хотите получить список файловых дескрипторов:

    #fdl 1

    или

    #fdl procnto

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

    В командной строке можно передать несколько параметров:

    -n запустит fdl в режиме netstat –an – будут отображены все активные в системе сокеты.
    -P отобразит всех родителей процесса вплоть до procnto. Обратите внимание на следующую особенность – если у процесса в качестве родительского указан только procnto, то скорее всего настоящий родительский процесс уже завершился.
    -m отобразит информацию по всем копиям процесса. Так, например, если у вас открыто несколько Photon file manager, то вызов

    #fdl –m pfm

    отобразит информацию для каждого из них.

    Исходный код fdl разбит на несколько файлов:
    fdl.h – заголовочный файл с определениями структур и функций.
    main.c – разбор параметров командной строки и вызов соответствующих процедур.
    proc.c - функции работы с процессами – поиск процесса по заданному идентификатору, поиск идентификатора по заданному имени и т.п.
    file.c – работа с файловыми дескрипторами
    socket.c – работа с сокетами
    makefile – может принимать несколько параметров:

    DBG=DEBUG – создать отладочную версию с дополнительным отладочным выводом
    сlean – удалить все объектные файлы, но оставить исполняемый
    clear – удалить все, оставить только файлы исходного кода
    install – скопировать исполняемый файл в /usr/local/bin
    backup – создать tgz файл с исходными кодами

    Для установки исходного кода скопируйте файл fdl.tgz в любой каталог и выполните команду tar –xzvf fdl.tgz - будет создан каталог src/fdl. Перейдите в него и вызовите make или make install.

    [1] Getting started with QNX Neutrino 2.00 : a guide for realtime Programmers. By Rob Krten.
    [2] UNIX Network Programming. Networking APIs: Sockets and XTI. Volume 1. Second Edition. By W. Richard Stevens
    [3] http://qnx.org.ru/docs-devel/ - все статьи разработчику.
    [4] http://qnx.org.ru/forum - форум.
    [5] Перевод статьи Томаса Флетчера "Разделение файловых дескрипторов между процессами". http://qnx.org.ru/docs-devel/sharefd.html
    [6] Перевод части "Writing Resource Managers" руководства разработчика. ftp://ftp.qnx.org.ru/pub/docs/resmgr.zip
    [7] Web-based QNX CVS Repository: http://cvs.qnx.com
    [8] NetBSD CVS Repositories: http://cvsweb.netbsd.org/bsdweb.cgi

    [Вернуться к списку]
    ©   2000-2003 Команда проекта QNX.ORG.RU // QNX.ORG.RU Team
    Авторы проекта: Дмитрий Алексеев [dmi] и Дмитрий Васильев. Техническое сопровождение проекта: Игорь Сорокин [isorokin]. Информационное сопровождение: Дмитрий Алексеев [dmi]
    QNX - зарегистрированная торговая марка QNX Software Systems, Ltd., Canada. Остальные упоминаемые на сайте торговые марки и логотипы являются исключительно собственностью их уважаемых владельцев. Ничьи права не затронуты. Материалы сайта не могут быть скопированы и где-либо использованы в той или иной форме без письменного разрешения разработчиков сайта.
    Powered by Mambo Open Source