如何在Linux Kernel內送一個封包



如何在Linux Kernel內送一個封包?

I. 前言

當我們想送一個封包到網路上的時候, 只需要呼叫一個發送的函式即可完成. 然而封包的傳送並不是依照priority決定先後順序, 對於做real time network system的人而言並不是那麼好用. 因此我們要先去了解Linux Kernel內部如何去發送封包, 再去做進一步的改良.

II. 概述(Introduction)

開機時, Linux系統會作初始化的動作, 會針對不同的網路介面卡(network interface card, NIC), 執行對應的驅動程式並設定各個動作的處理函式(handler function), 例如送出或接收封包的處理函式.

另外Linux 系統初始化時會對不同Network Layer protocol (像是IP), 設定創造對應的socket時, 所要呼叫的創建函式(create function). 並且設定與network layer protocal和transport layer protocal相關的變數, 以在應用程式創造socket時使用.

在應用程式作系統呼叫(system call)以送出封包時, 會先經過transport layer的處理函式(以tcp為例則是tcp_sendmsg() )來設定好transport layer相關的標頭(header), 再送交給network layer的處理函式, 此時會作routing和加上對應的標頭, 最後傳送到NIC上的緩衝區, 再發送到網路上.

關於Linux核心與NIC的互動大致上如下圖:

[pic]

以下是從應用程式開始傳送封包到真的傳到NIC並送到網路上, 這段過程的詳細解釋.

III. 本文

一、

首先send 函式 family會先各自做處理, 接著在kernel內實際上呼叫的就是 sock_sendmsg(sock, &msg, len). sock_sendmsg()將一個區域變數iocb初始化後會接著呼叫 __sock_sendmsg(&iocb, sock, msg, size), 而__sock_sendmsg()會對iocb略作更動後, 呼叫 sock->ops->sendmsg(). 事實上這裡的sock->ops->sendmsg()以tcp socket為例, 就是tcp_sendmsg().

[pic]

sock->ops->sendmsg()是在create socket的時候設定. 在__sock_create(family, type, protocol, res, 0)中可以找到net_families[family]->create(sock, protocol), net_families的形態的就是struct net_proto_family的陣列, 這個型態的變數中以inet_family_ops為例, 其中.create = inet_create,因此net_families[family]->create(sock, protocol)即為inet_create.

[pic]

inet_create會依現在需要的protocol來將新的sock中各種handler設好. 作法是比對一個list inetsw[sock->type]中何者protocol符合, 之後將 answer設定為找到的這個element. 而answer的型態為 struct inet_protosw *, 由這個型態中以inetsw_array[]為例子, 這裡面有放著TCP、UDP等, 以TCP為例, 其中很容易發現.ops = &inet_stream_ops, 接著找下去就會看到inet_stream_ops其中的.sendmsg = inet_sendmsg, 而inet_sendmsg大致上只是sk->sk_prot->sendmsg的包裹, 回到inetsw_array[]這裡面定義的.prot=&tcp_prot, 在tcp_prot的sendmsg欄位被設為tcp_sendmsg, 因此在用TCP的socket連線時, 封包就會被tcp_sendmsg處理.

[pic]

[pic]

二、

下一步就是向TCP邁進, 承接上一部份tcp_sendmsg()會呼叫 tcp_push_one(sk, mss_now), tcp_push_one在作了一些測試並且設定time stamp之後呼叫tcp_transmit_skb(sk, skb, 1, sk->sk_allocation), tcp_transmit_skb()大致上將tcp header設定好後,會呼叫icsk->icsk_af_ops->queue_xmit(skb, 0); 在tcp_transmit_skb的開頭中找到const struct inet_connection_sock *icsk = inet_csk(sk), 類似之前找尋inet_create等函式的方式, 很快的發現 .queue_xmit = ip_queue_xmit.

[pic]

