文件I/O学习笔记

文件I/O(Input/Output)是与外部存储(如硬盘、SSD)中的文件进行数据交换的基础。

C语言提供了两套主要的文件操作API:标准I/O系统I/O

  • 标准I/O (Standard I/O): C语言标准库 (<stdio.h>) 提供的函数。它们具有良好的可移植性,并内置了用户态缓冲区机制以提高效率。是日常应用开发的首选。
  • 系统I/O (System I/O): 操作系统内核直接提供的系统调用(System Call),在Linux/Unix中遵循POSIX标准(如<unistd.h>, <fcntl.h>)。它们更接近底层,功能更原始,不带用户态缓冲区(数据直达内核缓冲区)。
特性对比 标准 I/O (stdio.h) 系统 I/O (unistd.h, fcntl.h)
核心标识 FILE * (文件指针) int (文件描述符)
缓冲机制 (全/行/无缓冲) (数据直达内核缓冲区)
可移植性 (遵循C标准) (遵循POSIX标准,Windows有差异)
操作对象 任何C程序能识别的“流” 操作系统层面的一切皆“文件”
效率 读写大块数据时,因缓冲而效率高 频繁进行小数据量读写时效率低
常用函数 fopen, fclose, fread, fwrite, fgets open, close, read, write, lseek
比喻 精装的饭店: 提供现成菜品,服务周到 原始的菜市场: 提供原生食材,需自行加工

第一部分:标准I/O <stdio.h>

标准I/O通过FILE结构体来管理文件流,并利用缓冲区优化性能

文件的打开与关闭 fopen, fclose

  • FILE *fopen(const char *path, const char *mode);: 打开一个文件,返回一个指向FILE结构体的指针(文件指针)。如果失败,返回NULL
  • int fclose(FILE *fp);: 关闭文件,将缓冲区剩余数据刷入文件并释放资源。

常用打开模式 mode:

模式 描述 文件不存在 文件存在
r 模式 (Read) 打开失败 从头读取
w 模式 (Write) 创建新文件 清空并从头写入
a 追加模式 (Append) 创建新文件 从末尾追加写入
r+ 读写模式 打开失败 从头读写
w+ 读写模式 创建新文件 清空并从头读写
a+ 读写模式 创建新文件 从末尾追加,可读

多种读写方式

1.0 错误处理 feof,ferror

feof:(file end of) 判断是否到文件末尾

ferror:(file error) 判断是否遇到文件错误

1.1 按字符读写 fgetc, fputc

fgetc

  • 相当于标准输入的 getchar()
  • 成功时返回读取的字符,失败或到文件末尾时返回EOF
  • 所以必须用int类型变量接收,以区分EOF(-1)和值为255的字符。

fgetc返回EOF时,必须使用feof()ferror()来进一步区分是文件正常结束还是读写中途发生错误

  • feof(fp): 如果是因为到达文件末尾而失败,返回真(非零)。
  • ferror(fp): 如果是因为发生I/O错误而失败,返回真(非零)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 从src.txt拷贝到dest.txt,带完整错误处理
FILE *fp = fopen("src.txt", "r");
if (!fp) { perror("打开失败"); return -1; }

int ch;
while ((ch = fgetc(fp)) != EOF) {
fputc(ch, dest_fp);
}

if (ferror(fp)) { // 检查是否因读取错误而退出循环
perror("读取错误");
} else if (feof(fp)) { // 检查是否因读到文件末尾而退出
printf("成功拷贝\n");
}
fclose(fp);

1.2 按字符串(行)读写 fgets, fputs

fgets

  • 是读取字符串的安全选择,因为它有边界检查,能有效防止缓冲区溢出
  • 最多读取size-1个字符,或读到换行符\n为止
  • 会保留\n

fputs(buf, fp)

  • 将字符串写入文件,不会自动添加\n
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FILE *fp1 = fopen(argv[1], "r");
FILE *fp2 = fopen(argv[2], "w");

