Compare commits
6 Commits
master
...
geesefs-0-
Author | SHA1 | Date |
---|---|---|
Vitaliy Filippov | a7dcac672f | |
Vitaliy Filippov | ec521aa7b7 | |
Vitaliy Filippov | 575b70f3fd | |
Vitaliy Filippov | 513d4815ce | |
Vitaliy Filippov | fcabfc89e9 | |
Vitaliy Filippov | f1a2d0d300 |
|
@ -333,7 +333,7 @@ func (c *Connection) readMessage() (*buffer.InMessage, error) {
|
|||
|
||||
// Loop past transient errors.
|
||||
for {
|
||||
// Attempt a reaed.
|
||||
// Attempt a read.
|
||||
err := m.Init(c.dev)
|
||||
|
||||
// Special cases:
|
||||
|
@ -400,7 +400,7 @@ func (c *Connection) ReadOp() (_ context.Context, op interface{}, _ error) {
|
|||
|
||||
// Convert the message to an op.
|
||||
outMsg := c.getOutMessage()
|
||||
op, err = convertInMessage(inMsg, outMsg, c.protocol)
|
||||
op, err = convertInMessage(&c.cfg, inMsg, outMsg, c.protocol)
|
||||
if err != nil {
|
||||
c.putOutMessage(outMsg)
|
||||
return nil, nil, fmt.Errorf("convertInMessage: %v", err)
|
||||
|
@ -480,8 +480,15 @@ func (c *Connection) Reply(ctx context.Context, opErr error) {
|
|||
outMsg := state.outMsg
|
||||
fuseID := inMsg.Header().Unique
|
||||
|
||||
suppressReuse := false
|
||||
if wr, ok := op.(*fuseops.WriteFileOp); ok {
|
||||
suppressReuse = wr.SuppressReuse
|
||||
}
|
||||
|
||||
// Make sure we destroy the messages when we're done.
|
||||
if !suppressReuse {
|
||||
defer c.putInMessage(inMsg)
|
||||
}
|
||||
defer c.putOutMessage(outMsg)
|
||||
|
||||
// Clean up state for this op.
|
||||
|
@ -505,10 +512,16 @@ func (c *Connection) Reply(ctx context.Context, opErr error) {
|
|||
noResponse := c.kernelResponse(outMsg, inMsg.Header().Unique, op, opErr)
|
||||
|
||||
if !noResponse {
|
||||
err := c.writeMessage(outMsg.Bytes())
|
||||
var err error
|
||||
if outMsg.Sglist != nil {
|
||||
_, err = writev(int(c.dev.Fd()), outMsg.Sglist)
|
||||
} else {
|
||||
err = c.writeMessage(outMsg.Bytes())
|
||||
}
|
||||
if err != nil && c.errorLogger != nil {
|
||||
c.errorLogger.Printf("writeMessage: %v %v", err, outMsg.Bytes())
|
||||
}
|
||||
outMsg.Sglist = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ import (
|
|||
//
|
||||
// The caller is responsible for arranging for the message to be destroyed.
|
||||
func convertInMessage(
|
||||
config *MountConfig,
|
||||
inMsg *buffer.InMessage,
|
||||
outMsg *buffer.OutMessage,
|
||||
protocol fusekernel.Protocol) (o interface{}, err error) {
|
||||
|
@ -206,6 +207,15 @@ func convertInMessage(
|
|||
}
|
||||
|
||||
names := inMsg.ConsumeBytes(inMsg.Len())
|
||||
// closed-source macfuse 4.x has broken compatibility with osxfuse 3.x:
|
||||
// it passes an additional 64-bit field (flags) after RenameIn regardless
|
||||
// that we don't enable the support for RENAME_SWAP/RENAME_EXCL
|
||||
// the simplest fix is just to check for the presence of all-zero flags
|
||||
if len(names) >= 8 &&
|
||||
names[0] == 0 && names[1] == 0 && names[2] == 0 && names[3] == 0 &&
|
||||
names[4] == 0 && names[5] == 0 && names[6] == 0 && names[7] == 0 {
|
||||
names = names[8 : ]
|
||||
}
|
||||
// names should be "old\x00new\x00"
|
||||
if len(names) < 4 {
|
||||
return nil, errors.New("Corrupt OpRename")
|
||||
|
@ -271,24 +281,28 @@ func convertInMessage(
|
|||
return nil, errors.New("Corrupt OpRead")
|
||||
}
|
||||
|
||||
if !config.UseVectoredRead {
|
||||
// Use part of the incoming message storage as a read buffer
|
||||
buf := inMsg.GetFree(int(in.Size))
|
||||
to := &fuseops.ReadFileOp{
|
||||
Inode: fuseops.InodeID(inMsg.Header().Nodeid),
|
||||
Handle: fuseops.HandleID(in.Fh),
|
||||
Offset: int64(in.Offset),
|
||||
Dst: buf,
|
||||
OpContext: fuseops.OpContext{Pid: inMsg.Header().Pid},
|
||||
}
|
||||
o = to
|
||||
} else {
|
||||
// Don't allocate any buffers when zero-copy is used
|
||||
to := &fuseops.VectoredReadOp{
|
||||
Inode: fuseops.InodeID(inMsg.Header().Nodeid),
|
||||
Handle: fuseops.HandleID(in.Fh),
|
||||
Offset: int64(in.Offset),
|
||||
Size: int64(in.Size),
|
||||
OpContext: fuseops.OpContext{Pid: inMsg.Header().Pid},
|
||||
}
|
||||
o = to
|
||||
|
||||
readSize := int(in.Size)
|
||||
p := outMsg.GrowNoZero(readSize)
|
||||
if p == nil {
|
||||
return nil, fmt.Errorf("Can't grow for %d-byte read", readSize)
|
||||
}
|
||||
|
||||
sh := (*reflect.SliceHeader)(unsafe.Pointer(&to.Dst))
|
||||
sh.Data = uintptr(p)
|
||||
sh.Len = readSize
|
||||
sh.Cap = readSize
|
||||
|
||||
case fusekernel.OpReaddir:
|
||||
in := (*fusekernel.ReadIn)(inMsg.Consume(fusekernel.ReadInSize(protocol)))
|
||||
|
@ -476,6 +490,7 @@ func convertInMessage(
|
|||
o = to
|
||||
|
||||
readSize := int(in.Size)
|
||||
if readSize > 0 {
|
||||
p := outMsg.GrowNoZero(readSize)
|
||||
if p == nil {
|
||||
return nil, fmt.Errorf("Can't grow for %d-byte read", readSize)
|
||||
|
@ -485,6 +500,9 @@ func convertInMessage(
|
|||
sh.Data = uintptr(p)
|
||||
sh.Len = readSize
|
||||
sh.Cap = readSize
|
||||
} else {
|
||||
to.Dst = nil
|
||||
}
|
||||
|
||||
case fusekernel.OpListxattr:
|
||||
type input fusekernel.ListxattrIn
|
||||
|
@ -705,9 +723,11 @@ func (c *Connection) kernelResponseForOp(
|
|||
}
|
||||
|
||||
case *fuseops.ReadFileOp:
|
||||
// convertInMessage already set up the destination buffer to be at the end
|
||||
// of the out message. We need only shrink to the right size based on how
|
||||
// much the user read.
|
||||
m.Append(o.Dst)
|
||||
m.ShrinkTo(buffer.OutMessageHeaderSize + o.BytesRead)
|
||||
|
||||
case *fuseops.VectoredReadOp:
|
||||
m.Append(o.Data...)
|
||||
m.ShrinkTo(buffer.OutMessageHeaderSize + o.BytesRead)
|
||||
|
||||
case *fuseops.WriteFileOp:
|
||||
|
|
5
debug.go
5
debug.go
|
@ -97,6 +97,11 @@ func describeRequest(op interface{}) (s string) {
|
|||
addComponent("offset %d", typed.Offset)
|
||||
addComponent("%d bytes", len(typed.Dst))
|
||||
|
||||
case *fuseops.VectoredReadOp:
|
||||
addComponent("handle %d", typed.Handle)
|
||||
addComponent("offset %d", typed.Offset)
|
||||
addComponent("%d bytes", typed.Size)
|
||||
|
||||
case *fuseops.WriteFileOp:
|
||||
addComponent("handle %d", typed.Handle)
|
||||
addComponent("offset %d", typed.Offset)
|
||||
|
|
|
@ -654,6 +654,37 @@ type ReadFileOp struct {
|
|||
OpContext OpContext
|
||||
}
|
||||
|
||||
// Vectored read - same as ReadFileOp, but the buffer isn't provided by the library.
|
||||
// The file system returns a list of slices instead.
|
||||
type VectoredReadOp struct {
|
||||
// The file inode that we are reading, and the handle previously returned by
|
||||
// CreateFile or OpenFile when opening that inode.
|
||||
Inode InodeID
|
||||
Handle HandleID
|
||||
|
||||
// The offset within the file at which to read.
|
||||
Offset int64
|
||||
|
||||
// The size of the read.
|
||||
Size int64
|
||||
|
||||
// Set by the file system: data to send back to the client.
|
||||
Data [][]byte
|
||||
|
||||
// Set by the file system: the number of bytes read.
|
||||
//
|
||||
// The FUSE documentation requires that exactly the requested number of bytes
|
||||
// be returned, except in the case of EOF or error (http://goo.gl/ZgfBkF).
|
||||
// This appears to be because it uses file mmapping machinery
|
||||
// (http://goo.gl/SGxnaN) to read a page at a time. It appears to understand
|
||||
// where EOF is by checking the inode size (http://goo.gl/0BkqKD), returned
|
||||
// by a previous call to LookUpInode, GetInodeAttributes, etc.
|
||||
//
|
||||
// If direct IO is enabled, semantics should match those of read(2).
|
||||
BytesRead int
|
||||
OpContext OpContext
|
||||
}
|
||||
|
||||
// Write data to a file previously opened with CreateFile or OpenFile.
|
||||
//
|
||||
// When the user writes data using write(2), the write goes into the page
|
||||
|
@ -711,6 +742,17 @@ type WriteFileOp struct {
|
|||
// because it uses file mmapping machinery (http://goo.gl/SGxnaN) to write a
|
||||
// page at a time.
|
||||
Data []byte
|
||||
|
||||
// Set by the file system: "no reuse" flag.
|
||||
//
|
||||
// By default, the Data buffer is reused by the library, so the file system
|
||||
// must copy the data if it wants to use it later.
|
||||
//
|
||||
// However, if the file system sets this flag to true, the library doesn't
|
||||
// reuse this buffer, so the file system can safely store and use Data slice
|
||||
// without copying memory.
|
||||
SuppressReuse bool
|
||||
|
||||
OpContext OpContext
|
||||
}
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ type FileSystem interface {
|
|||
ReleaseDirHandle(context.Context, *fuseops.ReleaseDirHandleOp) error
|
||||
OpenFile(context.Context, *fuseops.OpenFileOp) error
|
||||
ReadFile(context.Context, *fuseops.ReadFileOp) error
|
||||
VectoredRead(context.Context, *fuseops.VectoredReadOp) error
|
||||
WriteFile(context.Context, *fuseops.WriteFileOp) error
|
||||
SyncFile(context.Context, *fuseops.SyncFileOp) error
|
||||
FlushFile(context.Context, *fuseops.FlushFileOp) error
|
||||
|
@ -190,6 +191,9 @@ func (s *fileSystemServer) handleOp(
|
|||
case *fuseops.ReadFileOp:
|
||||
err = s.fs.ReadFile(ctx, typed)
|
||||
|
||||
case *fuseops.VectoredReadOp:
|
||||
err = s.fs.VectoredRead(ctx, typed)
|
||||
|
||||
case *fuseops.WriteFileOp:
|
||||
err = s.fs.WriteFile(ctx, typed)
|
||||
|
||||
|
|
|
@ -138,6 +138,12 @@ func (fs *NotImplementedFileSystem) ReadFile(
|
|||
return fuse.ENOSYS
|
||||
}
|
||||
|
||||
func (fs *NotImplementedFileSystem) VectoredRead(
|
||||
ctx context.Context,
|
||||
op *fuseops.VectoredReadOp) error {
|
||||
return fuse.ENOSYS
|
||||
}
|
||||
|
||||
func (fs *NotImplementedFileSystem) WriteFile(
|
||||
ctx context.Context,
|
||||
op *fuseops.WriteFileOp) error {
|
||||
|
|
|
@ -42,13 +42,16 @@ func init() {
|
|||
type InMessage struct {
|
||||
remaining []byte
|
||||
storage []byte
|
||||
size int
|
||||
}
|
||||
|
||||
// Initialize with the data read by a single call to r.Read. The first call to
|
||||
// Consume will consume the bytes directly after the fusekernel.InHeader
|
||||
// struct.
|
||||
func (m *InMessage) Init(r io.Reader) error {
|
||||
if m.storage == nil {
|
||||
m.storage = make([]byte, bufSize, bufSize)
|
||||
}
|
||||
n, err := r.Read(m.storage[:])
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -60,6 +63,7 @@ func (m *InMessage) Init(r io.Reader) error {
|
|||
return fmt.Errorf("Unexpectedly read only %d bytes.", n)
|
||||
}
|
||||
|
||||
m.size = n
|
||||
m.remaining = m.storage[headerSize:n]
|
||||
|
||||
// Check the header's length.
|
||||
|
@ -108,3 +112,11 @@ func (m *InMessage) ConsumeBytes(n uintptr) []byte {
|
|||
|
||||
return b
|
||||
}
|
||||
|
||||
// Get the next n bytes after the message to use them as a temporary buffer
|
||||
func (m *InMessage) GetFree(n int) []byte {
|
||||
if n <= 0 || n > len(m.storage)-m.size {
|
||||
return nil
|
||||
}
|
||||
return m.storage[m.size : m.size+n]
|
||||
}
|
||||
|
|
|
@ -17,4 +17,4 @@ package buffer
|
|||
// The maximum fuse write request size that InMessage can acommodate.
|
||||
//
|
||||
// As of kernel 4.20 Linux accepts writes up to 256 pages or 1MiB
|
||||
const MaxWriteSize = 1 << 20
|
||||
const MaxWriteSize = 1 << 17
|
||||
|
|
|
@ -16,7 +16,6 @@ package buffer
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
|
||||
|
@ -33,30 +32,15 @@ const OutMessageHeaderSize = int(unsafe.Sizeof(fusekernel.OutHeader{}))
|
|||
//
|
||||
// Must be initialized with Reset.
|
||||
type OutMessage struct {
|
||||
// The offset into payload to which we're currently writing.
|
||||
payloadOffset int
|
||||
|
||||
header fusekernel.OutHeader
|
||||
payload [MaxReadSize]byte
|
||||
}
|
||||
|
||||
// Make sure that the header and payload are contiguous.
|
||||
func init() {
|
||||
a := unsafe.Offsetof(OutMessage{}.header) + uintptr(OutMessageHeaderSize)
|
||||
b := unsafe.Offsetof(OutMessage{}.payload)
|
||||
|
||||
if a != b {
|
||||
log.Panicf(
|
||||
"header ends at offset %d, but payload starts at offset %d",
|
||||
a, b)
|
||||
}
|
||||
Sglist [][]byte
|
||||
}
|
||||
|
||||
// Reset resets m so that it's ready to be used again. Afterward, the contents
|
||||
// are solely a zeroed fusekernel.OutHeader struct.
|
||||
func (m *OutMessage) Reset() {
|
||||
m.payloadOffset = 0
|
||||
m.header = fusekernel.OutHeader{}
|
||||
m.Sglist = nil
|
||||
}
|
||||
|
||||
// OutHeader returns a pointer to the header at the start of the message.
|
||||
|
@ -69,25 +53,15 @@ func (m *OutMessage) OutHeader() *fusekernel.OutHeader {
|
|||
// insufficient space, it returns nil.
|
||||
func (m *OutMessage) Grow(n int) unsafe.Pointer {
|
||||
p := m.GrowNoZero(n)
|
||||
if p != nil {
|
||||
jacobsa_fuse_memclr(p, uintptr(n))
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// GrowNoZero is equivalent to Grow, except the new segment is not zeroed. Use
|
||||
// with caution!
|
||||
func (m *OutMessage) GrowNoZero(n int) unsafe.Pointer {
|
||||
// Will we overflow the buffer?
|
||||
o := m.payloadOffset
|
||||
if len(m.payload)-o < n {
|
||||
return nil
|
||||
}
|
||||
|
||||
p := unsafe.Pointer(uintptr(unsafe.Pointer(&m.payload)) + uintptr(o))
|
||||
m.payloadOffset = o + n
|
||||
|
||||
b := make([]byte, n)
|
||||
m.Append(b)
|
||||
p := unsafe.Pointer(&b[0])
|
||||
return p
|
||||
}
|
||||
|
||||
|
@ -100,51 +74,63 @@ func (m *OutMessage) ShrinkTo(n int) {
|
|||
n,
|
||||
m.Len()))
|
||||
}
|
||||
|
||||
m.payloadOffset = n - OutMessageHeaderSize
|
||||
if n == OutMessageHeaderSize {
|
||||
m.Sglist = nil
|
||||
} else {
|
||||
i := 1
|
||||
n -= OutMessageHeaderSize
|
||||
for len(m.Sglist) > i && n >= len(m.Sglist[i]) {
|
||||
n -= len(m.Sglist[i])
|
||||
i++
|
||||
}
|
||||
if n > 0 {
|
||||
m.Sglist[i] = m.Sglist[i][0 : n]
|
||||
i++
|
||||
}
|
||||
m.Sglist = m.Sglist[0 : i]
|
||||
}
|
||||
}
|
||||
|
||||
// Append is equivalent to growing by len(src), then copying src over the new
|
||||
// segment. Int panics if there is not enough room available.
|
||||
func (m *OutMessage) Append(src []byte) {
|
||||
p := m.GrowNoZero(len(src))
|
||||
if p == nil {
|
||||
panic(fmt.Sprintf("Can't grow %d bytes", len(src)))
|
||||
func (m *OutMessage) Append(src ...[]byte) {
|
||||
if m.Sglist == nil {
|
||||
m.Sglist = append(m.Sglist, nil)
|
||||
*(*reflect.SliceHeader)(unsafe.Pointer(&m.Sglist[0])) = reflect.SliceHeader{
|
||||
Data: uintptr(unsafe.Pointer(&m.header)),
|
||||
Len: OutMessageHeaderSize,
|
||||
Cap: OutMessageHeaderSize,
|
||||
}
|
||||
|
||||
sh := (*reflect.SliceHeader)(unsafe.Pointer(&src))
|
||||
jacobsa_fuse_memmove(p, unsafe.Pointer(sh.Data), uintptr(sh.Len))
|
||||
|
||||
}
|
||||
m.Sglist = append(m.Sglist, src...)
|
||||
return
|
||||
}
|
||||
|
||||
// AppendString is like Append, but accepts string input.
|
||||
func (m *OutMessage) AppendString(src string) {
|
||||
p := m.GrowNoZero(len(src))
|
||||
if p == nil {
|
||||
panic(fmt.Sprintf("Can't grow %d bytes", len(src)))
|
||||
}
|
||||
|
||||
sh := (*reflect.StringHeader)(unsafe.Pointer(&src))
|
||||
jacobsa_fuse_memmove(p, unsafe.Pointer(sh.Data), uintptr(sh.Len))
|
||||
|
||||
m.Append([]byte(src))
|
||||
return
|
||||
}
|
||||
|
||||
// Len returns the current size of the message, including the leading header.
|
||||
func (m *OutMessage) Len() int {
|
||||
return OutMessageHeaderSize + m.payloadOffset
|
||||
if m.Sglist == nil {
|
||||
return OutMessageHeaderSize
|
||||
}
|
||||
r := 0
|
||||
for _, b := range m.Sglist {
|
||||
r += len(b)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Bytes returns a reference to the current contents of the buffer, including
|
||||
// the leading header.
|
||||
// Bytes returns a byte slice containing the current header.
|
||||
func (m *OutMessage) Bytes() []byte {
|
||||
l := m.Len()
|
||||
l := OutMessageHeaderSize
|
||||
sh := reflect.SliceHeader{
|
||||
Data: uintptr(unsafe.Pointer(&m.header)),
|
||||
Len: l,
|
||||
Cap: l,
|
||||
}
|
||||
|
||||
return *(*[]byte)(unsafe.Pointer(&sh))
|
||||
}
|
||||
|
|
|
@ -107,7 +107,10 @@ func TestOutMessageAppend(t *testing.T) {
|
|||
t.Errorf("om.Len() = %d, want %d", got, want)
|
||||
}
|
||||
|
||||
b := om.Bytes()
|
||||
b := []byte(nil)
|
||||
for i := 0; i < len(om.Sglist); i++ {
|
||||
b = append(b, om.Sglist[i]...)
|
||||
}
|
||||
if got, want := len(b), wantLen; got != want {
|
||||
t.Fatalf("len(om.Bytes()) = %d, want %d", got, want)
|
||||
}
|
||||
|
@ -137,7 +140,10 @@ func TestOutMessageAppendString(t *testing.T) {
|
|||
t.Errorf("om.Len() = %d, want %d", got, want)
|
||||
}
|
||||
|
||||
b := om.Bytes()
|
||||
b := []byte(nil)
|
||||
for i := 0; i < len(om.Sglist); i++ {
|
||||
b = append(b, om.Sglist[i]...)
|
||||
}
|
||||
if got, want := len(b), wantLen; got != want {
|
||||
t.Fatalf("len(om.Bytes()) = %d, want %d", got, want)
|
||||
}
|
||||
|
@ -168,7 +174,10 @@ func TestOutMessageShrinkTo(t *testing.T) {
|
|||
t.Errorf("om.Len() = %d, want %d", got, want)
|
||||
}
|
||||
|
||||
b := om.Bytes()
|
||||
b := []byte(nil)
|
||||
for i := 0; i < len(om.Sglist); i++ {
|
||||
b = append(b, om.Sglist[i]...)
|
||||
}
|
||||
if got, want := len(b), wantLen; got != want {
|
||||
t.Fatalf("len(om.Bytes()) = %d, want %d", got, want)
|
||||
}
|
||||
|
@ -283,7 +292,10 @@ func TestOutMessageGrow(t *testing.T) {
|
|||
t.Errorf("om.Len() = %d, want %d", got, want)
|
||||
}
|
||||
|
||||
b := om.Bytes()
|
||||
b := []byte(nil)
|
||||
for i := 0; i < len(om.Sglist); i++ {
|
||||
b = append(b, om.Sglist[i]...)
|
||||
}
|
||||
if got, want := len(b), wantLen; got != want {
|
||||
t.Fatalf("len(om.Len()) = %d, want %d", got, want)
|
||||
}
|
||||
|
@ -304,7 +316,7 @@ func BenchmarkOutMessageReset(b *testing.B) {
|
|||
om.Reset()
|
||||
}
|
||||
|
||||
b.SetBytes(int64(unsafe.Offsetof(om.payload)))
|
||||
b.SetBytes(int64(om.Len()))
|
||||
})
|
||||
|
||||
// Many megabytes worth of buffers, which should defeat the CPU cache.
|
||||
|
@ -321,7 +333,7 @@ func BenchmarkOutMessageReset(b *testing.B) {
|
|||
oms[i%numMessages].Reset()
|
||||
}
|
||||
|
||||
b.SetBytes(int64(unsafe.Offsetof(oms[0].payload)))
|
||||
b.SetBytes(int64(oms[0].Len()))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
82
mount.go
82
mount.go
|
@ -17,7 +17,10 @@ package fuse
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Server is an interface for any type that knows how to serve ops read from a
|
||||
|
@ -93,3 +96,82 @@ func Mount(
|
|||
|
||||
return mfs, nil
|
||||
}
|
||||
|
||||
func fusermount(binary string, argv []string, additionalEnv []string, wait bool) (*os.File, error) {
|
||||
// Create a socket pair.
|
||||
fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Socketpair: %v", err)
|
||||
}
|
||||
|
||||
// Wrap the sockets into os.File objects that we will pass off to fusermount.
|
||||
writeFile := os.NewFile(uintptr(fds[0]), "fusermount-child-writes")
|
||||
defer writeFile.Close()
|
||||
|
||||
readFile := os.NewFile(uintptr(fds[1]), "fusermount-parent-reads")
|
||||
defer readFile.Close()
|
||||
|
||||
// Start fusermount/mount_macfuse/mount_osxfuse.
|
||||
cmd := exec.Command(binary, argv...)
|
||||
cmd.Env = append(os.Environ(), "_FUSE_COMMFD=3")
|
||||
cmd.Env = append(cmd.Env, additionalEnv...)
|
||||
cmd.ExtraFiles = []*os.File{writeFile}
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
// Run the command.
|
||||
if wait {
|
||||
err = cmd.Run()
|
||||
} else {
|
||||
err = cmd.Start()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("running %v: %v", binary, err)
|
||||
}
|
||||
|
||||
// Wrap the socket file in a connection.
|
||||
c, err := net.FileConn(readFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FileConn: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
// We expect to have a Unix domain socket.
|
||||
uc, ok := c.(*net.UnixConn)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Expected UnixConn, got %T", c)
|
||||
}
|
||||
|
||||
// Read a message.
|
||||
buf := make([]byte, 32) // expect 1 byte
|
||||
oob := make([]byte, 32) // expect 24 bytes
|
||||
_, oobn, _, _, err := uc.ReadMsgUnix(buf, oob)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ReadMsgUnix: %v", err)
|
||||
}
|
||||
|
||||
// Parse the message.
|
||||
scms, err := syscall.ParseSocketControlMessage(oob[:oobn])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ParseSocketControlMessage: %v", err)
|
||||
}
|
||||
|
||||
// We expect one message.
|
||||
if len(scms) != 1 {
|
||||
return nil, fmt.Errorf("expected 1 SocketControlMessage; got scms = %#v", scms)
|
||||
}
|
||||
|
||||
scm := scms[0]
|
||||
|
||||
// Pull out the FD returned by fusermount
|
||||
gotFds, err := syscall.ParseUnixRights(&scm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("syscall.ParseUnixRights: %v", err)
|
||||
}
|
||||
|
||||
if len(gotFds) != 1 {
|
||||
return nil, fmt.Errorf("wanted 1 fd; got %#v", gotFds)
|
||||
}
|
||||
|
||||
// Turn the FD into an os.File.
|
||||
return os.NewFile(uintptr(gotFds[0]), "/dev/fuse"), nil
|
||||
}
|
||||
|
|
|
@ -156,6 +156,11 @@ type MountConfig struct {
|
|||
// actually utilise any form of qualifiable UNIX permissions.
|
||||
DisableDefaultPermissions bool
|
||||
|
||||
// Use VectoredReadOp instead of ReadFileOp.
|
||||
// Vectored read allows file systems to reduce memory copying overhead if
|
||||
// the data is already in memory when they return it to FUSE.
|
||||
UseVectoredRead bool
|
||||
|
||||
// OS X only.
|
||||
//
|
||||
// The name of the mounted volume, as displayed in the Finder. If empty, a
|
||||
|
|
|
@ -40,6 +40,10 @@ type osxfuseInstallation struct {
|
|||
|
||||
// Environment variable used to pass the "called by library" flag.
|
||||
LibVar string
|
||||
|
||||
// Open device manually (false) or receive the FD through a UNIX socket,
|
||||
// like with fusermount (true)
|
||||
UseCommFD bool
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -51,6 +55,7 @@ var (
|
|||
Mount: "/Library/Filesystems/macfuse.fs/Contents/Resources/mount_macfuse",
|
||||
DaemonVar: "_FUSE_DAEMON_PATH",
|
||||
LibVar: "_FUSE_CALL_BY_LIB",
|
||||
UseCommFD: true,
|
||||
},
|
||||
|
||||
// v3
|
||||
|
@ -106,6 +111,36 @@ func openOSXFUSEDev(devPrefix string) (dev *os.File, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
func convertMountArgs(daemonVar string, libVar string,
|
||||
cfg *MountConfig) ([]string, []string, error) {
|
||||
|
||||
// The mount helper doesn't understand any escaping.
|
||||
for k, v := range cfg.toMap() {
|
||||
if strings.Contains(k, ",") || strings.Contains(v, ",") {
|
||||
return nil, nil, fmt.Errorf(
|
||||
"mount options cannot contain commas on darwin: %q=%q",
|
||||
k,
|
||||
v)
|
||||
}
|
||||
}
|
||||
|
||||
env := []string{ libVar+"=" }
|
||||
if daemonVar != "" {
|
||||
env = append(env, daemonVar+"="+os.Args[0])
|
||||
}
|
||||
argv := []string{
|
||||
"-o", cfg.toOptionsString(),
|
||||
// Tell osxfuse-kext how large our buffer is. It must split
|
||||
// writes larger than this into multiple writes.
|
||||
//
|
||||
// OSXFUSE seems to ignore InitResponse.MaxWrite, and uses
|
||||
// this instead.
|
||||
"-o", "iosize="+strconv.FormatUint(buffer.MaxWriteSize, 10),
|
||||
}
|
||||
|
||||
return argv, env, nil
|
||||
}
|
||||
|
||||
func callMount(
|
||||
bin string,
|
||||
daemonVar string,
|
||||
|
@ -115,39 +150,21 @@ func callMount(
|
|||
dev *os.File,
|
||||
ready chan<- error) error {
|
||||
|
||||
// The mount helper doesn't understand any escaping.
|
||||
for k, v := range cfg.toMap() {
|
||||
if strings.Contains(k, ",") || strings.Contains(v, ",") {
|
||||
return fmt.Errorf(
|
||||
"mount options cannot contain commas on darwin: %q=%q",
|
||||
k,
|
||||
v)
|
||||
}
|
||||
argv, env, err := convertMountArgs(daemonVar, libVar, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Call the mount helper, passing in the device file and saving output into a
|
||||
// buffer.
|
||||
cmd := exec.Command(
|
||||
bin,
|
||||
"-o", cfg.toOptionsString(),
|
||||
// Tell osxfuse-kext how large our buffer is. It must split
|
||||
// writes larger than this into multiple writes.
|
||||
//
|
||||
// OSXFUSE seems to ignore InitResponse.MaxWrite, and uses
|
||||
// this instead.
|
||||
"-o", "iosize="+strconv.FormatUint(buffer.MaxWriteSize, 10),
|
||||
argv = append(argv,
|
||||
// refers to fd passed in cmd.ExtraFiles
|
||||
"3",
|
||||
dir,
|
||||
)
|
||||
cmd := exec.Command(bin, argv...)
|
||||
cmd.ExtraFiles = []*os.File{dev}
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, libVar+"=")
|
||||
|
||||
daemon := os.Args[0]
|
||||
if daemonVar != "" {
|
||||
cmd.Env = append(cmd.Env, daemonVar+"="+daemon)
|
||||
}
|
||||
cmd.Env = env
|
||||
|
||||
var buf bytes.Buffer
|
||||
cmd.Stdout = &buf
|
||||
|
@ -174,6 +191,23 @@ func callMount(
|
|||
return nil
|
||||
}
|
||||
|
||||
func callMountCommFD(
|
||||
bin string,
|
||||
daemonVar string,
|
||||
libVar string,
|
||||
dir string,
|
||||
cfg *MountConfig) (*os.File, error) {
|
||||
|
||||
argv, env, err := convertMountArgs(daemonVar, libVar, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
env = append(env, "_FUSE_COMMVERS=2")
|
||||
argv = append(argv, dir)
|
||||
|
||||
return fusermount(bin, argv, env, false)
|
||||
}
|
||||
|
||||
// Begin the process of mounting at the given directory, returning a connection
|
||||
// to the kernel. Mounting continues in the background, and is complete when an
|
||||
// error is written to the supplied channel. The file system may need to
|
||||
|
@ -189,6 +223,16 @@ func mount(
|
|||
continue
|
||||
}
|
||||
|
||||
if loc.UseCommFD {
|
||||
// Call the mount binary with the device.
|
||||
ready <- nil
|
||||
dev, err = callMountCommFD(loc.Mount, loc.DaemonVar, loc.LibVar, dir, cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("callMount: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Open the device.
|
||||
dev, err = openOSXFUSEDev(loc.DevicePrefix)
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
package fuse
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
@ -23,92 +21,6 @@ func findFusermount() (string, error) {
|
|||
return path, nil
|
||||
}
|
||||
|
||||
func fusermount(dir string, cfg *MountConfig) (*os.File, error) {
|
||||
// Create a socket pair.
|
||||
fds, err := syscall.Socketpair(syscall.AF_FILE, syscall.SOCK_STREAM, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Socketpair: %v", err)
|
||||
}
|
||||
|
||||
// Wrap the sockets into os.File objects that we will pass off to fusermount.
|
||||
writeFile := os.NewFile(uintptr(fds[0]), "fusermount-child-writes")
|
||||
defer writeFile.Close()
|
||||
|
||||
readFile := os.NewFile(uintptr(fds[1]), "fusermount-parent-reads")
|
||||
defer readFile.Close()
|
||||
|
||||
// Start fusermount, passing it a buffer in which to write stderr.
|
||||
var stderr bytes.Buffer
|
||||
|
||||
fusermount, err := findFusermount()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd := exec.Command(
|
||||
fusermount,
|
||||
"-o", cfg.toOptionsString(),
|
||||
"--",
|
||||
dir,
|
||||
)
|
||||
|
||||
cmd.Env = append(os.Environ(), "_FUSE_COMMFD=3")
|
||||
cmd.ExtraFiles = []*os.File{writeFile}
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
// Run the command.
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("running fusermount: %v\n\nstderr:\n%s", err, stderr.Bytes())
|
||||
}
|
||||
|
||||
// Wrap the socket file in a connection.
|
||||
c, err := net.FileConn(readFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FileConn: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
// We expect to have a Unix domain socket.
|
||||
uc, ok := c.(*net.UnixConn)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Expected UnixConn, got %T", c)
|
||||
}
|
||||
|
||||
// Read a message.
|
||||
buf := make([]byte, 32) // expect 1 byte
|
||||
oob := make([]byte, 32) // expect 24 bytes
|
||||
_, oobn, _, _, err := uc.ReadMsgUnix(buf, oob)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ReadMsgUnix: %v", err)
|
||||
}
|
||||
|
||||
// Parse the message.
|
||||
scms, err := syscall.ParseSocketControlMessage(oob[:oobn])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ParseSocketControlMessage: %v", err)
|
||||
}
|
||||
|
||||
// We expect one message.
|
||||
if len(scms) != 1 {
|
||||
return nil, fmt.Errorf("expected 1 SocketControlMessage; got scms = %#v", scms)
|
||||
}
|
||||
|
||||
scm := scms[0]
|
||||
|
||||
// Pull out the FD returned by fusermount
|
||||
gotFds, err := syscall.ParseUnixRights(&scm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("syscall.ParseUnixRights: %v", err)
|
||||
}
|
||||
|
||||
if len(gotFds) != 1 {
|
||||
return nil, fmt.Errorf("wanted 1 fd; got %#v", gotFds)
|
||||
}
|
||||
|
||||
// Turn the FD into an os.File.
|
||||
return os.NewFile(uintptr(gotFds[0]), "/dev/fuse"), nil
|
||||
}
|
||||
|
||||
func enableFunc(flag uintptr) func(uintptr) uintptr {
|
||||
return func(v uintptr) uintptr {
|
||||
return v | flag
|
||||
|
@ -198,7 +110,16 @@ func mount(dir string, cfg *MountConfig, ready chan<- error) (*os.File, error) {
|
|||
// have the CAP_SYS_ADMIN capability.
|
||||
dev, err := directmount(dir, cfg)
|
||||
if err == errFallback {
|
||||
return fusermount(dir, cfg)
|
||||
fusermountPath, err := findFusermount()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
argv := []string{
|
||||
"-o", cfg.toOptionsString(),
|
||||
"--",
|
||||
dir,
|
||||
}
|
||||
return fusermount(fusermountPath, argv, []string{}, true)
|
||||
}
|
||||
return dev, err
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"github.com/jacobsa/fuse"
|
||||
"github.com/jacobsa/fuse/samples/readbenchfs"
|
||||
)
|
||||
|
||||
var fMountPoint = flag.String("mount_point", "", "Path to mount point.")
|
||||
var fReadOnly = flag.Bool("read_only", false, "Mount in read-only mode.")
|
||||
var fVectored = flag.Bool("vectored", false, "Use vectored read.")
|
||||
var fDebug = flag.Bool("debug", false, "Enable debug logging.")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
server, err := readbenchfs.NewReadBenchServer()
|
||||
if err != nil {
|
||||
log.Fatalf("makeFS: %v", err)
|
||||
}
|
||||
|
||||
// Mount the file system.
|
||||
if *fMountPoint == "" {
|
||||
log.Fatalf("You must set --mount_point.")
|
||||
}
|
||||
|
||||
cfg := &fuse.MountConfig{
|
||||
ReadOnly: *fReadOnly,
|
||||
UseVectoredRead: *fVectored,
|
||||
}
|
||||
|
||||
if *fDebug {
|
||||
cfg.DebugLogger = log.New(os.Stderr, "fuse: ", 0)
|
||||
}
|
||||
|
||||
mfs, err := fuse.Mount(*fMountPoint, server, cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Mount: %v", err)
|
||||
}
|
||||
|
||||
// Wait for it to be unmounted.
|
||||
if err = mfs.Join(context.Background()); err != nil {
|
||||
log.Fatalf("Join: %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
// Copyright 2021 Vitaliy Filippov
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readbenchfs
|
||||
|
||||
import (
|
||||
"golang.org/x/net/context"
|
||||
"os"
|
||||
"io"
|
||||
"math/rand"
|
||||
|
||||
"github.com/jacobsa/fuse"
|
||||
"github.com/jacobsa/fuse/fuseops"
|
||||
"github.com/jacobsa/fuse/fuseutil"
|
||||
)
|
||||
|
||||
type readBenchFS struct {
|
||||
fuseutil.NotImplementedFileSystem
|
||||
buf []byte
|
||||
}
|
||||
|
||||
const FILE_SIZE = 1024*1024*1024*1024
|
||||
|
||||
var _ fuseutil.FileSystem = &readBenchFS{}
|
||||
|
||||
func NewReadBenchServer() (server fuse.Server, err error) {
|
||||
// 1 GB of random data to exceed CPU cache
|
||||
buf := make([]byte, 1024*1024*1024)
|
||||
rand.Read(buf)
|
||||
server = fuseutil.NewFileSystemServer(&readBenchFS{
|
||||
buf: buf,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (fs *readBenchFS) StatFS(
|
||||
ctx context.Context,
|
||||
op *fuseops.StatFSOp) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *readBenchFS) LookUpInode(
|
||||
ctx context.Context,
|
||||
op *fuseops.LookUpInodeOp) error {
|
||||
if op.Name == "test" {
|
||||
op.Entry = fuseops.ChildInodeEntry{
|
||||
Child: 2,
|
||||
Attributes: fuseops.InodeAttributes{
|
||||
Size: FILE_SIZE,
|
||||
Nlink: 1,
|
||||
Mode: 0444,
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fuse.ENOENT
|
||||
}
|
||||
|
||||
func (fs *readBenchFS) GetInodeAttributes(
|
||||
ctx context.Context,
|
||||
op *fuseops.GetInodeAttributesOp) error {
|
||||
if op.Inode == 1 {
|
||||
op.Attributes = fuseops.InodeAttributes{
|
||||
Nlink: 1,
|
||||
Mode: 0755 | os.ModeDir,
|
||||
}
|
||||
return nil
|
||||
} else if op.Inode == 2 {
|
||||
op.Attributes = fuseops.InodeAttributes{
|
||||
Size: FILE_SIZE,
|
||||
Nlink: 1,
|
||||
Mode: 0444,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fuse.ENOENT
|
||||
}
|
||||
|
||||
func (fs *readBenchFS) OpenDir(
|
||||
ctx context.Context,
|
||||
op *fuseops.OpenDirOp) error {
|
||||
// Allow opening any directory.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *readBenchFS) ReadDir(
|
||||
ctx context.Context,
|
||||
op *fuseops.ReadDirOp) error {
|
||||
if op.Inode != 1 {
|
||||
return fuse.ENOENT
|
||||
}
|
||||
if op.Offset > 0 {
|
||||
return nil
|
||||
}
|
||||
entries := []fuseutil.Dirent{
|
||||
fuseutil.Dirent{
|
||||
Offset: 1,
|
||||
Inode: 2,
|
||||
Name: "test",
|
||||
Type: fuseutil.DT_File,
|
||||
},
|
||||
}
|
||||
for _, e := range entries[op.Offset:] {
|
||||
n := fuseutil.WriteDirent(op.Dst[op.BytesRead:], e)
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
op.BytesRead += n
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *readBenchFS) OpenFile(
|
||||
ctx context.Context,
|
||||
op *fuseops.OpenFileOp) error {
|
||||
// Allow opening any file.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *readBenchFS) ReadFile(
|
||||
ctx context.Context,
|
||||
op *fuseops.ReadFileOp) error {
|
||||
if op.Offset > FILE_SIZE {
|
||||
return io.EOF
|
||||
}
|
||||
end := op.Offset+int64(len(op.Dst))
|
||||
if end > FILE_SIZE {
|
||||
end = FILE_SIZE
|
||||
}
|
||||
buflen := int64(len(fs.buf))
|
||||
for pos := op.Offset; pos < end; {
|
||||
s := pos % buflen
|
||||
e := buflen
|
||||
if e-s > end-pos {
|
||||
e = s+end-pos
|
||||
}
|
||||
copy(op.Dst[pos-op.Offset : ], fs.buf[s : ])
|
||||
pos = op.Offset+e
|
||||
}
|
||||
op.BytesRead = int(end-op.Offset)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *readBenchFS) VectoredRead(
|
||||
ctx context.Context,
|
||||
op *fuseops.VectoredReadOp) error {
|
||||
if op.Offset > FILE_SIZE {
|
||||
return io.EOF
|
||||
}
|
||||
end := op.Offset+op.Size
|
||||
if end > FILE_SIZE {
|
||||
end = FILE_SIZE
|
||||
}
|
||||
buflen := int64(len(fs.buf))
|
||||
for pos := op.Offset; pos < end; {
|
||||
s := pos % buflen
|
||||
e := buflen
|
||||
if e-s > end-pos {
|
||||
e = s+end-pos
|
||||
}
|
||||
op.Data = append(op.Data, fs.buf[s : e])
|
||||
pos = op.Offset+e
|
||||
}
|
||||
op.BytesRead = int(end-op.Offset)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *readBenchFS) ReleaseDirHandle(
|
||||
ctx context.Context,
|
||||
op *fuseops.ReleaseDirHandleOp) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *readBenchFS) GetXattr(
|
||||
ctx context.Context,
|
||||
op *fuseops.GetXattrOp) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *readBenchFS) ListXattr(
|
||||
ctx context.Context,
|
||||
op *fuseops.ListXattrOp) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *readBenchFS) ForgetInode(
|
||||
ctx context.Context,
|
||||
op *fuseops.ForgetInodeOp) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *readBenchFS) ReleaseFileHandle(
|
||||
ctx context.Context,
|
||||
op *fuseops.ReleaseFileHandleOp) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *readBenchFS) FlushFile(
|
||||
ctx context.Context,
|
||||
op *fuseops.FlushFileOp) error {
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package fuse
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func writev(fd int, packet [][]byte) (n int, err error) {
|
||||
iovecs := make([]syscall.Iovec, 0, len(packet))
|
||||
for _, v := range packet {
|
||||
if len(v) == 0 {
|
||||
continue
|
||||
}
|
||||
vec := syscall.Iovec{
|
||||
Base: &v[0],
|
||||
}
|
||||
vec.SetLen(len(v))
|
||||
iovecs = append(iovecs, vec)
|
||||
}
|
||||
n1, _, e1 := syscall.Syscall(
|
||||
syscall.SYS_WRITEV,
|
||||
uintptr(fd), uintptr(unsafe.Pointer(&iovecs[0])), uintptr(len(iovecs)),
|
||||
)
|
||||
n = int(n1)
|
||||
if e1 != 0 {
|
||||
err = syscall.Errno(e1)
|
||||
}
|
||||
return
|
||||
}
|
Loading…
Reference in New Issue