Unix/Linux系统相关异常

Unix/Linux系统相关异常主要封装了操作系统调用失败时的错误信息,是连接底层系统错误与上层应用逻辑的桥梁。以下从核心概念、常见类型、实战应用到最佳实践,系统讲解这类异常:

层级关系

std::runtime_error
└─ UnixError (封装errno,系统调用通用异常)
   ├─ FileError (文件操作异常)
   │  ├─ FileNotFoundException (文件不存在)
   │  ├─ FileExistsError (文件已存在)
   │  ├─ FilePermissionError (权限不足)
   │  └─ FileUnexpectedEOFError (文件读取不完整)
   ├─ IOError (输入输出异常)
   │  ├─ ReadError (读取失败)
   │  └─ WriteError (写入失败)
   ├─ MemoryError (内存操作异常)
   │  ├─ MmapError (内存映射失败)
   │  └─ AllocationError (内存分配失败)
   └─ ProcessError (进程操作异常)
      ├─ ForkError (fork失败)
      └─ ExecError (执行程序失败)

一、Unix/Linux系统异常的核心概念

1. 系统错误的源头:errno

  • 定义errno是Unix/Linux系统中的全局整数变量(线程局部存储,每个线程独立),当系统调用(如openread)失败时,会被设置为特定错误码,对应具体错误类型。
  • 查看错误信息:通过strerror(errno)可将错误码转为可读字符串,例如:
    errno = ENOENT;  // 文件不存在
    printf("%s\n", strerror(errno));  // 输出 "No such file or directory"
    

2. 异常封装的意义

系统调用通常返回-1表示失败,但上层应用需要更清晰的错误类型和上下文。通过封装errno到异常中,可以:

  • 区分不同错误类型(如文件不存在、权限不足、磁盘满)。
  • 传递错误上下文(如操作的文件名、函数名)。
  • 统一错误处理流程,避免重复编写if (ret == -1)判断。

二、常见Unix/Linux系统异常类型

1. 基础系统异常类

定位:封装所有 Unix/Linux 系统调用的错误(如 open、read、lseek、fork 等)。

#include <stdexcept>
#include <cstring>  // strerror

class UnixError : public std::runtime_error {
public:
    // 无参构造:直接使用errno的错误信息
    UnixError() : std::runtime_error(strerror(errno)) {}
    
    // 带参数构造:添加自定义错误描述
    UnixError(const std::string& msg) 
        : std::runtime_error(msg + ": " + strerror(errno)) {}
};

2. 文件操作相关异常

定位:专门处理与文件操作相关的错误(如文件打开失败、读写错误)。

class FileError : public UnixError {
public:
    // 封装文件操作的具体错误(打开/读写/关闭)
    FileError(const std::string& path, const std::string& operation)
        : UnixError("File " + operation + " failed for " + path) {}
};

// 使用示例
if (open("data.db", O_RDONLY) == -1) {
    throw FileError("data.db", "open");
}

3. I/O操作异常

class IOError : public UnixError {
public:
    IOError(const std::string& operation, int fd) {
        char buf[100];
        snprintf(buf, sizeof(buf), "IO %s failed on fd %d", 
                operation.c_str(), fd);
        what_str_ = buf;
    }
    
private:
    std::string what_str_;
    const char* what() const noexcept override {
        return what_str_.c_str();
    }
};

4. 内存操作异常

class MemoryError : public UnixError {
public:
    MemoryError(const std::string& operation)
        : UnixError("Memory " + operation + " failed") {}
};

// 示例:mmap失败时抛出
void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, 
                MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ptr == MAP_FAILED) {
    throw MemoryError("mmap");
}

三、系统异常的实战应用场景

1. 文件I/O操作中的异常处理

void read_file(int fd, char* buffer, size_t size) {
    ssize_t bytes_read = read(fd, buffer, size);
    if (bytes_read == -1) {
        throw IOError("read", fd);
    } else if (bytes_read < size) {
        throw std::runtime_error("Partial read, expected " + 
                                std::to_string(size) + " bytes, got " + 
                                std::to_string(bytes_read));
    }
}

