一、tcp 棧的三次握手簡述
進一步的分析,都是以 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;
……
if (tcp_v4_send_synack(sk, req, dst))
goto drop_and_free;
if (want_cookie) {
……
} else {
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;
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;
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 來到後,會進入下列函數:
tcp_v4_rcv
->tcp_v4_do_rcv
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:
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 tcp_sock *tp = tcp_sk(sk);
struct sock *nsk;
struct open_request **prev;
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 表:
static struct open_request *tcp_v4_search_req(struct tcp_sock *tp,
struct open_request ***prevp,
__u16 rport,
__u32 raddr, __u32 laddr)
{
struct tcp_listen_opt *lopt = tp->listen_opt;
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)
{
struct tcp_sock *tp = tcp_sk(sk);
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;
}
二、sys_accept
如上,當 listen(2)調用準備就緒的時候,伺服器可以通過調用 accept(2)接受或等待(注意這個“或等待”是相當的重要)連接配接隊列中的第一個請求:
int accept(int s, struct sockaddr * addr ,socklen_t *addrlen);
accept()調用,隻是針對有連接配接模式。socket 一旦經過 listen()調用進入監聽狀态後,就被動地調用accept(),接受來自客 戶端的連接配接請求。accept()調用是阻塞的,也就是說如果沒有連接配接請求到達,它會去睡覺,等到連接配接請求到來後(或者是逾時),才會傳回。同樣地,操 作碼 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;
__module_get(newsock->ops->owner);
err = sock->ops->accept(sock, newsock, sock->file->f_flags);
if (err < 0)
goto out_release;
if (upeer_sockaddr) {
if(newsock->ops->getname(newsock, (struct sockaddr *)address, &len, 2)<0) {
err = -ECONNABORTED;
goto out_release;
}
err = move_addr_to_user(address, len, upeer_sockaddr, upeer_addrlen);
if (err < 0)
goto out_release;
}
if ((err = sock_map_fd(newsock)) < 0)
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;
}
代碼稍長了點,逐漸來分析它。
一個 socket,經過 listen(2)設定成 server 套接字後,就永遠不會再與任何用戶端套接字建立連接配接了。因為一旦它接受了一個連接配接請求,就會 建立出一個新的 socket,新的 socket 用來描述新到達的連接配接,而原先的 server套接字并無改變,并且還可以通過下一次 accept()調用 再建立一個新的出來,就像母雞下蛋一樣,“隻取蛋,不殺雞”,server 套接字永遠保持接受新的連接配接請求的能力。
函數先通過 sockfd_lookup(),根據 fd,找到對應的 sock,然後通過 sock_alloc配置設定一個新的 sock。接着就調用協定簇的 accept()函數:
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 << sk2->sk_state) &
(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT | TCPF_CLOSE));
sock_graft(sk2, newsock);
newsock->state = SS_CONNECTED;
err = 0;
release_sock(sk2); do_err:
return err;
}
函數第一步工作是調用協定的 accept 函數,然後調用 sock_graft()函數,接下來,設定新的套接字的狀态為 SS_CONNECTED。
struct sock *tcp_accept(struct sock *sk, int flags, int *err)
{
struct tcp_sock *tp = tcp_sk(sk);
struct open_request *req;
struct sock *newsk;
int error;
lock_sock(sk);
error = -EINVAL;
if (sk->sk_state != TCP_LISTEN)
goto out;
if (!tp->accept_queue) {
long timeo = sock_rcvtimeo(sk, flags & O_NONBLOCK);
error = -EAGAIN;
if (!timeo)
goto out;
error = wait_for_connect(sk, timeo);
if (error)
goto out;
}
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;
out:
release_sock(sk);
*err = error;
return NULL;
}
tcp_accept()函數,當發現 tp->accept_queue 準備就緒後,就直接調用
req = tp->accept_queue;
if ((tp->accept_queue = req->dl_next) == NULL)
tp->accept_queue_tail = NULL;
newsk = req->sk;
出隊,并取得相應的 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_v4_rcv
->tcp_v4_do_rcv
->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)
{
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:
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->sk_state) & (TCPF_CLOSE | TCPF_SYN_SENT)) &&
peer == 1))
return -ENOTCONN;
sin->sin_port = inet->dport;
sin->sin_addr.s_addr = inet->daddr;
} else {
__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<0 || len> MAX_SOCK_ADDR)
return -EINVAL;
if(len)
{
if(copy_to_user(uaddr,kaddr,len))
return -EFAULT;
}
return __put_user(klen, ulen);
}
也就是調用 copy_to_user的過程了。
sys_accept 的最後一步工作,是将新的 socket 結構,與檔案系統挂鈎:
if ((err = sock_map_fd(newsock)) < 0)
goto out_release;
函數 sock_map_fd 在建立 socket 中已經見過了。
小結:
accept 有幾件事情要做:
1、要 accept,需要三次握手完成,連接配接請求入 tp->accept_queue 隊列(新為用戶端分析的 sk,也在其中),其才能出隊;
2、為 accept配置設定一個 sokcet 結構,并将其與新的 sk 關聯;
3、如果調用時,需要擷取用戶端位址,即第二個參數不為 NULL,則從新的 sk 中,取得其想的葫蘆;
4、将新的 socket 結構與檔案系統挂鈎;