MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Linux C语言文件系统调用的深入剖析

2022-08-143.1k 阅读

Linux 文件系统基础

在深入探讨 Linux C 语言文件系统调用之前,有必要先了解一些 Linux 文件系统的基础知识。

文件系统结构

Linux 文件系统采用一种层次化的树形结构。根目录(/)位于树的顶端,从根目录开始,众多的子目录和文件构成了整个文件系统。例如,/etc 目录通常存放系统配置文件,/usr 目录包含用户相关的程序和数据等。

在 Linux 中,一切皆文件。设备(如硬盘、串口等)也被表示为文件,这极大地简化了系统的管理和编程。比如,硬盘设备文件可能是 /dev/sda,串口设备文件可能是 /dev/ttyS0

inode 与数据块

每个文件在 Linux 文件系统中都由两部分重要信息组成:inode(索引节点)和数据块。

inode 记录了文件的元数据,如文件的所有者、权限、大小、创建时间、修改时间等,同时还包含指向数据块的指针。一个文件的 inode 是唯一的,系统通过 inode 来识别和管理文件。

数据块则实际存储文件的内容。对于大文件,可能会分散存储在多个数据块中,inode 中的指针会指向这些数据块。例如,一个文本文件的内容就存储在数据块中,而文件的权限等信息存储在 inode 中。

C 语言文件操作函数与系统调用的区别

在 C 语言中,我们熟悉一些标准库提供的文件操作函数,如 fopenfreadfwrite 等。这些函数是基于缓冲区的,它们在用户空间实现,为程序员提供了方便易用的接口。

而 Linux 文件系统调用,如 openreadwrite 等,是直接与内核交互的接口。它们没有用户空间的缓冲区,直接操作内核缓冲区。

例如,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);
  • opendirname 参数是要打开的目录路径名,返回值是一个 DIR 类型的指针,用于后续的 readdirclosedir 操作。
  • readdirdirp 参数是 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 */
};
  • closedirdirp 参数同样是 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:设备或资源忙,例如尝试删除一个正在被使用的文件。

在编写代码时,应该始终检查系统调用的返回值,并在失败时使用 perrorstrerror 函数来获取错误信息。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_initinotify_add_watchread

inotify_init 用于初始化 inotify 实例,返回一个文件描述符:

int inotify_init(void);

inotify_add_watch 用于为指定的文件或目录添加监控项,其函数原型为:

int inotify_add_watch(int fd, const char *pathname, uint32_t mask);
  • fdinotify_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 应用程序。无论是日常的文件处理,还是系统级的文件管理任务,这些知识都具有重要的实用价值。