Linux 網絡協議及其協議棧


Linux 網絡協議及其協議棧

一、 協議和協議棧的區別?

1.通信協議就是通信雙方事前約定好的通信規則可以簡單的理解為各個計算機之間進行相互會話所使用的共同語言。

2.協議棧是協議的具體的實現形式,我們通俗的來講就是用代碼實現的庫函數,從而方便開發人員的調用。

協議棧是網絡中各層協議的總和,其形象的反映了 一個網絡中文件傳輸過程;由上層協議到底層協議,再由底層協議到上層協議。

 

二、 linux網絡棧結構

linux網絡棧的層次結構非常清晰,並沒有按照OSI七層模型來實現,而是壓縮並擴展了一些層。如下圖中的所示:

從上而下,依次為應用層,系統調用接口層,協議無關接口層,網絡棧層,設備無關接口層,設備驅動層。因為linux的網絡棧中的socket是繼承自BSD的,socket插口為應用層使用網絡服務提供了簡單的方法,它屏蔽了具體的下層協議族的差異。下面重點說一下中間的4層。

系統調用接口層。系統調用接口層提供了socket接口的系統調用。

協議無關接口層。為什么會有這一層呢?協議無關指的又是什么無關?首先呢,我們得知道,網絡世界里是有很有種協議族的,比如我們最常用的tcp/ipv4協議族,但是除此之外還有很多協議族存在,比如netlink,unix等,因此,為了使用上的方便,抽象了一個協議無關接口層,只需要在創建socket時,傳入對應的參數,就能創建出對應的協議族socket類型。具體的可以看一下socket函數的參數:socket(int domain, int type, int protocol);第一個參數就定義了使用的協議族,ipv4的?ipv6的?unix的?等等。第二個參數就是指定socket類型,是流式套接字還是用戶數據報?還是原始套接字?一般來說,前兩個參數選定了,就能確定一個socket的類型和使用的傳輸層協議了,如流式套接字對應使用tcp/ip中的tcp協議用戶數據包對應使用tcp/ip中的udp協議。

網絡棧層。這一層就是具體的各類協議的實現了。包括傳輸層和網絡層。對於我們最經常使用的tcp/ip來說,傳輸層主要包括tcp和udp協議。網絡層就是ip協議。這一部分也是這個系列重點需要解釋的,后面仔細說。

設備無關接口層。這一層夾在網絡棧和驅動層之間,至於為什么會有這么一層存在?可以想象一下,網絡設備種類多樣,當收到數據包時,怎么傳遞給網絡棧?如果沒有設備無關接口層的抽象,勢必會導致兩層之間的調用花樣百出,因此,有必要抽象出設備無關層,如驅動向上的傳遞接口,通用設備表示等。從這個設計來看,給我們很多啟示,聯想上面的協議無關接口層,可以看出,在一對多這種情況下,設計一個通用層會有很多好處。

.linux網絡棧文件分布層次

在第一節對實現網絡設計結構做了說明之后,這一節說明一下實現文件的分布。

1. 文件的實現主要在/linux/net目錄下。

2. /linux/net目錄下的幾乎每個文件夾就是一個協議族的集合。如ipv6,ipv4,802,ATM。

3. 對於ipv4的網絡層,傳輸層的實現分布在/linux/net/ipv4中。

4. 協議無關接口層和設備無關接口層分布在/linux/net/core文件夾中。

socket操作系統調用

我們在上一節中說到過,在應用層和協議無關層之間,是一個系統調用接口層。系統調用接口如下:

  • socketcall socket系統調用
  • socket 建立socket
  • bind 綁定socket到端口
  • connect 連接遠程主機
  • accept 響應socket連接請求
  • send 通過socket發送信息
  • sendto 發送UDP信息
  • sendmsg 參見send
  • recv 通過socket接收信息
  • recvfrom 接收UDP信息
  • recvmsg 參見recv
  • listen 監聽socket端口
  • select 對多路同步I/O進行輪詢
  • shutdown 關閉socket上的連
  • getsockname 取得本地socket名字
  • getpeername 獲取通信對方的socket名字
  • getsockopt 取端口設置
  • setsockopt 設置端口參數
  • sendfile 在文件或端口間傳輸數據
  • socketpair 創建一對已聯接的無名socket

當在應用中調用socket()函數時,就會觸發系統調用,跟socket相關的操作函數都會被映射到sys_socketcall的系統調用中(32位系統),在文件unistd_32.h中有其系統調用號表。對於64位系統,系統調用號會不一樣,在文件unistd_64.h中,跟socket相關的系統調用會直接對應,不用都映射到sys_socketcall(實際上,64位系統中會通過定義__NO_STUBS宏屏蔽這個調用號)。具體的系統調用過程可以參考如下鏈接:http://lib.csdn.net/article/embeddeddevelopment/55382

