TCP receive copy offload是一种异步IO技术,这种技术使用网络协议栈中的DMA引擎代替CPU来实现copy-to-user的操作,节约了CPU周期。DMA引擎是指数据移动加速(Data Movement Acceleration)引擎,它将某些传输数据的操作从CPU转移到专用硬件,从而可以进行异步传输并减轻CPU负载。
支持这个功能需要开启CONFIG_NET_DMA内核编译选项(硬件方面的支持目前尚不清楚)。DMA接收器会在tcp_recvmsg函数中安装:
1545 int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, 1546 size_t len, int nonblock, int flags, int *addr_len) 1547 { 1548 struct tcp_sock *tp = tcp_sk(sk); 1549 int copied = 0; 1550 u32 peek_seq; 1551 u32 *seq; 1552 unsigned long used; 1553 int err; 1554 int target; /* Read at least this many bytes */ 1555 long timeo; 1556 struct task_struct *user_recv = NULL; 1557 bool copied_early = false; 1558 struct sk_buff *skb; 1559 u32 urg_hole = 0; ... 1596 #ifdef CONFIG_NET_DMA 1597 tp->ucopy.dma_chan = NULL; 1598 preempt_disable(); //禁止内核抢占 1599 skb = skb_peek_tail(&sk->sk_receive_queue); 1600 { 1601 int available = 0; 1602 1603 if (skb) 1604 available = TCP_SKB_CB(skb)->seq + skb->len - (*seq);//计算receive_queue队列中未copy数据的总长度 1605 if ((available < target) && //当前接收队列中未读数据长度无法满足要求 1606 (len > sysctl_tcp_dma_copybreak) && !(flags & MSG_PEEK) && //进程缓存中的空间大于启用DMA copy的下限 1607 !sysctl_tcp_low_latency && //允许较大延迟 1608 net_dma_find_channel()) { //能够找到DMA通道 1609 preempt_enable_no_resched(); //激活内核抢占但不再检查任何被挂起的需调度任务 1610 tp->ucopy.pinned_list = 1611 dma_pin_iovec_pages(msg->msg_iov, len); //固定住用户缓存中len长度的页用于DMA通信 1612 } else { 1613 preempt_enable_no_resched(); 1614 } 1615 } 1616 #endif ... 1748 #ifdef CONFIG_NET_DMA 1749 if (tp->ucopy.dma_chan) { //通过DMA传输过数据 1750 if (tp->rcv_wnd == 0 && //通告窗口为0,意味着接收缓存空间十分紧张 1751 !skb_queue_empty(&sk->sk_async_wait_queue)) { //异步等待队列非空 1752 tcp_service_net_dma(sk, true); //清理全部sk->sk_async_wait_queue中等待DMA传输确认的包 1753 tcp_cleanup_rbuf(sk, copied); 1754 } else 1755 dma_async_issue_pending(tp->ucopy.dma_chan); //使用异步机制等待数据到来 1756 } 1757 #endif 1758 if (copied >= target) { 1759 /* Do not sleep, just process backlog. */ 1760 release_sock(sk); 1761 lock_sock(sk); 1762 } else 1763 sk_wait_data(sk, &timeo); 1764 1765 #ifdef CONFIG_NET_DMA 1766 tcp_service_net_dma(sk, false); /* Don't block */ //在release_sock过程中会有一些包由软中断利用DAM传输数据到用户缓存 1767 tp->ucopy.wakeup = 0; //允许在快速路径中进行early copy 1768 #endif ... 1826 if (!(flags & MSG_TRUNC)) { 1827 #ifdef CONFIG_NET_DMA 1828 if (!tp->ucopy.dma_chan && tp->ucopy.pinned_list) 1829 tp->ucopy.dma_chan = net_dma_find_channel();//查找DMA通道 1830 1831 if (tp->ucopy.dma_chan) { 1832 tp->ucopy.dma_cookie = dma_skb_copy_datagram_iovec( 1833 tp->ucopy.dma_chan, skb, offset, 1834 msg->msg_iov, used, 1835 tp->ucopy.pinned_list);//利用DMA将数据copy到用户缓存空间;这个copy是异步的,此函数返回时数据copy可能尚未完成 1836 1837 if (tp->ucopy.dma_cookie < 0) { //DMA数据传输出现异常 1838 1839 pr_alert("%s: dma_cookie < 0\n", 1840 __func__); 1841 1842 /* Exception. Bailout! */ 1843 if (!copied) 1844 copied = -EFAULT; 1845 break; 1846 } 1847 1848 dma_async_issue_pending(tp->ucopy.dma_chan);//将等待中的事件交由硬件处理 1849 1850 if ((offset + used) == skb->len) //整个skb的数据全部copy了 1851 copied_early = true; 1852 1853 } else 1854 #endif ... 1883 if (!(flags & MSG_PEEK)) { 1884 sk_eat_skb(sk, skb, copied_early); 1885 copied_early = false; 1886 } 1887 continue; 1888 1889 found_fin_ok: 1890 /* Process the FIN. */ 1891 ++*seq; 1892 if (!(flags & MSG_PEEK)) { 1893 sk_eat_skb(sk, skb, copied_early); 1894 copied_early = false; 1895 } .. 1918 #ifdef CONFIG_NET_DMA 1919 tcp_service_net_dma(sk, true); /* Wait for queue to drain */ 1920 tp->ucopy.dma_chan = NULL; 1921 1922 if (tp->ucopy.pinned_list) { 1923 dma_unpin_iovec_pages(tp->ucopy.pinned_list); //释放DMA资源 1924 tp->ucopy.pinned_list = NULL; 1925 } 1926 #endif ...1597-1613:安装DMA接收器。值得注意的是,receive队列中未读数据如果足够多则不会安装接收器;另外, net.ipv4.tcp_low_latency内核选项为必须为0,且不能使用PEEK模式
1837-1845:出现异常则会退出循环,skb则还会留在receive_queue中
1850-1851:如果整个skb的数据由DMA传输完毕,则copied_early的值为true
1884-1885、1893-1894:在调用sk_eat_skb函数删除skb时:
2187 #ifdef CONFIG_NET_DMA 2188 static inline void sk_eat_skb(struct sock *sk, struct sk_buff *skb, bool copied_early) 2189 { 2190 __skb_unlink(skb, &sk->sk_receive_queue); 2191 if (!copied_early) 2192 __kfree_skb(skb); 2193 else 2194 __skb_queue_tail(&sk->sk_async_wait_queue, skb); 2195 } 2196 #else 2197 static inline void sk_eat_skb(struct sock *sk, struct sk_buff *skb, bool copied_early) 2198 { 2199 __skb_unlink(skb, &sk->sk_receive_queue); 2200 __kfree_skb(skb); 2201 } 2202 #endif可见对于DMA传输成功的skb不会释放,只是会将其从receive_queue中转移到async_wait_queue中作为备份,以便DMA传输失败时使用。
1399 #ifdef CONFIG_NET_DMA 1400 static void tcp_service_net_dma(struct sock *sk, bool wait) 1401 { 1402 dma_cookie_t done, used; 1403 dma_cookie_t last_issued; 1404 struct tcp_sock *tp = tcp_sk(sk); 1405 1406 if (!tp->ucopy.dma_chan) 1407 return; 1408 1409 last_issued = tp->ucopy.dma_cookie; 1410 dma_async_issue_pending(tp->ucopy.dma_chan); 1411 1412 do { 1413 if (dma_async_is_tx_complete(tp->ucopy.dma_chan, 1414 last_issued, &done, 1415 &used) == DMA_SUCCESS) { //最后一次DMA传输成功 1416 /* Safe to free early-copied skbs now */ 1417 __skb_queue_purge(&sk->sk_async_wait_queue); //释放整个队列 1418 break; 1419 } else { //否则再传输一次 1420 struct sk_buff *skb; 1421 while ((skb = skb_peek(&sk->sk_async_wait_queue)) && 1422 (dma_async_is_complete(skb->dma_cookie, done, 1423 used) == DMA_SUCCESS)) { 1424 __skb_dequeue(&sk->sk_async_wait_queue); 1425 kfree_skb(skb); 1426 } 1427 } 1428 } while (wait); 1429 } 1430 #endif1428:wait为true则会一直循环到成功传输全部数据为止 ,否则只传输一次
在进程释放sock时,如果DMA接收器已经安装则skb不会被放入prequeue队列中:
1961 int tcp_v4_rcv(struct sk_buff *skb) 1962 { 1963 const struct iphdr *iph; 1964 const struct tcphdr *th; 1965 struct sock *sk; 1966 int ret; 1967 struct net *net = dev_net(skb->dev); ... 2026 if (!sock_owned_by_user(sk)) { 2027 #ifdef CONFIG_NET_DMA 2028 struct tcp_sock *tp = tcp_sk(sk); 2029 if (!tp->ucopy.dma_chan && tp->ucopy.pinned_list)//tp->ucopy.pinned_list不为空意味着有用户进程在通过DMA通道收取数据 2030 tp->ucopy.dma_chan = net_dma_find_channel(); 2031 if (tp->ucopy.dma_chan) 2032 ret = tcp_v4_do_rcv(sk, skb); 2033 else 2034 #endif 2035 { 2036 if (!tcp_prequeue(sk, skb)) 2037 ret = tcp_v4_do_rcv(sk, skb); 2038 } ...在快速处理路径中也会使用DMA收数据:
5076 int tcp_rcv_established(struct sock *sk, struct sk_buff *skb, 5077 const struct tcphdr *th, unsigned int len) 5078 { 5079 struct tcp_sock *tp = tcp_sk(sk); ... 5160 int eaten = 0; 5161 int copied_early = 0; 5162 bool fragstolen = false; 5163 5164 if (tp->copied_seq == tp->rcv_nxt && 5165 len - tcp_header_len <= tp->ucopy.len) { 5166 #ifdef CONFIG_NET_DMA 5167 if (tp->ucopy.task == current && //处于进程上下文 5168 sock_owned_by_user(sk) && //进程正在掉release_sock处理backlog中的包 5169 tcp_dma_try_early_copy(sk, skb, tcp_header_len)) {//利用DMA传送数据到用户缓存 5170 copied_early = 1; 5171 eaten = 1; 5172 } 5173 #endif 5174 if (tp->ucopy.task == current && 5175 sock_owned_by_user(sk) && !copied_early) { //如果DMA传输成功则不用再copy数据了 5176 __set_current_state(TASK_RUNNING); 5177 5178 if (!tcp_copy_to_iovec(sk, skb, tcp_header_len)) 5179 eaten = 1; 5180 } ... 5238 no_ack: 5239 #ifdef CONFIG_NET_DMA 5240 if (copied_early) 5241 __skb_queue_tail(&sk->sk_async_wait_queue, skb);//将DMA传输成功的包放入队列中作为备份 5242 else 5243 #endif ...tcp_dma_try_early_copy函数会使用DMA传输数据:
4935 #ifdef CONFIG_NET_DMA 4936 static bool tcp_dma_try_early_copy(struct sock *sk, struct sk_buff *skb, 4937 int hlen) 4938 { 4939 struct tcp_sock *tp = tcp_sk(sk); 4940 int chunk = skb->len - hlen; 4941 int dma_cookie; 4942 bool copied_early = false; 4943 4944 if (tp->ucopy.wakeup) //不允许在快速路径中进行early copy 4945 return false; 4946 4947 if (!tp->ucopy.dma_chan && tp->ucopy.pinned_list) 4948 tp->ucopy.dma_chan = net_dma_find_channel(); 4949 4950 if (tp->ucopy.dma_chan && skb_csum_unnecessary(skb)) { 4951 4952 dma_cookie = dma_skb_copy_datagram_iovec(tp->ucopy.dma_chan, 4953 skb, hlen, 4954 tp->ucopy.iov, chunk, 4955 tp->ucopy.pinned_list); //利用DMA传输数据 4956 4957 if (dma_cookie < 0) //传输失败 4958 goto out; 4959 4960 tp->ucopy.dma_cookie = dma_cookie; 4961 copied_early = true; 4962 4963 tp->ucopy.len -= chunk; 4964 tp->copied_seq += chunk; 4965 tcp_rcv_space_adjust(sk); //调整接收缓存空间 4966 4967 if ((tp->ucopy.len == 0) || //用户空间已满 4968 (tcp_flag_word(tcp_hdr(skb)) & TCP_FLAG_PSH) || //有PSH标记位 4969 (atomic_read(&sk->sk_rmem_alloc) > (sk->sk_rcvbuf >> 1))) { //TCP接收缓存空间紧张 4970 tp->ucopy.wakeup = 1; //禁用快速路径中的earl copy 4971 sk->sk_data_ready(sk, 0); //唤醒进程收数据 4972 } 4973 } else if (chunk > 0) { //skb中有数据 4974 tp->ucopy.wakeup = 1; //禁用快速路径中的earl copy 4975 sk->sk_data_ready(sk, 0); //唤醒进程收数据 4976 } 4977 out: 4978 return copied_early; 4979 } 4980 #endif /* CONFIG_NET_DMA */在使用DMA传输数据时,虽然 net.ipv4.tcp_low_latency内核选项为0,但prequeue队列会无法使用。不过在DMA传输失败或在慢速处理路径中接收数据时,TCP所使用的数据copy方法与prequeue机制是一样的。