C 进阶(3) - 文件和目录

本节将描述文件系统的其他特征和文件的性质

1. stat, fstat, lstat 函数

在 Linux 系统编程中,statfstatlstat 是用于获取文件元数据(即"文件的身份信息")的一组核心系统调用。

它们就像是文件的"体检报告",能告诉你文件的大小、权限、所有者、时间戳以及它在磁盘上的具体位置(inode 编号)等信息。

📊 三者的核心区别

这三个函数最终获取到的信息是一模一样的(都填充到 struct stat 结构体中),它们唯一的区别在于**"如何找到目标文件"** 以及**"遇到符号链接时的态度"**:

函数 传入参数 遇到符号链接(软链接)时
stat 文件路径名 穿透:跟随链接,返回实际指向文件的信息
lstat 文件路径名 不穿透:返回符号链接本身的信息
fstat 文件描述符 (fd) 不涉及路径,直接返回已打开文件的信息

在 Linux 系统编程中,statfstatlstat 是用于获取文件元数据(即"文件的身份信息")的一组核心系统调用。

结合你之前关注的文件 I/O 效率与底层驱动开发,理解这组函数至关重要。它们就像是文件的"体检报告",能告诉你文件的大小、权限、所有者、时间戳以及它在磁盘上的具体位置(inode 编号)等信息。

📊 三者的核心区别

这三个函数最终获取到的信息是一模一样的(都填充到 struct stat 结构体中),它们唯一的区别在于**"如何找到目标文件"** 以及**"遇到符号链接时的态度"**:

函数 传入参数 遇到符号链接(软链接)时
stat 文件路径名 穿透 :跟随链接,返回实际指向文件的信息
lstat 文件路径名 不穿透 :返回符号链接本身的信息
fstat 文件描述符 (fd) 不涉及路径,直接返回已打开文件的信息

⚙️ 函数原型与 struct stat

在使用前,通常需要包含 <sys/stat.h><unistd.h> 头文件。

复制代码
#include <sys/stat.h>
#include <unistd.h>

int stat(const char *pathname, struct stat *buf);
int lstat(const char *pathname, struct stat *buf);
int fstat(int fd, struct stat *buf);
  • 返回值 :成功返回 0,失败返回 -1 并设置 errno(如文件不存在 ENOENT、权限不足 EACCES 等)。

获取到的信息会被填充到 struct stat 结构体中,以下是你在驱动开发和系统运维中最关心的几个字段:

复制代码
struct stat {
    dev_t     st_dev;     // 文件所在的设备 ID
    ino_t     st_ino;     // 文件的 inode 节点号(文件的唯一身份证)
    mode_t    st_mode;    // 文件类型和权限(如 S_IFREG, S_IFDIR)
    nlink_t   st_nlink;   // 硬链接的数量
    uid_t     st_uid;     // 文件所有者的用户 ID
    gid_t     st_gid;     // 文件所有者的组 ID
    off_t     st_size;    // 文件总大小(字节)
    time_t    st_atime;   // 最后一次访问文件的时间
    time_t    st_mtime;   // 文件内容最后一次被修改的时间
    time_t    st_ctime;   // 文件状态(权限、所有者等)最后一次改变的时间
};

注:现代 Linux 内核中,时间戳通常已经升级为纳秒级精度的 struct timespec

💡 实战场景解析

1. stat:日常运维的"透视眼"

当你通过路径名查看文件信息时,通常使用 stat。它会"穿透"符号链接,让你看到最终文件的真实大小和属性。

  • 典型场景 :你在 Shell 中执行 ls -l 命令时,底层调用的就是 stat,它显示的是链接指向的真实文件信息。
2. lstat:驱动与文件管理的"显微镜"

lstat 专门用于识别符号链接本身。如果你对一个软链接调用 stat,你得到的是目标文件的信息;而调用 lstat,你得到的是这个链接文件本身的信息(比如链接文件本身的大小通常就是它存储的路径字符串的长度)。

  • 典型场景 :在编写文件遍历工具、打包工具(如 tar)或者设备驱动中需要精确识别文件类型时,必须用 lstat。否则,如果遇到指向目录的循环软链接,程序可能会陷入死循环。
3. fstat:高性能 I/O 的"贴身管家"

fstat 不需要再次通过路径名去查找文件,而是直接通过已经打开的文件描述符(fd)获取信息。

  • 典型场景
    • 效率极高 :在频繁读写文件时,你已经有了 fd,使用 fstat 可以避免内核再次进行耗时的路径名解析。
    • 匿名文件 :对于管道(pipe)、套接字(socket)等没有具体路径名的文件,只能通过 fstat 来获取它们的状态。

💻 综合代码示例

下面是一个简单的 C 语言示例,演示了如何判断一个路径是普通文件、目录还是符号链接:

