在linux系統中,其網卡驅動大多通過PCI總線與系統相連,同時,核心對于所有PCI總線上的裝置是通過PCI子系統來進行管理,通過PCI子系統提供各種PCI裝置驅動程式共同的所有通用功能。是以,作為linux系統中的PCI網卡裝置,其加載的流程必然分為兩個部分:作為PCI裝置向PCI子系統注冊;作為網卡裝置向網絡子系統注冊。
下面也将從兩個方面,分析一下網卡驅動在核心加載的兩個流程。
PCI裝置驅動程式的注冊:
PCI裝置驅動程式使用pci_register_driver函數完成核心的注冊,其定義在include/linux/pci.h檔案内
#define pci_register_driver(driver) \
__pci_register_driver(driver, THIS_MODULE, KBUILD_MODNAME)
其主要結構是名為driver的pci_driver結構體,
struct pci_driver {
struct list_head node;
const char *name;
const struct pci_device_id *id_table;
int (*probe) (struct pci_dev *dev, const struct pci_device_id *id);
void (*remove) (struct pci_dev *dev);
int (*suspend) (struct pci_dev *dev, pm_message_t state);
int (*suspend_late) (struct pci_dev *dev, pm_message_t state);
int (*resume_early) (struct pci_dev *dev);
int (*resume) (struct pci_dev *dev);
void (*shutdown) (struct pci_dev *dev);
struct pci_error_handlers *err_handler;
struct device_driver driver;
struct pci_dynids dynids;
};
在pri_driver結構體中有兩個比較關鍵的函數,分别是struct pci_device_id *id_table、以及probe函數。其中id_table是一個id向量表,核心通過其來把一些相關的裝置關聯到此驅動程式,PCI裝置獨一無二的識别方式是通過一些參數的組合,包含開發商以及模型等,這些參數都存儲在核心的pci_device_id資料結構中:
struct pci_device_id {
__u32 vendor, device;
__u32 subvendor, subdevice;
__u32 class, class_mask;
kernel_ulong_t driver_data;
};
對于每一個PCI裝置驅動程式,在注冊的時候都會把一個pci_device_id執行個體注冊到核心中,這個執行個體向量就包含了此驅動程式所能處理的所有裝置ID。
另外一個probe函數,主要用于當PCI子系統發現它正在搜尋驅動程式的裝置ID與前面所提到的id_table比對,就會調用此函數。對于網絡裝置而言,此函數會開啟硬體、配置設定net_device結構、初始化并注冊新裝置。
總的來說,對于所有的PCI裝置,在系統引導時,會建立一種資料庫,把每個總線都關聯一份已偵測并且使用該總線的裝置清單。對于PCI裝置來說,系統中就存在着這樣一個資料庫,其中儲存着所有使用PCI總線的裝置ID,此ID即上文提到的pci_device_id。以下圖為例,來說明當裝置驅動程式加載時會發生什麼。
此時,(a)圖就代表着所有使用PCI總線的裝置資料庫。當裝置驅動程式A被加載時,會調用pci_register_driver并提供pci_driver執行個體與PCI層注冊,同時pci_driver結構中包含一個驅動程式所能處理的裝置ID表;接着,PCI子系統使用該表去查在已經儲存的裝置資料庫中是否存在比對,于是會建立該驅動程式的裝置清單,如圖(b)所示;此外,對每個比對的裝置而言,PCI層會調用相比對的驅動程式中的pci_driver結構中所提供的probe函數,由probe函數完成必須的操作,如對于網絡裝置來說,probe函數就會建立并注冊相關聯的網絡裝置。
網絡裝置的注冊:
作為一個PCI網絡裝置,再其完成PCI總線的注冊後,剩下的工作就是通過probe函數來完成作為網絡裝置的注冊流程。對于每一個網絡裝置在核心中都通過一個net_device結構來表示(此結構涉及内容較多,就不再細說),是以網絡裝置的注冊流程從字面上也應該分為兩個部分,net_device結構的配置設定、完成配置設定到的dev結構向網絡子系統的注冊。
下面,首先說一下net_device結構的配置設定,在核心中通過alloc_netdev函數來完成配置設定
#define alloc_netdev(sizeof_priv, name, setup) \
alloc_netdev_mqs(sizeof_priv, name, setup, 1, 1)
struct net_device *alloc_netdev_mqs(int sizeof_priv, const char *name,
void (*setup)(struct net_device *),
unsigned int txqs, unsigned int rxqs)
此函數的參數主要有三個:sizeof_priv,私有資料結構的大小,因為作為任一個網絡裝置都有一個net_device結構,但又由于裝置的多樣性,很難在dev結構中包含所有必需的字段,是以每一個裝置一般都需要一個私有結構來儲存一些自己的資訊;name,裝置名稱,此名稱可能隻是一部分名稱,由核心通過某種規則來完成,比如對于以太網裝置,隻知道是eth裝置,但不知道具體的标号,此時可通過傳遞eth%d作為name參數,由核心完成具體的配置設定流程;setup,設定函數,此函數用于初始化net_device的部分字段,主要用于一些具有某種共同特性字段的初始化,比如對于以太網裝置,其MTU等字段具有相同的值,是以可通過ether_setup來完成公共字段的初始化。
對于Ethernet裝置為例,其注冊流程如上圖所示,首先通過調用alloc_netdev的包裹函數,alloc_etherdev,來完成net_device函數的配置設定以及部分資料的初始化;接下來,通過netdev_boot_setup_check函數來檢查加載核心時使用者是否提供了任何引導期間參數;最終,新的裝置net_device執行個體會利用register_netdevice插入至網絡裝置資料庫。
PS:對于系統中的所有net_device資料結構存在于三張表中,一個是全局的清單dev_base,另外兩張hash表dev_name_head、dev_index_head。dev_base執行個體的全局清單能夠友善核心浏覽裝置;dev_name_head是以裝置名為索引,方面通過dev的名稱搜尋到裝置的dev結構;dev_index_head是以裝置的ID,dev->ifindex為索引,方面通過裝置的ID來搜尋裝置的。
對于一個完整的net_device結構的注冊,register_netdevice實際上隻完成了一部分工作,而将剩餘部分的工作讓netdev_run_todo予以完成。
對于register_netdev函數的功能主要包括:
int register_netdev(struct net_device *dev)
{
int err;
rtnl_lock();
if (strchr(dev->name, '%')) {
err = dev_alloc_name(dev, dev->name);
if (err < 0)
goto out;
}
err = register_netdevice(dev);
out:
rtnl_unlock();
return err;
}
void rtnl_unlock(void)
{
netdev_run_todo();
}
下面是register_netdevice的函數過程:
int register_netdevice(struct net_device *dev)
{
int ret;
struct net *net = dev_net(dev);
BUG_ON(dev_boot_phase);
ASSERT_RTNL();
might_sleep();
BUG_ON(dev->reg_state != NETREG_UNINITIALIZED);
BUG_ON(!net);
spin_lock_init(&dev->addr_list_lock);
netdev_set_addr_lockdep_class(dev);
dev->iflink = -1;
if (dev->netdev_ops->ndo_init) {
ret = dev->netdev_ops->ndo_init(dev);
if (ret) {
if (ret > 0)
ret = -EIO;
goto out;
}
}
ret = dev_get_valid_name(dev, dev->name, 0);
if (ret)
goto err_uninit;
dev->ifindex = dev_new_index(net);
if (dev->iflink == -1)
dev->iflink = dev->ifindex;
if ((dev->features & NETIF_F_HW_CSUM) &&
(dev->features & (NETIF_F_IP_CSUM|NETIF_F_IPV6_CSUM))) {
printk(KERN_NOTICE "%s: mixed HW and IP checksum settings.\n",
dev->name);
dev->features &= ~(NETIF_F_IP_CSUM|NETIF_F_IPV6_CSUM);
}
if ((dev->features & NETIF_F_NO_CSUM) &&
(dev->features & (NETIF_F_HW_CSUM|NETIF_F_IP_CSUM|NETIF_F_IPV6_CSUM))) {
printk(KERN_NOTICE "%s: mixed no checksumming and other settings.\n",
dev->name);
dev->features &= ~(NETIF_F_IP_CSUM|NETIF_F_IPV6_CSUM|NETIF_F_HW_CSUM);
}
dev->features = netdev_fix_features(dev->features, dev->name);
if (dev->features & NETIF_F_SG)
dev->features |= NETIF_F_GSO;
dev->vlan_features |= (NETIF_F_GRO | NETIF_F_HIGHDMA);
ret = call_netdevice_notifiers(NETDEV_POST_INIT, dev);
ret = notifier_to_errno(ret);
if (ret)
goto err_uninit;
ret = netdev_register_kobject(dev);
if (ret)
goto err_uninit;
dev->reg_state = NETREG_REGISTERED;
set_bit(__LINK_STATE_PRESENT, &dev->state);
dev_init_scheduler(dev);
dev_hold(dev);
list_netdevice(dev);
ret = call_netdevice_notifiers(NETDEV_REGISTER, dev);
ret = notifier_to_errno(ret);
if (ret) {
rollback_registered(dev);
dev->reg_state = NETREG_UNREGISTERED;
}
if (!dev->rtnl_link_ops ||
dev->rtnl_link_state == RTNL_LINK_INITIALIZED)
rtmsg_ifinfo(RTM_NEWLINK, dev, ~0U);
out:
return ret;
err_uninit:
if (dev->netdev_ops->ndo_uninit)
dev->netdev_ops->ndo_uninit(dev);
goto out;
從簡單來說,網卡驅動加載大緻流程就已經結束了,不過再實際中,對于一個具體的網卡來說,比如最近接觸到的igb、ixgbe網卡驅動,在probe函數中要進行的初始化操作要多的多,涉及到諸多的硬體寄存器配置、隊列設定等等。是以呢,這篇文章總結起來隻能算作是入門而已。。。