因為網絡棧是在內核態,所以從socket api到操作socket插口存在一個系統調用層。在本文中,我們看到了當使用socket api時,是怎么調用到系統調用的,在下一篇中,將介紹對應的系統的調用是怎么操作socket插口的。

 

. socket api看協議無關層

前面的系列已經說了系統調用接口層,在應用層使用socket api,填充對應的參數,就能創建出想要使用的socket類型。這個過程就是協議無關層完成的。簡單的說過程就是:根據參數,匹配注冊的協議族,使用對應的協議。接下來重點分析幾個socket api的BSD無關層實現,來更深一步理解這個問題。

1.1 socket()

在應用層調用socket()后,就會觸發sys_socket系統調用。那我們就去看看在這個系統調用函數中都做了什么事:net/socket.c文件

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
    int retval;
    struct socket *sock;
    int flags;
 
    /* Check the SOCK_* constants for consistency.  */
    BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
    BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
    BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
    BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);
 
    flags = type & ~SOCK_TYPE_MASK;
    if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
        return -EINVAL;
    type &= SOCK_TYPE_MASK;
 
    if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
        flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
 
    retval = sock_create(family, type, protocol, &sock);
    if (retval < 0)
        goto out;
 
    retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
    if (retval < 0)
        goto out_release;
 
out:
    /* It may be already another descriptor 8) Not kernel problem. */
    return retval;
 
out_release:
    sock_release(sock);
    return retval
}

省略前面的檢查,實際上只有2個函數:sock_create()sock_map_fd()。接着看sock_create(),sock_create()->__sock_create().

  1. security_socket_create()這是創建安全的socket,沒定義的話,就是空操作。
  2. sock_alloc()創建了一個struct socket,注意這個結構,后面還有其他的很相似的結構。struct socket就是BSD層維護的socket對象。因為UNIX秉承一切皆文件的思想,所以,這個socket對象也是和inode結構一起創建並綁定到一起的。創建之后,填充socket的類型:sock->type = type;
  3. 根據協議族和類型,創建對應類型的socket(struct sock)。如下代碼有刪減。
    pf = rcu_dereference(net_families[family]);
 
    err = pf->create(net, sock, protocol);
    if (err < 0)
        goto out_module_put;

pf->create()到這里才是真正的無關層體現的地方,即根據協議族和類型,創建socket。但這里的socket實際指的是struct sock結構,這個結構貫穿了整個協議棧。那它和struct socket有啥區別和聯系呢?這里先賣個關子,到數據結構那節再具體說。現在接着說pf->create,pf是從net_families[family]中取出來的,pf的結構如下:

struct net_proto_family {
    int     family;
    int     (*create)(struct net *net, struct socket *sock, int protocol);
    struct module   *owner;
};

這實際上是代表了一個協議族,每個協議族有自己的create函數,如inet就是inet_create()。那肯定要問了,那這些協議族是什么時候注冊的呢?答案是在協議族初始化的時候。還拿inet族舉例,就是在inet_init()時,再往上追就是fs_initcall(inet_init);
我們看到這么一句話:

(void)sock_register(&inet_family_ops);

你瞧,inet_family_opsstruct net_proto_family)把自己注冊進"活着"的協議族列表,仿佛在說:“hi,BSD,我INET協議族來報道啦!”

int sock_register(const struct net_proto_family *ops)
{
    int err;
 
    if (ops->family >= NPROTO) {
        printk(KERN_CRIT "protocol %d >= NPROTO(%d)\n", ops->family,
               NPROTO);
        return -ENOBUFS;
    }
 
    spin_lock(&net_family_lock);
    if (net_families[ops->family])
        err = -EEXIST;
    else {
        net_families[ops->family] = ops;
        err = 0;
    }
    spin_unlock(&net_family_lock);
 
    printk(KERN_INFO "NET: Registered protocol family %d\n", ops->family);
    return err;
}

看那net_families[ops->family]不就是前面提到的么?這就是“活着”的協議族的集合。

4.inet_create(),創建socket。

list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
 
        err = 0;
        /* Check the non-wild match. */
        if (protocol == answer->protocol) {
            if (protocol != IPPROTO_IP)
                break;
        } else {
            /* Check for the two wild cases. */
            if (IPPROTO_IP == protocol) {
                protocol = answer->protocol;
                break;
            }
            if (IPPROTO_IP == answer->protocol)
                break;
        }
        err = -EPROTONOSUPPORT;
    }

先根據sock->type找到要創建的socket的模板,模板是什么,怎么來的呢?對於inet協議族而言,模板就是都在inetsw這個鏈表里存着啦,自然會想到是不是也是在inet_init()時添加進去的呢?沒錯,我們回過頭去看:

