Skip to main content
โšก Calmops

System Calls and Low-Level Programming in Go

System Calls and Low-Level Programming in Go

Introduction

System calls provide direct access to OS functionality. This guide covers using the syscall package and low-level programming techniques in Go.

Low-level programming enables performance optimization and direct OS interaction when needed.

System Calls Basics

File Operations via Syscalls

package main

import (
	"fmt"
	"syscall"
)

// OpenFile opens a file using syscall
func OpenFile(path string) (int, error) {
	fd, err := syscall.Open(path, syscall.O_RDONLY, 0)
	if err != nil {
		return -1, fmt.Errorf("open failed: %w", err)
	}
	return fd, nil
}

// CloseFile closes a file descriptor
func CloseFile(fd int) error {
	return syscall.Close(fd)
}

// ReadFile reads from file descriptor
func ReadFile(fd int, buf []byte) (int, error) {
	n, err := syscall.Read(fd, buf)
	if err != nil {
		return 0, fmt.Errorf("read failed: %w", err)
	}
	return n, nil
}

// WriteFile writes to file descriptor
func WriteFile(fd int, data []byte) (int, error) {
	n, err := syscall.Write(fd, data)
	if err != nil {
		return 0, fmt.Errorf("write failed: %w", err)
	}
	return n, nil
}

// Example usage
func SyscallExample() {
	fd, err := OpenFile("/etc/hostname")
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	defer CloseFile(fd)

	buf := make([]byte, 256)
	n, _ := ReadFile(fd, buf)
	fmt.Printf("Read %d bytes\n", n)
}

Good: Proper Low-Level Implementation

package main

import (
	"fmt"
	"syscall"
	"unsafe"
)

// SystemInfo provides system information
type SystemInfo struct {
	Uptime    int64
	Loads     [3]uint64
	TotalRAM  uint64
	FreeRAM   uint64
	SharedRAM uint64
	BufferRAM uint64
}

// GetSystemInfo gets system information
func GetSystemInfo() (*SystemInfo, error) {
	var sysinfo syscall.Sysinfo_t
	if err := syscall.Sysinfo(&sysinfo); err != nil {
		return nil, fmt.Errorf("sysinfo failed: %w", err)
	}

	return &SystemInfo{
		Uptime:    sysinfo.Uptime,
		Loads:     sysinfo.Loads,
		TotalRAM:  sysinfo.Totalram,
		FreeRAM:   sysinfo.Freeram,
		SharedRAM: sysinfo.Sharedram,
		BufferRAM: sysinfo.Bufferram,
	}, nil
}

// ProcessStats provides process statistics
type ProcessStats struct {
	UserTime   int64
	SystemTime int64
	MaxRSS     int64
}

// GetProcessStats gets process statistics
func GetProcessStats() (*ProcessStats, error) {
	var usage syscall.Rusage
	if err := syscall.Getrusage(syscall.RUSAGE_SELF, &usage); err != nil {
		return nil, fmt.Errorf("getrusage failed: %w", err)
	}

	return &ProcessStats{
		UserTime:   usage.Utime.Sec*1000 + int64(usage.Utime.Usec)/1000,
		SystemTime: usage.Stime.Sec*1000 + int64(usage.Stime.Usec)/1000,
		MaxRSS:     usage.Maxrss,
	}, nil
}

// MemoryMapping maps memory
type MemoryMapping struct {
	addr   uintptr
	length int
	prot   int
	flags  int
}

// NewMemoryMapping creates a new memory mapping
func NewMemoryMapping(length int) (*MemoryMapping, error) {
	addr, err := syscall.Mmap(-1, 0, length, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_ANON|syscall.MAP_PRIVATE)
	if err != nil {
		return nil, fmt.Errorf("mmap failed: %w", err)
	}

	return &MemoryMapping{
		addr:   uintptr(unsafe.Pointer(&addr[0])),
		length: length,
	}, nil
}

