12、Linux编程中的错误处理与errno使用指南

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Linux编程中的错误处理与errno使用指南

在Linux和类UNIX系统的编程中,错误处理是一个至关重要的环节。大多数错误往往与权限或系统资源有关。下面我们将详细探讨如何在编程中处理这些错误,以及如何利用 errno 变量来获取更详细的错误信息。

常见错误类型与处理基础

在编程过程中,许多错误都与权限或系统资源相关。例如,在处理用户输入和缓冲区溢出时,像 strcat() strcpy() strdup() 这类函数是不安全的,因为它们会复制接收到的所有内容,而不考虑目标缓冲区是否有足够的空间。当输入的字符串长度超过缓冲区容量时,程序可能会因段错误而崩溃。

为了避免这种情况,可以使用带有 n 字符的等效函数,如 strncat() 。这些函数只会复制作为第三个参数指定的大小。例如,使用 sizeof(buf)-1 作为大小参数,这样可以为字符串末尾的空字符 \0 留出空间。使用 sizeof(buf) 比使用字面数字更安全,因为如果后续更改了缓冲区大小,使用字面数字可能会忘记更新 strncat() 的参数。

errno变量介绍

在Linux和其他类UNIX系统中,大多数系统调用函数在发生错误时会设置一个特殊的变量 errno 。通过函数的返回值(通常为 -1)可以得到一个通用的错误代码,而通过查看 errno 变量可以获取更具体的错误信息。

学习 errno 对于系统编程至关重要,因为它与系统调用密切相关。接下来,我们将通过一系列示例来展示如何使用 errno

准备工作

在进行后续的编程操作之前,需要确保已经安装了以下组件:
- GCC编译器
- Make工具
- POSIX程序员手册

如果尚未安装,可以参考相关资料进行安装。

使用errno检查特定错误

下面我们将在之前的 simple-touch-v2.c 基础上进行扩展,编写 simple-touch-v3.c ,使用 errno 变量来检查错误是否由权限错误( EACCES )或其他未知错误引起。

#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <linux/limits.h>
int main(int argc, char *argv[])
{
    char filename[PATH_MAX] = { 0 };
    if (argc != 2)
    {
        fprintf(stderr, "You must supply a filename as an argument\n");
        return 1;
    }
    strncpy(filename, argv[1], sizeof(filename)-1);
    if ( creat(filename, 00644) == -1 )
    {
        fprintf(stderr, "Can't create file %s\n", filename);
        if (errno == EACCES)
        {
            fprintf(stderr, "Permission denied\n");
        }
        else
        {
            fprintf(stderr, "Unknown error\n");
        }
        return 1;
    }
    return 0;
}

编译并运行该程序:

$> make simple-touch-v3
gcc -Wall -Wextra -pedantic -std=c99 simple-touch-v3.c -o simple-touch-v3
$> ./simple-touch-v3 asdf
$> ls -l asdf
-rw-r--r-- 1 jake jake 0 okt 13 23:30 asdf
$> ./simple-touch-v3 /asdf
Can't create file /asdf
Permission denied
$> ./simple-touch-v3 /non-existent-dir/hello
Can't create file /non-existent-dir/hello
Unknown error

在这个版本中,我们引入了 errno.h 头文件,使用 sizeof(filename)-1 作为 strncpy() 的大小参数,并通过 if (errno == EACCES) 检查是否为权限错误。需要注意的是,在使用 errno 时,应先检查函数或系统调用的返回值,因为 errno 变量不会在系统调用后自动清除。

处理更多的errno宏

在之前的版本中,我们只处理了权限错误,当尝试在不存在的目录中创建文件时会输出“Unknown error”。接下来,我们将查找导致该错误的具体宏,并在程序中进行处理。

操作步骤如下:
1. 使用 man 2 creat 查看 creat() 函数的手册页,滚动到“ERRORS”部分,找到表示路径名不存在的宏 ENOENT
2. 编写 simple-touch-v4.c ,实现一个新的 if 语句来处理 ENOENT 错误。

#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <linux/limits.h>
int main(int argc, char *argv[])
{
    char filename[PATH_MAX] = { 0 };
    if (argc != 2)
    {
        fprintf(stderr, "You must supply a filename as an argument\n");
        return 1;
    }
    strncpy(filename, argv[1], sizeof(filename)-1);
    if ( creat(filename, 00644) == -1 )
    {
        fprintf(stderr, "Can't create file %s\n", filename);
        if (errno == EACCES)
            fprintf(stderr, "Permission denied\n");
        else if (errno == ENOENT)
            fprintf(stderr, "Parent directories does not exist\n");
        else
            fprintf(stderr, "Unknown error\n");
        return 1;
    }
    return 0;
}