char buf[10]; // 缓冲区
while (fgets(buf, 10, fp1) != NULL) {
// fgets一次最多读sizeof(buf)-1个字符,即9个
// 如果一行超过9个字符,它会分多次读取
fputs(buf, fp2);
}

// fgets返回NULL时,同样需要用feof和ferror检查
if(feof(fp1)) printf("拷贝完毕!\n");
else if(ferror(fp1)) perror("读取错误");

fclose(fp1); fclose(fp2);

1.3 按数据块读写 fread, fwrite

按指定大小的数据块读写,是读写二进制文件或结构体的最常用、最高效的方式

  • 返回值: freadfwrite都返回成功读/写的数据块nmemb)数量。

  • 到达文件末尾时,fread可能没有读最后一块数据(不足一块),此时返回值会小于预期,甚至为0

    此时实际读入缓冲区的数据字节数可能不是size * n

    必须结合ftell或自己计算字节数来处理文件末尾不足一个块的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 1. 只读打开源文件、只写目标文件(若不存在则创建,若存在则清空)
FILE *fp1 = fopen(file1.txt, "r");
FILE *fp2 = fopen(file2.txt, "w");

// 2. 循环读取源文件并放入目标文件
char *buf = calloc(5, 20); // 分配了100字节
while (1) {
fseek(fp1, 0, SEEK_END); // 将文件指针移到文件末尾
long file_size = ftell(fp1); // 获取当前文件指针位置,即文件大小
fseek(fp1, 0, SEEK_SET); // 将文件指针移回文件开头,以便后续读取

// fread返回的是整数块,不足一整块的不会被计入n
// 但是是可以被正常读取
// 例如读到96个字节,返回4,读到46个字节,返回2,读到16个字节,返回0
int n = fread(buf, 20, 5, fp1); // 用buf缓冲区,读分配fp1指向文件的20字节*5块
if (n < 5) { // a. 读取失败或到文件尾
if (feof(fp1)) {
long b = ftell(fp1); // 将剩余的 0-99 个字节拷贝到目标文件
fwrite(buf, b - a, 1, fp2);
break;
}
}
fwrite(buf, 20, 5, fp2); // b. 正常读到100个字节,写入目标文件
}

// 3. 释放资源
fclose(fp1); fclose(fp2);

1.4 按格式化I/O读写 fscanf, fprintf,snprintf

可以像scanf/printf一样,对文件或字符串进行格式化I/O。

函数 源 / 目标 描述
scanf / printf 键盘 / 终端 标准输入/输出
fscanf / fprintf 文件 (FILE*) 从/向指定文件流进行格式化I/O
sscanf / sprintf 字符串 (char*) 从/向字符串进行格式化I/O。sprintf不安全!
snprintf 字符串 (char*) sprintf的安全版本,指定最大写入长度,强烈推荐
1
2
3
4
5
6
7
8
9
10
FILE *fp1 = fopen(argv[1], "r");
FILE *fp2 = fopen(argv[2], "w");

int a; char buf[20]; float f;
// fscanf返回成功匹配并赋值的项数
while (fscanf(fp1, "%d %s %f", &a, buf, &f) == 3) {
fprintf(fp2, "INT: %d, STR: %s, FLOAT: %.2f\n\n", a, buf, f);
}

fclose(fp1); fclose(fp2);

