您的位置:首页 > 理论基础 > 计算机网络

Linux网络协议栈 -- socket accept接收连接

2012-07-08 20:06 1011 查看
一、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:

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 表:

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;

/*

* 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;

}

代码稍长了点,逐步来分析它。

一个 socket,经过 listen(2)设置成 server 套接字后,就永远不会再与任何客户端套接字建立连接了。因为一旦它接受了一个连接请求,就会 创建出一个新的 socket,新的 socket 用来描述新到达的连接,而原先的 server套接字并无改变,并且还可以通过下一次 accept()调用 再创建一个新的出来,就像母鸡下蛋一样,“只取蛋,不杀鸡”,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 结构与文件系统挂钩;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: