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