复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("用法: %s <文件路径>\n", argv[0]);
        return 1;
    }

    struct stat buf;
    // 使用 lstat 能够准确识别出符号链接本身
    if (lstat(argv[1], &buf) == -1) {
        perror("获取文件状态失败");
        return 1;
    }

    // 通过 st_mode 字段配合宏来判断文件类型
    if (S_ISREG(buf.st_mode)) {
        printf("%s 是一个普通文件,大小: %ld 字节\n", argv[1], buf.st_size);
    } 
    else if (S_ISDIR(buf.st_mode)) {
        printf("%s 是一个目录\n", argv[1]);
    } 
    else if (S_ISLNK(buf.st_mode)) {
        printf("%s 是一个符号链接\n", argv[1]);
        // 如果想知道它指向哪里,可以再用 stat 穿透获取真实文件信息
        if (stat(argv[1], &buf) == 0) {
            printf("它指向的真实文件大小为: %ld 字节\n", buf.st_size);
        }
    }
    // 还可以判断 S_ISCHR (字符设备), S_ISBLK (块设备) 等
    return 0;
}

总结一下

  • 如果你手里有 fd,优先用 fstat(最快)。
  • 如果你只有路径名,且想穿透软链接看真实文件,用 stat
  • 如果你需要精准识别路径本身是不是软链接(比如做文件备份、驱动资源管理),务必用 lstat

文件类型

Linux 系统一共定义了 7 种 POSIX 标准文件类型 。在使用 ls -l 命令查看文件时,输出结果的第一个字符就代表了该文件的类型:

标识符 文件类型 英文全称 说明与常见示例
- 普通文件 Regular file 最常见的文件,包含文本、图片、二进制程序、压缩包等。
d 目录文件 Directory 用于组织文件的文件夹,里面存放的是文件名和 inode 的映射列表。
l 符号链接 Symbolic link 类似于 Windows 的"快捷方式",指向另一个文件的路径。
c 字符设备 Character device 以字节流(顺序)方式读写的硬件,如键盘、鼠标、串口 (/dev/tty)。
b 块设备 Block device 以固定大小的数据块(支持随机读写)访问的硬件,如硬盘、U盘 (/dev/sda)。
p 命名管道 Named pipe (FIFO) 用于两个进程之间单向传递数据的特殊文件,大小为 0 字节。
s 套接字 Socket 用于进程间通信(包括本机或网络通信),常见于服务程序的通信接口。

📂 深入理解各类文件

