十七、驅動程式設計與實作實務
解析實際的網路裝置驅動程式如何實作,並解說原始碼的原理。



17-1、網路裝置驅動程式的實作實務
linux 的網路裝置驅動程式不屬於 character 也不屬於 block 類型,是自成一格的特殊類型。
user process 不是直接與網路驅動程式溝通,中間一定要經過 kernel 的 TCP/IP stack。

網路程式不是使用裝置檔,而是使用「socket interface」與驅動程式溝通,對驅動程式送出 IOCTL 時,也是透過 socket interface 執行的, ifconfig 與 ethtool 等指令的操作也是。

TCP/IP stack 與網路驅動程式之間的介面稱為「網路裝置」,驅動程式開發者必須知道網路裝置的用法。
網路裝置驅動程式的原始碼位於 drivers/net 目錄下,接著會以 8139too 驅動程式為例(drivers/net/8139too.c) ,解說網路機置驅動程式的設計與實作。

17-1.1、網路裝置的規格書
要取得網路裝置控制對象的規格書,要先知道晶片的廠商與種類,在 8139too.c 可以看到:
8139too.c: A RealTek RTL-8139 Fast Ethernet driver for Linux.
得知這個驅動程式可控制 RealTek 公司生產的「RTL8139」晶片。
在 www.realtek.com.tw 以 「RTL8139」為關鍵字搜尋,即可取得 datasheet。

17-1.2、網路裝置
TCP/IP stack 與驅動程式之間的介面是 Linux kernel 定義的 「網路裝置」。
網路裝置是 net_device 結構,定義在 include/linux/netdevie.h 檔案內。


17-1.3、驅動程式的種類
網路驅動程式大多是 kernel module,但也可以靜態連結到 kernel 內部,嵌入式 linux 有時會在開機時透過 NFS 掛載 root filesystem,這時需要在 runlevel 1 就連上網路,因此只能把驅動程式靜態連結到 kernel 內部。

以 kernel module 的形式載入驅動程式的話,要先寫到 /etc/modprobe.conf 檔案:
alias eth0 e1000
alias eth1 tulip

以 alias 指定 module 名稱。

modprobe.conf 會被 rc script 載入、交給 modprobe 指定載入驅動程式。
而驅動程式檔需要事先複製到 /lib/modules/`uname -r`/kernel/net 目錄內。



17-2、辨識 PCI 裝置
網路裝置是 PCI 裝置的話,需要先將網路裝置驅動程式登記為 kernel 的 PCI 裝置驅動程式,透過 pci_register_driver() 將 PCI ID table、probe 程序、remove 程序登記至 kernel:
int pci_register_driver(struct pci_driver *driver);
void pci_unregister_driver(struct pci_driver *);


OS 開機時或裝置 hotplug 時,會在偵測到新的 PCI 裝置時搜尋 id_table(rtl8139_pci_tbl),表格內有符合的 PCI 裝置時,就會呼叫 probe 程序(rtl8139_init_one)。

驅動程式卸載或是移除裝置時,則會依序呼叫清理(rtl8139_cleanup_module)與移除(rtl8139_remove_one)處理程序。

17-2.1、登錄 probe 程序
probe 程序會收到 pci_dev 結構與 pci_device_id 結構,這是 kernel 準備好的資料空間。
probe 程序被呼叫前會先取得 semaphore 進行鎖定,但將來為了縮短 OS 開機時間,還是有可能同時呼叫多個 probe 程序,所以在設計驅動程式時先寫成可重進入的形式會比較好。

probe 成功時會傳回 0,否則傳回非 0 值,如果裝置故障,則驅動程式不僅要回報錯誤,還要呼叫 pci_disable_device() 禁止進一步使用此裝置:
void pci_disable_device(struct pci_dev *dev);

probe 程序在呼叫 rtl8139_init_board() 將 PCI 裝置初始化後,接著從裝置裡讀出 MAC address,以讓 kernel 進行 ARP 處理。
並將此 MAC address 登錄到 dev->perm_addr[] ,主要是 ethtool 等指令會用到。

