对于sendto,Linux 内核最后会调用udp_sendmsg,大概的调用堆栈是
udp_sendmsg
security_socket_sendmsg
__sock_sendmsg
sock_sendmsg
sendto
在sento里面,会根据socket的模式把一个标志传递给内核:
if (sock->file->f_flags & O_NONBLOCK)
flags |= MSG_DONTWAIT;
这个flags的值在后面会有用处
udp_sendmsg的第三个参数msg保存着等待发送的数据,即msg.msg_iov.iov_base,这个指针目前仍然是最上面用户层传递进来的函数临时变量,
udp_sendmsg里面有大量的细节暂且不管,涉及数据拷贝和发动的是ip_append_data,而它调用的是__ip_append_data,ip_append_data会传递一个getfrag函数指针作为回调,getfrag的目的就是执行发送数据的拷贝。getfrag在哪里被执行呢,继续往下看
在__ip_append_data里,代码分成2个执行分支,1
1调用ip_ufo_append_data,
2调用sock_alloc_send_skb
如果走分支2,直接调用sock_alloc_send_skb,sock_alloc_send_skb只有一个调用sock_alloc_send_pskb,sock_alloc_send_pskb是阻塞与非阻塞的关键所在,此函数首先调用timeo = sock_sndtimeo(sk, noblock);
noblock由最初的flags决定,意思是阻塞模式的话会一直等到到超时为止,如果是非阻塞则不等待,那么在哪里等待呢,下面一段代码展示了基本的逻辑
if (atomic_read(&sk->sk_wmem_alloc) >= sk->sk_sndbuf) {
set_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);
set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
err = -EAGAIN;
if (!timeo)
goto failure;
if (signal_pending(current))
goto interrupted;
timeo = sock_wait_for_wmem(sk, timeo);
continue;
}
意思是需要发送的数据长度是大于发送缓存情况下,并且timeo为0,返回EAGAIN给用户层,如果timeo非0,则调用sock_wait_for_wmem一直检查发送缓存的情况,直到发送缓存足够长或者超时。
在非阻塞情况下,sock_alloc_send_skb返回EAGAIN,会跳转到error。而在阻塞情况下如果返回成功,后面才会调用getfrag执行真正的数据拷贝。
补充一下,不管阻塞还是非阻塞模式,在申请skb之前都会调用lock_sock,从而可能进入锁竞争状态,其实也不绝对的非阻塞。
lock_sock这个函数也值得研究,他既调用了自旋锁,又调用了互斥锁。
除了申请kbuffer存在阻塞,发现还需要研究一下真正send数据的时候存在阻塞,发送数据会从udp->ip分层的往下逐层进行调用,最后到达dev,即网络驱动。里面细节太多,主要会调用下面几个函数:
ip_send_skb 》 ip_local_out 》ip_output 》ip_finish_output2 》__dev_queue_xmit
最后调用的是__dev_queue_xmit,这个函数比较复杂,如果想看详细的分析可以看这里http://shaojiashuai123456.iteye.com/blog/842236,
__dev_queue_xmit 获取逻辑设备对象,然后判断其是否有发送队列,如果有发送队列则调用__dev_xmit_skb,并依次调用__qdisc_run-》qdisc_restart->sch_direct_xmit->dev_hard_start_xmit
dev_hard_start_xmit 直接调用的是 rc = ops->ndo_start_xmit(skb, dev);
ndo_start_xmit则属于驱动层的调用,典型的是ethernet,对应的函数是cvm_oct_xmit,驱动层会把这个skb放入等待发送队列,然后直接返回NETDEV_TX_OK。
从这里可以看到,不管阻塞还是非阻塞send,都会经历漫长的调用堆栈,知道驱动层把skb放入发送队列,才会返回,其中会有多次对rcu锁的申请。