歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux管理 >> Linux問題解決 >> Linux下將套接口綁定到網絡設備的方法

Linux下將套接口綁定到網絡設備的方法

日期:2017/3/6 10:16:34   编辑:Linux問題解決

1. 起因

事情的起因是我准備用兩個CDMA modem來拓展點對點連接的帶寬,並且希望藉此實現兩個modem之間的負載均衡。但是不幸的是,聯通公司的接入設備不支持Multilink-PPP。於是,沒有辦法,我只好自己來實現負載均衡。實現負載均衡的辦法有幾種,網絡上給出的一種辦法是采用iproute2來完成包級別的負載均衡,這是在內核一級實現的。但是我並不想把一切都交給內核去完成,我希望能夠自己控制每一個modem上的流量。那麼,我應該怎麼辦呢?

2. 解決方案

一開始,我想到的辦法是創建兩個套接口,然後將每個套接口都綁定到一個本地IP地址上,我以為這樣將會導致數據從所綁定的IP地址所在的網絡設備上發出去。但是實踐證明這種想法是錯誤的。因為每次在發送數據包之前,內核都要查找路由表來決定從哪個網絡接口上發送數據包。一旦找到一個合適的路由表項,就從該路由表項所指出的網絡接口上將數據包發送出去。這樣,就有一個問題,由於路由表是高速緩存的,因此每次發送數據包之後,發送數據包的那個接口將會有更大的機會被內核再一次選中。在最壞的情況下,將導致一個modem忙得不可開交,而另一個modem卻“無人問津”。這顯然違背了我的初衷。試驗結果表明,當一個modem上發送了幾百KB的數據之後,另外一個上仍然只發送了幾十個B。看來,此路不通!

循著上述思路,一種稍微令人錯愕的做法是每次發送數據包之前,都先調整路由表。調整路由表是很容易做到的。但是這樣做的話也實在太麻煩了一點,所以,此想法也被我拋棄了。我甚至沒有測試此法是否可行,但是從理論上來說是行得通的。而且,在網上介紹的方法中,路由級別的負載均衡好像就是這樣來實現的,不過,僅僅是好像而已,我並沒有深究。

幾番嘗試未果之後,我把《UNIX網絡編程》搬了出來,把UDP有關的部分細細地篩了一遍。只發現了一個可能有作用的地方:可以通過setsockopt()設置一個套接口選項:SO_DONTROUTE,但是對該選項的作用說得比較含糊。含糊歸含糊,我還是實際嘗試了一下,結果仍然是不行,原因未知。

於是,我不得不回到Linux本身,對著浩浩蕩蕩的一大堆man手冊讀起來。當我看到socket(7)的時候,忽然看到了一個令我眼前一亮的套接口選項:SO_BINDTODEVICE。從字面上看,這個選項應該就是我要的了。後來的試驗結果證明事實的確如此。

在socket(7)中對該套接口選項的說明如下:

SO_BINDTODEVICE
Bind this socket to a particular device like "eth0", as specified in the passed interface name. If the name is an empty string or the option length is zero, the socket device binding is removed. The passed option is a variable-length null terminated interface name string with the maximum size of IFNAMSIZ. If a socket is bound to an interface, only packets received from that particular interface are processed by the socket. Note that this only works for some socket types, particularly AF_INET sockets. It is not supported for packet sockets (use normal bind(8) there).
這裡,我是直接照搬過來的。不過,最後的那個bind(8)肯定是錯的,很顯然應該是bind(2)才對。不管它,這不是我現在要解決的事情。這段話的中心意思是:當套接口被綁定到指定的網絡設備接口之後,只有來自該設備的數據包才會被套接口處理。那麼,如果是套接口向外發送數據包呢?是否也只會從該網絡接口發出呢?可惡的是,這裡沒有說。不過沒關系,我們試驗一下就知道了。
一開始,我想當然地以為可以像下面這樣:

