В статье приведён обзор общего подхода к разработке драйверов сетевой подсистемы
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;}
Предыдущий раздел: Библиотека разработки сетевых драйверов