for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
        INIT_LIST_HEAD(r);
 
for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
        inet_register_protosw(q);

到這里不得不說一下socket類型這個東西,站在BSD的角度,抽象了幾種socket類型出來:

enum sock_type {
    SOCK_STREAM = 1,
    SOCK_DGRAM = 2,
    SOCK_RAW = 3,
    SOCK_RDM = 4,
    SOCK_SEQPACKET = 5,
    SOCK_DCCP  = 6,
    SOCK_PACKET = 10,
};

對於具體的協議族怎么實現這些對應的類型,BSD是不關心的,比如在inet協議族,tcp對應的是SOCK_STREAM類型,udp對應的是SOCK_DGRAM等。對於其他的協議族而言,要對應起自己的協議。再看上面的兩句,初始化了每種inet每種socket類型的鏈表。然后inet_register_protosw()inetsw_array數組中對應的類型注冊到inetsw對應類型的鏈表中。這個inetsw_array數組其實就是每個協議族連接BSD和自己的傳輸層的橋梁。就拿其中一個元素來看:

{
        .type =       SOCK_STREAM,
        .protocol =   IPPROTO_TCP,
        .prot =       &tcp_prot,
        .ops =        &inet_stream_ops,
        .capability = -1,
        .no_check =   0,
        .flags =      INET_PROTOSW_PERMANENT |
                  INET_PROTOSW_ICSK,
},

就是說,如果BSD要創建SOCK_STREAM類型的套接字,那么就對應於inet族的tcp協議,自然對於BSD層的操作,也有抽象出來的操作集,就是struct proto_ops表示。那么為什么還會有一個struct ops的結構呢?很明顯,這個是用於inet層的操作集,上文提到,每一種類型的socket都有自己的協議鏈表,也就是對於inet的流式套接字而言,也可能有多種協議(當然,現在只注冊了tcp一種)。

接着剛才的創建往下看,找到對應的類型后,就填充其中的成員:

sock->ops = answer->ops;
answer_prot = answer->prot;
answer_no_check = answer->no_check;
answer_flags = answer->flags;

然后就分配了struct sock對象和struct inet_sock對象,
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);。初始化sock對象和inet_sock對象。最后調用sock對象的init。主要是針對tcp和raw的,需要初始化,udp就沒有init操作。

if (sk->sk_prot->init) {
        err = sk->sk_prot->init(sk);
        if (err)
            sk_common_release(sk);
    }

5.映射socket到文件描述符。在完成socket創建后,就把這個socket和一個文件描述符fd關聯起來,以后就可以直接用讀寫文件的方式訪問這個socket了。sock_map_fd()。這一步操作過后,socket創建就算完成了。

1.2 sendto()

1.3 recvfrom()

這兩個接口的介紹,就放到下一篇中進行,選着兩個主要就是想說一下數據包從內核態到用戶態的過程。本篇已經很長了,原諒我先挖個坑吧 :)

六、相關數據結構

  1. struct socketstruct sockstruct inet_sock
    這兩個數據結構實際上是分工協作的,不單純是分層的關系。struct socket代表的是BSD層上對所有socket的抽象,而struct sock代表的是每個協議族對socket的抽象。這是兩個很重要的層次關系,我們說協議無關層下面就是傳輸層,但並不是說sock是對傳輸層的socket的抽象,所以,無論是對BSD層的抽象還是對每個協議族的抽象,都應該屬於協議無關層,只是對協議無關層兩個方面的抽象。這時候就有疑問了:既然都屬於協議無關層的抽象,為什么不合並這兩個結構呢?因為struct socket是使用inode關聯的,我們也看到struct sock結構龐大,如果把struct sock合並到struct socket中,豈不是建每個文件inode都需要占用很大空間咯?所以,把套接字中的跟文件有關的部分拿出來就是struct socket,而把剩余的跟socket數據有關的都放到struct sock中。而struct inet_sock則是為了更加方便使用inet族,在struct sock的基礎上,又封裝而成的。
  2. struct proto_opsstruct proto
    自然,BSD層既然抽象了socket,就會有抽象的操作集,struct proto_ops就是抽象的操作集,它操作的就是協議族;struct proto就是對協議族層面的操作集的抽象,它操作的就是實際的傳輸層協議。這個可以跟上面的struct socket和struct sock進行類比。

這些結構共同實現了網絡棧協議無關層。所以,可以看到,這一篇沒有說到具體傳輸層的東西,比如tcp,udp操作等。

. 文件分布

協議無關層的文件分布主要在:

  • net/socket.c
  • net/core/sock.c

. 小結

linux網絡棧非常龐大,包羅萬象,支持各種各樣的協議,為了能夠讓應用層簡單方便的使用socket,linux提供了抽象層,把跟協議無關的東西拿出來,讓應用層僅僅提供參數,就能在適配合適的協議族和使用的協議。大大減小了應用層的復雜度。

 

