Linux C语言文件系统调用的深入剖析
Linux 文件系统基础
在深入探讨 Linux C 语言文件系统调用之前,有必要先了解一些 Linux 文件系统的基础知识。
文件系统结构
Linux 文件系统采用一种层次化的树形结构。根目录(/
)位于树的顶端,从根目录开始,众多的子目录和文件构成了整个文件系统。例如,/etc
目录通常存放系统配置文件,/usr
目录包含用户相关的程序和数据等。
在 Linux 中,一切皆文件。设备(如硬盘、串口等)也被表示为文件,这极大地简化了系统的管理和编程。比如,硬盘设备文件可能是 /dev/sda
,串口设备文件可能是 /dev/ttyS0
。
inode 与数据块
每个文件在 Linux 文件系统中都由两部分重要信息组成:inode(索引节点)和数据块。
inode 记录了文件的元数据,如文件的所有者、权限、大小、创建时间、修改时间等,同时还包含指向数据块的指针。一个文件的 inode 是唯一的,系统通过 inode 来识别和管理文件。
数据块则实际存储文件的内容。对于大文件,可能会分散存储在多个数据块中,inode 中的指针会指向这些数据块。例如,一个文本文件的内容就存储在数据块中,而文件的权限等信息存储在 inode 中。
C 语言文件操作函数与系统调用的区别
在 C 语言中,我们熟悉一些标准库提供的文件操作函数,如 fopen
、fread
、fwrite
等。这些函数是基于缓冲区的,它们在用户空间实现,为程序员提供了方便易用的接口。
而 Linux 文件系统调用,如 open
、read
、write
等,是直接与内核交互的接口。它们没有用户空间的缓冲区,直接操作内核缓冲区。
例如,fwrite
函数在写入数据时,会先将数据写入用户空间的缓冲区,当缓冲区满或者调用 fflush
函数时,才会将数据真正写入内核缓冲区并最终写入磁盘。而 write
系统调用则直接将数据写入内核缓冲区。
这种区别带来了不同的性能和行为特点。标准库函数的缓冲区机制可以减少系统调用的次数,提高 I/O 效率,但也可能导致数据不能及时写入磁盘。而系统调用则更直接,适用于对数据实时性要求较高的场景。
常用的 Linux C 语言文件系统调用
open 系统调用
open
系统调用用于打开或创建一个文件,其函数原型如下:
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.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
(如果文件不存在则创建)、O_APPEND
(追加写入)等。mode
:当使用O_CREAT
标志创建文件时,用于指定文件的权限,如S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH
表示文件所有者有读写权限,组用户和其他用户有读权限。
示例代码:
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
int fd;
fd = open("test.txt", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open");
return 1;
}
printf("File opened successfully with fd: %d\n", fd);
close(fd);
return 0;
}
在这个例子中,我们尝试以只写和创建的方式打开 test.txt
文件,如果文件不存在则创建,文件权限设置为所有者可读写。如果 open
调用失败,perror
函数会打印错误信息。
close 系统调用
close
系统调用用于关闭一个打开的文件描述符,其函数原型为:
#include <unistd.h>
int close(int fd);
fd
:要关闭的文件描述符。
关闭文件描述符可以释放系统资源,并且将缓冲区中的数据写入磁盘(如果有未写入的数据)。例如,在上面 open
示例代码中,最后我们调用 close(fd)
关闭打开的文件。
read 系统调用
read
系统调用用于从打开的文件中读取数据,其函数原型为:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
fd
:要读取数据的文件描述符。buf
:用于存储读取数据的缓冲区。count
:要读取的字节数。
返回值为实际读取的字节数,如果返回 0
表示到达文件末尾,返回 -1
表示发生错误。
示例代码:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
char buffer[1024];
ssize_t bytes_read;
fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read == -1) {
perror("read");
close(fd);
return 1;
}
buffer[bytes_read] = '\0';
printf("Read %zd bytes: %s\n", bytes_read, buffer);
close(fd);
return 0;
}
在这个例子中,我们打开 test.txt
文件进行只读操作,然后从文件中读取数据到 buffer
缓冲区,最后打印读取的字节数和数据内容。
write 系统调用
write
系统调用用于向打开的文件中写入数据,其函数原型为:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
fd
:要写入数据的文件描述符。buf
:包含要写入数据的缓冲区。count
:要写入的字节数。
返回值为实际写入的字节数,如果返回 -1
表示发生错误。
示例代码:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
const char *message = "Hello, Linux file system!";
ssize_t bytes_written;
fd = open("test.txt", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open");
return 1;
}
bytes_written = write(fd, message, strlen(message));
if (bytes_written == -1) {
perror("write");
close(fd);
return 1;
}
printf("Written %zd bytes to the file.\n", bytes_written);
close(fd);
return 0;
}
在这个例子中,我们打开 test.txt
文件进行只写操作,如果文件不存在则创建,然后将字符串 Hello, Linux file system!
写入文件,并打印实际写入的字节数。
文件描述符与文件表
文件描述符
在 Linux 系统中,文件描述符是一个非负整数,它是内核为了标识一个打开的文件而分配给进程的。当进程调用 open
系统调用成功打开一个文件时,内核会返回一个文件描述符。
每个进程都有一个文件描述符表,该表记录了进程打开的所有文件描述符及其对应的文件状态信息。文件描述符通常从 0
开始分配,在一个进程中,0
通常代表标准输入(stdin
),1
代表标准输出(stdout
),2
代表标准错误输出(stderr
)。
例如,在前面的代码示例中,open
函数返回的 fd
就是一个文件描述符,我们可以通过这个文件描述符来对打开的文件进行读写等操作。
文件表
除了进程的文件描述符表,系统还有一个文件表。文件表记录了系统中所有打开文件的状态信息,包括文件的当前读写位置、文件的访问模式等。
当一个进程打开一个文件时,内核会在文件表中为该文件创建一个表项,并将文件描述符与文件表中的表项关联起来。多个进程可以通过不同的文件描述符指向同一个文件表项,从而共享对同一个文件的访问。
例如,两个进程都打开同一个文件,它们会获得不同的文件描述符,但这些文件描述符都指向文件表中同一个文件的表项。这就使得不同进程对文件的操作(如读写位置的移动)会相互影响。
文件属性操作系统调用
fstat 系统调用
fstat
系统调用用于获取打开文件的属性信息,其函数原型为:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int fstat(int fd, struct stat *buf);
fd
:要获取属性的文件描述符。buf
:用于存储文件属性信息的结构体指针,struct stat
结构体定义如下:
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* file type and mode */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for file system I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last status change */
};
示例代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd;
struct stat file_stat;
fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
if (fstat(fd, &file_stat) == -1) {
perror("fstat");
close(fd);
return 1;
}
printf("File size: %ld bytes\n", (long)file_stat.st_size);
printf("Last modification time: %ld\n", (long)file_stat.st_mtime);
close(fd);
return 0;
}
在这个例子中,我们打开 test.txt
文件,然后使用 fstat
获取文件的大小和最后修改时间,并打印出来。
stat 系统调用
stat
系统调用与 fstat
类似,但它通过文件名来获取文件属性,而不是文件描述符,其函数原型为:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *pathname, struct stat *buf);
pathname
:要获取属性的文件路径名。buf
:与fstat
中的buf
作用相同。
示例代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
struct stat file_stat;
if (stat("test.txt", &file_stat) == -1) {
perror("stat");
return 1;
}
printf("File size: %ld bytes\n", (long)file_stat.st_size);
printf("Last access time: %ld\n", (long)file_stat.st_atime);
return 0;
}
此示例通过 stat
直接获取 test.txt
文件的大小和最后访问时间并打印。
chmod 系统调用
chmod
系统调用用于改变文件的权限,其函数原型为:
#include <sys/types.h>
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
pathname
:要改变权限的文件路径名。mode
:新的文件权限,如S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH
。
示例代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
int main() {
if (chmod("test.txt", S_IRUSR | S_IWUSR | S_IRGRP) == -1) {
perror("chmod");
return 1;
}
printf("File permissions changed successfully.\n");
return 0;
}
在这个例子中,我们将 test.txt
文件的权限修改为所有者可读写,组用户可读。
文件目录操作系统调用
mkdir 系统调用
mkdir
系统调用用于创建一个新的目录,其函数原型为:
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
pathname
:要创建的目录路径名。mode
:新目录的权限,如S_IRWXU | S_IRWXG | S_IRWXO
表示所有者、组用户和其他用户都有读、写和执行权限。
示例代码:
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
int main() {
if (mkdir("new_dir", S_IRWXU) == -1) {
perror("mkdir");
return 1;
}
printf("Directory new_dir created successfully.\n");
return 0;
}
此代码尝试创建一个名为 new_dir
的目录,权限设置为所有者有读、写和执行权限。
rmdir 系统调用
rmdir
系统调用用于删除一个空目录,其函数原型为:
#include <unistd.h>
int rmdir(const char *pathname);
pathname
:要删除的目录路径名。
示例代码:
#include <stdio.h>
#include <unistd.h>
int main() {
if (rmdir("new_dir") == -1) {
perror("rmdir");
return 1;
}
printf("Directory new_dir deleted successfully.\n");
return 0;
}
在这个例子中,我们删除之前创建的 new_dir
目录,如果目录不为空,rmdir
调用会失败。
opendir、readdir 和 closedir 系统调用
opendir
用于打开一个目录,readdir
用于读取目录中的条目,closedir
用于关闭打开的目录,它们的函数原型如下:
#include <dirent.h>
DIR *opendir(const char *name);
struct dirent *readdir(DIR *dirp);
int closedir(DIR *dirp);
opendir
的name
参数是要打开的目录路径名,返回值是一个DIR
类型的指针,用于后续的readdir
和closedir
操作。readdir
的dirp
参数是opendir
返回的DIR
指针,返回值是一个struct dirent
结构体指针,该结构体定义如下:
struct dirent {
ino_t d_ino; /* inode number */
off_t d_off; /* offset to the next dirent */
unsigned short d_reclen; /* length of this record */
unsigned char d_type; /* type of file; not supported by all file system types */
char d_name[256]; /* filename */
};
closedir
的dirp
参数同样是opendir
返回的DIR
指针,用于关闭目录并释放资源。
示例代码:
#include <stdio.h>
#include <dirent.h>
int main() {
DIR *dir;
struct dirent *entry;
dir = opendir(".");
if (dir == NULL) {
perror("opendir");
return 1;
}
while ((entry = readdir(dir)) != NULL) {
printf("Directory entry: %s\n", entry->d_name);
}
closedir(dir);
return 0;
}
在这个例子中,我们打开当前目录,然后使用 readdir
读取目录中的每个条目并打印其名称,最后关闭目录。
文件系统调用的错误处理
在使用 Linux C 语言文件系统调用时,错误处理是非常重要的。几乎所有的系统调用在失败时都会返回 -1
,并设置 errno
变量来表示具体的错误原因。
errno
是一个全局变量,定义在 <errno.h>
头文件中。常见的 errno
值及其含义如下:
EACCES
:权限不足,例如尝试打开一个没有读权限的文件。ENOENT
:文件或目录不存在,如open
一个不存在的文件。EBUSY
:设备或资源忙,例如尝试删除一个正在被使用的文件。
在编写代码时,应该始终检查系统调用的返回值,并在失败时使用 perror
或 strerror
函数来获取错误信息。perror
函数会在标准错误输出上打印一条包含错误描述的消息,而 strerror
函数会返回一个指向错误描述字符串的指针。
例如,在前面的 open
示例代码中:
fd = open("test.txt", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("open");
return 1;
}
这里如果 open
调用失败,perror("open")
会打印类似于 open: No such file or directory
的错误信息,方便我们定位问题。
高级文件系统调用应用场景
实现简单文件复制
我们可以利用文件系统调用实现一个简单的文件复制程序。思路是先打开源文件进行读取,然后创建目标文件并进行写入。
示例代码:
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#define BUFFER_SIZE 1024
int main(int argc, char *argv[]) {
if (argc != 3) {
printf("Usage: %s source_file destination_file\n", argv[0]);
return 1;
}
int source_fd, dest_fd;
char buffer[BUFFER_SIZE];
ssize_t bytes_read, bytes_written;
source_fd = open(argv[1], O_RDONLY);
if (source_fd == -1) {
perror("open source file");
return 1;
}
dest_fd = open(argv[2], O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
if (dest_fd == -1) {
perror("open destination file");
close(source_fd);
return 1;
}
while ((bytes_read = read(source_fd, buffer, BUFFER_SIZE)) > 0) {
bytes_written = write(dest_fd, buffer, bytes_read);
if (bytes_written == -1) {
perror("write to destination file");
close(source_fd);
close(dest_fd);
return 1;
}
}
if (bytes_read == -1) {
perror("read from source file");
close(source_fd);
close(dest_fd);
return 1;
}
close(source_fd);
close(dest_fd);
printf("File copied successfully.\n");
return 0;
}
在这个例子中,我们从命令行获取源文件和目标文件的名称,打开源文件进行读取,打开目标文件进行写入。通过循环读取源文件数据并写入目标文件,实现文件复制功能。
监控文件变化
可以利用文件系统调用结合 inotify
机制来监控文件或目录的变化。inotify
是 Linux 内核提供的一种文件系统变化通知机制。
首先需要包含 <sys/inotify.h>
头文件,主要用到的函数有 inotify_init
、inotify_add_watch
和 read
。
inotify_init
用于初始化 inotify
实例,返回一个文件描述符:
int inotify_init(void);
inotify_add_watch
用于为指定的文件或目录添加监控项,其函数原型为:
int inotify_add_watch(int fd, const char *pathname, uint32_t mask);
fd
:inotify_init
返回的文件描述符。pathname
:要监控的文件或目录路径名。mask
:监控的事件类型,如IN_MODIFY
(文件被修改)、IN_CREATE
(文件被创建)等。
示例代码:
#include <stdio.h>
#include <sys/inotify.h>
#include <unistd.h>
#include <string.h>
#define EVENT_SIZE (sizeof(struct inotify_event))
#define BUFFER_SIZE (1024 * (EVENT_SIZE + 16))
int main() {
int fd, wd;
char buffer[BUFFER_SIZE];
ssize_t length;
fd = inotify_init();
if (fd == -1) {
perror("inotify_init");
return 1;
}
wd = inotify_add_watch(fd, ".", IN_MODIFY | IN_CREATE | IN_DELETE);
if (wd == -1) {
perror("inotify_add_watch");
close(fd);
return 1;
}
printf("Monitoring current directory for changes...\n");
length = read(fd, buffer, BUFFER_SIZE);
if (length == -1) {
perror("read");
close(fd);
return 1;
}
int i = 0;
while (i < length) {
struct inotify_event *event = (struct inotify_event *)&buffer[i];
if (event->len) {
if (event->mask & IN_CREATE) {
if (event->mask & IN_ISDIR) {
printf("Directory %s created.\n", event->name);
} else {
printf("File %s created.\n", event->name);
}
} else if (event->mask & IN_MODIFY) {
printf("File %s modified.\n", event->name);
} else if (event->mask & IN_DELETE) {
if (event->mask & IN_ISDIR) {
printf("Directory %s deleted.\n", event->name);
} else {
printf("File %s deleted.\n", event->name);
}
}
}
i += EVENT_SIZE + event->len;
}
close(fd);
return 0;
}
在这个例子中,我们初始化 inotify
实例,为当前目录添加监控项,监控文件的创建、修改和删除事件。通过 read
读取监控到的事件,并根据事件类型打印相应的信息。
通过以上对 Linux C 语言文件系统调用的深入剖析,我们对文件系统操作有了更全面和深入的理解,这将有助于我们编写高效、可靠的 Linux 应用程序。无论是日常的文件处理,还是系统级的文件管理任务,这些知识都具有重要的实用价值。