在ip_queue_xmit中, 會回傳NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev, dst_output), NF_HOOK是一個巨集(macro), 定義為#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) (okfn)(skb) (註[1]), 所以說他就是做了dst_output(skb), 所以NF_HOOK整串就等同於skb->dst->output(skb), 他所做的事情就是去找路由的路徑(routing path), 以下是skb->dst->output(skb)被設定的過程.

[pic]

在做NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev, dst_output)之前, ip_queue_xmit內會呼叫函式ip_route_output_flow(), 這之後會作一連串的函式呼叫(如下圖), 一直到__mkroute_output(), 最後終於在這個函式看到了rth->u.dst.output=ip_output的設定, 因此我們可以知道在tcp_transmit_skb()中的NF_HOOK()實際上呼叫了ip_output() .

[pic]

三、

ip_output會進行一連串的函式呼叫(如下圖), 並且最後會由ip_finish_output2()回傳 hh->hh_output(skb).

[pic]

實際上hh->hh_output(skb) 呼叫了dev_queue_xmit(註[2]), 他會呼叫 dev_hard_start_xmit(skb, dev), 在dev_hard_start_xmit()中會判斷需不需要作GSO(註[3]), 若不需要或只有一個segment就直接呼叫dev->hard_start_xmit(skb) 將segment送交給NIC, 若被切割成多個segment則依序嘗試送出. 到這裡TCP/IP的工作就算告一段落.

[pic]

為了尋找dev->hard_start_xmit在哪設定, 我們需要去找NIC的驅動程式, 這邊以ne2為範例.

在ne2.c這個檔案內, __init init_module()會把驅動程式在建立Linux Kernel的時候做成一個模組(Module),通常命名為ne2.ko; 這個函式內會呼叫 alloc_ei_netdev(), 接下來會有一連串的函式呼叫(如下圖), 最後會呼叫ethdev_setup(struct net_device *dev), 其中dev->hard_start_xmit=&ei_start_xmit. 所以在模組載入到Linux的時候,以ne2的驅動程式為例, 會將dev->hard_start_xmit設定為 ei_start_xmit.

[pic]

四、

接下來是真正的把封包丟入NIC的緩衝區內, 在ei_start_xmit中會呼叫ei_block_output(), 實際上即是呼叫ne_block_output()

又ei_block_output為什麼是ne_block_output呢? 因為ei_block_output其實是一個巨集, 定義為:

#define ei_block_output (ei_local->block_output)

而ei_local 在ei_start_xmit中的定義為:

struct ei_device *ei_local = (struct ei_device *)netdev_priv(dev)

至於ei_local->block_output是在哪設定, 是在探測(probe)NIC的時候執行了ne2_probe1(), 其中有

ei_status.block_output = &ne_block_output, 在8390.h中定義了

#define ei_status (*(struct ei_device *)netdev_priv(dev)), 可知ei_status即是ei_start_xmit()中的ei_local, 所以ei_local->block_output 被設為ne_block_output, 也就是在ei_start_xmit()中的ei_block_output即是ne_block_output.

[pic]

接著把封包丟到NIC的緩衝去後, 會執行以下的函式NS8390_trigger_send(dev, send_length, output_page), 接著會啟動一個中斷處理程式(interrupt handler): enable_irq(dev->irq), 現在就要等NIC發出中斷給作業系統.

[pic]

五、

當網路卡發出中斷後, Linux kernel會啟動中斷處理程式, 也就是ei_interrupt, 若此函式判斷是在正確地送完一個封包後被呼叫的, 就呼呼叫ei_tx_intr(dev). 而在與ne2相容的NIC上通常都有兩個緩衝區, 因此在送出封包後會去, ei_tx_intr會判斷是不是還有封包還留在NIC上, 如果還有封包. 會調用之前使用的函式NS8390_trigger_send去發送封包, 無論剛剛的判斷是否成立, 都會呼叫netif_wake_queue(dev), 把NIC使用權力還給Kernel. 接著就可以繼續送下一個封包.