為什么 Linux 不將網絡協議棧在用戶態實現?

做為一個宏內核操作系統,將網卡驅動程序放在內核態無可厚非。但是,為什么Linux要將龐大的網絡協議棧也放在內核呢?

在內核態實現網絡協議棧,不僅代碼與網卡驅動程序緊耦合,網卡驅動程序代碼BUG可能誤踩協議棧內存使整個系統掛死;而且使用戶態應用程序攔截報文時不得不通過各種內核鈎子(例如netfilter)才能將報文trap到用戶態,對性能本身也是損耗。

當初Linux設計時,可能是因為考慮到內核態和用戶態之間切換損耗性能而將協議棧在內核實現。但是現代CPU都已經有專門的內核態和用戶態切換指令,切換速度已經很快。那么為什么Linux仍然不考慮將龐大的網絡協議棧移到用戶態呢?

目前很多通訊行業的產品,包含倆套協議棧,一個在內核態,一個在用戶態。用戶態的一般繞過內核,負責數據的處理也就是常說的數據面。而內核態保留原有功能,負責控制面的一些功能。比如ftp加載版本,telnet,ssh,ping(普通),syslog告警等等。工作在數據面采用dpdk則是常態!

 

linux網絡協議是什么?該如何去理解?

網絡協議有很多,但大多是針對windows的,那么linux網絡協議你是怎么樣理解的呢?本文和大家一起探討這個問題。

  Linux網絡協議棧基於分層的設計思想,總共分為四層,從下往上依次是:物理層,鏈路層,網絡層,應用層。

  Linux網絡協議棧其實是源於BSD的協議棧,它向上以及向下的接口以及協議棧本身的軟件分層組織的非常好。 Linux的協議棧基於分層的設計思想,總共分為四層,從下往上依次是:物理層,鏈路層,網絡層,應用層。

  物理層主要提供各種連接的物理設備,如各種網卡,串口卡等;

  鏈路層主要指的是提供對物理層進行訪問的各種接口卡的驅動程序,如網卡驅動等;

  網絡層的作用是負責將網絡數據包傳輸到正確的位置,最重要的網絡層協議當然就是IP協議了,其實網絡層還有其他的協議如ICMP,ARP,RARP等,只不過不像IP那樣被多數人所熟悉;

  傳輸層的作用主要是提供端到端,說白一點就是提供應用程序之間的通信,傳輸層最着名的協議非TCP與UDP協議末屬了;

  應用層,顧名思義,當然就是由應用程序提供的,用來對傳輸數據進行語義解釋的“人機界面”層了,比如HTTP,SMTP,FTP等等,其實應用層還不是人們最終所看到的那一層,最上面的一層應該是“解釋層”,負責將數據以各種不同的表項形式最終呈獻到人們眼前。

  Linux網絡核心架構Linux的網絡架構從上往下可以分為三層,分別是: 用戶空間的應用層內核空間的網絡協議棧層物理硬件層。其中最重要最核心的當然是內核空間的協議棧層了。

  Linux網絡協議棧結構Linux的整個網絡協議棧都構建與Linux Kernel中,整個棧也是嚴格按照分層的思想來設計的,整個棧共分為五層,分別是 :

  1,系統調用接口層,實質是一個面向用戶空間應用程序的接口調用庫,向用戶空間應用程序提供使用網絡服務的接口。

  2,協議無關的接口層,就是SOCKET層,這一層的目的是屏蔽底層的不同協議(更准確的來說主要是TCP與UDP,當然還包括RAW IP, SCTP等),以便與系統調用層之間的接口可以簡單,統一。簡單的說,不管我們應用層使用什么協議,都要通過系統調用接口來建立一個SOCKET,這個SOCKET其實是一個巨大的sock結構,它和下面一層的網絡協議層聯系起來,屏蔽了不同的網絡協議的不同,只吧數據部分呈獻給應用層(通過系統調用接口來呈獻)。

  3,網絡協議實現層,毫無疑問,這是整個協議棧的核心。這一層主要實現各種網絡協議,最主要的當然是IP,ICMP,ARP,RARP,TCP,UDP等。這一層包含了很多設計的技巧與算法,相當的不錯。

  4,與具體設備無關的驅動接口層,這一層的目的主要是為了統一不同的接口卡的驅動程序與網絡協議層的接口,它將各種不同的驅動程序的功能統一抽象為幾個特殊的動作,如opencloseinit等,這一層可以屏蔽底層不同的驅動程序

  5,驅動程序層,這一層的目的就很簡單了,就是建立與硬件的接口層。 可以看到,Linux網絡協議棧是一個嚴格分層的結構,其中的每一層都執行相對獨立的功能,結構非常清晰。 其中的兩個“無關”層的設計非常棒,通過這兩個“無關”層,其協議棧可以非常輕松的進行擴展。在我們自己的軟件設計中,可以吸收這種設計方法。

   以上就是如何理解Linux網絡協議的方法,根據Linux網絡協議四層相互之間的關系了解性的記憶會比較有幫助,謝謝大家的閱讀。

 

