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编程中更好地处理错误,提高程序的质量。

2503

被折叠的 条评论
为什么被折叠?



