- 浏览: 1401303 次
- 性别:
- 来自: 火星
文章分类
最新评论
-
aidd:
内核处理time_wait状态详解 -
ahtest:
赞一下~~
一个简单的ruby Metaprogram的例子 -
itiProCareer:
简直胡说八道,误人子弟啊。。。。谁告诉你 Ruby 1.9 ...
ruby中的类变量与类实例变量 -
dear531:
还得补充一句,惊群了之后,数据打印显示,只有一个子线程继续接受 ...
linux已经不存在惊群现象 -
dear531:
我用select试验了,用的ubuntu12.10,内核3.5 ...
linux已经不存在惊群现象
相比于发送数据,接收数据更复杂一些。接收数据这里和3层的接口是tcp_v4_rcv(我前面的blog有介绍3层和4层的接口的实现).而4层和用户空间,也就是系统调用是socket_recvmsg(其他的读取函数也都会调用这个函数).而这个系统调用会调用__sock_recvmsg.下面我们就先来看下这个函数。
它的主要功能是初始化sock_iocb,以便与将来数据从内核空间拷贝到用户空间。然后调用
recvmsg这个虚函数(tcp协议的话也就是tcp_recvmsg).
内核对待数据的接收分为2部分,一部分是当用户是阻塞的读取数据时,这时如果有数据则是直接拷贝到用户空间。而另一方面,如果是非阻塞,则会先把数据拷贝到接收队列。
而在内核中这个队列分为3种形式。分别是:
1 sock域结构的 sk_backlog队列。
2 tcp_sock的ucopy.prequeue队列。
3 sock结构的 receive_queue队列。
我们先来看两个主要的结构体,然后再来解释这3各队列的区别,首先是ucopy结构.
这个结构表示将要直接复制到用户空间的数据。
接下来是sock的sock_lock结构.
内核的注释很详细,这个锁主要是用来对软中断和进程上下文之间提供一个同步。
然后来看3个队列的区别。
首先sk_backlog队列是当当前的sock在进程上下文中被使用时,如果这个时候有数据到来,则将数据拷贝到sk_backlog.
prequeue则是数据buffer第一站一般都是这里,如果prequeue已满,则会拷贝数据到receive_queue队列种。
最后一个receive_queue也就是进程上下文第一个取buffer的队列。(后面介绍tcp_recvmsg时会再介绍这3个队列).
这里为什么要有prequeue呢,直接放到receive_queue不就好了.这里我是认receive_queue的处理比较繁琐(看tcp_rcv_established的实现就知道了,分为slow path和fast path),而软中断每次只能处理一个数据包(在一个cpu上),因此为了软中断能尽快完成,我们就可以先将数据放到prequeue中(tcp_prequeue),然后软中断就直接返回.而处理prequeue就放到进程上下文去处理了.
最后在分析tcp_v4_rcv和tcp_recvmsg之前,我们要知道tcp_v4_rcv还是处于软中断上下文,而tcp_recvmsg是处于进程上下文,因此比如socket_lock_t才会提供一个owned来锁住对应的sock。而我们也就是需要这3个队列来进行软中断上下文和进程上下文之间的通信。最终当数据拷贝到对应队列,则软中断调用返回。这里要注意的是相同的函数在软中断上下文和进程上下文种调用是不同的,我们下面就会看到(比如tcp_rcv_established函数)
ok,现在来看tcp_v4_rcv的源码。这个函数是在软中断上下文中被调用的,我们这里来看下她的代码片断:
上面的流程很简单,我们接下来来看几个跳过的函数,第一个是tcp_prequeue。
这里我们可以看到sysctl_tcp_low_latency可以决定我们是否使用prequeue队列.
我们这里只关注TCP_ESTABLISHED状态,来看tcp_v4_do_rcv:它主要是通过判断相应的tcp状态来进入相关的处理函数。
因此我们这里重点要看的函数就是tcp_rcv_established,当它在软中断上下文中被调用时,主要的目的是将skb加入到receive_queue队列中。因此这里我们只看这一部分,等下面分析tcp_recvmsg时,我们再来看进程上下文才会处理的一部分。
接下来来看tcp_rcvmsg函数。
通过上面我们知道有找个队列可供我们取得skbuf,那么具体的次序是什么呢,我这里摘抄内核的注释,它讲的非常清楚:
由于这个函数比较复杂,因此我们分段来分析这个函数。
首先是处理包之前的一些合法性判断,以及取得一些有用的值。
在上面我们看到了lock_sock,这个函数是用来锁住当前的sock, 我们来看它的详细实现,它最终会调用lock_sock_nested:
我们再来看__lock_sock如何来处理的。
ok,再回到tcp_recvmsg.接下来我们来看如何处理数据包。
下面这一段主要是用来从receive队列中读取数据。
接下来是对tcp状态做一些校验。这里要注意,copied表示的是已经复制到用户空间的skb的大小。而len表示还需要拷贝多少数据。
然后就是根据已经复制的数据大小来清理receive队列中的数据,并且发送ACK给对端。然后就是给tcp_socket的ucopy域赋值,主要是iov域和task域。一个是数据区,一个是当前从属的进程。
上面的分析中有release_sock函数,这个函数用来release这个sock,也就是对这个sock解除锁定。然后唤醒等待队列。
这里要注意,sock一共有两个等待队列,一个是sock的sk_sleep等待队列,这个等待队列用来等待数据的到来。一个是ucopy域的等待队列wq,这个表示等待使用这个sock。
然后来看主要的处理函数__release_sock,它主要是遍历backlog队列,然后处理skb。这里它有两个循环,外部循环是遍历backlog,而内部循环是遍历skb(也就是数据)。
而当数据tp->ucopy.prequeue为空,并且所复制的数据不能达到所期望的值,此时我们进入sk_wait_data等待数据的到来。
接下来就是一些域的更新,以及处理prequeue队列:
在分析tcp_prequeue_process之前,我们先来看下什么情况下release_sock会直接复制数据到用户空间。我们知道它最终会调用tcp_rcv_established函数,因此来看tcp_rcv_established的代码片断
内核接收到的数据包有可能不是正序的,可是内核传递给用户空间的数据必须是正序的,只有这样才能拷贝给用户空间。
通过上面的判断条件我们很容易看出前面调用release_sock,为何有时将数据拷贝到用户空间,有时拷贝到receive队列。
ok,最后我们来看下tcp_prequeue_process的实现。它的实现很简单,就是遍历prequeue,然后处理buf。这里要注意,它会处理完所有的prequeue,也就是会清空prequeue.
最后简要的分析下数据如何复制到用户空间。这里的主要函数是skb_copy_datagram_iovec。最终都是通过这个函数复制到用户空间的。
我们知道内核存储数据有两种形式如果支持S/G IO的网卡,它会保存数据到skb_shinfo(skb)->frags(详见前面的blog),否则则会保存在skb的data区中。
因此这里也是分为两部分处理。
还有一个就是这里遍历frags也是遍历两次,第一次遍历是查找刚好
它的主要功能是初始化sock_iocb,以便与将来数据从内核空间拷贝到用户空间。然后调用
recvmsg这个虚函数(tcp协议的话也就是tcp_recvmsg).
static inline int __sock_recvmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg, size_t size, int flags) { int err; struct sock_iocb *si = kiocb_to_siocb(iocb); ///初始化si。 si->sock = sock; si->scm = NULL; si->msg = msg; si->size = size; si->flags = flags; err = security_socket_recvmsg(sock, msg, size, flags); if (err) return err; //调用tcp_recvmsg return sock->ops->recvmsg(iocb, sock, msg, size, flags); }
内核对待数据的接收分为2部分,一部分是当用户是阻塞的读取数据时,这时如果有数据则是直接拷贝到用户空间。而另一方面,如果是非阻塞,则会先把数据拷贝到接收队列。
而在内核中这个队列分为3种形式。分别是:
1 sock域结构的 sk_backlog队列。
2 tcp_sock的ucopy.prequeue队列。
3 sock结构的 receive_queue队列。
我们先来看两个主要的结构体,然后再来解释这3各队列的区别,首先是ucopy结构.
这个结构表示将要直接复制到用户空间的数据。
/* Data for direct copy to user */ struct { ///prequeue队列。 struct sk_buff_head prequeue; ///表示当前所处的进程,其实也就是skb的接受者。 struct task_struct *task; ///数据区 struct iovec *iov; ///prequeue队列总的所占用的内存大小 int memory; ///这个域表示用户所请求的长度(要注意这个值是可变的,随着拷贝给用户的数据而减少) int len; ........................ } ucopy;
接下来是sock的sock_lock结构.
内核的注释很详细,这个锁主要是用来对软中断和进程上下文之间提供一个同步。
/* This is the per-socket lock. The spinlock provides a synchronization * between user contexts and software interrupt processing, whereas the * mini-semaphore synchronizes multiple users amongst themselves. */ typedef struct { ///自选锁 spinlock_t slock; ///如果有用户进程在使用这个sock 则owned为1,否则为0 int owned; ///等待队列,也就是当sock被锁住后,等待使用这个sock对象。 wait_queue_head_t wq; /* * We express the mutex-alike socket_lock semantics * to the lock validator by explicitly managing * the slock as a lock variant (in addition to * the slock itself): */ #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_map dep_map; #endif } socket_lock_t;
然后来看3个队列的区别。
首先sk_backlog队列是当当前的sock在进程上下文中被使用时,如果这个时候有数据到来,则将数据拷贝到sk_backlog.
prequeue则是数据buffer第一站一般都是这里,如果prequeue已满,则会拷贝数据到receive_queue队列种。
最后一个receive_queue也就是进程上下文第一个取buffer的队列。(后面介绍tcp_recvmsg时会再介绍这3个队列).
这里为什么要有prequeue呢,直接放到receive_queue不就好了.这里我是认receive_queue的处理比较繁琐(看tcp_rcv_established的实现就知道了,分为slow path和fast path),而软中断每次只能处理一个数据包(在一个cpu上),因此为了软中断能尽快完成,我们就可以先将数据放到prequeue中(tcp_prequeue),然后软中断就直接返回.而处理prequeue就放到进程上下文去处理了.
最后在分析tcp_v4_rcv和tcp_recvmsg之前,我们要知道tcp_v4_rcv还是处于软中断上下文,而tcp_recvmsg是处于进程上下文,因此比如socket_lock_t才会提供一个owned来锁住对应的sock。而我们也就是需要这3个队列来进行软中断上下文和进程上下文之间的通信。最终当数据拷贝到对应队列,则软中断调用返回。这里要注意的是相同的函数在软中断上下文和进程上下文种调用是不同的,我们下面就会看到(比如tcp_rcv_established函数)
ok,现在来看tcp_v4_rcv的源码。这个函数是在软中断上下文中被调用的,我们这里来看下她的代码片断:
int tcp_v4_rcv(struct sk_buff *skb) { ///一些用到的变量 const struct iphdr *iph; struct tcphdr *th; struct sock *sk; int ret; struct net *net = dev_net(skb->dev); ............................ //通过四元组得到对应的sock。 sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest); if (!sk) goto no_tcp_socket; process: ///如果是time_wait状态,则进入相关处理(这次不会分析time_wait状态,以后分析tcp的断开状态变迁时,会详细分析这个). if (sk->sk_state == TCP_TIME_WAIT) goto do_time_wait; ................................. ///加下半部的锁 bh_lock_sock_nested(sk); ret = 0; ///这个宏很简单就是判断(sk)->sk_lock.owned.也就是当进程上下文在使用这个sock时为1. if (!sock_owned_by_user(sk)) { 。........................ { ///先将buffer放到prequeue队列中。如果成功则返回1. if (!tcp_prequeue(sk, skb)) ///假设失败,则直接调用tcp_v4_do_rcv处理这个skb(其实也就是直接放到receive_queue中). ret = tcp_v4_do_rcv(sk, skb); } } else ///当有进程在使用这个sock则放buf到sk_backlog中。 sk_add_backlog(sk, skb); //解锁。 bh_unlock_sock(sk); sock_put(sk); return ret; ...................................................
上面的流程很简单,我们接下来来看几个跳过的函数,第一个是tcp_prequeue。
这里我们可以看到sysctl_tcp_low_latency可以决定我们是否使用prequeue队列.
static inline int tcp_prequeue(struct sock *sk, struct sk_buff *skb) { struct tcp_sock *tp = tcp_sk(sk); ///如果启用tcp_low_latency或者ucopy.task为空则返回0.ucopy.task为空一般是表示进程空间有进程在等待sock的数据的到来,因此我们需要直接复制数据到receive队列。并唤醒它。 if (sysctl_tcp_low_latency || !tp->ucopy.task) return 0; ///加数据包到prequeue队列。 __skb_queue_tail(&tp->ucopy.prequeue, skb); ///update内存大小。 tp->ucopy.memory += skb->truesize; ///如果prequeue已满,则将处理prequeue队列。 if (tp->ucopy.memory > sk->sk_rcvbuf) { struct sk_buff *skb1; BUG_ON(sock_owned_by_user(sk)); ///遍历prequeue队列。 while ((skb1 = __skb_dequeue(&tp->ucopy.prequeue)) != NULL) { ///这个函数最终也会调用tcp_v4_do_rcv(也就是加入到receive队列中). sk_backlog_rcv(sk, skb1); NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPPREQUEUEDROPPED); } ///清空内存。 tp->ucopy.memory = 0; } else if (skb_queue_len(&tp->ucopy.prequeue) == 1) { ///这里表示这个数据包是prequeue的第一个包。然后唤醒等待队列。 wake_up_interruptible_poll(sk->sk_sleep, POLLIN | POLLRDNORM | POLLRDBAND); ///这里的定时器以后会详细介绍。 if (!inet_csk_ack_scheduled(sk)) inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK, (3 * tcp_rto_min(sk)) / 4, TCP_RTO_MAX); } return 1; }
我们这里只关注TCP_ESTABLISHED状态,来看tcp_v4_do_rcv:它主要是通过判断相应的tcp状态来进入相关的处理函数。
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb) { struct sock *rsk; ................................... if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */ TCP_CHECK_TIMER(sk); ///处理数据包。 if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) { rsk = sk; goto reset; } TCP_CHECK_TIMER(sk); return 0; } ........................................ }
因此我们这里重点要看的函数就是tcp_rcv_established,当它在软中断上下文中被调用时,主要的目的是将skb加入到receive_queue队列中。因此这里我们只看这一部分,等下面分析tcp_recvmsg时,我们再来看进程上下文才会处理的一部分。
///程序如何到达这里,我们在分析tcp_recvmsg时会再次分析tcp_rcv_established,那个时候会介绍这个。 if (!eaten) { ///进行checksum if (tcp_checksum_complete_user(sk, skb)) goto csum_error; .................................................. __skb_pull(skb, tcp_header_len); ///最重要的在这里,我们可以看到直接将skb加入到sk_receive队列中。 __skb_queue_tail(&sk->sk_receive_queue, skb); skb_set_owner_r(skb, sk); ///更新rcv_nxt,也就是表示下一个接收序列起始号。 tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq; } ............................................
接下来来看tcp_rcvmsg函数。
通过上面我们知道有找个队列可供我们取得skbuf,那么具体的次序是什么呢,我这里摘抄内核的注释,它讲的非常清楚:
引用
Look: we have the following (pseudo)queues:
1. packets in flight
2. backlog
3. prequeue
4. receive_queue
Each queue can be processed only if the next ones are empty. At this point we have empty receive_queue.But prequeue _can_ be not empty after 2nd iteration, when we jumped to start of loop because backlog
processing added something to receive_queue. We cannot release_sock(), because backlog containd packets arrived _after_ prequeued ones.
Shortly, algorithm is clear --- to process all the queues in order. We could make it more directly,requeueing packets from backlog to prequeue, if is not empty. It is more elegant, but eats cycles,
1. packets in flight
2. backlog
3. prequeue
4. receive_queue
Each queue can be processed only if the next ones are empty. At this point we have empty receive_queue.But prequeue _can_ be not empty after 2nd iteration, when we jumped to start of loop because backlog
processing added something to receive_queue. We cannot release_sock(), because backlog containd packets arrived _after_ prequeued ones.
Shortly, algorithm is clear --- to process all the queues in order. We could make it more directly,requeueing packets from backlog to prequeue, if is not empty. It is more elegant, but eats cycles,
由于这个函数比较复杂,因此我们分段来分析这个函数。
首先是处理包之前的一些合法性判断,以及取得一些有用的值。
int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len, int nonblock, int flags, int *addr_len) { ................................... ///锁住当前的socket。 lock_sock(sk); TCP_CHECK_TIMER(sk); err = -ENOTCONN; if (sk->sk_state == TCP_LISTEN) goto out; ///得到超时时间(前面已经介绍过了).如果非阻塞则为0. timeo = sock_rcvtimeo(sk, nonblock); /* Urgent data needs to be handled specially. */ if (flags & MSG_OOB) goto recv_urg; ///取得当前tcp字节流中的未读数据的起始序列号。 seq = &tp->copied_seq; if (flags & MSG_PEEK) { peek_seq = tp->copied_seq; seq = &peek_seq; } ///主要是用来处理MSG_WAITALL套接字选项。这个选项是用来标记是否等待所有的数据到达才返回的。 target = sock_rcvlowat(sk, flags & MSG_WAITALL, len); }
在上面我们看到了lock_sock,这个函数是用来锁住当前的sock, 我们来看它的详细实现,它最终会调用lock_sock_nested:
void lock_sock_nested(struct sock *sk, int subclass) { might_sleep(); ///首先加锁。 spin_lock_bh(&sk->sk_lock.slock); ///如果owned为1,也就是有其他进程在使用这个sock。此时调用__lock_sock(这个函数用来休眠进程,进入等待队列)。 if (sk->sk_lock.owned) __lock_sock(sk); ///当sock可以使用了,则设置owned为1,标记被当前进程所使用。 sk->sk_lock.owned = 1; ///解锁。 spin_unlock(&sk->sk_lock.slock); /* * The sk_lock has mutex_lock() semantics here: */ mutex_acquire(&sk->sk_lock.dep_map, subclass, 0, _RET_IP_); local_bh_enable(); }
我们再来看__lock_sock如何来处理的。
static void __lock_sock(struct sock *sk) { DEFINE_WAIT(wait); for (;;) { ///加入等待队列,可以看到加入的等待队列是sl_lock.wq,也就是我们上面介绍过得。而这个等待队列的唤醒我们下面会介绍。 prepare_to_wait_exclusive(&sk->sk_lock.wq, &wait, TASK_UNINTERRUPTIBLE); ///解锁。 spin_unlock_bh(&sk->sk_lock.slock); ///让出cpu,进入休眠。 schedule(); spin_lock_bh(&sk->sk_lock.slock); ///如果轮到我们处理这个sock,则跳出循环。 if (!sock_owned_by_user(sk)) break; } finish_wait(&sk->sk_lock.wq, &wait); }
ok,再回到tcp_recvmsg.接下来我们来看如何处理数据包。
下面这一段主要是用来从receive队列中读取数据。
do { u32 offset; ///是否有urgent数据,如果已经读取了一些数据或者有个未决的sigurg信号,则直接退出循环。 if (tp->urg_data && tp->urg_seq == *seq) { if (copied) break; if (signal_pending(current)) { copied = timeo ? sock_intr_errno(timeo) : -EAGAIN; break; } } ///开始处理buf,首先是从receive队列中读取buf。 skb_queue_walk(&sk->sk_receive_queue, skb) { ///开始遍历receive_queue. if (before(*seq, TCP_SKB_CB(skb)->seq)) { printk(KERN_INFO "recvmsg bug: copied %X " "seq %X\n", *seq, TCP_SKB_CB(skb)->seq); break; } ///由于tcp是字节流,因此我们拷贝给用户空间,需要正序的拷贝给用户,这里的第一个seq前面已经描述了,表示当前的总的sock连接中的未读数据的起始序列号,而后一个seq表示当前skb的起始序列号。因此这个差值如果小于skb->len,就表示,当前的skb就是我们需要读取的那个skb(因为它的序列号最小). offset = *seq - TCP_SKB_CB(skb)->seq; ///跳过syn。 if (tcp_hdr(skb)->syn) offset--; ///找到skb。 if (offset < skb->len) goto found_ok_skb; if (tcp_hdr(skb)->fin) goto found_fin_ok; WARN_ON(!(flags & MSG_PEEK)); } .................................... }while(len > 0)
接下来是对tcp状态做一些校验。这里要注意,copied表示的是已经复制到用户空间的skb的大小。而len表示还需要拷贝多少数据。
///如果复制的值大于等于所需要复制的,并且sk_backlog为空,则跳出循环。这是因为我们每次复制完毕之后,都需要将sk_backlog中的数据复制到receive队列中。 if (copied >= target && !sk->sk_backlog.tail) break; if (copied) { if (sk->sk_err || sk->sk_state == TCP_CLOSE || (sk->sk_shutdown & RCV_SHUTDOWN) || !timeo || signal_pending(current)) break; } else { ///如没有复制到数据(也就是receive为空),则判断是否有错误发生。这里主要是状态的判断和超时的判断。 if (sock_flag(sk, SOCK_DONE)) break; if (sk->sk_err) { copied = sock_error(sk); break; } if (sk->sk_shutdown & RCV_SHUTDOWN) break; if (sk->sk_state == TCP_CLOSE) { if (!sock_flag(sk, SOCK_DONE)) { copied = -ENOTCONN; break; } break; } if (!timeo) { copied = -EAGAIN; break; } if (signal_pending(current)) { copied = sock_intr_errno(timeo); break; } }
然后就是根据已经复制的数据大小来清理receive队列中的数据,并且发送ACK给对端。然后就是给tcp_socket的ucopy域赋值,主要是iov域和task域。一个是数据区,一个是当前从属的进程。
tcp_cleanup_rbuf(sk, copied); if (!sysctl_tcp_low_latency && tp->ucopy.task == user_recv) { ///循环的第一次的话user_recv为空,因此给ucopy得想关域赋值。 if (!user_recv && !(flags & (MSG_TRUNC | MSG_PEEK))) { //进程为当前进程。 user_recv = current; tp->ucopy.task = user_recv; tp->ucopy.iov = msg->msg_iov; } ///长度为还需拷贝的数据的长度。 tp->ucopy.len = len; WARN_ON(tp->copied_seq != tp->rcv_nxt && !(flags & (MSG_PEEK | MSG_TRUNC))); ///如果prequeue不为空则跳到 do_prequeue,处理backlog队列。 if (!skb_queue_empty(&tp->ucopy.prequeue)) goto do_prequeue; /* __ Set realtime policy in scheduler __ */ } ///已经复制完毕,则开始拷贝back_log队列到receive队列。 if (copied >= target) { /* Do not sleep, just process backlog. */ release_sock(sk); lock_sock(sk); } else ///否则进入休眠,等待数据的到来。 sk_wait_data(sk, &timeo);
上面的分析中有release_sock函数,这个函数用来release这个sock,也就是对这个sock解除锁定。然后唤醒等待队列。
这里要注意,sock一共有两个等待队列,一个是sock的sk_sleep等待队列,这个等待队列用来等待数据的到来。一个是ucopy域的等待队列wq,这个表示等待使用这个sock。
void release_sock(struct sock *sk) { mutex_release(&sk->sk_lock.dep_map, 1, _RET_IP_); spin_lock_bh(&sk->sk_lock.slock); ///如果backlog队列不为空,则调用__release_sock处理 if (sk->sk_backlog.tail) __release_sock(sk); ///处理完毕则给owened赋值为0.释放对这个sock的控制。 sk->sk_lock.owned = 0; ///唤醒wq上的所有元素。 if (waitqueue_active(&sk->sk_lock.wq)) wake_up(&sk->sk_lock.wq); spin_unlock_bh(&sk->sk_lock.slock); }
然后来看主要的处理函数__release_sock,它主要是遍历backlog队列,然后处理skb。这里它有两个循环,外部循环是遍历backlog,而内部循环是遍历skb(也就是数据)。
static void __release_sock(struct sock *sk) { struct sk_buff *skb = sk->sk_backlog.head; ///遍历backlog队列。 do { sk->sk_backlog.head = sk->sk_backlog.tail = NULL; bh_unlock_sock(sk); do { struct sk_buff *next = skb->next; skb->next = NULL; ///这个函数我们知道最终会调tcp_v4_do_rcv.而在tcp_v4_do_rcv中,会把数据复制到receive_queue队列中。 sk_backlog_rcv(sk, skb); cond_resched_softirq(); skb = next; } while (skb != NULL); bh_lock_sock(sk); } while ((skb = sk->sk_backlog.head) != NULL); }
而当数据tp->ucopy.prequeue为空,并且所复制的数据不能达到所期望的值,此时我们进入sk_wait_data等待数据的到来。
#define sk_wait_event(__sk, __timeo, condition) \ ({int __rc; \ ///这个我们通过上面知道,会将数据复制到receive-queue队列。 release_sock(__sk); \ __rc = condition; ///当sk_wait_data调用时,rc是用来判断receive_queue是否为空的, \ if (!__rc) { ///如果为空则会休眠等待,sk_sleep等待队列的唤醒。 \ *(__timeo) = schedule_timeout(*(__timeo)); \ } \ lock_sock(__sk); \ __rc = condition; \ __rc; \ }) int sk_wait_data(struct sock *sk, long *timeo) { int rc; DEFINE_WAIT(wait); ///加入sk_sleep的等待队列 prepare_to_wait(sk->sk_sleep, &wait, TASK_INTERRUPTIBLE); set_bit(SOCK_ASYNC_WAITDATA, &sk->sk_socket->flags); ///处理事件 rc = sk_wait_event(sk, timeo, !skb_queue_empty(&sk->sk_receive_queue)); clear_bit(SOCK_ASYNC_WAITDATA, &sk->sk_socket->flags); finish_wait(sk->sk_sleep, &wait); return rc; }
接下来就是一些域的更新,以及处理prequeue队列:
if (user_recv) { int chunk; /* __ Restore normal policy in scheduler __ */ ///这个判断主要是由于在release_sock中,有可能会将数据直接复制到用户空间了。此时我们需要更新len以及copied域。 if ((chunk = len - tp->ucopy.len) != 0) { NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMBACKLOG, chunk); len -= chunk; copied += chunk; } ///tp->rcv_nxt == tp->copied_seq主要用来判断是否receive队列中还需要数据要执行吗(下面会说明为什么)。 if (tp->rcv_nxt == tp->copied_seq && !skb_queue_empty(&tp->ucopy.prequeue)) { do_prequeue: ///执行prequeue tcp_prequeue_process(sk); ///和上面一样,更新len和cpoied域。 if ((chunk = len - tp->ucopy.len) != 0) { NET_ADD_STATS_USER(sock_net(sk), LINUX_MIB_TCPDIRECTCOPYFROMPREQUEUE, chunk); len -= chunk; copied += chunk; } } } ................................... continue;
在分析tcp_prequeue_process之前,我们先来看下什么情况下release_sock会直接复制数据到用户空间。我们知道它最终会调用tcp_rcv_established函数,因此来看tcp_rcv_established的代码片断
内核接收到的数据包有可能不是正序的,可是内核传递给用户空间的数据必须是正序的,只有这样才能拷贝给用户空间。
else { int eaten = 0; int copied_early = 0; ///判断从这里开始。copied_seq表示未读的skb的序列号。而rcv_nxt为我们所期望接收的下一个数据的序列号。这里我们是要保证字节流的正序。而第二个条件len - tcp_header_len <= tp->ucopy.len这个说明用户请求的数据还没有复制够。如果已经复制够了,则会复制数据到receive_queue队列。 if (tp->copied_seq == tp->rcv_nxt && len - tcp_header_len <= tp->ucopy.len) { ......................... ///然后判断从属的进程必须等于当前调用进程。并且必须为进程上下文。 if (tp->ucopy.task == current && sock_owned_by_user(sk) && !copied_early) { __set_current_state(TASK_RUNNING); ///开始复制数据到用户空间。 if (!tcp_copy_to_iovec(sk, skb, tcp_header_len)) eaten = 1; } ...............................
通过上面的判断条件我们很容易看出前面调用release_sock,为何有时将数据拷贝到用户空间,有时拷贝到receive队列。
ok,最后我们来看下tcp_prequeue_process的实现。它的实现很简单,就是遍历prequeue,然后处理buf。这里要注意,它会处理完所有的prequeue,也就是会清空prequeue.
static void tcp_prequeue_process(struct sock *sk) { struct sk_buff *skb; struct tcp_sock *tp = tcp_sk(sk); NET_INC_STATS_USER(sock_net(sk), LINUX_MIB_TCPPREQUEUED); /* RX process wants to run with disabled BHs, though it is not * necessary */ local_bh_disable(); ///遍历并处理skb。 while ((skb = __skb_dequeue(&tp->ucopy.prequeue)) != NULL) ///最终会调用tcp_rcv_established. sk_backlog_rcv(sk, skb); local_bh_enable(); ///内存清空为0. tp->ucopy.memory = 0; }
最后简要的分析下数据如何复制到用户空间。这里的主要函数是skb_copy_datagram_iovec。最终都是通过这个函数复制到用户空间的。
我们知道内核存储数据有两种形式如果支持S/G IO的网卡,它会保存数据到skb_shinfo(skb)->frags(详见前面的blog),否则则会保存在skb的data区中。
因此这里也是分为两部分处理。
还有一个就是这里遍历frags也是遍历两次,第一次遍历是查找刚好
int skb_copy_datagram_iovec(const struct sk_buff *skb, int offset, struct iovec *to, int len) { int start = skb_headlen(skb); int i, copy = start - offset; struct sk_buff *frag_iter; ///支持S/G IO的网卡,第一个数据包也是保存在data域中的。 if (copy > 0) { if (copy > len) copy = len; ///复制data域。 if (memcpy_toiovec(to, skb->data + offset, copy)) goto fault; if ((len -= copy) == 0) return 0; offset += copy; } ///遍历frags,开始复制数据。 for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) { int end; WARN_ON(start > offset + len); ///计算复制的字节数 end = start + skb_shinfo(skb)->frags[i].size; ///判断将要复制的字节数是否足够 if ((copy = end - offset) > 0) { int err; u8 *vaddr; skb_frag_t *frag = &skb_shinfo(skb)->frags[i]; struct page *page = frag->page; ///如果将要复制的数据太大,则缩小它为请求的长度。 if (copy > len) copy = len; ///转换物理地址到虚拟地址。 vaddr = kmap(page); ///复制数据。 err = memcpy_toiovec(to, vaddr + frag->page_offset + offset - start, copy); kunmap(page); if (err) goto fault; ///如果复制完毕则返回0 if (!(len -= copy)) return 0; ///更新offset域。 offset += copy; } //更新start域。 start = end; } ///到达这里说明数据还没有宝贝完毕,也就是请求的数据还没拷贝完成。此时我们就需要变化offset域。 skb_walk_frags(skb, frag_iter) { int end; WARN_ON(start > offset + len); end = start + frag_iter->len; if ((copy = end - offset) > 0) { if (copy > len) copy = len; ///改变offset域为offset-start递归重新开始。 if (skb_copy_datagram_iovec(frag_iter, offset - start, to, copy)) goto fault; if ((len -= copy) == 0) return 0; offset += copy; } start = end; } if (!len) return 0; fault: return -EFAULT; }
发表评论
-
Receive packet steering patch详解
2010-07-25 16:46 12126Receive packet steering简称rp ... -
内核中拥塞窗口初始值对http性能的影响分析
2010-07-11 00:20 9696这个是google的人提出的 ... -
linux 内核tcp拥塞处理(一)
2010-03-12 16:17 9572这次我们来分析tcp的拥塞控制,我们要知道协议栈都是很保守的, ... -
内核tcp协议栈SACK的处理
2010-01-24 21:13 12154上一篇处理ack的blog中我 ... -
内核tcp的ack的处理
2010-01-17 03:06 11155我们来看tcp输入对于ack,段的处理。 先是ack的处理, ... -
内核处理time_wait状态详解
2010-01-10 17:39 6802这次来详细看内核的time_wait状态的实现,在前面介绍定时 ... -
tcp协议栈处理各种事件的分析
2009-12-30 01:29 13622首先我们来看socket如何将一些状态的变化通知给对应的进程, ... -
linux内核sk_buff的结构分析
2009-12-25 00:42 47904我看的内核版本是2.6.32. 在内核中sk_buff表示一 ... -
tcp的输入段的处理
2009-12-18 00:56 8351tcp是全双工的协议,因此每一端都会有流控。一个tcp段有可能 ... -
内核协议栈tcp层的内存管理
2009-11-28 17:13 12061我们先来看tcp内存管理相关的几个内核参数,这些都能通过pro ... -
linux内核定时器的实现
2009-10-31 01:44 10191由于linux还不是一个实时的操作系统,因此如果需要更高精度, ... -
linux内核中tcp连接的断开处理
2009-10-25 21:47 10319我们这次主要来分析相关的两个断开函数close和shotdow ... -
linux内核tcp的定时器管理(二)
2009-10-05 20:52 5414这次我们来看后面的3个定时器; 首先是keep alive定 ... -
linux内核tcp的定时器管理(一)
2009-10-04 23:29 9824在内核中tcp协议栈有6种 ... -
linux 内核tcp数据发送的实现
2009-09-10 01:41 19767在分析之前先来看下SO_RCVTIMEO和SO_SNDTIME ... -
tcp connection setup的实现(三)
2009-09-03 00:34 5188先来看下accept的实现. 其实accept的作用很简单, ... -
tcp connection setup的实现(二)
2009-09-01 00:46 8428首先来看下内核如何处理3次握手的半连接队列和accept队列( ... -
tcp connection setup的实现(一)
2009-08-23 04:10 5810bind的实现: 先来介绍几个地址结构. struct ... -
linux内核中socket的实现
2009-08-15 04:38 21096首先来看整个与socket相关的操作提供了一个统一的接口sys ... -
ip层和4层的接口实现分析
2009-08-08 03:50 6199首先来看一下基于3层的ipv4以及ipv6实现的一些4层的协议 ...
相关推荐
根据提供的文件标题、描述、标签以及部分内容,我们可以推断出这份文档主要关注的是Linux内核中的TCP/IP协议栈实现分析。接下来将详细阐述这一主题下的关键知识点。 ### 一、Linux内核源码剖析概述 #### 1. Linux...
Linux内核的TCP实现涉及了大量底层数据结构和功能函数。在TCP层数据结构中,比如tcphdr定义了TCP头部的结构,tcp_options_received用于处理接收到的TCP选项,tcp_sock用于存储TCP连接的状态信息。这些结构和函数的...
《Linux内核TCP/IP协议栈源码分析》 在深入探讨Linux内核的TCP/IP协议栈之前,我们先理解一下TCP/IP协议栈的基本结构。TCP/IP协议栈是互联网通信的核心,它将网络通信分为四层:应用层、传输层、网络层和数据链路层...
在TCP/IP实现中,还会讲解到关键的数据结构,如sk_buff(socket缓冲区),它是Linux内核处理网络数据的核心结构,存储了网络包的头部和数据。此外,书中可能还会涉及网络子系统的锁机制、中断处理和异步I/O等内容,...
在本资料"Linux内核TCP/IP协议栈分析"中,我们将深入探讨这个核心组件的工作原理。 TCP/IP协议栈分为四个主要层次:应用层、传输层、网络层和数据链路层。在Linux内核中,每一层都有相应的模块负责处理相关的协议和...
《Linux内核设计与实现》第三版是一本深入解析Linux内核的重要著作,它详尽地阐述了Linux操作系统的核心设计理念和实现机制。本书是Linux爱好者、系统管理员、软件开发人员以及对操作系统有深入兴趣的读者不可或缺的...
《Linux内核设计与实现》第三版是一本深入解析Linux操作系统内核的权威书籍,对于想要深入了解Linux系统底层工作原理的初学者来说,是不可多得的参考资料。这本书全面覆盖了Linux内核的设计哲学、核心架构以及关键...
5. **网络协议栈**:Linux内核的网络子系统实现了完整的TCP/IP协议栈,包括网络接口层、网络层、传输层和应用层。它处理网络数据包的收发、路由选择、拥塞控制、错误检测与纠正,以及套接字编程接口,使得应用程序...
4. 网络堆栈:Linux内核中的网络部分处理各种网络协议栈,如TCP/IP协议族,负责数据包的发送和接收。 5. 设备驱动:为了与硬件设备通信,Linux内核包含了大量设备驱动程序,使得操作系统能够与各种硬件设备交互。 ...
在Linux内核中,TCP和UDP模块处理连接建立、数据传输、流量控制和拥塞控制等问题。 5. **应用层**:这一层包含各种应用协议,如HTTP、FTP、SMTP等,它们直接与用户交互。Linux内核通过socket API为上层应用提供了与...
《Linux内核TCP协议栈部分,下册》深入解析了Linux操作系统中TCP协议栈的实现细节,涵盖了TCP协议从启动到控制发送的全过程,以及不启用DSACK(duplicate SACK,重复确认选择)等相关主题。这本书是理解网络通信底层...
它实现了TCP/IP协议族,支持多种网络协议,如TCP、UDP、IP等,处理网络数据的收发和路由。 6. **设备驱动**:设备驱动程序是操作系统与硬件之间的桥梁。Linux内核拥有丰富的驱动支持,涵盖了从显卡、声卡到硬盘等...
5. **网络协议栈**:Linux内核实现了完整的TCP/IP协议栈,包括网络接口层、数据链路层、网络层、传输层等。理解套接字编程、网络包的接收与发送过程对于网络应用开发非常有用。 6. **系统调用**:Linux系统调用是...
在阅读《Linux内核完全注释(修正版v5.0)》时,你可以了解到每个模块的实现细节,包括数据结构设计、函数接口、同步原语等。这不仅有助于理解Linux内核的工作原理,也有助于开发人员编写更高效的系统级代码,解决性能...
5. **网络协议栈**:Linux内核包含了完整的TCP/IP协议栈,用于处理网络通信。书中会详细讲解网络套接字编程、网络数据包的接收和发送、网络连接的建立与断开等。 6. **中断和异常处理**:中断是硬件与内核通信的...
Linux内核的网络子系统支持TCP/IP协议栈,实现数据包的发送和接收,包括网络接口驱动、协议处理、路由选择等。它还包括套接字API,允许用户空间应用程序进行网络通信。 七、设备驱动 设备驱动是连接硬件和内核的...
6. **网络协议栈**:Linux内核的网络子系统实现了TCP/IP协议族,包括ARP、IP、TCP、UDP等。它处理网络数据包的收发,实现了网络连接的建立、维护和关闭,以及拥塞控制等功能。 7. **中断和异常处理**:中断是硬件向...
5. **网络协议栈**:分析TCP/IP协议族在Linux内核中的实现,包括网络数据包的接收、发送过程,套接字编程接口,以及TCP、UDP等传输层协议的细节。 6. **系统调用**:阐述系统调用作为用户空间和内核空间交互的桥梁...
《Linux内核设计与实现》是Linux系统编程领域的一部经典著作,分为第三版的中文版和英文版。这本书深入浅出地介绍了Linux内核的工作原理及其设计思想,是理解和学习Linux内核不可或缺的资源。以下是对书中的关键知识...
4. **接收数据**:使用`read()`函数从套接字读取数据,这个函数会从TCP接收缓冲区取出数据并复制到用户空间。 5. **处理接收到的数据**:读取到的数据通常需要处理或存储,这部分代码可能根据实际应用需求有所不同。...