理解 Linux 網絡棧(1):Linux 網絡協議棧簡單總結

  1. Linux 網絡路徑

 

1.1    發送端

1.1.1          應用層

(1)      Socket

  應用層的各種網絡應用程序基本上都是通過 Linux Socket 編程接口來和內核空間的網絡協議棧通信的。Linux Socket 是從 BSD Socket 發展而來的,它是 Linux 操作系統的重要組成部分之一,它是網絡應用程序的基礎。從層次上來說,它位於應用層,是操作系統為應用程序員提供的 API,通過它,應用程序可以訪問傳輸層協議。

  • socket 位於傳輸層協議之上,屏蔽了不同網絡協議之間的差異
  • socket 是網絡編程的入口,它提供了大量的系統調用,構成了網絡程序的主體

  在Linux系統中,socket 屬於文件系統的一部分,網絡通信可以被看作是對文件的讀取,使得我們對網絡的控制和對文件的控制一樣方便。

 

 

2應用層處理流程

  1. 網絡應用調用Socket API socket (int family, int type, int protocol) 創建一個 socket,該調用最終會調用 Linux system call socket() ,並最終調用 Linux Kernel 的 sock_create() 方法。該方法返回被創建好了的那個 socket 的 file descriptor。對於每一個 userspace 網絡應用創建的 socket,在內核中都有一個對應的 struct socket和 struct sock。其中,struct sock 有三個隊列(queue),分別是 rx , tx 和 err,在 sock 結構被初始化的時候,這些緩沖隊列也被初始化完成;在收據收發過程中,每個 queue 中保存要發送或者接受的每個 packet 對應的 Linux 網絡棧 sk_buffer 數據結構的實例 skb。
  2. 對於 TCP socket 來說,應用調用 connect()API ,使得客戶端和服務器端通過該 socket 建立一個虛擬連接。在此過程中,TCP 協議棧通過三次握手會建立 TCP 連接。默認地,該 API 會等到 TCP 握手完成連接建立后才返回。在建立連接的過程中的一個重要步驟是,確定雙方使用的 Maxium Segemet Size (MSS)。因為 UDP 是面向無連接的協議,因此它是不需要該步驟的。
  3. 應用調用 Linux Socket 的 send 或者 write API 來發出一個 message 給接收端
  4. sock_sendmsg 被調用,它使用 socket descriptor 獲取 sock struct,創建 message header 和 socket control message
  5. _sock_sendmsg 被調用,根據 socket 的協議類型,調用相應協議的發送函數。
    1. 對於 TCP ,調用 tcp_sendmsg 函數。

對於 UDP 來說,userspace 應用可以調用 send()/sendto()/sendmsg() 三個 system call 中的任意一個來發送 UDP message,它們最終都會調用內核中的 udp_sendmsg() 函數。

 

1.1.2 傳輸層

  傳輸層的最終目的是向它的用戶提供高效的、可靠的和成本有效的數據傳輸服務,主要功能包括 (1)構造 TCP segment (2)計算 checksum (3)發送回復(ACK)包 (4)滑動窗口(sliding windown)等保證可靠性的操作。TCP 協議棧的大致處理過程如下圖所示:

 

TCP 棧簡要過程:

tcp_sendmsg 函數會首先檢查已經建立的 TCP connection 的狀態,然后獲取該連接的 MSS,開始 segement 發送流程。

構造 TCP 段的 playload:它在內核空間中創建該 packet 的 sk_buffer 數據結構的實例 skb,從 userspace buffer 中拷貝 packet 的數據到 skb 的 buffer。

構造 TCP header。

計算 TCP 校驗和(checksum)和 順序號 (sequence number)。

TCP 校驗和是一個端到端的校驗和,由發送端計算,然后由接收端驗證。其目的是為了發現TCP首部和數據在發送端到接收端之間發生的任何改動。如果接收方檢測到校驗和有差錯,則TCP段會被直接丟棄。TCP校驗和覆蓋 TCP 首部和 TCP 數據。

TCP的校驗和是必需的

發到 IP 層處理:調用 IP handler 句柄 ip_queue_xmit,將 skb 傳入 IP 處理流程。

UDP 棧簡要過程:

UDP 將 message 封裝成 UDP 數據報

調用 ip_append_data() 方法將 packet 送到 IP 層進行處理。

