TCP
TCP通過校驗和、序列號、確認應答、重發控制、連接管理和窗口控制實現可靠傳輸。
TCP通過確認應答ACK來實現有保障的數據傳輸,但是由於各種原因,目標主機可能無法收到ACK信號,導致源主機不停重發數據。為此,引入序列號與確認信號相結合,實現有效的重發控制。
作為面向連接的協議,TCP在數據通信前,通過TCP首部發送一個SYN包作為建立連接的請求等待確認應答。一個連接的建立與斷開至少要來回發送7個包才能完成。其中建立連接三次握手,需要發送3個包。切斷連接四次揮手,需要發送4個包。
LINUX & TCP
下圖展示了LINUX中TCP客戶機和服務器交互時調用的基本套接字函數。
(圖源:UNIX網絡編程-第三版)
一個進程要進行網絡讀寫,第一個執行的就是socket()
,它定義在socket.h
文件中:
#include <sys/socket.h>
int socket(int family, int type, int protocol);
其中family
指定協議類型簇(IPv4、IPv6、Unix域協議、路由套接字、密鑰套接字),type
指定套接字類型,protocol
指定協議的類型常量(TCP、UDP、SCTP)。socket()
函數返回套接字描述符。
跟蹤調用棧
(圖源:https://www.jianshu.com/p/5d82a685b5b6)
啟動qemuOS,打開GDB調試,連接到遠程。在__sys_socket()
函數處打上斷點,追蹤函數調用棧:
在gdb中根據情況交替使用next
和step
命令,追蹤程序運行。函數調用了__sock_create()
實際執行socket創建任務。對__socket_create()
進行追蹤。
在__sock_create()
函數中
sock_alloc()
在__socket_create()
中,又調用了sock_alloc()
函數將socket分配給一個結構體變量sock
。
這里,sock
的類型為struct socket*
,在linux源碼目錄下使用正則表達式find -name "*.h" | xargs grep "strcut socket {" -rn
找到該結構體定義在./linux-5.0.1/include/linux/net.h
中,如下:
struct socket {
socket_state state;
short type;
unsigned long flags;
struct socket_wq *wq;
struct file *file;
struct sock *sk;
const struct proto_ops *ops;
};
state
是socket的狀態,type
是類型,flags
是標志位,eq
是等待隊列,file
是指針列表。sock
是一個重要的結構體,同理可以找到結構體sock的定義,根據注釋描述,它表示一個網絡層的socket。
new_inode_pseudo()
接着調用了new_inode_pseudo()
函數,在相應的文件系統中創建一個inode。其代碼位於./linux-5.0.1/fs/inode.c
中:
struct inode *new_inode_pseudo(struct super_block *sb)
{
struct inode *inode = alloc_inode(sb);
if (inode) {
spin_lock(&inode->i_lock);
inode->i_state = 0;
spin_unlock(&inode->i_lock);
INIT_LIST_HEAD(&inode->i_sb_list);
}
return inode;
}
inode為socket文件系統中的節點。inode和socket對象是連結在一起的,可以根據inode取得socket對象。分配inode后,應用程序可以通過文件描述符對socket進行讀寫訪問。
協議簇初始化socket
繼續往下,有一條語句:
pf = rcu_dereference(net_families[family]);
用於查找內核初始化時注冊的協議簇。當傳入的family
字段存在於協議簇中時,又繼續執行一條語句:
err = pf->create(net, sock, protocol, kern);
這一步調用的是inet_create()
函數,它位於./net/ipv4/Af_inet.c
中。
inet_create()
這個函數主要完成四個工作:
1 將socket的state設置為SS_UNCONNECTED
sock->state = SS_UNCONNECTED;
2 根據type字段找到對應套接字類型
lookup_protocol:
err = -ESOCKTNOSUPPORT;
rcu_read_lock();
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;
}
if (unlikely(err)) {
if (try_loading_module < 2) {
rcu_read_unlock();
/*
* Be more specific, e.g. net-pf-2-proto-132-type-1
* (net-pf-PF_INET-proto-IPPROTO_SCTP-type-SOCK_STREAM)
*/
if (++try_loading_module == 1)
request_module("net-pf-%d-proto-%d-type-%d",
PF_INET, protocol, sock->type);
/*
* Fall back to generic, e.g. net-pf-2-proto-132
* (net-pf-PF_INET-proto-IPPROTO_SCTP)
*/
else
request_module("net-pf-%d-proto-%d",
PF_INET, protocol);
goto lookup_protocol;
} else
goto out_rcu_unlock;
}
err = -EPERM;
if (sock->type == SOCK_RAW && !kern &&
!ns_capable(net->user_ns, CAP_NET_RAW))
goto out_rcu_unlock;
sock->ops = answer->ops;
answer_prot = answer->prot;
answer_flags = answer->flags;
rcu_read_unlock();
3 初始化socket的sk
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
if (!sk)
goto out;
err = 0;
if (INET_PROTOSW_REUSE & answer_flags)
sk->sk_reuse = SK_CAN_REUSE;
4 建立socket與sock之間的關系
inet = inet_sk(sk);
inet->is_icsk = (INET_PROTOSW_ICSK & answer_flags) != 0;
inet->nodefrag = 0;
if (SOCK_RAW == sock->type) {
inet->inet_num = protocol;
if (IPPROTO_RAW == protocol)
inet->hdrincl = 1;
}
if (net->ipv4.sysctl_ip_no_pmtu_disc)
inet->pmtudisc = IP_PMTUDISC_DONT;
else
inet->pmtudisc = IP_PMTUDISC_WANT;
inet->inet_id = 0;
sock_init_data(sock, sk);
5 使用具體協議初始化socket
這一步通過hook機制(回調函數)實現。
回到__sys_socket()
在函數的最后使用sock_map_fd()
,將初始化好的socket與文件系統關聯,對於TCP和其他協議來說,區別在於socket結構體中family字段的不同:
return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));