就像 character 類型的驅動程式登記各種處理函式,網路驅動程式也要透過 net_device 結構登記處理程序。

17-2.2、登記 remove 處理程序
呼叫 remove 處理程序的時機為:
  • 卸載驅動程式時(rmmod)
  • 透過 hotplug 移除裝置時

卸除驅動程式時,必須讓裝置完全停止運作,如果時機抓錯,很容易導致 kernel panic 停止工作。
以 rmmod 來卸除網路驅動程式時,如果網路裝置仍然 up ,就會導致錯誤,所以在卸除前,一定要先將裝置 down 下來:
# ifconfig eth0 down
把網路裝置 down 下來時,會呼叫驅動程式的 stop 程序。

所以在呼叫 remove 程序時,網路裝置會在停止狀態,remove 程序該做的是切離網路裝置並關閉 PCI 裝置。

8139too驅動程式因為有用到 work queue,所以在網路裝置 down 之後到卸除驅動程式之前,可能還會有 work queue 運作,因此要呼叫 flush_scheduled_work() 等待 work queue 執行完畢。



17-3、IOCTL
網路驅動程式並不具有裝置檔,所以 linux 是透過「socket」來呼叫 IOCTL ,也因為是經由 TCP/IP stack,所以 IOCTL 指令名稱無法讓驅動程式開發者自行定義。

網路驅動程式內可以自行定義的 IOCTL 指令定義在 linux/sockios.h 檔案內,最多為16個。

網路驅動程式 IOCTL 方法定義為 do_ioctl:
int (*do_ioctl) (struct net_device *dev, struct ifreq *ifr, int cmd);

ioctl 範例:
static int sample_ioctl(struct net_device *dev, struct ifreq *ifr, int cmd) {
int rc = 0; /* success return */
switch (cmd) {
case SIOCDEVPRIVATE:
break;
case SIOCDEVPRIVATE+1:
break;
default:
rc = -EOPNOTSUPP;
}
return (rc);
}


user process 想送出 IOCTL 時,必須打開 RAW socket 透過 raw socket 送出 IOCTL,並以 ifreq 結構的 name 指定要控制的網路介面。

從 user process 送出 IOCTL 的範例:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

int main(void) {
int sock;
struct ifreq f;
int ret;

sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sock == -1) {
perror("Can't create socket\n");
exit(1);
}

strcpy(f.ifr_name, "eth0");
ret = ioctl(sock, SIOCDEVPRIVATE, &f);
if (ret == -1)
perror("ioctl error\n");

close(sock);
return 0;
}




17-4、Media Independent Interface
網路驅動程式實作 IOCTL 方法時,相較於上一節自行實作的方式,透過「MII(Media Independent Interface)」提供介面的例子也不少。

MII 是 IEEE 802.ea 定義,介於 MAC(Media Access Control) 與 PHY(Physical) 兩層之間的介面,透過 MII 即可讀寫網路裝置的 PHY 暫存器,讀取 PHY 暫存器即可得知自動協調狀態以及物理連線資訊。

如果驅動程式有支援 MII 的話,即可用 ethtool 指令取得、設定網路裝置的資訊:
# ethtool eth0

在 8139too 驅動程式的實作 IOCTL 方法程式碼中,只呼叫了 kernel 的 generic_mii_ioctl() 而已:
int generic_mii_ioctl(struct mii_if_info *mii_if, struct mii_ioctl_data *mii_data, int cmd, unsigned int *duplex_chg_out);

實際讀寫 MII 暫存器的是驅動程式,因此 MII 介面要在驅動程式初始化時設定好,而此介面是透過 MDC/MDIO 腳位控制,這是類似 I2C 的兩線式簡單介面。
MDC 是 Management Data Clock。
MDIO 是 Management Data Input/Output。