编译并运行该程序:

$> make simple-touch-v4
gcc -Wall -Wextra -pedantic -std=c99 simple-touch-v4.c -o simple-touch-v4
$> ./simple-touch-v4 asdf123
$> ./simple-touch-v4 /hello
Can't create file /hello
Permission denied
$> ./simple-touch-v4 /non-existent/hello
Can't create file /non-existent/hello
Parent directories do not exist

在这个版本中,我们处理了 ENOENT 错误,但需要注意代码中移除部分 if 语句括号可能导致的误导性缩进问题。

使用strerror()函数简化错误处理

手动查找和处理每个可能的 errno 宏比较繁琐,使用 strerror() 函数可以将 errno 代码转换为可读的错误消息,更加方便快捷。

编写 simple-touch-v5.c ,使用 strerror() 函数替换之前的 if 语句:

#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <linux/limits.h>
int main(int argc, char *argv[])
{
    int errornum;
    char filename[PATH_MAX] = { 0 };
    if (argc != 2)
    {
        fprintf(stderr, "You must supply a filename as an argument\n");
        return 1;
    }
    strncpy(filename, argv[1], sizeof(filename)-1);
    if ( creat(filename, 00644) == -1 )
    {
        errornum = errno;
        fprintf(stderr, "Can't create file %s\n", filename);
        fprintf(stderr, "%s\n", strerror(errornum));
        return 1;
    }
    return 0;
}

编译并运行该程序:

$> make simple-touch-v5
gcc -Wall -Wextra -pedantic -std=c99 simple-touch-v5.c -o simple-touch-v5
$> ./simple-touch-v5 hello123
$> ls hello123
hello123
$> ./simple-touch-v5 /asdf123
Can't create file /asdf123
Permission denied
$> ./simple-touch-v5 /asdf123/hello
Can't create file /asdf123/hello
No such file or directory

在这个版本中,我们将 errno 的值保存到 errornum 变量中,以防止后续错误覆盖 errno 的值。 strerror() 函数将错误代码转换为人类可读的错误消息。

使用perror()函数直接输出错误信息

除了 strerror() 函数,还有一个类似的函数 perror() ,它的名称表示“print error”,可以直接将错误消息打印到 stderr

编写 simple-touch-v6.c ,使用 perror() 函数替换之前的 fprintf() 语句:

#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <linux/limits.h>
int main(int argc, char *argv[])
{
    char filename[PATH_MAX] = { 0 };
    if (argc != 2)
    {
        fprintf(stderr, "You must supply a filename as an argument\n");
        return 1;
    }
    strncpy(filename, argv[1], sizeof(filename)-1);
    if ( creat(filename, 00644) == -1 )
    {
        perror("Can't create file");
        return 1;
    }
    return 0;
}

编译并运行该程序:

$> make simple-touch-v6
gcc -Wall -Wextra -pedantic -std=c99 simple-touch-v6.c -o simple-touch-v6
$> ./simple-touch-v6 abc123
$> ./simple-touch-v6 /asdf123
Can't create file: Permission denied
$> ./simple-touch-v6 /asdf123/hello
Can't create file: No such file or directory

perror() 函数接受一个字符串参数,用于描述错误的上下文,它会自动获取 errno 的值并将错误消息附加在描述信息之后输出。

返回错误值

除了输出人类可读的错误消息,还需要向shell返回一个表示错误的数值。通常,返回0表示程序正常运行,返回其他值(通常为1)表示发生了错误。我们可以返回更具体的 errno 值,以便其他依赖该程序的程序能够读取这些数值。

编写 simple-touch-v7.c ,返回 errno 变量的值:

#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <linux/limits.h>
int main(int argc, char *argv[])
{
    char filename[PATH_MAX] = { 0 };
    if (argc != 2)
    {
        fprintf(stderr, "You must supply a filename as an argument\n");
        return 1;
    }
    strncpy(filename, argv[1], sizeof(filename)-1);
    if ( creat(filename, 00644) == -1 )
    {
        perror("Can't create file");
        return errno;
    }
    return 0;
}

编译并运行该程序:

$> make simple-touch-v7
gcc -Wall -Wextra -pedantic -std=c99 simple-touch-v7.c -o simple-touch-v7
$> ./simple-touch-v7 asdf
$> echo $?
0
$> ./simple-touch-v7 /asdf
Can't create file: Permission denied
$> echo $?
13
$> ./simple-touch-v7 /asdf/hello123
Can't create file: No such file or directory
$> echo $?
2

在这个版本中,当发生错误时,程序会返回 errno 变量的值,不同的错误宏对应不同的整数值,如 EACCES 对应13, ENOENT 对应2。

综上所述,通过合理使用 errno 变量以及相关的函数,如 strerror() perror() ,可以在Linux编程中更有效地处理错误,并向用户提供详细的错误信息。同时,返回具体的错误值可以方便其他程序对错误进行进一步处理。

下面是一个简单的流程图,展示了使用 errno 处理错误的基本流程:

graph TD;
    A[开始] --> B[调用系统调用函数];
    B --> C{返回值是否为 -1};
    C -- 是 --> D[检查errno变量];
    D --> E{errno是否为特定值};
    E -- 是 --> F[处理特定错误];
    E -- 否 --> G[处理未知错误];
    C -- 否 --> H[程序正常运行];
    F --> I[返回错误值];
    G --> I;
    H --> J[返回0];
    I --> K[结束];
    J --> K;

通过这个流程图,可以更清晰地理解在编程中如何使用 errno 进行错误处理。

在实际编程中,还可以参考 man 2 syscalls 中列出的函数手册页,查看不同函数设置的 errno 宏,以及 man errno.h 获取有关这些宏的更多信息。同时, man 3 strerror man 3 perror 手册页中也包含了一些有用的提示和相关函数的介绍。

Linux编程中的错误处理与errno使用指南(续)

不同错误处理方式对比

为了更清晰地展示不同错误处理方式的特点,我们可以通过一个表格进行对比:
| 处理方式 | 优点 | 缺点 | 适用场景 |
| — | — | — | — |
| 手动检查errno宏 | 可以针对特定错误进行精细处理 | 需要手动查找和处理每个可能的宏,工作量大且容易出错 | 需要对特定错误进行定制化处理时 |
| 使用strerror() | 快速将errno代码转换为可读消息,减少手动工作量,更安全 | 不能针对特定错误进行定制化处理 | 只需要简单输出错误信息时 |
| 使用perror() | 直接将错误消息打印到stderr,代码简洁 | 不能灵活控制错误消息的输出格式 | 快速输出错误信息,对格式要求不高时 |
| 返回errno值 | 向其他程序提供具体的错误信息,方便后续处理 | 对于普通用户来说,错误码不够直观 | 其他程序依赖该程序的错误信息时 |

错误处理的最佳实践

在实际的Linux编程中,为了更好地进行错误处理,我们可以遵循以下最佳实践:
1. 优先使用现有函数 :如 strerror() perror() ,这些函数可以减少手动工作量,提高代码的安全性和可读性。
2. 保存errno值 :在使用 errno 时,及时将其值保存到一个新的变量中,避免后续错误覆盖原有的错误信息。
3. 明确错误上下文 :在使用 perror() 时,提供清晰的错误描述信息,方便调试和定位问题。
4. 返回具体错误值 :向shell返回具体的 errno 值,以便其他程序能够根据这些值进行进一步的处理。

深入理解errno宏

几乎所有在 man 2 syscalls 中列出的函数都会设置 errno 变量。我们可以查看这些函数的手册页,滚动到“RETURN VALUE”和“ERRORS”部分,了解不同函数设置的 errno 宏。同时, man errno.h 也包含了关于这些宏的有用信息。

例如,我们可以查看 open() 函数的手册页:

man 2 open

在手册页中,我们可以找到该函数可能设置的 errno 宏以及对应的错误描述。

结合实际案例分析

假设我们要编写一个程序,用于读取一个文件的内容。以下是一个简单的示例代码:

#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#define BUFFER_SIZE 1024

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "You must supply a filename as an argument\n");
        return 1;
    }

    int fd = open(argv[1], O_RDONLY);
    if (fd == -1) {
        perror("Can't open file");
        return errno;
    }

    char buffer[BUFFER_SIZE];
    ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE);
    if (bytes_read == -1) {
        perror("Error reading file");
        close(fd);
        return errno;
    }

    buffer[bytes_read] = '\0';
    printf("File content:\n%s\n", buffer);

    close(fd);
    return 0;
}

