深入理解TCP/IP協議的實現之socket(基於linux1.2.13)
- 2020 年 3 月 12 日
- 筆記
socket大家都知道是用於網絡通信的,也知道他是ip和端口的組合。但是很多同學可能不是很清楚socket的原理和實現。下面我們深入理解一下socket到底是什麼。 我們回憶一下socket編程的步驟,不管是客戶端還是服務端,第一個調的函數都是socket。我們就從這個函數的實現開始,看看一個socket到底是什麼。
// 新建一個socket結構體,並且創建一個下層的sock結構體,互相關聯 static int sock_socket(int family, int type, int protocol) { int i, fd; struct socket *sock; struct proto_ops *ops; // 找到對應的協議族,比如unix域、ipv4 for (i = 0; i < NPROTO; ++i) { // 從props數組中找到family協議對應的操作函數集,props由系統初始化時sock_register進行操作 if (pops[i] == NULL) continue; if (pops[i]->family == family) break; } if (i == NPROTO) { return -EINVAL; } // 函數集 ops = pops[i]; // 檢查一下類型 if ((type != SOCK_STREAM && type != SOCK_DGRAM && type != SOCK_SEQPACKET && type != SOCK_RAW && type != SOCK_PACKET) || protocol < 0) return(-EINVAL); // 分配一個新的socket結構體 if (!(sock = sock_alloc())) { ... } // 設置類型和操作函數集 sock->type = type; sock->ops = ops; if ((i = sock->ops->create(sock, protocol)) < 0) { sock_release(sock); return(i); } // 返回一個新的文件描述符 if ((fd = get_fd(SOCK_INODE(sock))) < 0) { sock_release(sock); return(-EINVAL); } return(fd); }
我們從上到下,逐步分析這個過程。 1. 根據傳的協議類型,找到對應的函數集,因為不同的協議族他的底層操作是不一樣的。 2. 分配一個socket結構體。定義如下。我們大概了解一下字段就行。
struct socket { short type; /* SOCK_STREAM, ... */ socket_state state; long flags; struct proto_ops *ops; // 這個字段要記一下 void *data; struct socket *conn; struct socket *iconn; struct socket *next; struct wait_queue **wait; struct inode *inode; struct fasync_struct *fasync_list; }; struct socket *sock_alloc(void) { struct inode * inode; struct socket * sock; // 獲取一個可用的inode節點 inode = get_empty_inode(); if (!inode) return NULL; // 初始化某些字段 inode->i_mode = S_IFSOCK; inode->i_sock = 1;// socket文件 inode->i_uid = current->uid; inode->i_gid = current->gid; // 指向inode的socket結構體,初始化inode結構體的socket結構體 sock = &inode->u.socket_i; sock->state = SS_UNCONNECTED; sock->flags = 0; sock->ops = NULL; sock->data = NULL; sock->conn = NULL; sock->iconn = NULL; sock->next = NULL; sock->wait = &inode->i_wait; // 互相引用 sock->inode = inode; /* "backlink": we could use pointer arithmetic instead */ sock->fasync_list = NULL; // socket數加一 sockets_in_use++; // 返回新的socket結構體,他掛載在inode中 return sock; }
sock_alloc首先分配了一個inode,inode節點裏有一個socket結構體,然後初始化socket結構體的一些字段,並把他的地址返回。
3. 這時候我們拿到一個socket結構體。接着調create函數(省略了部分代碼)。
// 創建一個sock結構體,和socket結構體互相關聯 static int inet_create(struct socket *sock, int protocol) { struct sock *sk; struct proto *prot; int err; // 分配一個sock結構體 sk = (struct sock *) kmalloc(sizeof(*sk), GFP_KERNEL); switch(sock->type) { case SOCK_STREAM: protocol = IPPROTO_TCP; // 函數集 prot = &tcp_prot; break; case SOCK_DGRAM: protocol = IPPROTO_UDP; prot=&udp_prot; break; } // sock結構體的socket字段指向上層的socket結構體 sk->socket = sock; // 省略一堆對sock結構體的初始化代碼 }
我們發現創建一個socket的時候,申請了一個socket結構體,同時也申請了一個sock結構體。為什麼需要兩個結構體,並且這兩個結構體關聯在一起呢?這要說到網絡協議的複雜性,而這個設計就是linux對這個複雜性的解決方案。我們回頭看看socket函數的參數。
socket(int family, int type, int protocol)
family是協議簇,比如unix域、ipv4、ipv6,type是在第一個參數的基礎上的子分類。比如ipv4下有tcp、udp、raw、packet。protocol對tcp、udp沒用,對raw、packet的話是標記上層協議類型。這好比一棵樹一樣,從根節點開始,有很多分支。socket結構體是整個網絡協議實現的最上層結構,是第一層抽象。根據協議簇的不同,有不同的實現函數,在同一協議簇下,也有不同的子分類,比如ipv4下有tcp、udp等。不同子類具體的邏輯也不一樣。即數據結構和算法都不一樣。所以socket結構體有一個data字段,他是自定義的,對於ipv4的實現,他是指向一個sock結構體,對於unix域的實現,unix_proto_data結構體。這就解決了不同協議簇(family)不同實現的問題。那對於同一協議簇下的不同子類型,又如何實現呢?比如ipv4下的tcp、udp。linux給出的方案是在sock結構體中定義一個字段,根據子類型type的值,指向不同的底層協議函數集。

在申請完sock結構體並且和socket結構體互相關聯後。這時候我們拿到了一個inode,一個socket結構體,一個sock結構體。然後根據inode拿到一個file和fd文件描述符。最後返回fd給用戶。內容結構圖如下。

這就是socket函數返回後的內存結構體。後續我們調用bind,listen等等函數,傳入fd,系統就會根據上面圖的指向,一直找到tcp函數集,執行對應的函數,對於udp也是一樣,不同是tcp函數集變成udp函數集。這一篇我們先介紹socket函數的邏輯,下面繼續分析socket編程系列函數的實現。