【问题标题】:Using GNU/Linux system call `splice` for zero-copy Socket to Socket data transfers in Haskell在 Haskell 中使用 GNU/Linux 系统调用`splice`进行零拷贝 Socket 到 Socket 数据传输
【发布时间】:2012-04-22 06:23:45
【问题描述】:

更新:尼莫先生的回答帮助解决了这个问题!下面的代码包含修复!请参阅下面的 nb Falsenb True 调用。

还有一个名为 splice 的新 Haskell 包(它具有最知名的套接字到套接字数据传输循环的操作系统特定和可移植实现)

我有以下(Haskell)代码:

#ifdef LINUX_SPLICE
#include <fcntl.h>
{-# LANGUAGE CPP #-}
{-# LANGUAGE ForeignFunctionInterface #-}
#endif

module Network.Socket.Splice (
    Length
  , zeroCopy
  , splice
#ifdef LINUX_SPLICE
  , c_splice
#endif
  ) where

import Data.Word
import Foreign.Ptr

import Network.Socket
import Control.Monad
import Control.Exception
import System.Posix.Types
import System.Posix.IO

#ifdef LINUX_SPLICE
import Data.Int
import Data.Bits
import Unsafe.Coerce
import Foreign.C.Types
import Foreign.C.Error
import System.Posix.Internals
#else
import System.IO
import Foreign.Marshal.Alloc
#endif


zeroCopy :: Bool
zeroCopy =
#ifdef LINUX_SPLICE
  True
#else
  False
#endif


type Length =
#ifdef LINUX_SPLICE
  (#type size_t)
#else
  Int
#endif


-- | The 'splice' function pipes data from
--   one socket to another in a loop.
--   On Linux this happens in kernel space with
--   zero copying between kernel and user spaces.
--   On other operating systems, a portable
--   implementation utilizes a user space buffer
--   allocated with 'mallocBytes'; 'hGetBufSome'
--   and 'hPut' are then used to avoid repeated 
--   tiny allocations as would happen with 'recv'
--   'sendAll' calls from the 'bytestring' package.
splice :: Length -> Socket -> Socket -> IO ()
splice l (MkSocket x _ _ _ _) (MkSocket y _ _ _ _) = do

  let e  = error "splice ended"

#ifdef LINUX_SPLICE

  (r,w) <- createPipe
  print ('+',r,w)
  let s  = Fd x -- source
  let t  = Fd y -- target
  let c  = throwErrnoIfMinus1 "Network.Socket.Splice.splice"
  let u  = unsafeCoerce :: (#type ssize_t) -> (#type size_t)
  let fs = sPLICE_F_MOVE .|. sPLICE_F_MORE
  let nb v = do setNonBlockingFD x v
                setNonBlockingFD y v
  nb False
  finally
    (forever $ do 
       b <- c $ c_splice s nullPtr w nullPtr    l  fs
       if b > 0
         then   c_splice r nullPtr t nullPtr (u b) fs)
         else   e
    (do closeFd r
        closeFd w
        nb True
        print ('-',r,w))

#else

  -- ..    

#endif


#ifdef LINUX_SPLICE
-- SPLICE

-- fcntl.h
-- ssize_t splice(
--   int          fd_in,
--   loff_t*      off_in,
--   int          fd_out,
--   loff_t*      off_out,
--   size_t       len,
--   unsigned int flags
-- );

foreign import ccall "splice"
  c_splice
  :: Fd
  -> Ptr (#type loff_t)
  -> Fd
  -> Ptr (#type loff_t)
  -> (#type size_t)
  -> Word
  -> IO (#type ssize_t)

sPLICE_F_MOVE :: Word
sPLICE_F_MOVE = (#const "SPLICE_F_MOVE")

sPLICE_F_MORE :: Word
sPLICE_F_MORE = (#const "SPLICE_F_MORE")
#endif

注意: 上面的代码现在可以正常工作了!感谢 Nemo,下面的代码不再有效!

我调用splice 上面定义的两个打开和连接的套接字(它们已经用于使用套接字API sendrecv 调用传输最少量的握手数据,或者转换为句柄并与@ 一起使用987654329@ 和 hPut) 我不断收到:

Network.Socket.Splice.splice: resource exhausted (Resource temporarily unavailable)

在第一个c_splice 调用站点:c_splice 返回-1 并将一些errno 设置为一个值(可能是EAGAIN),当查找时读取为resource exhausted | resource temporarily unavailable

我测试了使用不同的Length 值调用splice10248192

【问题讨论】:

  • 每次调用 splice() 时,您的当前版本都会创建一个新管道。如果您总是移动大块,这是可以的,但对于可能会带来很大开销的小块。我通常创建一个“Splicer”对象来拥有管道,然后使用 from+to 描述符重复调用它来移动数据。
  • @Nemo splice(不是c_splice)实际上是由于forever 而导致的无限循环。我想我应该将splice 重命名为loopSplice 以明确这一点。所以目前它为每个代理连接创建一个管道,而不是每个c_splice 调用。
  • 啊...告诉你我实际上并不了解 Haskell :-)。 (搜索“Haskell non-blocking socket”让我找到了相关来源。)哦,在我可以用函数式语言编码的日子里......
  • @CetinSert 谢谢你的包,它肯定会有用。对于我们不熟悉这个内核特性的人,你能解释一下如何用它做一个 HTTP 1.1 反向代理吗?此示例需要读取和解释用户空间中不同长度的标头,然后将套接字拼接到套接字以获得有限的字节数,然后在用户空间中取回控制权以进行下一个请求。这可能吗?
  • @CetinSert 啊!我确实没有看正确的模块。 splice_c 具有正确的 API 来将用户空间逻辑与内核空间字节复制交错。谢谢你提供这个包,我希望我有时间玩它。

标签: linux sockets haskell network-programming splice


【解决方案1】:

sendfile() 系统调用对您有用吗?如果是这样,您可以使用sendfile package

【讨论】:

  • 谢谢。据此:kerneltrap.org/node/6505splice 是正确的方法,如果双方都是套接字。我错了吗?顺便说一句,尼莫的回答为我解决了这个问题,所以我会坚持我对splice的实施!
  • 我的代码中注释掉的部分包含一段可移植的用户空间代码,使用 mallocByteshGetBufSomehPut,其性能优于 network-bytestringrecv、@987654331 @也一样。我会尽快完善它并把它放在 Hackage 上。我正在开发一个高性能、极其简单、小而干净的代理应用程序,它的设计原则之一是:除非绝对必要,否则不要依赖外部包,尽管sendfile 的依赖列表与大多数其他软件包相比,它太干净了,我见过竞争对手的代理软件也使用splice。 :)
  • sendfile 发送文件。如果源不是文件或目标不是套接字,则它不起作用。 splice 是文件到文件或套接字到套接字零拷贝所需要的。 (虽然它通常不是真正的零拷贝……长篇大论)
  • @Nemo:如果你有更长版本的链接,我很想听听完整的故事。我目前的理解是,复制仍在发生,但紧紧地保存在内核空间的某个地方,而不是跨越内核 - 用户空间边界。
  • @Cetin:我没有实际的参考资料,但这基本上是正确的。 splice(和sendfile)在从文件发送到套接字时只是零拷贝;内核将只传递对页面缓存的引用,并直接从那里获得网卡 DMA。在所有其他情况下,至少会制作一份副本,但您是正确的,这发生在内核空间中,因此可能更快。 (尽管使用小缓冲区的用户空间读+写——即通过 L1 缓存——也可以非常快。通常情况下,测量比预测更容易。)
【解决方案2】:

我不知道 Haskell,但是“资源暂时不可用”是 EAGAIN

默认情况下它看起来像Haskell sets its sockets to non-blocking mode。因此,如果您在没有数据时尝试从其中读取,或者在缓冲区已满时尝试写入其中,您将失败并返回 EAGAIN

弄清楚如何将套接字更改为阻塞模式,我打赌你会解决你的问题。

[更新]

或者,在尝试读取或写入套接字之前调用selectpoll。但是您仍然需要处理EAGAIN,因为在极少数情况下,Linux select 会指示套接字已准备好,而实际上它并没有。

【讨论】:

  • 感谢您的提示!我正在看看它有多大帮助:)
  • 哇,你还不知道 Haskell,但你可以找出确切的路线来帮助解决我的问题,惊人的答案!
  • 它在解决问题的splice 调用周围适当地设置了非阻塞模式。我真的很感谢你的优雅回答。 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-08-22
  • 2012-05-25
  • 2023-03-05
  • 1970-01-01
  • 1970-01-01
  • 2013-06-25
  • 1970-01-01
相关资源
最近更新 更多