// Write writes to mapped memory
func (mm *MemoryMapping) Write(offset int, data []byte) error {
	if offset+len(data) > mm.length {
		return fmt.Errorf("write exceeds mapped region")
	}

	ptr := unsafe.Pointer(mm.addr + uintptr(offset))
	copy((*[1 << 30]byte)(ptr)[:len(data)], data)

	return nil
}

// Read reads from mapped memory
func (mm *MemoryMapping) Read(offset, length int) ([]byte, error) {
	if offset+length > mm.length {
		return nil, fmt.Errorf("read exceeds mapped region")
	}

	ptr := unsafe.Pointer(mm.addr + uintptr(offset))
	return (*[1 << 30]byte)(ptr)[:length], nil
}

// Unmap unmaps memory
func (mm *MemoryMapping) Unmap() error {
	return syscall.Munmap((*[1 << 30]byte)(unsafe.Pointer(mm.addr))[:mm.length])
}

// FileDescriptorSet manages file descriptors
type FileDescriptorSet struct {
	set syscall.FdSet
}

// NewFileDescriptorSet creates a new FD set
func NewFileDescriptorSet() *FileDescriptorSet {
	return &FileDescriptorSet{}
}

// Set sets a file descriptor
func (fds *FileDescriptorSet) Set(fd int) {
	fds.set.Bits[fd/64] |= 1 << uint(fd%64)
}

// Clear clears a file descriptor
func (fds *FileDescriptorSet) Clear(fd int) {
	fds.set.Bits[fd/64] &= ^(1 << uint(fd%64))
}

// IsSet checks if file descriptor is set
func (fds *FileDescriptorSet) IsSet(fd int) bool {
	return fds.set.Bits[fd/64]&(1<<uint(fd%64)) != 0
}

Bad: Improper Low-Level Programming

package main

// BAD: No error handling
func BadSyscall() {
	syscall.Open("/etc/hostname", syscall.O_RDONLY, 0)
	// Ignoring error
}

// BAD: Unsafe memory access
func BadMemoryAccess() {
	// Direct unsafe pointer manipulation
	// No bounds checking
}

// BAD: Resource leaks
func BadFileDescriptor() {
	fd, _ := syscall.Open("/etc/hostname", syscall.O_RDONLY, 0)
	// Never closed
}

Problems:

  • No error handling
  • Unsafe memory access
  • Resource leaks
  • No bounds checking

Unsafe Operations

package main

import (
	"unsafe"
)

// UnsafePointerExample demonstrates unsafe pointer usage
func UnsafePointerExample() {
	// Convert between types
	var x int64 = 0x0102030405060708
	ptr := unsafe.Pointer(&x)
	bytes := (*[8]byte)(ptr)

	// Access bytes
	for i, b := range bytes {
		println(i, b)
	}
}

// StructFieldAccess accesses struct fields via unsafe
func StructFieldAccess() {
	type Point struct {
		X int32
		Y int32
	}

	p := Point{X: 10, Y: 20}
	ptr := unsafe.Pointer(&p)

	// Access X field
	xPtr := (*int32)(ptr)
	println(*xPtr) // 10

	// Access Y field
	yPtr := (*int32)(unsafe.Pointer(uintptr(ptr) + unsafe.Offsetof(p.Y)))
	println(*yPtr) // 20
}

Best Practices

1. Always Check Errors

if err := syscall.Sysinfo(&info); err != nil {
	return err
}

2. Close Resources

defer syscall.Close(fd)

3. Use Bounds Checking

if offset+length > mm.length {
	return fmt.Errorf("out of bounds")
}

4. Minimize Unsafe Code

// Use unsafe only when necessary
// Document why it's needed

Common Pitfalls

1. No Error Handling

Always check syscall errors.

2. Resource Leaks

Always close file descriptors.

3. Unsafe Memory Access

Always check bounds.

4. Platform Differences

Test on target platforms.

Resources

Summary

Low-level programming requires care. Key takeaways:

  • Check all syscall errors
  • Close file descriptors
  • Validate memory access
  • Minimize unsafe code
  • Test on target platforms
  • Document unsafe operations
  • Consider performance trade-offs

By mastering low-level programming, you can optimize performance when needed.

Comments