在这个示例中,我们使用了 open() 函数打开文件, read() 函数读取文件内容。如果打开文件或读取文件时发生错误,使用 perror() 输出错误信息,并返回 errno 值。

错误处理的扩展应用

在复杂的程序中,可能会有多个系统调用和函数调用,错误处理也会更加复杂。我们可以将错误处理封装成一个独立的函数,提高代码的复用性。

以下是一个简单的错误处理函数示例:

#include <stdio.h>
#include <errno.h>

void handle_error(const char *message) {
    perror(message);
    return errno;
}

在主程序中,可以这样使用该函数:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "You must supply a filename as an argument\n");
        return 1;
    }

    int fd = open(argv[1], O_RDONLY);
    if (fd == -1) {
        int error = handle_error("Can't open file");
        return error;
    }

    // 其他操作...

    close(fd);
    return 0;
}
总结

通过本文的介绍,我们了解了在Linux编程中如何处理常见的错误,以及如何利用 errno 变量获取详细的错误信息。我们学习了多种错误处理方式,包括手动检查 errno 宏、使用 strerror() perror() 函数,以及返回 errno 值。同时,我们还探讨了错误处理的最佳实践和扩展应用。

在实际编程中,应根据具体的需求选择合适的错误处理方式,确保程序的健壮性和可维护性。通过合理使用 errno 和相关函数,能够更高效地定位和解决程序中的错误,提高开发效率。

下面是一个更复杂的流程图,展示了在一个包含多个系统调用的程序中如何使用 errno 进行错误处理:

graph TD;
    A[开始] --> B[调用系统调用1];
    B --> C{返回值是否为 -1};
    C -- 是 --> D[检查errno变量1];
    D --> E{errno1是否为特定值};
    E -- 是 --> F[处理特定错误1];
    E -- 否 --> G[处理未知错误1];
    C -- 否 --> H[调用系统调用2];
    H --> I{返回值是否为 -1};
    I -- 是 --> J[检查errno变量2];
    J --> K{errno2是否为特定值};
    K -- 是 --> L[处理特定错误2];
    K -- 否 --> M[处理未知错误2];
    I -- 否 --> N[程序正常运行];
    F --> O[返回错误值];
    G --> O;
    L --> O;
    M --> O;
    N --> P[返回0];
    O --> Q[结束];
    P --> Q;

通过这个流程图,我们可以更清晰地看到在多个系统调用的情况下,如何使用 errno 进行错误处理。希望本文的内容能够帮助你在Linux编程中更好地处理错误,提高程序的质量。

源码直接下载地址: https://pan.quark.cn/s/32a64cc0d812 LKH 算法在中文中的表述为 LKH 算法,它是一种用于处理 TSP(旅行商问题) VRP(车辆配送问题)等组合优化挑战的启发式算法,并且该算法是 Lin-Kernighan 启发式方法的进一步发展。该算法的开发执行过程具有相当的挑战性,然而,它被认为是获取对称旅行商问题最优或接近最优答案的最有效途径之一。LKH 算法的升级版本通过运用灵敏度分析来引导并约束搜索过程,从而使得该算法能够在可接受的时间内为大规模问题找出最优解。通过计算实验的验证,证明该方法具备高效性,能够在不足一秒的时间范围内寻得典型100座城市问题的最优方案,而对于典型的1000座城市问题,也能在不到一分钟的时间框内找到最优解。旅行商问题(TSP)是组合优化领域中研究最为深入的课题之一,该问题可以通过成本矩阵 C 的特性来进行分类。此问题可划分为对称性情形非对称性情形,同时依据三角不等式的成立否,可进一步区分为度量性情形非度量性情形。TSP 的显著地位源于其广泛的实际应用,其中许多应用看似旅行路径无直接关联。众多现实场景能够以 TSP 的形式来模拟,例如计算机内部布线、车辆路径规划、晶体结构分析、机器人导航控制、印刷电路板打孔定位以及时间表的制定等。TSP 作为一种典型的组合优化课题,其研究对于解决该学科范畴内的其他课题往往具有指导意义。事实上,组合优化领域的诸多突破均可追溯至对 TSP 问题的深入探索。计算方法中广为人知的 branch and bound 技术最初便是在 TSP 的研究背景下被引入的。攻克 TSP 所面临的智力难题亦起到了推动作用,该问题的表述看似简单,却极难求解。当考虑到可能...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值