1.1.3 IP 網絡層 - 添加header 和 checksum,路由處理,IP fragmentation

    網絡層的任務就是選擇合適的網間路由和交換結點, 確保數據及時傳送。網絡層將數據鏈路層提供的幀組成數據包,包中封裝有網絡層包頭,其中含有邏輯地址信息- -源站點和目的站點地址的網絡地址。其主要任務包括 (1)路由處理,即選擇下一跳 (2)添加 IP header(3)計算 IP header checksum,用於檢測 IP 報文頭部在傳播過程中是否出錯 (4)可能的話,進行 IP 分片(5)處理完畢,獲取下一跳的 MAC 地址,設置鏈路層報文頭,然后轉入鏈路層處理。

 

  首先,ip_queue_xmit(skb)會檢查skb->dst路由信息。如果沒有,比如套接字的第一個包,就使用ip_route_output()選擇一個路由。接着,填充IP包的各個字段,比如版本、包頭長度、TOS等。

中間的一些分片等,可參閱相關文檔。基本思想是,當報文的長度大於mtu,gso的長度不為0就會調用 ip_fragment 進行分片,否則就會調用ip_finish_output2把數據發送出去。ip_fragment 函數中,會檢查 IP_DF 標志位,如果待分片IP數據包禁止分片,則調用 icmp_send()向發送方發送一個原因為需要分片而設置了不分片標志的目的不可達ICMP報文,並丟棄報文,即設置IP狀態為分片失敗,釋放skb,返回消息過長錯誤碼。

接下來就用 ip_finish_ouput2 設置鏈路層報文頭了。如果,鏈路層報頭緩存有(即hh不為空),那就拷貝到skb里。如果沒,那么就調用neigh_resolve_output,使用 ARP 獲取。

1.1.4 數據鏈路層

   功能上,在物理層提供比特流服務的基礎上,建立相鄰結點之間的數據鏈路,通過差錯控制提供數據幀(Frame)在信道上無差錯的傳輸,並進行各電路上的動作系列。數據鏈路層在不可靠的物理介質上提供可靠的傳輸。該層的作用包括:物理地址尋址、數據的成幀、流量控制、數據的檢錯、重發等。在這一層,數據的單位稱為幀(frame)。數據鏈路層協議的代表包括:SDLC、HDLC、PPP、STP、幀中繼等。

   實現上,Linux 提供了一個 Network device 的抽象層,其實現在 linux/net/core/dev.c。具體的物理網絡設備在設備驅動中(driver.c)需要實現其中的虛函數。Network Device 抽象層調用具體網絡設備的函數。

 

 

1.1.5 物理層 - 物理層封裝和發送

 

  物理層在收到發送請求之后,通過 DMA 將該主存中的數據拷貝至內部RAM(buffer)之中。在數據拷貝中,同時加入符合以太網協議的相關header,IFG、前導符和CRC。對於以太網網絡,物理層發送采用CSMA/CD,即在發送過程中偵聽鏈路沖突。

一旦網卡完成報文發送,將產生中斷通知CPU,然后驅動層中的中斷處理程序就可以刪除保存的 skb 了。

1.1.6 簡單總結

 

1.2 接收端

1.2.1 物理層和數據鏈路層

 

簡要過程:

一個 package 到達機器的物理網絡適配器,當它接收到數據幀時,就會觸發一個中斷,並將通過 DMA 傳送到位於 linux kernel 內存中的 rx_ring。

網卡發出中斷,通知 CPU 有個 package 需要它處理。中斷處理程序主要進行以下一些操作,包括分配 skb_buff 數據結構,並將接收到的數據幀從網絡適配器I/O端口拷貝到skb_buff 緩沖區中;從數據幀中提取出一些信息,並設置 skb_buff 相應的參數,這些參數將被上層的網絡協議使用,例如skb->protocol;

終端處理程序經過簡單處理后,發出一個軟中斷(NET_RX_SOFTIRQ),通知內核接收到新的數據幀。

內核 2.5 中引入一組新的 API 來處理接收的數據幀,即 NAPI。所以,驅動有兩種方式通知內核:(1) 通過以前的函數netif_rx;(2)通過NAPI機制。該中斷處理程序調用 Network device的 netif_rx_schedule 函數,進入軟中斷處理流程,再調用 net_rx_action 函數。

該函數關閉中斷,獲取每個 Network device 的 rx_ring 中的所有 package,最終 pacakage 從 rx_ring 中被刪除,進入 netif _receive_skb 處理流程。

netif_receive_skb 是鏈路層接收數據報的最后一站。它根據注冊在全局數組 ptype_all 和 ptype_base 里的網絡層數據報類型,把數據報遞交給不同的網絡層協議的接收函數(INET域中主要是ip_rcv和arp_rcv)。該函數主要就是調用第三層協議的接收函數處理該skb包,進入第三層網絡層處理。