以下是最後一份流程圖:

[pic]

[pic]

六、下表為將第三四五小節簡化成的示意圖:

[pic]

-----------------------

[1] 這裡是考慮沒有NETFILTER 的情形.

[2] 這裡參考了 這個網頁的敘述.

[3] GSO : Generic Segmentation Offload, 即是由Linux kernel替網卡作segmentation的動作. 參考此網頁

-----------------------

Net_families[family]->create(sock, protocol)

__Sock_create

Sock_create

Sys_socket

static inline int __mkroute_output()

return sock->ops->sendmsg ()

__sock_sendmsg

Sock_sendmsg

Send_families

Struct inet_protosw *answer=NULL;

foreach (answer, inetsw[sock->type])

if (protocol == answer->protocol)

break;

sock->ops = answer->ops;

Struct net_proto_family inet_family_ops ={

.family = PF_INET,

.create = inet_create,

.owner = THIS_MODULE,

};

Inet_create()

err = icsk->icsk_af_ops->queue_xmit(skb, 0)

tcp_transmit_skb()

tcp_push_one()

struct proto tcp_prot = {…

.sendmsg = tcp_sendmsg,… }

Inet_stream_ops

return sk->sk_prot->sendmsg()

Inet_sendmsg()

struct proto_ops inet_stream_ops = {



.sendmsg = inet_sendmsg, … }

static struct inet_protosw={…

.prot = &tcp_prot,

.ops = &inet_stream_ops, …}

tcp_sendmsg()

rth->u.dst.output=ip_output;

static inline int dst_output(struct sk_buff *skb)

{return skb->dst->output(skb);}

return NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev, dst_output);

*** #define NF_HOOK(pf, hook, skb, indev, outdev, okfn) (okfn)(skb) ***

Ip_queue_xmit()

struct inet_connection_sock_af_ops ipv4_specific={

.queue_xmit = ip_queue_xmit,….}

static inline int ip_mkroute_output_def()

static inline int ip_mkroute_output()

static int ip_route_output_slow()

int __ip_route_output_key()

int ip_route_output_flow()

if (ip_route_output_flow(&rt, &fl, sk, 0)){ ... }

Ip_queue_xmit()

if (hh) { …

return hh->hh_output(skb)

}

static inline int ip_finish_output2()

static inline int ip_finish_output()

rc = dev->hard_start_xmit(nskb, dev);

int ip_output()

int dev_hard_start_xmit()

int dev_queue_xmit()

dev->hard_start_xmit = &ei_start_xmit;

static void ethdev_setup()

struct net_device *__alloc_ei_netdev()

static inline struct net_device *alloc_ei_netdev()

int __init init_module()

等待中斷

ei_block_output(dev, send_length, data, output_page);



NS8390_trigger_send(dev, send_length, output_page);

enable_irq(dev->irq);

static int __init ne2_probe1(){

ei_status.block_output = &ne_block_output;

}

* #define ei_status (*(struct ei_device *)netdev_priv(dev)) *

struct ei_device *ei_local = (struct ei_device *) netdev_priv(dev);

ei_block_output(dev, send_length, data, output_page);

* #define ei_block_output (ei_local->block_output)*

static int ei_start_xmit()

static int ei_start_xmit()

if (interrupts & ENISR_TX)ei_tx_intr(dev);

irqreturn_t ei_interrupt()

中斷發生

netif_wake_queue(dev);

Case2:

else if (ei_local->tx2 < 0){

if (ei_local->tx1 > 0)

NS8390_trigger_send(dev, ei_local->tx1,

ei_local->tx_start_page);

Case1:

if (ei_local->tx1 < 0){

if (ei_local->tx2 > 0)

NS8390_trigger_send(dev, ei_local->tx2,

ei_local->tx_start_page + 6);}

static void ei_tx_intr(struct net_device *dev)

................
................

In order to avoid copyright disputes, this page is only a partial summary.

Google Online Preview   Download