天天看點

linux加載網卡驅動到核心,linux網卡驅動的加載流程

在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函數中要進行的初始化操作要多的多,涉及到諸多的硬體寄存器配置、隊列設定等等。是以呢,這篇文章總結起來隻能算作是入門而已。。。