char *dev = "ppp0";
int sock1 = socket(AF_INET, SOCK_DGRAM, 0);
setsockopt(sock1, SOL_SOCKET, SO_BINDTODEVICE,
dev, sizeof(dev));

然而,實踐再一次證明我想錯了。但是我能有什麼辦法呢,socket(7)中的說明寫得就是這麼晦澀,我看不出端倪來也是情有可原的。不過,有google在手,這點小問題我又何懼之有?於是google之,很快就發現了問題的症結所在:在Linux下,對網絡設備的引用都是通過struct ifreq來完成的。在netdevice(7)中對該結構體的說明如下:

struct ifreq {
char ifr_name[IFNAMSIZ];/* Interface name */
union {
struct sockaddrifr_addr;
struct sockaddrifr_dstaddr;
struct sockaddrifr_broadaddr;
struct sockaddrifr_netmask;
struct sockaddrifr_hwaddr;
short ifr_flags;
int ifr_ifindex;
int ifr_metric;
int ifr_mtu;
struct ifmapifr_map;
char ifr_slave[IFNAMSIZ];
char ifr_newname[IFNAMSIZ];
char * ifr_data;
};
};

這裡,我只需要ifr_name這個成員域就夠了。代碼修改成了下面這樣:

struct ifreq if_ppp0;
struct ifreq if_ppp1;
strncpy(if_ppp0.ifr_name, "ppp0", IFNAMSIZ);
strncpy(if_ppp1.ifr_name, "ppp1", IFNAMSIZ);
sock1 = socket(AF_INET, SOCK_DGRAM, 0);
sock2 = socket(AF_INET, SOCK_DGRAM, 0);

if (setsockopt(sock1, SOL_SOCKET, SO_BINDTODEVICE,
(char *)&if_ppp0, sizeof(if_ppp0))
/*error handling*/
}
if (setsockopt(sock2, SOL_SOCKET, SO_BINDTODEVICE,
(char *)&if_ppp1, sizeof(if_ppp1))
/*error handling*/
}

然後,在程序的主體部分,每次在sock1上發送一個數據包,同時也就會在sock2上發送一個數據包,並且程序中沒有任何接收數據的動作。由於所有數據包的大小都是相等的。因此可以預計在兩個網絡接口上發送的數據量應該相差不大才對。測試結果有力地支持了這一猜想:在運行程序一段時間後,接口ppp0上發送的數據量為702KB,而ppp1接口上發送的數據量為8Array5KB。雖然仍然相差了將近200KB,但是無論如何,比起原來的情況已經提高了不少。至於為什麼會有這樣200KB的差距,作者也正在找原因。www.it165.net

3. 更多結論

針對SO_BINDTODEVICE套接口選項,作者在全面閱讀man手冊之後,得出的結論如下:
(1) 對於TCP套接口、UDP套接口、RAW套接口,可以通過SO_BINDTODEVICE套接口選項將套接口綁定到指定的網絡接口上。綁定之後,套接口的所有數據包收發都只經過指定的網絡接口;
(2) 對於PACKET類型的套接口,不能通過SO_BINDTODEVICE綁定到指定的網絡接口上,而要通過bind(2)來與特定的網絡接口綁定,所用的套接口地址結構為struct sockaddr_ll,此套接口地址結構是鏈路層的地址結構,獨立於具體的網絡設備。比如,該地址結構既可以用於表示PPP設備,也能用於表示ethernet設備。
(3) SO_BINDTODEVICE套接口選項只適用於Linux系統。如果要編寫運行在多操作系統平台上的程序,不能依賴SO_BINDTODEVICE來完成套接口與具體設備的綁定。
不過,作者並沒有對TCP套接口和RAW套接口進行測試。對於PACKET套接口,上述結論是可信的,因為我閱讀了dhcpd的源代碼,發現對於PACKET套接口的確是通過bind(2)綁定到指定的網絡接口上的。

Copyright © Linux教程網 All Rights Reserved