MII 資訊定義為 mii_if_info 結構,其中設有讀寫 PHY 暫存器用的函式:
int (*mdio_read) (struct net_device *dev, int phy_id, int location);
void (*mdio_write) (struct net_device *dev, int phy_id, int location, int val);
這兩個函式由驅動程式負責,因為 MII 的控制方式隨裝置而異。
phy_id引數是「PHYID」或「PHYAD」所指的「Physical Address」,這個值是裝置固定的值。
location 引數是「PHYREG」所指的「Physical Register」。



17-5、open/stop 處理程序
17-5.1、start 處理程序
用 ifconfig 將網路介面 up 時,會呼叫驅動程式的 open 處理程序,這時驅動程式該做的事情包括:
  • 登記中斷程序
  • 配置 DMA 暫存區
  • 將裝置初始化
  • 指示啟動封包傳送佇列

中斷程序可透過 request_irq() 登錄,IRQ 編號 kernel 會事先設定好,直接使用這個值即可(dev->irq)。
裝置如果支援 DMA ,就必須配置 DMA 暫存區,可透過 pci_alloc_consistent() 完成。
裝置做好準備後,須告知上層可以開始傳送封包,這時呼叫的是 netif_start_queue():
void netif_start_queue(struct net_device *dev);

17-5.2、stop 處理程序
用 ifconfig 將網路介面 down 時,會呼叫驅動程式的 stop 處理程序,這個在 character 類型的驅動程式是稱為 release 程序。
這時驅動程式該做的事情包括:
  • 指示停止封包傳送佇列
  • 停止裝置
  • 卸除中斷處理程序
  • 釋放 DMA 暫存區
處理順序與 open 處理程序相反。

停止封包傳送佇列的工作可透過 netif_stop_queue() 完成:
void netif_stop_queue(struct net_device *dev);
卸除中斷處理程序可由 free_irq() 完成,但此時中斷處理程序可能還在執行,所以要先以 synchronize_irq() 等待。
釋放 DMA 暫存區可透過 pci_free_consistent() 完成。



17-6、傳送與接收處理程序
17-6.1、傳送處理程序
網路介面 up 之後,應用程式即可透過 socket 送出封包,這時會呼叫驅動程式的傳送程序(dev->hard_start_xmit)。
其中 xmit 是 transmit 的縮寫。

在 hard_start_xmit() 中,封包會以 socket buffer (struct sk_buff *skb) 的形式傳入,封包大小可透過「skb->len」取得,這是包含 Ethernet frame header 的大小,此時已經解讀 ARP、填好接收端的 MAC address 了。

socket buffer 寫入 DMA 暫存區的動作由 skb_copy_and_csum_dev() 完成,並透過 dev_kfree_sdk() 釋放。

寫入暫存器啟動 DMA 後,將 dev->trans_start 設為目前時間(jiffies),這是用來檢測傳送時限的值。
如果經過一段時間後仍未完成傳送,就是傳送逾時,此時會呼叫 rtl8139_tx_timeout() ,透過 work queue 處理逾時情形。

封包傳送成功後,則結束中斷,呼叫中斷處理程序 rtl8139_interrupt() ,並轉交 rtl8139_tx_interrupt() 處理傳送結束的情形,此時如果有暫停傳送佇列的話,會呼叫 netif_wake_queue() 將之啟動。

17-6.2、接收處理程序
網路裝置從外部收到封包後會引發中斷,呼叫中斷處理程序 rtl8139_interrupt()。
實際的接收工作是以 polling 的方式進行,以減輕 kernel 的負擔,所以只需要呼叫 __netif_rx_schedule() 。

實際的接收工作由 rtl8139_poll() 來執行,並檢查中斷原因,如果是要接收封包的話,就呼叫 rtl8139_rx()。



17-7、結語
網路裝置需由網路裝置驅動程式來控制,linux 的網路裝置驅動程式既不屬於 characher 類型,也不屬於 block 類型,則讀現有的驅動程式原始碼,即可理解其處理機制。
arrow
arrow
    全站熱搜

    silverfoxkkk 發表在 痞客邦 留言(0) 人氣()