Linux原始套接字实现分析---转
http://blog.chinaunix.net/uid-27074062-id-3388166.html
本文從IPV4協議棧原始套接字的分類入手,詳細介紹了鏈路層和網絡層原始套接字的特點及其內核實現細節。并結合原始套接字的實際應用,說明各類型原始套接字的適應范圍,以及在實際使用時需要注意的問題。
?
?
?
?
?
一、原始套接字概述
?
協議棧的原始套接字從實現上可以分為“鏈路層原始套接字”和“網絡層原始套接字”兩大類。本節主要描述各自的特點及其適用范圍。
鏈路層原始套接字可以直接用于接收和發送鏈路層的MAC幀,在發送時需要由調用者自行構造和封裝MAC首部。而網絡層原始套接字可以直接用于接收和發送IP層的報文數據,在發送時需要自行構造IP報文頭(取決是否設置IP_HDRINCL選項)。
?
1.1??鏈路層原始套接字
?
鏈路層原始套接字調用socket()函數創建。第一個參數指定協議族類型為PF_PACKET,第二個參數type可以設置為SOCK_RAW或SOCK_DGRAM,第三個參數是協議類型(該參數只對報文接收有意義)。協議類型protocol不同取值的意義具體見表1所示:
??????
a)???????參數type設置為SOCK_RAW時,套接字接收和發送的數據都是從MAC首部開始的。在發送時需要由調用者從MAC首部開始構造和封裝報文數據。type設置為SOCK_RAW的情況應用是比較多的,因為某些項目會使用到自定義的二層報文類型。
?
b)??????參數type設置為SOCK_DGRAM時,套接字接收到的數據報文會將MAC首部去掉。同時在發送時也不需要再手動構造MAC首部,只需要從IP首部(或ARP首部,取決于封裝的報文類型)開始構造即可,而MAC首部的填充由內核實現的。若對于MAC首部不關心的場景,可以使用這種類型,這種用法用得比較少。
?????
?
?
?
表1??protocol不同取值
?
?
?
?
?
?
| protocol | 值 | 作用 |
| ETH_P_ALL | ?0x0003 | 報收本機收到的所有二層報文 |
| ETH_P_IP | 0x0800 | 報收本機收到的所有IP報文 |
| ETH_P_ARP | 0x0806 | 報收本機收到的所有ARP報文 |
| ETH_P_RARP | 0x8035 | 報收本機收到的所有RARP報文 |
| 自定義協議 | 比如0x0810 | 報收本機收到的所有類型為0x0810的二層報文 |
| 不指定 | 0 | 不能用于接收,只用于發送 |
| …… | …… | …… |
?
?
?
?
表1中protocol的取值中有兩個值是比較特殊的。當protocol為ETH_P_ALL時,表示能夠接收本機收到的所有二層報文(包括IP, ARP,?自定義二層報文等),同時這種類型套接字還能夠將外發的報文再收回來。當protocol為0時,表示該套接字不能用于接收報文,只能用于發送。具體的實現細節在2.2節中會詳細介紹。
?
?
?
?
?
?
1.2??網絡層原始套接字
?
創建面向連接的TCP和創建面向無連接的UDP套接字,在接收和發送時只能操作數據部分,而不能對IP首部或TCP和UDP首部進行操作。如果想要操作IP首部或傳輸層協議首部,就需要調用如下socket()函數創建網絡層原始套接字。第一個參數指定協議族的類型為PF_INET,第二個參數為SOCK_RAW,第三個參數protocol為協議類型(不同取值的意義見表2)。產品線有使用OSPF和RSVP等協議,需要使用這種類型的套接字。
a)???????接收報文
網絡層原始套接字接收到的報文數據是從IP首部開始的,即接收到的數據包含了IP首部, TCP/UDP/ICMP等首部,?以及數據部分。
?
?????
b)??????發送報文
網絡層原始套接字發送的報文數據,在默認情況下是從IP首部之后開始的,即需要由調用者自行構造和封裝TCP/UDP等協議首部。
?
?
這種套接字也提供了發送時從IP首部開始構造數據的功能,通過setsockopt()給套接字設置上IP_HDRINCL選項,就需要在發送時自行構造IP首部。
?
| ??? |
?
?
?
?
?
表2??protocol不同取
| protocol | 值 | 作用 |
| IPPROTO_TCP | 6 | 報收TCP類型的報文 |
| IPPROTO_UDP | 17 | 報收UDP類型的報文 |
| IPPROTO_ICMP | 1 | 報收ICMP類型的報文 |
| IPPROTO_IGMP | 2 | 報收IGMP類型的報文 |
| IPPROTO_RAW | 255 | 不能用于接收,只用于發送(需要構造IP首部) |
| OSPF | 89 | 接收協議號為89的報文 |
| …… | …… | …… |
表2中protocol取值為IPPROTO_RAW是比較特殊的,表示套接字不能用于接收,只能用于發送(且發送時需要從IP首部開始構造報文)。具體的實現細節在2.3節中會詳細介紹。
?
?
?
?
?
?
二、原始套接字實現
?
本節主要首先介紹鏈路層和網絡層原始套接字報文的收發總體流程,再分別對兩類套接字的創建、接收、發送等具體實現細節進行介紹。
?
?
?
?
?
?
2.1??原始套接字報文收發流程
?
?
圖1??原始套接字收發流程
?
?
?
?
?
?
如上圖1所示為鏈路層和網絡層原始套接字的收發總體流程。網卡驅動收到報文后在軟中斷上下文中由netif_receive_skb()處理,匹配是否有注冊的鏈路層原始套接字,若匹配上就通過skb_clone()來克隆報文,并將報文交給相應的原始套接字。對于IP報文,在協議棧的ip_local_deliver_finish()函數中會匹配是否有注冊的網絡層原始套接字,若匹配上就通過skb_clone()克隆報文并交給相應的原始套接字來處理。
注意:這里只是將報文克隆一份交給原始套接字,而該報文還是會繼續走后續的協議棧處理流程。
?
?
?
?
?
??????鏈路層原始套接字的發送,直接由套接字層調用packet_sendmsg()函數,最終再調用網卡驅動的發送函數。網絡層原始套接字的發送實現要相對復雜一些,由套接字層調用inet_sendmsg()->raw_sendmsg(),再經過路由和鄰居子系統的處理后,最終調用網卡驅動的發送函數。若注冊了ETH_P_ALL類型套接字,還需要將外發報文再收回去。
?
?
?
?
?
?
2.2??鏈路層原始套接字的實現
?
2.2.1??套接字創建
?
調用socket()函數創建套接字的流程如下,鏈路層原始套接字最終由packet_create()創建。
sys_socket()->sock_create()->__sock_create()->packet_create()
?
??? 當socket()函數的第三個參數protocol為非零的情況下,會調用dev_add_pack()將鏈路層套接字packet_sock的packet_type結構鏈到ptype_all鏈表或ptype_base鏈表中。????
????當protocol為ETH_P_ALL時,會將套接字加入到ptype_all鏈表中。如圖2所示,這里創建了兩個鏈路層原始套接字。
?
?
圖2??ptype_all鏈表
?
?
?
?
?
當protocol為其它非0值時,會將套接字加入到ptype_base鏈表中。如圖3所示,協議棧本身也需要注冊packet_type結構,圖中淺色的兩個packet_type結構分別是IP協議和ARP協議注冊的,其處理函數分別為ip_rcv()和arp_rcv()。圖中另外3個深色的packet_type結構則是鏈路層原始套接字注冊的,分別用于接收類型為ETH_P_IP、ETH_P_ARP和0x0810類型的報文。
?
?
圖3??ptype_base鏈表
?
?
?
?
?
?
?
?
?
?
?
2.2.2??報文接收
?
網卡驅動程序接收到報文后,在軟中斷上下文由netif_receive_skb()處理。首先會逐個遍歷ptype_all鏈表中的packet_type結構,若滿足條件“(!ptype->dev || ptype->dev == skb->dev)”,即套接字未綁定或者套接字綁定網口與skb所在網口匹配,就增加報文引用計數并交給packet_rcv()函數處理(若使用PACKET_MMAP收包方式則由tpacket_rcv()函數處理)。
網卡驅動->netif_receive_skb()->deliver_skb()->packet_rcv()/tpacket_rcv()
?
??? 以非PACKET_MMAP收包方式為例進行說明,packet_rcv()函數中比較重要的代碼片段如下。當報文skb到達packet_rcv()函數時,其skb->data所指的數據是不包含MAC首部的,所以對于type為非SOCK_DGRAM(即SOCK_RAW)類型,需要將skb->data指針前移,以便數據部分可以包含MAC首部。最后將skb放到套接字的接收隊列sk->sk_receive_queue中,并喚醒用戶態進程來讀取套接字中的數據。
PACKET_MMAP收包方式的實現有所不同,tpacket_rcv()函數將skb->data拷貝到與用戶態mmap映射的共享內存中,最后喚醒用戶態進程來讀取數據。由于報文的內容已存放在內核空間和用戶空間共享的緩沖區中,用戶態可以直接讀取以減少數據的拷貝,所以這種方式效率比較高。
?
??? 上面介紹了報文接收在軟中斷的處理流程。下面以非PACKET_MMAP收包方式為例,介紹用戶態讀取報文數據的流程。用戶態recvmsg()最終調用skb_recv_datagram(),如果套接字接收隊列sk->sk_receive_queue中有報文就取skb并返回。否則調用wait_for_packet()等待,直到內核軟中斷收到報文并喚醒用戶態進程。
sys_recvmsg()->sock_recvmsg()->…->packet_recvmsg()->skb_recv_datagram()
?
?
?
?
?
?
2.2.3??報文發送
?
用戶態調用sendto()或sendmsg()發送報文的內核態處理流程如下,由套接字層最終會調用到packet_sendmsg()。
sys_sendto()->sock_sendmsg()->__sock_sendmsg()->packet_sendmsg()->dev_queue_xmit()
?
??? 該函數比較重要的函數片段如下。首先進行參數檢查及skb分配,再調用驅動程序的hard_header函數(對于以太網驅動是eth_header()函數)來構造報文的MAC頭部,此時的skb->data是指向MAC首部的,且skb->len為MAC首部長度(即14)。對于創建時指定type為SOCK_RAW類型套接字,由于在發送時需要自行構造MAC頭部,所以將skb->tail指針恢復到MAC首部開始的位置,并將skb->len設置為0(即不使用內核構造的MAC首部)。接著再調用memcpy_fromiovec()從skb->tail的位置開始拷貝報文數據,最終調用網卡驅動的發送函數將報文發送出去。
注:如果創建套接字時指定type為SOCK_DGRAM,則使用內核構造的MAC首部,用戶態發送的數據中不含MAC頭部數據。
?
| ????????? ? |
?
2.2.4??其它
?
?
a)?????????套接字的綁定
鏈路層原始套接字可調用bind()函數進行綁定,讓packet_type結構dev字段指向相應的net_device結構,即將套接字綁定到相應的網口上。如2.2.2節報文接收的描述,在接收時如果套接口有綁定就需要進一步確認當前skb->dev是否與綁定網口相匹配,只有匹配的才會將報文上送到相應的套接字。
sys_bind()->packet_bind()->packet_do_bind()
b)????????套接字選項
以下是比較常用的套接字選項
PACKET_RX_RING:用于PACKET_MMAP收包方式設置接收環形隊列
PACKET_STATISTICS:用于讀取收包統計信息
?
c)???????信息查看
鏈路層原始套接字的信息可通過/proc/net/packet進行查看。如下為圖2和圖3中創建的原始套接字的信息,可以查看到創建時指定的協議類型、是否綁定網口、已使用的接收緩存大小等信息。這些信息對于分析和定位問題有幫助。?
?
?
?
?
?
2.3??網絡層原始套接字的實現
?
2.3.1??套接字創建
?
如圖4所示,在IPV4協議棧中一個傳輸層協議(如TCP,UDP,UDP-Lite等)對應一個inet_protosw結構,而inet_protosw結構中又包含了proto_ops結構和proto結構。網絡子系統初始化時將所有的inet_protosw結構hash到全局的inetsw[]數組中。proto_ops結構實現的是從與協議無關的套接口層到協議相關的傳輸層的轉接,而proto結構又將傳輸層映射到網絡層。
?
?
圖4??inetsw[]數組結構
?
?
?
?
?
?
??? 調用socket()函數創建套接字的流程如下,網絡層原始套接字最終由inet_create()創建。
sys_socket()->sock_create()->__sock_create()->inet_create()
?
??? inet_create()函數除用于創建網絡層原始套接字外,還用于創建TCP、UDP套接字。首先根據socket()函數的第二個參數(即SOCK_RAW)在inetsw[]數組中匹配到相應的inet_protosw結構。并將套接字結構的ops設置為inet_sockraw_ops,將套接字結構的sk_prot設置為raw_prot。然后對于SOCK_RAW類型套接字,還要將inet->num設置為協議類型,以便最后能調用proto結構的hash函數(即raw_v4_hash())。
?
| ????????? ? |
經過如上操作后,相應的套接字結構sock會通過raw_v4_hash()函數鏈到raw_v4_htable鏈表中,網絡層原始套接字報文接收時需要使用到raw_v4_htable。如圖5所示,共創建了3個網絡層原始套接字,協議類型分別為IPPROTO_TCP、IPPROTO_ICMP和89。
?
?
?
圖5??raw_v4_htable鏈表
?
?
?
?
?
?
?
?
?
?
?
2.3.2??報文接收
?
網卡驅動收到報文后在軟中斷上下文由netif_receive_skb()處理,對于IP報文且目的地址為本機的會由ip_rcv()最終調用ip_local_deliver_finish()函數。ip_local_deliver_finish()主要功能的代碼片段如下,先根據報文的L4層協議類型hash值在圖5中的raw_v4_htable表中查找是否有匹配的sock。如果有匹配的sock結構,就進一步調用raw_v4_input()處理網絡層原始套接字。不管是否有原始套接字要處理,該報文都會走后續的協議棧處理流程。即會繼續匹配inet_protos[]數組,根據L4層協議類型走TCP、UDP、ICMP等不同處理流程。
?
| ????????? ? |
如圖6所示的inet_protos[]數組,每項由net_protocol結構組成。表示一個協議(包括傳輸層協議和網絡層附屬協議)的接收處理函數集,一般包括一個正常接收函數和一個出錯接收函數。圖中TCP、UDP和ICMP協議的接收處理函數分別為tcp_v4_rcv()、udp_rcv()和icmp_rcv()。如果在inet_protos[]數組中未配置到相應的net_protocol結構,報文就會被丟棄掉。比如OSPF報文(協議類型為89)在inet_protos[]數組中沒有相應的項,內核會將其丟棄掉,這種報文只能提供網絡層原始套接字接收到用戶態來處理。
?
?
?
圖6??inet_protos[]數組結構
?
?
?
?
?
?
??? 網絡層原始套接字的總體接收流程如下,最終會將skb掛到相應套接字上,并喚醒用戶態進程讀取報文數據。
網卡驅動->netif_receive_skb()->ip_rcv()->ip_rcv_finish()->ip_local_deliver()->ip_local
_deliver_finish()->raw_v4_input()->raw_rcv()->raw_rcv_skb()->sock_queue_rcv_skb()
?
| |
?
?
?
?
?
???????上面介紹了報文接收在軟中斷的處理流程,下面介紹用戶態進程讀取報文是如何實現的。用戶態的recvmsg()最終會調用raw_recvmsg(),后者再調用skb_recv_datagram。如果套接字接收隊列sk->sk_receive_queue中有報文就取skb并返回。否則調用wait_for_packet()等待,直到內核軟中斷收到報文并喚醒用戶態進程。
sys_recvmsg()->sock_recvmsg()->…->sock_common_recvmsg()->raw_recvmsg()
?
?
?
?
?
?
2.3.3??報文發送
?
用戶態調用sendto()或sendmsg()發送報文的內核態處理流程如下,最終由raw_sendmsg()進行發送。
sys_sendto()->sock_sendmsg()->__sock_sendmsg()->inet_sendmsg()->raw_sendmsg()
????此函數先進行一些參數合法性檢測,然后調用ip_route_output_slow()進行選路。選路成功后主要執行如下代碼片段,根據inet->hdrincl是否設置走不同的流程。raw_send_hdrinc()函數表示用戶態發送的數據中需要包含IP首部,即由調用者在發送時自行構造IP首部。如果inet->hdrincl未置位,表示內核會構造IP首部,即調用者發送的數據中不包含IP首部。不管走哪個流程,最終都會經過ip_output()->ip_finish_output()->…->dev_queue_xmit()將報文交給網卡驅動的發送函數發送出去。
?
???注:inet->hdrincl置位表示用戶態發送的數據中要包含IP首部,inet->hdrincl在以下兩種情況下被置位。
????a).?給套接字設置IP_HDRINCL選項
??????????setsockopt (sockfd, IPPROTO_IP, IP_HDRINCL, &val, sizeof(val))
????b).?調用socket()創建套接字時,第三個參數指定為IPPROTO_RAW,見2.3.1節。
??????????socktet(PF_INET, SOCK_RAW, IPPROTO_RAW)
?
?
?
?
?
?
2.3.4??其它
?
a)???????套接字綁定
若原始套接字調用bind()綁定了一個地址,則該套接口只能收到目的IP地址與綁定地址相匹配的報文。內核的具體實現是raw_bind(),將inet->rcv_saddr設置為綁定地址。在原始套接字接收時,__raw_v4_lookup()在設置了inet->rcv_saddr字段的情況下,會判斷該字段是否與報文目的IP地址相同。
sys_bind()->inet_bind()->raw_bind()
?
b)??????信息查看
網絡層原始套接字的信息可通過/proc/net/raw進行查看。如下為圖5所創建的3個網絡層原始套接字的信息,可以查看到創建套接字時指定的協議類型、綁定的地址、發送和接收隊列已使用的緩存大小等信息。這些信息對于分析和定位問題有幫助。
?
?三、應用及注意事項
?
?
3.1? 使用鏈路層原始套接字
?
?
注意事項:
?
?
?
?
?
?
a)???????盡量避免創建過多原始套接字,且原始套接字要盡量綁定網卡。因為收到每個報文除了會將其分發給綁定在該網卡上的原始套接字外,還會分發給沒有綁定網卡的原始套接字。如果原始套接字較多,一個報文就會在軟中斷上下文中分發多次,造成處理時間過長。
?
b)??????發包和收包盡量使用同一個原始套接字。如果發包與收包使用兩個不同的原始套接字,會由于接收報文時分發多次而影響性能。而且用于發送的那個套接字的接收隊列上也會緩存報文,直至達到接收隊列大小限制,會造成內存泄露。
?
c)???????若只接收指定類型二層報文,在調用socket()時指定第三個參數的協議類型,而最好不要使用ETH_P_ALL。因為ETH_P_ALL會接收所有類型的報文,而且還會將外發報文收回來,這樣就需要做BPF過濾,比較影響性能。
?
?
3.2??使用網絡層原始套接字
?
?
?
?
?
注意事項:
?
?
?
?
?
?
a)???????由于IP報文的重組是在網絡層原始套接字接收流程之前執行的,所以該原始套接字不能接收到UDP和TCP的分組數據。
?
b)??????若原始套接字已由bind()綁定了某個本地IP地址,那么只有目的IP地址與綁定地址匹配的報文,才能遞送到這個套接口上。
?
c)???????若原始套接字已由connect()指定了某個遠地IP地址,那么只有源IP地址與這個已連接地址匹配的報文,才能遞送到這個套接口上。
?
?
?
?
?
?
3.3??網絡診斷工具使用原始套接字
?
很多網絡診斷工具也是利用原始套接字來實現的,經常會使用到的有tcpdump, ping和traceroute等。
tcpdump
?
?
?
?
?
?
該工具用于截獲網口上的報文流量。其實現原理是創建ETH_P_ALL類型的鏈路層原始套接字,讀取和解析報文數據并將信息顯示出來。
?
ping
?
?
?
?
?
?
該工具用于檢查網絡連接。其實現原理是創建網絡層原始套接字,指定協議類型為IPPROTO_ICMP。檢測方構造ICMP回射請求報文(類型為ICMP_ECHO),根據ICMP協議實現,被檢測方收到該請求報文后會響應一個ICMP回射應答報文(類型為ICMP_ECHOREPLY)。然后檢測方通過原始套接字讀取并解析應答報文,并顯示出序號、TTL等信息。
?
traceroute
?
?
?
?
?
?
該工具用于跟蹤IP報文在網絡中的路由過程。其實現原理也是創建網絡層原始套接字,指定協議類型為IPPROTO_ICMP。假設從A主機路由到D主機,需要依次經過B主機和C主機。使用traceroute來跟蹤A主機到D主機的路由途徑,具體步驟如下,在每次探測過程中會顯示各節點的IP、時間等信息。
a)???????A主機使用普通的UDP套接字向目的主機發送TTL為1(使用套接口選項IP_TTL來修改)的UDP報文;
b)??????B主機收到該UDP報文后,由于TTL為1會拒絕轉發,并且向A主機發送code為ICMP_EXC_TTL的ICMP報文;
c)???????A主機用創建的網絡層原始套接字讀取并解析ICMP報文。如果ICMP報文code是ICMP_EXC_TTL,就將UDP報文的TTL增加1并回到步驟a)繼續進行探測;如果ICMP報文的code是ICMP_PROT_UNREACH,表示UDP報文到達了目的地。
?
??????????????A主機―>B主機―>C主機―>D主機
?
?
參考資料
?
?
轉載于:https://www.cnblogs.com/davidwang456/p/3463291.html
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的Linux原始套接字实现分析---转的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 以安全模式启动firefox
- 下一篇: Linux 的启动流程--转