(字符串格式化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char buf[100];
// 安全地将格式化数据写入字符串
snprintf(buf, sizeof(buf), "%d %s,%f data ...", 100, "hello", 3.14);

// 从字符串中解析数据
int a; char s[20]; float f;
sscanf(buf, "%d %s。%f", &a, s, &f);

printf("a: %d, s: %s, f: %f\n", a, s, f);
// 解析结果为:
// a: 100
// s: hello。3.140000
// f: 0.000000
// 因为“%s”后面加上字符后,hello和3.14会被连着读

文件定位 fseek, ftell, rewind

int fseek(FILE *stream, long offset, int whence);: 移动文件读写指针。

  • whence: SEEK_SET (文件头), SEEK_CUR (当前位置), SEEK_END (文件尾)。

long ftell(FILE *stream);: 返回当前读写指针相对于文件头的字节偏移量

void rewind(FILE *stream);: 将指针移回文件头,等价于 fseek(stream, 0, SEEK_SET)

fp (获取文件大小):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FILE *fp = fopen(file.txt, "rb");

// 1. 将指针移动到文件末尾
if (fseek(fp, 0, SEEK_END) != 0) {
perror("无法读到文件末尾");
fclose(fp); return 1;
}

// 2. 获取当前位置,即文件大小
long file_size = ftell(fp);
if (file_size == -1L) {
perror("ftell");
fclose(fp);
return 1;
}
printf("文件: '%s' 大小: %ld bytes\n", file.txt, file_size);
rewind(fp);// 3. 将指针移回文件开头,以便后续读取

fclose(fp);

缓冲区

stdout默认行缓冲stderr默认无缓冲,普通文件默认全缓冲

缓冲区刷新的6大时机:

  1. 缓冲区已满
  2. 遇到换行符 \n (仅限行缓冲)
  3. 程序正常退出 (return, exit())。abort()等异常退出不会刷新
  4. 手动调用 fflush(fp)
  5. 关闭文件 fclose(fp)
  6. 在一个流上执行任何输入操作前,会先刷新该流的输出缓冲区

setvbuf(fp, buf, mode, size): 自定义缓冲区刷新策略

  • fp: 文件指针
  • buf: 缓冲区地址
  • mode: _IOFBF(全), _IOLBF(行), _IONBF(无)。
  • size: 缓冲区大小
1
2
3
4
5
6
7
8
9
10
fprintf(stdout, "abcd");  // 标准输出模式,默认放入缓冲区
fprintf(stderr, "1234"); // 标准错误模式,默认直接输出

FILE *fp = fopen("test.txt", "w+"); // 默认先放进缓冲区,所以不会立即放入文件

fprintf(fp, "1234"); // 默认放入缓冲区
char buf[100];
setvbuf(fp, buf, _IOLBF, 100); // 设置缓冲区,_IOLBF表示行缓冲
fprintf(fp, "abcd\n"); // 修改为了行缓冲,且数据有换行符,立即放入文件
abort(); // 此时虽然程序也被强制中断,但数据已经保存到文件中,数据不丢失

第二部分:系统I/O <fcntl.h>

系统I/O直接与操作系统内核交互,操作的是一个整数——文件描述符 (File Descrip, fd)

它是访问文件的底层句柄,更加强大和灵活,是理解操作系统工作原理的基石

  • 0: 标准输入 (stdin) - STDIN_FILENO
  • 1: 标准输出 (stdout) - STDOUT_FILENO
  • 2: 标准错误 (stderr) - STDERR_FILENO

错误处理机制: errno, perror, strerror

包含在errno.h

当系统调用失败时,通常会返回 -1,并设置一个全局整型变量 errno 来表示具体的错误原因。

errno 的值只在系统调用或库函数发生错误时才会被设置,因此只有在函数返回错误时检查errno才有意义

perror(const char *s): 先打印自己的字符串 s,然后输出errno`对应的错误描述

char *strerror(int errnum): 返回错误码对应字符串的指针,可以自定义输出格式

1
2
3
4
5
6
7
8
9
10
11
12
int fd = open("non_existent_file.txt", O_RDONLY);
if (fd == -1) {
// 方法一:使用 perror (推荐)
// 输出: 打开文件失败: No such file or directory
perror("打开文件失败");

// 方法二:使用 strerror (更灵活)
// 输出: Error opening file: No such file or directory
printf("打开文件失败: %s\n", strerror(errno));

return 1;
}

文件的打开与关闭 open,close

包含在fcntl.h

int open(const char *pathname, int flags, ...);: 打开或创建一个文件。

  • pathname: 文件路径。
  • flags: 通过|组合的标志位,决定操作模式。
  • ...: 可变参数,当flags包含O_CREAT时,必须提供第三个参数mode_t mode来指定新文件的权限(如0644)。

int close(int fd);: 关闭文件描述符,释放相关内核资源。一个进程能打开的文件描述符数量是有限的,务必及时关闭不再使用的fd

flags 标志位详解

flags 参数通过按位或 | 操作符组合,用于精确控制文件的打开方式。

标志 (Flag) 英文全称 / 释义 描述
访问模式 以下三个标志是互斥的,必须选择其一
O_RDONLY Open Read Only 以只读模式打开文件。
O_WRONLY Open Write Only 以只写模式打开文件。
O_RDWR Open Read/Write 以读写模式打开文件。
可选标志 **以下标志可通过 `
O_CREAT Open Create 如果文件不存在,则创建它。使用此标志时必须提供第三个mode参数。
O_TRUNC Open Truncate 如果文件已存在并且是可写模式打开,则将其长度截断为0(即清空内容)。
O_APPEND Open Append 每次写入数据时,都自动将文件指针移动到文件末尾进行追加操作。
O_NONBLOCK Open Non-blocking 非阻塞模式打开文件。主要用于设备文件和管道,对它们的读写操作不会被阻塞。
O_EXCL Open Exclusive O_CREAT 配合使用。如果文件已存在,open调用会失败并返回错误。这是实现“文件锁”或确保文件唯一性的原子操作。
1
2
3
4
5
6
7
8
9
10
11
// 场景1: 读写方式打开,不存在则创建,权限为 rw-r--r--
int fd1 = open("log.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd1 == -1) { perror("open log.txt failed"); return 1; }
printf("文件已创建: %d\n", fd1);
close(fd1);

// 场景2: 安全地创建文件,如果文件已存在则失败
int fd2 = open("lock.file", O_WRONLY | O_CREAT | O_EXCL, 0600);
if (fd2 == -1) { perror("无法创建文件,可能文件已存在"); return 1; }
printf("文件已创建: %d\n", fd2);
close(fd2);

文件内容读、写与定位

3.1 核心函数: read / write / lseek

存在于unistd.h

ssize_t read(int fd, void *buf, size_t count);:

  • fd读取最多count字节到buf。返回实际读取的字节数,0表示文件结束,-1表示错误。

ssize_t write(int fd, const void *buf, size_t count);:

  • buf中的count字节写入fd。返回实际写入的字节数,可能小于count,需循环写入。

off_t lseek(int fd, off_t offset, int whence);:

  • 移动文件读写指针。
  • whence 可以是SEEK_SET(文件头), SEEK_CUR(当前位置), SEEK_END(文件尾)。

3.2 实践案例: 获取文件大小

1
2
3
4
5
6
7
int fd = open("file.txt", O_RDONLY);

// 将指针移动到文件末尾,lseek返回值即为文件大小
off_t file_size = lseek(fd, 0, SEEK_END);
printf("文件'%s' 大小为 %ld bytes.\n", argv[1], file_size);

close(fd);

3.3 实践案例: 创建文件空洞

1
2
3
4
5
6
7
int fd = open("file.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
write(fd, "start", 5);
// 从当前位置向后移动1MB,形成空洞
lseek(fd, 1024 * 1024, SEEK_CUR);
write(fd, "end", 3);
close(fd);
// 使用 `ls -lh` 和 `du -h` 命令查看文件大小和实际占用空间的区别

高级文件描述符操作

4.1 I/O重定向: dup / dup2

存在于unistd.h

int dup(int oldfd);: 复制oldfd,返回一个新的、当前未被使用的最小文件描述符

int dup2(int oldfd, int newfd);: 将newfd重定向到oldfd。如果newfd已打开,会先关闭它

这是实现Shell重定向(如 ><)的关键。

在shell脚本中,0是标准输入(键盘),1是标准输出(终端),2是错误输出

用dup2(fd,0);就吧标准输入改为fd里,类似于shell里的

1
2
3
4
5
6
7
8
9
int log_fd = open("file.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);

// 将标准输出(fd=1)重定向到log_fd
if (dup2(log_fd, STDOUT_FILENO) == -1) { perror("dup2"); return 1; }

close(log_fd); // 原log_fd可关闭,fd=1已指向日志文件

printf("这句话会输出到文件里.\n");
fprintf(stderr, "这句话将直接输出到终端\n");

4.2 文件状态控制: fcntl

存在于fcntl.h

int fcntl(int fd, int cmd, ...);: fcntl (file control) 是一个多功能工具箱,能对文件描述符执行多种控制操作

cmd决定了fcntl的功能,常用的有F_GETFL (获取状态) 和 F_SETFL (设置状态)

具体的参数详解可以查看此博客文章:Linux fcntl函数详解

fp (设置非阻塞I/O):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int fd = STDIN_FILENO; // 用标准输入做演示

// 1. 获取当前状态标志
long flags = fcntl(fd, F_GETFL);
if (flags == -1) { perror("获取标准输入状态失败"); return 1; }

// 2. 添加非阻塞标志
flags |= O_NONBLOCK;

// 3. 应用新标志
if (fcntl(fd, F_SETFL, flags) == -1) { perror("设置标准输入状态失败"); return 1; }

char buf[10];
ssize_t n = read(fd, buf, sizeof(buf));
if (n < 0) {
// 非阻塞read在无数据时立即返回-1
// 因为理论上应该从用户输入读取数据,但用户没输入就直接开始读取数据,所以这里出错
perror("在以非阻塞模式读取");
} else {
printf("读取了 %ld bytes.\n", n);
}

特殊文件I/O

5.1 设备I/O控制: ioctl

ioctl (input/output control) 是一个专用于设备I/O的“后门”,允许应用程序直接与设备驱动程序通信。

通过这个接口,可以实现在linux应用层控制内核层下的驱动,进而控制具体的外设

  • 应用程序通过代码片段与设备节点交互,来操作具体硬件如 LED、读取 ADC 和控制 PWM
  • 设备节点作为接口连接应用和驱动程序,完成用户空间与内核空间的通信,每个节点通过系统调用(如 open())、read()、ioctl())与应用程序交互
  • 驱动程序封装具体的硬件操作逻辑,由设备节点调用,每个驱动程序专注于特定功能模块

linux系统通过这样的分层封装实现功能分离,硬件工程师负责底层设备操作,应用软件工程师只需使用抽象化设备,无需了解底层原理

ioctl的行为完全由request命令码和设备驱动的实现决定,具体应用可以查看linux编程笔记

linux驱动基础概念以及驱动程序框架搭建

img

5.2 内存映射I/O: mmap

存在于sys/mman.h

*mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);: 将文件或设备直接映射到进程的虚拟内存空间

之后对这块内存的读写就等同于对文件的读写,省去了read/write的系统调用开销和内核/用户态数据拷贝,效率极高

prot: 内存保护标志 (PROT_READ, PROT_WRITE)。

flags: MAP_SHARED(修改会同步到文件)或 MAP_PRIVATE(写时复制,修改不影响文件)。

munmap(void *addr, size_t length) : Memory UNMAP,解除内存映射。

内存映射文件:

1
2
3
4
5
6
7
int fd = open("mmap_test.txt", O_RDWR | O_CREAT, 0644);
ftruncate(fd, 100); // 确保文件至少有100字节
char *mapped_mem = mmap(NULL, 100, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mapped_mem == MAP_FAILED) { perror("内存映射失败"); close(fd); return 1; }
close(fd); // mmap后可以立即关闭fd,映射关系依然存在
strcpy(mapped_mem, "你好内存映射,像操作内存一样操作文件,复制这段话到文件");
munmap(mapped_mem, 100); // 操作完成,解除映射

第三部分:文件与目录管理

在linux系统编程中,除了对文件内容进行读写,我们还经常需要获取和管理文件本身的属性(如大小、权限、类型)以及组织这些文件的目录结构。

这部分操作主要依赖于POSIX标准提供的系统调用。

1. 获取文件元数据: stat / fstat / lstat

这组函数用于获取文件的元数据,并将其填充到一个struct stat结构体中

int stat(const char *pathname, struct stat *statbuf);: 通过文件名获取属性

int fstat(int fd, struct stat *statbuf);: 通过文件描述符获取属性

int lstat(const char *pathname, struct stat *statbuf);:

  • 类似stat,但如果pathname是一个符号链接 lstat获取的是链接本身的属性,而不是它所指向的文件的属性。
  • stat则会“穿透”链接,获取目标文件的属性。

1.0 struct stat 结构体

不用记这个结构体,了解大概有什么就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct stat {
dev_t st_dev; /* 存放该文件的设备 ID(device ID) */
ino_t st_ino; /* inode 编号(Inode number) */
mode_t st_mode; /* 文件类型和权限模式(file type & mode/permissions) */
nlink_t st_nlink; /* 硬链接数量(number of hard links) */
uid_t st_uid; /* 文件所有者的用户 ID(user ID of owner) */
gid_t st_gid; /* 文件所有者的组 ID(group ID of owner) */
dev_t st_rdev; /* 设备 ID(device ID,特殊文件才有) */
off_t st_size; /* 文件总大小(total size, in bytes) */
blksize_t st_blksize; /* 文件系统 I/O 的建议块大小(block size for filesystem I/O) */
blkcnt_t st_blocks; /* 实际分配的 512 字节块数量(number of 512B blocks allocated) */
struct timespec st_atim; /* 最后一次访问时间(time of last access) */
struct timespec st_mtim; /* 最后一次修改时间(time of last modification) */
struct timespec st_ctim; /* 最后一次状态变化时间(time of last status change) */
};

这些字段中要注意的:

  • st_mode: 一个16位的字段,包含了文件类型和权限信息。需要使用宏来解析。
  • st_rdev: 仅当文件是字符设备或块设备时才有意义,表示该设备的设备号。
  • st_size: 对于普通文件,是文件的字节大小。对于设备文件,此值无意义。

1.1 获取文件信息

不用管前面的那么多参数,只需要下面三个步骤就可以获取上面的信息

具体需要获取什么信息 就用man手册查一下结构体内容就行

1
2
3
struct stat st;//申请一个空间存文件信息
bzero(&st,sizeof(st));//清空文件信息表
stat("a.txt", &st);//读取文件信息

1.2 实践案例: 实现一个功能完整的 ls -l

这个案例是stat函数最经典的用例,完美展示了如何解析struct stat中的各种信息。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <grp.h>  // 用于 获取组信息 get by grope id
#include <pwd.h> // 用于 获取用户信息 get password file entry by user ID
#include <stdio.h>
#include <sys/stat.h>
#include <sys/sysmacros.h> // 用于 major 和 minor 宏
#include <time.h>
#include <unistd.h>
#include <fcntl.h>

// 辅助函数:解析文件类型
char get_file_type(mode_t mode) {
if (S_ISREG(mode)) return '-';
if (S_ISDIR(mode)) return 'd';
if (S_ISLNK(mode)) return 'l';
if (S_ISCHR(mode)) return 'c';
if (S_ISBLK(mode)) return 'b';
if (S_ISFIFO(mode)) return 'p';
if (S_ISSOCK(mode)) return 's';
return '?';
}

// 辅助函数:解析权限
void get_permissions(mode_t mode, char *str) {
str[0] = (mode & S_IRUSR) ? 'r' : '-';
str[1] = (mode & S_IWUSR) ? 'w' : '-';
str[2] = (mode & S_IXUSR) ? 'x' : '-';
str[3] = (mode & S_IRGRP) ? 'r' : '-';
str[4] = (mode & S_IWGRP) ? 'w' : '-';
str[5] = (mode & S_IXGRP) ? 'x' : '-';
str[6] = (mode & S_IROTH) ? 'r' : '-';
str[7] = (mode & S_IWOTH) ? 'w' : '-';
str[8] = (mode & S_IXOTH) ? 'x' : '-';
str[9] = '\0';
}

int main(int argc, char const *argv[]) {
if (argc != 2) {
fprintf(stderr, "使用方法: %s <文件或路径参数>\n", argv[0]);
return 1;
}

struct stat st;
if (lstat(argv[1], &st) == -1) {
perror("lstat failed");
return 1;
}

// 1. 打印文件类型和权限
char perms[10];
get_permissions(st.st_mode, perms);
printf("%c%s ", get_file_type(st.st_mode), perms);

// 2. 打印硬链接数
printf("%ld ", st.st_nlink);

// 3. 打印所有者和所属组
printf("%s %s ", getpwuid(st.st_uid)->pw_name, getgrgid(st.st_gid)->gr_name);

// 4. 关键:区分普通文件和设备文件
if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode)) {
// 如果是设备文件,打印主、次设备号
printf("%u, %-4u ", major(st.st_rdev), minor(st.st_rdev));
} else {
// 如果是普通文件,打印大小
printf("%8ld ", st.st_size);
}

// 5. 打印最后修改时间
char time_buf[20];
strftime(time_buf, sizeof(time_buf), "%b %d %H:%M", localtime(&st.st_mtime));
printf("%s ", time_buf);

// 6. 打印文件名
printf("%s\n", argv[1]);

return 0;
}

