<b>Tcp棧的三次握手簡述</b>
進一步的分析,都是以 tcp 協定為例,因為 udp要相對簡單得多,分析完 tcp,udp的基本已經被覆寫了。
這裡主要是分析 socket,但是因為它将與 tcp/udp傳輸層互動,是以不可避免地接觸到這一層面的代碼,
這裡隻是摘取其主要流程的一些代碼片段,以更好地分析accept的實作過程。
當套接字進入 LISTEN後,意味着伺服器端已經可以接收來自用戶端的請求。當一個 syn 包到達後,伺服器認為它是一個
tcp請求封包,根據tcp協定,TCP 網絡棧将會自動應答它一個 syn+ack 封包,并且将它放入 syn_table 這個 hash 表
中,靜靜地等待用戶端第三次握手封包的來到。一個 tcp 的 syn 封包進入 tcp 堆棧後,會按以下函數調用,
最終進入 tcp_v4_conn_request:
tcp_v4_rcv
->tcp_v4_do_rcv
->tcp_rcv_state_process
->tp->af_specific->conn_request
tcp_ipv4.c 中,tcp_v4_init_sock初始化時,有tp->af_specific = &ipv4_specific;
struct tcp_func ipv4_specific = {
.queue_xmit = ip_queue_xmit,
.send_check = tcp_v4_send_check,
.rebuild_header = tcp_v4_rebuild_header,
.conn_request = tcp_v4_conn_request,
.syn_recv_sock = tcp_v4_syn_recv_sock,
.remember_stamp = tcp_v4_remember_stamp,
.net_header_len = sizeof(struct iphdr),
.setsockopt = ip_setsockopt,
.getsockopt = ip_getsockopt,
.addr2sockaddr = v4_addr2sockaddr,
.sockaddr_len = sizeof(struct sockaddr_in),
};
是以 af_specific->conn_request實際指向的是 tcp_v4_conn_request:
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
struct open_request *req;
……
/* 配置設定一個連接配接請求 */
req = tcp_openreq_alloc();
if (!req)
goto drop;
……
/* 根據資料包的實際要素,如來源/目的位址等,初始化它*/
tcp_openreq_init(req, &tmp_opt, skb);
req->af.v4_req.loc_addr = daddr;
req->af.v4_req.rmt_addr = saddr;
req->af.v4_req.opt = tcp_v4_save_options(sk, skb);
req->class = &or_ipv4;
/* 回送一個 syn+ack 的二次握手封包 */
if (tcp_v4_send_synack(sk, req, dst))
goto drop_and_free;
if (want_cookie) {
……
} else { /* 将連接配接請求 req 加入連接配接監聽表 syn_table */
tcp_v4_synq_add(sk, req);
}
return 0;
}
syn_table 在前面分析的時候已經反複看到了。它的作用就是記錄 syn 請求封包,建構一個 hash 表。
這裡調用的 tcp_v4_synq_add()就完成了将請求添加進該表的操作:
static void tcp_v4_synq_add(struct sock *sk, struct open_request *req)
struct tcp_sock *tp = tcp_sk(sk);
struct tcp_listen_opt *lopt = tp->listen_opt;
/* 計算一個 hash值 */
u32 h = tcp_v4_synq_hash(req->af.v4_req.rmt_addr, req->rmt_port, lopt->hash_rnd);
req->expires = jiffies + TCP_TIMEOUT_INIT;
req->retrans = 0;
req->sk = NULL;
/*指針移到 hash 鍊的未尾*/
req->dl_next = lopt->syn_table[h];
write_lock(&tp->syn_wait_lock);
/*加入目前節點*/
lopt->syn_table[h] = req;
write_unlock(&tp->syn_wait_lock);
tcp_synq_added(sk);
這樣所有的 syn 請求都被放入這個表中,留待第三次 ack 的到來的比對。當第三次 ack 來到後,會進入下列函數:
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
……
if (sk->sk_state == TCP_LISTEN) {
struct sock *nsk = tcp_v4_hnd_req(sk, skb);
因為目前 sk還是 TCP_LISTEN狀态,是以會進入 tcp_v4_hnd_req:
[code]static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
struct tcphdr *th = skb->h.th;
struct iphdr *iph = skb->nh.iph;
struct sock *nsk;
struct open_request **prev;
/* Find possible connection requests. */
struct open_request *req = tcp_v4_search_req(tp, &prev, th->source,
iph->saddr, iph->daddr);
if (req)
return tcp_check_req(sk, skb, req, prev);
tcp_v4_search_req 就是查找比對 syn_table 表:
[code]static struct open_request *tcp_v4_search_req(struct tcp_sock *tp,
struct open_request ***prevp,
__u16 rport,
__u32 raddr, __u32 laddr)
struct open_request *req, **prev;
for (prev = &lopt->syn_table[tcp_v4_synq_hash(raddr, rport, lopt->hash_rnd)];
(req = *prev) != NULL;
prev = &req->dl_next) {
if (req->rmt_port == rport &&
req->af.v4_req.rmt_addr == raddr &&
req->af.v4_req.loc_addr == laddr &&
TCP_INET_FAMILY(req->class->family)) {
BUG_TRAP(!req->sk);
*prevp = prev;
break;
}
return req;
hash 表的查找還是比較簡單的,調用 tcp_v4_synq_hash 計算出 hash 值,找到 hash 鍊入口,周遊該
鍊即可。 排除逾時等意外因素,剛才加入 hash 表的 req 會被找到,這樣,tcp_check_req()函數将會被繼續調用:
struct sock *tcp_check_req(struct sock *sk,struct sk_buff *skb,
struct open_request *req,
struct open_request **prev)
tcp_acceptq_queue(sk, req, child);
req 被找到,表明三次握手已經完成,連接配接已經成功建立,tcp_check_req 最終将調用tcp_acceptq_queue(),
把這個建立好的連接配接加入至 tp->accept_queue 隊列,等待使用者調用 accept(2)來讀取之。
static inline void tcp_acceptq_queue(struct sock *sk, struct open_request *req,
struct sock *child)
req->sk = child;
sk_acceptq_added(sk);
if (!tp->accept_queue_tail) {
tp->accept_queue = req;
} else {
tp->accept_queue_tail->dl_next = req;
tp->accept_queue_tail = req;
req->dl_next = NULL;
<b>sys_accept</b>
當 listen(2)調用準備就緒的時候,伺服器可以通過調用 accept(2)接受或等待(注意這個“或等
待”是相當的重要)連接配接隊列中的第一個請求:
int accept(int s, struct sockaddr * addr ,socklen_t *addrlen);
accept(2)調用,隻是針對有連接配接模式。socket 一旦經過 listen(2)調用進入監聽狀态後,就被動地調用
accept(2)接受來自用戶端的連接配接請求。accept(2)調用是阻塞的,也就是說如果沒有連接配接請求到達,它會去睡覺,
等到連接配接請求到來後(或者是逾時)才會傳回。同樣地操作碼 SYS_ACCEPT 對應的是函數sys_accept
asmlinkage long sys_accept(int fd, struct sockaddr __user *upeer_sockaddr, int __user
*upeer_addrlen) {
struct socket *sock, *newsock;
int err, len;
char address[MAX_SOCK_ADDR];
sock = sockfd_lookup(fd, &err);
if (!sock)
goto out;
err = -ENFILE;
if (!(newsock = sock_alloc()))
goto out_put;
newsock->type = sock->type;
newsock->ops = sock->ops;
err = security_socket_accept(sock, newsock);
if (err)
goto out_release;
/*
* We don't need try_module_get here, as the listening socket (sock)
* has the protocol module (sock->ops->owner) held.
*/
__module_get(newsock->ops->owner);
err = sock->ops->accept(sock, newsock, sock->file->f_flags);
if (err goto out_release;
if (upeer_sockaddr) {
if(newsock->ops->getname(newsock, (struct sockaddr *)address, &len, 2) err = -ECONNABORTED;
goto out_release;
err = move_addr_to_user(address, len, upeer_sockaddr, upeer_addrlen);
if (err goto out_release;
/* File flags are not inherited via accept() unlike another OSes. */
if ((err = sock_map_fd(newsock)) goto out_release;
security_socket_post_accept(sock, newsock);
out_put:
sockfd_put(sock);
out:
return err;
out_release:
sock_release(newsock);
goto out_put;
}[/code]
代碼稍長了點,逐漸來分析它。
一個 socket,經過 listen(2)設定成 server 套接字後,就永遠不會再與任何用戶端套接字建立連接配接了。
因為一旦它接受了一個連接配接請求,就會建立出一個新的socket,新的 socket 用來描述新到達的連接配接,而原先的 server
套接字并無改變,并且還可以通過下一次 accept(2)調用 再建立一個新的出來,就像母雞下蛋一樣,“隻取蛋,不殺雞”,
server 套接字永遠保持接受新的連接配接請求的能力。
函數先通過 sockfd_lookup(),根據 fd,找到對應的 sock,然後通過 sock_alloc配置設定一個新的 sock。
接着就調用協定簇的 accept()函數:
/*
* Accept a pending connection. The TCP layer now gives BSD semantics.
*/
int inet_accept(struct socket *sock, struct socket *newsock, int flags)
struct sock *sk1 = sock->sk;
int err = -EINVAL;
struct sock *sk2 = sk1->sk_prot->accept(sk1, flags, &err);
if (!sk2)
goto do_err;
lock_sock(sk2);
BUG_TRAP((1 sk_state) &
(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT | TCPF_CLOSE));
sock_graft(sk2, newsock);
newsock->state = SS_CONNECTED;
err = 0;
release_sock(sk2); do_err:
函數第一步工作是調用協定的 accept 函數,然後調用 sock_graft()函數,
接下來設定新的套接字的狀态為 SS_CONNECTED.
* This will accept the next outstanding connection.
struct sock *tcp_accept(struct sock *sk, int flags, int *err)
struct sock *newsk;
int error;
lock_sock(sk);
/* We need to make sure that this socket is listening,
* and that it has something pending.
error = -EINVAL;
if (sk->sk_state != TCP_LISTEN)
/* Find already established connection */
if (!tp->accept_queue) {
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
/* If this is a non blocking socket don't sleep */
error = -EAGAIN;
if (!timeo)
goto out;
error = wait_for_connect(sk, timeo);
if (error)
req = tp->accept_queue;
if ((tp->accept_queue = req->dl_next) == NULL)
tp->accept_queue_tail = NULL;
newsk = req->sk;
sk_acceptq_removed(sk);
tcp_openreq_fastfree(req);
BUG_TRAP(newsk->sk_state != TCP_SYN_RECV);
release_sock(sk);
return newsk;
*err = error;
return NULL;
tcp_accept()函數,當發現 tp->accept_queue 準備就緒後,就直接調用
tp->accept_queue_tail = NULL;
出隊,并取得相應的 sk。 否則,就在擷取逾時時間後,調用 wait_for_connect 等待連接配接的到來。這也是說,
強調“或等待”的原因所在了。
OK,繼續回到 inet_accept 中來,當取得一個就緒的連接配接的 sk(sk2)後,先校驗其狀态,再調用sock_graft()函數。
在 sys_accept 中,已經調用了 sock_alloc,配置設定了一個新的 socket 結構(即 newsock),但 sock_alloc
必竟不是 sock_create,它并不能為 newsock 配置設定一個對應的 sk。是以這個套接字并不完整。
另一方面,當一個連接配接到達到,根據用戶端的請求,産生了一個新的 sk(即 sk2,但這個配置設定過程
沒有深入 tcp 棧去分析其實作,隻分析了它對應的 req 入隊的代碼)。呵呵,将兩者一關聯,就 OK
了,這就是 sock_graft 的任務:
static inline void sock_graft(struct sock *sk, struct socket *parent)
write_lock_bh(&sk->sk_callback_lock);
sk->sk_sleep = &parent->wait;
parent->sk = sk;
sk->sk_socket = parent;
write_unlock_bh(&sk->sk_callback_lock);
這樣,一對一的聯系就建立起來了。這個為 accept 配置設定的新的 socket 也大功告成了。接下來将其狀
态切換為 SS_CONNECTED,表示已連接配接就緒,可以來讀取資料了——如果有的話。
順便提一下,新的 sk 的配置設定,是在:
->tcp_check_req
->tp->af_specific->syn_recv_sock(sk, skb, req, NULL);
即 tcp_v4_syn_recv_sock函數,其又調用 tcp_create_openreq_child()來配置設定的。
struct sock *tcp_create_openreq_child(struct sock *sk, struct open_request *req, struct sk_buff *skb)
/* allocate the newsk from the same slab of the master sock,
* if not, at sk_free time we'll try to free it from the wrong
* slabcache (i.e. is it TCPv4 or v6?), this is handled thru sk->sk_prot -acme */
struct sock *newsk = sk_alloc(PF_INET, GFP_ATOMIC, sk->sk_prot, 0);
if(newsk != NULL) {
……
memcpy(newsk, sk, sizeof(struct tcp_sock));
newsk->sk_state = TCP_SYN_RECV;
等到分析 tcp 棧的實作的時候,再來仔細分析它。但是這裡新的 sk 的有限狀态機被切換至了
TCP_SYN_RECV(按我的想法,似乎應進入 establshed 才對呀,是不是哪兒看漏了,隻有看了後頭的代碼再來印證了)
回到 sys_accept 中來,如果調用者要求傳回用戶端的位址,則調用新的 sk 的getname 函數指針,
也就是 inet_getname:
* This does both peername and sockname.
int inet_getname(struct socket *sock, struct sockaddr *uaddr,
int *uaddr_len, int peer)
struct sock *sk = sock->sk;
struct inet_sock *inet = inet_sk(sk);
struct sockaddr_in *sin = (struct sockaddr_in *)uaddr;
sin->sin_family = AF_INET;
if (peer) {
if (!inet->dport ||
(((1 sk_state) & (TCPF_CLOSE | TCPF_SYN_SENT)) &&
peer == 1))
return -ENOTCONN;
sin->sin_port = inet->dport;
sin->sin_addr.s_addr = inet->daddr;
__u32 addr = inet->rcv_saddr; if (!addr)
addr = inet->saddr;
sin->sin_port = inet->sport;
sin->sin_addr.s_addr = addr;
memset(sin->sin_zero, 0, sizeof(sin->sin_zero));
*uaddr_len = sizeof(*sin);
return 0;
函數的工作是建構 struct sockaddr_in 結構出來,接着在 sys_accept中,調用 move_addr_to_user()
函數來拷貝至使用者空間:
int move_addr_to_user(void *kaddr, int klen, void __user *uaddr, int __user *ulen)
int err;
int len;
if((err=get_user(len, ulen)))
return err;
if(len>klen)
len=klen;
if(len MAX_SOCK_ADDR)
return -EINVAL;
if(len)
{
if(copy_to_user(uaddr,kaddr,len))
return -EFAULT;
* "fromlen shall refer to the value before truncation.."
* 1003.1g
return __put_user(klen, ulen);
也就是調用 copy_to_user的過程了。
sys_accept 的最後一步工作,是将新的 socket 結構,與檔案系統挂鈎:
if ((err = sock_map_fd(newsock)) goto out_release;
函數 sock_map_fd 在建立 socket 中已經見過了。
小結:
accept 有幾件事情要做
1. 要 accept需要三次握手完成, 連接配接請求入tp->accept_queue隊列(新為用戶端分析的 sk, 也在其中), 其才能出隊
2. 為 accept配置設定一個sokcet結構, 并将其與新的sk關聯
3. 如果調用時,需要擷取用戶端位址,即第二個參數不為 NULL,則從新的 sk 中,取得其想的葫蘆;
4. 将新的 socket 結構與檔案系統挂鈎;