1. 普通文件(-

这是用户最常打交道的文件。Linux 不以后缀名严格区分文件类型,普通文件内部可以是:

  • ASCII 文本文件 :如 .txt, .c, .py, 配置文件等,可以直接用 catvim 查看。
  • 二进制文件 :如编译后的可执行程序(.out, .elf)、图片(.jpg)、压缩包(.tar.gz)等。
2. 目录文件(d

目录在 Linux 中也是一种文件,只不过它的内容是一张"登记表",记录了该目录下有哪些文件以及它们对应的 inode 编号。只有内核才有权限直接写入目录文件(比如当你创建新文件时)。

3. 符号链接(l

也叫软链接。它本质上是一个包含了"另一个文件路径"的小文件。

  • 特点:可以跨文件系统,类似于 Windows 的快捷方式。
  • 注意:如果删除了原始文件,符号链接就会失效(变成"悬空链接")。
4. 设备文件(cb

Linux 将硬件设备抽象为文件,存放在 /dev 目录下,方便程序通过标准的 I/O 操作(如 open, read, write, ioctl)来与硬件交互。

  • 字符设备(c:像流水一样顺序读写,不支持随机定位。比如你敲击键盘,数据就是一个接一个的字符流。
  • 块设备(b:像硬盘一样,可以随意读取任意位置的数据块(如 4KB 大小)。系统对块设备有复杂的缓存和调度机制。
5. 进程间通信文件(ps

这两种文件主要用于程序(进程)之间的数据交换,它们通常不占用实际的磁盘存储空间(大小为 0),只存在于内存或文件系统的特定节点中。

  • 命名管道(p:也叫 FIFO(先进先出)。一个进程往里写,另一个进程从另一头读,数据单向流动。
  • 套接字(s:功能最强大的通信文件。它既可以实现本机上的两个程序通信(Unix Domain Socket),也可以实现跨网络的通信(如 Web 服务器的 80 端口)。

🛠️ 如何快速识别文件类型?

除了通过 ls -l 查看第一个字符外,Linux 还提供了一个非常强大的命令 file。它不依赖文件后缀名,而是通过分析文件内容的"魔法字符串"(Magic Number)来精准判断文件类型。

复制代码
$ ls -l /dev/sda /dev/tty /bin/ls
brw-rw---- 1 root disk 8, 0 May  2 10:00 /dev/sda      # b 开头,块设备
crw-rw-rw- 1 root tty  5, 0 May  2 10:00 /dev/tty      # c 开头,字符设备
-rwxr-xr-x 1 root root 142312 Apr 5 12:00 /bin/ls      # - 开头,普通文件

$ file /bin/ls
/bin/ls: ELF 64-bit LSB pie executable, x86-64...     # 识别出是 ELF 可执行程序

掌握这 7 种文件类型,能帮助你更好地理解 Linux 是如何统一管理各种数据和硬件资源的。

文件访问权限

🔑 权限的三大基础角色

Linux 为每个文件和目录设定了三类不同的访问对象:

  • 所有者 (User/Owner, u):创建该文件的用户。
  • 所属组 (Group, g):与所有者同属一个用户组的其他用户。
  • 其他人 (Others, o):系统中除了上述两类人之外的所有其他用户。

📊 读、写、执行权限详解

针对这三类对象,Linux 分配了三种基础权限:读(r)、写(w)、执行(x)。

特别注意:这三种权限作用在"普通文件"和"目录"上时,含义有着天壤之别:

权限 标识 文件的作用 目录的作用
r 可以读取文件的实际内容(如 cat, vim 可以列出目录中的文件名列表(如 ls
w 可以修改、覆盖文件的实际内容 可以在目录内创建、删除、重命名文件
执行 x 可以将该文件作为程序或脚本运行 可以进入 该目录(如 cd),并访问目录内文件的元数据

📝 如何看懂权限字符串?

当你执行 ls -l 命令时,输出结果的第一列就是权限信息。例如 -rwxr-xr--,这 10 个字符可以这样拆解:

  • 第 1 位 :文件类型(- 代表普通文件,d 代表目录,l 代表符号链接)。
  • 第 2-4 位 :**所有者(u)**的权限。
  • 第 5-7 位 :**所属组(g)**的权限。
  • 第 8-10 位 :**其他人(o)**的权限。

举例说明:
-rwxr-xr-- 表示:这是一个普通文件,所有者拥有读/写/执行权限,所属组拥有读/执行权限,其他人只有读权限。

🔢 权限的数字(八进制)表示法

在 Linux 中,r、w、x 也可以用数字来代替,这在设置权限时非常高效:

  • r (读) = 4
  • w (写) = 2
  • x (执行) = 1
  • - (无权限) = 0

将每个角色的权限数字相加,就能得到该角色的权限值。例如 rwx = 4+2+1 = 7。

常见的权限组合有:

  • 755 (rwxr-xr-x):所有者全开,其他人可读可执行(常用于脚本、程序)。
  • 644 (rw-r--r--):所有者可读写,其他人只读(常用于普通数据文件、配置文件)。
  • 700 (rwx------):只有所有者能读、写、执行(常用于私密目录)。

🛠️ 常用权限管理命令

  1. chmod (修改权限)
    • 数字方式:chmod 755 filename(将文件权限设为 rwxr-xr-x)
    • 符号方式:chmod u+x filename(给所有者增加执行权限)
  2. chown (修改所有者)
    • chown user:group filename(同时修改所有者和所属组)
  3. chgrp (修改所属组)
    • chgrp groupname filename(仅修改所属组)

💡 两个容易踩坑的权限常识

  1. 目录的"执行权限"是通行证

    如果你想访问某个目录下的文件(例如 cat /dir1/file.txt),你不仅需要对 file.txt 有读权限,还必须对路径上的每一级目录(如 //dir1)拥有执行(x)权限 。没有目录的 x 权限,你连 cd 进去都做不到。

  2. 删除文件看的是"目录的写权限" :在 Linux 中,能否删除一个文件,不取决于该文件本身的权限,而取决于该文件所在目录的写(w)权限 。只要你对某个目录有写权限,哪怕目录里的文件权限是 000(任何人都无权限),你依然可以把这个文件删掉。

access函数

access() 是 Linux 系统编程中用于检查进程对指定文件的访问权限的函数。它的核心作用是判断"当前进程"是否有权对某个文件进行读、写或执行操作。

access() 是 Linux 系统编程中用于检查进程对指定文件的访问权限的函数。它的核心作用是判断"当前进程"是否有权对某个文件进行读、写或执行操作。

📝 函数原型

它定义在 <unistd.h> 头文件中:

复制代码
#include <unistd.h>

int access(const char *pathname, int mode);

⚙️ 参数详解

  • pathname:要检查的文件路径(可以是绝对路径或相对路径)。
  • mode :要检查的权限类型,可以是以下值的组合(通过按位或 | 运算):
    • F_OK:检查文件是否存在。
    • R_OK:检查是否可读。
    • W_OK:检查是否可写。
    • X_OK:检查是否可执行。

📤 返回值

  • 成功(权限满足) :返回 0
  • 失败(权限不足或文件不存在) :返回 -1 ,并设置 errno 错误码。

umask函数

umask 函数是 Linux/Unix 系统编程中用于设置**文件模式创建掩码(File Mode Creation Mask)**的系统调用。简单来说,它决定了你新建文件或目录时的"默认权限"。

📝 函数原型

umask 定义在 <sys/stat.h> 头文件中:

复制代码
#include <sys/stat.h>

mode_t umask(mode_t mask);
  • 参数 mask :你希望设置的新掩码值(例如 00220077)。
  • 返回值 :返回上一次设置的旧掩码值。这个特性常被用来临时修改权限并随后恢复。

⚙️ 核心作用与计算逻辑

umask 的作用是屏蔽 (即"拿走")新文件或目录的某些权限。

当进程创建新文件(如调用 open()O_CREAT)或新目录(mkdir())时,系统会按照以下公式计算最终权限:

最终权限 = 预设起始权限 & (~umask)

  • 普通文件的预设起始权限0666(即 rw-rw-rw-,Linux 出于安全考虑,默认不给新文件执行权限)。
  • 目录的预设起始权限0777(即 rwxrwxrwx,目录必须有执行权限才能进入)

举个例子:

假设你将 umask 设置为 0022(即屏蔽掉"组用户"和"其他用户"的写权限):

  • 新建文件0666 & (~0022) = 0644 (rw-r--r--)
  • 新建目录0777 & (~0022) = 0755 (rwxr-xr-x)

💻 代码实战与技巧

在 C 语言编程中,umask 常用于确保程序创建的文件具有预期的安全性。

1. 常规设置:

如果你希望程序创建的文件只有自己(所有者)能读写,其他人完全无法访问,可以在程序开头设置 umask(0077)

复制代码
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

int main() {
    // 设置掩码,屏蔽掉组和其他人的所有权限
    umask(0077); 
    
    // 尝试以 666 权限创建文件,但受 umask 影响,最终权限会变成 600 (rw-------)
    int fd = open("private.txt", O_CREAT | O_WRONLY, 0666);
    if (fd != -1) {
        printf("文件创建成功!\n");
        close(fd);
    }
    return 0;
}

2. 临时修改并恢复(高级技巧):

由于 umask 函数会返回旧的掩码值,我们可以利用这一点在局部代码段临时改变权限规则,用完后再改回去,避免影响程序其他部分:

复制代码
mode_t old_mask = umask(0); // 临时将 umask 设为 0(即不屏蔽任何权限),并保存旧值
int fd = open("exact_perm.txt", O_CREAT | O_WRONLY, 0644); // 此时文件权限会严格等于 0644
umask(old_mask); // 恢复原来的 umask 设置

⚠️ 重要注意事项

  1. 进程级影响umask 的设置只对当前进程及其子进程有效。它不会改变系统全局的配置,也不会影响其他正在运行的程序。
  2. 与 Shell 命令的区别 :在 Shell 中直接输入 umask 022 是修改当前 Shell 环境的内置命令;而在 C 程序中调用 umask() 函数,是修改当前程序的系统调用,两者概念类似但作用域不同。
  3. 目录的写权限风险 :即使你通过 umask 限制了文件的权限,如果用户对一个目录 拥有写权限(w),他们依然可以删除该目录下的文件(无论该文件属于谁)。如果需要限制这种删除行为,需要用到目录的特殊权限------粘滞位(Sticky Bit)

chmod 和 fchmod函数

chmodfchmod 都是 Linux/Unix 系统编程中用于修改文件或目录访问权限的系统调用,它们定义在 <sys/stat.h> 头文件中。

📝 函数原型

复制代码
#include <sys/stat.h>

int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
  • 返回值 :两个函数成功时都返回 0 ,失败时返回 -1 并设置 errno

chmodfchmod 都是 Linux/Unix 系统编程中用于修改文件或目录访问权限的系统调用,它们定义在 <sys/stat.h> 头文件中。

📝 函数原型

复制代码
#include <sys/stat.h>

int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
  • 返回值 :两个函数成功时都返回 0 ,失败时返回 -1 并设置 errno

⚙️ 核心区别与适用场景

它们最根本的区别在于指定目标文件的方式不同:

  1. chmod(基于路径)

    • 操作对象 :通过文件的路径名pathname)来指定要修改权限的文件。
    • 适用场景:适用于你知道文件的具体路径,且文件不一定处于打开状态的场景。它是最常用的修改权限的方式。
  2. fchmod(基于文件描述符)

    • 操作对象 :通过已经打开的文件描述符fd)来指定目标文件。
    • 适用场景 :适用于文件已经被 open() 打开,你手里已经有了文件描述符的情况。它不需要再次通过路径去查找文件,且能避免一些路径相关的竞态条件(TOCTOU)问题。

⚠️ 权限与特殊位说明

  • 权限要求:为了改变一个文件的权限位,进程的有效用户ID必须等于文件的所有者ID,或者该进程必须具有超级用户(root)权限。
  • 权限设置(mode)mode 参数由 <sys/stat.h> 中预定义的宏通过按位或(|)组合而成。常用的权限宏如下:
权限类型 所有者 (User) 组 (Group) 其他用户 (Other)
读权限 S_IRUSR (00400) S_IRGRP (00040) S_IROTH (00004)
写权限 S_IWUSR (00200) S_IWGRP (00020) S_IWOTH (00002)
执行权限 S_IXUSR (00100) S_IXGRP (00010) S_IXOTH (00001)

例如,S_IRUSR | S_IWUSR | S_IRGRP 表示"所有者可读可写,组可读"(对应八进制权限 640)。

💻 代码实战

1. 使用 chmod 修改文件权限:

复制代码
#include <sys/stat.h>
#include <stdio.h>

int main() {
    // 将 "example.txt" 的权限修改为:所有者可读写,组可读,其他人无权限 (640)
    if (chmod("example.txt", S_IRUSR | S_IWUSR | S_IRGRP) == 0) {
        printf("chmod 修改权限成功!\n");
    } else {
        perror("chmod 失败");
    }
    return 0;
}

2. 使用 fchmod 修改已打开文件的权限:

复制代码
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    // 先打开文件获取文件描述符
    int fd = open("example.txt", O_RDONLY);
    if (fd == -1) {
        perror("打开文件失败");
        return 1;
    }

    // 通过文件描述符修改权限(例如改为 644)
    if (fchmod(fd, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) == 0) {
        printf("fchmod 修改权限成功!\n");
    } else {
        perror("fchmod 失败");
    }

    close(fd); // 记得关闭文件描述符
    return 0;
}

📊 总结

  • chmod :通过路径操作,适用于已知路径的常规权限修改。
  • fchmod :通过文件描述符操作,适用于已打开文件的权限修改,且更加安全(规避了路径被恶意篡改的风险)。

chown、fchown、lchown 函数

chownfchownlchown 都是 Linux/Unix 系统编程中用于修改文件所有者(UID)所属组(GID) 的系统调用,定义在 <unistd.h> 头文件中。

📝 函数原型

复制代码
#include <unistd.h>

int chown(const char *pathname, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int lchown(const char *pathname, uid_t owner, gid_t group);
  • 返回值 :三个函数成功时都返回 0 ,失败时返回 -1 并设置 errno
  • 参数说明 :如果 ownergroup 参数被设为 -1,则表示对应的 ID 保持不变。

⚙️ 核心区别与适用场景

这三个函数最根本的区别在于指定目标文件的方式 以及对符号链接(软链接)的处理逻辑

  1. chown(基于路径,跟随符号链接)

    • 操作对象 :通过路径名pathname)指定文件。
    • 符号链接处理 :如果 pathname 是一个符号链接,chown跟随(dereference) 该链接,实际修改的是符号链接指向的目标文件的所有者和组。
    • 适用场景:最常规的文件所有权修改,且你希望修改的是软链接背后的真实文件。
  2. fchown(基于文件描述符)

    • 操作对象 :通过已经打开的文件描述符fd)指定文件。
    • 符号链接处理 :由于它操作的是已经打开的文件句柄,因此不存在符号链接跟随的问题(你无法直接打开一个符号链接并获取其指向文件的描述符,除非在打开时已经跟随了)。
    • 适用场景 :文件已经被 open() 打开,你手里已经有了文件描述符。它不需要再次通过路径去查找文件,且能避免路径相关的竞态条件(TOCTOU)。
  3. lchown(基于路径,不跟随符号链接)

    • 操作对象 :通过路径名pathname)指定文件。
    • 符号链接处理 :如果 pathname 是一个符号链接,lchown 不会跟随 该链接,而是直接修改符号链接文件本身的所有者和组。
    • 适用场景:当你需要专门管理符号链接文件自身的属性,而不想触碰它所指向的真实文件时。

⚠️ 权限与安全隐患说明

  • 权限要求:为了改变一个文件的所有者,调用进程通常需要具有超级用户(root)权限。普通用户通常只能修改文件的所属组(且只能改为自己所属的组)。
  • 安全位清除 :当 root 用户使用这些函数修改文件所有者或组时,如果文件原本设置了 SUID(设置用户ID)SGID(设置组ID) 权限位,系统出于安全考虑,会自动清除这些特殊权限位。

💻 代码实战

复制代码
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main() {
    uid_t new_owner = 1000; // 假设要修改为 UID 1000 的用户
    gid_t new_group = 1000; // 假设要修改为 GID 1000 的组

    // 1. 使用 chown:修改真实文件 "target.txt" 的所有者
    // 如果 "link.txt" 是指向它的软链接,这条命令也会修改 "target.txt"
    if (chown("target.txt", new_owner, new_group) == 0) {
        printf("chown 修改成功!\n");
    } else {
        perror("chown 失败");
    }

    // 2. 使用 lchown:仅修改软链接 "link.txt" 本身的所有者
    // 它指向的真实文件 "target.txt" 的所有者不会改变
    if (lchown("link.txt", new_owner, new_group) == 0) {
        printf("lchown 修改软链接本身成功!\n");
    } else {
        perror("lchown 失败");
    }

    // 3. 使用 fchown:通过文件描述符修改
    int fd = open("target.txt", O_RDONLY);
    if (fd != -1) {
        if (fchown(fd, new_owner, -1) == 0) { // -1 表示不修改组ID
            printf("fchown 修改所有者成功,组ID保持不变!\n");
        } else {
            perror("fchown 失败");
        }
        close(fd);
    }

    return 0;
}
  • chown :通过路径 操作,会跟随符号链接修改真实文件。
  • fchown :通过文件描述符操作,针对已打开的文件,更加安全高效。
  • lchown :通过路径 操作,不跟随符号链接,专门用来修改软链接自身的属性。

文件长度

获取文件长度(文件大小)主要有两种标准且常用的方法。一种是使用 POSIX 标准的系统调用(stat),另一种是使用 C 标准库的文件流操作(fseekftell)。

1. 使用 stat 函数(推荐)

这是 Linux/Unix 系统编程中最推荐的方式。stat 函数可以直接通过文件路径获取文件的元数据(包含文件大小、权限、修改时间等),不需要真正打开和读取文件,因此效率非常高。

  • 核心逻辑 :调用 stat 函数,将文件信息填充到 struct stat 结构体中,通过访问结构体成员 st_size 即可获取大小。

  • 所需头文件<sys/types.h>, <sys/stat.h>, <unistd.h>

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <unistd.h>

    long get_file_size_stat(const char *filepath) {
    struct stat st;
    // stat 成功返回 0,失败返回 -1
    if (stat(filepath, &st) == 0) {
    return st.st_size; // st_size 即为文件大小(字节)
    }
    perror("stat failed");
    return -1;
    }

    int main() {
    long size = get_file_size_stat("example.txt");
    if (size != -1) {
    printf("File size (stat): %ld bytes\n", size);
    }
    return 0;
    }

2. 使用 fseekftell 函数

这是 C 语言标准库(stdio.h)提供的通用方法。它通过操作文件指针来计算大小,适用于所有支持 C 标准库的平台。

  • 核心逻辑

    1. fopen 以二进制模式("rb")打开文件。
    2. fseek 将文件指针移动到文件末尾(SEEK_END)。
    3. ftell 获取当前指针相对于文件开头的偏移量,这个偏移量就是文件大小。
  • 注意事项ftell 返回的是 long 类型。在 32 位系统(x86)上,long 最大只能表示约 2.1GB(2^31 - 1 字节)。如果处理超大文件,可能会发生整数溢出。

    #include <stdio.h>

    long get_file_size_ftell(const char *filepath) {
    FILE *file = fopen(filepath, "rb"); // 必须以二进制模式打开
    if (file == NULL) {
    perror("fopen failed");
    return -1;
    }

    复制代码
      // 将文件指针移动到末尾
      if (fseek(file, 0, SEEK_END) != 0) {
          fclose(file);
          return -1;
      }
    
      // 获取当前指针位置(即文件大小)
      long size = ftell(file);
      fclose(file);
      return size;

    }

    int main() {
    long size = get_file_size_ftell("example.txt");
    if (size != -1) {
    printf("File size (ftell): %ld bytes\n", size);
    }
    return 0;
    }

💡 总结与建议

  • 日常开发首选 stat :如果你是在 Linux 环境下开发,stat 是最快、最安全且支持大文件的方式,因为它不需要分配文件流缓冲区,也不受 long 类型在 32 位系统上的大小限制。
  • 跨平台兼容选 ftell :如果你的代码需要极强的跨平台兼容性(比如要同时跑在 Windows、嵌入式等非 POSIX 系统上),可以使用 fseek + ftell 的组合。

另外,如果你只是想在 Linux 终端(Shell) 中快速查看文件长度,可以直接使用 wc -c 命令(例如:wc -c example.txt),它会直接输出文件的字节数。

link、unlink、remove、rename函数

在 Linux C 语言文件系统中,linkunlinkremoverename 是用于操作文件链接、删除和重命名的核心函数。

📝 函数原型与核心作用

复制代码
#include <unistd.h>
int link(const char *existingpath, const char *newpath);
int unlink(const char *pathname);

#include <stdio.h>
int remove(const char *pathname);
int rename(const char *oldname, const char *newname);
  • 返回值 :这四个函数在调用成功时都返回 0 ,失败时返回 -1 并设置 errno

⚙️ 详细解析

1. link(创建硬链接)
  • 核心作用:为现有的文件创建一个新的目录项(即硬链接)。
  • 参数说明existingpath 是已经存在的文件路径,newpath 是要创建的新链接路径。
  • 注意事项
    • 如果 newpath 已经存在,函数会报错返回。
    • 硬链接不能跨文件系统(分区)创建,且通常不能对目录创建硬链接。
    • 创建成功后,两个路径名指向同一个 Inode,文件的链接计数(Link Count)会加 1。
2. unlink(删除目录项/解除链接)
  • 核心作用:删除一个文件的目录项,并将该文件的链接计数减 1。
  • 底层原理unlink 的本质是"解除链接"而不是直接"擦除数据"。只有当文件的链接计数降为 0 ,且没有任何进程打开该文件时,内核才会真正释放文件占用的磁盘空间。
  • 经典应用场景 :程序在运行时需要创建临时文件,为了防止程序意外崩溃导致临时文件残留,可以先 open 创建文件,然后立刻调用 unlink。此时文件在目录中不可见(因为目录项被删除了),但只要进程不关闭文件描述符,就可以继续正常读写。当进程关闭文件或终止时,文件数据会被系统自动彻底清除。
  • 符号链接 :如果 pathname 是一个符号链接(软链接),unlink 只会删除这个软链接本身,而不会影响它指向的真实文件。
3. remove(解除文件或目录的链接)
  • 核心作用 :它是 unlinkrmdir(删除空目录)的通用封装。
  • 行为逻辑
    • 如果 pathname 指的是一个文件remove 的功能与 unlink 完全相同。
    • 如果 pathname 指的是一个目录remove 的功能与 rmdir 相同(即只能删除空目录)。
4. rename(重命名或移动文件)
  • 核心作用:修改文件或目录的名称,甚至将其移动到另一个目录下。
  • 行为逻辑
    • 如果 newname 已经存在且是一个普通文件,它会被静默覆盖(前提是进程有权限)。
    • 如果 newname 是一个非空目录rename 会操作失败。
    • 如果 oldnamenewname 指向同一个文件,函数不做任何处理并返回成功。

💡 总结与对比

表格

函数 核心作用 适用对象 关键特性
link 创建硬链接 现有文件 增加链接计数,不能跨分区
unlink 删除目录项 文件/软链接 链接计数减 1,若被占用则延迟删除数据
remove 通用删除接口 文件或目录 文件等同 unlink,目录等同 rmdir
rename 重命名/移动 文件或目录 若新名已存在会覆盖文件,但不能覆盖非空目录

在 Linux 中,我们平时在终端使用的 rm 命令,其底层核心调用的其实就是 unlink 系统调用。理解这些函数,能让你更透彻地掌握 Linux 文件系统"文件名"与"Inode(真实数据)"分离的设计哲学。

chdir、fchdir、getcwd函数

chdirfchdirgetcwd 是 Linux C 语言编程中用于管理进程当前工作目录(Current Working Directory, CWD) 的核心函数。它们都定义在 <unistd.h> 头文件中。

📝 函数原型

复制代码
#include <unistd.h>

int chdir(const char *path);
int fchdir(int fd);
char *getcwd(char *buf, size_t size);
  • 返回值chdirfchdir 成功时返回 0 ,失败时返回 -1 并设置 errnogetcwd 成功时返回指向 buf 的指针,失败时返回 NULL

⚙️ 详细解析

1. chdir(通过路径改变目录)
  • 核心作用 :将调用进程的当前工作目录更改为 path 参数指定的目录。
  • 参数说明path 可以是绝对路径(如 /home/user),也可以是相对路径(如 ../src)。
  • 注意事项 :这是最常用的切换目录方式。如果 path 指向的目录不存在,或者进程没有相应的搜索/执行权限,调用会失败。
2. fchdir(通过文件描述符改变目录)
  • 核心作用 :将调用进程的当前工作目录更改为文件描述符 fd 所指向的目录。
  • 参数说明fd 必须是一个已经通过 open() 函数成功打开的目录的文件描述符。
  • 适用场景 :当你需要频繁地在几个目录之间来回切换,或者为了避免路径字符串解析的开销和潜在的路径竞态条件(TOCTOU)时,fchdir 是非常高效且安全的选择。
3. getcwd(获取当前目录)
  • 核心作用 :获取进程当前工作目录的绝对路径
  • 参数说明
    • buf:用于存放绝对路径字符串的缓冲区。
    • size:缓冲区的大小(字节)。
  • 特殊用法 :如果将 buf 设为 NULL,并且 size 设为 0getcwd 会自动使用 malloc 分配足够大的内存来存储路径。在这种情况下,调用者必须记得在使用完毕后调用 free() 释放这块内存。

💡 核心特性与实战

🛡️ 进程的独立属性

当前工作目录是进程的一个独立属性。这意味着:

  • 在程序中调用 chdir 只会改变当前进程及其后续创建的子进程的工作目录,而不会影响父进程(比如你运行程序的 Shell 终端)。
  • 通过 fork() 创建的子进程会继承父进程的当前工作目录;而通过 execve() 执行新程序时,当前工作目录保持不变。
💻 代码实战:目录切换与"目录栈"技巧
复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/limits.h> // 包含 PATH_MAX 宏定义

int main() {
    char cwd[PATH_MAX];
    int dir_fd;

    // 1. 获取并打印初始工作目录
    if (getcwd(cwd, sizeof(cwd)) != NULL) {
        printf("初始工作目录: %s\n", cwd);
    }

    // 2. 使用 chdir 切换到 /tmp 目录
    if (chdir("/tmp") == -1) {
        perror("chdir 到 /tmp 失败");
        return 1;
    }
    printf("切换到了 /tmp 目录\n");

    // 3. 高级技巧:保存当前目录的文件描述符,以便日后返回
    // 打开当前目录("." 代表当前目录)
    dir_fd = open(".", O_RDONLY);
    if (dir_fd == -1) {
        perror("打开当前目录失败");
        return 1;
    }

    // 4. 再次切换目录(比如去 /var)
    if (chdir("/var") == -1) {
        perror("chdir 到 /var 失败");
        return 1;
    }
    printf("切换到了 /var 目录\n");

    // 5. 使用 fchdir 配合之前保存的文件描述符,瞬间回到 /tmp
    if (fchdir(dir_fd) == -1) {
        perror("fchdir 返回原目录失败");
        return 1;
    }
    printf("通过 fchdir 成功返回到了 /tmp 目录\n");

    // 记得关闭文件描述符
    close(dir_fd);

    return 0;
}

📊 总结与对比

函数 核心作用 指定目标的方式 关键特性
chdir 改变工作目录 路径名字符串 (const char *path) 最常用,支持绝对和相对路径
fchdir 改变工作目录 已打开的目录文件描述符 (int fd) 高效安全,适合频繁切换或保存现场
getcwd 获取当前目录 缓冲区与大小 (char *buf, size_t size) 获取绝对路径,支持自动分配内存

设备特殊文件

在 Linux C 语言编程中,设备特殊文件(Device Special File)是理解"一切皆文件"这一核心哲学的关键。它们通常位于 /dev 目录下,是用户空间程序与内核驱动程序及硬件设备进行交互的统一接口。

⚙️ 设备文件的两大分类

在终端使用 ls -l /dev 查看设备文件时,文件权限前的第一个字符会标识其类型:

  • 字符设备 (Character Device, 标识为 c)
    • 特点:以字节流(字符)的形式进行数据传输,通常没有缓冲区,数据实时传输,不支持随机访问。
    • 典型设备 :键盘、鼠标、串口终端(如 /dev/ttyUSB0)、虚拟终端(/dev/tty1)等。
  • 块设备 (Block Device, 标识为 b)
    • 特点:以固定大小的数据块(如 512B 或 4KB)为单位进行传输,带有缓冲机制,支持随机访问(可以直接定位到任意数据块)。
    • 典型设备 :硬盘(如 /dev/sda)、U盘、SSD(如 /dev/nvme0n1)、光盘驱动器等。

🔢 核心标识:主设备号与次设备号

每个设备文件都关联着一对整数,即主设备号和次设备号(在 ls -l 输出中通常显示为逗号分隔的两个数字,如 8, 0):

  • 主设备号 (Major Number) :标识该设备对应的内核驱动程序。例如,主设备号 8 通常对应 SCSI/SATA 磁盘驱动。
  • 次设备号 (Minor Number) :由驱动程序使用,用于区分使用该驱动程序的具体设备实例 。例如,/dev/sda(第一块硬盘)和 /dev/sdb(第二块硬盘)的主设备号相同,但次设备号不同。

🛠️ 常见特殊设备文件解析

除了物理硬件,Linux 还提供了一些非常有用的虚拟(伪)设备文件:

  • /dev/null :空设备。写入它的所有数据都会被直接丢弃,读取它则会立即得到文件结束符(EOF)。常用于屏蔽命令输出(如 command > /dev/null)。
  • /dev/zero :零源。读取它会源源不断地得到空字节(\0),常用于创建指定大小的空白文件。
  • /dev/full:满设备。写入它会一直返回"设备无空间(ENOSPC)"错误,常用于测试程序在磁盘写满时的异常处理逻辑。
  • /dev/random/dev/urandom:随机数生成器。提供高质量的随机数,常用于生成加密密钥。
  • /dev/tty:代表当前进程的控制终端。程序可以通过读写它来直接与用户进行交互。

💻 C 语言编程实战

在 C 语言中,你可以像操作普通文件一样,使用标准的系统调用(open, read, write, ioctl 等)来操作设备文件。

实战示例:读取系统随机

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd = open("/dev/urandom", O_RDONLY);
    if (fd == -1) {
        perror("打开 /dev/urandom 失败");
        return 1;
    }

    unsigned int random_num;
    // 从设备文件中读取 4 个字节(一个 unsigned int 的大小)
    if (read(fd, &random_num, sizeof(random_num)) == -1) {
        perror("读取随机数失败");
        close(fd);
        return 1;
    }

    printf("从 /dev/urandom 读取到的随机数是: %u\n", random_num);
    
    close(fd);
    return 0;
}

📝 补充:手动创建设备文件 (mknod)

在现代 Linux 系统中,设备文件通常由 udev 等守护进程在硬件插入时自动动态创建和管理。但在某些特定场景(如编写内核驱动或嵌入式开发)下,你可能需要使用 mknod 命令手动创建。

  • 基本语法mknod [路径] [类型] [主设备号] [次设备号]
  • 示例sudo mknod /dev/mydevice c 1 3(创建一个主设备号为 1,次设备号为 3 的字符设备)。

掌握设备特殊文件,意味着你打通了用户程序与底层硬件交互的"任督二脉"。

相关推荐
weixin_421725263 小时前
C语言是一种通用的计算机编程语言,广泛应用于各类
c语言·计算机·编程语言·软件开发·历史演变
不断提高3 小时前
别再写 while(1) 死循环了,嵌入式开发该换个活法
c语言·嵌入式硬件·嵌入式·状态模式
bucenggaibian3 小时前
为什么有这么多以字母 “C” 为开头的编程语言?
c语言·编程语言·历史·发展·家族
bucenggaibian3 小时前
C语言超级全面的学习平台
c语言·sqlite·easylogger·pat练习·tencentos-tiny
50万马克的面包4 小时前
三子棋小游戏(C语言详解)
c语言·开发语言·算法
我不是懒洋洋5 小时前
AC自动机:从KMP到多模式匹配,敏感词过滤神器
c语言
无限进步_5 小时前
【C++】AVL树完全解析:从平衡因子到四种旋转
c语言·开发语言·数据结构·c++·后端·算法·github
嵌入式小杰6 小时前
一阶低通滤波入门教程:从原理到单片机 C 代码实现
c语言·开发语言·stm32·单片机·算法
学会去珍惜6 小时前
8天学会C语言编程第2天:变量、数据类型和输入/输出,3分钟上手
c语言·实战·变量·编程入门·输入输出