2. 目录操作

2.1 核心函数: opendir / readdir / closedir / chdir

核心知识点:

  • DIR *opendir(const char *name);: 打开一个目录,返回一个指向DIR结构体的目录流指针。
  • struct dirent *readdir(DIR *dirp);: 从目录流中读取下一个目录项。当读取完毕或发生错误时,返回NULLstruct dirent中最重要的成员是d_name(文件名)。
  • int closedir(DIR *dirp);: 关闭目录流,释放资源。
  • int chdir(const char *path);: 改变当前进程的工作目录 (Change Directory)。这是一个非常有用的函数,在遍历目录时,可以先chdir到目标目录,然后直接对目录内的文件名使用stat,避免了手动拼接完整路径的麻烦。

2.2 实践案例: 遍历目录并获取文件信息

这个案例展示了如何结合目录遍历和文件属性获取,实现一个简单的目录列表程序。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char const *argv[]) {
const char *dirpath = (argc > 1) ? argv[1] : ".";

DIR *dp = opendir(dirpath);
if (dp == NULL) {
perror("opendir failed");
return 1;
}

// *** 关键技巧:改变当前工作目录 ***
if (chdir(dirpath) == -1) {
perror("chdir failed");
closedir(dp);
return 1;
}

printf("Content of directory '%s':\n", dirpath);
struct dirent *entry;
struct stat st;

// 循环读取目录中的每一个条目
while ((entry = readdir(dp)) != NULL) {
// 跳过隐藏文件 "." 和 ".."
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}

// 因为已经chdir,所以可以直接对文件名stat
if (lstat(entry->d_name, &st) == -1) {
perror(entry->d_name); // 如果stat失败,打印文件名和错误
continue;
}

printf("%-20s \t Size: %ld bytes\n", entry->d_name, st.st_size);
}

closedir(dp);

// 好的编程习惯是返回到原来的目录,但这非必需
// chdir("..");

return 0;
}

3. 目录的创建与删除: mkdir / rmdir

核心知识点:

  • int mkdir(const char *pathname, mode_t mode);: 创建一个新目录。mode参数指定目录的权限,但会受到umask的影响。
  • int rmdir(const char *pathname);: 删除一个目录。如果目录非空,rmdir会失败。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <sys/stat.h>
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

int main() {
const char *dir_name = "my_new_dir";

// 创建目录,权限为 rwxr-xr-x
if (mkdir(dir_name, 0755) == -1) {
perror("mkdir failed");
return 1;
}
printf("目录 '%s' 创建成功\n", dir_name);

// ... 可以在这里做一些操作 ...

// 删除目录
if (rmdir(dir_name) == -1) {
perror("删除失败");
return 1;
}
printf("文件夹 '%s' 删除成功\n", dir_name);

return 0;
}