目录
在 Linux 系统中,文件操作是非常核心且基础的功能。无论是应用程序的开发,还是对系统的日常管理,都离不开对文件的创建、打开、读写、定位和关闭等操作。而这些操作的底层实现,很大程度上依赖于系统调用。本文将详细介绍 Linux 文件操作中涉及的主要系统调用,深入理解 Linux 文件操作的原理和实现方式。
一、文件创建(creat 系统调用)
在 Linux 中,创建一个新文件可以使用 creat 系统调用。它的主要功能是创建一个新的空文件,如果指定的文件已经存在,则会将其截断为零长度(即清空文件内容)。
1.1 函数原型
#include <fcntl.h>
int creat(const char *pathname, mode_t mode);
1.2 参数说明
- pathname:指向要创建的文件路径名的字符串指针。可以是绝对路径,也可以是相对路径。
- mode:指定新文件的权限模式。它由一系列权限位组成,这些权限位决定了文件所有者、所属组和其他用户对该文件的访问权限。常见的权限位有:
- S_IRUSR:所有者具有读权限(0400)
- S_IWUSR:所有者具有写权限(0200)
- S_IXUSR:所有者具有执行权限(0100)
- S_IRGRP:所属组具有读权限(0040)
- S_IWGRP:所属组具有写权限(0020)
- S_IXGRP:所属组具有执行权限(0010)
- S_IROTH:其他用户具有读权限(0004)
- S_IWOTH:其他用户具有写权限(0002)
- S_IXOTH:其他用户具有执行权限(0001)
这些权限位可以通过按位或(|)运算组合使用,例如,S_IRUSR | S_IWUSR 表示所有者具有读写权限。
1.3 返回值
- 成功:返回一个非负的文件描述符(file descriptor),该文件描述符用于后续对文件的操作。
- 失败:返回 - 1,并设置 errno 来指示错误类型。常见的错误有:
- EEXIST:指定的文件已经存在,且没有被截断(当使用 O_CREAT | O_EXCL 标志时,如果文件存在则会返回该错误,但 creat 系统调用本身如果文件存在会截断文件,所以此错误在 creat 中较少见)
- ENOENT:路径名中的目录不存在
- EACCES:没有权限创建文件或访问路径中的目录
- ENOSPC:文件系统没有足够的空间创建新文件
1.4 使用示例
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
int main() {
int fd;
// 创建一个名为"testfile.txt"的文件,所有者具有读写权限,所属组和其他用户无权限
fd = creat("testfile.txt", S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("creat failed");
exit(EXIT_FAILURE);
}
printf("File created successfully with file descriptor: %d\n", fd);
close(fd); // 创建文件后要记得关闭文件描述符
return 0;
}
使用 creat 函数创建了一个名为 “testfile.txt” 的文件,设置所有者具有读写权限。如果创建成功,会输出文件描述符;如果失败,会通过 perror 函数打印错误信息。
二、文件打开(open 系统调用)
open 系统调用是 Linux 中用于打开一个已存在的文件或创建一个新文件的主要系统调用。它比 creat 系统调用功能更强大,可以通过不同的标志组合实现多种操作。
2.1 函数原型
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
2.2 参数说明
pathname:与 creat 系统调用中的 pathname 参数相同,指向要打开或创建的文件路径名。
flags:用于指定打开文件的方式和行为,是一个整数,可以由多个标志按位或组合而成。常见的标志有:
- O_RDONLY:以只读方式打开文件
- O_WRONLY:以只写方式打开文件
- O_RDWR:以读写方式打开文件
- O_CREAT:如果指定的文件不存在,则创建该文件。此时需要提供第三个参数 mode 来指定新文件的权限。
- O_EXCL:与 O_CREAT 一起使用,如果文件已经存在,则 open 调用失败(返回 - 1),可以用于防止覆盖已存在的文件。
- O_TRUNC:如果文件已经存在且以可写方式打开(O_WRONLY 或 O_RDWR),则将文件截断为零长度。
- O_APPEND:以追加方式打开文件,每次写操作都将数据添加到文件的末尾。
mode:当 flags 中包含 O_CREAT 标志时,该参数用于指定新文件的权限模式,与 creat 系统调用中的 mode 参数相同。如果 flags 中不包含 O_CREAT,则该参数被忽略。
2.3 返回值
- 成功:返回一个非负的文件描述符,用于后续对文件的操作。
- 失败:返回 - 1,并设置 errno 指示错误类型。常见的错误与 creat 系统调用类似,此外还有:
- EINVAL:flags 参数无效
- EMFILE:进程已打开的文件描述符达到上限
2.4 使用示例
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
int fd1, fd2, fd3;
// 以只读方式打开已存在的文件"testfile.txt"
fd1 = open("testfile.txt", O_RDONLY);
if (fd1 == -1) {
perror("open for read failed");
exit(EXIT_FAILURE);
}
// 以读写方式打开文件,如果文件不存在则创建,权限为所有者读写,组和其他用户只读
fd2 = open("newfile.txt", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (fd2 == -1) {
perror("open for read-write and create failed");
close(fd1);
exit(EXIT_FAILURE);
}
// 以只写、追加方式打开文件,若文件不存在则创建
fd3 = open("logfile.txt", O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
if (fd3 == -1) {
perror("open for write and append failed");
close(fd1);
close(fd2);
exit(EXIT_FAILURE);
}
printf("fd1: %d, fd2: %d, fd3: %d\n", fd1, fd2, fd3);
// 关闭文件描述符
close(fd1);
close(fd2);
close(fd3);
return 0;
}
展示了 open 系统调用的几种常见用法:只读打开已存在文件、读写打开并创建新文件、只写追加打开并创建新文件。
三、文件读写(read 和 write 系统调用)
打开文件后,就可以进行读写操作了。Linux 提供了 read 和 write 系统调用来实现对文件的读写。
3.1 read 系统调用
read 系统调用用于从已打开的文件中读取数据。
①函数原型
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
②参数说明
- fd:文件描述符,即 open 或 creat 系统调用返回的非负整数,标识要读取的文件。
- buf:指向一个缓冲区的指针,用于存储读取到的数据。
- count:指定要读取的字节数。
③ 返回值
- 成功:返回实际读取到的字节数。如果已经到达文件末尾,则返回 0。
- 失败:返回 - 1,并设置 errno 指示错误类型,如 EBADF(文件描述符无效)、EIO(I/O 错误)等。
3.2 write 系统调用
write 系统调用用于向已打开的文件中写入数据。
①函数原型
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
②参数说明
- fd:文件描述符,标识要写入的文件。
- buf:指向一个缓冲区的指针,该缓冲区中存储了要写入的数据。
- count:指定要写入的字节数。
③返回值
- 成功:返回实际写入的字节数。注意,实际写入的字节数可能小于 count,这并不一定表示错误,可能是由于各种原因(如磁盘空间不足、信号中断等)导致的。
- 失败:返回 - 1,并设置 errno 指示错误类型,如 EBADF(文件描述符无效或不具有写权限)、EIO(I/O 错误)等。
3.3 使用示例
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd;
char write_buf[] = "Hello, Linux File Operation!";
char read_buf[1024];
ssize_t nwritten, nread;
// 以读写方式打开文件,不存在则创建
fd = open("rwtest.txt", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open failed");
exit(EXIT_FAILURE);
}
// 写入数据
nwritten = write(fd, write_buf, strlen(write_buf));
if (nwritten == -1) {
perror("write failed");
close(fd);
exit(EXIT_FAILURE);
}
printf("Wrote %zd bytes: %s\n", nwritten, write_buf);
// 将文件指针移到文件开头(否则读取不到刚写入的数据)
off_t offset = lseek(fd, 0, SEEK_SET);
if (offset == -1) {
perror("lseek failed");
close(fd);
exit(EXIT_FAILURE);
}
// 读取数据
nread = read(fd, read_buf, sizeof(read_buf) - 1); // 留一个字节给'\0'
if (nread == -1) {
perror("read failed");
close(fd);
exit(EXIT_FAILURE);
} else if (nread == 0) {
printf("Reached end of file\n");
} else {
read_buf[nread] = '\0'; // 加上字符串结束符
printf("Read %zd bytes: %s\n", nread, read_buf);
}
close(fd);
return 0;
}
先向文件写入一段字符串,然后使用 lseek 系统调用将文件指针移到文件开头,再读取文件内容并打印。需要注意的是,write 调用返回的实际写入字节数可能小于请求的字节数,因此在实际应用中可能需要循环写入,直到所有数据都被写入。同样,read 调用也可能需要循环读取,直到读取到预期的数据量或到达文件末尾。
四、文件定位(lseek 系统调用)
在对文件进行读写操作时,系统会维护一个文件偏移量(file offset),也称为文件指针,它指示了下一次读写操作开始的位置。lseek 系统调用用于修改这个文件偏移量,实现对文件的随机访问。
4.1 函数原型
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
4.2 参数说明
- fd:文件描述符,标识要操作的文件。
- offset:偏移量,是一个有符号整数,表示要移动的字节数。
- whence:用于指定偏移量的参考位置,有以下三种取值:
- SEEK_SET:将文件偏移量设置为 offset 字节(从文件开头开始计算)。
- SEEK_CUR:将文件偏移量设置为当前偏移量加上 offset 字节。
- SEEK_END:将文件偏移量设置为文件末尾加上 offset 字节(offset 为负时表示向文件开头方向移动)。
4.3 返回值
- 成功:返回新的文件偏移量(从文件开头开始计算的字节数)。
- 失败:返回 - 1,并设置 errno 指示错误类型,如 EBADF(文件描述符无效)、ESPIPE(文件是管道、FIFO 或套接字,这些文件不支持定位操作)等。
4.4 使用示例
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd;
char buf[100];
off_t offset;
// 创建并打开一个文件
fd = open("seektest.txt", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open failed");
exit(EXIT_FAILURE);
}
// 写入一些数据
const char *data = "0123456789abcdefghijklmnopqrstuvwxyz";
write(fd, data, strlen(data));
// 将文件指针移到开头
offset = lseek(fd, 0, SEEK_SET);
if (offset == -1) {
perror("lseek to beginning failed");
close(fd);
exit(EXIT_FAILURE);
}
printf("Moved to beginning, offset: %ld\n", (long)offset);
// 读取5个字节
ssize_t nread = read(fd, buf, 5);
if (nread == -1) {
perror("read failed");
close(fd);
exit(EXIT_FAILURE);
}
buf[nread] = '\0';
printf("Read %zd bytes: %s\n", nread, buf);
// 从当前位置向后移动10个字节
offset = lseek(fd, 10, SEEK_CUR);
if (offset == -1) {
perror("lseek from current failed");
close(fd);
exit(EXIT_FAILURE);
}
printf("Moved 10 bytes from current, new offset: %ld\n", (long)offset);
// 读取5个字节
nread = read(fd, buf, 5);
if (nread == -1) {
perror("read failed");
close(fd);
exit(EXIT_FAILURE);
}
buf[nread] = '\0';
printf("Read %zd bytes: %s\n", nread, buf);
// 从文件末尾向前移动20个字节
offset = lseek(fd, -20, SEEK_END);
if (offset == -1) {
perror("lseek from end failed");
close(fd);
exit(EXIT_FAILURE);
}
printf("Moved 20 bytes before end, new offset: %ld\n", (long)offset);
// 读取5个字节
nread = read(fd, buf, 5);
if (nread == -1) {
perror("read failed");
close(fd);
exit(EXIT_FAILURE);
}
buf[nread] = '\0';
printf("Read %zd bytes: %s\n", nread, buf);
close(fd);
return 0;
}
展示了 lseek 系统调用的三种使用方式:从文件开头设置偏移量、从当前位置调整偏移量、从文件末尾调整偏移量。通过 lseek,我们可以灵活地定位到文件的任意位置进行读写操作,实现随机访问。
五、文件关闭(close 系统调用)
当对文件的操作完成后,需要使用 close 系统调用来关闭文件,释放与该文件相关的资源,如文件描述符等。
5.1 函数原型
#include <unistd.h>
int close(int fd);
5.2 参数说明
- fd:要关闭的文件的文件描述符。
5.3 返回值
- 成功:返回 0。
- 失败:返回 - 1,并设置 errno 指示错误类型,如 EBADF(文件描述符无效或已经关闭)等。
5.4 注意事项
- 每个进程能打开的文件描述符数量是有限的,因此在使用完文件后,一定要及时调用 close 关闭文件,以释放文件描述符,避免资源耗尽。
- 关闭文件后,该文件描述符将不再有效,不能再用于对文件的读写等操作。
5.5 使用示例
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
int fd;
fd = open("closetest.txt", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open failed");
exit(EXIT_FAILURE);
}
// 对文件进行一些操作...
// 关闭文件
if (close(fd) == -1) {
perror("close failed");
exit(EXIT_FAILURE);
}
printf("File closed successfully\n");
return 0;
}
这个示例比较简单,展示了打开文件后进行一些操作,然后关闭文件的过程。在实际应用中,无论对文件的操作是否成功,都应该在适当的时候关闭文件。
六、总结
在实际使用这些系统调用时,需要注意以下几点:
- 始终检查系统调用的返回值,及时处理可能出现的错误。
- 注意文件权限的设置,确保文件的访问安全。
- 及时关闭不再使用的文件,释放系统资源。
- 对于读写操作,要考虑到实际读写的字节数可能小于请求的字节数,必要时进行循环操作。
随着 Linux 系统的不断发展,文件操作的方式和接口也在不断完善。但这些基础的系统调用仍然是理解 Linux 文件系统和进行底层开发的关键。未来,我们可以进一步学习 Linux 文件系统的底层原理,以及如何利用这些系统调用来实现更复杂的文件操作功能,如文件复制、移动、权限修改等。同时,也可以学习更高层次的文件操作库函数(如标准 C 库中的 fopen、fread、fwrite 等),它们是对这些系统调用的封装,使用起来更加方便,但了解其底层实现的系统调用有助于我们更好地理解和使用这些库函数。
通过不断学习和实践,我们可以更深入地掌握 Linux 文件操作技术,为开发高效、稳定的 Linux 应用程序打下坚实的基础。