1.2.2 網絡層

 

  IP 層的入口函數在 ip_rcv 函數。該函數首先會做包括 package checksum 在內的各種檢查,如果需要的話會做 IP defragment(將多個分片合並),然后 packet 調用已經注冊的 Pre-routing netfilter hook ,完成后最終到達 ip_rcv_finish 函數。

ip_rcv_finish 函數會調用 ip_router_input 函數,進入路由處理環節。它首先會調用 ip_route_input 來更新路由,然后查找 route,決定該 package 將會被發到本機還是會被轉發還是丟棄:

如果是發到本機的話,調用 ip_local_deliver 函數,可能會做 de-fragment(合並多個 IP packet),然后調用 ip_local_deliver 函數。該函數根據 package 的下一個處理層的 protocal number,調用下一層接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。對於 TCP 來說,函數 tcp_v4_rcv 函數會被調用,從而處理流程進入 TCP 棧。

如果需要轉發 (forward),則進入轉發流程。該流程需要處理 TTL,再調用 dst_input 函數。該函數會 (1)處理 Netfilter Hook (2)執行 IP fragmentation (3)調用 dev_queue_xmit,進入鏈路層處理流程。

 

1.2.3 傳輸層 (TCP/UDP)

傳輸層 TCP 處理入口在 tcp_v4_rcv 函數(位於 linux/net/ipv4/tcp ipv4.c 文件中),它會做 TCP header 檢查等處理。

調用 _tcp_v4_lookup,查找該 package 的 open socket。如果找不到,該 package 會被丟棄。接下來檢查 socket 和 connection 的狀態。

如果socket 和 connection 一切正常,調用 tcp_prequeue 使 package 從內核進入 user space,放進 socket 的 receive queue。然后 socket 會被喚醒,調用 system call,並最終調用 tcp_recvmsg 函數去從 socket recieve queue 中獲取 segment。

1.2.4 接收端 - 應用層

每當用戶應用調用  read 或者 recvfrom 時,該調用會被映射為/net/socket.c 中的 sys_recv 系統調用,並被轉化為 sys_recvfrom 調用,然后調用 sock_recgmsg 函數。

對於 INET 類型的 socket,/net/ipv4/af inet.c 中的 inet_recvmsg 方法會被調用,它會調用相關協議的數據接收方法。

對 TCP 來說,調用 tcp_recvmsg。該函數從 socket buffer 中拷貝數據到 user buffer。

對 UDP 來說,從 user space 中可以調用三個 system call recv()/recvfrom()/recvmsg() 中的任意一個來接收 UDP package,這些系統調用最終都會調用內核中的 udp_recvmsg 方法。

1.2.5 報文接收過程簡單總結

 

2. Linux sk_buff struct 數據結構和隊列(Queue)

2.1 sk_buff

