本节将描述文件系统的其他特征和文件的性质
1. stat, fstat, lstat 函数
在 Linux 系统编程中,stat、fstat 和 lstat 是用于获取文件元数据(即"文件的身份信息")的一组核心系统调用。
它们就像是文件的"体检报告",能告诉你文件的大小、权限、所有者、时间戳以及它在磁盘上的具体位置(inode 编号)等信息。
📊 三者的核心区别
这三个函数最终获取到的信息是一模一样的(都填充到 struct stat 结构体中),它们唯一的区别在于**"如何找到目标文件"** 以及**"遇到符号链接时的态度"**:
| 函数 | 传入参数 | 遇到符号链接(软链接)时 |
|---|---|---|
| stat | 文件路径名 | 穿透:跟随链接,返回实际指向文件的信息 |
| lstat | 文件路径名 | 不穿透:返回符号链接本身的信息 |
| fstat | 文件描述符 (fd) | 不涉及路径,直接返回已打开文件的信息 |
在 Linux 系统编程中,stat、fstat 和 lstat 是用于获取文件元数据(即"文件的身份信息")的一组核心系统调用。
结合你之前关注的文件 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, 配置文件等,可以直接用cat或vim查看。 - 二进制文件 :如编译后的可执行程序(
.out,.elf)、图片(.jpg)、压缩包(.tar.gz)等。
2. 目录文件(d)
目录在 Linux 中也是一种文件,只不过它的内容是一张"登记表",记录了该目录下有哪些文件以及它们对应的 inode 编号。只有内核才有权限直接写入目录文件(比如当你创建新文件时)。
3. 符号链接(l)
也叫软链接。它本质上是一个包含了"另一个文件路径"的小文件。
- 特点:可以跨文件系统,类似于 Windows 的快捷方式。
- 注意:如果删除了原始文件,符号链接就会失效(变成"悬空链接")。
4. 设备文件(c 和 b)
Linux 将硬件设备抽象为文件,存放在 /dev 目录下,方便程序通过标准的 I/O 操作(如 open, read, write, ioctl)来与硬件交互。
- 字符设备(
c):像流水一样顺序读写,不支持随机定位。比如你敲击键盘,数据就是一个接一个的字符流。 - 块设备(
b):像硬盘一样,可以随意读取任意位置的数据块(如 4KB 大小)。系统对块设备有复杂的缓存和调度机制。
5. 进程间通信文件(p 和 s)
这两种文件主要用于程序(进程)之间的数据交换,它们通常不占用实际的磁盘存储空间(大小为 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------):只有所有者能读、写、执行(常用于私密目录)。
🛠️ 常用权限管理命令
chmod(修改权限)- 数字方式:
chmod 755 filename(将文件权限设为 rwxr-xr-x) - 符号方式:
chmod u+x filename(给所有者增加执行权限)
- 数字方式:
chown(修改所有者)chown user:group filename(同时修改所有者和所属组)
chgrp(修改所属组)chgrp groupname filename(仅修改所属组)
💡 两个容易踩坑的权限常识
-
目录的"执行权限"是通行证 :
如果你想访问某个目录下的文件(例如
cat /dir1/file.txt),你不仅需要对file.txt有读权限,还必须对路径上的每一级目录(如/和/dir1)拥有执行(x)权限 。没有目录的 x 权限,你连cd进去都做不到。 -
删除文件看的是"目录的写权限" :在 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:你希望设置的新掩码值(例如0022或0077)。 - 返回值 :返回上一次设置的旧掩码值。这个特性常被用来临时修改权限并随后恢复。
⚙️ 核心作用与计算逻辑
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 设置
⚠️ 重要注意事项
- 进程级影响 :
umask的设置只对当前进程及其子进程有效。它不会改变系统全局的配置,也不会影响其他正在运行的程序。 - 与 Shell 命令的区别 :在 Shell 中直接输入
umask 022是修改当前 Shell 环境的内置命令;而在 C 程序中调用umask()函数,是修改当前程序的系统调用,两者概念类似但作用域不同。 - 目录的写权限风险 :即使你通过
umask限制了文件的权限,如果用户对一个目录 拥有写权限(w),他们依然可以删除该目录下的文件(无论该文件属于谁)。如果需要限制这种删除行为,需要用到目录的特殊权限------粘滞位(Sticky Bit)
chmod 和 fchmod函数
chmod 和 fchmod 都是 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。
chmod 和 fchmod 都是 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。
⚙️ 核心区别与适用场景
它们最根本的区别在于指定目标文件的方式不同:
-
chmod(基于路径)- 操作对象 :通过文件的路径名 (
pathname)来指定要修改权限的文件。 - 适用场景:适用于你知道文件的具体路径,且文件不一定处于打开状态的场景。它是最常用的修改权限的方式。
- 操作对象 :通过文件的路径名 (
-
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 函数
chown、fchown 和 lchown 都是 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。 - 参数说明 :如果
owner或group参数被设为 -1,则表示对应的 ID 保持不变。
⚙️ 核心区别与适用场景
这三个函数最根本的区别在于指定目标文件的方式 以及对符号链接(软链接)的处理逻辑:
-
chown(基于路径,跟随符号链接)- 操作对象 :通过路径名 (
pathname)指定文件。 - 符号链接处理 :如果
pathname是一个符号链接,chown会跟随(dereference) 该链接,实际修改的是符号链接指向的目标文件的所有者和组。 - 适用场景:最常规的文件所有权修改,且你希望修改的是软链接背后的真实文件。
- 操作对象 :通过路径名 (
-
fchown(基于文件描述符)- 操作对象 :通过已经打开的文件描述符 (
fd)指定文件。 - 符号链接处理 :由于它操作的是已经打开的文件句柄,因此不存在符号链接跟随的问题(你无法直接打开一个符号链接并获取其指向文件的描述符,除非在打开时已经跟随了)。
- 适用场景 :文件已经被
open()打开,你手里已经有了文件描述符。它不需要再次通过路径去查找文件,且能避免路径相关的竞态条件(TOCTOU)。
- 操作对象 :通过已经打开的文件描述符 (
-
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 标准库的文件流操作(fseek 和 ftell)。
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. 使用 fseek 和 ftell 函数
这是 C 语言标准库(stdio.h)提供的通用方法。它通过操作文件指针来计算大小,适用于所有支持 C 标准库的平台。
-
核心逻辑 :
- 用
fopen以二进制模式("rb")打开文件。 - 用
fseek将文件指针移动到文件末尾(SEEK_END)。 - 用
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 语言文件系统中,link、unlink、remove 和 rename 是用于操作文件链接、删除和重命名的核心函数。
📝 函数原型与核心作用
#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(解除文件或目录的链接)
- 核心作用 :它是
unlink和rmdir(删除空目录)的通用封装。 - 行为逻辑 :
- 如果
pathname指的是一个文件 ,remove的功能与unlink完全相同。 - 如果
pathname指的是一个目录 ,remove的功能与rmdir相同(即只能删除空目录)。
- 如果
4. rename(重命名或移动文件)
- 核心作用:修改文件或目录的名称,甚至将其移动到另一个目录下。
- 行为逻辑 :
- 如果
newname已经存在且是一个普通文件,它会被静默覆盖(前提是进程有权限)。 - 如果
newname是一个非空目录 ,rename会操作失败。 - 如果
oldname和newname指向同一个文件,函数不做任何处理并返回成功。
- 如果
💡 总结与对比
表格
| 函数 | 核心作用 | 适用对象 | 关键特性 |
|---|---|---|---|
| link | 创建硬链接 | 现有文件 | 增加链接计数,不能跨分区 |
| unlink | 删除目录项 | 文件/软链接 | 链接计数减 1,若被占用则延迟删除数据 |
| remove | 通用删除接口 | 文件或目录 | 文件等同 unlink,目录等同 rmdir |
| rename | 重命名/移动 | 文件或目录 | 若新名已存在会覆盖文件,但不能覆盖非空目录 |
在 Linux 中,我们平时在终端使用的 rm 命令,其底层核心调用的其实就是 unlink 系统调用。理解这些函数,能让你更透彻地掌握 Linux 文件系统"文件名"与"Inode(真实数据)"分离的设计哲学。
chdir、fchdir、getcwd函数
chdir、fchdir 和 getcwd 是 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);
- 返回值 :
chdir和fchdir成功时返回 0 ,失败时返回 -1 并设置errno;getcwd成功时返回指向buf的指针,失败时返回 NULL。
⚙️ 详细解析
1. chdir(通过路径改变目录)
- 核心作用 :将调用进程的当前工作目录更改为
path参数指定的目录。 - 参数说明 :
path可以是绝对路径(如/home/user),也可以是相对路径(如../src)。 - 注意事项 :这是最常用的切换目录方式。如果
path指向的目录不存在,或者进程没有相应的搜索/执行权限,调用会失败。
2. fchdir(通过文件描述符改变目录)
- 核心作用 :将调用进程的当前工作目录更改为文件描述符
fd所指向的目录。 - 参数说明 :
fd必须是一个已经通过open()函数成功打开的目录的文件描述符。 - 适用场景 :当你需要频繁地在几个目录之间来回切换,或者为了避免路径字符串解析的开销和潜在的路径竞态条件(TOCTOU)时,
fchdir是非常高效且安全的选择。
3. getcwd(获取当前目录)
- 核心作用 :获取进程当前工作目录的绝对路径。
- 参数说明 :
buf:用于存放绝对路径字符串的缓冲区。size:缓冲区的大小(字节)。
- 特殊用法 :如果将
buf设为NULL,并且size设为0,getcwd会自动使用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 的字符设备)。
掌握设备特殊文件,意味着你打通了用户程序与底层硬件交互的"任督二脉"。