C/C++ Linux系统编程:详解常见的系统调用函数,文件I/O核心:open, close, read, write
引言
在学习 Linux 系统编程的过程中,系统调用(System Call)是绕不开的核心内容。它们是用户程序与操作系统内核交互的桥梁。本文记录了学习Linux系统编程中基础文件I/O系统调用 open()
, close()
, read()
, write()
的过程。包含详细的功能说明、函数原型解析、参数含义、返回值处理、关键注意事项。适合初学者巩固基础,也方便日后回顾查阅。
核心概念铺垫:文件描述符 (File Descriptor)
在深入具体函数之前,必须先理解 文件描述符(fd) 的概念,这里只做简单的概念介绍,过多的理论可以看我之前的一篇博客文章:C/C++ Linux系统编程:深度解析文件描述符,从概念到内核-CSDN博客
文件描述符是一个代替文件本身的非负整数,当进程打开一个现有的文件、创建一个新文件内核都会向该进程返回一个文件描述符。
它本质上是内核为每个进程维护的 打开文件记录表 的一个索引。后续所有的I/O操作(read
, write
, close
, lseek
等)都需要通过这个文件描述符来引用相应的文件或资源。
下面介绍标准输入输出流:
每个进程启动时,默认打开三个文件描述符:
-
0
(STDIN_FILENO): 标准输入 (stdin) -
1
(STDOUT_FILENO): 标准输出 (stdout) -
2
(STDERR_FILENO): 标准错误 (stderr)
文件描述符由 open()
或类似函数创建,通过 close()
释放。进程结束时,内核会自动关闭所有打开的文件描述符,但显式关闭是良好的编程习惯。
系统调用详解
1. 什么是系统调用?
系统调用是操作系统提供给用户程序的一组接口,用于请求内核服务(如文件操作、进程控制、内存管理等)。我们通常使用 <unistd.h>
和 <fcntl.h>
等头文件来调用这些函数。
相关数据类型:
1. ssize_t
ssize_t相关的宏定义如下
typedef __ssize_t ssize_t;
__STD_TYPE __SSIZE_T_TYPE __ssize_t;
# define __STD_TYPE typedef
#define __SSIZE_T_TYPE __SWORD_TYPE
# define __SWORD_TYPE long int
ssize_t是__ssize_t的别名,后者是long int的别名,long是long int的简写,因此,ssize_t实际上是long类型的别名。
2. size_t
typedef __SIZE_TYPE__ size_t;
#define __SIZE_TYPE__ long unsigned int
unsigned long是long unsigned int的简写,size_t实质上是unsigned long。
2. open()
- 打开/创建文件
open()
函数原型如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
参数解析
pathname
: 要打开或创建的文件路径 (绝对路径或相对路径)。
flags
: 标志位,指定打开方式和行为。使用 |
组合。
-
核心标志:
-
访问模式 (必选其一):
-
O_RDONLY
: 只读 -
O_WRONLY
: 只写 -
O_RDWR
: 读写
-
-
创建/截断选项:
-
O_CREAT
: 如果文件不存在则创建它。需要提供第三个参数mode
。 -
O_EXCL
: 与O_CREAT
同用时,如果文件已存在,则open()
失败。常用于确保原子性地创建新文件。 -
O_TRUNC
: 如果文件已存在并且以可写方式(O_WRONLY
或O_RDWR
)打开,则将其长度截断为0字节。
-
-
其他常用选项:
-
O_APPEND
: 每次写操作前都将文件偏移量移动到文件末尾。避免并发写覆盖的关键! -
O_NONBLOCK
/O_NDELAY
: 以非阻塞方式打开文件。
-
-
mode
: 该参数仅在 O_CREAT
时必需填写,指定新创建文件的访问权限。通常用八进制数表示 ,以四位八进制数字表示,第一位为0表示以下为八进制数字,之后三位分别表示文件创建者、创建者所在组以及其他组的权限,比如0644。
返回值
-
成功: 返回新分配的文件描述符(一个非负整数)。
-
失败: 返回
-1
,并设置全局变量errno
以指示具体错误类型。
示例代码片段:
int fd;
// 只读方式打开现有文件
fd = open("existing.txt", O_RDONLY);
if (fd == -1) {perror("open existing.txt (O_RDONLY) failed");exit(EXIT_FAILURE);
}// 以读写方式打开文件,不存在则创建,权限设为 rw-r--r--
fd = open("newfile.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {perror("open newfile.txt (O_RDWR|O_CREAT|O_TRUNC) failed");exit(EXIT_FAILURE);
}// 以追加方式打开日志文件(不存在则创建)
fd = open("app.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd == -1) {perror("open app.log (O_WRONLY|O_CREAT|O_APPEND) failed");exit(EXIT_FAILURE);
}
3. close()
- 关闭文件描述符
该函数功能是:关闭一个打开的文件描述符。释放该描述符以及它可能持有的任何资源(如文件锁)。重要:显式关闭不再需要的文件描述符是良好的编程习惯!
函数原型:
#include int close(int fd);
参数解析
该函数只有一个参数 fd 即要关闭的文件描述符。
返回值
-
成功: 返回
0
。 -
失败: 返回
-1
,并设置errno
(常见错误:EBADF
-fd
不是有效的打开文件描述符)。
示例代码片段
int fd = open(...); // 打开文件
if (fd == -1) { ... } // 错误处理// ... 使用fd进行read/write操作 ...if (close(fd) == -1) {perror("close failed");// 处理关闭错误(通常仍退出或记录日志)
}
4. read()
- 从文件描述符读取数据
该函数功能是: 尝试从文件描述符 fd
所引用的文件中读取最多 count
字节的数据,并将其存储到 buf
指向的缓冲区中。
其函数原型
#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);
参数解析
-
fd
: 要读取数据的文件描述符(必须是以可读方式打开的)。 -
buf
: 指向用于存储读取数据的缓冲区的指针。 -
count
: 请求读取的最大字节数,缓冲区buf
必须至少能容纳count
字节。
返回值
-
成功: 返回实际读取到的字节数。这个数字可能小于
count
,甚至为0
!-
> 0
: 成功读取到若干字节。 -
0
: 表示到达文件末尾 (EOF - End Of File)。对于普通文件、管道、终端等,读到末尾返回0是正常的。
-
-
失败: 返回
-1
,并设置errno
该函数需要注意的点:
函数返回值是实际读取的字节数,不一定最大读取字节数count
必须根据返回值处理部分读取的情况。永远不要假设 read()
会一次性读完你请求的所有数据。
函数返回值 0
表示 EOF,不是错误! 区分错误(-1
)和正常结束(0
)非常重要。
缓冲区 buf
必须足够大且有效。 count
不能超过缓冲区实际大小。
示例代码片段
char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);
if (bytes_read > 0) {buffer[bytes_read] = '\0'; // 添加字符串结束符printf("读取内容: %s\n", buffer);
} else if (bytes_read == 0) {printf("文件已读完。\n");
} else {perror("read error");
}
5. write()
- 向文件描述符写入数据
该函数的功能是: 尝试将 buf
指向的缓冲区中的最多 count
字节数据写入到文件描述符 fd
所引用的文件中
函数原型为
#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);
参数解析
-
f
d
: 要写入数据的文件描述符(必须是以可写方式打开的)。 -
buf
: 指向包含待写入数据的缓冲区的指针。 -
count
: 请求写入的最大字节数。
函数返回值
-
成功: 返回实际写入的字节数。这个数字可能小于
count
!-
> 0
: 成功写入了若干字节。 -
0
: 通常写入0字节没有意义,但在非阻塞写入特定设备时可能返回0(表示未写入任何数据)。
-
-
失败: 返回
-1
,并设置errno
该函数使用时的注意点:
返回值是实际写入的字节数,不一定是 count
必须根据返回值处理部分写入的情况。永远不要假设 write()
会一次性写完你提供的所有数据。
O_APPEND
标志的影响: 如果文件是用 O_APPEND
打开的,则在每次 write()
操作之前,文件偏移量会自动被设置到文件末尾。这保证了并发写入时数据不会被覆盖。这是多进程安全写日志的关键机制!
缓冲区 buf
必须包含有效数据且 count
合理。
示例代码片段
const char *msg = "Hello, Linux System Programming!\n";
ssize_t bytes_written = write(fd, msg, strlen(msg));
if (bytes_written == -1) {perror("write error");
} else {printf("成功写入 %zd 字节\n", bytes_written);
}
📌注意事项大全
1. 文件描述符是核心: 所有操作都围绕文件描述符进行
2. 检查返回值!检查返回值!检查返回值! 重要的事情说三遍。系统调用失败是常态,必须处理 -1
和 errno
。使用 perror()
或 strerror(errno)
打印错误信息。
3. 理解部分I/O: read
和 write
不保证读取/写入请求的所有字节。必须编写循环来处理部分读取/写入的情况。
4. 区分 EOF 和错误: read
返回 0
表示文件结束也就是EOF,不是错误。返回 -1
才是错误。
5. 文件偏移量: 内核为每个打开的文件维护一个当前读写位置也就是文件读写偏移量。read
/write
会更新它,如果要追加则要在使用 open 函数时添加 O_APPEND 标记。
6. 资源管理: 使用 close()
显式关闭不再需要的文件描述符,避免泄漏
🎯总结
通过学习和实践 open
, close
, read
, write
这四个基础系统调用,我们掌握了在Linux下进行文件I/O操作的核心能力。它们是构建更复杂文件操作、管道、套接字通信等的基石。理解文件描述符的概念、牢记检查返回值、正确处理部分I/O和错误情况、合理使用标志位(如 O_APPEND
)、管理好文件描述符资源,是编写健壮系统程序的关键。
创作不易,如果觉得本文对你有帮助,欢迎点赞、收藏、评论,关注我获取更多 Linux/C++ 系统编程干货!
👉 你的每一次互动,都是我持续输出的动力!