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;
……
/* 回送一个 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 来到后,会进入下列函数:
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:
[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 tcp_sock *tp = tcp_sk(sk);
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 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(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 < 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;
}
/* File flags are not inherited via accept() unlike another OSes. */
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;
}[/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 << 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.
/*
* This will accept the next outstanding connection.
*/
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);
/* We need to make sure that this socket is listening,
* and that it has something pending.
*/
error = -EINVAL;
if (sk->sk_state != TCP_LISTEN)
goto out;
/* 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)
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)
{
/* 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->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;
}
/*
* "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)) < 0)
goto out_release;
函数 sock_map_fd 在创建 socket 中已经见过了。
小结:
accept 有几件事情要做
1. 要 accept需要三次握手完成, 连接请求入tp->accept_queue队列(新为客户端分析的 sk, 也在其中), 其才能出队
2. 为 accept分配一个sokcet结构, 并将其与新的sk关联
3. 如果调用时,需要获取客户端地址,即第二个参数不为 NULL,则从新的 sk 中,取得其想的葫芦;
4. 将新的 socket 结构与文件系统挂钩;
阅读(607) | 评论(0) | 转发(0) |