2. 进程操作异常

pid_t pid = fork();
if (pid == -1) {
    throw UnixError("fork failed");
} else if (pid == 0) {
    // 子进程逻辑
} else {
    // 父进程逻辑
}

3. 网络套接字异常

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
    throw UnixError("socket creation failed");
}

struct sockaddr_in addr;
// 初始化addr...

if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
    throw UnixError("bind failed");
}

四、系统异常处理的最佳实践

1. 保存errno避免竞争

void safe_write(int fd, const char* data, size_t size) {
    int original_errno = errno;  // 先保存errno,避免多线程修改
    if (write(fd, data, size) != size) {
        original_errno = errno;  // 若write失败,errno已被设置为具体错误
        throw UnixError("write failed");
    }
    errno = original_errno;  // 恢复errno(可选,视场景而定)
}

2. 区分可恢复与不可恢复错误

  • 可恢复错误:如临时文件锁冲突(EAGAIN),可重试。
  • 不可恢复错误:如文件不存在(ENOENT),需上层处理。
try {
    // 尝试打开文件
    int fd = open("data.db", O_RDONLY);
} catch (const UnixError& e) {
    if (errno == ENOENT) {
        // 文件不存在,创建新文件
        fd = open("data.db", O_RDWR | O_CREAT, 0666);
    } else {
        // 其他错误,无法恢复
        throw;
    }
}

3. 错误码与异常类型映射

对高频错误码,可封装为特定异常类,便于针对性处理:

class FileNotFoundException : public FileError {
public:
    FileNotFoundException(const std::string& path) 
        : FileError(path, "open") {
        // 确保errno是ENOENT
        errno = ENOENT;
    }
};

// 使用
try {
    open_file("non_existent.txt");
} catch (const FileNotFoundException& e) {
    std::cout << "File not found, creating new one..." << std::endl;
    create_new_file("non_existent.txt");
}

4. 资源管理与异常安全

使用RAII(资源获取即初始化)模式,确保异常抛出时资源自动释放:

class FileGuard {
public:
    FileGuard(const std::string& path, int flags) {
        fd_ = open(path.c_str(), flags);
        if (fd_ == -1) {
            throw FileError(path, "open");
        }
    }
    
    ~FileGuard() {
        if (fd_ != -1) {
            close(fd_);
        }
    }
    
    int get_fd() const { return fd_; }
    
private:
    int fd_ = -1;
};

// 使用
void process_file(const std::string& path) {
    FileGuard file(path, O_RDWR);
    // 操作file.get_fd(),异常时自动关闭文件
}

五、常见Unix错误码与场景

错误码 宏定义 含义 典型场景
2 ENOENT 文件或目录不存在 open("nonexist.txt")
13 EACCES 权限不足 访问受保护的文件
28 ENOSPC 磁盘空间不足 写入文件时磁盘满
9 EBADF 无效的文件描述符 使用已关闭的fd读写
11 EAGAIN 资源暂时不可用(可重试) 非阻塞I/O操作时无数据可读
22 EINVAL 无效参数 lseek使用非法whence参数
12 ENOMEM 内存不足 malloc/mmap分配失败

总结

Unix/Linux系统相关异常的核心是对errno的封装与抽象,通过将系统错误码转换为语义化的异常类型,使上层应用能更清晰地处理不同错误场景。在数据库等系统级项目中,合理设计系统异常体系可以:

  1. 区分底层系统错误与业务逻辑错误。
  2. 提供丰富的错误上下文,加速问题定位。
  3. 结合RAII模式保证异常安全,避免资源泄漏。

掌握这些异常的使用,能显著提升代码的健壮性和可维护性,尤其是在处理文件I/O、进程管理、内存映射等关键操作时。

posted @ 2025-06-19 19:18  韩熙隐ario  阅读(33)  评论(0)    收藏  举报