(本章節摘選自 http://amsekharkernel.blogspot.com/2014/08/what-is-skb-in-linux-kernel-what-are.html)

2.1.1 sk_buff 是什么

  當網絡包被內核處理時,底層協議的數據被傳送更高層,當數據傳送時過程反過來。由不同協議產生的數據(包括頭和負載)不斷往下層傳遞直到它們最終被發送。因為這些操作的速度對於網絡層的表現至關重要,內核使用一個特定的結構叫 sk_buff, 其定義文件在 skbuffer.h。Socket buffer被用來在網絡實現層交換數據而不用拷貝來或去數據包 –這顯著獲得速度收益。

sk_buff 是 Linux 網絡的一個核心數據結構,其定義文件在 skbuffer.h。

socket kernel buffer (skb) 是 Linux 內核網絡棧(L2 到 L4)處理網絡包(packets)所使用的 buffer,它的類型是 sk_buffer。簡單來說,一個 skb 表示 Linux 網絡棧中的一個 packet;TCP 分段和 IP 分組生產的多個 skb 被一個 skb list 形式來保存。

struct sock 有三個 skb 隊列(sk_buffer queue),分別是 rx , tx 和 err。

 

它的主要結構成員:

struct sk_buff {

    /* These two members must be first. */ # packet 可以存在於 list 或者 queue 中,這兩個成員用於鏈表處理

    struct sk_buff        *next;

    struct sk_buff        *prev;

    struct sk_buff_head    *list; #該 packet 所在的 list

 ...

    struct sock        *sk; #跟該 skb 相關聯的 socket

    struct timeval        stamp; # packet 發送或者接收的時間,主要用於 packet sniffers

    struct net_device    *dev;  #這三個成員跟蹤該 packet 相關的 devices,比如接收它的設備等

    struct net_device    *input_dev;

    struct net_device    *real_dev;

 

    union {                  #指向各協議層 header 結構

        struct tcphdr    *th;

        struct udphdr    *uh;

        struct icmphdr    *icmph;

        struct igmphdr    *igmph;

        struct iphdr    *ipiph;

        struct ipv6hdr    *ipv6h;

        unsigned char    *raw;

    } h;

 

    union {

        struct iphdr    *iph;

        struct ipv6hdr    *ipv6h;

        struct arphdr    *arph;

        unsigned char    *raw;

    } nh;

 

    union {

        unsigned char    *raw;

    } mac;

 

  struct  dst_entry    *dst; #指向該 packet 的路由目的結構,告訴我們它會被如何路由到目的地

  char    cb[40];   # SKB control block,用於各協議層保存私有信息,比如 TCP 的順序號和幀的重發狀態

  unsigned int  len, #packet 的長度

  data_len,

  mac_len,  # MAC header 長度

  csum;  # packet 的 checksum,用於計算保存在 protocol header 中的校驗和。發送時,當 checksum offloading 時,不設置;接收時,可以由device計算

 

  unsigned char local_df, #用於 IPV4 在已經做了分片的情況下的再分片,比如 IPSEC 情況下。

  cloned:1, #在 skb 被 cloned 時設置,此時,skb 各成員是自己的,但是數據是shared的

  nohdr:1,  #用於支持 TSO

  pkt_type, #packet 類型

  ip_summed; # 網卡能支持的校驗和計算的類型,NONE 表示不支持,HW 表示支持,

 

  __u32 priority; #用於 QoS

  unsigned short protocol, # 接收 packet 的協議

  security;

2.1.2 skb 的主要操作

(1)分配 skb = alloc_skb(len, GFP_KERNEL)

 

(2)添加 payload (skb_put(skb, user_data_len))

 

(3)使用 skb->push 添加 protocol header,或者 skb->pull 刪除 header

 

2.2 Linux 網絡棧使用的驅動隊列driver queue        

(本章節摘選自 Queueing in the Linux Network Stack by Dan Siemon

2.2.1 隊列

 

 

在 IP 棧和 NIC 驅動之間,存在一個 driver queue (驅動隊列)。典型地,它被實現為 FIFO ring buffer,簡單地可以認為它是固定大小的。這個隊列不包含 packet data,相反,它只是保存 socket kernel buffer (skb)的指針,而 skb 的使用如上節所述是貫穿內核網絡棧處理過程的始終的。

  該隊列的輸入時 IP 棧處理完畢的 packets。這些packets 要么是本機的應用產生的,要么是進入本機又要被路由出去的。被 IP 棧加入隊列的 packets 會被網絡設備驅動(hardware driver)取出並且通過一個數據通道(data bus)發到 NIC 硬件設備並傳輸出去。

  在不使用 TSO/GSO 的情況下,IP 棧發到該隊列的 packets 的長度必須小於 MTU。

2.2.2 skb 大小 - 默認最大大小為 NIC MTU

    絕大多數的網卡都有一個固定的最大傳輸單元(maximum transmission unit, MTU)屬性,它是該網絡設備能夠傳輸的最大幀(frame)的大小。對以太網來說,默認值為 1500 bytes,但是有些以太網絡可以支持巨幀(jumbo frame),最大能到 9000 bytes。在 IP 網絡棧內,MTU 表示能發給 NIC 的最大 packet 的大小。比如,如果一個應用向一個 TCP socket 寫入了 2000 bytes 數據,那么 IP 棧需要創建兩個 IP packets 來保持每個 packet 的大小等於或者小於 1500 bytes。可見,對於大數據傳輸,相對較小的 MTU 會導致產生大量的小網絡包(small packets)並被傳入 driver queue。這成為 IP 分片 (IP fragmentation)。

    下圖表示 payload 為 1500 bytes 的 IP 包,在 MTU 為 1000 和 600 時候的分片情況:

 

備注:

以上資料是從網絡上獲取的各種資料整理而來

這一塊本身就比較復雜,而且不同的 linux 內核的版本之間也有差異,文中的內容還需要進一步加工,錯誤在所難免。

 

物理設備接收到數據后,基本步驟是:
1.
硬件發出中斷
2.
中斷處理程序在主內存總分配DMA buffer
3.
硬件向 buffer 寫入數據。在完成時發出另一個中斷。
4.
中斷處理程序再見 DMA buffer 中的數據分發出去。
那么在發出時,也應該是類似的兩個步驟:
1.
將數據從主內存通過DMA 拷貝到網卡的 buffer 里面
2.
中斷處理程序將網卡buffer里面的數據發出去

 

user space data ----> copy to DMA buffer & add header
DMA buffer copy done --> issue DMA
DMA buffer ----(DMA interrupt)---> Card data register

 轉自:https://www.cnblogs.com/sammyliu/p/5225623.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM