В статье приведён обзор общего подхода к разработке драйверов сетевой подсистемы
typedef struct {struct device sc_dev; /* common device */struct ethercom ec; /* common ethernet */struct _iopkt_self *iopkt;struct cache_ctrl cachectl; /* для управления кэшем */struct mii_data bsd_mii; /* для работы с шиной MII */void *pci_dev_hdl; /* если устройство на шине PCI*/nic_config_t cfg; /* структура с конфигурацией устройства */nic_stats_t stats; /* структура со статистикой */mdi_t *mdi; /* для работы с MDI */void *sd_hook; /* аварийный callout-вызов *//* PCI and Irq */int iid; /* для передачи interrupt id */int irq[ALL_IRQS_AMOUNT]; /* массив для id прерываний, если их несколько */struct pci_dev_info pci_info; /* структура PCI устройства */struct _iopkt_inter inter_misc; /* структура потокового обработчика событий */struct _iopkt_inter inter_rx;struct _iopkt_inter inter_tx;/* Memory */volatile uint8_t *hw_mem; /* указатель на память устройства */int32_t bmtrans; /* смещение адресов если это PCI устройство *//* Rings *//* Здесь описываются кольца дескрипторов, уже зависит от вашей реализации *//* Other */struct callout mii_callout; /* для периодического вызова монитора MII шины */struct callout reaper_callout; /* для периодического вызова очистки кольца дескрипторов */uint16_t mii_timeout;uint16_t reaper_timeout;} device_dev_t
Для каждого устройства она может отличаться, но описанные базовые вещи выше, есть в каждом драйвере.
IOPKT_DRVR_ENTRY_SYM_INIT()
. Также необходимо проинициализировать структуру cfattach
С помощью вызова макроса CFATTACH_DECL()
. В своём коде слово "device..." меняется на название вашего сетевого адаптера, например eth2500_entry.
struct _iopkt_drvr_entry IOPKT_DRVR_ENTRY_SYM(device) = IOPKT_DRVR_ENTRY_SYM_INIT(device_entry);CFATTACH_DECL(device,sizeof(device_dev_t),NULL,device_attach,device_detach,NULL);
На данном этапе мы зарегистрировали точку входа в наш драйвер device_entry().
Теперь необходимо определить и реализовать данную функцию.
Прототип определён в struct _iopkt_drvr_entry :: drvr_init().
В общем случае в ней реализуется, как правило, парсинг строки параметров, передаваемых в драйвер и заполнение структуры nic_config_t.
VID
:DID
и deviceindex
адаптера на шине PCI. Данное действие осуществляется в цикле, т.к. при нахождении нескольких соответствующих устройств для каждого из них должна быть вызвана device_attach() функция. Пример реализации device_entry() для PCI ethernet адаптера:
struct i_attach_args {struct _iopkt_self *iopkt;char *options;unsigned busvendor, busdevice;int busindex;uint8_t bus, devfn;uint32_t my_parsed1_param;uint32_t my_parsed2_param;uint8_t fill[2];};static unsigned known_device_ids[] = {PCI_DEVICE_ID_VENDOR_DEVICE0, /* 0x0001 */PCI_DEVICE_ID_VENDOR_DEVICE1, /* 0x0002 */PCI_DEVICE_ID_VENDOR_DEVICE2, /* 0x0003 */0};int device_entry (void *dll_hdl, struct _iopkt_self *iopkt, char *options){nic_config_t *cfg;int devid, idx;unsigned bus, dev_func;int err, single;int instance;struct device *dev;struct i_attach_args iargs;cfg = calloc(1, sizeof(*cfg));if (cfg == NULL) {return ENOMEM;}if (options != NULL) {cfg->vendor_id = 0xffffffff;cfg->device_id = 0xffffffff;cfg->device_index = -1;if ((err = device_parse_options (NULL, options, cfg)) != EOK) {(free)(cfg);return (err);}if (cfg->device_id != 0xffffffff) {known_device_ids[0] = cfg->device_id;known_device_ids[1] = 0;}if (cfg->vendor_id == 0xffffffff) {cfg->vendor_id = PCI_VENDOR_ID_VENDOR;}else {cfg->vendor_id = PCI_VENDOR_ID_VENDOR;cfg->device_index = -1;}memset (&iargs, 0x00, sizeof(iargs));iargs.iopkt = iopkt;iargs.options = options;instance = single = 0;for (devid = 0; known_device_ids[devid] != 0; devid++) {idx = ((cfg->device_index == -1) ? 0 : cfg->device_index);while (1) {if (pci_find_device (known_device_ids[devid],cfg->vendor_id, idx, &bus, &dev_func) != PCI_SUCCESS) {break;}iargs.busvendor = cfg->vendor_id;iargs.busdevice = known_device_ids[devid];iargs.busindex = idx;iargs.bus = (uint8_t) bus;iargs.devfn = (uint8_t) dev_func;dev = NULL; /* No Parent */if (dev_attach ("wm", options, &device_pci_ca, &iargs, &single,&dev, NULL) != EOK) {goto done;}dev->dv_dll_hdl = dll_hdl;instance++;if (cfg->device_index != -1)/* Only looking at a specific index */break;idx++;}}done:(free)(cfg);if (instance)return (EOK);return (ENODEV);}
Данный код является довольно шаблонным для всех PCI ethernet устройств.
В нём осуществляется парсинг параметров переданных в драйвер с помощью функции device_parse_options(), реализацию которой объясним ниже.
Также заполнение структуры i_attach_args
. В ней могут быть заполнены определённые нами параметры для текущего устройства и параметры PCI. Далее данная структура передаётся в device_attach() функцию.
В цикле происходит поиск устройства на PCI шине с помощью функции pci_find_device() по VID
:DID
. DID
берётся из определённого нами массива known_device_ids[]
. В случае если драйвер универсален для разных устройств одного вендора.
Если устройство найдено, вызывается dev_attach(), который в свою очередь передаст исполнение в device_attach функцию.
int dev_attach( char *drvr,char *options,struct cfattach *ca,void *cfat_arg,int *single,struct device **devnp,int (*print)( void *, const char * ) );
Параметры:
Отдельно рассмотрим реализацию функции device_parse_options(). Она тоже является шаблонной, вызывается несколько раз:
static char *device_opts[] = {"num_receive", // 0NULL // END};int device_parse_options (device_dev_t *device, const char *optstring, nic_config_t *cfg){char *value, *options, *freeptr, *c;int opt, invalid, rc = EOK;int tmp;if (optstring == NULL)return 0;/* getsubopt() is destructive */options = malloc (strlen (optstring) + 1, M_TEMP, M_NOWAIT);if (options == NULL)return ENOMEM;strcpy (options, optstring);freeptr = options;while (options && *options != '\0') {c = options;invalid = 0;if ((opt = getsubopt (&options, device_opts, &value)) != -1) {if (device == NULL)continue;switch (opt) {case 0:device->num_receive = strtoul(value, 0, 0);break;default:rc = EINVAL;invalid = 1;break;}}elseif (nic_parse_options (cfg, value) != EOK) {rc = EINVAL;invalid = 1;}if (invalid) {slogf (_SLOGC_NETWORK, _SLOG_WARNING, "devnp-e1000: unknown option %s", c);}}free (freeptr, M_TEMP);return rc;}
В device_opts[]
устанавливаются названия параметров, передаваемых в драйвер.
В switch/case заносим код для данного параметра.
После обработки параметров и нахождения устройства вызывается функция dev_attach(), которая впоследствии вызовет функцию device_attach().
В функции device_attach() как правило необходимо:
Прототип определён в struct cfattach :: ca_attach().
Первое, что необходимо сделать - заполнить nic_config_t значениями по-умолчанию. Также заполнить структуру устройства с уникальными для него параметрами. Затем вызвать функцию device_parse_options(), которая изменит стандартные параметры на переданные в драйвер.
Устанавливаем флаги ifp->if_flags
, которые отвечают за отображение режима приёма устройства:
struct ifnet *ifp = NULL;ifp = &device_dev->ec.ec_if;ifp->if_softc = device_dev;ifp->if_flags = IFF_BROADCAST | IFF_SIMPLEX | IFF_MULTICAST;
Далее заполняем поле valid_stats
в nic_stats_t и nic_ethernet_stats_t. Исходя из документации на устройство, указываем какая статистика актуальна.
Пример для PCI устройства:
device_dev->pci_dev_hdl = pci_attach_device( NULL,PCI_INIT_ALL | PCI_INIT_IRQ | PCI_MASTER_ENABLE,busindex,pci_info);if (device_dev->pci_dev_hdl == NULL) {err = errno;return errno;}device_dev->hw_mem = mmap_device_memory( NULL,pci_info->BaseAddressSize[0],PROT_READ | PROT_WRITE | PROT_NOCACHE, MAP_SHARED,PCI_MEM_ADDR(pci_info->CpuBaseAddress[0]));if (device_dev->hw_mem == MAP_FAILED) {return errno;}
Затем инициализируем работу с кэшем процессора:
if (cache_init(0, &device_dev->cachectl, NULL) == -1) {err = errno;return (err);}
В дальнейшем это необходимо для управления когерентностью кэша процессора при помощи макросов CACHE_INVAL() и CACHE_FLUSH()
При помощи mmap(), mmap64() выделяем память для колец дескрипторов.
![]() | Поскольку mmap(), mmap64() выделяет память страницами по 4КБ, стоит задуматься над тем, чтобы в одно выделение мы поместили и RX и TX кольцо. |
device_dev->mem_area = mmap(NULL,TX_RING_SIZE(device_dev) * sizeof(device_tx_desc_t) +RX_RING_SIZE(device_dev) * sizeof(device_rx_desc_t) +NET_CACHELINE_SIZE,prot_flags,map_flags,tmem_fd,0);
Затем при помощи drvr_mphys() получаем физический адрес памяти нужного кольца и передаём его в сетевое устройство.
Для обработки событий требуется заполнить структуру struct _iopkt_inter
, в которой мы регистрируем 2 callback-функции func(), enable() и поле arg
, в которое мы сохраняем указатель на структуру нашего сетевого устройства.
func() - функция обработчик событий в потоке. В ней будет вся тяжеловесная логика обработки события.
enable() - функция включения/размаскирования прерываний. Вызывается сразу после func().
Пример для регистрации события RX в драйвере:
device_dev->inter_rx.func = device_process_interrupt_rx;device_dev->inter_rx.enable = device_intr_enable_rx;device_dev->inter_rx.arg = device_dev;if ((interrupt_entry_init(&device_dev->inter_rx, 0, NULL, device_dev->priority)) != EOK)return (-1);
Для пополнения очереди событий нужно вызвать interrupt_queue().
Прототип interrupt_queue().
const struct sigevent * interrupt_queue( struct _iopkt_self *,struct _iopkt_inter * );
Механизм BSD Media используется для управления сетевыми интерфейсами, а именно скоростью, дуплексным режимом и rx/tx-pause (flow control).
Для упрощения разработки, код bsd_media копируется из драйвера в драйвер. Меняются параметры и указатель на устройство.
Описание полной реализации в будущем будет вынесено в отдельную статью, примеры есть в исходных кодах драйверов.
Ниже описание API:
bsd_mii
структуры. Добавление вариантов медиастатусов интерфейса. bsd_mii
структуры.
PHY - это приёмо-передатчик физического интерфейса, реализующий физический уровень.
Связь с MAC уровнем осуществляется по шине MII. Как правило, общение с PHY осуществляется путём записи и чтения регистров MAC уровня (MII_ACCESS
, MII_DATA
, и т.д.).
Необходимо настроить режим автосогласования PHY и определение соединения (link).
Для этого есть два варианта пути:
Для работы с API MDI понадобится:
Реализации phy_read() и phy_write() аппаратно-зависимы, прототипы функций представлены в MDI_Register_Extended().
Пример реализации mdi_callback():
void mdi_callback ( void *hdl,uint8_t phy_id,uint8_t link_state ){device_dev *dev = (device_dev*)hdl;int i, mode;char *s;struct ifnet *ifp = &dev->ecom.ec_if;switch (link_state) {case MDI_LINK_UP:if ((i = MDI_GetActiveMedia(dev->mdi, dev->cfg.phy_addr, &mode)) != MDI_LINK_UP)mode = 0;switch (mode) {case MDI_10bTFD:s = "10 BaseT Full Duplex";dev->cfg.duplex = 1;dev->cfg.media_rate = 10000L;break;case MDI_10bT:s = "10 BaseT Half Duplex";dev->cfg.duplex = 0;dev->cfg.media_rate = 10000L;break;case MDI_100bTFD:s = "100 BaseT Full Duplex";dev->cfg.duplex = 1;dev->cfg.media_rate = 100000L;break;case MDI_100bT:s = "100 BaseT Half Duplex";dev->cfg.duplex = 0;dev->cfg.media_rate = 100000L;break;case MDI_100bT4:s = "100 BaseT4";dev->cfg.duplex = 0;dev->cfg.media_rate = 100000L;break;case MDI_1000bT:s = "1000 BaseT Half Duplex";dev->cfg.duplex = 0;dev->cfg.media_rate = 1000000L;break;case MDI_1000bTFD:s = "1000 BaseT Full Duplex";dev->cfg.duplex = 1;dev->cfg.media_rate = 1000000L;break;default:s = "Unknown";dev->cfg.duplex = 0;dev->cfg.media_rate = 0L;break;}dev->cfg.flags &= ~NIC_FLAG_LINK_DOWN;if_link_state_change(ifp, LINK_STATE_UP);break;case MDI_LINK_DOWN:dev->cfg.media_rate = dev->cfg.duplex = -1;MDI_AutoNegotiate (dev->mdi, dev->cfg.phy_addr, NoWait);dev->cfg.flags |= NIC_FLAG_LINK_DOWN;if_link_state_change(ifp, LINK_STATE_DOWN);break;default:break;}}
Суть данной функции в установке текущего медиастатуса и состояния соединения в интерфейсе. Вызывается она при помощи MDI_MonitorPhy().
При обнаружении MDI_MonitorPhy() изменения в состоянии соединения (link), вызывается mdi_callback().
Есть два варианта реализации мониторинга состояния PHY:
Второй предпочтительнее, но не все контроллеры имеют такую возможность.
Инициализация callback-функций в структуре сетевого интерфейса "struct ifnet":
ifp->if_ioctl = device_ioctl;ifp->if_start = device_start;ifp->if_init = device_init;ifp->if_stop = device_stop;IFQ_SET_READY(&ifp->if_snd);if_attach(ifp);
Установка описания устройства и установка MAC-адреса:
#define DEVICE_DESCRIPTION "Ethernet device description"strcpy((char *)cfg->device_description, DEVICE_DESCRIPTION);ether_ifattach(ifp, cfg->current_address);
Функция ether_ifattach() доинициализирует интерфейс как интерфейс медиасреды Ethernet (IEEE 802.3). Существуют другие варианты доинициализации: ieee80211_ifattach(); token_ifattach(); и другие.
Установка callout-вызова, который будет запущен при завершении работы стека:
void device_shutdown(void *arg);device_dev->sd_hook = shutdownhook_establish(device_shutdown, device_dev);
На этом attach сетевого устройства завершён.
Прототип определён в struct ifnet :: if_init().
Данный callback вызывается при ifconfig up интерфейса. Также при смене параметров, например ifconfig mtu 9000.
Задачи данного callback'a:
Данный пункт является аппаратно-зависимым. Как правило это запись включения в регистр MAC блока. Тоже самое и с FIFO блоком.
Сетевые устройства используют дескрипторы для приёма/передачи фреймов по сети.
Кольцевые RX/TX буферы состоят из данных дескрипторов. Для того, чтобы отправить фрейм в сеть, необходимо заполнить дескриптор и выставить флаг OWN
, означающий, что данным дескриптором теперь владеет сетевое устройство (флаг владения дескриптором на некоторых контроллерах может называться по-другому). После передачи дескриптора устройству, начинается процесс отправки буфера (находящегося в дескрипторе) в сеть. Иногда несколько дескрипторов собираются в один фрейм. Это рассматривается ниже в описании процесса отправки. Структуры дескрипторов, как правило, аппаратно-зависимы, их определения чаще всего присутствуют в документации на конкретное сетевое устройство.
В качестве примера ниже приведена структура дескриптора сетевого контроллера ETH2500:
typedef struct {uint32_t base; /* RBADR [31:0] */int16_t buf_length; /* BCNT only [13:0] */int16_t status;int16_t msg_length; /* MCNT only [13:0] */uint16_t reserved1;uint32_t etmr; /* timer count for ieee 1588 */} __attribute__((packed)) eth2500_rx_desc_t;
В device_init() необходимо инициализировать rx-дескрипторы. Как правило, это подразумевает:
Пример инициализации rx-дескрипторов:
struct mbuf *m;off64_t phys;device_tx_desc_t *desc = rx_desc_ring[i];m = m_getcl_wtp(M_DONTWAIT, MT_DATA, M_PKTHDR, wtp); /* Получаем mbuf от стека */phys = pool_phys(m->m_data, m->m_ext.ext_page); /* Получаем физический адрес данного mbuf */CACHE_INVAL(&device_dev->cachectl, m->m_data, phys, m->m_ext.ext_size); /* Инвалидируем кэш процессора *//* Присваимаем текущему дескриптору физический адрес и размер mbuf */desc->base = phys;desc->buf_lenght = m->m_ext.ext_size;desc->status = RD_OWN; /* Указываем, что данным дескриптором управляет сетевое устрйство */
Всё это делается в цикле для каждого дескриптора.
В сетевом драйвере обычно используются 2 callout'a: mii_callout и reaper_callout.
mii_callout_dev() - Включается для мониторинга состояния соединения (если нет возможности подключить прерывание на изменение состояния соединения)
reaper_callout_dev() - Включается для очистки кольца TX дескрипторов. Для того, чтобы в каждом TX прерывании успешной передачи не очищать mbuf, указатели на них накапливаются в отдельной очереди и затем периодическим вызовом reaper_callout() очищаются накопленные mbuf. Если не хватает дескрипторов, в device_start будет вызываться функция очистки принудительно. Инициализируются они в device_attach(), а включаются в device_init().
Пример инициализации и включения callout'a:
struct callout mii_callout;int interval = 3 * 1000; /* Задержка, спустя которую будет вызвана переданная функция. 3 секунды */void mii_callout_dev(void *arg) {device_dev_t* device_dev = (device_dev_t*)arg;/* логика проверки состояния соединения *//* ... */callout_msec(&mii_callout, device_dev->interval, mii_callout_dev, device_dev); /* Для повторного вызова */}callout_init(&mii_callout); /* Инициализация callout'a. В device_attach() */callout_msec(&mii_callout, interval, mii_callout_dev, device_dev); /* Для запуска callout'a. В device_init() */
Прерывания могут быть трёх типов:
Как правило в SoC и PCI устройствах используются Legacy прерывания. MSI и MSI-X относятся именно к шине PCI, но их активация и поддержка не везде реализована.
В PCI устройствах тип прерываний определяется после pci_attach_device() в структуре struct pci_dev_info в поле msi
.
А номер прерывания для регистрации обработчика прерываний в поле Irq
.
В SoC устройствах номера прерываний как правило задаются в таблице прерываний устройства в документации. Для некоторых устройств подобная таблица может описывать отдельно SPI (shared peripheral interrupts) поэтому для получения правильного номера прерывания необходимо будет добавить 32
.
Обработчик прерываний должен проходить как можно быстрее. Обработка "события-источника прерывания" происходит в потоке. Очередь событий для потока заполняет обработчик прерываний. Обработчик прерываний, как правило, должен: замаскировать прерывания; добавить событие в очередь. Остальные операции в обработчике прерываний зависят от конкретного контроллера.
Заполняем структуры и регистрируем обработчики:
/* Записываем номер прерывания. Данная секция должна находиться в device_attach() */cfg->num_irqs = ALL_IRQS_AMOUNT;device_dev->irq[IRQ_RX0] = cfg->irq[0] = pci_info->Irq;/* Регистрируем обработчик прерывания. Данная секция должна находиться в device_init() */if ((InterruptAttach_r( device_dev->irq[IRQ_RX0],device_isr_rx,device_dev,sizeof(*device_dev),_NTO_INTR_FLAGS_TRK_MSK)) < 0)return -1;/* Регистрируем потоковый обработчик. Данная секция должна находиться в device_attach()*/device_dev->inter_rx.func = device_process_interrupt_rx;device_dev->inter_rx.enable = device_intr_enable_rx;device_dev->inter_rx.arg = device_dev;if ((interrupt_entry_init(&device_dev->inter_rx, 0, NULL, device_dev->priority)) != EOK)return (-1);
Подробнее об InterruptAttach(), InterruptAttach_r()
Подробнее реализация обработчиков прерываний будет рассмотрена ниже.
Данные флаги необходимы для работы логики прёма/передачи:
ifp->if_flags_tx |= IFF_RUNNING;ifp->if_flags_tx &= ~IFF_OACTIVE;ifp->if_flags |= IFF_RUNNING;
Флаги описаны выше в пункте Заполнение структур
Прототип определён в struct ifnet :: if_ioctl().
IOCTL вызовы необходимы для манипуляции с базовыми параметрами устройств. Копирование статистики nicinfo. Управление интерфейсом с помощью ifconfig
Обычно данная реализация является шаблонной:
intdevice_ioctl(struct ifnet * ifp, unsigned long cmd, caddr_t data){int error = 0;device_dev_t *device = ifp->if_softc;struct drvcom_config *dcfgp;struct drvcom_stats *dstp;struct ifdrv_com *ifdc;switch (cmd) {case SIOCGDRVCOM:ifdc = (struct ifdrv_com *)data;switch (ifdc->ifdc_cmd) {case DRVCOM_CONFIG:dcfgp = (struct drvcom_config *)ifdc;if (ifdc->ifdc_len != sizeof(nic_config_t)) {error = EINVAL;break;}memcpy(&dcfgp->dcom_config, &device->cfg, sizeof(device->cfg));break;case DRVCOM_STATS:dstp = (struct drvcom_stats *)ifdc;if (ifdc->ifdc_len != sizeof(nic_stats_t)) {error = EINVAL;break;}// update_stats(device);memcpy(&dstp->dcom_stats, &device->stats, sizeof(device->stats));break;default:error = ENOTTY;}break;case SIOCSIFMEDIA:case SIOCGIFMEDIA: {struct ifreq *ifr = (struct ifreq *)data;error = ifmedia_ioctl(ifp, ifr, &device->bsd_mii.mii_media, cmd);break;}default:error = ether_ioctl(ifp, cmd, data);if (error == ENETRESET) {error = 0;}break;}return error;}
Прототип определён в struct ifnet :: if_start().
Когда необходимо отправить фрейм в сеть, io-pkt-* вызывает функцию device_start().
В данной функции необходимо реализовать:
![]() | Функции дефрагментации и паддинга фрейма нужны не всем сетевым устройствам. |
Представленный ниже код будет аппаратно-зависимым. Но принципы общие.
Проверка включённости интерфейса и установка необходимых флагов:
if ((ifp->if_flags_tx & IFF_RUNNING) == 0) {NW_SIGUNLOCK (&ifp->if_snd_ex, device->iopkt);return;}ifp->if_flags_tx |= IFF_OACTIVE; /* Установка флага, указывающего, что интерфейс занят отправкой. */
Реализация отправки фреймов в сеть осуществляется в цикле, для того, чтобы отправить всю накопленную очередь.
/* Проверка количества свободных дескрипторов и сравнение с пороговым значением */if (device->tx_free < device->tx_reap)device_reap(device); /* Запуск процесса очистки дескрипторов перед отправкой *//* Основной цикл извлечения mbuf'ов из очереди */for (;;) {IFQ_POLL(&ifp->if_snd, m0); /* Макрос для просмотра очереди передачи. Не извлекает из очереди mbuf */if (m0 == NULL)goto done;/* Ещё раз убеждаемся, что есть свободные дескрипторы */if (!device->tx_free) {device->stats.tx_failed_allocs++;goto done;}/* Функция паддинга фрейма. Заполняем нулями, если размер фрейма слишком мал для данного сетевого устройства. */if (device_pad(m0)) {device->stats.tx_failed_allocs++;goto done;}IFQ_DEQUEUE(&ifp->if_snd, m0); /* Макрос для извлечения mbuf из очереди. Если извлекаем, то обязаны отправить фрейм. */#ifndef CHAIN_SUPPORT /* Если устройство не поддерживает отправку фрейма несколькими дескрипторами запускаем дефрагментацию */if ((m = device_defrag(m0)) == NULL) {device->stats.tx_failed_allocs++;ifp->if_oerrors++;goto done;}m0 = m;#endif/* Подсчитываем количество mbuf'ов в цепочке */for (num_frag=0, m=m0; m; num_frag++) {m = m->m_next;}cur_tx_wptr = device->cur_tx_wptr;free_wptr = cur_tx_wptr;for (m=m0; m; m = m->m_next) {if (!m->m_len) {num_frag--;continue;}tdesc = &device->tdesc[cur_tx_wptr]; /* Берём свободный дескриптор */phys = mbuf_phys(m); /* Получаем физический адрес mbuf'a */CACHE_FLUSH(&device->cachectl, m->m_data, phys, m->m_len); /* Сбрасываем кэш процессора *//* Заполняем TX дескриптор. У каждого устройства своя реализация дескрипторов. */tdesc->base = ENDIAN_LE32((uint32_t)(phys + device->bmtrans));tdesc->buf_length = ENDIAN_LE16(-m->m_len);tdesc->misc = 0x00000000;#ifdef CHAIN_SUPPORT /* Если устройство поддерживает отправку фрейма несколькими дескрипторами */tdesc->status = TX_ST_OWN;if (m == m0)tdesc->status |= TX_ST_STP;if (m->m_next == NULL)tdesc->status |= TX_ST_ENP;tdesc->status = ENDIAN_LE16(tdesc->status);#elsetdesc->status = ENDIAN_LE16(TX_ST_OWN | TX_ST_ENP | TX_ST_STP);#endiffree_wptr = cur_tx_wptr;cur_tx_wptr = (cur_tx_wptr + 1) % device->num_transmit;}device->reg[E_CSR] = INEA|TDMD; /* Запускаем передачу. У данного устройства необходимо выставить флаг *//* Сохраняем указатель на mbuf, чтобы очистить его позднее */device->tx_mbuf[free_wptr] = m0;device->cur_tx_wptr = cur_tx_wptr;device->tx_free -= num_frag;#if NBPFILTER > 0 /* Включаем отправку сырых пакетов всем слушающим сокетам. Например для работы tcpdump */if (ifp->if_bpf) {bpf_mtap(ifp->if_bpf, m0);}#endif} // fordone:device->start_running = 0;ifp->if_flags_tx &= ~IFF_OACTIVE; /* Выставляем флаг окончания процесса отправки. */NW_SIGUNLOCK_P(&ifp->if_snd_ex, iopkt_selfp, wtp); /* Освобождаем мьютекс отправки */
Некоторые устройства не могут отправлять в сеть фреймы меньше определённого размера. Для этого необходимо реализовать функцию заполнения конца фрейма нулями.
Данная реализация является шаблонной:
static inline int device_pad(struct mbuf *pkt){struct mbuf *last = NULL;int padlen;if (likely(pkt->m_pkthdr.len >= ETHER_MIN_NOPAD))goto done;padlen = ETHER_MIN_NOPAD - pkt->m_pkthdr.len;if (pkt->m_pkthdr.len == pkt->m_len &&M_TRAILINGSPACE(pkt) >= padlen) {last = pkt;} else {for (last = pkt; last->m_next != NULL; last = last->m_next) {continue;}if (M_TRAILINGSPACE(last) < padlen) {struct mbuf *n;MGET(n, M_DONTWAIT, MT_DATA);if (n == NULL)return ENOBUFS;n->m_len = 0;last->m_next = n;last = n;}}KDASSERT(!M_READONLY(last));KDASSERT(M_TRAILINGSPACE(last) >= padlen);memset(mtod(last, caddr_t) + last->m_len, 0, padlen);last->m_len += padlen;pkt->m_pkthdr.len += padlen;done:return 0;}
От стека на отправку приходят пакеты в цепочке mbuf'ов. Связано это с оптимизацией отправки фрейма состоящего из нескольких уровней модели OSI.
Некоторые устройства не могут отправлять цепочку mbuf'ов несколькими дескрипторами. В таком случае необходимо использовать функцию дефрагментации цепочки mbuf'ов.
Данная функция является затратной, поскольку производится копирование данных из цепочки в один буфер.
Реализация данной функции является шаблонной:
struct mbuf *device_defrag(struct mbuf *m){struct mbuf *m2;if (m->m_pkthdr.len > MCLBYTES) {m_freem(m);return NULL;}MGET(m2, M_DONTWAIT, MT_DATA);if (m2 == NULL) {m_freem(m);return NULL;}M_COPY_PKTHDR(m2, m);MCLGET(m2, M_DONTWAIT);if ((m2->m_flags & M_EXT) == 0) {m_freem(m);m_freem(m2);return NULL;}m_copydata(m, 0, m->m_pkthdr.len, mtod(m2, caddr_t));m2->m_pkthdr.len = m2->m_len = m->m_pkthdr.len;m_freem(m);return m2;}
При реализации отправки лучше всего начинать с простого:
Как правило приём на сетевых устройствах реализован посредством прерываний. В редких случаях это polling (периодический опрос контроллера).
При завершении получения фрейма контроллер генерирует прерывание. При выходе из обработчика прерывания обычно необходимо маскировать прерывание. При этом в потоковом обработчике событий для минимизации использования прерываний по завершении обработки ранее полученного фрейма желательно производить опрос оборудования на предмет получения нового фрейма.
В самом обработчике есть два варианта реализации работы с прерываниями:
Обработчиков прерываний может быть несколько, поскольку могут быть задействованы несколько линий прерываний для разных типов событий (получение/отправка фрейма, ошибки работы MAC/PHY).
У PCI сетевых карт обычно используется одна линия для всех прерываний и нужное прерывание вычисляется при помощи флагов.
У SoC сетевого устройства обычно несколько линий прерываний.
Реализация обработчика прерывания RX и потокового обработчика событий.
Ниже рассмотрена реализация без маскирования прерывания. Сбрасываем все прерывания и регистрируем событие:
/* Обработчик прерывания */const struct sigevent *device_isr(void *arg, int iid){device_dev_t *device;struct _iopkt_inter *ient;uint16_t csr0;device = arg;ient = &device->inter;csr0 = device->reg[E_CSR]; /* Считываем статус регистр прерываний */if (ient->on_list == 0 &&!((device->csr0 = csr0) & INTR)) {/* Прерывание не принадлежит нашему сетевому устройству. */return NULL;}device->iid = iid;/* Сбрасываем прерывания. Сообщаем сетевому устройству, что мы их обработали */csr0 &= (BABL|CERR|MISS|MERR|RINT|TINT);device->reg[E_CSR] = csr0 | IDON;device->csr0 = csr0;return interrupt_queue(device->iopkt, ient); /* Добавляем событие в потоковый обработчик событий */}/* Потоковый обработчик событий */intdevice_process_interrupt(void *arg, struct nw_work_thread *wtp){device_dev_t *device_dev = (device_dev_t*)arg;struct ifnet *ifp = &device_dev->ec.ec_if;device_rx_desc_t *rdesc;int cur_rx_rptr;struct mbuf *m, *rm;off64_t phys;nic_stats_t *gstats = &device_dev->stats;nic_ethernet_stats_t *estats = &gstats->un.estats;int16_t status = 0;int16_t pkt_len = 0;const uint32_t csr0 = readl(device_dev->hw_mem + E_CSR);/* Различные if(csr0 & ERR) для обработки ошибок *//* ... *//* RX прерывание */if(csr0 & Q0_RX_INT){ /* Q0 RECEIVER INTERRUPT. */cur_rx_rptr = device_dev->cur_rx_rptr;rdesc = &device_dev->rdesc[cur_rx_rptr];while((status = (int16_t)ENDIAN_LE16(rdesc->status)) >= 0){/* Проверка дескриптора на ошибки */if(status & RD_ERR){ /* error */estats->internal_rx_errors++;goto next_pkt;}pkt_len = (ENDIAN_LE16(rdesc->msg_length) & 0xFFF) - ETHER_CRC_LEN; /* Вычитаем длину CRC из пакета. Не для всех устройств необходимо */m = m_getcl_wtp(M_DONTWAIT, MT_DATA, M_PKTHDR, wtp); /* Получаем mbuf для кольца RX, чтобы заместить который сейчас с данными. */if(m == NULL){return -1;}rm = device_dev->rx_mbuf[cur_rx_rptr];device_dev->rx_mbuf[cur_rx_rptr] = m;phys = pool_phys(m->m_data, m->m_ext.ext_page); /* Получаем физический адрес mbuf */#ifdef USE_LIBCACHECACHE_INVAL(&device_dev->cachectl, m->m_data, phys, m->m_ext.ext_size); /* Инвалидируем кэш для нового mbuf */#endif // USE_LIBCACHE/* Подготавливаем новый дескриптор для кольца RX */rdesc->base = ENDIAN_LE32((uint32_t)phys);rm->m_pkthdr.rcvif = ifp;rm->m_len = pkt_len;rm->m_pkthdr.len = pkt_len;#if NBPFILTER > 0 /* Включаем отправку сырых пакетов всем слушающим сокетам. Например для работы tcpdump */if (ifp->if_bpf)bpf_mtap(ifp->if_bpf, rm);#endif/* Обновляем статистику */gstats->rxed_ok++;gstats->octets_rxed_ok += pkt_len;/* Отправляем полученный фрейм в стек */ifp->if_ipackets++;(*ifp->if_input)(ifp, rm);next_pkt:/* Передаём обновлённый дескриптор с новым mbuf сетевой карте. */rdesc->buf_length = ENDIAN_LE16(-ETHER_MAX_LEN);rdesc->status = RD_OWN;cur_rx_rptr = (cur_rx_rptr + 1) % RX_RING_SIZE(device_dev);rdesc = &device_dev->rdesc[cur_rx_rptr];}device_dev->cur_rx_rptr = cur_rx_rptr;}return 1;}/* callback включения прерывания. Вызывается после обработки события.*/int device_intr_enable_rx(void *arg){device_dev_t *device_dev = (device_dev_t*)arg;const uint32_t q0_csr = readl(device_dev->hw_mem + E_Q0CSR);writel(device_dev->hw_mem + E_Q0CSR, q0_csr | Q_RINT | Q_MISS | Q_RINT_EN | Q_MISS_EN); /* Включаем прерывания */return 1;}
Прототип определён в struct ifnet :: if_stop().
Данная callback-функция вызывается при ifconfig down сетевого интерфейса.
Останавливает все операции приёма и передачи, очищает используемые буферы, отключает прерывания. Не освобождает структуры драйвера и аппаратные ресурсы.
Реализация аппаратно-зависимая:
void device_stop(struct ifnet *ifp, int disable){device_dev_t *device_dev = ifp->if_softc;struct _iopkt_self *iopkt = device_dev->iopkt;struct nw_work_thread *wtp = WTP;/* Остановка MAC уровня контроллера *//* ... *//* Отключение прерываний *//* ... *//* Очистка колец дескрипторов RX/TX *//* ... */ifp->if_flags &= ~(IFF_RUNNING | IFF_OACTIVE); /* Установка флагов отключённого устройства. */}
Прототип определён в struct cfattach :: ca_detach().
Данная callback-функция вызывается при ifconfig destroy сетевого интерфейса.
Сбрасывает сетевое устройство, освобождает все ресурсы. Ожидается, что драйвер в скором времени будет выгружен из сетевого стека, но стек продолжит работу.
Реализация шаблонная:
int device_detach(struct device *dev, int flags){device_dev_t *device_dev = (device_dev_t*)dev;struct ifnet *ifp = &device_dev->ec.ec_if;device_dev->sc_dev.dv_dll_hdl = NULL;device_stop(&device_dev->ec.ec_if, 0); /* Вызываем device_stop() *//* Код освобождения ресурсов *//* ... */#if 1ether_ifdetach(ifp); /* Отсоединяем ethernet стек */#elseieee80211_ifdetach( &device_dev->sc_ic ); /* Отсоединяем wifi стек */#endifif_detach(ifp); /* Отсоединяем интерфейс */shutdownhook_disestablish( device_dev->sd_hook ); /* Не забывает отсоединить shutdown_hook, если мы его подключили */return 0;}
Предыдущий раздел: Библиотека разработки сетевых драйверов